Python - Bài 35: Overfitting

🔬 Bài 35: Overfitting — nhìn tận mắt

Chặng 8 · Học máy thực chiến (Bài 31 → 42) · đây là bài 5/12 của chặng.

🎯 Mục tiêu: gọi đúng tên hiện tượng đã bắt quả tang ở Bài 34, và nhận ra nó bằng mắt lẫn bằng con số.

👥 Cần biết trước: Bài 32 (column_stack) và Bài 34 (train_test_split). Bài này không dùng thêm công cụ nào mới — chỉ có hai khái niệm mới phải nhớ.

📖 Cách đọc 3 lớp: mỗi phần có ba khối màu song song — 🟢 Lớp A trực giác, 🔵 Lớp B cơ chế, 🟣 Lớp C nghiên cứu.

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

  • Biết cách làm cho mô hình đường thẳng vẽ được đường cong — chỉ bằng column_stack đã học.
  • Gọi đúng tên hai căn bệnh trái ngược: underfitting (quá đơn giản) và overfitting (quá phức tạp).
  • Nhận ra dấu hiệu của overfitting
  • Hiểu vì sao “mô hình mạnh hơn” không đồng nghĩa với “mô hình tốt hơn”.

Hãy nhớ lại ở Bài 34, ta nhận thấy một hiện tượng là mô hình đạt điểm tuyệt đối trên đề đã ôn nhưng sai gấp 22 lần trên đề mới.

Tên nó là overfitting — tạm dịch khớp quá mức. Và hôm nay bạn sẽ không chỉ nghe tên: bạn sẽ thấy nó xuất hiện dần dần, từ chỗ chưa có gì cho tới lúc phá nát mô hình, chỉ bằng cách vặn một núm duy nhất.

1. Cho đường thẳng biết uốn cong

🟢 Lớp A · Trực giác

Có một vấn đề cần giải quyết trước. Muốn thấy overfitting, ta cần một mô hình có thể phức tạp dần lên. Nhưng cả chặng tới giờ ta chỉ có LinearRegression — nó vẽ đường thẳng, mà đường thẳng thì làm gì có mức phức tạp nào để vặn?

Quay lại bảng dữ liệu, nhớ lại Bài 32: mỗi cột là một đặc trưng. Ta tạo thêm cột từ cột đã có:

  • Cột 1: x
  • Cột 2: x² — lấy cột 1 bình phương lên
  • Cột 3: x³… và cứ thế

Mô hình vẫn làm đúng một việc quen thuộc: nhân mỗi cột với một số rồi cộng lại. Nhưng vì trong đó có x² và x³, kết quả vẽ ra lại là một đường cong.

Càng thêm nhiều cột lũy thừa, đường cong càng uốn éo được nhiều chỗ. Số cột chính là núm vặn độ phức tạp mà ta cần.

🔵 Lớp B · Cơ chế

Không cần công cụ mới — đúng column_stack của Bài 32:

def tao_cot(x, bac):
    """bac=1 -> [x]  |  bac=3 -> [x, x^2, x^3]"""
    return np.column_stack([x**k for k in range(1, bac + 1)])

X = tao_cot(gio, 3)
print(X.shape)          # (14, 3)  - 14 mau, 3 dac trung

mo_hinh = LinearRegression().fit(X, y)
print(mo_hinh.coef_)     # 3 so, mot cho moi cot

Đổi bac từ 1 lên 12 là ta có mười hai mô hình với mười hai mức phức tạp khác nhau — mà vẫn chỉ dùng LinearRegression từ đầu tới cuối.

🟣 Lớp C · Nghiên cứu

Câu hỏi hay: nếu đường vẽ ra là đường cong, sao vẫn gọi mô hình này là tuyến tính?

Vì chữ “tuyến tính” ở đây nói về quan hệ giữa mô hình và các tham số w, chứ không phải giữa mô hình và x. Công thức vẫn là w₁·cột₁ + w₂·cột₂ + … + b — mỗi w vẫn chỉ được nhân một lần rồi cộng vào. Bản thân các cột có cong queo thế nào cũng không đổi được điều đó.

