Bỏ qua để đến nội dung

Overfitting và Underfitting

Sau bài này, bạn sẽ:

  • Nhận ra underfitting (thiếu khớp) và overfitting (quá khớp) chỉ bằng hai con số: MSE train và MSE test.
  • Hiểu vì sao hồi quy đa thức (polynomial regression) vẫn là hồi quy tuyến tính, và vì sao bậc (degree) của đa thức quyết định mô hình linh hoạt tới đâu.
  • Giải thích đường cong sai số hình chữ U bằng sự đánh đổi bias–variance (bias–variance trade-off), ở mức trực giác.
  • Chọn bậc bằng kiểm định chéo (cross-validation) với cross_val_score, và biết nên sửa mô hình theo hướng nào.

Ba bạn An, Bình và Chi cùng nhận 16 điểm trên giấy kẻ ô. Đề bài: vẽ một đường cho thấy quy luật của các điểm, để sau này đoán yy ở những xx mới.

  • An đặt thước, kẻ một đường thẳng xuyên qua đám điểm.
  • Bình vẽ một đường cong mềm: lên một nhịp, xuống một nhịp, đi gần các điểm nhưng không chạm hết.
  • Chi cẩn thận nhất: đường của Chi đi qua đúng cả 16 điểm, không lệch điểm nào.

Nếu chấm trên chính 16 điểm đó, Chi được điểm tuyệt đối. Nhưng đề bài là đoán điểm mới. Ai sẽ đoán tốt nhất?

Ngoài đời, bạn không bao giờ biết quy luật thật nằm sau dữ liệu. Bài này thì cố ý biết. Các điểm được sinh từ đường sin y=sin⁡(2πx)y = \sin(2\pi x) với xx từ 0 tới 1, rồi cộng thêm nhiễu (noise) ngẫu nhiên. Đường sin là “đáp án”. Nhiễu là phần tình cờ, không quy luật nào đoán trước được.

Có tất cả 24 điểm. Xếp theo xx tăng dần, cứ 3 điểm thì lấy 1 làm điểm test: 16 điểm train, 8 điểm test. Bài Tổng quan chia ngẫu nhiên, còn bài này chia cố định để mọi điểm test nằm xen giữa các điểm train. Như vậy sai số bạn thấy không lẫn với lỗi ngoại suy (extrapolation), một chuyện khác hẳn (xem bài Hồi quy tuyến tính).

Xem bảng 24 điểm
16 điểm train, tự sinh từ đường sin y = sin(2πx) cộng nhiễu. Không phải số liệu thật.
xy
0.030.15
0.121.06
0.150.84
0.230.96
0.260.83
0.350.94
0.410.30
0.47−0.09
0.51−0.17
0.60−0.32
0.64−0.68
0.73−0.84
0.77−0.28
0.85−0.82
0.90−0.53
0.98−0.02
8 điểm test, sinh cùng cách với tập train và không dùng khi khớp.
xy
0.060.33
0.180.66
0.320.93
0.430.43
0.55−0.42
0.67−1.11
0.80−1.18
0.94−0.45

Đường thẳng của An, đường cong của Bình và đường của Chi lần lượt là đa thức bậc 1, bậc 3 và bậc 15, cùng khớp trên 16 điểm train. Ô code đầu tiên sẽ vẽ chúng. Sai số đo bằng MSE:

ĐườngBậcMSE trainMSE test
An: đường thẳng10.16630.2732
Bình: đường cong mềm30.03770.0754
Chi: đi qua mọi điểm150.0000khoảng 133
  • An sai nhiều trên cả hai tập. Đường thẳng quá cứng, không uốn theo sóng được. Đây là underfitting: mô hình quá đơn giản so với quy luật.
  • Chi không sai điểm train nào, nhưng sai tới mức vô lý trên điểm test. Để đi qua đủ 16 điểm, đường của Chi uốn theo cả nhiễu, và giữa các điểm thì vọt lên, cắm xuống. Đây là overfitting: mô hình học thuộc cả những chi tiết tình cờ của tập train.
  • Bình có MSE test nhỏ nhất. Đường của Bình đủ linh hoạt để theo sóng, nhưng không đủ để chạy theo từng hạt nhiễu.

Bài toán của bài này là: chọn độ linh hoạt của mô hình sao cho đủ để bắt được quy luật, mà không đủ để học thuộc nhiễu, khi trong tay chỉ có dữ liệu.

