Chẩn đoán mạng không học: đọc đường cong loss
Xem thửThứ Hai, Minh nhận năm lần chạy huấn luyện mô hình dự đoán thời gian giao từ năm đồng nghiệp. Cùng dữ liệu 2.400 đơn, cùng mạng MLP một lớp ẩn, 12 epoch. Mỗi người sửa "một chút" trong script. Kết quả MAE kiểm tra: 11,1 phút; nan; 74 phút; 35,2 phút; và 10,1 phút.
Lần chạy 10,1 phút là tốt nhất, và Bình muốn đưa nó lên production ngay. Lần chạy 35,2 phút không báo lỗi gì, loss có giảm ở hai epoch đầu rồi đứng ở khoảng 1.600. Không ai đọc kỹ script của ai.
Trước khi đọc tiếp, hãy dự đoán: chỉ nhìn đường loss huấn luyện theo epoch, bạn có phân biệt được năm lỗi khác nhau không? Và lần chạy 10,1 phút có thật sự là mô hình tốt nhất?
Sau bài này, bạn có thể
- Đọc một đường loss theo epoch và xếp nó vào một trong các dạng: khỏe, nổ, phẳng, dừng sớm ở mức cao, tách đôi huấn luyện và kiểm tra.
- Nối mỗi dạng với nguyên nhân hay gặp (learning rate, quên
zero_grad, sai shape của nhãn, rò rỉ dữ liệu, overfitting) và cách kiểm chứng. - Chạy bài kiểm tra "học thuộc một batch" trước mọi lần huấn luyện dài.
Cần biết trước: cả sáu bài trước của chương này, đặc biệt Optimizer và Regularization; bài Chia dữ liệu theo thời gian (Chương 9).
Mức độ: Trung bình · Thời gian: khoảng 20 phút · Từ khóa: loss curve, debugging, zero_grad, data leakage, sanity check
Đường loss là nhật ký của quá trình học
Mỗi epoch ghi một con số loss huấn luyện, và nếu có, một con số trên tập kiểm định. Hai dãy số này là thứ rẻ nhất bạn có để biết bên trong đã xảy ra chuyện gì. Mô hình tư duy của bài: hình dạng đường loss cho biết loại lỗi. Không phải con số cuối, mà là cách nó đi tới đó.
| Dạng đường loss | Trông như thế nào | Nguyên nhân hay gặp |
|---|---|---|
| Khỏe | Giảm nhanh rồi chậm dần, phẳng ở mức thấp | Không có lỗi rõ ràng |
| Nổ | nan hoặc tăng vọt sau vài bước | Learning rate quá lớn, khởi tạo quá lớn |
| Phẳng từ đầu | Gần như không đổi ở mức cao | Learning rate quá nhỏ, gradient tắt dần, nhãn sai shape |
| Giảm rồi kẹt ở mức cao | Đi xuống vài epoch rồi đứng hoặc nhích lên | Quên zero_grad, learning rate hơi lớn |
| Tách đôi | Huấn luyện tiếp tục giảm, kiểm định tăng | Overfitting |
| Đẹp bất thường | Kiểm định tốt hơn mọi lần khác, không rõ vì sao | Rò rỉ dữ liệu |
Hai mốc giúp đọc số: loss khi đoán mọi đơn bằng trung bình (với dữ liệu này, MSE khoảng 862, tương ứng MAE khoảng 24 phút) và loss của một mô hình đơn giản như hồi quy tuyến tính (MAE khoảng 11 phút). Đường nằm quanh mốc thứ nhất nghĩa là mô hình chưa học gì.
Sơ đồ sau là trình tự chẩn đoán Minh dùng cho cả năm lần chạy.
Đang tải sơ đồ…
Cách đọc hình: bắt đầu từ đường loss (ô cyan) và hỏi lần lượt ba câu ở các ô cam. Có nan hoặc tăng vọt trong vài epoch đầu thì xử lý learning rate, khởi tạo, thang đo đầu vào. Loss huấn luyện kẹt gần mức "đoán trung bình" thì kiểm learning rate, độ lớn gradient, shape của nhãn và zero_grad. Kiểm định tăng trong khi huấn luyện giảm là overfitting. Qua cả ba câu mà vẫn ổn thì đường loss khỏe, nhưng ô xanh lá nhắc một điều đường loss không bao giờ cho thấy: rò rỉ dữ liệu, phải kiểm bằng cách đọc danh sách đặc trưng.
Animation dưới đây chạy lại lần chạy khỏe và lần chạy quên zero_grad theo từng epoch. Hãy đoán dạng đường trước khi bấm nút biến thể.
Đang tải Animation…
Hai đường bắt đầu gần nhau (4.510 và 3.250). Đường quên zero_grad thậm chí giảm nhanh hơn ở epoch 1, vì gradient cộng dồn làm các bước đầu dài hơn, giống momentum không có hệ số giảm. Sau đó gradient cộng dồn quá lớn, mỗi bước bắn tham số đi xa và loss kẹt quanh 1.600, gấp gần 10 lần đường khỏe.
Làm cùng mình
Bước 1: tái hiện năm lần chạy. Một hàm huấn luyện có các công tắc gây lỗi; năm kịch bản chỉ khác nhau ở công tắc.
def run(lr, zero_grad=True, leak=False, epochs=12):
Xa, Xb = (Ltr, Lva) if leak else (Xtr, Xva)
torch.manual_seed(0)
model = nn.Sequential(nn.Linear(Xa.shape[1], 32), nn.ReLU(), nn.Linear(32, 1))
opt = torch.optim.SGD(model.parameters(), lr=lr)
g = torch.Generator().manual_seed(0)
curve = []
for epoch in range(epochs):
model.train()
total = 0.0
for idx in torch.randperm(len(Xa), generator=g).split(128):
loss = nn.functional.mse_loss(model(Xa[idx]), ytr[idx])
if zero_grad:
opt.zero_grad()
loss.backward()
opt.step()
total += loss.item() * len(idx)
curve.append(total / len(Xa))
return curve, mae(model, Xb, yva)
| Dòng code | Máy làm gì | Vì sao |
|---|---|---|
Ltr, Lva = torch.cat([..., late[...]], 1) | Thêm cột is_late vào đặc trưng | Cột này chỉ biết sau khi giao xong: rò rỉ |
if zero_grad: opt.zero_grad() | Tắt lời gọi xóa gradient ở kịch bản lỗi | Gradient các bước trước cộng dồn vào bước sau |
torch.optim.SGD(..., lr=lr) | Cùng optimizer, chỉ đổi learning rate | Tách riêng ảnh hưởng của lr |
curve.append(total / len(Xa)) | Ghi loss trung bình mỗi epoch | Đây là đường cần đọc |
Kết quả [Đã chạy] Python 3.12, PyTorch 2.15 (CPU), NumPy 2.5, pandas 3.1, ngày 04/11/2026:
scenario train loss (MSE) at epoch 1, 2, 3, 6, 12 val MAE
healthy lr=0.001 4.51e+03, 569, 178, 171, 166 11.1
lr too high 0.05 nan, nan, nan, nan, nan nan
lr too low 1e-6 5.53e+03, 5.53e+03, 5.53e+03, 5.52e+03, 5.51e+03 74
no zero_grad 3.25e+03, 1.83e+03, 1.45e+03, 1.58e+03, 1.6e+03 35.2
leak is_late 4.49e+03, 522, 152, 145, 141 10.1
Đọc từng dòng theo bảng dạng đường:
healthy lr=0.001: 4.510 rồi 569 rồi 178, phẳng quanh 166. Dạng khỏe, MAE kiểm tra 11,1 phút, ngang hồi quy tuyến tính.lr too high 0.05:nanngay epoch 1. Dạng nổ.lr too low 1e-6: 5.530 suốt 12 epoch, chỉ giảm ở chữ số thứ ba. Dạng phẳng; MAE 74 phút, còn tệ hơn đoán trung bình vì mô hình vẫn đoán quanh 0.no zero_grad: 3.250, 1.830, 1.450 rồi nhích lên 1.580, 1.600. Dạng giảm rồi kẹt ở mức cao; MAE 35,2 phút.leak is_late: 4.490, 522, 152, phẳng quanh 141. Dạng khỏe, thậm chí đẹp hơn lần chạy khỏe; MAE 10,1 phút.
Câu trả lời cho câu hỏi mở bài: bốn lỗi đầu phân biệt được bằng hình dạng. Lần chạy 10,1 phút thì không: đường của nó khỏe y như lần chạy đúng. Nó tốt hơn chỉ vì được nhìn thấy is_late, một cột suy ra từ chính thời gian giao (DATA_CARD ghi rõ is_late là delivery_minutes > promised_minutes). Khi dự đoán cho đơn mới, cột này chưa tồn tại. Đưa lần chạy này lên production là đưa một mô hình không chạy được, hoặc tệ hơn, chạy với cột bị điền giá trị mặc định. Rò rỉ chỉ phát hiện được bằng cách đọc danh sách đặc trưng và hỏi "cột này biết được vào lúc nào?".
Bước 2: một lỗi im lặng, nhãn sai shape. Mô hình trả ra shape (128, 1); nếu nhãn có shape (128,), phép trừ trong MSE dùng broadcasting (Chương 2) và tạo bảng (128, 128).
for name, target in [("y shape (n, 1)", ytr), ("y shape (n,)", ytr.squeeze(1))]:
torch.manual_seed(0)
model = nn.Sequential(nn.Linear(Xtr.shape[1], 32), nn.ReLU(), nn.Linear(32, 1))
opt = torch.optim.Adam(model.parameters(), lr=1e-2)
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
for step in range(300):
idx = torch.randint(0, len(Xtr), (128,), generator=torch.Generator().manual_seed(step))
loss = nn.functional.mse_loss(model(Xtr[idx]), target[idx])
opt.zero_grad()
loss.backward()
opt.step()
full = nn.functional.mse_loss(model(Xtr), ytr).item()
print(f"{name:15s} loss shown: {loss.item():7.1f} true train MSE: {full:7.1f} warnings: {len(caught)}")
if caught:
print(" warning:", str(caught[0].message).split(". ")[0] + ".")
Kết quả [Đã chạy] Python 3.12, PyTorch 2.15 (CPU), NumPy 2.5, pandas 3.1, ngày 04/11/2026:
y variance (MSE of always predicting the mean): 862.5
y shape (n, 1) loss shown: 177.8 true train MSE: 179.8 warnings: 0
y shape (n,) loss shown: 846.4 true train MSE: 857.2 warnings: 300
warning: Using a target size (torch.Size([128])) that is different to the input size (torch.Size([128, 1])).
Với nhãn shape (n,), loss kẹt ở 846, sát 862 là mức đoán trung bình: dạng phẳng, mô hình học được đúng một điều là giá trị trung bình, vì mỗi dự đoán bị so với cả 128 nhãn trong batch. Không có exception nào. PyTorch có in một cảnh báo (UserWarning), 300 lần, nhưng cảnh báo rất dễ bị trôi trong log. Cách phòng: assert pred.shape == y.shape trước khi tính loss.
Bước 3: kiểm tra "học thuộc một batch". Trước khi huấn luyện lâu, cho mô hình học đi học lại đúng 32 đơn. Một mô hình và vòng lặp đúng phải làm loss trên 32 đơn đó gần về 0, vì nó đủ tham số để nhớ chúng.
xb, yb = Xtr[:32], ytr[:32] # đúng một batch 32 đơn
torch.manual_seed(0)
model = nn.Sequential(nn.Linear(Xtr.shape[1], 64), nn.ReLU(), nn.Linear(64, 1))
opt = torch.optim.Adam(model.parameters(), lr=1e-2)
for step in range(1, 1001):
loss = nn.functional.mse_loss(model(xb), yb)
opt.zero_grad()
loss.backward()
opt.step()
if step in (1, 100, 300, 1000):
print(f"step {step:4d}: loss on the same 32 orders = {loss.item():9.3f}")
Kết quả [Đã chạy] Python 3.12, PyTorch 2.15 (CPU), NumPy 2.5, pandas 3.1, ngày 04/11/2026:
step 1: loss on the same 32 orders = 5458.558
step 100: loss on the same 32 orders = 81.952
step 300: loss on the same 32 orders = 8.106
step 1000: loss on the same 32 orders = 0.134
Loss đi từ 5.459 xuống 0,134 sau 1.000 bước. Nếu bài kiểm tra này không qua, đừng chạy huấn luyện dài: lỗi nằm ở vòng lặp, ở nhãn, hoặc ở kiến trúc, và thêm dữ liệu không sửa được. Bài kiểm tra mất vài giây; ở kịch bản nhãn sai shape, loss sẽ kẹt quanh phương sai của 32 nhãn thay vì về 0.
Áp dụng vào Giao Hàng Anh Tí
Minh viết thành quy trình bắt buộc cho mọi lần huấn luyện mô hình dự đoán thời gian giao:
- Trước khi chạy dài: học thuộc một batch,
assert pred.shape == y.shape, in độ lớn gradient từng lớp ở bước đầu. - Khi chạy: ghi loss huấn luyện, loss kiểm định và learning rate mỗi epoch vào cùng một bảng; so với hai mốc "đoán trung bình" và "hồi quy tuyến tính".
- Với mỗi đặc trưng: ghi thời điểm nó được biết (lúc tạo đơn, lúc lấy hàng, sau khi giao). Cột nào biết sau thời điểm dự đoán thì bỏ.
is_late,delivery_minutesvà mọi cột suy ra từ chúng nằm trong danh sách cấm. - Kết quả tốt bất thường là tín hiệu cần kiểm tra, không phải tin vui.
Lab cuối chương đưa cho bạn một script có ba lỗi trong số này để sửa và giải thích bằng đường loss.
Hiểu lầm thường gặp
Hiểu lầm 1: "Không có exception thì code huấn luyện đúng." Ba trong năm lần chạy (lr quá nhỏ, quên zero_grad, rò rỉ) và lỗi nhãn sai shape đều chạy hết mà không báo lỗi. Lỗi huấn luyện phần lớn là lỗi im lặng; chỉ đường loss và các bài kiểm tra chủ động mới lộ ra chúng.
Hiểu lầm 2: "Loss kiểm định thấp nhất là mô hình tốt nhất." Chỉ khi các đặc trưng hợp lệ. Lần chạy rò rỉ có loss kiểm định thấp nhất nhưng là mô hình duy nhất không dùng được.
Hiểu lầm 3: "Loss không giảm thì tăng learning rate." Loss phẳng có nhiều nguyên nhân: lr quá nhỏ, nhưng cũng có thể là gradient tắt dần (bài Khởi tạo) hoặc nhãn sai shape. Tăng lr với nhãn sai shape không sửa được gì; tăng lr khi đã gần giới hạn thì gây nổ.
| Triệu chứng | Nguyên nhân | Cách kiểm chứng | Cách sửa |
|---|---|---|---|
nan ở epoch 1 | Lr quá lớn | Giảm lr 10 lần, loss hết nan | Lr nhỏ hơn, warmup, chuẩn hóa đầu vào |
| Loss phẳng ở mức gần "đoán trung bình" | Lr quá nhỏ, gradient tắt, hoặc nhãn sai shape | In gradient từng lớp; assert pred.shape == y.shape | Tùy nguyên nhân: tăng lr, khởi tạo He, sửa shape |
Loss giảm rồi kẹt cao, không có nan | Quên opt.zero_grad() | Gọi hai bước liên tiếp trên cùng batch, so gradient với gradient tính lại từ đầu | Gọi zero_grad() mỗi bước |
| MAE kiểm định tốt hơn hẳn mọi lần chạy khác | Rò rỉ dữ liệu | Liệt kê đặc trưng và thời điểm mỗi cột được biết | Bỏ cột rò rỉ |
| Huấn luyện giảm, kiểm định tăng | Overfitting | Vẽ hai đường cạnh nhau | Early stopping, regularization, thêm dữ liệu |
Tự kiểm tra (có đáp án)
Câu 1 (giải thích). Vì sao lần chạy quên zero_grad lại giảm loss nhanh hơn lần chạy khỏe ở epoch 1 (3.250 so với 4.510)?
Đáp án: gradient của các bước trước được cộng vào bước sau, nên những bước đầu (khi gradient còn cùng hướng) dài hơn, giống momentum không có hệ số giảm. Về sau tổng gradient cộng dồn quá lớn và không bao giờ được xóa, nên mỗi bước đi quá xa và loss kẹt ở khoảng 1.600.
Câu 2 (dự đoán). Chạy bài kiểm tra học thuộc một batch với nhãn shape (32,) trong khi mô hình trả (32, 1). Loss sau 1.000 bước sẽ gần 0 hay không?
Đáp án: không. Mỗi dự đoán bị so với cả 32 nhãn, nên tốt nhất mô hình chỉ có thể đoán trung bình của batch; loss kẹt quanh phương sai của 32 nhãn đó.
Câu 3 (vận dụng). Hà đề xuất thêm đặc trưng shipper_pickup_minutes (số phút shipper mất để tới lấy hàng) vào mô hình dự đoán thời gian giao lúc khách đặt đơn. Bạn hỏi lại câu gì trước khi chấp nhận?
Đáp án: "giá trị này biết được vào lúc nào?". Nếu mô hình dự đoán lúc khách đặt đơn, số phút lấy hàng chưa xảy ra nên là rò rỉ. Nếu dự đoán lại sau khi shipper đã lấy hàng thì dùng được, nhưng đó là một mô hình khác với thời điểm dự đoán khác.
Tự tay làm
Trong loss_curve_shapes.py, thêm kịch bản ("lr 0.01", dict(lr=1e-2)). Trước khi chạy, đặt nó vào bảng dạng đường: lr 0,01 nằm giữa 0,001 (khỏe) và 0,05 (nổ). Bạn đoán nó khỏe, nổ, hay giảm rồi kẹt?
Kết quả mong đợi để tự kiểm: loss 1.840, 585, 351, 244, 197 ở epoch 1, 2, 3, 6, 12 và MAE kiểm tra 13,4 phút. Không nổ, không kẹt, nhưng cũng không khỏe bằng lr 0,001: epoch 1 giảm nhanh hơn, rồi giảm chậm và dừng ở mức cao hơn (197 so với 166), vì bước lớn làm tham số dao động quanh điểm tốt (bài Learning rate schedule). Viết một câu: với kết quả này, bạn sẽ thử lịch learning rate nào.
Điều cần nhớ
- Hình dạng đường loss cho biết loại lỗi: nổ là lr hoặc khởi tạo, phẳng là lr nhỏ, gradient tắt hoặc nhãn sai shape, giảm rồi kẹt là quên
zero_grad, tách đôi là overfitting. - Phần lớn lỗi huấn luyện là lỗi im lặng; không có exception không có nghĩa là đúng.
- Rò rỉ dữ liệu không hiện trên đường loss; nó làm đường đẹp hơn. Kiểm bằng cách hỏi mỗi đặc trưng được biết vào lúc nào.
- Học thuộc một batch và
assert pred.shape == y.shapetrước mọi lần chạy dài. Chương 12 viết lại vòng lặp này thành vòng lặp PyTorch chuẩn, có train, eval và checkpoint.
Nguồn tham khảo
- Karpathy (2019), A Recipe for Training Neural Networks: https://karpathy.github.io/2019/04/25/recipe/
- PyTorch,
torch.optim.Optimizer.zero_grad: https://docs.pytorch.org/docs/stable/generated/torch.optim.Optimizer.zero_grad.html - PyTorch,
torch.nn.MSELoss(yêu cầu shape của input và target): https://docs.pytorch.org/docs/stable/generated/torch.nn.MSELoss.html - Stanford CS231n, Neural Networks Part 3 (theo dõi loss và learning rate khi huấn luyện): https://cs231n.github.io/neural-networks-3/
- AI Engineering from Scratch (MIT), phase 03, Debugging neural networks: https://github.com/rohitg00/ai-engineering-from-scratch/tree/main/phases/03-deep-learning-core/13-debugging-neural-networks