[ML101] Chương 4: Huấn luyện Mô hình (Training Models)
Các phương pháp huấn luyện mô hình Machine Learning, gradient descent và regularization
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.
Trong các chương trước, chúng ta đã sử dụng các mô hình Machine Learning như những “hộp đen”: đưa dữ liệu vào và nhận kết quả đầu ra mà không thực sự đi sâu vào cơ chế hoạt động bên trong. Trong chương này, chúng ta sẽ mở những chiếc hộp đó ra để tìm hiểu cách thức hoạt động, cách chúng được huấn luyện và tối ưu hóa các tham số nội tại.
Việc hiểu rõ cơ chế hoạt động sẽ giúp bạn:
- Lựa chọn mô hình phù hợp cho bài toán.
- Chọn thuật toán huấn luyện thích hợp.
- Tinh chỉnh các siêu tham số (hyperparameters) hiệu quả hơn.
- Phân tích và khắc phục lỗi (debug) khi mô hình hoạt động không như ý muốn.
Bạn có thể chạy trực tiếp các đoạn mã code tại: Google Colab.
Cài đặt và Khởi tạo (Setup)
Trước tiên, vẫn như thường lệ, chúng ta cần đảm bảo môi trường Python và các thư viện cần thiết (như Scikit-Learn) đáp ứng yêu cầu phiên bản tối thiểu.
# Dự án này yêu cầu Python 3.10 trở lên
import sys
assert sys.version_info >= (3, 10)
# Yêu cầu Scikit-Learn ≥ 1.6.1
from packaging.version import Version
import sklearn
assert Version(sklearn.__version__) >= Version("1.6.1")
Như đã thực hiện ở các chương trước, 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à thẩm mỹ hơn.
import matplotlib.pyplot as plt
# Thiết lập kích thước phông chữ cho 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)
Hồi quy Tuyến tính (Linear Regression)
Một mô hình hồi quy tuyến tính đưa ra dự đoán bằng cách tính tổng trọng số của các đặc trưng đầu vào cộng với một hằng số (được gọi là bias hay intercept).
Phương trình tổng quát:
Trong đó:
- là giá trị dự đoán.
- là số lượng đặc trưng (features).
- là giá trị đặc trưng thứ .
- là các tham số của mô hình (với là bias, và là trọng số đặc trưng).
Để huấn luyện mô hình, ta cần tìm bộ tham số sao cho giảm thiểu sai số dự đoán. Thước đo phổ biến nhất cho bài toán hồi quy là Sai số Bình phương Trung bình (Mean Squared Error - MSE).
Phương trình Pháp tuyến (The Normal Equation)
Để tìm ra giá trị tối ưu nhằm cực tiểu hóa hàm mất mát MSE, tồn tại một giải pháp dạng đóng (closed-form solution) được gọi là Phương trình Pháp tuyến:
Hãy cùng tạo ra một bộ dữ liệu giả lập dạng tuyến tính để thử nghiệm.
import numpy as np
rng = np.random.default_rng(seed=42) # Khởi tạo bộ sinh số ngẫu nhiên
m = 200 # số lượng mẫu dữ liệu (instances)
# Tạo dữ liệu X ngẫu nhiên
X = 2 * rng.random((m, 1)) # vector cột
# Tạo dữ liệu y có mối quan hệ tuyến tính với X (y = 4 + 3x + nhiễu)
y = 4 + 3 * X + rng.standard_normal((m, 1)) # vector cột
# đoạn code phụ – dùng để tạo Hình 4–1
import matplotlib.pyplot as plt
plt.figure(figsize=(6, 4))
plt.plot(X, y, "b.") # Vẽ các điểm dữ liệu màu xanh
plt.xlabel("$x_1$")
plt.ylabel("$y$", rotation=0)
plt.axis([0, 2, 0, 15]) # Giới hạn trục toạ độ
plt.grid()
plt.show()

Bây giờ, chúng ta sẽ sử dụng Phương trình Pháp tuyến để tính toán . Chúng ta sẽ sử dụng hàm inv() từ mô-đun đại số tuyến tính của NumPy (np.linalg) để tính nghịch đảo ma trận, và toán tử @ để thực hiện nhân ma trận.
from sklearn.preprocessing import add_dummy_feature
# Thêm x0 = 1 vào mỗi mẫu dữ liệu (để nhân với theta_0 - bias term)
X_b = add_dummy_feature(X)
# Áp dụng công thức Phương trình Pháp tuyến
theta_best = np.linalg.inv(X_b.T @ X_b) @ X_b.T @ y
theta_best
array([[3.69084138],
[3.32960458]])
Chúng ta hy vọng và . Kết quả nhận được khá gần với giá trị thực tế (do dữ liệu có nhiễu Gaussian nên không thể chính xác tuyệt đối).
Bây giờ ta có thể sử dụng để đưa ra dự đoán cho các giá trị mới:
X_new = np.array([[0], [2]])
X_new_b = add_dummy_feature(X_new) # thêm x0 = 1 vào mỗi mẫu
y_predict = X_new_b @ theta_best
y_predict
array([[ 3.69084138],
[10.35005055]])
Hãy vẽ mô hình dự đoán (đường hồi quy) đè lên dữ liệu thực tế:
import matplotlib.pyplot as plt
plt.figure(figsize=(6, 4)) # đoạn code phụ – định dạng biểu đồ
plt.plot(X_new, y_predict, "r-", label="Predictions") # Vẽ đường dự đoán màu đỏ
plt.plot(X, y, "b.") # Vẽ dữ liệu thực tế
# đoạn code phụ – làm đẹp cho Hình 4–2
plt.xlabel("$x_1$")
plt.ylabel("$y$", rotation=0)
plt.axis([0, 2, 0, 15])
plt.grid()
plt.legend(loc="upper left")
plt.show()