Mô hình đa thức (polynomial) bậc dd dự đoán:

y^=w1x+w2x2+⋯+wdxd+b\hat{y} = w_1x + w_2x^2 + \dots + w_dx^d + b
  • xx là đầu vào, y^\hat{y} là giá trị dự đoán.
  • dd là bậc, số mũ lớn nhất. Bậc 1 là đường thẳng y^=w1x+b\hat{y} = w_1x + b. Bậc 3 thêm x2x^2 và x3x^3.
  • w1,…,wdw_1, \dots, w_d là các trọng số, bb là hệ số tự do. Tất cả có d+1d + 1 tham số.

Thoạt nhìn, đây là một đường cong, không còn là hồi quy tuyến tính. Nhưng hãy coi mỗi lũy thừa là một đặc trưng (feature) riêng: x1=xx_1 = x, x2=x2x_2 = x^2, …, xd=xdx_d = x^d. Khi đó mô hình viết lại thành

y^=w1x1+w2x2+⋯+wdxd+b=w⊤x+b\hat{y} = w_1x_1 + w_2x_2 + \dots + w_dx_d + b = \mathbf{w}^\top\mathbf{x} + b

với x=[x,x2,…,xd]⊤\mathbf{x} = [x, x^2, \dots, x^d]^\top. Đây đúng là hồi quy tuyến tính nhiều đặc trưng, với số đặc trưng n=dn = d, dù đầu vào gốc chỉ có một biến xx.

Mô hình cong theo xx nhưng tuyến tính theo tham số, như hộp Lỗi hay gặp đầu tiên của bài Hồi quy tuyến tính đã nói: mỗi xjx^j chỉ là một cột số tính sẵn. Vì vậy ma trận thiết kế có hàng thứ ii là [ x(i),(x(i))2,…,(x(i))d,1 ]\left[\,x^{(i)}, \left(x^{(i)}\right)^2, \dots, \left(x^{(i)}\right)^d, 1\,\right], và vẫn giải bằng bình phương tối thiểu.

Bậc 15 trên 16 điểm. Bậc 15 có 15+1=1615 + 1 = 16 tham số, đúng bằng số điểm train. Đòi đường đi qua cả 16 điểm cho ta 16 phương trình bậc nhất với 16 ẩn. Khi các x(i)x^{(i)} khác nhau đôi một, hệ này có đúng một nghiệm, nên đường của Chi đi qua mọi điểm và MSE train bằng 0. Bậc cao hơn nữa thì số tham số vượt số điểm, và có vô số nghiệm (xem mục “Khi nào nghiệm không duy nhất” ở bài Hồi quy tuyến tính). Vì vậy bài này dừng ở bậc 15.

Mô hình chỉ được khớp trên tập train, có mtrain=16m_{\text{train}} = 16 điểm. Sau đó, cùng một công thức MSE được tính trên hai tập khác nhau:

Ltrain=1mtrain∑i∈train(y^(i)−y(i))2Ltest=1mtest∑i∈test(y^(i)−y(i))2\begin{aligned} \mathcal{L}_{\text{train}} &= \frac{1}{m_{\text{train}}}\sum_{i \in \text{train}}\left(\hat{y}^{(i)} - y^{(i)}\right)^2 \\ \mathcal{L}_{\text{test}} &= \frac{1}{m_{\text{test}}}\sum_{i \in \text{test}}\left(\hat{y}^{(i)} - y^{(i)}\right)^2 \end{aligned}

Dòng đầu là MSE train, trên 16 điểm đã dùng để khớp. Dòng thứ hai là MSE test, trên mtest=8m_{\text{test}} = 8 điểm mà mô hình chưa thấy. Huấn luyện làm Ltrain\mathcal{L}_{\text{train}} nhỏ nhất, nhưng thứ bạn thật sự muốn nhỏ là Ltest\mathcal{L}_{\text{test}}.

Vì sao MSE train không bao giờ tăng theo bậc

Phần tiêu đề “Vì sao MSE train không bao giờ tăng theo bậc”

Lấy một đa thức bậc dd bất kỳ. Thêm số hạng wd+1xd+1w_{d+1}x^{d+1} với wd+1=0w_{d+1} = 0, bạn được một đa thức bậc d+1d + 1 cho đúng cùng dự đoán. Vậy mọi đường mà bậc dd vẽ được, bậc d+1d + 1 cũng vẽ được, và còn vẽ được thêm nhiều đường khác.