Đây không phải chuyện chữ nghĩa. Chính vì tuyến tính theo w mà bài toán vẫn giữ được cái “chảo” trơn có đúng một đáy của Bài 28, và vẫn giải thẳng ra nghiệm được như Bài 31 đã nói. Nếu w mà nhân với nhau thì mọi thứ khó lên rất nhiều.

Trong thực tế người ta dùng sẵn PolynomialFeatures của scikit-learn thay vì tự viết tao_cot. Kết quả như nhau; ở đây ta tự viết để thấy rõ chuyện gì đang xảy ra.

2. Ba mức phức tạp, ba số phận

🌡️ Dữ liệu: nhiệt độ ngoài trời đo mỗi giờ, từ 6h sáng tới 19h tối.

Giờ678910111213141516171819
Nhiệt độ (°C)23,525,628,029,330,331,933,233,432,632,132,031,729,229,1

Nhiệt độ lên dần tới đầu giờ chiều rồi hạ — rõ ràng không phải đường thẳng. Như Bài 34, ta chia 70% làm đề ôn (9 giờ) và giữ 30% làm đề mới (5 giờ).

Cùng một bộ dữ liệu, ba mức phức tạp Chấm xanh là nhiệt độ đo được từ 6h tới 19h. Đường đỏ là thứ mô hình vẽ ra. bậc 1 Quá đơn giản bậc 2 Vừa đúng bậc 12 Quá phức tạp bỏ sót hẳn chỗ nóng nhất bám đúng hình dạng thật uốn éo qua từng chấm rồi lao khỏi khung

Hình 1 — Cùng 14 chấm dữ liệu, chỉ đổi số cột lũy thừa. Bậc 1 quá cứng nên bỏ sót hẳn chỗ nóng nhất. Bậc 2 bám đúng hình dạng. Bậc 12 uốn éo để đi qua từng chấm một, rồi lao thẳng ra khỏi khung ở mép phải.

🟢 Lớp A · Trực giác

Ba bức tranh, ba căn bệnh khác nhau — và hai trong ba có tên riêng.

Bậc 1 — underfitting (khớp chưa tới). Mô hình quá cứng nhắc, không đủ sức mô tả cả hình dạng chính. Đường thẳng không thể vừa lên vừa xuống, nên nó bỏ qua hẳn chuyện trời nóng nhất vào đầu giờ chiều. Giống một học sinh chưa học đủ: sai cả những chỗ cơ bản.

Bậc 2 — vừa đúng. Đủ mềm để cong theo hình dạng thật, nhưng không mềm quá đến mức chạy theo từng hạt nhiễu.

Bậc 12 — overfitting (khớp quá mức). Mô hình quá mềm. Nó không còn tìm quy luật nữa mà bẻ mình đi qua từng chấm một, kể cả những chấm chỉ lệch đi vì sai số đo. Giống học sinh học thuộc lòng cả những lỗi in trong sách.

🔵 Lớp B · Cơ chế

Con số nói cùng một câu chuyện:

BậcMSE đề đã ônMSE đề mớiChẩn đoán
12,542415,2719Underfitting — tệ ở cả hai
20,06660,6363Vừa đúng — đề mới thấp nhất
120,000086.165,9433Overfitting — hoàn hảo giả tạo

Hãy đọc bảng này theo hàng dọc. Cột giữa cứ giảm đều: 2,54 → 0,07 → 0,00. Nếu chỉ nhìn cột ấy, bậc 12 là mô hình tốt nhất trong ba cái.

Cột bên phải kể chuyện ngược lại: 15,27 → 0,64 → 86.166. Bậc 12 tệ hơn bậc 2 hơn 135.000 lần.

🟣 Lớp C · Nghiên cứu

Vì sao bậc 12 lại đạt đúng 0 trên đề đã ôn? Đếm là ra: đề ôn có 9 điểm, còn mô hình bậc 12 có 12 hệ số cộng thêm b là 13 núm vặn. Nhiều núm hơn số điểm phải đi qua thì luôn tồn tại cách đặt để đi qua hết.

