⭐ Bài 34: Không được chấm bằng chính đề đã ôn
Chặng 8 · Học máy thực chiến (Bài 31 → 42) · đây là bài 4/12 của chặng.
⭐ Đây là bài quan trọng nhất của cả chặng.
🎯 Mục tiêu: hiểu vì sao chấm điểm mô hình bằng chính dữ liệu nó đã học là vô nghĩa, và biết cách chấm cho đúng.
📖 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ẽ:
- Giải thích được vì sao MSE đo trên dữ liệu đã học không nói lên điều gì về chất lượng mô hình.
- Dùng được
train_test_splitđể giữ lại một phần dữ liệu làm “đề thi”. - Nhìn thấy tận mắt một mô hình đạt điểm tuyệt đối trên đề đã ôn nhưng tệ gấp 22 lần trên đề mới.
- Nhớ được quy tắc vàng: phần dữ liệu để thi phải được giấu kín tới phút cuối.
Nhớ lại cuối Bài 33, ta có một chi tiết khi mô hình giá nhà cho MSE bằng 0,0000. Không sai một đồng nào trên cả tám căn.
Nghe như một thành tựu. Thật ra đó là một cảnh báo.
Vì con số ấy được đo trên chính tám căn nhà mà mô hình vừa học thuộc. Cũng giống như thầy giáo cho ôn đúng 20 câu, rồi thi lại đúng 20 câu đó, rồi khen cả lớp giỏi. Điểm 10 ấy chẳng nói lên điều gì.
1. Cái bẫy: chấm điểm bằng đề đã ôn
Hãy nghĩ về hai bạn học sinh cùng ôn thi:
- Bạn An hiểu bản chất bài toán. Gặp đề lạ vẫn làm được, nhưng thỉnh thoảng vẫn sai vặt.
- Bạn Bình học thuộc lòng đáp án của đúng 20 câu trong đề cương. Không hiểu gì cả.
Bây giờ thi đúng 20 câu trong đề cương. Bình được 10 tròn trĩnh, An được 8,5. Theo bảng điểm thì Bình giỏi hơn.
Nhưng ai cũng biết kết luận đó sai. Chỉ cần đổi sang một đề khác là lộ ra ngay: An vẫn làm được 8, còn Bình về 2.
Mô hình học máy cũng hệt như vậy. Mục đích của nó không phải là trả lời đúng những dữ liệu ta đã có — những dữ liệu ấy ta biết đáp án rồi, cần gì mô hình. Mục đích là trả lời đúng những trường hợp chưa từng gặp.
Nên muốn biết mô hình có tốt thật không, phải cho nó làm một đề nó chưa từng thấy.
Nhìn lại đúng những gì ta đã làm ở Bài 31, 32 và 33:
mo_hinh.fit(X, y) # hoc tren TOAN BO X mse = ((mo_hinh.predict(X) - y) ** 2).mean() # cham diem cung tren TOAN BO X
Cùng một chữ X ở cả hai dòng. Đó chính là chỗ hỏng: đề ôn và đề thi là một.
Với mô hình đường thẳng một cột thì tác hại còn nhẹ, vì đường thẳng chỉ có hai núm vặn, muốn học thuộc lòng cũng không đủ chỗ chứa. Nhưng chỉ cần cho mô hình thêm vài cột nữa, nó sẽ có đủ chỗ để nhớ vanh vách từng dòng — và MSE sẽ tụt xuống 0 mà chẳng học được gì thật.
Phần 3 sẽ cho bạn thấy điều đó xảy ra tận mắt.
Nói cho chặt chẽ: có hai câu hỏi hoàn toàn khác nhau mà người mới hay gộp làm một.
| Câu hỏi | Đo bằng gì | Có ích không |
|---|---|---|
| Mô hình khớp dữ liệu đã có tốt không? | MSE trên dữ liệu đã học | Gần như không — luôn có thể làm đẹp bằng cách thêm tham số |
| Mô hình dự đoán trường hợp mới tốt không? | MSE trên dữ liệu chưa từng thấy | Đây mới là điều ta cần biết |
Ngành học máy gọi khả năng làm tốt trên dữ liệu mới là khả năng tổng quát hóa (generalization). Toàn bộ giá trị thực tế của một mô hình nằm ở đó. Một mô hình khớp hoàn hảo dữ liệu cũ mà hỏng trên dữ liệu mới thì vô dụng — hiện tượng ấy có tên riêng, và Bài 35 sẽ gọi tên cùng mổ xẻ nó.
2. Cách sửa: giấu một phần dữ liệu đi
Cách chữa đơn giản: trước khi cho mô hình học, hãy giấu bớt một phần dữ liệu đi.
- Phần ôn (thường 70–80%) — đưa cho mô hình học. Tên tiếng Anh: training set.
- Phần thi (20–30% còn lại) — cất đi, tuyệt đối không cho
fitnhìn thấy. Tên tiếng Anh: test set.
Học xong, đem phần thi ra chấm. Điểm đó mới là điểm thật, vì mô hình chưa từng nhìn thấy những dòng ấy bao giờ.
Có một chi tiết dễ bỏ sót: phải xáo trộn trước khi chia. Dữ liệu thường được xếp theo thứ tự nào đó — theo ngày, theo tên, theo điểm. Nếu cứ cắt phăng 25% cuối làm đề thi, bạn có thể vô tình dồn hết những bạn học giỏi nhất vào một bên.
Hình 1 — Toàn bộ ý tưởng của bài nằm ở bức tường nét đứt. Mô hình học bên trái, bị chấm điểm bên phải. Nó không bao giờ được nhìn sang phải trong lúc học.
scikit-learn có sẵn một hàm làm đúng việc này. Nó nằm ở ngăn model_selection:
from sklearn.model_selection import train_test_split X_on, X_moi, y_on, y_moi = train_test_split( X, y, test_size=0.25, random_state=42) print(X_on.shape, X_moi.shape) # (9, 1) (3, 1)
Ba điều cần để ý:
- Thứ tự trả về là
X_on, X_moi, y_on, y_moi— hai phần củaXtrước, rồi hai phần củay. Đảo nhầm thứ tự là một lỗi rất hay gặp, và tệ ở chỗ chương trình vẫn chạy. test_size=0.25nghĩa là để 25% sang phần thi. Có 12 bạn thì 3 bạn đi thi, 9 bạn ở lại ôn.random_state=42cố định cách xáo trộn. Không có nó, mỗi lần chạy sẽ chia khác nhau và kết quả nhảy múa — khó đối chiếu. Số 42 không có gì đặc biệt, viết 7 hay 2024 đều được, miễn giữ nguyên.
Và từ đây, hai dòng chấm điểm phải dùng hai bộ dữ liệu khác nhau:
mo_hinh.fit(X_on, y_on) # hoc tren phan on mse_on = ((mo_hinh.predict(X_on) - y_on ) ** 2).mean() mse_moi = ((mo_hinh.predict(X_moi) - y_moi) ** 2).mean() <- con so that
Chia bao nhiêu là hợp lý? Có một sự đánh đổi. Để phần thi nhiều thì điểm chấm đáng tin hơn, nhưng phần ôn ít đi nên mô hình học kém hơn. Thực tế người ta hay dùng 20–30% cho phần thi. Với dữ liệu rất lớn — hàng triệu dòng — thì 1% cũng đã quá đủ để chấm.
Vì sao random_state lại quan trọng đến thế? Không phải để kết quả đẹp hơn, mà để lặp lại được. Khoa học đòi hỏi người khác chạy lại code của bạn phải ra đúng con số ấy. Đây là thói quen nghề nghiệp, không phải chi tiết kỹ thuật vặt.
Một điểm yếu cần biết trước. Chia một lần thì điểm chấm phụ thuộc vào việc ai rơi vào phần thi. Với bộ dữ liệu nhỏ, may rủi rất lớn: cùng một mô hình mà đổi cách chia có thể cho điểm khác hẳn. Ta sẽ thấy con số cụ thể ở phần 3, và Bài 36 sẽ đưa ra cách khắc phục.
3. Thí nghiệm: hai mô hình, hai câu trả lời trái ngược
📊 Dữ liệu: 12 học sinh. Cột thật sự có ý nghĩa là giờ tự học.
| Giờ tự học | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Điểm thi | 2,7 | 3,5 | 4,0 | 4,4 | 5,3 | 5,8 | 6,9 | 8,1 | 8,1 | 8,8 | 9,9 | 10,5 |
Ngoài ra ta còn ghi thêm bảy cột hoàn toàn vô nghĩa, đúng tinh thần Bộ 2 của Bài 33: số ký tự trong tên, ngày sinh, số áo, chiều cao của bố, số nhà, số anh chị em, số km đến trường.
Hai mô hình đem so:
- Mô hình 1 — chỉ dùng cột giờ tự học.
- Mô hình 2 — dùng cả 8 cột, tức thêm 7 cột rác.
🤔 Đoán trước khi chạy. Bảy cột kia chẳng liên quan gì tới điểm thi. Bạn nghĩ mô hình 2 sẽ tệ hơn mô hình 1 — nhưng tệ hơn ở chỗ nào, trên đề đã ôn hay trên đề mới? Nghĩ xong hãy đọc tiếp.
💻 Máy trả lời:
1) Chi cot 'gio tu hoc' MSE de da on = 0.0653 | MSE de moi = 0.0379 2) 'gio' + 7 cot vo nghia MSE de da on = 0.0000 | MSE de moi = 0.8433
Hình 2 — Mô hình 2 đạt điểm tuyệt đối trên đề nó đã ôn, nhưng sai gấp 22 lần mô hình 1 khi gặp đề mới. Nếu chỉ đo theo cách của ba bài trước, ta sẽ chọn nhầm mô hình.
Đây là toàn bộ lý do bài này tồn tại. Nếu chỉ nhìn MSE trên đề đã ôn, bạn sẽ kết luận mô hình 2 hoàn hảo còn mô hình 1 có sai sót — và chọn đúng cái tệ hơn.
Mô hình 2 làm được điều đó bằng cách nào? Nó có 8 cột để vặn, mà chỉ phải khớp 9 dòng. Gần như đủ chỗ để nhớ vanh vách từng bạn một, thay vì tìm quy luật. Nó ghi nhớ được rằng “bạn có số nhà 144 thì được 2,7 điểm” — hoàn toàn vô dụng khi gặp bạn thứ mười ba.
⚠️ MSE 0,0000 ấy có thật sự bằng 0 không? Không tròn trịa như vậy. Con số thật là khoảng 3,5 × 10⁻²⁹ — số 0, dấu phẩy, rồi 28 số 0 nữa mới tới chữ số đầu tiên. Nhỏ tới mức máy tính coi như bằng 0, và khi in với 4 chữ số thập phân thì thành 0.0000.
Nếu bạn print thẳng không định dạng, bạn sẽ thấy 3.4994746267721e-29 chứ không phải 0.0. Đừng hoảng — đó là cùng một con số.
🔍 Một chi tiết đáng để ý. Mô hình 1 có MSE trên đề mới (0,0379) thấp hơn trên đề đã ôn (0,0653). Nghe ngược đời — sao làm bài lạ lại tốt hơn bài đã ôn?
Đó chỉ là may mắn. Ba bạn rơi vào phần thi tình cờ là ba bạn dễ đoán. Với 12 dòng dữ liệu, chuyện này rất hay xảy ra. Nó không phải quy luật, và cũng không có gì đáng lo.
📝 Bảng ghi nhớ
| Điều cần nhớ | Cụ thể | Ghi chú |
|---|---|---|
| Nhập hàm chia | from sklearn.model_selection import train_test_split | Ngăn model_selection, không phải linear_model |
| Chia dữ liệu | X_on, X_moi, y_on, y_moi = train_test_split(X, y, test_size=0.25, random_state=42) | Đúng thứ tự này, đảo là sai |
| Học | fit(X_on, y_on) | Chỉ phần ôn |
| Chấm điểm thật | predict(X_moi) rồi so với y_moi | Đây mới là con số đáng tin |
test_size | Thường 0,2 đến 0,3 | Dữ liệu càng lớn càng để ít |
random_state | Một số bất kỳ, giữ cố định | Để chạy lại ra đúng kết quả cũ |
| Quy tắc vàng | Phần thi không tham gia bất kỳ bước học nào | Nhìn trộm một lần là hỏng |
| Dấu hiệu nguy hiểm | MSE đề đã ôn rất nhỏ, MSE đề mới lớn | Mô hình đang học vẹt — Bài 35 |
✎ Kiểm tra nhanh
Câu 1. Vì sao MSE đo trên chính dữ liệu mô hình đã học lại không đáng tin?
- Vì công thức MSE chỉ đúng với dữ liệu mới
- Vì scikit-learn tính sai trên dữ liệu cũ
- Vì mô hình đã thấy sẵn đáp án của những dòng đó, giống thi lại đúng đề đã ôn
- Vì dữ liệu cũ thường có nhiều nhiễu hơn
Câu 2. train_test_split(X, y, test_size=0.25) trả về bốn thứ theo thứ tự nào?
X_on, y_on, X_moi, y_moiX_on, X_moi, y_on, y_moiy_on, y_moi, X_on, X_moiX_moi, X_on, y_moi, y_on
X trước, rồi hai phần của y. Đảo nhầm là lỗi rất hay gặp và đặc biệt khó chịu, vì chương trình vẫn chạy bình thường, chỉ có kết quả là vô nghĩa.Câu 3. Mô hình 2 đạt MSE bằng 0,0000 trên đề đã ôn. Điều đó có nghĩa là:
- Mô hình hoàn hảo, nên dùng nó
- Dữ liệu không có nhiễu
- Đã tìm ra đúng quy luật thật
- Mô hình có đủ chỗ để nhớ vanh vách từng dòng, thay vì tìm quy luật — và đó là dấu hiệu xấu
Câu 4. random_state=42 dùng để làm gì?
- Cố định cách xáo trộn, để chạy lại lần sau vẫn ra đúng kết quả cũ
- Làm cho mô hình chính xác hơn
- Chọn ra 42 dòng làm phần thi
- Chạy thuật toán 42 vòng lặp
Câu 5. Bạn thấy một mô hình có MSE trên đề đã ôn rất nhỏ nhưng MSE trên đề mới rất lớn. Nên hiểu thế nào?
- Cần tăng
test_sizelên - Phần thi bị chọn sai, nên chia lại cho tới khi đẹp
- Mô hình đang học vẹt dữ liệu cũ chứ không nắm được quy luật
- Cần đổi
random_state
Câu 6. Quy tắc vàng của bài này là:
- Luôn để
test_size=0.25 - Phần thi không được tham gia vào bất kỳ bước học nào
- Luôn dùng càng nhiều cột càng tốt
- Luôn đặt
random_state=42
💻 Thực hành: tự tay bắt quả tang mô hình học vẹt
▶ 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. Toàn bộ số liệu được ghi thẳng trong code nên bạn sẽ ra đúng những con số trong bài.
💡 Thử thách (làm được cả ba là bạn đã nắm chắc bài):
- Chấm sai có chủ đích. Trong
cham_diem, đổi dòngfit(X_on, y_on)thànhfit(X, diem)— tức cho mô hình học trên toàn bộ dữ liệu, đúng như ba bài trước. Giờ “MSE đề mới” của mô hình 2 ra bao nhiêu? Vì sao con số ấy không còn ý nghĩa gì nữa? - Bỏ bớt cột rác. Trong
X_8cot, thử lần lượt giữ 1, 3, 5 rồi 7 cột vô nghĩa. Ghi lại hai con số mỗi lần. Thêm cột rác làm MSE đề đã ôn đi theo chiều nào, và MSE đề mới đi theo chiều nào? - Đổi tỉ lệ chia. Thử
test_sizebằng 0,1 rồi 0,5. Với 12 bạn thì mỗi lựa chọn cho mấy bạn đi thi? Con số chấm điểm trở nên đáng tin hơn hay bấp bênh hơn, và vì sao?
Bài tiếp theo: hôm nay ta đã bắt quả tang một mô hình học vẹt, nhưng chưa gọi tên hiện tượng đó. Bài 35 sẽ gọi đúng tên nó — overfitting — và cho bạn thấy nó xuất hiện dần dần thế nào khi mô hình mỗi lúc một phức tạp hơn. Bạn cũng sẽ gặp người anh em ít được nhắc tới của nó: underfitting.