Khớp bậc d+1d + 1 là tìm đường có MSE train nhỏ nhất trong tập rộng hơn này. Đường tốt nhất của bậc dd nằm trong tập đó, nên kết quả của bậc d+1d + 1 chỉ có thể bằng hoặc nhỏ hơn:

Ltrain(d+1)≤Ltrain(d)\mathcal{L}_{\text{train}}(d + 1) \le \mathcal{L}_{\text{train}}(d)

MSE test thì không có bảo đảm nào như vậy, vì điểm test không tham gia khớp. Đây là gốc rễ của mọi chuyện trong bài.

Khớp lần lượt bậc 1 tới 15 rồi vẽ hai MSE theo bậc, bạn được hai đường rất khác nhau (ô code thứ hai in đủ 15 bậc):

Bậc ddMSE trainMSE test
10.16630.2732
20.16240.2766
30.03770.0754
50.03010.0969
80.02160.1215
100.01890.3286
120.00133.5439
150.0000khoảng 133
  • MSE train đi xuống suốt, đúng như vừa chứng minh.
  • MSE test tăng nhẹ ở bậc 2, tới bậc 3 mới giảm mạnh và đạt nhỏ nhất, rồi tăng dần, và tăng vọt từ khoảng bậc 10. Vẽ ra, nó có hình chữ U.

Bậc 2 gần như không hơn bậc 1. Vòng sóng sin đối xứng qua điểm (0.5,0)(0.5, 0): nửa trái là nửa phải lật ngược. Parabol x2x^2 đối xứng theo kiểu khác hẳn, nên không giúp được gì. Phải có x3x^3, đường mới uốn được hình chữ S của sóng.

Từ đó có hai định nghĩa bằng lời:

  • Underfitting: mô hình quá đơn giản. MSE train đã lớn, và MSE test cũng lớn. Đó là phần bên trái đáy chữ U.
  • Overfitting: mô hình quá linh hoạt. MSE train nhỏ, nhưng MSE test lớn hơn hẳn và tệ hơn mức có thể đạt được. Đó là phần bên phải đáy chữ U.

Chữ “bias” ở mục này không phải hệ số tự do bb, dù tiếng Anh dùng cùng một chữ. Ở đây nó là độ chệch (bias) của mô hình. Để tránh nhầm, bài này luôn gọi bb là “hệ số tự do”.

Hình dung bạn lặp lại thí nghiệm nhiều lần: mỗi lần lấy 16 điểm train mới từ cùng đường sin, với nhiễu mới, rồi khớp lại cùng một bậc. Sai số trên dữ liệu mới đến từ ba nguồn. Hai nguồn đầu đổi theo bậc bạn chọn, nguồn thứ ba là nhiễu, không bỏ được:

  • Độ chệch (bias): sai lệch có hệ thống, do họ hàm quá cứng. Lấy bao nhiêu bộ dữ liệu thì đường thẳng cũng không uốn theo sóng được: lần nào nó cũng quá thấp ở đỉnh sóng, quá cao ở đáy sóng.
  • Variance (phương sai của mô hình): mức đường khớp thay đổi khi đổi bộ dữ liệu train. Đường thẳng gần như đứng yên. Đa thức bậc cao thì uốn theo nhiễu riêng của từng bộ, nên mỗi lần một hình, nhất là ở hai mép.
  • Nhiễu: phần tình cờ trong từng điểm dữ liệu. Không mô hình nào đoán trước được nó.

Tăng bậc làm độ chệch giảm, vì họ hàm rộng ra, chứa được những đường gần đường sin hơn. Nhưng variance tăng, vì mô hình có thêm chỗ để uốn theo nhiễu. Bậc thấp thì độ chệch lấn át: underfitting. Bậc cao thì variance lấn át: overfitting. Đáy chữ U là chỗ cân bằng.

Nhiễu cũng giải thích vì sao MSE train dễ đánh lừa. Ngay cả đường sin thật, nếu dùng làm mô hình, cũng có MSE trên 16 điểm train khoảng 0.06. Mô hình nào khớp trên tập train cũng nuốt một ít nhiễu, nên xuống dưới mức đó: bậc 3 có MSE train 0.0377. Bậc 15 thì nuốt trọn, MSE train xuống tận 0. Phần nhiễu đã nuốt không giúp gì với điểm mới, nên nó hiện ra ở MSE test.