Thực ra chỉ cần bậc 8 là đã đủ: 8 hệ số cộng b vừa đúng 9 núm cho 9 điểm. Bạn sẽ thấy trong bảng đầy đủ ở phần 3 rằng MSE đề đã ôn chạm 0 chính xác từ bậc 8 trở đi.

Đây cũng chính là cơ chế của Bài 34, chỉ khác cách nhìn: ở đó ta thêm 7 cột dữ liệu rác, ở đây ta thêm 7 cột lũy thừa. Với mô hình thì cột nào cũng là cột — nó không biết cột nào có ý nghĩa. Cứ nhiều núm hơn số điểm là nó ghi nhớ được thay vì học.

3. Đường cong chữ U — dấu hiệu không thể nhầm

🟢 Lớp A · Trực giác

Ba mức vẫn còn ít. Hãy vặn núm từ bậc 1 tới bậc 12 và theo dõi cả hai con số cùng lúc. Sẽ hiện ra một hình rất đặc trưng, gặp một lần là nhớ mãi:

  • Đường “đề đã ôn” chỉ có đi xuống. Thêm cột là thêm chỗ xoay xở, nên mô hình luôn khớp dữ liệu cũ tốt hơn hoặc ít nhất là bằng. Nó không bao giờ đi lên.
  • Đường “đề mới” xuống rồi lại lên. Ban đầu thêm độ phức tạp là có ích thật. Nhưng qua một điểm nào đó, mô hình bắt đầu học cả nhiễu, và khả năng dự đoán tụt dốc.

Chỗ thấp nhất của đường thứ hai là điểm ngọt — mức phức tạp vừa phải. Bên trái nó là underfitting, bên phải là overfitting.

Mô hình càng phức tạp thì hai đường đi hai hướng khác nhau Trục dọc: mỗi vạch gấp 10 lần vạch dưới. 0,001 0,01 0,1 1 10 100 1.000 10.000 100.000 1 2 3 4 5 6 7 8 9 10 11 12 bậc của đa thức — mô hình càng phức tạp về bên phải điểm ngọt: bậc 2 đề đã ôn: chỉ có giảm đề mới: giảm rồi vọt lên

Hình 2 — Trục dọc vẽ theo kiểu mỗi vạch gấp 10 lần vạch dưới, vì các con số chênh nhau quá xa để vẽ chung. Đường xám không bao giờ đi lên. Đường đỏ chạm đáy ở bậc 2 — đúng bằng bậc của quy luật thật mà máy chưa từng được cho biết.

🔵 Lớp B · Cơ chế

Bảng đầy đủ, cả mười hai bậc:

BậcMSE đề đã ônMSE đề mới 
12,542415,2719quá đơn giản
20,06660,6363⭐ điểm ngọt
30,04431,3412 
40,02344,2181 
50,02177,8290 
60,01911,3334 
70,0066336,5031 
80,0000241,6249đủ núm để ghi nhớ
90,00001.372,3732 
100,00006.074,5845 
110,000023.778,1017 
120,000086.165,9433hỏng hẳn

Cột giữa giảm đơn điệu từ 2,5424 xuống 0,0000 rồi nằm im. Cột phải chạm đáy ở bậc 2 rồi leo lên tới hơn tám mươi sáu nghìn.

🟣 Lớp C · Nghiên cứu

Vì sao cột giữa không bao giờ tăng? Mô hình bậc 3 chứa mọi mô hình bậc 2 bên trong nó — chỉ cần đặt hệ số của x³ bằng 0 là ra đúng mô hình bậc 2. Nên nghiệm tốt nhất của bậc 3 không thể tệ hơn nghiệm tốt nhất của bậc 2. Thêm cột thì tệ nhất cũng là hòa. Đó là lý do MSE trên dữ liệu đã học là thước đo vô dụng để chọn độ phức tạp: nó luôn bảo “càng phức tạp càng tốt”.

