使用torch.autograd自動微分
- 張量、函數和計算圖
- 計算梯度
- 禁用梯度追蹤
- 關于計算圖的更多信息
- 張量梯度和雅可比乘積
在訓練神經網絡時,最常用的算法是反向傳播。在該算法中,參數(模型權重)根據損失函數的梯度相對于給定參數進行調整。
為了計算這些梯度,PyTorch有一個內置的微分引擎,名為torch.autograd。它支持為任何計算圖自動計算梯度。
考慮最簡單的一層神經網絡,具有輸入x、參數w和b以及一些損失函數。它可以通過以下方式在PyTorch中定義:
import torchx = torch.ones(5) # input tensor
y = torch.zeros(3) # expected output
w = torch.randn(5, 3, requires_grad=True)
b = torch.randn(3, requires_grad=True)
z = torch.matmul(x, w)+b
loss = torch.nn.functional.binary_cross_entropy_with_logits(z, y)
張量、函數和計算圖
剛才的代碼定義了以下計算圖:
在這個網絡中,w和b是參數,我們需要優化。因此,我們需要能夠計算關于這些變量的損失函數的梯度。
為了做到這一點,我們設置了這些張量的requires_grad
屬性。
注:您可以在創建張量時設置`requires_grad`的值,或者稍后使用`x.requires_grad_(True)`方法。
在 PyTorch 中,用于構建計算圖的張量操作函數實際上是Function類的對象。該對象不僅負責處理正向傳播時的函數計算,還能在反向傳播過程中計算導數。反向傳播函數的引用會存儲在張量的grad_fn
屬性中。你可以在官方文檔中找到關于Function類的更多詳細信息。
print(f"Gradient function for z = {z.grad_fn}")
print(f"Gradient function for loss = {loss.grad_fn}")
# 輸出
Gradient function for z = <AddBackward0 object at 0x7fdf8cca1c30>
Gradient function for loss = <BinaryCrossEntropyWithLogitsBackward0 object at 0x7fdf8cca2c20>
計算梯度
為了優化神經網絡中參數的權重,我們需要計算損失函數關于參數的導數。也就是說,在輸入值 x 和目標值 y 的固定取值下,我們需要計算 ? l o s s ? w \frac{\partial loss}{\partial w} ?w?loss? 和 ? l o s s ? b \frac{\partial loss}{\partial b} ?b?loss?。要計算這些導數,我們只需調用 loss.backward()
,然后從參數 w.grad
和 b.grad
中獲取對應的導數值。
loss.backward()
print(w.grad)
print(b.grad)
# 輸出
tensor([[0.3313, 0.0626, 0.2530],[0.3313, 0.0626, 0.2530],[0.3313, 0.0626, 0.2530],[0.3313, 0.0626, 0.2530],[0.3313, 0.0626, 0.2530]])
tensor([0.3313, 0.0626, 0.2530])
注:我們只能獲得計算圖葉節點的grad屬性,因為這些葉節點的requires_grad屬性是True。對于圖中其他的節點,獲得梯度屬性的方法將不可用。
另外出于性能原因,我們只能在給定圖上使用backward執行一次梯度計算。如果我們需要在同一張圖上執行幾個backward調用,我們需要將retain_graph=True傳遞給backward調用。
禁用梯度追蹤
默認情況下,所有具有requires_grad=True
的張量都在跟蹤它們的計算歷史并支持梯度計算。
但是,有些情況下我們不需要這樣做。
例如,當我們已經訓練了模型并只想將其應用于一些輸入數據時,即我們只想通過網絡進行轉發計算。我們可以通過用torch.no_grad()
塊:
z = torch.matmul(x, w)+b
print(z.requires_grad)with torch.no_grad():z = torch.matmul(x, w)+b
print(z.requires_grad)# 輸出
True
False
實現相同結果的另一種方法是使用對張量detach()
方法:
z = torch.matmul(x, w)+b
z_det = z.detach()
print(z_det.requires_grad)
# 輸出
False
可能想要禁用漸變跟蹤的原因如下:
- 將神經網絡中的某些參數標記為凍結參數。
- 當您只進行前向傳遞時,要加快計算速度,因為不跟蹤梯度的張量上的計算會更有效。
關于計算圖的更多信息
從概念上講,自動微分在由Function對象組成的有向無環圖(DAG)中記錄數據(張量)和所有執行的操作(以及生成的新張量)。
在這個有向無環圖(DAG)中,葉是輸入張量,根是輸出張量。通過從根到葉跟蹤這個圖,您可以使用鏈式規則自動計算梯度。
在向前傳播中,自動微分同時做兩件事:
- 運行請求的操作以計算生成的張量
- 在有向無環圖中維護操作的梯度函數。
當在有向無環圖(DAG)的根節點上調用 .backward()
方法時,反向傳播過程就啟動了。隨后,自動求導系統會進行以下操作:
- 計算每個
.grad_fn
的梯度。 - 將它們累加到相應張量的
.grad
屬性中 - 使用鏈式規則,一直傳播到圖的葉張量(葉節點)。
在 PyTorch 中,有向無環圖(DAG)是動態的。需要重點理解的是:圖會 從頭重新構建;每次調用 .backward()
后,自動求導機制(autograd
)都會開始生成一幅新圖。而這正是模型中能使用控制流語句(如循環、條件判斷)的關鍵——如果有需求,你完全可以在每次迭代時調整圖的形狀、規模以及具體操作。
張量梯度和雅可比乘積
在許多場景中,我們會用到標量損失函數,此時需要計算損失函數關于某些參數的梯度。但也存在輸出函數是任意張量的情況。這時,PyTorch 支持計算所謂的 雅可比積,而非直接計算實際的梯度。
對于向量函數 y ? = f ( x ? ) \vec{y} = f(\vec{x}) y?=f(x)(其中 x ? = ? x 1 , . . . , x n ? \vec{x} = \langle x_1, ..., x_n \rangle x=?x1?,...,xn??, y ? = ? y 1 , . . . , y m ? \vec{y} = \langle y_1, ..., y_m \rangle y?=?y1?,...,ym??), y ? \vec{y} y? 關于 x ? \vec{x} x 的梯度由雅可比矩陣表示:
J = ( ? y 1 ? x 1 ? ? y 1 ? x n ? ? ? ? y m ? x 1 ? ? y m ? x n ) J = \begin{pmatrix} \frac{\partial y_1}{\partial x_1} & \cdots & \frac{\partial y_1}{\partial x_n} \\ \vdots & \ddots & \vdots \\ \frac{\partial y_m}{\partial x_1} & \cdots & \frac{\partial y_m}{\partial x_n} \end{pmatrix} J= ??x1??y1????x1??ym????????xn??y1????xn??ym??? ?
PyTorch 并不直接計算雅可比矩陣本身,而是允許針對給定的輸入向量 v = ( v 1 , . . . , v m ) v = (v_1, ..., v_m) v=(v1?,...,vm?) 計算雅可比積 v T ? J v^T \cdot J vT?J。這一過程通過將 v v v 作為參數調用 backward
實現。需要注意的是, v v v 的維度必須與我們希望計算雅可比積的原始張量的維度一致。
inp = torch.eye(4, 5, requires_grad=True)
out = (inp+1).pow(2).t()
out.backward(torch.ones_like(out), retain_graph=True)
print(f"First call\n{inp.grad}")
out.backward(torch.ones_like(out), retain_graph=True)
print(f"\nSecond call\n{inp.grad}")
inp.grad.zero_()
out.backward(torch.ones_like(out), retain_graph=True)
print(f"\nCall after zeroing gradients\n{inp.grad}")
# 輸出
First call
tensor([[4., 2., 2., 2., 2.],[2., 4., 2., 2., 2.],[2., 2., 4., 2., 2.],[2., 2., 2., 4., 2.]])Second call
tensor([[8., 4., 4., 4., 4.],[4., 8., 4., 4., 4.],[4., 4., 8., 4., 4.],[4., 4., 4., 8., 4.]])Call after zeroing gradients
tensor([[4., 2., 2., 2., 2.],[2., 4., 2., 2., 2.],[2., 2., 4., 2., 2.],[2., 2., 2., 4., 2.]])
請注意,當我們使用sameargument
第二次調用backward
時,梯度的值是不同的。發生這種情況是因為在進行backward
傳播時,PyTorch累積梯度,即計算梯度的值被添加到計算圖所有葉節點的grad
屬性中。
如果你想計算正確的梯度,你需要先將grad
屬性歸零。在實際訓練中,優化器能幫助我們做到這一點。
注:之前我們調用 backward() 函數時沒有傳入參數。實際上,這等同于調用 backward(torch.tensor(1.0))。在處理標量值函數時,這樣做是一種很實用的計算梯度的方法,比如在神經網絡訓練過程中計算損失函數的梯度就可以用這種方式。
更多內容請看:自動求導機制