Thực hiện Hồi quy Tuyến tính bằng Scikit-Learn rất đơn giản với lớp LinearRegression.
from sklearn.linear_model import LinearRegression
lin_reg = LinearRegression()
lin_reg.fit(X, y)
# Hiển thị bias (intercept_) và trọng số (coef_)
lin_reg.intercept_, lin_reg.coef_
(array([3.69084138]), array([[3.32960458]]))
# Dự đoán giá trị mới
lin_reg.predict(X_new)
array([[ 3.69084138],
[10.35005055]])
Lớp LinearRegression thực chất dựa trên hàm scipy.linalg.lstsq() (viết tắt của “least squares” - bình phương tối thiểu). Bạn có thể gọi hàm này trực tiếp nếu muốn:
theta_best_svd, residuals, rank, s = np.linalg.lstsq(X_b, y, rcond=1e-6)
theta_best_svd
array([[3.69084138],
[3.32960458]])
Hàm này tính toán , trong đó là ma trận giả nghịch đảo (pseudoinverse) của (cụ thể là nghịch đảo Moore-Penrose). Bạn có thể tính trực tiếp giả nghịch đảo bằng np.linalg.pinv():
np.linalg.pinv(X_b) @ y
array([[3.69084138],
[3.32960458]])
Gradient Descent (Hạ độ dốc)
Gradient Descent là một thuật toán tối ưu hóa tổng quát rất quan trọng. Ý tưởng cơ bản là tinh chỉnh các tham số lặp đi lặp lại để cực tiểu hóa hàm chi phí (cost function).
Hãy tưởng tượng bạn đang bị lạc trên núi trong sương mù dày đặc; bạn chỉ cảm nhận được độ dốc của mặt đất dưới chân. Chiến lược tốt nhất để xuống chân núi nhanh chóng là đi theo hướng dốc xuống mạnh nhất. Gradient Descent hoạt động theo cách tương tự: nó đo gradient cục bộ của hàm lỗi theo vector tham số , và đi theo hướng gradient giảm dần. Khi gradient bằng 0, bạn đã đạt đến điểm cực tiểu.
Batch Gradient Descent (Hạ độ dốc toàn cục)
Để thực hiện Gradient Descent, bạn cần tính gradient của hàm chi phí đối với từng tham số mô hình . Phương pháp Batch Gradient Descent sử dụng toàn bộ tập dữ liệu huấn luyện tại mỗi bước để tính toán gradient này. Do đó, nó có thể rất chậm nếu tập dữ liệu huấn luyện quá lớn.
Công thức cập nhật tham số:
Trong đó là tốc độ học (learning rate).
eta = 0.1 # learning rate (tốc độ học)
n_epochs = 1000 # số lần lặp lại quy trình huấn luyện
m = len(X_b) # số lượng mẫu dữ liệu
rng = np.random.default_rng(seed=42)
theta = rng.standard_normal((2, 1)) # khởi tạo tham số ngẫu nhiên
for epoch in range(n_epochs):
# Tính gradient của hàm loss MSE
gradients = 2 / m * X_b.T @ (X_b @ theta - y)
# Cập nhật theta
theta = theta - eta * gradients
Sau khi huấn luyện, hãy xem các tham số mô hình thu được:
theta
array([[3.69084138],
[3.32960458]])
Kết quả này khớp chính xác với những gì Phương trình Pháp tuyến đã tìm ra. Gradient Descent đã hoạt động hoàn hảo.
Tuy nhiên, tốc độ học đóng vai trò rất quan trọng. Hình dưới đây sẽ minh họa quá trình Gradient Descent với ba giá trị tốc độ học khác nhau.
# đoạn code phụ – tạo Hình 4–8
import matplotlib as mpl
def plot_gradient_descent(theta, eta):
m = len(X_b)
plt.plot(X, y, "b.")
n_epochs = 1000
n_shown = 20
theta_path = []
for epoch in range(n_epochs):
if epoch < n_shown:
y_predict = X_new_b @ theta
# Thay đổi màu sắc đường dự đoán dần dần từ nhạt sang đậm
color = mpl.colors.rgb2hex(plt.cm.OrRd(epoch / n_shown + 0.15))
plt.plot(X_new, y_predict, linestyle="solid", color=color)
gradients = 2 / m * X_b.T @ (X_b @ theta - y)
theta = theta - eta * gradients
theta_path.append(theta)
plt.xlabel("$x_1$")
plt.axis([0, 2, 0, 15])
plt.grid()
plt.title(fr"$\eta = {eta}$")
return theta_path
rng = np.random.default_rng(seed=42)
theta = rng.standard_normal((2, 1)) # khởi tạo tham số ngẫu nhiên
plt.figure(figsize=(10, 4))
plt.subplot(131)
plot_gradient_descent(theta, eta=0.02)
plt.ylabel("$y$", rotation=0)
plt.subplot(132)
theta_path_bgd = plot_gradient_descent(theta, eta=0.1)
plt.gca().axes.yaxis.set_ticklabels([])
plt.subplot(133)
plt.gca().axes.yaxis.set_ticklabels([])
plot_gradient_descent(theta, eta=0.5)
plt.show()

- Bên trái: quá thấp, thuật toán sẽ hội tụ nhưng mất rất nhiều thời gian.
- Ở giữa: vừa phải, hội tụ nhanh chóng.
- Bên phải: quá cao, thuật toán nhảy loạn xạ và phân kỳ (diverge), ngày càng xa điểm tối ưu.
Stochastic Gradient Descent (Hạ độ dốc ngẫu nhiên)
Vấn đề chính của Batch Gradient Descent là nó sử dụng toàn bộ tập huấn luyện để tính gradient tại mỗi bước, khiến nó rất chậm khi dữ liệu lớn. Stochastic Gradient Descent (SGD) giải quyết vấn đề này bằng cách chọn ngẫu nhiên một mẫu dữ liệu tại mỗi bước và tính gradient chỉ dựa trên mẫu đó.
Do tính ngẫu nhiên, SGD ít ổn định hơn Batch GD. Thay vì giảm đều đặn cho đến khi chạm đáy, hàm chi phí sẽ nảy lên xuống, giảm dần theo trung bình nhưng không bao giờ ổn định hoàn toàn tại một điểm. Để giúp thuật toán hội tụ, chúng ta cần giảm dần tốc độ học theo thời gian (learning schedule).
theta_path_sgd = [] # dùng để lưu đường đi của theta để vẽ biểu đồ sau này
n_epochs = 50
t0, t1 = 5, 50 # siêu tham số cho lịch trình học (learning schedule)
def learning_schedule(t):
return t0 / (t + t1)
rng = np.random.default_rng(seed=42)
theta = rng.standard_normal((2, 1)) # khởi tạo tham số ngẫu nhiên
n_shown = 20 # chỉ vẽ 20 bước đầu tiên
plt.figure(figsize=(6, 4)) # đoạn code phụ – định dạng biểu đồ
for epoch in range(n_epochs):
for iteration in range(m):
# đoạn code phụ – vẽ các đường dự đoán trong quá trình học
if epoch == 0 and iteration < n_shown:
y_predict = X_new_b @ theta
color = mpl.colors.rgb2hex(plt.cm.OrRd(iteration / n_shown + 0.15))
plt.plot(X_new, y_predict, color=color)
# Chọn ngẫu nhiên một mẫu dữ liệu
random_index = rng.integers(m)
xi = X_b[random_index : random_index + 1]
yi = y[random_index : random_index + 1]
# Tính gradient dựa trên mẫu đơn lẻ này
gradients = 2 * xi.T @ (xi @ theta - yi) # SGD không chia cho m
# Cập nhật learning rate và theta
eta = learning_schedule(epoch * m + iteration)
theta = theta - eta * gradients
theta_path_sgd.append(theta) # lưu lại để vẽ sau
# đoạn code phụ – làm đẹp Hình 4–10
plt.plot(X, y, "b.")
plt.xlabel("$x_1$")
plt.ylabel("$y$", rotation=0)
plt.axis([0, 2, 0, 15])
plt.grid()
plt.show()