Muốn biết mô hình đang ở đâu, hãy đặt MSE train cạnh MSE trên dữ liệu chưa dùng để khớp. “Lớn” hay “nhỏ” là so với mức sai số tốt nhất có thể đạt được: mức nhiễu nếu ước lượng được, hoặc sai số của mô hình tốt nhất bạn biết cho bài toán đó.

MSE trainMSE trên dữ liệu mớiChẩn đoánHướng sửa
LớnLớn, gần MSE trainUnderfittingMô hình linh hoạt hơn: tăng bậc, thêm đặc trưng
NhỏLớn hơn hẳn MSE trainOverfittingThêm dữ liệu, hạ bậc, regularization
NhỏNhỏ, gần MSE trainVừa khớpGiữ nguyên

Ba hướng sửa overfitting:

  • Thêm dữ liệu. Với 16 điểm, đa thức bậc 15 bị buộc đi qua từng điểm. Với 1 000 điểm, nó không còn uốn theo từng hạt nhiễu được. Dữ liệu thêm phải đại diện cho dữ liệu thật, không phải bản sao của dữ liệu cũ.
  • Giảm độ linh hoạt. Hạ bậc, hoặc bớt đặc trưng.
  • Regularization. Giữ bậc cao, nhưng cộng vào hàm mất mát một khoản phạt khi trọng số lớn. Để uốn kịp các điểm, đa thức bậc 15 ở trên có hệ số lớn tới hàng chục tỉ. Phạt trọng số lớn kéo nó về một đường mềm hơn. Bài Ridge, Lasso, Elastic Net sẽ nói kỹ.

Chọn bậc: tập kiểm định và kiểm định chéo

Phần tiêu đề “Chọn bậc: tập kiểm định và kiểm định chéo”

Cách dễ nghĩ nhất là chọn bậc có MSE test nhỏ nhất. Nhưng đó chính là lỗi “chọn mô hình bằng tập test” ở bài Tổng quan về Học máy: bậc được chọn sẽ hợp với đúng 8 điểm test này, và MSE test thành lạc quan. Bậc là một siêu tham số (hyperparameter): con số bạn đặt trước khi khớp, không phải con số mà việc khớp tìm ra. Siêu tham số phải được chọn mà không chạm vào tập test.

Cách thứ nhất là giữ lại một phần tập train làm tập kiểm định (validation set): khớp trên phần còn lại, chấm trên tập kiểm định, chọn bậc tốt nhất. Nhưng tập train chỉ có 16 điểm. Giữ 4 điểm làm tập kiểm định thì còn 12 điểm để khớp, và như bài Tổng quan đã cho thấy, điểm số trên vài mẫu dao động theo cách chia.

Kiểm định chéo tận dụng dữ liệu tốt hơn. Với kk fold:

  1. Chia tập train thành kk phần gần bằng nhau, gọi là fold.
  2. Lần thứ jj (j=1,…,kj = 1, \dots, k): khớp trên k−1k - 1 fold còn lại, rồi tính MSE trên fold jj, gọi là Lj\mathcal{L}_j.
  3. Lấy trung bình kk con số đó.
CV=1k∑j=1kLj\text{CV} = \frac{1}{k}\sum_{j=1}^{k}\mathcal{L}_j

Mỗi điểm train được chấm đúng một lần. Làm vậy cho từng bậc, chọn bậc có CV nhỏ nhất, rồi khớp lại bậc đó trên cả tập train và chấm một lần trên tập test.

  • Một chiều, ít điểm. Ở đây bạn nhìn tận mắt đường cong uốn theo nhiễu. Với hàng chục đặc trưng thì không vẽ được, chỉ còn cách so các con số MSE.
  • Bậc chỉ là một cái núm. Mỗi loại mô hình có núm riêng: số láng giềng của KNN, độ sâu của cây quyết định (decision tree), số lớp của mạng nơ-ron. Hình chữ U thường vẫn hiện ra, chỉ đổi trục ngang.
  • Kiểm định chéo cũng dao động khi dữ liệu ít. Đổi cách chia fold có thể đổi bậc được chọn (cuối mục Ví dụ code).
  • Không thay được dữ liệu tốt. Nếu tập train không đại diện cho dữ liệu thật, mô hình vẫn đoán kém ngoài đời, dù kiểm định chéo đẹp đến đâu.