Đường đỏ có mượt không? Không hẳn — bậc 6 tụt xuống 1,3334, thấp hơn bậc 4 và bậc 5. Đề mới chỉ có 5 điểm nên may rủi còn xen vào. Xu hướng chung thì không thể nhầm, nhưng nếu chỉ dựa vào một lần chia để chọn bậc, bạn có thể chọn nhầm bậc 6. Bài 36 chữa đúng điểm yếu này.

Điều đáng chú ý nhất: điểm ngọt rơi vào bậc 2, đúng bằng bậc của quy luật mà ta sử dụng để tạo bảng dữ liệu mẫu. Máy không hề biết công thức ấy — nó tìm ra chỉ bằng cách thử và đo trên dữ liệu chưa từng thấy. Đây là toàn bộ ý nghĩa của việc chia đề ôn và đề mới.

⚠️ “Mô hình mạnh hơn” không có nghĩa là “mô hình tốt hơn”. Đây là hiểu lầm phổ biến nhất của người mới. Bậc 12 mạnh hơn bậc 2 theo mọi nghĩa — nó vẽ được nhiều hình hơn, khớp dữ liệu cũ giỏi hơn. Và nó tệ hơn 135.000 lần ở việc duy nhất mà ta cần.

Sức mạnh dư thừa không nằm im vô hại. Nó sẽ được dùng để ghi nhớ nhiễu.

📝 Bảng ghi nhớ

Khái niệmDấu hiệuCách chữa
Underfitting
(khớp chưa tới)
MSE cao ở cả đề đã ôn lẫn đề mớiCho mô hình mạnh hơn: thêm cột, thêm bậc
Overfitting
(khớp quá mức)
MSE rất thấp trên đề đã ôn nhưng cao trên đề mớiGiảm độ phức tạp, hoặc thu thêm dữ liệu
Vừa đúngHai con số gần nhau và cùng thấpDừng ở đây
Tạo cột lũy thừanp.column_stack([x**k for k in range(1, bac+1)])Núm vặn độ phức tạp
MSE trên dữ liệu đã họcChỉ có giảm khi thêm cộtKhông dùng để chọn độ phức tạp
Số núm vặnBậc d có d + 1 númNhiều núm hơn số điểm → ghi nhớ được hết

✎ Kiểm tra nhanh

Câu 1. Làm sao để LinearRegression vẽ được đường cong?

  • Đổi sang mô hình khác, nó chỉ vẽ được đường thẳng
  • Đặt tham số curve=True khi tạo mô hình
  • Thêm các cột x², x³… vào bảng dữ liệu
  • Tăng số vòng lặp huấn luyện
Mô hình vẫn chỉ nhân mỗi cột với một số rồi cộng lại. Nhưng khi trong bảng có cột x², kết quả vẽ ra là đường cong. Mẹo nằm ở dữ liệu, không nằm ở mô hình.

Câu 2. Một mô hình có MSE cao ở cả đề đã ôn lẫn đề mới. Đó là:

  • Overfitting
  • Underfitting
  • Mô hình vừa đúng
  • Lỗi lập trình
Sai ở cả hai nơi nghĩa là mô hình quá cứng, chưa đủ sức mô tả cả hình dạng chính. Cách chữa ngược với overfitting: cho nó mạnh hơn chứ không phải yếu đi.

Câu 3. Vì sao MSE trên dữ liệu đã học không bao giờ tăng khi ta thêm cột?

  • Vì scikit-learn tự động bỏ qua các cột thừa
  • Vì thêm cột làm dữ liệu sạch hơn
  • Đó chỉ là trùng hợp trong ví dụ này
  • Vì mô hình phức tạp hơn chứa sẵn mô hình đơn giản bên trong — chỉ cần đặt hệ số cột mới bằng 0
Mô hình bậc 3 có thể bắt chước y hệt bậc 2 bằng cách cho hệ số của x³ bằng 0. Nên nghiệm tốt nhất của nó không thể tệ hơn. Đây chính là lý do con số ấy vô dụng khi chọn độ phức tạp: nó luôn khuyên bạn chọn cái phức tạp nhất.