theta
array([[3.69826475],
[3.30748311]])
Trong Scikit-Learn, chúng ta có thể thực hiện SGD cho hồi quy tuyến tính bằng lớp SGDRegressor.
from sklearn.linear_model import SGDRegressor
# Chạy SGD tối đa 1000 epochs, dừng nếu loss không cải thiện quá 1e-5 (tol)
# penalty=None nghĩa là không dùng regularization (sẽ học sau)
sgd_reg = SGDRegressor(max_iter=1000, tol=1e-5, penalty=None, eta0=0.01,
n_iter_no_change=100, random_state=42)
sgd_reg.fit(X, y.ravel()) # y.ravel() vì fit() yêu cầu mảng 1 chiều cho target
sgd_reg.intercept_, sgd_reg.coef_
(array([3.68899733]), array([3.33054574]))
Mini-batch Gradient Descent
Mini-batch Gradient Descent là sự kết hợp giữa Batch GD và SGD. Thay vì tính gradient trên toàn bộ dữ liệu hay chỉ một mẫu, nó tính trên một tập hợp nhỏ ngẫu nhiên các mẫu gọi là mini-batch.
Ưu điểm chính của Mini-batch GD so với SGD là bạn có thể tận dụng khả năng tính toán ma trận tối ưu của phần cứng (đặc biệt là GPU).
Đoạn mã dưới đây minh họa Mini-batch GD và so sánh đường đi của tham số trong không gian tham số giữa ba thuật toán.
# đoạn code phụ – tạo Hình 4–11
from math import ceil
n_epochs = 50
minibatch_size = 20
n_batches_per_epoch = ceil(m / minibatch_size)
rng = np.random.default_rng(seed=42)
theta = rng.standard_normal((2, 1))
t0, t1 = 200, 1000
def learning_schedule(t):
return t0 / (t + t1)
theta_path_mgd = []
for epoch in range(n_epochs):
# Xáo trộn dữ liệu đầu mỗi epoch
shuffled_indices = rng.permutation(m)
X_b_shuffled = X_b[shuffled_indices]
y_shuffled = y[shuffled_indices]
for iteration in range(0, n_batches_per_epoch):
idx = iteration * minibatch_size
xi = X_b_shuffled[idx : idx + minibatch_size]
yi = y_shuffled[idx : idx + minibatch_size]
gradients = 2 / minibatch_size * xi.T @ (xi @ theta - yi)
eta = learning_schedule(epoch * n_batches_per_epoch + iteration)
theta = theta - eta * gradients
theta_path_mgd.append(theta)
theta_path_bgd = np.array(theta_path_bgd)
theta_path_sgd = np.array(theta_path_sgd)
theta_path_mgd = np.array(theta_path_mgd)
plt.figure(figsize=(7, 4))
plt.plot(theta_path_sgd[:, 0], theta_path_sgd[:, 1], "r-s", linewidth=1,
label="Stochastic")
plt.plot(theta_path_mgd[:, 0], theta_path_mgd[:, 1], "g-+", linewidth=2,
label="Mini-batch")
plt.plot(theta_path_bgd[:, 0], theta_path_bgd[:, 1], "b-o", linewidth=3,
label="Batch")
plt.legend(loc="upper left")
plt.xlabel(r"$\theta_0$")
plt.ylabel(r"$\theta_1$ ", rotation=0)
plt.axis([2.7, 4.5, 2.6, 3.7])
plt.grid()
plt.show()

Biểu đồ cho thấy:
- Batch GD (xanh dương): Đi thẳng đến đích nhưng chậm (mỗi bước tốn nhiều tính toán).
- Stochastic GD (đỏ): Nhảy lung tung nhưng đến đích khá nhanh, tuy nhiên không bao giờ đứng yên.
- Mini-batch GD (xanh lá): Ổn định hơn SGD và đến đích gần hơn Batch GD.
Hồi quy Đa thức (Polynomial Regression)
Làm thế nào nếu dữ liệu thực tế phức tạp hơn một đường thẳng? Đáng ngạc nhiên là bạn vẫn có thể sử dụng mô hình tuyến tính để khớp dữ liệu phi tuyến (non-linear data). Một cách đơn giản là thêm lũy thừa của mỗi đặc trưng như một đặc trưng mới, sau đó huấn luyện mô hình tuyến tính trên tập hợp đặc trưng mở rộng này.
Hãy thử tạo một dữ liệu tuân theo phương trình bậc hai: .
rng = np.random.default_rng(seed=42)
m = 200
X = 6 * rng.random((m, 1)) - 3
y = 0.5 * X ** 2 + X + 2 + rng.standard_normal((m, 1))
# đoạn code phụ – tạo Hình 4–12
plt.figure(figsize=(6, 4))
plt.plot(X, y, "b.")
plt.xlabel("$x_1$")
plt.ylabel("$y$", rotation=0)
plt.axis([-3, 3, 0, 10])
plt.grid()
plt.show()

Rõ ràng một đường thẳng sẽ không thể khớp tốt với dữ liệu này. Chúng ta sẽ sử dụng PolynomialFeatures của Scikit-Learn để bình phương các đặc trưng đầu vào (từ tạo ra ).
from sklearn.preprocessing import PolynomialFeatures
# Tạo đặc trưng bậc 2, include_bias=False vì LinearRegression sẽ tự thêm bias
poly_features = PolynomialFeatures(degree=2, include_bias=False)
X_poly = poly_features.fit_transform(X)
X[0]
array([1.64373629])
X_poly[0]
array([1.64373629, 2.701869 ])
X_poly bây giờ chứa đặc trưng gốc và bình phương của nó . Bây giờ ta có thể áp dụng LinearRegression.
lin_reg = LinearRegression()
lin_reg.fit(X_poly, y)
lin_reg.intercept_, lin_reg.coef_
(array([2.00540719]), array([[1.11022126, 0.50526985]]))
Mô hình ước lượng: . Rất gần với thực tế cộng nhiễu.
# đoạn code phụ – tạo Hình 4–13
X_new = np.linspace(-3, 3, 100).reshape(100, 1)
X_new_poly = poly_features.transform(X_new)
y_new = lin_reg.predict(X_new_poly)
plt.figure(figsize=(6, 4))
plt.plot(X, y, "b.")
plt.plot(X_new, y_new, "r-", linewidth=2, label="Predictions")
plt.xlabel("$x_1$")
plt.ylabel("$y$", rotation=0)
plt.legend(loc="upper left")
plt.axis([-3, 3, 0, 10])
plt.grid()
plt.show()

Đường cong Học tập (Learning Curves)
Nếu sử dụng đa thức bậc quá cao (ví dụ bậc 300), mô hình sẽ cố gắng đi qua mọi điểm dữ liệu huấn luyện, dẫn đến Overfitting (Quá khớp). Ngược lại, mô hình bậc 1 (tuyến tính) quá đơn giản, dẫn đến Underfitting (Chưa khớp).
Hình dưới đây minh họa điều này:
# đoạn code phụ – tạo Hình 4–14
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
plt.figure(figsize=(6, 4))
# Vẽ 3 mô hình với các bậc đa thức khác nhau
for style, width, degree in (("r-+", 2, 1), ("b--", 2, 2), ("g-", 1, 300)):
polybig_features = PolynomialFeatures(degree=degree, include_bias=False)
std_scaler = StandardScaler()
lin_reg = LinearRegression()
polynomial_regression = make_pipeline(polybig_features, std_scaler, lin_reg)
polynomial_regression.fit(X, y)
y_newbig = polynomial_regression.predict(X_new)
label = f"{degree} degree{'s' if degree > 1 else ''}"
plt.plot(X_new, y_newbig, style, label=label, linewidth=width)
plt.plot(X, y, "b.", linewidth=3)
plt.legend(loc="upper left")
plt.xlabel("$x_1$")
plt.ylabel("$y$", rotation=0)
plt.axis([-3, 3, 0, 10])
plt.grid()
plt.show()

Để phát hiện mô hình đang bị overfitting hay underfitting, chúng ta có thể xem xét Learning Curves (Đường cong học tập). Đường cong này biểu diễn hiệu suất của mô hình (RMSE) trên tập huấn luyện và tập kiểm định (validation set) theo kích thước của tập huấn luyện.
Đầu tiên, hãy xem đường cong học tập của mô hình tuyến tính đơn giản:
from sklearn.model_selection import learning_curve
train_sizes, train_scores, valid_scores = learning_curve(
LinearRegression(), X, y, train_sizes=np.linspace(0.01, 1.0, 40), cv=5,
scoring="neg_root_mean_squared_error")
# Đổi dấu vì scoring trả về giá trị âm
train_errors = -train_scores.mean(axis=1)
valid_errors = -valid_scores.mean(axis=1)
plt.figure(figsize=(6, 4)) # đoạn code phụ
plt.plot(train_sizes, train_errors, "r-+", linewidth=2, label="train")
plt.plot(train_sizes, valid_errors, "b-", linewidth=3, label="valid")
# đoạn code phụ – làm đẹp Hình 4–15
plt.xlabel("Training set size")
plt.ylabel("RMSE")
plt.grid()
plt.legend(loc="upper right")
plt.axis([0, 160, 0, 2.5])
plt.show()

