[ML101] Chương 6: Học kết hợp và Rừng ngẫu nhiên
Ensemble Learning, Random Forests và các phương pháp boosting
Bài viết có tham khảo, sử dụng và sửa đổi tài nguyên từ kho lưu trữ handson-mlp, tuân thủ giấy phép Apache‑2.0. Chúng tôi chân thành cảm ơn tác giả Aurélien Géron (@aureliengeron) vì sự chia sẻ kiến thức tuyệt vời và những đóng góp quý giá cho cộng đồng.
Chào mừng bạn đến với Chương 6. Trong chương này, chúng ta sẽ khám phá một trong những kỹ thuật mạnh mẽ nhất trong Học máy (Machine Learning): Học kết hợp (Ensemble Learning).
Ý tưởng cốt lõi của Học kết hợp rất đơn giản nhưng hiệu quả: thay vì phụ thuộc vào một mô hình duy nhất (ví dụ: một Cây quyết định), chúng ta tập hợp một nhóm các mô hình dự đoán lại với nhau. Giống như việc tham khảo ý kiến của một hội đồng chuyên gia thường tốt hơn là tin vào một người duy nhất, một nhóm các mô hình (được gọi là một ensemble) thường sẽ đưa ra dự đoán chính xác hơn so với mô hình tốt nhất trong nhóm đó.
Chúng ta cũng sẽ tìm hiểu về Rừng ngẫu nhiên (Random Forests), một thuật toán Học kết hợp phổ biến dựa trên Cây quyết định, cũng như các kỹ thuật Boosting (Tăng cường) và Stacking (Xếp chồng).
Bạn có thể chạy trực tiếp các đoạn mã code tại: Google Colab.
Cài đặt môi trường
Trước khi đi vào chi tiết, như thường lệ, chúng ta cần đảm bảo môi trường Python đáp ứng các yêu cầu về phiên bản. Dự án này yêu cầu Python 3.10 trở lên và Scikit-Learn phiên bản 1.6.1 trở lên.
# Kiểm tra phiên bản Python
import sys
assert sys.version_info >= (3, 10)
# Kiểm tra phiên bản Scikit-Learn
from packaging.version import Version
import sklearn
assert Version(sklearn.__version__) >= Version("1.6.1")
Như thường lệ, chúng ta sẽ thiết lập kích thước phông chữ mặc định cho thư viện matplotlib để các biểu đồ hiển thị rõ ràng và đẹp mắt hơn.
import matplotlib.pyplot as plt
# Thiết lập kích thước phông chữ và các thành phần biểu đồ
plt.rc('font', size=14)
plt.rc('axes', labelsize=14, titlesize=14)
plt.rc('legend', fontsize=14)
plt.rc('xtick', labelsize=10)
plt.rc('ytick', labelsize=10)
1. Bộ phân loại biểu quyết (Voting Classifiers)
Hãy bắt đầu với hình thức học kết hợp đơn giản nhất: Biểu quyết (Voting). Giả sử bạn có một đồng xu hơi lệch một chút, với xác suất ra mặt ngửa là 51% và mặt sấp là 49%. Nếu bạn tung nó 1.000 lần, theo Luật số lớn (Law of Large Numbers), tỷ lệ mặt ngửa sẽ tiệm cận 51%. Điều này tương tự như việc tập hợp nhiều bộ phân loại yếu (weak learners) để tạo thành một bộ phân loại mạnh (strong learner).
Đoạn mã dưới đây mô phỏng việc tung đồng xu 10.000 lần cho 10 thí nghiệm khác nhau để minh họa sự hội tụ của xác suất.
# extra code – ô này tạo ra Hình 6–3 minh họa Luật số lớn
import matplotlib.pyplot as plt
import numpy as np
heads_proba = 0.51 # Xác suất mặt ngửa là 51%
rng = np.random.default_rng(seed=42)
# Mô phỏng 10.000 lần tung đồng xu cho 10 thí nghiệm
coin_tosses = (rng.random((10000, 10)) < heads_proba).astype(np.int32)
# Tính tổng tích lũy số lần mặt ngửa
cumulative_heads = coin_tosses.cumsum(axis=0)
# Tính tỷ lệ mặt ngửa tích lũy
cumulative_heads_ratio = cumulative_heads / np.arange(1, 10001).reshape(-1, 1)
# Vẽ biểu đồ
plt.figure(figsize=(8, 3.5))
plt.plot(cumulative_heads_ratio)
plt.plot([0, 10000], [0.51, 0.51], "k--", linewidth=2, label="51%")
plt.plot([0, 10000], [0.5, 0.5], "k-", label="50%")
plt.xlabel("Number of coin tosses")
plt.ylabel("Heads ratio")
plt.legend(loc="lower right")
plt.axis([0, 10000, 0.42, 0.58])
plt.grid()
plt.show()