Giờ bạn tự kéo bậc. Hình dưới vẽ 16 điểm train (chấm đặc) và 8 điểm test (vòng rỗng), cùng đa thức đã khớp trên tập train. Thanh trượt đổi bậc dd từ 1 tới 15. Bên dưới là hai số MSE, và một biểu đồ nhỏ vẽ MSE train và MSE test theo bậc, có vạch dọc ở bậc đang chọn. Nút Hiện hàm thật bật đường sin không có nhiễu. Hãy thử:

  1. Lúc đầu d=1d = 1: đường thẳng, MSE train 0.1663, MSE test 0.2732. Hình ghi underfitting.
  2. Kéo lên d=3d = 3: đường uốn theo sóng, MSE test giảm còn 0.0754, nhỏ nhất trong 15 bậc. Hình hiện dấu ✓.
  3. Kéo tiếp tới d=8d = 8 rồi d=9d = 9: MSE train vẫn giảm (0.0216, rồi 0.0206), nhưng MSE test tăng (0.1215, rồi 0.1656). Ở bậc 9, đầu trái của đường cong bắt đầu chúi xuống ra khỏi khung.
  4. Kéo tới d=15d = 15: đường đi qua đúng mọi chấm đặc, MSE train 0.0000, MSE test 133.20. Từ bậc 11, MSE test lớn tới mức nằm ngoài biểu đồ nhỏ. Bật hàm thật để thấy đường bậc 15 khác xa nó thế nào.

Nhãn trên hình theo một mốc gần đúng: so với bậc 3, MSE train gấp đôi trở lên là underfitting, MSE test gấp đôi trở lên là overfitting. Đây là cách dẫn dắt, không phải định nghĩa: bậc 8 vẫn ghi “vừa khớp”, dù đường cong đã uốn khá mạnh ở hai mép.

Kéo thanh trượt để đổi bậc của đa thức. Đa thức chỉ được khớp trên các điểm train, còn các điểm test dùng để chấm điểm.
  • Điểm train
  • Điểm test
  • Đa thức đã khớp
  • Hàm thật (không có nhiễu)
MSE train0.1663
MSE test0.2732
  • MSE test vượt khung (lớn hơn 0.4)

Underfitting: đa thức quá đơn giản, cả MSE train lẫn MSE test đều lớn.

16 điểm train, x từ 0.03 tới 0.98, đi theo một vòng sóng: lên gần 1 khi x từ 0.1 tới 0.35, xuống gần −1 khi x từ 0.7 tới 0.85, rồi về gần 0. Các điểm lệch khỏi dạng sóng vì có nhiễu. 8 điểm test nằm xen giữa các điểm train, theo cùng dạng sóng. Dữ liệu được sinh từ đường sin y = sin(2πx) cộng nhiễu ngẫu nhiên.

MSE train giảm dần từ 0.1663 ở bậc 1 xuống 0.0000 ở bậc 15. MSE test nhỏ nhất ở bậc 3 (0.0754), rồi tăng tới 133.20 ở bậc 15.

Ba ô dưới đây chạy độc lập: ô nào cũng tự chép 24 điểm của bài. Ô chạy đầu tiên lâu hơn một chút, vì trình duyệt phải tải thêm scikit-learn.

Ô đầu tiên khớp ba đường của An, Bình và Chi, in hai MSE, rồi vẽ. make_pipeline nối hai bước thành một mô hình: PolynomialFeatures biến xx thành các cột x,x2,…,xdx, x^2, \dots, x^d, rồi LinearRegression khớp trên các cột đó. include_bias=False bỏ cột toàn số 1 mà PolynomialFeatures thêm theo mặc định, vì LinearRegression đã tự lo hệ số tự do bb. x_train[:, None] biến dãy 16 số thành bảng 16 hàng, 1 cột.

Khớp bậc 1, 3 và 15