Đây là dấu hiệu của Underfitting:
- Lỗi trên tập huấn luyện và tập kiểm định đều cao.
- Chúng khá gần nhau (plateau).
Tiếp theo, hãy xem đường cong của mô hình đa thức bậc 10:
from sklearn.pipeline import make_pipeline
polynomial_regression = make_pipeline(
PolynomialFeatures(degree=10, include_bias=False),
LinearRegression())
train_sizes, train_scores, valid_scores = learning_curve(
polynomial_regression, X, y, train_sizes=np.linspace(0.01, 1.0, 40), cv=5,
scoring="neg_root_mean_squared_error")
# đoạn code phụ – tạo Hình 4–16
train_errors = -train_scores.mean(axis=1)
valid_errors = -valid_scores.mean(axis=1)
plt.figure(figsize=(6, 4))
plt.plot(train_sizes, train_errors, "r-+", linewidth=2, label="train")
plt.plot(train_sizes, valid_errors, "b-", linewidth=3, label="valid")
plt.legend(loc="upper right")
plt.xlabel("Training set size")
plt.ylabel("RMSE")
plt.grid()
plt.axis([0, 160, 0, 2.5])
plt.show()

Đây là dấu hiệu của Overfitting:
- Lỗi trên tập huấn luyện thấp hơn đáng kể so với tập kiểm định.
- Có một khoảng cách (gap) giữa hai đường, cho thấy mô hình hoạt động tốt trên dữ liệu đã học nhưng kém trên dữ liệu mới.
Mô hình Tuyến tính có Chính quy hóa (Regularized Linear Models)
Một cách hiệu quả để giảm overfitting là Chính quy hóa (Regularization) mô hình, tức là hạn chế khả năng tự do của nó (thường bằng cách giảm độ lớn của các trọng số ).
Ridge Regression (Hồi quy Ridge)
Ridge Regression (hay Tikhonov regularization) thêm một số hạng vào hàm mất mát (loss function) bằng tổng bình phương của các trọng số (chuẩn ):
Siêu tham số kiểm soát mức độ chính quy hóa. càng lớn, đường dự đoán càng phẳng (ít phương sai hơn, nhưng độ lệch bias cao hơn).
Hãy tạo một tập dữ liệu nhỏ rất nhiễu để thử nghiệm:
# đoạn code phụ – tạo dữ liệu nhiễu
rng = np.random.default_rng(seed=42)
m = 20 # số lượng mẫu ít
X = 3 * rng.random((m, 1))
y = 1 + 0.5 * X + rng.standard_normal((m, 1)) / 1.5
X_new = np.linspace(0, 3, 100).reshape(100, 1)
# đoạn code phụ – hiển thị dữ liệu
plt.figure(figsize=(6, 4))
plt.plot(X, y, ".")
plt.xlabel("$x_1$")
plt.ylabel("$y$ ", rotation=0)
plt.axis([0, 3, 0, 3.5])
plt.grid()
plt.show()

from sklearn.linear_model import Ridge
# Ridge Regression với solver Cholesky (dạng đóng)
ridge_reg = Ridge(alpha=0.1, solver="cholesky")
ridge_reg.fit(X, y)
ridge_reg.predict([[1.5]])
array([1.84414523])
Đoạn mã dưới đây so sánh các mô hình Ridge với các giá trị khác nhau trên dữ liệu tuyến tính (trái) và dữ liệu đa thức (phải).
# đoạn code phụ – tạo Hình 4–17
def plot_model(model_class, polynomial, alphas, **model_kwargs):
plt.plot(X, y, "b.", linewidth=3)
for alpha, style in zip(alphas, ("b:", "g--", "r-")):
if alpha > 0:
model = model_class(alpha, **model_kwargs)
else:
model = LinearRegression()
if polynomial:
model = make_pipeline(
PolynomialFeatures(degree=10, include_bias=False),
StandardScaler(),
model)
model.fit(X, y)
y_new_regul = model.predict(X_new)
plt.plot(X_new, y_new_regul, style, linewidth=2,
label=fr"$\alpha = {alpha}$")
plt.legend(loc="upper left")
plt.xlabel("$x_1$")
plt.axis([0, 3, 0, 3.5])
plt.grid()
plt.figure(figsize=(9, 3.5))
plt.subplot(121)
plot_model(Ridge, polynomial=False, alphas=(0, 10, 100), random_state=42)
plt.ylabel("$y$ ", rotation=0)
plt.subplot(122)
plot_model(Ridge, polynomial=True, alphas=(0, 10**-5, 1), random_state=42)
plt.gca().axes.yaxis.set_ticklabels([])
plt.show()

Bạn cũng có thể sử dụng SGD để thực hiện Ridge Regression bằng cách thiết lập penalty="l2".
sgd_reg = SGDRegressor(penalty="l2", alpha=0.1 / m, tol=None,
max_iter=1000, eta0=0.01, random_state=42)
sgd_reg.fit(X, y.ravel())
sgd_reg.predict([[1.5]])
array([1.83659707])
Lasso Regression
Lasso Regression (Least Absolute Shrinkage and Selection Operator) sử dụng chuẩn để chính quy hóa:
Điểm đặc biệt quan trọng của Lasso là nó có xu hướng loại bỏ hoàn toàn các trọng số của các đặc trưng ít quan trọng (đưa chúng về 0). Nói cách khác, Lasso tự động thực hiện lựa chọn đặc trưng (feature selection) và tạo ra một mô hình thưa (sparse model).
from sklearn.linear_model import Lasso
lasso_reg = Lasso(alpha=0.1)
lasso_reg.fit(X, y)
lasso_reg.predict([[1.5]])
array([1.87550211])
# đoạn code phụ – tạo Hình 4–18 minh họa Lasso
plt.figure(figsize=(9, 3.5))
plt.subplot(121)
plot_model(Lasso, polynomial=False, alphas=(0, 0.1, 1), random_state=42)
plt.ylabel("$y$ ", rotation=0)
plt.subplot(122)
plot_model(Lasso, polynomial=True, alphas=(0, 1e-2, 1), random_state=42)
plt.gca().axes.yaxis.set_ticklabels([])
plt.show()

