你所不知道的 Pytorch 大補包(十一):Pytorch 如何實驗 Backpropagation 之 Pytorch AutoGrad 幫我們做了什麼事?
本文接續著上一篇 [你所不知道的 Pytorch 大補包(十):Pytorch 如何實做出 Backpropagation 之什麼是 Backpropagation] 繼續更深入的了解 Pytorch 的底層
keywords: AutoGrad
pytorch AutoGrad 幫我們做了什麼事?
以上我們成功的用 numpy 手刻了一個超~簡單的神經網路出來,並且訓練它,還取得了 100% 正確率的成果,但剛剛 Foward 的函式很簡單,簡單到它的梯度甚至不用到 chain rule 就算得出來,如果今天是一個 100 層深的網路,那我們的算式就會變得超長,跟本沒有辨法像剛剛直接用一條算式表達出來
這個時候幫我們實作 Backpropagation 的 pytorch 的派上用場了,pytorch 使用 AutoGrad 自動的幫我們把所有梯度都算出來,並且也做完 Backpropagation,在解釋一切程式碼之前,我們再來回頭看看用 pytorch 寫出來的程式會多麼的簡潔
1 | import torch |
什麼是 requires_grad?
一般在建立一個新的 tensor 時,我們會使用 torch.tensor 來達成,但是如果 tensor 想要實作自動計算梯度的話,我們必需在後面加一個參數 requires_grad
1 | x = torch.tensor([1, 2, 3, 4], dtype=torch.float32) |
打開這個 requires_grad 屬性後,pytorch 會幫我們打開更多的屬性,而這些屬性是只有在做 Backpropagation 時才會用到的,所以平常把它關起來減少記憶體的消耗。還記得前面有提到訓練網路的四大步驟嗎?等等會依照這四個步驟的順序來介紹
Foward Pass 前傳導
前傳導其實就是由一堆運算式所組成,輸入一個值,經過這個複雜的運算式後得到一個結果就稱為 Foward Pass 前傳導,那如果我們在前傳導的式子中加入 required_grad 會發生什麼事情呢?以下用兩個程式來對比
1 | a = torch.tensor(2.0) |

1 | a = torch.tensor(2.0, requires_grad=True) |

可以由上圖分析出幾個重點:
- 打開 requires_grad 後,tensor 的所有運算操作都會畫成一個 Graph
- 一個運算當中只要有一個變量 requires_grad=True,未來運算所新增的 tensor 一樣會是 requires_grad=True
- 有三個新的屬性:grad、grad_fn、is_leaf
grad 值在前傳導時為 None,要等到做 Backpropagation 時才會把值填上去
grad_fn 是在前傳導時 pytorch 自動幫我們加上去的,意思是「對應運算符微分後的算式」pytorch 提供了一大堆的 grad_fn 以應付各種微分運算,加速 Backpropagation 的流程
is_leaf 為了要表示這個權重節點是不是在葉節點上,為什麼這個這麼重要呢?因為 pytorch 只會在葉結點上儲存 grad 資訊,目的是為了保留記憶體
Backpropagation
前傳導完,記錄了許多數值後,接著就利用這個數值來做 Backpropagation,程式以及對應的 Graph 如下:
1 | a = torch.tensor(2.0, requires_grad=True) |

可以看到,簡簡單單的一行 c.backward() pytorch 竟然幫我們做了這麼多事情…。宏觀上來看這一行指令幫我們算出來 a 的 grad 值 3,微觀上來看這一行指令幫我們畫了超多的圖…同時也吃掉了不少記憶體
首先當呼叫 c.backward() 時,pytroch 會先去尋找 grad_fn,接著根據 grad_fn 裡面的微分運算計算 grad,然後放進 AccumulateGrad,累計不同次運算的 grad,最後再放到對應節點權重的 grad 屬性中
以數字的例子為:
1 | 假設一開始初始值為 1.0 -> |
再用一個更複雜的例子來舉例:
1 | # 假設 pytorch 的運算變成這個樣子: |