import matplotlib.pyplot as plt
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures
# 24 điểm tự sinh từ y = sin(2πx) cộng nhiễu: 16 điểm train, 8 điểm test.
# dataset: sine-train
x_train = np.array([0.03, 0.12, 0.15, 0.23, 0.26, 0.35, 0.41, 0.47, 0.51, 0.6, 0.64, 0.73, 0.77, 0.85, 0.9, 0.98])
y_train = np.array([0.15, 1.06, 0.84, 0.96, 0.83, 0.94, 0.3, -0.09, -0.17, -0.32, -0.68, -0.84, -0.28, -0.82, -0.53, -0.02])
# dataset: sine-test
x_test = np.array([0.06, 0.18, 0.32, 0.43, 0.55, 0.67, 0.8, 0.94])
y_test = np.array([0.33, 0.66, 0.93, 0.43, -0.42, -1.11, -1.18, -0.45])
xs = np.linspace(0, 1, 201) # lưới x dày để vẽ đường cong cho mượt
plt.scatter(x_train, y_train, color="k", zorder=3, label="train")
plt.scatter(x_test, y_test, facecolors="none", edgecolors="C0",
zorder=3, label="test")
for d, style, color in [(1, ":", "C1"), (3, "-", "C2"), (15, "--", "C3")]:
# Biến x thành các cột x, x^2, ..., x^d, rồi hồi quy tuyến tính.
model = make_pipeline(PolynomialFeatures(d, include_bias=False),
LinearRegression())
model.fit(x_train[:, None], y_train) # chỉ học từ tập train
tr = mean_squared_error(y_train, model.predict(x_train[:, None]))
te = mean_squared_error(y_test, model.predict(x_test[:, None]))
print(f"degree {d:2d}: MSE train = {tr:.4f}, test = {te:.4f}")
plt.plot(xs, model.predict(xs[:, None]), style, color=color,
label=f"degree {d}")
# Đa thức bậc 15 vọt rất xa ở hai mép: cắt trục y cho dễ nhìn.
plt.ylim(-2, 2)
plt.xlabel("x")
plt.ylabel("y")
plt.legend()
plt.show()

Lần chạy đầu cần tải Python, khoảng 10 MB.

Ba dòng in ra khớp bảng ở phần Trực giác. Nhìn hình, bạn thấy rõ ba kiểu:

  • Đường bậc 1 chỉ bắt được xu hướng “đi xuống” chung, bỏ qua cả đỉnh lẫn đáy sóng.
  • Đường bậc 3 đi gần cả chấm đen lẫn vòng rỗng.
  • Đường bậc 15 đi đúng qua từng chấm đen, kể cả điểm (0.77,−0.28)(0.77, -0.28), điểm bị nhiễu đẩy xa đường sin nhất (đường sin thật ở đó quanh −0.99-0.99). Đường bậc 3 đi ở khoảng −0.79-0.79: chỉ bị điểm này kéo lên một chút, không đuổi theo nó.
  • Giữa hai điểm train đầu tiên (x=0.03x = 0.03 và x=0.12x = 0.12), đường bậc 15 vọt lên khỏi khung, đúng chỗ có điểm test x=0.06x = 0.06. Ở đó nó đoán hơn 30, trong khi yy thật là 0.33. Riêng điểm này chiếm phần lớn con số 133.

Ô thứ hai khớp lần lượt bậc 1 tới 15 và vẽ đường cong sai số.

MSE train và MSE test theo bậc

import matplotlib.pyplot as plt
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures
# 24 điểm tự sinh từ y = sin(2πx) cộng nhiễu: 16 điểm train, 8 điểm test.
# dataset: sine-train
x_train = np.array([0.03, 0.12, 0.15, 0.23, 0.26, 0.35, 0.41, 0.47, 0.51, 0.6, 0.64, 0.73, 0.77, 0.85, 0.9, 0.98])
y_train = np.array([0.15, 1.06, 0.84, 0.96, 0.83, 0.94, 0.3, -0.09, -0.17, -0.32, -0.68, -0.84, -0.28, -0.82, -0.53, -0.02])
# dataset: sine-test
x_test = np.array([0.06, 0.18, 0.32, 0.43, 0.55, 0.67, 0.8, 0.94])
y_test = np.array([0.33, 0.66, 0.93, 0.43, -0.42, -1.11, -1.18, -0.45])
degrees = range(1, 16)
mse_train, mse_test = [], []
print(" d train test")
for d in degrees:
model = make_pipeline(PolynomialFeatures(d, include_bias=False),
LinearRegression())
model.fit(x_train[:, None], y_train)
tr = mean_squared_error(y_train, model.predict(x_train[:, None]))
te = mean_squared_error(y_test, model.predict(x_test[:, None]))
mse_train.append(tr)
mse_test.append(te)
print(f"{d:2d} {tr:.4f} {te:.4f}")
plt.plot(degrees, mse_train, "o-", color="k", label="MSE train")
plt.plot(degrees, mse_test, "o--", color="C0", mfc="none",
label="MSE test")
# MSE test từ bậc 11 lớn hơn 0.4 nên nằm ngoài khung hình.
plt.ylim(0, 0.4)
plt.xlabel("Polynomial degree d")
plt.ylabel("MSE")
plt.legend()
plt.show()