Câu 4. Đề ôn có 9 điểm. Từ bậc mấy trở đi mô hình có đủ núm vặn để đi qua trọn vẹn cả 9 điểm?

  • Bậc 8 — vì 8 hệ số cộng thêm b là vừa đúng 9 núm
  • Bậc 9
  • Bậc 12
  • Không bậc nào làm được
Bậc d có d hệ số cộng với b, tổng cộng d + 1 núm. Với 9 điểm thì cần 9 núm, tức bậc 8. Bảng đầy đủ xác nhận: MSE đề đã ôn chạm 0,0000 đúng từ bậc 8.

Câu 5. Trong thí nghiệm, điểm ngọt rơi đúng vào bậc 2 — bằng bậc của quy luật giấu sẵn. Điều đó cho thấy:

  • scikit-learn đã đoán được công thức gốc
  • Bậc 2 luôn là lựa chọn tốt nhất cho mọi bài toán
  • Đo trên dữ liệu chưa từng thấy là cách tìm ra độ phức tạp phù hợp, dù ta không biết quy luật thật
  • Dữ liệu quá ít nên kết quả không có ý nghĩa
Máy không hề được cho biết công thức. Nó tìm ra mức phức tạp đúng chỉ bằng cách thử nhiều bậc rồi chấm trên đề mới. Đó là toàn bộ giá trị của việc chia dữ liệu ở Bài 34.

Câu 6. Câu nào sau đây sai?

  • Mô hình mạnh hơn khớp dữ liệu cũ tốt hơn hoặc bằng
  • Mô hình mạnh hơn thì luôn dự đoán dữ liệu mới tốt hơn
  • Sức mạnh dư thừa sẽ được dùng để ghi nhớ nhiễu
  • Overfitting và underfitting là hai căn bệnh trái ngược nhau
Đây là hiểu lầm phổ biến nhất của người mới. Bậc 12 mạnh hơn bậc 2 về mọi mặt, mà tệ hơn 135.000 lần ở đúng việc ta cần. Sức mạnh dư thừa không nằm im — nó luôn được dùng vào việc học thuộc nhiễu.

💻 Thực hành: tự tay vặn núm phức tạp

▶ Cách dùng: bấm Khởi động Python một lần (khoảng 15 giây), đợi báo sẵn sàng rồi bấm Chạy code. Số liệu ghi thẳng trong code nên bạn sẽ ra đúng bảng trong bài.

💡 Thử thách (làm được cả ba là bạn đã nắm chắc bài):

  1. Đổi cách chia. Thay random_state=6 bằng 0, 1, rồi 11. Điểm ngọt có còn rơi vào bậc 2 không? Cột “đề đã ôn” có bao giờ đi lên không? Hai câu trả lời này khác nhau ở chỗ nào — và vì sao?
  2. Cho thêm dữ liệu. Nhân đôi số điểm bằng cách đo mỗi nửa giờ (bạn có thể tạo gio mới bằng np.arange(6, 19.5, 0.5) và sinh nhiet theo công thức -0.15*(gio-14)**2 + 33). Với nhiều dữ liệu hơn, bậc mấy mới bắt đầu overfitting? Điều đó gợi ý gì về cách chữa overfitting ngoài việc giảm độ phức tạp?
  3. Nhìn vào chỗ trống. Chạy phần cuối của chương trình rồi đọc kỹ bốn con số. Đường bậc 12 đi qua đúng mọi chấm dữ liệu, vậy vì sao ở những giờ giữa hai chấm nó lại đưa ra con số vô lý đến thế?

Bài tiếp theo: hôm nay ta chọn được bậc 2 nhờ chấm trên đề mới. Nhưng đề mới chỉ có 5 điểm, và bạn đã thấy đường đỏ hơi gợn ở bậc 6 — chia một lần thì còn may rủi. Bài 36 sẽ đưa ra cách chia nhiều lần rồi lấy trung bình, để con số chấm điểm bớt phụ thuộc vào chuyện ai rơi vào đề thi.