Tiếp theo, chúng ta sẽ xây dựng một bộ phân loại biểu quyết thực sự bằng cách sử dụng tập dữ liệu moons. Chúng ta sẽ kết hợp ba mô hình khác nhau: Hồi quy Logistic (Logistic Regression), Rừng ngẫu nhiên (Random Forest) và Máy vector hỗ trợ (SVM).
Trong Hard Voting (Biểu quyết cứng), mỗi bộ phân loại sẽ đưa ra một dự đoán, và lớp nào nhận được nhiều phiếu nhất sẽ là kết quả cuối cùng.
from sklearn.datasets import make_moons
from sklearn.ensemble import RandomForestClassifier, VotingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.svm import SVC
# Tạo dữ liệu moons
X, y = make_moons(n_samples=500, noise=0.30, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
# Khởi tạo bộ phân loại biểu quyết với 3 mô hình con
voting_clf = VotingClassifier(
estimators=[
('lr', LogisticRegression(random_state=42)),
('rf', RandomForestClassifier(random_state=42)),
('svc', SVC(random_state=42))
])
# Huấn luyện mô hình
voting_clf.fit(X_train, y_train)
VotingClassifier(estimators=[('lr', LogisticRegression(random_state=42)),
('rf', RandomForestClassifier(random_state=42)),
('svc', SVC(random_state=42))])VotingClassifier(estimators=[('lr', LogisticRegression(random_state=42)),
('rf', RandomForestClassifier(random_state=42)),
('svc', SVC(random_state=42))])LogisticRegression(random_state=42)
RandomForestClassifier(random_state=42)
SVC(random_state=42)
Sau khi huấn luyện, chúng ta hãy xem độ chính xác của từng mô hình riêng lẻ trên tập kiểm tra (test set).
for name, clf in voting_clf.named_estimators_.items():
print(name, "=", clf.score(X_test, y_test))
lr = 0.864
rf = 0.896
svc = 0.896
Bây giờ, hãy xem dự đoán của mô hình biểu quyết cho mẫu đầu tiên trong tập kiểm tra.
voting_clf.predict(X_test[:1])
array([1])
Để hiểu rõ hơn, chúng ta có thể xem dự đoán riêng lẻ của từng mô hình thành viên:
[clf.predict(X_test[:1]) for clf in voting_clf.estimators_]
[array([1]), array([1]), array([0])]
Cuối cùng, hãy kiểm tra độ chính xác tổng thể của bộ phân loại biểu quyết. Thông thường, nó sẽ cao hơn độ chính xác của các mô hình thành phần.
voting_clf.score(X_test, y_test)
0.912
Soft Voting (Biểu quyết mềm)
Nếu tất cả các bộ phân loại đều có thể ước tính xác suất của từng lớp (có phương thức predict_proba()), chúng ta có thể sử dụng Soft Voting. Trong phương pháp này, Scikit-Learn sẽ tính trung bình xác suất của từng lớp trên tất cả các bộ phân loại và chọn lớp có xác suất trung bình cao nhất. Soft Voting thường cho kết quả tốt hơn Hard Voting vì nó cân nhắc đến mức độ tự tin của từng mô hình.
Lưu ý: Mô hình SVC mặc định không tính xác suất, nên ta phải đặt probability=True.
# Chuyển sang chế độ soft voting
voting_clf.voting = "soft"
# Cấu hình lại SVC để hỗ trợ tính xác suất
voting_clf.named_estimators["svc"].probability = True
# Huấn luyện lại và đánh giá
voting_clf.fit(X_train, y_train)
voting_clf.score(X_test, y_test)
0.92
2. Bagging và Pasting
Thay vì sử dụng các thuật toán khác nhau, một cách tiếp cận khác là sử dụng cùng một thuật toán nhưng huấn luyện trên các tập con ngẫu nhiên khác nhau của dữ liệu huấn luyện.
- Bagging (Bootstrap Aggregating): Lấy mẫu có hoàn lại (cùng một mẫu có thể xuất hiện nhiều lần trong một tập con).
- Pasting: Lấy mẫu không hoàn lại.
Cả hai phương pháp đều giúp giảm phương sai (variance) của mô hình, giúp tránh overfitting.
Dưới đây, chúng ta sử dụng BaggingClassifier của Scikit-Learn với 500 cây quyết định. Mỗi cây được huấn luyện trên 100 mẫu được chọn ngẫu nhiên từ tập huấn luyện (có hoàn lại).
from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier
# Khởi tạo BaggingClassifier với 500 cây quyết định
bag_clf = BaggingClassifier(DecisionTreeClassifier(), n_estimators=500,
max_samples=100, n_jobs=-1, random_state=42)
bag_clf.fit(X_train, y_train)
BaggingClassifier(estimator=DecisionTreeClassifier(), max_samples=100,
n_estimators=500, n_jobs=-1, random_state=42)BaggingClassifier(estimator=DecisionTreeClassifier(), max_samples=100,
n_estimators=500, n_jobs=-1, random_state=42)DecisionTreeClassifier()
DecisionTreeClassifier()
Để minh họa hiệu quả của Bagging, chúng ta sẽ so sánh ranh giới quyết định (decision boundary) của một Cây quyết định đơn lẻ với một tập hợp Bagging gồm 500 cây. Bạn sẽ thấy ranh giới quyết định của Bagging mượt mà hơn và tổng quát hóa tốt hơn.
# extra code – ô này tạo ra Hình 6–5 so sánh Decision Tree và Bagging
def plot_decision_boundary(clf, X, y, alpha=1.0):
axes=[-1.5, 2.4, -1, 1.5]
x1, x2 = np.meshgrid(np.linspace(axes[0], axes[1], 100),
np.linspace(axes[2], axes[3], 100))
X_new = np.c_[x1.ravel(), x2.ravel()]
y_pred = clf.predict(X_new).reshape(x1.shape)
plt.contourf(x1, x2, y_pred, alpha=0.3 * alpha, cmap='Wistia')
plt.contour(x1, x2, y_pred, cmap="Greys", alpha=0.8 * alpha)
colors = ["#78785c", "#c47b27"]
markers = ("o", "^")
for idx in (0, 1):
plt.plot(X[:, 0][y == idx], X[:, 1][y == idx],
color=colors[idx], marker=markers[idx], linestyle="none")
plt.axis(axes)
plt.xlabel(r"$x_1$")
plt.ylabel(r"$x_2$", rotation=0)
tree_clf = DecisionTreeClassifier(random_state=42)
tree_clf.fit(X_train, y_train)
fig, axes = plt.subplots(ncols=2, figsize=(10, 4), sharey=True)
plt.sca(axes[0])
plot_decision_boundary(tree_clf, X_train, y_train)
plt.title("Decision Tree")
plt.sca(axes[1])
plot_decision_boundary(bag_clf, X_train, y_train)
plt.title("Decision Trees with Bagging")
plt.ylabel("")
plt.show()

Đánh giá OOB (Out-of-Bag Evaluation)
Trong Bagging, với mỗi bộ phân loại, một số mẫu dữ liệu có thể được chọn nhiều lần, trong khi một số khác không được chọn lần nào. Những mẫu không được chọn gọi là Out-of-Bag (OOB). Vì mô hình chưa bao giờ nhìn thấy các mẫu OOB này trong quá trình huấn luyện, chúng ta có thể sử dụng chúng như một tập kiểm tra (validation set) để đánh giá hiệu năng mà không cần tách riêng một tập dữ liệu kiểm tra.
# Bật tính năng oob_score=True
bag_clf = BaggingClassifier(DecisionTreeClassifier(), n_estimators=500,
oob_score=True, n_jobs=-1, random_state=42)
bag_clf.fit(X_train, y_train)
# Xem điểm số OOB
bag_clf.oob_score_
0.896
Chúng ta cũng có thể xem xác suất dự đoán của các mẫu OOB:
bag_clf.oob_decision_function_[:3] # Xác suất cho 3 mẫu đầu tiên
array([[0.32352941, 0.67647059],
[0.3375 , 0.6625 ],
[1. , 0. ]])
So sánh điểm OOB với độ chính xác thực tế trên tập kiểm tra:
from sklearn.metrics import accuracy_score
y_pred = bag_clf.predict(X_test)
accuracy_score(y_test, y_pred)
0.92
Lý thuyết xác suất đằng sau OOB
Nếu bạn lấy ngẫu nhiên một mẫu từ tập dữ liệu kích thước , xác suất để một mẫu cụ thể không được chọn là . Nếu lặp lại lần, xác suất để mẫu đó không bao giờ được chọn là . Khi tiến tới vô cùng, giá trị này tiệm cận về . Nghĩa là khoảng 37% số mẫu sẽ không được sử dụng để huấn luyện (OOB), và 63% sẽ được sử dụng.
# extra code – tính toán xác suất 63%
print(1 - (1 - 1 / 1000) ** 1000)
print(1 - np.exp(-1))
0.6323045752290363
0.6321205588285577
3. Rừng ngẫu nhiên (Random Forests)
Rừng ngẫu nhiên (Random Forest) về cơ bản là một tập hợp các Cây quyết định, thường được huấn luyện bằng phương pháp Bagging. Tuy nhiên, nó thêm một lớp ngẫu nhiên nữa: khi tách một nút trong cây, thay vì tìm đặc trưng tốt nhất trong tất cả các đặc trưng, nó chỉ tìm trong một tập con ngẫu nhiên các đặc trưng. Điều này làm tăng tính đa dạng của các cây và giúp giảm overfitting.
from sklearn.ensemble import RandomForestClassifier
# Khởi tạo Random Forest với 500 cây
rnd_clf = RandomForestClassifier(n_estimators=500, max_leaf_nodes=16,
n_jobs=-1, random_state=42)
rnd_clf.fit(X_train, y_train)
y_pred_rf = rnd_clf.predict(X_test)
Để chứng minh rằng Random Forest thực chất là Bagging của các Decision Tree với các tham số cụ thể, chúng ta có thể cấu hình BaggingClassifier để hoạt động giống hệt RandomForestClassifier.
# Một BaggingClassifier tương đương với RandomForest
bag_clf = BaggingClassifier(
DecisionTreeClassifier(max_features="sqrt", max_leaf_nodes=16),
n_estimators=500, n_jobs=-1, random_state=42)
# extra code – kiểm chứng dự đoán giống nhau
bag_clf.fit(X_train, y_train)
y_pred_bag = bag_clf.predict(X_test)
np.all(y_pred_bag == y_pred_rf) # Kiểm tra xem kết quả có giống hệt nhau không
np.True_
Tầm quan trọng của đặc trưng (Feature Importance)
Một tính năng tuyệt vời của Random Forest là khả năng đo lường tầm quan trọng của từng đặc trưng (feature). Scikit-Learn đo lường điều này bằng cách xem xét mức độ giảm tạp chất (impurity) trung bình mà mỗi đặc trưng đóng góp trên tất cả các cây trong rừng.
Dưới đây là ví dụ trên bộ dữ liệu hoa Iris:
from sklearn.datasets import load_iris
iris = load_iris(as_frame=True)
rnd_clf = RandomForestClassifier(n_estimators=500, random_state=42)
rnd_clf.fit(iris.data, iris.target)
for score, name in zip(rnd_clf.feature_importances_, iris.data.columns):
print(round(score, 2), name)
0.11 sepal length (cm)
0.02 sepal width (cm)
0.44 petal length (cm)
0.42 petal width (cm)
Chúng ta cũng có thể áp dụng kỹ thuật này trên bộ dữ liệu MNIST để xem phần nào của hình ảnh (pixel nào) quan trọng nhất để phân loại các chữ số.
# extra code – ô này tạo ra Hình 6–6: Bản đồ nhiệt (heatmap) tầm quan trọng của pixel trên MNIST
from sklearn.datasets import fetch_openml
X_mnist, y_mnist = fetch_openml('mnist_784', return_X_y=True, as_frame=False,
parser='auto')
rnd_clf = RandomForestClassifier(n_estimators=100, random_state=42)
rnd_clf.fit(X_mnist, y_mnist)
heatmap_image = rnd_clf.feature_importances_.reshape(28, 28)
plt.imshow(heatmap_image, cmap="hot")
cbar = plt.colorbar(ticks=[rnd_clf.feature_importances_.min(),
rnd_clf.feature_importances_.max()])
cbar.ax.set_yticklabels(['Not important', 'Very important'], fontsize=14)
plt.axis("off")
plt.show()

4. Boosting (Tăng cường)
Boosting là một kỹ thuật học kết hợp kết nối nhiều bộ phân loại yếu thành một bộ phân loại mạnh bằng cách huấn luyện các mô hình một cách tuần tự (sequential), trong đó mỗi mô hình cố gắng sửa chữa sai sót của mô hình trước đó.
AdaBoost (Adaptive Boosting)
Trong AdaBoost, mô hình mới sẽ tập trung nhiều hơn vào các mẫu dữ liệu mà mô hình trước đó đã phân loại sai (bằng cách gán trọng số cao hơn cho các mẫu này). Kết quả là các bộ phân loại tiếp theo sẽ tập trung vào các ca khó.
Đoạn mã dưới đây minh họa quá trình cập nhật trọng số và ranh giới quyết định của SVM qua 5 lần lặp.
# extra code – ô này tạo ra Hình 6–8 minh họa AdaBoost
m = len(X_train) # số lượng mẫu
fig, axes = plt.subplots(ncols=2, figsize=(10, 4), sharey=True)
for subplot, learning_rate in ((0, 1), (1, 0.5)):
sample_weights = np.ones(m) / m
plt.sca(axes[subplot])
for i in range(5):
svm_clf = SVC(C=0.2, gamma=0.6, random_state=42)
svm_clf.fit(X_train, y_train, sample_weight=sample_weights * m)
y_pred = svm_clf.predict(X_train)
error_weights = sample_weights[y_pred != y_train].sum()
r = error_weights / sample_weights.sum() # phương trình 7-1
alpha = learning_rate * np.log((1 - r) / r) # phương trình 7-2
sample_weights[y_pred != y_train] *= np.exp(alpha) # phương trình 7-3
sample_weights /= sample_weights.sum() # bước chuẩn hóa
plot_decision_boundary(svm_clf, X_train, y_train, alpha=0.4)
plt.title(f"learning_rate = {learning_rate}")
if subplot == 0:
plt.text(-0.75, -0.95, "1", fontsize=16)
plt.text(-1.05, -0.95, "2", fontsize=16)
plt.text(1.0, -0.95, "3", fontsize=16)
plt.text(-1.45, -0.5, "4", fontsize=16)
plt.text(1.36, -0.95, "5", fontsize=16)
else:
plt.ylabel("")
plt.show()

Sử dụng AdaBoostClassifier trong Scikit-Learn với mô hình cơ sở là Decision Stump (cây quyết định độ sâu 1).
from sklearn.ensemble import AdaBoostClassifier
# Khởi tạo AdaBoost với 30 cây quyết định độ sâu 1
ada_clf = AdaBoostClassifier(
DecisionTreeClassifier(max_depth=1), n_estimators=30,
learning_rate=0.5, random_state=42, algorithm="SAMME")
ada_clf.fit(X_train, y_train)
/usr/local/lib/python3.12/dist-packages/sklearn/ensemble/_weight_boosting.py:519: FutureWarning: The parameter 'algorithm' is deprecated in 1.6 and has no effect. It will be removed in version 1.8.
warnings.warn(
AdaBoostClassifier(algorithm='SAMME',
estimator=DecisionTreeClassifier(max_depth=1),
learning_rate=0.5, n_estimators=30, random_state=42)AdaBoostClassifier(algorithm='SAMME',
estimator=DecisionTreeClassifier(max_depth=1),
learning_rate=0.5, n_estimators=30, random_state=42)DecisionTreeClassifier(max_depth=1)
DecisionTreeClassifier(max_depth=1)
# extra code – vẽ ranh giới quyết định của AdaBoost
plot_decision_boundary(ada_clf, X_train, y_train)

Gradient Boosting
Gradient Boosting cũng hoạt động bằng cách thêm các mô hình vào tập hợp một cách tuần tự. Nhưng thay vì thay đổi trọng số của các mẫu dữ liệu như AdaBoost, phương pháp này cố gắng huấn luyện mô hình mới dựa trên sai số thặng dư (residual errors) của mô hình trước đó.
Hãy cùng thực hiện Gradient Boosting thủ công với bài toán hồi quy (Regression).
import numpy as np
from sklearn.tree import DecisionTreeRegressor
# Tạo dữ liệu bậc 2 có nhiễu
m = 100
rng = np.random.default_rng(seed=42)
X = rng.random((m, 1)) - 0.5
noise = 0.05 * rng.standard_normal(m)
y = 3 * X[:, 0] ** 2 + noise # y = 3x² + Gaussian noise
# Huấn luyện cây thứ nhất trên dữ liệu gốc
tree_reg1 = DecisionTreeRegressor(max_depth=2, random_state=42)
tree_reg1.fit(X, y)
DecisionTreeRegressor(max_depth=2, random_state=42)
DecisionTreeRegressor(max_depth=2, random_state=42)
Bây giờ, chúng ta tính toán sai số (residuals) của cây thứ nhất và huấn luyện cây thứ hai trên sai số này.
y2 = y - tree_reg1.predict(X) # Tính sai số thặng dư
tree_reg2 = DecisionTreeRegressor(max_depth=2, random_state=43)
tree_reg2.fit(X, y2) # Huấn luyện trên sai số
DecisionTreeRegressor(max_depth=2, random_state=43)
DecisionTreeRegressor(max_depth=2, random_state=43)
Tiếp tục làm tương tự với cây thứ ba.
y3 = y2 - tree_reg2.predict(X)
tree_reg3 = DecisionTreeRegressor(max_depth=2, random_state=44)
tree_reg3.fit(X, y3)
DecisionTreeRegressor(max_depth=2, random_state=44)
DecisionTreeRegressor(max_depth=2, random_state=44)
Dự đoán cuối cùng là tổng dự đoán của cả ba cây.
X_new = np.array([[-0.4], [0.], [0.5]])
sum(tree.predict(X_new) for tree in (tree_reg1, tree_reg2, tree_reg3))
array([0.57356534, 0.0405142 , 0.66914249])
Biểu đồ dưới đây minh họa quá trình này. Cột bên trái hiển thị dự đoán của từng cây riêng lẻ (trên residuals), cột bên phải hiển thị dự đoán tổng hợp của ensemble.
# extra code – ô này tạo ra Hình 6–9 minh họa Gradient Boosting
def plot_predictions(regressors, X, y, axes, style,
label=None, data_style="b.", data_label=None):
x1 = np.linspace(axes[0], axes[1], 500)
y_pred = sum(regressor.predict(x1.reshape(-1, 1))
for regressor in regressors)
plt.plot(X[:, 0], y, data_style, label=data_label)
plt.plot(x1, y_pred, style, linewidth=2, label=label)
if label or data_label:
plt.legend(loc="upper center")
plt.axis(axes)
plt.figure(figsize=(11, 11))
plt.subplot(3, 2, 1)
plot_predictions([tree_reg1], X, y, axes=[-0.5, 0.5, -0.2, 0.8], style="g-",
label="$h_1(x_1)$", data_label="Training set")
plt.ylabel("$y$ ", rotation=0)
plt.title("Residuals and tree predictions")
plt.subplot(3, 2, 2)
plot_predictions([tree_reg1], X, y, axes=[-0.5, 0.5, -0.2, 0.8], style="r-",
label="$h(x_1) = h_1(x_1)$", data_label="Training set")
plt.title("Ensemble predictions")
plt.subplot(3, 2, 3)
plot_predictions([tree_reg2], X, y2, axes=[-0.5, 0.5, -0.4, 0.6], style="g-",
label="$h_2(x_1)$", data_style="k+",
data_label="Residuals: $y - h_1(x_1)$")
plt.ylabel("$y$ ", rotation=0)
plt.subplot(3, 2, 4)
plot_predictions([tree_reg1, tree_reg2], X, y, axes=[-0.5, 0.5, -0.2, 0.8],
style="r-", label="$h(x_1) = h_1(x_1) + h_2(x_1)$")
plt.subplot(3, 2, 5)
plot_predictions([tree_reg3], X, y3, axes=[-0.5, 0.5, -0.4, 0.6], style="g-",
label="$h_3(x_1)$", data_style="k+",
data_label="Residuals: $y - h_1(x_1) - h_2(x_1)$")
plt.xlabel("$x_1$")
plt.ylabel("$y$ ", rotation=0)
plt.subplot(3, 2, 6)
plot_predictions([tree_reg1, tree_reg2, tree_reg3], X, y,
axes=[-0.5, 0.5, -0.2, 0.8], style="r-",
label="$h(x_1) = h_1(x_1) + h_2(x_1) + h_3(x_1)$")
plt.xlabel("$x_1$")
plt.show()

Scikit-Learn cung cấp lớp GradientBoostingRegressor để tự động hóa việc này. Chúng ta có thể kiểm soát tốc độ học (learning_rate). Tốc độ học thấp nghĩa là mỗi cây đóng góp ít hơn vào dự đoán tổng thể, do đó cần nhiều cây hơn, nhưng mô hình thường sẽ khái quát hóa tốt hơn (kỹ thuật này gọi là shrinkage).
from sklearn.ensemble import GradientBoostingRegressor
# Gradient Boosting cơ bản
gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=3,
learning_rate=1.0, random_state=42)
gbrt.fit(X, y)
GradientBoostingRegressor(learning_rate=1.0, max_depth=2, n_estimators=3,
random_state=42)GradientBoostingRegressor(learning_rate=1.0, max_depth=2, n_estimators=3,
random_state=42)Để tìm số lượng cây tối ưu, chúng ta có thể sử dụng kỹ thuật Dừng sớm (Early Stopping). Tham số n_iter_no_change giúp mô hình tự động dừng huấn luyện khi sai số không cải thiện sau một số vòng lặp nhất định.
# Gradient Boosting với Early Stopping
gbrt_best = GradientBoostingRegressor(
max_depth=2, learning_rate=0.05, n_estimators=500,
n_iter_no_change=10, random_state=42)
gbrt_best.fit(X, y)
GradientBoostingRegressor(learning_rate=0.05, max_depth=2, n_estimators=500,
n_iter_no_change=10, random_state=42)GradientBoostingRegressor(learning_rate=0.05, max_depth=2, n_estimators=500,
n_iter_no_change=10, random_state=42)# Số lượng cây thực tế được sử dụng
gbrt_best.n_estimators_
53
Biểu đồ so sánh giữa việc chưa đủ cây (underfitting) và số lượng cây tối ưu.
# extra code – ô này tạo ra Hình 6–10
fig, axes = plt.subplots(ncols=2, figsize=(10, 4), sharey=True)
plt.sca(axes[0])
plot_predictions([gbrt], X, y, axes=[-0.5, 0.5, -0.1, 0.8], style="r-",
label="Ensemble predictions")
plt.title(f"learning_rate={gbrt.learning_rate}, "
f"n_estimators={gbrt.n_estimators_}")
plt.xlabel("$x_1$")
plt.ylabel("$y$", rotation=0)
plt.sca(axes[1])
plot_predictions([gbrt_best], X, y, axes=[-0.5, 0.5, -0.1, 0.8], style="r-")
plt.title(f"learning_rate={gbrt_best.learning_rate}, "
f"n_estimators={gbrt_best.n_estimators_}")
plt.xlabel("$x_1$")
plt.show()

Histogram-based Gradient Boosting
Đối với các tập dữ liệu lớn, việc huấn luyện Gradient Boosting truyền thống có thể rất chậm. Scikit-Learn cung cấp HistGradientBoostingRegressor (lấy cảm hứng từ LightGBM). Nó nhóm các giá trị liên tục vào các bin (thùng) rời rạc (thường là 255 bin), giúp tăng tốc độ huấn luyện đáng kể (từ O(mnlog(m)) xuống O(m*n)).
Dưới đây, chúng ta sẽ tải dữ liệu nhà ở California để thử nghiệm.
# extra code – tải dữ liệu housing (tương tự chương 2)
from pathlib import Path
import tarfile
import urllib.request
import pandas as pd
from sklearn.model_selection import train_test_split
def load_housing_data():
tarball_path = Path("datasets/housing.tgz")
if not tarball_path.is_file():
Path("datasets").mkdir(parents=True, exist_ok=True)
url = "https://github.com/ageron/data/raw/main/housing.tgz"
urllib.request.urlretrieve(url, tarball_path)
with tarfile.open(tarball_path) as housing_tarball:
housing_tarball.extractall(path="datasets")
return pd.read_csv(Path("datasets/housing/housing.csv"))
housing = load_housing_data()
train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)
housing_labels = train_set["median_house_value"]
housing = train_set.drop("median_house_value", axis=1)
/tmp/ipython-input-375749916.py:15: DeprecationWarning: Python 3.14 will, by default, filter extracted tar archives and reject files or modify their metadata. Use the filter argument to control this behavior.
housing_tarball.extractall(path="datasets")
HistGradientBoostingRegressor cũng có khả năng xử lý trực tiếp các biến phân loại (categorical features) và các giá trị còn thiếu (missing values) mà không cần tiền xử lý phức tạp.
from sklearn.pipeline import make_pipeline
from sklearn.compose import make_column_transformer
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.preprocessing import OrdinalEncoder
# Tạo pipeline xử lý dữ liệu và huấn luyện mô hình
hgb_reg = make_pipeline(
make_column_transformer((OrdinalEncoder(), ["ocean_proximity"]),
remainder="passthrough",
force_int_remainder_cols=False),
HistGradientBoostingRegressor(categorical_features=[0], random_state=42))
hgb_reg.fit(housing, housing_labels)
Pipeline(steps=[('columntransformer',
ColumnTransformer(force_int_remainder_cols=False,
remainder='passthrough',
transformers=[('ordinalencoder',
OrdinalEncoder(),
['ocean_proximity'])])),
('histgradientboostingregressor',
HistGradientBoostingRegressor(categorical_features=[0],
random_state=42))])Pipeline(steps=[('columntransformer',
ColumnTransformer(force_int_remainder_cols=False,
remainder='passthrough',
transformers=[('ordinalencoder',
OrdinalEncoder(),
['ocean_proximity'])])),
('histgradientboostingregressor',
HistGradientBoostingRegressor(categorical_features=[0],
random_state=42))])ColumnTransformer(force_int_remainder_cols=False, remainder='passthrough',
transformers=[('ordinalencoder', OrdinalEncoder(),
['ocean_proximity'])])['ocean_proximity']
OrdinalEncoder()
['longitude', 'latitude', 'housing_median_age', 'total_rooms', 'total_bedrooms', 'population', 'households', 'median_income']
passthrough
HistGradientBoostingRegressor(categorical_features=[0], random_state=42)
Đánh giá mô hình bằng RMSE (Root Mean Squared Error) thông qua cross-validation.
# extra code – đánh giá RMSE
from sklearn.model_selection import cross_val_score
hgb_rmses = -cross_val_score(hgb_reg, housing, housing_labels,
scoring="neg_root_mean_squared_error", cv=10)
pd.Series(hgb_rmses).describe()
| 0 | |
|---|---|
| count | 10.000000 |
| mean | 47613.307194 |
| std | 1295.422509 |
| min | 44963.213061 |
| 25% | 47001.233485 |
| 50% | 48000.963564 |
| 75% | 48488.093243 |
| max | 49176.368465 |
5. Stacking (Xếp chồng)
Phương pháp cuối cùng chúng ta tìm hiểu là Stacking. Thay vì dùng các hàm đơn giản (như biểu quyết cứng) để tổng hợp các dự đoán, tại sao chúng ta không huấn luyện một mô hình để thực hiện việc tổng hợp này? Mô hình này được gọi là Blender hoặc Meta-learner.
Scikit-Learn cung cấp StackingClassifier để thực hiện việc này một cách dễ dàng.
from sklearn.ensemble import StackingClassifier
# Khởi tạo StackingClassifier
stacking_clf = StackingClassifier(
estimators=[
('lr', LogisticRegression(random_state=42)),
('rf', RandomForestClassifier(random_state=42)),
('svc', SVC(probability=True, random_state=42))
],
final_estimator=RandomForestClassifier(random_state=43),
cv=5 # số lượng fold cho cross-validation
)
stacking_clf.fit(X_train, y_train)
StackingClassifier(cv=5,
estimators=[('lr', LogisticRegression(random_state=42)),
('rf', RandomForestClassifier(random_state=42)),
('svc', SVC(probability=True, random_state=42))],
final_estimator=RandomForestClassifier(random_state=43))StackingClassifier(cv=5,
estimators=[('lr', LogisticRegression(random_state=42)),
('rf', RandomForestClassifier(random_state=42)),
('svc', SVC(probability=True, random_state=42))],
final_estimator=RandomForestClassifier(random_state=43))LogisticRegression(random_state=42)
RandomForestClassifier(random_state=42)
SVC(probability=True, random_state=42)
RandomForestClassifier(random_state=43)
Đánh giá độ chính xác của mô hình Stacking.
stacking_clf.score(X_test, y_test)
0.928
Ôn tập
-
Sức mạnh của Ensemble: Nếu bạn có 5 mô hình khác nhau cùng đạt độ chính xác 95%, bạn có thể kết hợp chúng thành một bộ phân loại biểu quyết để đạt kết quả tốt hơn. Điều này hoạt động tốt nhất khi các mô hình rất khác nhau (ví dụ: SVM, Cây quyết định, Logistic Regression) hoặc được huấn luyện trên các tập dữ liệu con khác nhau, giúp giảm thiểu rủi ro mắc cùng một loại sai lầm.
-
Hard vs. Soft Voting: Hard Voting đếm phiếu bầu (đa số thắng). Soft Voting tính trung bình xác suất dự đoán của từng lớp. Soft Voting thường tốt hơn vì nó coi trọng các phiếu bầu có độ tin cậy cao.
-
Phân tán huấn luyện (Distributed Training):
- Bagging/Pasting/Random Forest: Có thể phân tán vì các mô hình độc lập nhau.
- Boosting: Không thể, vì mô hình sau phụ thuộc vào mô hình trước (tuần tự).
- Stacking: Các mô hình trong cùng một lớp có thể huấn luyện song song, nhưng các lớp phải huấn luyện tuần tự.
-
Lợi ích của OOB: OOB cho phép đánh giá mô hình Bagging ngay trong quá trình huấn luyện mà không cần tách riêng tập validation, giúp tận dụng tối đa dữ liệu.
-
Random Forest vs. Extra-Trees: Random Forest tìm ngưỡng tốt nhất trong một tập hợp ngẫu nhiên các đặc trưng. Extra-Trees chọn ngưỡng ngẫu nhiên hoàn toàn cho mỗi đặc trưng. Extra-Trees nhanh hơn và có tính chất điều chuẩn (regularization) cao hơn.
-
Xử lý Underfitting với AdaBoost: Tăng số lượng estimators, giảm regularization của mô hình cơ sở, hoặc tăng learning rate.
-
Xử lý Overfitting với Gradient Boosting: Giảm learning rate, giảm số lượng estimators (dùng early stopping).
Bài tập thực hành 1: Voting Classifier trên MNIST
Bước 1: Chuẩn bị dữ liệu. Chúng ta sẽ sử dụng bộ dữ liệu MNIST, chia thành tập huấn luyện (50.000), tập validation (10.000) và tập kiểm tra (10.000).
# Chia dữ liệu MNIST
X_train, y_train = X_mnist[:50_000], y_mnist[:50_000]
X_valid, y_valid = X_mnist[50_000:60_000], y_mnist[50_000:60_000]
X_test, y_test = X_mnist[60_000:], y_mnist[60_000:]
Bước 2: Huấn luyện các mô hình đơn lẻ. Chúng ta sẽ huấn luyện Random Forest, Extra-Trees, SVM (LinearSVC) và mạng MLP (Multi-Layer Perceptron).
from sklearn.ensemble import ExtraTreesClassifier
from sklearn.svm import LinearSVC
from sklearn.neural_network import MLPClassifier
random_forest_clf = RandomForestClassifier(n_estimators=100, random_state=42)
extra_trees_clf = ExtraTreesClassifier(n_estimators=100, random_state=42)
svm_clf = LinearSVC(max_iter=100, tol=20, dual=True, random_state=42)
mlp_clf = MLPClassifier(random_state=42)
estimators = [random_forest_clf, extra_trees_clf, svm_clf, mlp_clf]
for estimator in estimators:
print("Training the", estimator)
estimator.fit(X_train, y_train)
Training the RandomForestClassifier(random_state=42)
Training the ExtraTreesClassifier(random_state=42)
Training the LinearSVC(dual=True, max_iter=100, random_state=42, tol=20)
Training the MLPClassifier(random_state=42)
Bước 3: Đánh giá các mô hình trên tập validation.
[estimator.score(X_valid, y_valid) for estimator in estimators]
[0.9736, 0.9743, 0.8662, 0.9613]
Có vẻ như LinearSVM có hiệu năng thấp nhất. Tuy nhiên, chúng ta vẫn giữ lại để xem liệu nó có đóng góp gì cho Ensemble hay không.
Bước 4: Tạo VotingClassifier.
from sklearn.ensemble import VotingClassifier
named_estimators = [
("random_forest_clf", random_forest_clf),
("extra_trees_clf", extra_trees_clf),
("svm_clf", svm_clf),
("mlp_clf", mlp_clf),]
voting_clf = VotingClassifier(named_estimators)
voting_clf.fit(X_train, y_train)
voting_clf.score(X_valid, y_valid)
0.975
Bước 5: Tinh chỉnh Ensemble.
Chúng ta cần lưu ý rằng VotingClassifier khi huấn luyện sẽ tạo ra các bản sao (clones) của mô hình gốc. Trên tập dữ liệu MNIST, các nhãn lớp là chuỗi (‘0’, ‘1’,…), nhưng các clones này được huấn luyện với index (số nguyên). Để đánh giá chính xác các estimators bên trong voting_clf, ta cần chuyển đổi nhãn validation về dạng số.
y_valid_encoded = y_valid.astype(np.int64)
[estimator.score(X_valid, y_valid_encoded)
for estimator in voting_clf.estimators_]
[0.9736, 0.9743, 0.8662, 0.9613]
Hãy thử loại bỏ SVM (mô hình yếu nhất) xem hiệu năng có cải thiện không.
# Đánh dấu loại bỏ SVM
voting_clf.set_params(svm_clf="drop")
# Loại bỏ SVM khỏi danh sách các estimator đã huấn luyện
svm_clf_trained = voting_clf.named_estimators_.pop("svm_clf")
voting_clf.estimators_.remove(svm_clf_trained)
# Đánh giá lại
voting_clf.score(X_valid, y_valid)
0.9761
Kết quả tốt hơn một chút! Bây giờ thử dùng Soft Voting. Lưu ý là Hard Voting đang thắng thế ở trường hợp này.
voting_clf.voting = "soft"
print("Soft voting score:", voting_clf.score(X_valid, y_valid))
# Quay lại Hard Voting vì nó tốt hơn
voting_clf.voting = "hard"
print("Hard voting score (test set):", voting_clf.score(X_test, y_test))
Soft voting score: 0.9703
Hard voting score (test set): 0.9733
So sánh với các mô hình đơn lẻ trên tập test:
[estimator.score(X_test, y_test.astype(np.int64))
for estimator in voting_clf.estimators_]
[0.968, 0.9703, 0.9618]
Kết quả cho thấy Ensemble đã giảm tỷ lệ lỗi đáng kể so với mô hình đơn lẻ tốt nhất.
Bài tập thực hành 2: Stacking Ensemble
Chúng ta sẽ tự xây dựng một hệ thống Stacking thủ công.
Bước 1: Tạo tập dữ liệu mới từ dự đoán của lớp 1 (Layer 1). Dùng các mô hình đã huấn luyện (RF, ExtraTrees, SVM, MLP) để dự đoán trên tập validation.
X_valid_predictions = np.empty((len(X_valid), len(estimators)), dtype=object)
for index, estimator in enumerate(estimators):
X_valid_predictions[:, index] = estimator.predict(X_valid)
X_valid_predictions
array([['3', '3', '3', '3'],
['8', '8', '8', '8'],
['6', '6', '6', '6'],
...,
['5', '5', '5', '5'],
['6', '6', '6', '6'],
['8', '8', '8', '8']], dtype=object)
Bước 2: Huấn luyện Blender. Chúng ta sử dụng một Random Forest làm Blender, huấn luyện trên các dự đoán vừa tạo ra.
rnd_forest_blender = RandomForestClassifier(n_estimators=200, oob_score=True,
random_state=42)
rnd_forest_blender.fit(X_valid_predictions, y_valid)
rnd_forest_blender.oob_score_
0.9738
Bước 3: Đánh giá trên tập kiểm tra. Để đánh giá, chúng ta cần quy trình 2 bước:
- Dùng các mô hình lớp 1 dự đoán trên tập test.
- Dùng Blender dự đoán trên kết quả của bước 1.
X_test_predictions = np.empty((len(X_test), len(estimators)), dtype=object)
for index, estimator in enumerate(estimators):
X_test_predictions[:, index] = estimator.predict(X_test)
y_pred = rnd_forest_blender.predict(X_test_predictions)
accuracy_score(y_test, y_pred)
0.9688
Kết quả không tốt bằng Voting Classifier. Lý do có thể là việc cài đặt thủ công chưa tối ưu.
Bước 4: Sử dụng StackingClassifier của Scikit-Learn.
Lớp này sử dụng Cross-validation để huấn luyện, giúp tận dụng dữ liệu tốt hơn.
# Gộp tập train và valid lại vì StackingClassifier dùng CV
X_train_full, y_train_full = X_mnist[:60_000], y_mnist[:60_000]
# Lưu ý: Quá trình này có thể tốn 15-30 phút tùy phần cứng
stack_clf = StackingClassifier(named_estimators,
final_estimator=rnd_forest_blender)
stack_clf.fit(X_train_full, y_train_full)
StackingClassifier(estimators=[('random_forest_clf',
RandomForestClassifier(random_state=42)),
('extra_trees_clf',
ExtraTreesClassifier(random_state=42)),
('svm_clf',
LinearSVC(dual=True, max_iter=100,
random_state=42, tol=20)),
('mlp_clf', MLPClassifier(random_state=42))],
final_estimator=RandomForestClassifier(n_estimators=200,
oob_score=True,
random_state=42))StackingClassifier(estimators=[('random_forest_clf',
RandomForestClassifier(random_state=42)),
('extra_trees_clf',
ExtraTreesClassifier(random_state=42)),
('svm_clf',
LinearSVC(dual=True, max_iter=100,
random_state=42, tol=20)),
('mlp_clf', MLPClassifier(random_state=42))],
final_estimator=RandomForestClassifier(n_estimators=200,
oob_score=True,
random_state=42))RandomForestClassifier(random_state=42)
ExtraTreesClassifier(random_state=42)
LinearSVC(dual=True, max_iter=100, random_state=42, tol=20)
MLPClassifier(random_state=42)
RandomForestClassifier(n_estimators=200, oob_score=True, random_state=42)
Đánh giá kết quả cuối cùng:
stack_clf.score(X_test, y_test)
0.9795
Thật tuyệt vời! StackingClassifier đã vượt qua cả Voting Classifier và các mô hình đơn lẻ. Điều này là nhờ vào việc được huấn luyện trên nhiều dữ liệu hơn và cơ chế Cross-validation hiệu quả.
Chúc mừng bạn đã hoàn thành chương về Học kết hợp và Rừng ngẫu nhiên!