Python - Bài 34: Chia dữ liệu train/test

⭐ 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

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

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.

🔵 Lớp B · Cơ chế

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.

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

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ọcGầ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

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

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 fit nhì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.

Giấu một phần đi trước khi cho mô hình học Phần ôn — 9 bạn (75%) mô hình được xem thoải mái 🔒 Phần thi — 3 bạn (25%) giấu kín, fit không được thấy fit(X_on, y_on) mô hình w, b predict(X_moi) Điểm thật của mô hình đo trên dữ liệu chưa từng thấy Quy tắc vàng: dữ liệu bên phải bức tường không được tham gia vào bất kỳ bước học nào. Nhìn trộm một lần là con số chấm điểm mất giá trị.

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.

🔵 Lớp B · Cơ chế

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ủa X trước, rồi hai phần của y. Đả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.25 nghĩ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=42 cố đị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
🟣 Lớp C · Nghiên cứu

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ọc123456789101112
Điểm thi2,73,54,04,45,35,86,98,18,18,89,910,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
Cùng hai mô hình, hai thước đo, hai kết luận ngược nhau Cột càng dài càng sai nhiều. Hãy so hai cột trong cùng một mô hình. Mô hình 1 chỉ 1 cột có nghĩa đề đã ôn: 0,0653 đề mới: 0,0379 — vẫn tốt Mô hình 2 thêm 7 cột rác đề đã ôn: 0,0000 — điểm tuyệt đối đề mới: 0,8433 — tệ gấp 22 lần mô hình 1 Chỉ nhìn cột xám thì mô hình 2 hoàn hảo. Nhìn cột màu mới thấy nó hỏng.

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 chiafrom sklearn.model_selection import train_test_splitNgăn model_selection, không phải linear_model
Chia dữ liệuX_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ọcfit(X_on, y_on)Chỉ phần ôn
Chấm điểm thậtpredict(X_moi) rồi so với y_moiĐây mới là con số đáng tin
test_sizeThường 0,2 đến 0,3Dữ liệu càng lớn càng để ít
random_stateMột số bất kỳ, giữ cố địnhĐể chạy lại ra đúng kết quả cũ
Quy tắc vàngPhần thi không tham gia bất kỳ bước học nàoNhìn trộm một lần là hỏng
Dấu hiệu nguy hiểmMSE đề đã ôn rất nhỏ, MSE đề mới lớnMô 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
Con số ấy chỉ cho biết mô hình ghi nhớ dữ liệu cũ tốt đến đâu, chứ không cho biết nó dự đoán trường hợp mới tốt đến đâu. Mà cái sau mới là mục đích của mọi mô hình.

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_moi
  • X_on, X_moi, y_on, y_moi
  • y_on, y_moi, X_on, X_moi
  • X_moi, X_on, y_moi, y_on
Hai phần của 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
Tám cột để vặn mà chỉ phải khớp chín dòng thì gần như đủ chỗ ghi nhớ từng bạn. Điểm tuyệt đối trên đề đã ôn hầu như luôn là dấu hiệu đáng ngờ chứ không phải thành tự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
Nó không ảnh hưởng gì tới chất lượng mô hình, chỉ khiến kết quả lặp lại được. Số 42 không đặc biệt — dùng 7 hay 2024 đều được, miễn giữ nguyên khi chạy lại.

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_size lê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
Khoảng cách lớn giữa hai con số là dấu hiệu kinh điển. Và lưu ý phương án 2: chia lại cho tới khi ra kết quả đẹp là hành vi tự lừa dối — nó biến phần thi thành một phần của quá trình học.

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
Chỉ cần mô hình “nhìn trộm” phần thi một lần — dù chỉ để chọn tham số — là con số chấm điểm mất giá trị. Bài 37 sẽ cho thấy việc nhìn trộm này xảy ra tinh vi tới mức nào.

💻 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):

  1. Chấm sai có chủ đích. Trong cham_diem, đổi dòng fit(X_on, y_on) thành fit(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?
  2. 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?
  3. Đổi tỉ lệ chia. Thử test_size bằ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.