⭐ Bài 37: Rò rỉ dữ liệu — khi đáp án lọt vào đề thi
Chặng 8 · Học máy thực chiến (Bài 31 → 42) · đây là bài 7/12 của chặng.
⭐ Bài quan trọng thứ hai của chặng, sau Bài 34. Ba bài vừa rồi dạy cách giấu dữ liệu cho kỹ. Bài này chỉ ra rằng giấu kỹ vẫn chưa đủ — thông tin có thể rò ra từ những chỗ không ai ngờ.
🎯 Mục tiêu: nhận ra hai dạng rò rỉ dữ liệu phổ biến nhất, và biết quy tắc chung để tránh cả hai.
👥 Cần biết trước: Bài 33 (tỉ lệ so với đoán bừa), Bài 34 (train_test_split), Bài 36 (vai trò của phần cắt riê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ẽ:
- Biết rò rỉ dữ liệu là gì, và vì sao nó xảy ra được ngay cả khi bạn đã chia dữ liệu đúng bài bản.
- Nhận ra dạng thứ nhất: một cột đầu vào chứa sẵn đáp án, hoặc chỉ có được sau khi biết đáp án.
- Nhận ra dạng thứ hai: một bước chuẩn bị dữ liệu đã nhìn trộm phần thi trước khi chia.
- Nhớ được một câu hỏi và một quy tắc đủ để chặn gần hết các trường hợp rò rỉ.
Ở Bài 36, tôi đã thử đo “lời nguyền của người thắng cuộc” — chuyện chọn cái tốt nhất rồi báo cáo con số đó — và không đo được, vì với sáu lựa chọn thì hiệu ứng còn chìm trong nhiễu.
Hôm nay nó hiện nguyên hình. Bạn sẽ thấy một mô hình được huấn luyện trên dữ liệu hoàn toàn ngẫu nhiên — không có một chút quy luật nào để học — vậy mà vẫn đạt điểm trông như đã học được thật. Không có bước nào sai về mặt câu lệnh. Dữ liệu được chia đúng, fit chỉ chạy trên phần ôn, điểm chấm trên phần thi.
Vấn đề chỉ nằm ở thứ tự của một bước. Và đó là lý do rò rỉ dữ liệu được xem là cái bẫy tinh vi nhất của nghề này.
📏 Thước đo dùng trong bài — không có gì mới, chính là phép chia ở Bài 33:
ti_le = 100 * MSE_mo_hinh / MSE_doan_bua # doan bua = luon doan bang gia tri trung binh
Gần 0% là mô hình giỏi hơn hẳn đoán bừa. Quanh 100% là chẳng hơn gì. Trên 100% là còn tệ hơn cả đoán bừa — chuyện rất bình thường khi dữ liệu chẳng có quy luật nào.
1. Dạng thứ nhất: đáp án nằm sẵn trong đề
Một trường muốn dự đoán điểm thi cuối kỳ của học sinh, để sớm phát hiện bạn nào cần kèm thêm. Họ có một bảng dữ liệu của năm trước với các cột: giờ tự học, điểm giữa kỳ, điểm cuối kỳ, và điểm tổng kết.
Ai đó đưa luôn cột điểm tổng kết vào làm đầu vào — “càng nhiều thông tin càng tốt mà”. Mô hình đạt kết quả gần như hoàn hảo. Mọi người vui mừng.
Nhưng hãy nghĩ xem điểm tổng kết được tính thế nào: (giữa kỳ + 2 × cuối kỳ) / 3. Nó chứa sẵn điểm cuối kỳ bên trong. Đưa nó vào mô hình chẳng khác gì đưa đề thi kèm đáp án.
Còn tệ hơn: mô hình này không thể dùng được vào đúng việc người ta cần. Muốn dự đoán điểm cuối kỳ thì phải dự đoán trước kỳ thi. Mà trước kỳ thi thì làm gì đã có điểm tổng kết.
Hình 1 — Vạch đỏ là thời điểm cần dự đoán. Mọi cột sinh ra sau vạch đỏ đều không có trong tay lúc dùng mô hình thật, nên không được làm đầu vào — dù trong bảng dữ liệu cũ chúng nằm ngay cạnh các cột khác, trông vô hại như nhau.
Bốn mươi học sinh, chia 30 bạn ôn và 10 bạn thi — hoàn toàn đúng quy trình Bài 34. Hai mô hình chỉ khác nhau đúng một cột:
gio + giua_ky -> de moi: 41.61% he so: 0.387 0.152 gio + giua_ky + tong_ket -> de moi: 0.11% he so: -0.001 -0.508 1.510
Mô hình trung thực đạt 41,61% — tức giảm được khoảng sáu phần mười sai số so với đoán bừa. Đó là con số thật về việc giờ tự học và điểm giữa kỳ nói được bao nhiêu về kỳ thi cuối.
Thêm cột tổng kết vào thì tụt xuống 0,11%. Gần như không sai chút nào.
Giờ hãy nhìn các hệ số của mô hình thứ hai: giờ tự học gần như bị bỏ qua (−0,001), điểm giữa kỳ bị trừ đi một nửa (−0,508), điểm tổng kết được nhân 1,510. Nghĩa là mô hình đang tính:
cuoi_ky ≈ 1.5 * tong_ket - 0.5 * giua_ky
Đó đúng là công thức tổng kết giải ngược lại. Mô hình không học được gì về việc học hành. Nó chỉ tìm ra cách moi đáp án từ một cột có sẵn đáp án.
Ngành gọi dạng này là rò rỉ từ đích (target leakage): một đặc trưng mang thông tin về đáp án mà lúc dự đoán thật sẽ không có. Nó hiếm khi lộ liễu như ví dụ trên. Thường gặp hơn là những cột chỉ có liên quan tới đáp án theo trình tự thời gian:
- Dự đoán khách hàng có hủy dịch vụ không, trong khi bảng có cột “số cuộc gọi tới tổng đài hủy hợp đồng”.
- Dự đoán một khoản vay có bị quá hạn không, trong khi bảng có cột “phí phạt trả chậm” — chỉ phát sinh sau khi đã quá hạn.
- Dự đoán bệnh nhân mắc một bệnh, trong khi bảng có cột “đã được kê thuốc điều trị bệnh đó”.
Điểm chung: trong bảng dữ liệu lịch sử, những cột này nằm ngay hàng ngay lối với các cột khác, không có dấu hiệu gì đặc biệt. Chỉ khi hỏi “thứ này có từ lúc nào?” thì mới thấy chúng đến sau đáp án.
Chia train/test không cứu được trường hợp này, vì cột rò rỉ có mặt ở cả phần ôn lẫn phần thi. Mô hình học được mẹo moi đáp án trên phần ôn, rồi áp đúng mẹo đó lên phần thi — và mẹo đó hoạt động hoàn hảo. Điểm thi đẹp, nhưng đẹp vì một lý do sẽ biến mất khi đưa ra dùng thật.
2. Dạng thứ hai: nhìn trộm lúc chuẩn bị dữ liệu
Dạng này khó thấy hơn nhiều, vì không có cột nào “xấu” cả.
Tưởng tượng bạn có 60 học sinh và 2.000 cột dữ liệu về mỗi bạn. Quá nhiều cột, nên bạn làm một việc rất hợp lý: chọn ra 10 cột có liên quan mạnh nhất tới điểm thi, bỏ hết số còn lại. Rồi chia ôn/thi, cho mô hình học trên 10 cột ấy, chấm điểm.
Nghe hoàn toàn đúng quy trình. Nhưng để ý bước chọn cột: nó được làm trên cả 60 bạn — tức là đã nhìn thấy điểm thi của cả những bạn sẽ bị đưa sang phần thi.
Với 2.000 cột thì chắc chắn sẽ có vài cột tình cờ khớp với điểm của 60 bạn này, chỉ do may rủi. Bước chọn cột nhặt đúng những cột tình cờ đó ra. Và vì 18 bạn thi nằm trong 60 bạn đã được dùng để chọn, những cột này tình cờ khớp luôn với cả phần thi.
Mô hình chưa bao giờ fit trên phần thi. Nhưng bước chọn cột thì đã nhìn thấy nó. Bức tường của Bài 34 bị thủng mà không ai để ý.
Để chứng minh không thể chối cãi, ta dùng dữ liệu hoàn toàn ngẫu nhiên: 2.000 cột số bốc thăm, điểm thi cũng bốc thăm. Không có quy luật nào tồn tại để học. Mô hình nào đạt dưới 100% trên dữ liệu này là đang tự lừa mình.
# CACH SAI: chon cot tren toan bo, roi moi chia cot = chon_cot(X, y, 10) X_on, X_moi, y_on, y_moi = train_test_split(X[:, cot], y, ...) # CACH DUNG: chia truoc, chon cot chi tren phan on i_on, i_moi = train_test_split(np.arange(N), ...) cot = chon_cot(X[i_on], y[i_on], 10)
Hai đoạn dùng đúng những câu lệnh giống nhau, chỉ đảo thứ tự hai dòng. Kết quả:
Chon cot TRUOC khi chia (sai) -> de moi: 51.92% Chia TRUOC khi chon cot (dung) -> de moi: 149.03%
Cách sai báo 51,92% — nhìn vào ai cũng nghĩ mô hình đã học được một nửa quy luật. Cách đúng báo 149,03% — tệ hơn cả đoán bừa, đúng bản chất của dữ liệu nhiễu.
Một lần có thể là may. Nên tôi lặp lại với 40 bộ nhiễu khác nhau:
Cach sai : trung vi 50.4% | trong nhu 'hoc duoc' (<70%): 33/40 Cach dung: trung vi 155.2% | khong te hon doan bua (<100%): 1/40
33 trên 40 lần, cách sai cho ra một mô hình trông như có ích — trên dữ liệu không chứa gì để học. Đây không phải chuyện may rủi. Đây là cách làm sai tạo ra ảo giác một cách có hệ thống.
Hình 2 — Cùng bốn bước, chỉ đổi chỗ bước 2 và bước 3. Ô đỏ là chỗ rò rỉ: bước chọn cột được làm trước khi dựng bức tường, nên nó nhìn thấy cả những bạn sau này sẽ đi thi.
Đây chính là “lời nguyền của người thắng cuộc” mà Bài 36 không đo được — giờ thì đo được, và rất lớn. Khác biệt nằm ở số lựa chọn: Bài 36 chọn trong 6 bậc, còn ở đây chọn 10 trong 2.000 cột. Nhặt ra những thứ khớp nhất trong hai nghìn thứ ngẫu nhiên thì chắc chắn sẽ nhặt được những thứ khớp nhờ may.
Thử thách 2 cuối bài sẽ cho bạn tự kiểm chứng điều này: bớt số cột rác đi thì cách sai lừa được ít dần.
Dạng rò rỉ này có tên là nhiễm bẩn giữa phần ôn và phần thi (train–test contamination). Năm 2002, hai nhà thống kê Ambroise và McLachlan chỉ ra rằng một số nghiên cứu về chọn gen đã mắc đúng lỗi này — chọn vài chục gen trong hàng nghìn gen trên toàn bộ dữ liệu bệnh nhân rồi mới kiểm định — nên độ chính xác được báo cáo lạc quan hơn thực tế rất nhiều. Sách giáo khoa kinh điển The Elements of Statistical Learning dành hẳn một mục cho nó, với tiêu đề thẳng thắn: “The Wrong and Right Way to Do Cross-validation”.
3. Một câu hỏi và một quy tắc
Hai dạng rò rỉ trông khác nhau, nhưng gốc rễ giống hệt: mô hình được biết một điều mà ngoài đời thật nó sẽ không được biết. Dạng một biết nhờ một cột; dạng hai biết nhờ một bước chuẩn bị.
Để chặn dạng một, hỏi cho từng cột:
“Lúc cần dự đoán thật, cột này đã có trong tay chưa?”
Để chặn dạng hai, nhớ một quy tắc:
Bất kỳ bước nào học từ dữ liệu, đều phải làm sau khi chia và chỉ trên phần ôn.
“Học từ dữ liệu” không chỉ là fit. Đó là mọi bước có tính toán gì đó dựa trên các con số trong bảng rồi dùng kết quả để quyết định.
Những bước “học từ dữ liệu” hay gặp mà người mới thường không coi là học:
| Bước chuẩn bị | Nó học gì từ dữ liệu | Làm đúng |
|---|---|---|
| Chọn các cột “quan trọng nhất” | Cột nào khớp với đáp án | Chỉ chọn trên phần ôn |
| Điền ô trống bằng giá trị trung bình | Giá trị trung bình của cột | Tính trung bình trên phần ôn, dùng nó điền cho cả phần thi |
| Đưa các cột về cùng thang đo | Trung bình và độ lệch của cột | Tính trên phần ôn, áp y nguyên cho phần thi |
| Loại bỏ điểm “bất thường” | Thế nào là bình thường | Xác định ngưỡng trên phần ôn |
| Chọn bậc, chọn mô hình | Cái nào cho điểm tốt nhất | Kiểm định chéo trong phần làm việc (Bài 36) |
Hàng thứ hai và thứ ba hay bị làm sai nhất, vì chúng trông quá vô hại. Mức độ rò rỉ của chúng thường nhỏ hơn nhiều so với việc chọn cột — nhưng nhỏ không có nghĩa là không có, và thói quen làm sai ở chỗ nhỏ sẽ theo bạn tới chỗ lớn.
Khi quy trình có nhiều bước chuẩn bị, rất dễ lỡ tay làm một bước trên toàn bộ dữ liệu. scikit-learn có công cụ tên Pipeline để gói tất cả các bước — chuẩn bị lẫn mô hình — thành một khối duy nhất có fit và predict. Khi đưa khối đó vào cross_val_score, mọi bước bên trong tự động chỉ học trên phần ôn của từng lượt. Ta sẽ không học Pipeline ở chặng này, nhưng biết nó tồn tại để làm gì là đủ.
Dấu hiệu cảnh báo đáng tin nhất trong thực tế: kết quả tốt đến khó tin. Trên nhiều bài toán thật, những người làm lâu năm có một phản xạ gần như vô điều kiện — thấy mô hình đạt điểm gần hoàn hảo là đi tìm chỗ rò rỉ trước, ăn mừng sau. Nhìn các hệ số như ở phần 1 thường là cách tìm ra nhanh nhất: cột nào có hệ số lớn bất thường thì cột đó đáng nghi đầu tiên.
⚠️ Vì sao rò rỉ nguy hiểm hơn overfitting?
Overfitting tự lộ ra: MSE đề đã ôn thấp, đề mới cao — chỉ cần chia dữ liệu là bắt được ngay (Bài 34, 35).
Rò rỉ thì không lộ ra trong bất kỳ con số nào bạn tính được. Cả điểm đề ôn lẫn điểm đề mới đều đẹp. Kiểm định chéo cũng đẹp. Mọi kiểm tra ở ba bài trước đều qua. Nó chỉ lộ ra khi mô hình được đưa vào dùng thật — lúc đó thì đã muộn.
Thứ duy nhất bắt được nó là suy nghĩ về dữ liệu, không phải một câu lệnh.
📝 Bảng ghi nhớ
| Rò rỉ từ đích | Nhìn trộm khi chuẩn bị | |
|---|---|---|
| Thông tin lọt qua đâu | Một cột chứa sẵn hoặc sinh ra sau đáp án | Một bước tính toán làm trên toàn bộ dữ liệu trước khi chia |
| Ví dụ trong bài | Điểm tổng kết khi đoán điểm cuối kỳ | Chọn 10 trong 2.000 cột trên cả 60 bạn |
| Chia train/test có chặn được không | Không — cột rò rỉ có ở cả hai phần | Không — bước rò rỉ xảy ra trước khi chia |
| Câu hỏi để phát hiện | Lúc dự đoán thật, cột này có chưa? | Bước này có tính gì trên phần thi không? |
| Cách chữa | Bỏ cột đó ra khỏi đầu vào | Chia trước, mọi bước học chỉ trên phần ôn |
| Dấu hiệu | Kết quả tốt đến khó tin; một cột có hệ số lớn bất thường | |
✎ Kiểm tra nhanh
Câu 1. Vì sao đưa cột “điểm tổng kết” vào để đoán “điểm cuối kỳ” là rò rỉ dữ liệu?
- Vì cột đó có quá nhiều số lẻ
- Vì mô hình có quá nhiều cột
- Vì điểm tổng kết được tính từ chính điểm cuối kỳ, và chưa hề có lúc cần dự đoán
- Vì chưa chia train/test
Câu 2. Dữ liệu đã được chia ôn/thi đúng cách. Tại sao rò rỉ từ đích vẫn xảy ra?
- Vì
train_test_splitcó lỗi - Vì cột rò rỉ có mặt ở cả phần ôn lẫn phần thi, nên mẹo moi đáp án dùng được ở cả hai
- Vì
random_statechọn chưa tốt - Vì phần thi quá nhỏ
Câu 3. Trên dữ liệu hoàn toàn ngẫu nhiên, chọn 10 cột tốt nhất trên toàn bộ 60 bạn rồi mới chia cho ra 51,92%. Điều đó có nghĩa là:
- Dữ liệu thật ra có một chút quy luật
- Mô hình đã học được khoảng một nửa
- 10 cột là quá ít, cần chọn nhiều hơn
- Bước chọn cột đã nhìn thấy phần thi, nên nhặt được những cột tình cờ khớp với cả phần thi
Câu 4. Bước nào sau đây cũng là “học từ dữ liệu” và phải làm sau khi chia?
- Điền ô trống bằng giá trị trung bình của cột
- Đổi tên cột cho dễ đọc
- Đổi đơn vị từ phút sang giờ bằng cách chia 60
- Xóa một cột mà ai cũng biết là vô nghĩa
Câu 5. Vì sao rò rỉ dữ liệu được coi là nguy hiểm hơn overfitting?
- Vì nó làm chương trình chạy chậm hơn
- Vì nó chỉ xảy ra với dữ liệu lớn
- Vì mọi con số kiểm tra đều đẹp, nó chỉ lộ ra khi mô hình được dùng thật
- Vì scikit-learn không hỗ trợ phát hiện overfitting
Câu 6. Mô hình của bạn đạt tỉ lệ 0,5% trên một bài toán thật mà trước giờ ai làm cũng chỉ được khoảng 40%. Phản xạ đúng là:
- Công bố ngay kết quả
- Đi tìm chỗ rò rỉ trước, nhìn các cột có hệ số lớn bất thường
- Tăng thêm số cột để đạt 0%
- Chạy lại với
random_statekhác cho chắc
💻 Thực hành: tự tạo ra ảo giác, rồi tự phá nó
▶ 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. Phần cuối lặp lại thí nghiệm 40 lần trên 2.000 cột nên cần thêm khoảng 10 giây — cứ đợi, trang không bị treo đâu. Các con số được sinh bằng bộ sinh số cố định 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ỉ giữ cột rò rỉ. Ở phần 1, thử mô hình chỉ gồm
giovàtong_ket, bỏgiua_kyđi. Tỉ lệ còn thấp không? Hệ số củatong_ketgiờ là bao nhiêu, và vì sao nó khác con số 1,510 lúc trước? - Bớt cột rác. Ở phần 2, đổi
P=2000thành 500, 100, rồi 20 trong dòngket = [thi_nghiem(k) ...](viếtthi_nghiem(k, P=100)). Trung vị của cách sai thay đổi ra sao? Từ đó giải thích: vì sao càng có nhiều cột để chọn thì rò rỉ càng lừa được mạnh? - Đừng tin một lần chạy. Đổi
thi_nghiem(9)thànhthi_nghiem(7). Lần này cách sai ra bao nhiêu phần trăm? Có phải lần đó không có rò rỉ không? Câu trả lời giải thích vì sao tôi phải lặp lại thí nghiệm 40 lần thay vì chỉ khoe một lần.
Bài tiếp theo: bạn đã đi hết năm bài lý thuyết nặng nhất của chặng — chia dữ liệu, overfitting, kiểm định chéo, rò rỉ. Bài 38 là bài luyện tập, không có khái niệm nào mới. Bạn sẽ gặp bốn tình huống, mỗi tình huống có một chỗ hỏng được giấu kỹ, và nhiệm vụ là tìm ra nó. Đây là lúc kiểm tra xem những gì vừa học đã thành phản xạ chưa.