Lần chạy đầu cần tải Python, khoảng 10 MB.

Cột MSE train giảm ở mọi bước, từ 0.1663 xuống 0.0000, đúng như chứng minh ở phần Lý thuyết. Cột MSE test nhỏ nhất ở bậc 3 (0.0754), tăng chậm tới 0.1215 ở bậc 8, rồi bùng lên: 0.3286 ở bậc 10, 3.5439 ở bậc 12. Trục tung bị cắt ở 0.4 để nhìn rõ đáy chữ U, nên từ bậc 11 trở đi MSE test nằm ngoài khung.

Ở bậc 14 và 15, hệ số của đa thức khổng lồ, nên chữ số thứ ba, thứ tư sau dấu chấm của MSE test có thể khác đôi chút giữa các máy hay phiên bản thư viện. Hãy đọc chúng là “khoảng 30” và “khoảng 133”.

Ô thứ ba chọn bậc bằng kiểm định chéo, chỉ trên 16 điểm train. KFold(n_splits=4, shuffle=True, random_state=0) xáo 16 điểm rồi chia thành 4 fold, mỗi fold 4 điểm. Mỗi lần khớp chỉ còn 12 điểm, nên ô chỉ thử bậc 1 tới 10.

Chọn bậc bằng kiểm định chéo

import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import KFold, cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures
# 24 điểm tự sinh từ y = sin(2πx) cộng nhiễu: 16 điểm train, 8 điểm test.
# dataset: sine-train
x_train = np.array([0.03, 0.12, 0.15, 0.23, 0.26, 0.35, 0.41, 0.47, 0.51, 0.6, 0.64, 0.73, 0.77, 0.85, 0.9, 0.98])
y_train = np.array([0.15, 1.06, 0.84, 0.96, 0.83, 0.94, 0.3, -0.09, -0.17, -0.32, -0.68, -0.84, -0.28, -0.82, -0.53, -0.02])
# dataset: sine-test
x_test = np.array([0.06, 0.18, 0.32, 0.43, 0.55, 0.67, 0.8, 0.94])
y_test = np.array([0.33, 0.66, 0.93, 0.43, -0.42, -1.11, -1.18, -0.45])
def poly(d):
return make_pipeline(PolynomialFeatures(d, include_bias=False),
LinearRegression())
# Chia 16 điểm train thành 4 fold, mỗi fold 4 điểm. Tập test không dùng.
cv = KFold(n_splits=4, shuffle=True, random_state=0)
cv_mse = {}
for d in range(1, 11):
scores = cross_val_score(poly(d), x_train[:, None], y_train, cv=cv,
scoring="neg_mean_squared_error")
cv_mse[d] = -scores.mean() # điểm là MSE mang dấu âm: đổi dấu lại
print(f"degree {d:2d}: CV MSE = {cv_mse[d]:.4f}")
best = min(cv_mse, key=cv_mse.get)
print(f"best degree: {best}")
# Chọn xong mới khớp lại trên cả tập train, rồi chấm một lần trên tập test.
model = poly(best).fit(x_train[:, None], y_train)
te = mean_squared_error(y_test, model.predict(x_test[:, None]))
print(f"test MSE: {te:.4f}")

Lần chạy đầu cần tải Python, khoảng 10 MB.

cross_val_score trả về 4 điểm số, mỗi fold một điểm. scikit-learn quy ước điểm càng lớn càng tốt, nên với MSE nó trả về số âm: neg_mean_squared_error. Code đổi dấu trung bình để được MSE kiểm định chéo.

  • MSE kiểm định chéo nhỏ nhất ở bậc 3, khoảng 0.0784. Đây là bậc được chọn, cùng bậc với đáy chữ U của MSE test, mà không cần nhìn tập test.
  • Từ bậc 7, MSE kiểm định chéo lên hàng đơn vị, và tới 830.2741 ở bậc 10. Bậc 10 có 11 tham số mà chỉ có 12 điểm để khớp, nên đường gần như bị buộc qua từng điểm và vọt mạnh ở chỗ trống của fold bị giữ lại.

Cuối cùng, ô khớp bậc 3 trên cả 16 điểm train và chấm một lần trên tập test: MSE test 0.0754, đúng như ô thứ hai. Con số này trung thực, vì tập test không tham gia việc chọn bậc.