Elastic Net
Elastic Net là sự kết hợp giữa Ridge và Lasso. Nó sử dụng cả hai loại penalty. Tỷ lệ pha trộn được kiểm soát bởi tham số l1_ratio.
Nên dùng cái nào? Thông thường: Ridge là lựa chọn mặc định tốt. Nếu bạn nghi ngờ chỉ một vài đặc trưng là hữu ích, hãy dùng Lasso hoặc Elastic Net. Elastic Net thường được ưa chuộng hơn Lasso vì Lasso có thể hành xử thất thường khi số lượng đặc trưng lớn hơn số mẫu huấn luyện hoặc khi các đặc trưng có tương quan mạnh.
from sklearn.linear_model import ElasticNet
elastic_net = ElasticNet(alpha=0.1, l1_ratio=0.5)
elastic_net.fit(X, y)
elastic_net.predict([[1.5]])
array([1.8645014])
Early Stopping (Dừng sớm)
Một cách chính quy hóa rất khác là dừng việc huấn luyện ngay khi sai số trên tập kiểm định (validation error) đạt giá trị tối thiểu. Phương pháp này gọi là Early Stopping.
Đoạn mã sau minh họa việc huấn luyện mô hình SGD và theo dõi lỗi trên tập validation.
from copy import deepcopy
from sklearn.metrics import root_mean_squared_error
from sklearn.preprocessing import StandardScaler
# đoạn code phụ – tạo lại dữ liệu bậc 2 và chia train/valid
rng = np.random.default_rng(seed=42)
m = 200
X = 6 * rng.random((m, 1)) - 3
y = 0.5 * X ** 2 + X + 2 + rng.standard_normal((m, 1))
X_train, y_train = X[: m // 2], y[: m // 2, 0]
X_valid, y_valid = X[m // 2 :], y[m // 2 :, 0]
preprocessing = make_pipeline(PolynomialFeatures(degree=90, include_bias=False),
StandardScaler())
X_train_prep = preprocessing.fit_transform(X_train)
X_valid_prep = preprocessing.transform(X_valid)
sgd_reg = SGDRegressor(penalty=None, eta0=0.002, random_state=42)
n_epochs = 500
best_valid_rmse = float('inf')
train_errors, val_errors = [], []
for epoch in range(n_epochs):
sgd_reg.partial_fit(X_train_prep, y_train)
y_valid_predict = sgd_reg.predict(X_valid_prep)
val_error = root_mean_squared_error(y_valid, y_valid_predict)
# Lưu lại mô hình tốt nhất
if val_error < best_valid_rmse:
best_valid_rmse = val_error
best_model = deepcopy(sgd_reg)
# Tính toán lỗi huấn luyện để vẽ biểu đồ
y_train_predict = sgd_reg.predict(X_train_prep)
train_error = root_mean_squared_error(y_train, y_train_predict)
val_errors.append(val_error)
train_errors.append(train_error)
# đoạn code phụ – tạo Hình 4–20
best_epoch = np.argmin(val_errors)
plt.figure(figsize=(6, 4))
plt.annotate('Best model',
xy=(best_epoch, best_valid_rmse),
xytext=(best_epoch, best_valid_rmse + 0.5),
ha="center",
arrowprops=dict(facecolor='black', shrink=0.05))
plt.plot([0, n_epochs], [best_valid_rmse, best_valid_rmse], "k:", linewidth=2)
plt.plot(val_errors, "b-", linewidth=3, label="Validation set")
plt.plot(best_epoch, best_valid_rmse, "bo")
plt.plot(train_errors, "r--", linewidth=2, label="Training set")
plt.legend(loc="upper right")
plt.xlabel("Epoch")
plt.ylabel("RMSE")
plt.axis([0, n_epochs, 0, 3.5])
plt.grid()
plt.show()

Hồi quy Logistic (Logistic Regression)
Mặc dù có tên là “Hồi quy”, Logistic Regression thực chất là một thuật toán dùng cho bài toán Phân loại (Classification). Nó ước tính xác suất một mẫu dữ liệu thuộc về một lớp cụ thể.
Ước lượng xác suất
Giống như Hồi quy Tuyến tính, Logistic Regression tính tổng trọng số của các đặc trưng đầu vào, nhưng thay vì xuất ra kết quả trực tiếp, nó đưa kết quả qua một hàm sigmoid (logistic function) để nén giá trị vào khoảng [0, 1].
Hàm Sigmoid:
# đoạn code phụ – tạo Hình 4–21 minh họa hàm Sigmoid
lim = 6
t = np.linspace(-lim, lim, 100)
sig = 1 / (1 + np.exp(-t))
plt.figure(figsize=(8, 3))
plt.plot([-lim, lim], [0, 0], "k-")
plt.plot([-lim, lim], [0.5, 0.5], "k:")
plt.plot([-lim, lim], [1, 1], "k:")
plt.plot([0, 0], [-1.1, 1.1], "k-")
plt.plot(t, sig, "b-", linewidth=2, label=r"$\sigma(t) = \dfrac{1}{1 + e^{-t}}$")
plt.xlabel("t")
plt.legend(loc="upper left")
plt.axis([-lim, lim, -0.1, 1.1])
plt.gca().set_yticks([0, 0.25, 0.5, 0.75, 1])
plt.grid()
plt.show()

Đường Ranh giới Quyết định (Decision Boundaries)
Chúng ta sẽ sử dụng bộ dữ liệu Iris nổi tiếng để minh họa. Bộ dữ liệu này chứa thông tin về độ dài/rộng đài hoa và cánh hoa của 3 loài hoa Iris.
from sklearn.datasets import load_iris
iris = load_iris(as_frame=True)
list(iris)
['data',
'target',
'frame',
'target_names',
'DESCR',
'feature_names',
'filename',
'data_module']
print(iris.DESCR) # Xem mô tả dữ liệu
.. _iris_dataset:
Iris plants dataset
--------------------
**Data Set Characteristics:**
:Number of Instances: 150 (50 in each of three classes)
:Number of Attributes: 4 numeric, predictive attributes and the class
:Attribute Information:
- sepal length in cm
- sepal width in cm
- petal length in cm
- petal width in cm
- class:
- Iris-Setosa
- Iris-Versicolour
- Iris-Virginica
:Summary Statistics:
============== ==== ==== ======= ===== ====================
Min Max Mean SD Class Correlation
============== ==== ==== ======= ===== ====================
sepal length: 4.3 7.9 5.84 0.83 0.7826
sepal width: 2.0 4.4 3.05 0.43 -0.4194
petal length: 1.0 6.9 3.76 1.76 0.9490 (high!)
petal width: 0.1 2.5 1.20 0.76 0.9565 (high!)
============== ==== ==== ======= ===== ====================
:Missing Attribute Values: None
:Class Distribution: 33.3% for each of 3 classes.
:Creator: R.A. Fisher
:Donor: Michael Marshall (MARSHALL%PLU@io.arc.nasa.gov)
:Date: July, 1988
The famous Iris database, first used by Sir R.A. Fisher. The dataset is taken
from Fisher's paper. Note that it's the same as in R, but not as in the UCI
Machine Learning Repository, which has two wrong data points.
This is perhaps the best known database to be found in the
pattern recognition literature. Fisher's paper is a classic in the field and
is referenced frequently to this day. (See Duda & Hart, for example.) The
data set contains 3 classes of 50 instances each, where each class refers to a
type of iris plant. One class is linearly separable from the other 2; the
latter are NOT linearly separable from each other.
.. dropdown:: References
- Fisher, R.A. "The use of multiple measurements in taxonomic problems"
Annual Eugenics, 7, Part II, 179-188 (1936); also in "Contributions to
Mathematical Statistics" (John Wiley, NY, 1950).
- Duda, R.O., & Hart, P.E. (1973) Pattern Classification and Scene Analysis.
(Q327.D83) John Wiley & Sons. ISBN 0-471-22361-1. See page 218.
- Dasarathy, B.V. (1980) "Nosing Around the Neighborhood: A New System
Structure and Classification Rule for Recognition in Partially Exposed
Environments". IEEE Transactions on Pattern Analysis and Machine
Intelligence, Vol. PAMI-2, No. 1, 67-71.
- Gates, G.W. (1972) "The Reduced Nearest Neighbor Rule". IEEE Transactions
on Information Theory, May 1972, 431-433.
- See also: 1988 MLC Proceedings, 54-64. Cheeseman et al"s AUTOCLASS II
conceptual clustering system finds 3 classes in the data.
- Many, many more ...
iris.data.head(3)
sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)
0 5.1 3.5 1.4 0.2
1 4.9 3.0 1.4 0.2
2 4.7 3.2 1.3 0.2
iris.target.head(3) # Lưu ý dữ liệu chưa được xáo trộn
| target | |
|---|---|
| 0 | 0 |
| 1 | 0 |
| 2 | 0 |
iris.target_names
array(['setosa', 'versicolor', 'virginica'], dtype='<U10')
Chúng ta sẽ huấn luyện một mô hình để phát hiện loài Iris virginica chỉ dựa trên đặc trưng độ rộng cánh hoa (petal width).
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
X = iris.data[["petal width (cm)"]].values
y = iris.target_names[iris.target] == 'virginica'
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
log_reg = LogisticRegression(random_state=42)
log_reg.fit(X_train, y_train)
LogisticRegression(random_state=42)
LogisticRegression(random_state=42)
Hãy xem xét xác suất dự đoán của mô hình đối với độ rộng cánh hoa từ 0 đến 3cm.
X_new = np.linspace(0, 3, 1000).reshape(-1, 1) # reshape thành cột vector
y_proba = log_reg.predict_proba(X_new)
decision_boundary = X_new[y_proba[:, 1] >= 0.5][0, 0]
plt.figure(figsize=(8, 3)) # đoạn code phụ
plt.plot(X_new, y_proba[:, 0], "b--", linewidth=2,
label="Not Iris virginica proba")
plt.plot(X_new, y_proba[:, 1], "g-", linewidth=2, label="Iris virginica proba")
plt.plot([decision_boundary, decision_boundary], [0, 1], "k:", linewidth=2,
label="Decision boundary")
# đoạn code phụ – làm đẹp Hình 4–23
plt.arrow(x=decision_boundary, y=0.08, dx=-0.3, dy=0,
head_width=0.05, head_length=0.1, fc="b", ec="b")
plt.arrow(x=decision_boundary, y=0.92, dx=0.3, dy=0,
head_width=0.05, head_length=0.1, fc="g", ec="g")
plt.plot(X_train[y_train == 0], y_train[y_train == 0], "bs")
plt.plot(X_train[y_train == 1], y_train[y_train == 1], "g^")
plt.xlabel("Petal width (cm)")
plt.ylabel("Probability")
plt.legend(loc="center left")
plt.axis([0, 3, -0.02, 1.02])
plt.grid()
plt.show()

Đường ranh giới quyết định (Decision Boundary) nằm ở điểm mà xác suất bằng 50%.
decision_boundary
np.float64(1.6516516516516517)
log_reg.predict([[1.7], [1.5]])
array([ True, False])
Biểu đồ dưới đây minh họa ranh giới quyết định tuyến tính trên không gian 2 chiều (sử dụng 2 đặc trưng).
# đoạn code phụ – tạo Hình 4–24
X = iris.data[["petal length (cm)", "petal width (cm)"]].values
y = iris.target_names[iris.target] == 'virginica'
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
log_reg = LogisticRegression(C=2, random_state=42)
log_reg.fit(X_train, y_train)
# cho biểu đồ đường đồng mức (contour plot)
x0, x1 = np.meshgrid(np.linspace(2.9, 7, 500).reshape(-1, 1),
np.linspace(0.8, 2.7, 200).reshape(-1, 1))
X_new = np.c_[x0.ravel(), x1.ravel()]
y_proba = log_reg.predict_proba(X_new)
zz = y_proba[:, 1].reshape(x0.shape)
# cho đường ranh giới quyết định
left_right = np.array([2.9, 7])
boundary = -((log_reg.coef_[0, 0] * left_right + log_reg.intercept_[0])
/ log_reg.coef_[0, 1])
plt.figure(figsize=(10, 4))
plt.plot(X_train[y_train == 0, 0], X_train[y_train == 0, 1], "bs")
plt.plot(X_train[y_train == 1, 0], X_train[y_train == 1, 1], "g^")
contour = plt.contour(x0, x1, zz, cmap=plt.cm.brg)
plt.clabel(contour, inline=1)
plt.plot(left_right, boundary, "k--", linewidth=3)
plt.text(3.5, 1.27, "Not Iris virginica", color="b", ha="center")
plt.text(6.5, 2.3, "Iris virginica", color="g", ha="center")
plt.xlabel("Petal length")
plt.ylabel("Petal width")
plt.axis([2.9, 7, 0.8, 2.7])
plt.grid()
plt.show()

Softmax Regression
Logistic Regression có thể được tổng quát hóa để hỗ trợ nhiều lớp (multiclass classification), mà không cần phải huấn luyện và kết hợp nhiều bộ phân loại nhị phân. Phiên bản này được gọi là Softmax Regression (hoặc Multinomial Logistic Regression).
Ý tưởng rất đơn giản: với mỗi instance , mô hình sẽ tính toán một điểm số (score) cho mỗi lớp , sau đó áp dụng hàm Softmax cho các điểm số này để ước tính xác suất của từng lớp.
Chúng ta có thể sử dụng lớp LogisticRegression của Scikit-Learn và nó sẽ tự động chuyển sang chế độ Softmax nếu dữ liệu có nhiều hơn 2 lớp (hoặc ta có thể ép buộc bằng tham số multi_class="multinomial").
X = iris.data[["petal length (cm)", "petal width (cm)"]].values
y = iris["target"]
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
# C=30 là nghịch đảo của alpha (regularization), C càng lớn -> regularization càng ít
softmax_reg = LogisticRegression(C=30, random_state=42)
softmax_reg.fit(X_train, y_train)
LogisticRegression(C=30, random_state=42)
LogisticRegression(C=30, random_state=42)
softmax_reg.predict([[5, 2]])
array([2])
softmax_reg.predict_proba([[5, 2]]).round(2)
array([[0. , 0.04, 0.96]])
Biểu đồ dưới đây minh họa các vùng quyết định của Softmax Regression cho 3 loài hoa Iris.
# đoạn code phụ – tạo Hình 4–25
from matplotlib.colors import ListedColormap
custom_cmap = ListedColormap(["#fafab0", "#9898ff", "#a0faa0"])
x0, x1 = np.meshgrid(np.linspace(0, 8, 500).reshape(-1, 1),
np.linspace(0, 3.5, 200).reshape(-1, 1))
X_new = np.c_[x0.ravel(), x1.ravel()]
y_proba = softmax_reg.predict_proba(X_new)
y_predict = softmax_reg.predict(X_new)
zz1 = y_proba[:, 1].reshape(x0.shape)
zz = y_predict.reshape(x0.shape)
plt.figure(figsize=(10, 4))
plt.plot(X[y == 2, 0], X[y == 2, 1], "g^", label="Iris virginica")
plt.plot(X[y == 1, 0], X[y == 1, 1], "bs", label="Iris versicolor")
plt.plot(X[y == 0, 0], X[y == 0, 1], "yo", label="Iris setosa")
plt.contourf(x0, x1, zz, cmap=custom_cmap)
contour = plt.contour(x0, x1, zz1, cmap="hot")
plt.clabel(contour, inline=1)
plt.xlabel("Petal length")
plt.ylabel("Petal width")
plt.legend(loc="center left")
plt.axis([0.5, 7, 0, 3.5])
plt.grid()
plt.show()

Ôn tập
1. Nếu bạn có một tập huấn luyện với hàng triệu đặc trưng (features), thuật toán Hồi quy Tuyến tính nào là phù hợp?
Bạn nên sử dụng Stochastic Gradient Descent (SGD) hoặc Mini-batch Gradient Descent, hoặc có thể là Batch Gradient Descent nếu tập dữ liệu vừa vặn trong bộ nhớ RAM. Tuy nhiên, bạn không thể sử dụng Phương trình Pháp tuyến (Normal Equation) hoặc cách tiếp cận SVD vì độ phức tạp tính toán của chúng tăng rất nhanh (hơn cả bậc hai) theo số lượng đặc trưng.
2. Nếu các đặc trưng trong tập huấn luyện có tỉ lệ (scale) rất khác nhau, điều gì sẽ xảy ra? Bạn nên làm gì?
Nếu các đặc trưng có tỉ lệ khác nhau, hàm chi phí sẽ có hình dạng một cái bát kéo dài (elongated bowl). Do đó, các thuật toán Gradient Descent sẽ mất rất nhiều thời gian để hội tụ đến đáy. Để giải quyết vấn đề này, bạn nên chuẩn hóa (scale) dữ liệu trước khi huấn luyện mô hình. Lưu ý rằng Phương trình Pháp tuyến hoặc SVD không cần chuẩn hóa dữ liệu vẫn hoạt động tốt. Ngoài ra, các mô hình có Regularization (Ridge, Lasso) bắt buộc phải chuẩn hóa dữ liệu, nếu không các đặc trưng có giá trị lớn sẽ bị phạt nặng hơn các đặc trưng có giá trị nhỏ một cách bất công.
3. Gradient Descent có thể bị mắc kẹt tại cực trị địa phương (local minimum) khi huấn luyện Logistic Regression không?
Không. Hàm chi phí của Logistic Regression là hàm lồi (convex). Điều này đảm bảo rằng nếu bạn vẽ một đường thẳng giữa hai điểm bất kỳ trên đường cong, đường thẳng đó sẽ không bao giờ cắt đường cong. Do đó, nó chỉ có một cực trị toàn cục (global minimum).
4. Tất cả các thuật toán Gradient Descent có dẫn đến cùng một mô hình không?
Nếu bài toán tối ưu là lồi (như Linear Regression hay Logistic Regression) và learning rate không quá cao, tất cả các thuật toán Gradient Descent sẽ tiếp cận cực trị toàn cục và tạo ra các mô hình khá tương đồng. Tuy nhiên, trừ khi bạn giảm dần learning rate, SGD và Mini-batch GD sẽ không bao giờ thực sự hội tụ mà sẽ dao động quanh điểm tối ưu. Do đó, chúng sẽ tạo ra các mô hình hơi khác nhau một chút.
5. Nếu validation error tăng liên tục sau mỗi epoch, chuyện gì đang xảy ra?
Nếu validation error tăng liên tục, một khả năng là learning rate đang quá cao và thuật toán đang bị phân kỳ (diverging). Nếu training error cũng tăng, thì chắc chắn đây là vấn đề, và bạn cần giảm learning rate. Tuy nhiên, nếu training error không tăng (hoặc giảm), thì mô hình của bạn đang bị Overfitting tập huấn luyện, và bạn nên dừng việc huấn luyện (Early Stopping).
6. Có nên dừng ngay lập tức khi validation error tăng lên trong Mini-batch GD không?
Do tính ngẫu nhiên, cả SGD và Mini-batch GD không đảm bảo cải thiện sau mỗi lần lặp. Nếu bạn dừng ngay khi validation error tăng, bạn có thể dừng quá sớm (trước khi đạt tối ưu). Một cách tốt hơn là lưu lại mô hình sau mỗi khoảng thời gian; và nếu mô hình không cải thiện sau một khoảng thời gian dài, bạn có thể quay lại (revert) mô hình tốt nhất đã lưu trước đó.
7. Thuật toán GD nào đạt tới lân cận của giải pháp tối ưu nhanh nhất? Thuật toán nào thực sự hội tụ?
Stochastic Gradient Descent có bước lặp nhanh nhất (vì chỉ xét 1 mẫu một lần), nên nó thường đến vùng lân cận của tối ưu nhanh nhất. Tuy nhiên, chỉ có Batch Gradient Descent mới thực sự hội tụ (nếu đủ thời gian). SGD và Mini-batch GD sẽ dao động quanh điểm tối ưu trừ khi giảm learning rate dần dần.
8. Nếu validation error cao hơn nhiều so với training error (Overfitting), bạn nên làm gì?
Đây là hiện tượng Overfitting. Cách khắc phục:
- Giảm bậc đa thức (nếu dùng Polynomial Regression).
- Thêm Regularization (Ridge, Lasso).
- Tăng kích thước tập dữ liệu huấn luyện.
9. Nếu cả training error và validation error đều cao và gần bằng nhau (Underfitting), bạn nên làm gì?
Đây là hiện tượng Underfitting (High Bias). Bạn nên giảm tham số regularization (nếu đang dùng) hoặc tăng độ phức tạp của mô hình.
10. Tại sao nên dùng Ridge thay vì Linear Regression? Lasso thay vì Ridge? Elastic Net thay vì Lasso?
- Ridge vs Linear: Một mô hình có regularization thường tốt hơn không có. Ridge là lựa chọn mặc định tốt.
- Lasso vs Ridge: Dùng Lasso nếu bạn nghi ngờ chỉ có vài đặc trưng thực sự quan trọng (Lasso giúp loại bỏ đặc trưng thừa).
- Elastic Net vs Lasso: Elastic Net thường ổn định hơn Lasso, đặc biệt khi các đặc trưng tương quan mạnh hoặc số đặc trưng lớn hơn số mẫu.
11. Phân loại ảnh trong nhà/ngoài trời và ban ngày/ban đêm?
Vì đây không phải là các lớp loại trừ lẫn nhau (ảnh có thể là “trong nhà + ban đêm”), bạn nên huấn luyện hai bộ phân loại Logistic Regression riêng biệt (một cho trong nhà/ngoài trời, một cho ngày/đêm) thay vì dùng một mô hình Softmax.
Bài tập thực hành: Cài đặt Batch Gradient Descent với Early Stopping cho Softmax Regression
Yêu cầu: Tự cài đặt thuật toán Batch Gradient Descent với Early Stopping cho Softmax Regression mà không dùng Scikit-Learn (chỉ dùng NumPy). Sử dụng nó trên tập dữ liệu Iris.
Hãy bắt đầu bằng việc tải dữ liệu:
X = iris.data[["petal length (cm)", "petal width (cm)"]].values
y = iris["target"].values
Chúng ta cần thêm bias term () cho mỗi instance. Dù có thể dùng Scikit-Learn, nhưng để hiểu sâu hơn, ta sẽ làm thủ công bằng NumPy:
X_with_bias = np.c_[np.ones(len(X)), X]
Tiếp theo, chia tập dữ liệu thành train, validation và test set một cách thủ công:
test_ratio = 0.2
validation_ratio = 0.2
total_size = len(X_with_bias)
test_size = int(total_size * test_ratio)
validation_size = int(total_size * validation_ratio)
train_size = total_size - test_size - validation_size
rng = np.random.default_rng(seed=42)
rnd_indices = rng.permutation(total_size)
X_train = X_with_bias[rnd_indices[:train_size]]
y_train = y[rnd_indices[:train_size]]
X_valid = X_with_bias[rnd_indices[train_size:-test_size]]
y_valid = y[rnd_indices[train_size:-test_size]]
X_test = X_with_bias[rnd_indices[-test_size:]]
y_test = y[rnd_indices[-test_size:]]
Hiện tại y chứa các chỉ số lớp (0, 1, 2). Để huấn luyện Softmax, chúng ta cần chuyển đổi sang dạng One-hot Encoding. Ví dụ: lớp 1 sẽ thành [0, 1, 0].
def to_one_hot(y):
# Tạo ma trận chéo với các số 1, sau đó dùng indexing để lấy dòng tương ứng
return np.diag(np.ones(y.max() + 1))[y]
# Kiểm tra thử
y_train[:10]
array([1, 1, 2, 0, 2, 2, 1, 2, 2, 0])
to_one_hot(y_train[:10])
array([[0., 1., 0.],
[0., 1., 0.],
[0., 0., 1.],
[1., 0., 0.],
[0., 0., 1.],
[0., 0., 1.],
[0., 1., 0.],
[0., 0., 1.],
[0., 0., 1.],
[1., 0., 0.]])
Áp dụng One-hot encoding cho toàn bộ các tập đích:
Y_train_one_hot = to_one_hot(y_train)
Y_valid_one_hot = to_one_hot(y_valid)
Y_test_one_hot = to_one_hot(y_test)
Bây giờ chúng ta sẽ chuẩn hóa (scale) dữ liệu đầu vào. Đây là bước quan trọng giúp GD hội tụ nhanh hơn.
mean = X_train[:, 1:].mean(axis=0)
std = X_train[:, 1:].std(axis=0)
# Chuẩn hóa (trừ mean, chia std), bỏ qua cột bias đầu tiên
X_train[:, 1:] = (X_train[:, 1:] - mean) / std
X_valid[:, 1:] = (X_valid[:, 1:] - mean) / std
X_test[:, 1:] = (X_test[:, 1:] - mean) / std
Hãy cài đặt hàm Softmax:
def softmax(logits):
exps = np.exp(logits)
exp_sums = exps.sum(axis=1, keepdims=True)
return exps / exp_sums
Thiết lập kích thước đầu vào và đầu ra:
n_inputs = X_train.shape[1] # == 3 (2 features + 1 bias)
n_outputs = len(np.unique(y_train)) # == 3 (3 loài hoa)
Đây là phần khó nhất: Vòng lặp huấn luyện. Chúng ta sẽ sử dụng hàm mất mát Cross Entropy và tính gradient của nó.
Công thức Gradient cho Softmax:
eta = 0.5 # learning rate
n_epochs = 5001
m = len(X_train)
epsilon = 1e-5 # tránh log(0)
rng = np.random.default_rng(seed=42)
Theta = rng.standard_normal((n_inputs, n_outputs))
for epoch in range(n_epochs):
logits = X_train @ Theta
Y_proba = softmax(logits)
# In ra loss sau mỗi 1000 epochs
if epoch % 1000 == 0:
Y_proba_valid = softmax(X_valid @ Theta)
xentropy_losses = -(Y_valid_one_hot * np.log(Y_proba_valid + epsilon))
print(epoch, xentropy_losses.sum(axis=1).mean())
# Tính Gradient
error = Y_proba - Y_train_one_hot
gradients = 1 / m * X_train.T @ error
# Cập nhật Theta
Theta = Theta - eta * gradients
0 2.973977344302766
1000 0.09313918303944396
2000 0.0890469894612578
3000 0.08847558719791132
4000 0.08861927669821175
5000 0.08894346006981158
Kiểm tra bộ tham số đã huấn luyện:
Theta
array([[-1.23843975, 5.9045082 , -4.65088428],
[-6.53446858, -2.85065468, 7.07247328],
[-6.919744 , -0.36831947, 7.08286012]])
Đánh giá độ chính xác trên tập validation:
logits = X_valid @ Theta
Y_proba = softmax(logits)
y_predict = Y_proba.argmax(axis=1)
accuracy_score = (y_predict == y_valid).mean()
accuracy_score
np.float64(0.9333333333333333)
Mô hình đạt độ chính xác ~93.3%. Hãy thử thêm L2 Regularization để xem có cải thiện được không.
eta = 0.5
n_epochs = 5001
m = len(X_train)
epsilon = 1e-5
alpha = 0.01 # tham số regularization
rng = np.random.default_rng(seed=42)
Theta = rng.standard_normal((n_inputs, n_outputs))
for epoch in range(n_epochs):
logits = X_train @ Theta
Y_proba = softmax(logits)
if epoch % 1000 == 0:
Y_proba_valid = softmax(X_valid @ Theta)
xentropy_losses = -(Y_valid_one_hot * np.log(Y_proba_valid + epsilon))
# Thêm L2 loss
l2_loss = 1 / 2 * (Theta[1:] ** 2).sum()
total_loss = xentropy_losses.sum(axis=1).mean() + alpha * l2_loss
print(epoch, total_loss.round(4))
error = Y_proba - Y_train_one_hot
gradients = 1 / m * X_train.T @ error
# Thêm đạo hàm của L2 penalty vào gradient (trừ bias term)
gradients += np.r_[np.zeros([1, n_outputs]), alpha * Theta[1:]]
Theta = Theta - eta * gradients
0 3.0065
1000 0.2711
2000 0.2711
3000 0.2711
4000 0.2711
5000 0.2711
logits = X_valid @ Theta
Y_proba = softmax(logits)
y_predict = Y_proba.argmax(axis=1)
accuracy_score = (y_predict == y_valid).mean()
accuracy_score
np.float64(0.9666666666666667)
Tuyệt vời! Độ chính xác đã tăng lên ~96.7% nhờ Regularization.
Cuối cùng, hãy thêm Early Stopping.
eta = 0.5
n_epochs = 50_001
m = len(X_train)
epsilon = 1e-5
C = 100 # Nghịch đảo của alpha
best_loss = np.inf
rng = np.random.default_rng(seed=42)
Theta = rng.standard_normal((n_inputs, n_outputs))
for epoch in range(n_epochs):
logits = X_train @ Theta
Y_proba = softmax(logits)
# Tính loss trên tập validation
Y_proba_valid = softmax(X_valid @ Theta)
xentropy_losses = -(Y_valid_one_hot * np.log(Y_proba_valid + epsilon))
l2_loss = 1 / 2 * (Theta[1:] ** 2).sum()
total_loss = xentropy_losses.sum(axis=1).mean() + 1 / C * l2_loss
if epoch % 1000 == 0:
print(epoch, total_loss.round(4))
# Kiểm tra Early Stopping
if total_loss < best_loss:
best_loss = total_loss
else:
print(epoch - 1, best_loss.round(4))
print(epoch, total_loss.round(4), "early stopping!")
break
error = Y_proba - Y_train_one_hot
gradients = 1 / m * X_train.T @ error
gradients += np.r_[np.zeros([1, n_outputs]), 1 / C * Theta[1:]]
Theta = Theta - eta * gradients
0 3.0065
402 0.2711
403 0.2711 early stopping!
logits = X_valid @ Theta
Y_proba = softmax(logits)
y_predict = Y_proba.argmax(axis=1)
accuracy_score = (y_predict == y_valid).mean()
accuracy_score
np.float64(0.9666666666666667)
Chúng ta vẫn giữ được độ chính xác cao nhưng thời gian huấn luyện ngắn hơn nhiều.
Hãy vẽ biểu đồ dự đoán trên toàn bộ không gian dữ liệu:
custom_cmap = mpl.colors.ListedColormap(['#fafab0', '#9898ff', '#a0faa0'])
x0, x1 = np.meshgrid(np.linspace(0, 8, 500).reshape(-1, 1),
np.linspace(0, 3.5, 200).reshape(-1, 1))
X_new = np.c_[x0.ravel(), x1.ravel()]
X_new = (X_new - mean) / std
X_new_with_bias = np.c_[np.ones(len(X_new)), X_new]
logits = X_new_with_bias @ Theta
Y_proba = softmax(logits)
y_predict = Y_proba.argmax(axis=1)
zz1 = Y_proba[:, 1].reshape(x0.shape)
zz = y_predict.reshape(x0.shape)
plt.figure(figsize=(10, 4))
plt.plot(X[y == 2, 0], X[y == 2, 1], "g^", label="Iris virginica")
plt.plot(X[y == 1, 0], X[y == 1, 1], "bs", label="Iris versicolor")
plt.plot(X[y == 0, 0], X[y == 0, 1], "yo", label="Iris setosa")
plt.contourf(x0, x1, zz, cmap=custom_cmap)
contour = plt.contour(x0, x1, zz1, cmap="hot")
plt.clabel(contour, inline=1)
plt.xlabel("Petal length")
plt.ylabel("Petal width")
plt.legend(loc="upper left")
plt.axis([0, 7, 0, 3.5])
plt.grid()
plt.show()

Cuối cùng, kiểm tra trên tập test:
logits = X_test @ Theta
Y_proba = softmax(logits)
y_predict = Y_proba.argmax(axis=1)
accuracy_score = (y_predict == y_test).mean()
accuracy_score
np.float64(0.9666666666666667)