Lesson 14 · 約 12 分鐘
Debugging Playbook
loss 不下降 / NaN / OOM 怎麼追 — 卡住時回來查
14 ・ Debugging Playbook:loss 不下降 / NaN / OOM 怎麼追
課程內容 99% 都假設「模型會跑、loss 會下降」。但真實學員 80% 的時間花 在模型不會跑上。這一講把實戰最常卡的五個狀況拆成決策樹,讓你下次撞到的 時候,30 秒就能定位問題、不是繞圈 3 小時。
0. 三個鐵則(每次撞牆先回來看)
1. Loss 不下降:5 種可能,30 秒鎖定
跑了 100 個 step,loss 紋風不動?照這順序排查:
1.1 學習率太小 / 太大
症狀:loss 一直平到底(太小)、或 loss 像心電圖(太大)。
驗法:把 lr 改成現在的 10× 跑 50 step,再改成 0.1× 跑 50 step,挑趨勢最像「平順下降」的那條。
1.2 標籤 / 損失函數對不上
症狀:loss 卡在某個「亂猜的常數」附近。對 N 類分類用 cross-entropy 而 loss 卡在 log(N) ≈ 2.30 ⇒ 模型在亂猜。
檢查:
- 標籤是不是真的對應到正確的 class?(印幾筆
(input, label)出來看) - Cross-entropy 在 PyTorch 是吃 logits(不是 softmax 後)— 你有沒有多 softmax 一次?
- 二分類用了
BCEWithLogitsLoss還是BCELoss?後者要先 sigmoid
1.3 梯度沒進去模型
症狀:loss 完全沒變,連數字小數點都一樣。
驗法:訓練前後印一個參數,看有沒有變:
w_before = model.layer.weight.detach().clone()
loss.backward()
optimizer.step()
w_after = model.layer.weight.detach()
print('changed:', (w_before - w_after).abs().max().item()) # 應該 > 0最常見的鬼:
optimizer = Adam(...)但傳的是[]或漏掉某層(list(model.parameters())確認)- 凍結層後沒
requires_grad=True解凍想訓的部分 - 計算圖在中間
.detach()掉了(常見於誤用 numpy 中介)
1.4 資料不對 / 模型容量不夠
症狀:loss 慢慢下,但永遠到不了你期待的位置。
先 sanity-check:模型在單一筆資料上能不能 overfit 到接近 0 loss?能 → 模型沒問題,問題在資料量 / 資料品質。不能 → 模型容量不夠或架構錯。
1.5 BatchNorm / Dropout / model.eval() 沒切
症狀:訓練 loss 漂亮,驗證像噪音 — 但實際是 eval() 沒切到。
驗法:print(model.training) 在 eval 階段該是 False。model.eval() + with torch.no_grad(): 兩個都要寫。
2. NaN / Inf:三條主要起源
2.1 學習率太大(最常見)
- 第一個 update 就讓 weight 爆 → 下個前向算
exp(...)直接 overflow → NaN。 - 解:lr ÷ 10。
- 加保險:
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)防梯度爆炸。
2.2 數值不穩的運算
exp(x)當x > 88就inf(float32)→ softmax 沒先減 max 必爆。code/01_attention_from_scratch.py的softmax有減np.max(x),看一下對齊。log(0)= -inf。log(x + 1e-9)是標準防身術。sqrt(x)當x浮點負小 → NaN。- 除法分母記得加 epsilon:
a / (b + 1e-8)。
2.3 資料 NaN
- 讀進來的
tensor自己就有 NaN(資料前處理有 bug)。 - 驗法:訓練前
assert not torch.isnan(x).any(),卡在第一筆出問題的 batch。
3. OOM(Out of Memory):五招由小到大
GPU 顯存爆掉時的處理順序(由便宜到貴):
3.1 batch_size ÷ 2
最便宜。多數時候直接解決。記得同時把 optimizer.zero_grad() 後配合 gradient accumulation:
for i, batch in enumerate(loader):
loss = model(batch) / ACCUM_STEPS
loss.backward()
if (i + 1) % ACCUM_STEPS == 0:
optimizer.step()
optimizer.zero_grad()等效 batch 不變,只是分批塞顯存。
3.2 開混合精度(AMP)
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler()
with autocast():
loss = model(batch)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()省 ~40% 顯存,訓練還可能更快。現代 GPU(Ampere+)幾乎必開。
3.3 gradient checkpointing
model.gradient_checkpointing_enable()(Hugging Face)— 用「重算前向」換顯存,省 ~50% activation memory,代價是慢 ~20%。微調大模型常用。
3.4 LoRA / QLoRA
只訓練插入的小矩陣,而不是全部 weight。對於 7B+ 模型,個人單卡的唯一路。本包 code/07_lora_finetune_reference.py 有最小範例。
3.5 換更大顯存的卡 / 多卡 DDP
走到這步基本是要進雲端 GPU 了(Colab Pro / Lambda / RunPod)。個人裝機別衝這條;雲端按小時付。
4. 過擬合 vs 欠擬合:看曲線
| 症狀 | train loss | val loss | 解法 |
|---|---|---|---|
| 欠擬合 | 高 | 跟 train 差不多高 | 模型太小 / 訓不夠 / lr 太小 → 加大、訓久、提高 lr |
| 過擬合 | 低 | 一開始一起降,後來上升 | regularization(dropout / weight decay / data aug)/ 多拿資料 / 早停 |
| 資料外洩 | 很低 | 也很低,但實際用就崩 | 訓練集摻到驗證集 / 用了未來資訊預測過去 |
| train 突然飆高 | 突然 spike | — | lr 太大、出 NaN、checkpoint load 錯 — 看 §2 |
5.「看起來對但實際崩」:Data Leakage Checklist
最難 debug 的是「指標漂亮、上線爆炸」。常見來源:
- ❌ 時序資料用了未來欄位預測過去(
y_t用了feature_{t+1}) - ❌ Cross-validation 沒按時間切,把同一個 user 的不同時刻分到 train/val
- ❌ 圖片資料增強(rotation / crop)放錯邊 — 在 train/val split 之前就 augment 等於兩邊看到相關樣本
- ❌ Tokenizer / scaler 拿全部資料 fit 再切 —
StandardScaler.fit(train)而不是.fit(all_data) - ❌ Eval set 含重複樣本或重複 paragraph(LLM 評測常踩)
6. 黃金 5 步 debugging workflow
7. 常用 debug 工具速查
| 場景 | 工具 |
|---|---|
| 看 tensor 的 mean/std/min/max | tensor.mean().item() / tensor.std().item() / .min().item() / .max().item() |
| 抓 NaN | torch.isnan(tensor).any().item() |
| 看模型參數量 | sum(p.numel() for p in model.parameters()) |
| 看哪一層卡 | for n, p in model.named_parameters(): print(n, p.shape) |
| 看哪一層沒梯度 | for n, p in model.named_parameters(): print(n, None if p.grad is None else p.grad.norm().item()) |
| 設 random seed | torch.manual_seed(42); np.random.seed(42); random.seed(42) |
| 中斷除錯 | import pdb; pdb.set_trace()(VS Code 用 breakpoint 也可以) |
| 視覺化曲線 | wandb(雲端,免費 tier 夠用)、tensorboard(本機) |
8. 自我檢測
接下來
這份是「卡住的時候回來查」 的 reference,不是讀過就忘的內容。建議:
- 加到瀏覽器書籤 / IDE snippet,真撞到 NaN 或 OOM 時 30 秒打開對照
- 下一講:13 · 學習筆記範本 — 把你 debug 過的痛點寫進
notes/,累積成個人的「我跟模型打架的歷史」,下次同樣 bug 30 秒就解決