覺得圖片變太複雜嗎 XD,沒關系我們一起從最下面慢慢算上去:
1 | 假設一開始初始值為 1.0 -> |
我們可以印印看結果是不是正如我們所計算的?
1 | # 假設 pytorch 的運算變成這個樣子: |
1 | a.grad -> 12.0 |
咦…怎麼跟想像中的答案不一樣…,a.grad d.grad 都是對的,c.grad e.grad 發生了什麼事…?其實剛剛也有提到,在 pytorch 中,只有葉結點 (is_leaf=True) 才會把 .grad 存起來,目的是為了節省不必要的記憶體,而且在圖中也可看到,資料流動 flow 如果不是指向葉結點,會直接把值放到下一個 grad_fn 中,不會有 AccumulateGrad 把值存到 .grad 中,因此 pytorch 才會跳提醒說這個操作不合理,並且回傳 None
那如果我們真的真的想要得到不是葉結點的 .grad 值呢?那我們就要在前傳導的時候先把它註冊下來,使用 retain_grad() 這個函式:
1 | import torch |
詳細 .retain_grad() 的說明可以看下面這個影片,裡面很詳細的介紹為什麼這樣就可以,以及一個新概念:hook 的用法:https://www.youtube.com/watch?v=syLFCVYua6Q
另外也可以發現,在圖中有一個 AccumulateGrad 的方塊,這是用來儲存每一次 Backpropagation 的結果,並把新算出來的 grad 與之前的相加存到 .grad 中間,也就是說它不會自動淨空!
所以通常在程式中我們會手動淨空 .grad 值,以確保每一次訓練時 .grad 都是最新的
1 | for epoch in range(1, epochs+1): |
至於為什麼要這樣設計,因為如果我們使用的設備記憶體不足,沒辨法一次結太多資料訓練,我們就可以使用 Gradient accumulation 的技巧,改成每訓練兩次更新一次參數,變向放大 Batch size
1 | for i,(image, label) in enumerate(train_loader): |
要怎麼去掉 requires_grad?
從上面的圖來看,只要我們在 tensor 中加入 require_grad 參數,pytorch 就會一直記錄追縱未來所有的運算,一直更新那一張大圖,記憶體開銷非常可觀,那我們要怎麼樣把圖中藍藍的那一堆 Backpropagation 專用的圖給去掉呢?一共有三個做法:
1 | # (1) x.requires_grad_(False) |
1 | # (2) x.detach() |
1 | # (3) torch.no_grad() |
最後一個 torch.no_grad() 最常看到,通常會在驗證、測試的程式碼中會出現,理由有兩個。
- 一、結省記憶體。有時候 nvidia 會噴記憶體不夠,不一定是 Batch size 設太大的問題,也有可能是驗證、測試時忘記寫到
torch.no_grad()把那一大堆 Backpropagation 的圖都載入了 - 二、不會更新參數。因為
torch.no_grad()把所有 Backpropagation 剛掉了,排除了所有會更新到參數的因素,因此可以放心的驗證、測試,而不用擔心動到網路的參數
以上就是全部的內容了!希望看完這篇文章可以更了解 pytorch 倒底背後幫我們做了什麼事情!
Reference
本篇文章大量參考了以下兩個 youtube:
(系列教學影片) PyTorch Tutorial 03 - Gradient Calculation With Autograd
(系列教學影片) PyTorch Tutorial 04 - Backpropagation - Theory With Example
(系列教學影片) PyTorch Tutorial 05 - Gradient Descent with Autograd and Backpropagation
PyTorch Autograd Explained - In-depth Tutorial
官方 Document 永遠是你最好的朋友
A GENTLE INTRODUCTION TO TORCH.AUTOGRAD
(iT邦幫忙) Day 2 動態計算圖:PyTorch's autograd (裡面有提到 in-place 的問題)
(Backpropagation 參考文章) 神经网络的传播(权重更新)
(為什麼我看不到 .grad?) Why cant I see .grad of an intermediate variable?