PyTorch 把 Keras 藏在 fit() 裡的事攤開來寫——每個 mini-batch 是一條六站生產線:歸零、前向、損失、反向、剪梯、更新。順序不是背誦題,是因果題:剪刀必須夾在梯度誕生(backward)與梯度使用(step)之間。本頁把三個錯序全部實跑:剪在 backward 前=剪空氣(clip 回傳 0.0);剪在 step 後=馬後炮(權重已被 40 倍超速更新);backward 後才歸零=冷凍庫(20 個 epoch 權重一動不動)。
PyTorch 分類模型要在每個 mini-batch 完成一次更新,並在更新前做 gradient clipping。下列執行順序何者正確?
for x, y in loader: # 一圈 = 一個 mini-batch # 依正確順序排列: # 六張工作卡等你排 # optimizer.zero_grad() # 歸零:清掉上一圈的梯度 # logits = model(x) # 前向:算預測 # loss = criterion(logits, y) # 損失:對答案 # loss.backward() # 反向:算梯度 # torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # 剪梯 # optimizer.step() # 更新:真正改權重 pass # 佔位:排好就把 pass 換掉
把一圈想成一次小考檢討:① 擦黑板(zero_grad——上一題的檢討筆記不能留著混進這一題)→ ② 作答(model(x) 前向算預測)→ ③ 對答案(criterion 算損失)→ ④ 逐科檢討(backward——算出每個參數「該往哪改、改多大」=梯度)→ ⑤ 檢討太激動就收斂點(clip——梯度太大先修剪)→ ⑥ 真正改(step——照梯度更新權重)。
順序的因果只有兩條:剪刀剪的是梯度——梯度在 ④ 誕生、在 ⑥ 被使用,所以剪刀只能站第五位;黑板要在寫字前擦——PyTorch 的梯度預設會累積(本頁實跑:連 backward 兩次,梯度恰為 2 倍),所以 ① 必須在 ④ 之前。
點擊後出現漸進式說明:白話說明 → 說清楚一點 → 常見錯誤與考點。
點擊後出現漸進式說明。
把六行按正確順序排好,一站一站走。右上角的「看位置」可以把這一站放回完整程式裡看。
optimizer.zero_grad() # 清掉上一圈留下的梯度
PyTorch 的梯度預設會累積——backward 算出的梯度是「加」到 .grad 上,不是覆蓋。不歸零就開算,上一圈的梯度會混進這一圈。本站實跑:不歸零連做三次 backward,梯度范數 8.72 → 17.44 → 26.16——恰好 1 倍、2 倍、3 倍(03 節實驗室可以自己按)。
為什麼要設計成累積?因為有正當用途:RNN 的多段 loss、以及「梯度累積」技巧(故意攢幾圈再更新,模擬大 batch——06 節)。預設累積、需要時自己歸零,是 PyTorch 的哲學:把控制權給你。
logits = model(x) # 資料流過網路,吐出每類的分數把這個 batch 的特徵 x 流過網路,得到 logits(每筆一排「各類分數」——上一頁 softmax 之前的那種原始分數)。PyTorch 會同時默默記下計算的每一步(計算圖),等一下 backward 就是沿著這份紀錄反著走。
loss = criterion(logits, y) # 預測 vs 正解,算出一個「爛的程度」criterion 是損失函數的慣用名——分類通常是 nn.CrossEntropyLoss()(它自帶 softmax,所以吃 logits 就好——與上一頁 Keras 配方同一家數學)。輸出一個純量 loss:這個 batch 猜得有多爛。
注意 ②③ 拆成兩行是 PyTorch 的日常寫法:logits 留著還能算準確率;Keras 的 fit 把這兩站藏在肚子裡。
loss.backward() # 沿計算圖反著走,算出每個參數的梯度
對 loss 呼叫 backward,PyTorch 沿著前向記下的計算圖反向傳播,替每個參數算出梯度、存進各自的 .grad。這一站之後,梯度才存在——所以任何「對梯度動手」的事(剪梯)都必須排在它後面。這正是選項 B 的死因:backward 之前剪,實跑 clip 回傳 0.0——沒有東西可剪。
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # 總范數 > 1 就整組等比縮到 1
把所有參數的梯度拼起來量總范數(總長度):若超過 max_norm,就整組等比縮小到剛好等於它——方向不變、只縮步伐。本站實跑:裁前總范數 22.957 → 裁後恰為 1.0,每個梯度乘上 0.0436;函式的回傳值就是裁前范數(順手可以拿來監控)。名字結尾的底線=就地修改:直接改 .grad 本人。
optimizer.step() # 照 .grad 的指示,真正修改權重
optimizer(SGD、Adam⋯⋯)讀取每個參數的 .grad,照自己的配方更新權重——整條生產線只有這一站真正動到權重。所以剪梯必須在它之前:step 一過,梯度就用掉了——選項 C 在 step 後才剪,實跑這一步權重位移 3.98(正確版上限 lr × 1.0 = 0.1)——40 倍超速,剪刀成了馬後炮。
把 A、B、C、D 各自的排法放上六站生產線——出問題的站直接標紅,配上實跑的後果:
PyTorch 的 backward 是「加到 .grad 上」不是「蓋過去」。同一個 batch,自己連按幾次看看:
它量的是全體參數梯度拼起來的總范數;超過 max_norm 就整組等比縮小——方向不變、只縮步伐。拉拉看:
六站排對之後,整條線跑 20 個 epoch(300 筆三群資料);再把同一組「瘋狂設定」(高學習率+深網路)分別在有剪/沒剪下跑:
順序的鐵律只有兩條(剪刀夾中間、歸零在反向前)——其他地方其實有彈性,考題也愛考:
| 變體 | 合法嗎 | 說明 |
|---|---|---|
| zero_grad 放迴圈開頭 | 合法 | 題目與本頁的寫法——最不容易忘。 |
| zero_grad 放 step 之後(圈尾) | 合法 | 等效:反正都在下一次 backward 之前。鐵律是「backward 前」,不是「第一行」。 |
| 故意 N 圈不歸零、攢滿才 step | 合法技巧 | 梯度累積:記憶體塞不下大 batch 時,累積 N 個小 batch 的梯度再一次更新——把「預設累積」變成功能。剪梯與歸零都移到 step 那一圈做。 |
| clip 移出「backward 之後、step 之前」 | 不合法 | B(剪空氣)與 C(馬後炮)的實跑下場見 02 節——這條沒有彈性。 |
| backward 之後、step 之前 zero_grad | 不合法 | D 的冷凍庫:把剛算好的梯度擦掉,step 拿零更新——實跑 20 epoch 權重一動不動。 |
兩個對照收尾:① clip_grad_value_——另一把剪刀,逐元素砍到 ±value(會改變梯度方向);clip_grad_norm_ 等比縮放、方向不變,實務與考題的主流是後者。② Keras 對照——上一頁的 model.fit() 把這六站全部藏進肚子裡自動執行;PyTorch 攤開讓你手寫,換來的是自由(想在哪站插東西都行),代價是順序要自己負責——這題考的就是那份責任。
兩條因果全滿足:歸零在反向前(黑板先擦——實跑不擦的話梯度 1×2×3× 累積);剪刀夾在梯度誕生後(backward)與使用前(step)。實跑 20 epoch:loss 0.927 → 0.075、準確率 0.977——六站生產線的標準運轉。
剪刀排在 backward 之前——那時梯度還不存在。實跑:clip 回傳 0.0(沒有東西可剪),接著 backward 生出范數 39.787 的大梯度、step 原封不動拿去更新——削波完全沒有發生,卻不會報任何錯。
剪刀排在 step 之後——梯度已經被用掉了。實跑:step 直接吃下未剪的 39.787,這一步權重位移 3.98(正確版的上限是 lr × max_norm = 0.1——40 倍超速);事後才剪的是「已經沒人要用的梯度」,白剪。
歸零排在 backward 之後——把剛算好的梯度整組擦掉。clip 面對零梯度(無事可做)、step 拿零更新。實跑 20 個 epoch:權重最大變化量 0.0、loss 全程卡在 2.62、準確率 0%——訓練跑完了,模型凍在初始狀態,訓了個寂寞。
再看一次同一個迴圈。這次你知道剪刀為什麼只能站第五位了。
PyTorch 分類模型要在每個 mini-batch 完成一次更新,並在更新前做 gradient clipping。下列執行順序何者正確?
for x, y in loader: # 一圈一個 batch optimizer.zero_grad() # ① 歸零:黑板先擦(backward 前) logits = model(x) # ② 前向 loss = criterion(logits, y) # ③ 損失 loss.backward() # ④ 反向:梯度誕生 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # ⑤ 剪梯 optimizer.step() # ⑥ 更新:梯度被使用
八題,全部都是本題的延伸。答錯會直接告訴你錯在哪。
「訓練迴圈順序」是中級科目三深度學習的必考題型,PyTorch 寫法近年占比漸增。最常見的六種問法:① 六步驟排順序(本題這種——用「剪刀夾中間、歸零在反向前」兩條因果推);② 問 zero_grad 的作用與不做的後果(梯度累積、方向污染);③ 問 gradient clipping 的目的與位置(防梯度爆炸;backward 後 step 前);④ 問 backward 與 step 各做什麼(算梯度 vs 用梯度更新權重);⑤ 問 clip_grad_norm_ 與 clip_grad_value_ 的差別(等比縮放 vs 逐元素截斷);⑥ 問梯度累積技巧(合法的「故意不歸零」)。一句口訣:歸零、前向、損失、反向、剪梯、更新——剪刀夾在反向與更新之間。
小考檢討流程:zero_grad=擦黑板(清掉上一題的檢討筆記);model(x)=作答(前向傳播,順便默默記下計算過程);criterion(logits, y)=對答案(算出「這批猜得多爛」的一個數字);backward()=逐科檢討(沿計算圖反向,替每個參數算出「該往哪改、改多大」=梯度,存進 .grad);clip_grad_norm_=檢討太激動就收斂點(總范數超標整組等比縮小);step()=真正動筆改(optimizer 讀 .grad 更新權重——整條線唯一動到權重的站)。順序的因果:梯度在④誕生、⑥被使用,剪刀⑤只能夾中間;黑板①要在寫字④之前擦。
是設計不是疏忽。backward 把新梯度加到 .grad 上(實跑:連按三次,范數恰為 1×、2×、3×),因為累積有正當用途:① 梯度累積技巧——GPU 記憶體塞不下 batch=256 時,用 batch=32 跑 8 圈不歸零、攢滿再 step,數學上等效大 batch;② 多個 loss 分頭 backward 再一起更新;③ RNN 的分段反傳。代價就是你要記得歸零——忘了的症狀:訓練不穩、loss 亂跳、效果莫名變差,而且不報錯。PyTorch 的哲學是把控制權(和責任)都給你;Keras 則是幫你全包。
三步:把所有參數的梯度視為一個大向量,算總 L2 范數;若總范數 ≤ max_norm,完全不動;若超過,每個梯度乘上 max_norm ÷ 總范數——整組等比縮小到總范數恰為 max_norm,方向完全不變。實跑:裁前 22.957、max_norm=1.0 → 縮放係數 0.0436、裁後恰為 1.0。回傳值是裁前的總范數——實務常拿來記錄監控(爆炸前兆是它持續飆高)。名字結尾的底線是 PyTorch 慣例:就地(in-place)修改 .grad 本人,不產生新物件。
全部實測。B(backward 前剪):那一刻 .grad 還不存在,clip 回傳 0.0(無物可剪);隨後 backward 生出范數 39.787 的梯度、step 原樣使用——削波從未發生,也不報錯。C(step 後剪):step 先用未剪梯度更新,單步權重位移 3.98——正確版上限是 lr×max_norm=0.1,等於 40 倍超速;事後剪的是已用完的梯度,白剪。D(backward 後歸零):剛算好的梯度被整組擦掉,clip 面對零梯度、step 拿零更新——20 個 epoch 權重最大變化量 0.0、loss 全程 2.62、acc 0%。三個都不會報錯——順序錯是典型的「無聲 bug」,跟第 9 頁的 shape、第 11 頁的負損失同族。
不一定。鐵律只有一條:在該圈的 backward 之前。所以「迴圈開頭」(本題寫法)與「上一圈 step 之後、圈尾」完全等效——都保證 backward 寫進乾淨的黑板。順帶兩個現代細節:① 新版 PyTorch 的 zero_grad 預設 set_to_none=True——把 .grad 設成 None 而不是零張量(更省記憶體、數學等效);② 也可以呼叫 model.zero_grad(),效果相同。考題若給「step 之後接 zero_grad」的迴圈,別急著判錯——檢查它是否仍在下一次 backward 之前。
兩把不同的剪刀。clip_grad_norm_(本題):量整體總范數、超標就全體等比縮小——梯度方向不變,只縮步伐;是 RNN/Transformer 訓練的主流選擇。clip_grad_value_:逐元素把每個梯度值硬砍到 [−value, +value]——大的元素被截平、小的不動,會改變梯度方向;簡單粗暴,特定場景才用。考點辨析:「等比縮放、方向不變」→ norm 版;「逐元素截斷」→ value 版。兩者都必須站在 backward 與 step 之間——位置的鐵律相同。
同一件事的兩種包裝。Keras(上一頁):compile() 裝好 loss 與 optimizer、fit() 自動執行「取 batch → 前向 → 損失 → 反向 → 更新」,六站藏在肚子裡——方便,但插手不易(要寫 callback 或自訂 train_step)。PyTorch:六站攤開手寫——多打幾行字,換來隨處可插手的自由:想剪梯度、想累積、想對不同層用不同學習率,直接寫在迴圈裡。本題正是這份自由的代價:順序自己負責。考證觀點:兩邊都要會認——Keras 考配方(上一頁)、PyTorch 考順序(本頁)。