Kết quả vẫn phụ thuộc cách chia. Đổi sang KFold(n_splits=5, shuffle=True, random_state=0) thì vẫn chọn bậc 3. Đổi sang LeaveOneOut() (import từ sklearn.model_selection), tức 16 fold, mỗi fold một điểm, thì chọn bậc 4: MSE kiểm định chéo 0.0774, so với 0.0793 của bậc 3. Hai bậc gần như ngang nhau. Với dữ liệu ít, đừng coi chênh lệch nhỏ là quan trọng.

Câu 1/4 Một mô hình có MSE train 0.0020.002 nhưng MSE test 2.52.5. Cách nào dưới đây ít có khả năng giúp nhất?
Câu 2/4 Bạn khớp đa thức bậc 1 tới 12 lên 25 điểm train, rồi giữ bậc có MSE train nhỏ nhất. Bạn sẽ được bậc nào?
Câu 3/4 Năm người, mỗi người có 16 điểm train riêng, sinh từ cùng đường sin cộng nhiễu (nhiễu khác nhau). Ai cũng khớp một đường thẳng, rồi một đa thức bậc 12. So năm đường của mỗi loại, bạn thấy gì?
Câu 4/4 Kiểm định chéo với scoring='neg_mean_squared_error' cho điểm trung bình: bậc 2 là −0.25-0.25, bậc 5 là −0.04-0.04, bậc 9 là −3.1-3.1. Nên chọn bậc nào?

Ý chính:

  • Hồi quy đa thức y^=w1x+w2x2+⋯+wdxd+b\hat{y} = w_1x + w_2x^2 + \dots + w_dx^d + b là hồi quy tuyến tính trên các đặc trưng x,x2,…,xdx, x^2, \dots, x^d: tuyến tính theo tham số, nên vẫn giải bằng bình phương tối thiểu. Bậc dd quyết định độ linh hoạt.
  • Tăng bậc thì MSE train không bao giờ tăng, còn MSE test giảm rồi tăng thành hình chữ U. Underfitting: cả hai MSE đều lớn. Overfitting: MSE train nhỏ, MSE test lớn hơn hẳn.
  • Bậc thấp có độ chệch (bias) cao: quá cứng để theo quy luật. Bậc cao có variance cao: uốn theo nhiễu riêng của từng bộ dữ liệu. Chữ “bias” ở đây khác hệ số tự do bb.
  • Bậc là một siêu tham số. Chọn nó bằng tập kiểm định hoặc kiểm định chéo (cross_val_score, nhớ đổi dấu neg_mean_squared_error), rồi khớp lại trên cả tập train và chấm tập test một lần. Sửa overfitting: thêm dữ liệu, hạ độ linh hoạt, regularization. Sửa underfitting: tăng độ linh hoạt.

Đọc thêm:

  • Vũ Hữu Tiệp (2017). Machine Learning cơ bản, Bài 15: Overfitting. Tiếng Việt, cũng dùng ví dụ hồi quy đa thức, rồi nói về tập kiểm định, kiểm định chéo và regularization. Ký hiệu hơi khác: NN là số mẫu, JJ là hàm mất mát.
  • scikit-learn developers (2007–2026). scikit-learn 1.8: Underfitting vs. Overfitting. Ví dụ chính thức với PolynomialFeatures và cross_val_score, trên dữ liệu khác bài này (hàm cos, 30 điểm, bậc 1, 4, 15).
  • scikit-learn developers (2007–2026). scikit-learn 1.8 User Guide: Cross-validation, mục 3.1. Cách dùng cross_val_score, KFold, LeaveOneOut, và vì sao cần tập kiểm định.
  • James, G., Witten, D., Hastie, T., Tibshirani, R. (2013). An Introduction to Statistical Learning (bản 1). Mục 2.2: đánh giá độ chính xác của mô hình, sự đánh đổi bias–variance. Chương 5: kiểm định chéo. Ký hiệu khác: nn là số mẫu, pp là số đặc trưng.
  • Bishop, C. M. (2006). Pattern Recognition and Machine Learning, Springer. Mục 1.1: khớp đa thức lên dữ liệu sinh từ sin⁡(2πx)\sin(2\pi x) cộng nhiễu, ví dụ kinh điển mà bài này mượn ý.

Nội dung theo giấy phép CC BY-NC-SA 4.0. Mã nguồn theo giấy phép MIT.Giới thiệu