Mở đầu: model 95% train accuracy — tin mừng hay tin dữ?
Bảy bài trước dạy bạn cách khiến loss giảm về gần $0$. Bài này dạy một sự thật khó chịu hơn: loss train thấp không đảm bảo điều gì về việc model có dùng được ngoài đời hay không. Một mạng đủ lớn có thể học thuộc lòng từng điểm dữ liệu huấn luyện — kể cả những điểm bị gán nhãn sai — trong khi hoàn toàn bất lực trước một điểm dữ liệu mới chưa từng thấy. Đây gọi là overfitting (quá khớp), và nó là lý do số $1$ khiến model "chạy tốt trên giấy" nhưng thất bại khi triển khai thật.
Bài này verify từng cạm bẫy bằng số thật, dùng đúng mạng MLP + autograd của Bài 6/Bài 7: bạn sẽ thấy một mạng tự "học thuộc lòng" 20 điểm dữ liệu nhiễu, thấy chính xác epoch nào nó bắt đầu quá khớp, và thấy 2 cách chữa kinh điển (L2, dropout) hoạt động — cùng với cạm bẫy dropout khiến model "say rượu" nếu quên đúng 1 dòng code lúc phục vụ thật.
backward() thật, không viết tay). Tài nguyên ngoài:
Google ML Crash Course — Overfitting,
PyTorch — nn.Dropout.
1. Train / Validation / Test: 3 tập, 3 vai trò không được lẫn lộn
Chia dữ liệu thành $3$ phần tách biệt, mỗi phần một nhiệm vụ duy nhất:
const all = makeMoons(60, 0.3, 0.15, 11); // 60 diem, seed CO DINH - tai lap duoc
const train = all.slice(0, 20); // cap nhat trong so
const val = all.slice(20, 40); // chon sieu tham so (capacity, lambda, epoch dung)
const test = all.slice(40, 60); // CHI danh gia 1 LAN duy nhat, sau khi da chot moi lua chon
| Tập | Dùng để làm gì | KHÔNG được dùng để làm gì |
|---|---|---|
| Train | Cập nhật trọng số (gradient descent chạy trên tập này) | — |
| Validation | Chọn siêu tham số (capacity, $\lambda$ L2, khi nào dừng) | Cập nhật trọng số trực tiếp |
| Test | Báo cáo kết quả cuối — CHỈ nhìn một lần, sau khi đã chốt mọi lựa chọn | Chọn bất kỳ thứ gì (siêu tham số, kiến trúc, thời điểm dừng) |
Nếu bạn thử nhiều lựa chọn (kiến trúc, seed, siêu tham số) rồi chọn cái nào cho điểm test đẹp nhất, con số đó không còn đáng tin — bạn đã ngầm "tối ưu hoá" cho đúng nhiễu riêng của tập test đó. Verify bằng số cụ thể: train $25$ mô hình cùng kiến trúc, chỉ khác seed khởi tạo ngẫu nhiên, trên cùng bộ dữ liệu $2$ trăng (moons) nhiễu:
| Cách chọn model để báo cáo | Test accuracy báo cáo |
|---|---|
| Đúng quy trình: chọn seed có val accuracy tốt nhất, báo test 1 LẦN | $0{,}84$ |
| RÒ RỈ: chọn seed có test accuracy tốt nhất trực tiếp (trong $25$ seed) | $0{,}92$ — nhưng đây là số cao nhất của $25$ lần thử, không phải hiệu năng thật |
| Test accuracy TRUNG BÌNH trên cả $25$ seed (không chọn lọc gì) | $0{,}837$ — gần với $0{,}84$ (đúng quy trình), rất xa $0{,}92$ |
Con số $0{,}92$ "đẹp" không sai về mặt tính toán — nó chỉ đơn giản là số cao nhất trong $25$ lần thử ngẫu nhiên, đúng hiệu ứng thống kê gọi là "multiple comparisons" (so sánh nhiều lần): thử càng nhiều, càng dễ tình cờ trúng một lần ăn may khớp với nhiễu riêng của bộ test đó. Đem model "$0{,}92$" này ra phục vụ dữ liệu thật (chưa từng xuất hiện trong toàn bộ quá trình), điểm số sẽ rơi về gần mức trung bình thật — "đẹp trên giấy, sập ngoài đời" đúng nghĩa đen. Quy tắc sống còn: test set chỉ được nhìn ĐÚNG MỘT LẦN, sau khi mọi quyết định (kiến trúc, $\lambda$, epoch dừng) đã chốt xong bằng validation set.
2. Bias–variance: 4 hình dạng loss curve kinh điển
Vẽ loss train và loss validation trên cùng một biểu đồ qua từng epoch — hình dạng của cặp đường này chẩn đoán chính xác vấn đề đang gặp phải. Dùng đúng mạng $2\to H\to1$ (Bài 6), dữ liệu $2$ trăng $20$ điểm train / $20$ điểm validation, $15\%$ nhãn bị nhiễu có chủ đích:
| Hình dạng | Train loss | Val loss | Chẩn đoán |
|---|---|---|---|
| Underfit ($H=1$, epoch 1800) | $0{,}1182$ (plateau CAO) | $0{,}2914$ (plateau CAO, gần train) | Model quá đơn giản — không đủ sức học ngay cả dữ liệu train |
| Vừa đủ ($H=64$, epoch 10) | $0{,}1820$ | $0{,}2272$ (ĐÁY — thấp nhất từng đạt) | Cả hai còn đang giảm, khoảng cách nhỏ — điểm tốt nhất để DỪNG |
| Overfit ($H=64$, epoch 5800) | $0{,}0411$ (rất thấp) | $0{,}4808$ (tăng NGƯỢC LẠI, tệ hơn cả epoch 0) | Model học thuộc lòng nhiễu của tập train, khoảng cách 2 đường doãng rộng |
Chú ý điều phản trực giác nhất bảng: cùng một mạng $H=64$, chỉ khác thời điểm dừng huấn luyện, cho ra $2$ hình dạng hoàn toàn khác nhau — "vừa đủ" ở epoch $10$, "overfit nặng" ở epoch $5800$. Val loss đạt đáy tại epoch $\approx 10$ rồi tăng liên tục từ đó — nghĩa là mọi epoch huấn luyện sau điểm đó chỉ làm model "thuộc lòng" thêm dữ liệu train mà không học thêm điều gì tổng quát. Đây chính là cơ sở của kỹ thuật early stopping (Mục 4): theo dõi val loss, dừng ngay khi nó ngừng giảm.
3. L2 Regularization / Weight Decay: phạt trọng số lớn
Trực giác: model overfit thường có trọng số lớn và "gồ ghề" — cần trọng số cực lớn để uốn boundary ôm sát từng điểm nhiễu riêng lẻ. L2 regularization phạt trực tiếp độ lớn trọng số bằng cách cộng thêm vào loss:
$$L_{total} = L_{data} + \frac{\lambda}{2}\sum_i w_i^2$$
Đạo hàm của số hạng phạt theo $w_i$ đơn giản là $\lambda w_i$ — nên trong code, L2 chỉ là cộng thêm $\lambda \cdot w$ vào gradient trước khi cập nhật (không cần đưa qua computation graph autograd, rẻ hơn nhiều):
function sgdStep(params, lr, l2) {
for (const k in params) {
const p = params[k];
for (let i = 0; i < p.size; i++) {
const g = p.grad[i] + l2 * p.data[i]; // weight decay: cong them lambda*w vao gradient
p.data[i] -= lr * g;
}
}
}
Quét $\lambda$ trên đúng mạng $H=64$ đã overfit nặng ở Mục 2 (huấn luyện $5800$ epoch — cùng số epoch đã cho train loss $0{,}0411$/val loss $0{,}4808$ khi $\lambda=0$):
| $\lambda$ | Train acc | Val acc | Nhận xét |
|---|---|---|---|
| $0$ | $0{,}950$ | $0{,}400$ | Overfit nặng — val TỆ HƠN đoán ngẫu nhiên ($0{,}5$) |
| $0{,}01$ | $0{,}900$ | $0{,}550$ | Bắt đầu cải thiện |
| $0{,}05$ | $0{,}850$ | $0{,}600$ | Cân bằng tốt nhất trong bảng — 2 số gần nhau hơn |
| $0{,}1$ (quá liều) | $0{,}550$ | $0{,}600$ | Phạt QUÁ MẠNH — train acc SẬP theo, giờ lại underfit |
$\lambda$ không phải "càng lớn càng tốt" — quá liều biến bài toán từ overfit sang underfit (train accuracy
chính nó cũng sập, từ $95\%$ xuống còn $55\%$, gần như đoán ngẫu nhiên). L2 gần như mặc định được bật
trong mọi model thật (PyTorch: tham số weight_decay= của optimizer) chính vì chi phí gần như
$0$ (chỉ 1 phép cộng) nhưng hiệu quả chống overfit rõ rệt ở mức $\lambda$ vừa phải.
4. Dropout & Early Stopping
Dropout: trong lúc huấn luyện, tắt ngẫu nhiên mỗi neuron với xác suất $p$ ở mỗi bước — buộc mạng không được "ỷ lại" vào bất kỳ neuron đơn lẻ nào, tương đương huấn luyện ngầm một ensemble của $2^n$ mạng con (mỗi tổ hợp bật/tắt neuron là 1 mạng con khác nhau) rồi lấy trung bình chúng. Cài đặt "inverted dropout" chuẩn: neuron còn sống được nhân bù $1/(1-p)$ để giữ nguyên tổng kỳ vọng đầu ra:
function dropoutMask(n, H, p, rng) {
const keepScale = 1 / (1 - p);
const rows = [];
for (let i = 0; i < n; i++) {
const row = [];
for (let h = 0; h < H; h++) row.push(rng() < p ? 0 : keepScale); // tat voi xac suat p, con lai NHAN BU
rows.push(row);
}
return Tensor.fromNested(rows);
}
// a1_dropped = mul(a1, mask) — dung DUNG phep mul() da co san, khong can op moi
Dropout CHỈ được bật lúc huấn luyện — lúc dự đoán thật (inference), phải dùng toàn bộ neuron (không mask). Quên bước này verify bằng số: huấn luyện $1$ mạng với dropout $p=0{,}5$, rồi gọi dự đoán $2$ lần trên CÙNG một input:
| Cách eval | Lần gọi 1 | Lần gọi 2 | Ổn định? |
|---|---|---|---|
| ĐÚNG (tắt dropout) | $[0{,}4347,\ 0{,}9475,\ 0{,}0693,\ \ldots]$ | $[0{,}4347,\ 0{,}9475,\ 0{,}0693,\ \ldots]$ | Giống hệt nhau tuyệt đối |
| SAI (quên tắt dropout) | $[0{,}430,\ 0{,}973,\ 0{,}016,\ \ldots]$ | $[0{,}503,\ 0{,}690,\ 0{,}053,\ \ldots]$ | Dao động rõ rệt mỗi lần gọi |
Accuracy đo trên cùng $1$ tập test, cùng $1$ model đã đóng băng, với eval SAI cách dao động $[0{,}85,\
0{,}85,\ 0{,}80,\ 0{,}80,\ 0{,}75]$ qua $5$ lần gọi liên tiếp — cùng một model, cùng dữ liệu, điểm số
vẫn nhảy loạn xạ tuỳ may rủi neuron nào bị tắt lần đó. PyTorch giải quyết bằng đúng $2$ dòng:
model.train() (bật dropout) trước vòng lặp huấn luyện, model.eval() (tắt
dropout, dùng toàn bộ neuron) trước khi dự đoán thật — quên gọi .eval() là lỗi cực kỳ phổ
biến khi mới triển khai model.
Early stopping — "regularization miễn phí" đơn giản nhất: theo dõi val loss mỗi vài epoch, lưu lại bộ trọng số tại điểm val loss THẤP NHẤT, dừng huấn luyện khi val loss không cải thiện sau một số epoch liên tiếp (patience). Áp lên đúng ví dụ Mục 2: val loss đạt đáy $0{,}2272$ ở epoch $\approx 10$; early stopping sẽ dừng quanh đó và LƯU LẠI bộ trọng số của epoch $10$ — thay vì chạy tiếp tới epoch $5800$ và kết thúc với val loss tệ hơn gấp đôi ($0{,}4808$). Không cần công thức toán học nào — chỉ cần biết dừng đúng lúc.
5. Thực hành: capacity + L2 — xem boundary từ mượt sang "ôm từng điểm nhiễu"
Chọn độ rộng mạng ($H$) và $\lambda$ L2, bấm Train để huấn luyện live trên đúng bộ dữ liệu $2$ trăng nhiễu của Mục 2 — quan sát boundary bên trái và cặp loss curve bên phải tách nhau đúng lúc overfit bắt đầu:
Chấm đặc = điểm train, chấm viền = điểm validation. Thử $H=64$, $\lambda=0$: xem đường val (đỏ) tách khỏi đường train (xanh) và ĐI LÊN sau vài trăm epoch đầu. Bật $\lambda=0{,}05$ rồi Reset + Train lại: khoảng cách 2 đường thu hẹp rõ rệt.
Đối chiếu công nghiệp: 2 dòng PyTorch dễ quên nhất, đúng cạm bẫy dropout Mục 4:
# Doi chieu 1-1 voi cam bay dropout Muc 4 - cai dat: pytorch.org/get-started/locally
import torch.nn as nn
model = nn.Sequential(nn.Linear(2, 64), nn.ReLU(), nn.Dropout(0.5), nn.Linear(64, 1), nn.Sigmoid())
optimizer = torch.optim.SGD(model.parameters(), lr=0.3, weight_decay=0.05) # L2 co san trong optimizer
model.train() # BAT dropout - dung luc huan luyen
# ... vong lap huan luyen, loss.backward(), optimizer.step() ...
model.eval() # TAT dropout - BAT BUOC truoc khi du doan that
with torch.no_grad():
pred = model(x_new) # thieu dong nay -> dung ket qua giong bang "SAI" o Muc 4
Tóm lược
- ✅ Train/val/test có $3$ vai trò tách biệt — test set chỉ nhìn ĐÚNG $1$ lần; tinh chỉnh theo test là rò rỉ dữ liệu (verified: $0{,}92$ "đẹp" vs $0{,}837$ trung bình thật, chênh gần $9$ điểm phần trăm).
- ✅ 4 hình dạng loss curve: underfit (cả 2 cao, gần nhau), vừa đủ (cả 2 thấp, gần nhau — điểm dừng lý tưởng), overfit (train thấp, val TĂNG TRỞ LẠI) — verified trên cùng 1 mạng $H=64$ chỉ khác thời điểm dừng (epoch $10$ vs $5800$).
- ✅ L2/weight decay phạt trọng số lớn bằng $\lambda w$ cộng vào gradient — vừa đủ thì giảm overfit rõ rệt, quá liều thì gây underfit ngược (verified: train acc sập từ $95\%$ xuống $55\%$ ở $\lambda=0{,}1$).
- ✅ Dropout = ensemble ngầm $2^n$ mạng con, PHẢI tắt lúc inference — quên tắt làm dự đoán dao động không kiểm soát được (verified: accuracy nhảy $0{,}75$–$0{,}85$ trên cùng model, cùng dữ liệu).
- ✅ Early stopping: theo dõi val loss, dừng và lưu trọng số tại điểm THẤP NHẤT — quy tắc chống overfit không cần công thức toán nào.
Tải file code thực hành minh họa bài học
File JavaScript gồm dữ liệu 2 trăng (moons), MLP + L2 + dropout (forward dùng NeuroJS, backward tự động
qua autograd Bài 7), với self-test đối chiếu mọi con số trích dẫn trong bài (chạy
node overfitting_demo.js):
Bình luận