Pytorch Tutorial 使用torch.autograd進行自動微分
本文翻譯自 PyTorch 官網教程。
原文:https://pytorch.org/tutorials/beginner/basics/autogradqs_tutorial.html#optional-reading-tensor-gradients-and-jacobian-products
在訓練神經網絡時,最常使用到的算法就是反向傳播(back propagation)。在該算法中,參數(模型權重)會根據損失函數相對于給定參數的**梯度(gradient)**進行更新。
為了計算這些梯度,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)
張量、函數和計算圖
上述代碼定義了這樣一個計算圖(computational graph):
在該網絡中,w
和 b
是需要優化更新的權重參數。因此,我們需要能夠計算損失函數關于這兩個變量的梯度。為此,我們設置這兩個變量的 requires_grad
屬性為 True
。
我們也可以在創建張量之后,再使用
x.requires_grad_(True)
方法來設置requires_grad
的值。
我們將某個函數作用于張量來構建計算圖,該函數實際上是 Function
類的一個對象。這個對象知道在前向傳播時怎樣計算該函數,也知道在反向傳播時怎樣計算它的微分。反向傳播函數的是依據張量的 grad_fn
屬性。更多信息請參考 Function
的[文檔][https://pytorch.org/docs/stable/autograd.html#function]。
print('Gradient function for z =', z.grad_fn)
print('Gradient function for loss =', loss.grad_fn)
輸出:
Gradient function for z = <AddBackward0 object at 0x7f06c1316a90>
Gradient function for loss = <BinaryCrossEntropyWithLogitsBackward0 object at 0x7f06c1316a90>
計算梯度
為了優化更新網絡中的權重參數,我們需要計算損失函數關于權重參數的導數,即我們需要在給定 x
和 y
值的情況下的 ?losss?w\frac{\partial losss}{\partial w}?w?losss? 和 ?losss?b\frac{\partial losss}{\partial b}?b?losss?。 為了計算這些導數,我們調用 loss.backward()
函數,然后再來查看 w.grad
和 b.grad
的值。
loss.backward()
print(w.grad)
print(b.grad)
輸出:
tensor([[0.2225, 0.2403, 0.3150],[0.2225, 0.2403, 0.3150],[0.2225, 0.2403, 0.3150],[0.2225, 0.2403, 0.3150],[0.2225, 0.2403, 0.3150]])
tensor([0.2225, 0.2403, 0.3150])
- 我們只能得到
requires_grad
設置為True
且為計算圖中葉子結點的grad
屬性,計算圖中的所有其他結點的梯度是無法得到的。- 通過
backward()
,我們只能對某張計算圖進行一次求梯度的運算,因為為了提升性能,通常計算圖在進行一次求梯度的計算后就會被釋放。如果我們需要多次對同一張計算圖調用backward()
,我們需要在調用backward
的時候傳入retain_grap=True
。
禁用梯度跟蹤
默認情況下,所有的 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
這里是我們可能會想要禁用梯度追蹤的一些原因:
- 將我們網絡中的某些參數凍結,這時我們要微調一個預訓練過的網絡時很常見的場景。
- 在我們只需要進行前向傳播時加速運算,因為不進行梯度追蹤的計算肯定會更高效。
更多關于計算圖的知識
從概念上講,autograd 在由 Function 對象組成的有向無環圖 (DAG) 中記錄數據(張量)和所有執行的操作(以及生成的新張量)。 在這個 DAG 中,葉子節點是輸入張量,根節點是輸出張量。 通過從根節點到葉子節點跟蹤此圖,可以使用鏈式法則自動計算梯度。
在前向傳播中,autograd 同時完成這兩件事情:
- 運行指定的操作來計算結果向量
- 將各操作的梯度函數保存在 DAG 中
當在 DAG 根上調用 .backward()
時,反向傳遞開始, 然后自動求導:
- 根據各個
.grad_fn
計算梯度 - 將它們累加到對應的張量的
.grad
屬性中 - 使用鏈式求導法則,一直傳播到葉子張量
在 PyTorch 中 DAG 是動態的
需要注意的重要一點是,在每次
.backward()
調用之后,DAG 是從頭開始重新創建的。autograd 開始構建一張新圖。這也是為什么在 PyTorch 中支持控制流語句; 我們可以根據需要在每次迭代時更改形狀、大小和操作。
選讀:張量梯度和雅克比乘積
在許多情況下,我們得到的損失都是一個標量,然后我們去計算它關于各個權重參數的梯度。然而,在某些情況下我們會輸出一個任意維度的張量,PyTorch 允許我們計算所謂的雅克比乘積,而非真實的梯度。
對于一個向量 y?=f(x?)\vec{y}=f(\vec{x})y?=f(x),其中 x?=<x1,…,xn>\vec{x}=<x_1,\dots,x_n>x=<x1?,…,xn?> ,y?=<y1,…,yn>\vec{y}=<y_1,\dots,y_n>y?=<y1?,…,yn?>,則 y?\vec{y}y? 關于 x?\vec{x}x 的梯度可以由雅克比矩陣給出:
PyTorch 允許我們計算給定輸入向量 v=(v1,…,vn)v=(v_1,\dots,v_n)v=(v1?,…,vn?) 的雅克比乘積 vTv^TvT 而非雅克比矩陣本身。只需在調用 backward
的時候將 vvv 作為參數傳入。vvv 的尺寸需要與我們想要計算雅克比乘積的原始張量相同:
inp = torch.eye(5, requires_grad=True)
out = (inp+1).pow(2)
out.backward(torch.ones_like(inp), retain_graph=True)
print("First call\n", inp.grad)
out.backward(torch.ones_like(inp), retain_graph=True)
print("\nSecond call\n", inp.grad)
inp.grad.zero_()
out.backward(torch.ones_like(inp), retain_graph=True)
print("\nCall after zeroing gradients\n", inp.grad)
輸出:
First calltensor([[4., 2., 2., 2., 2.],[2., 4., 2., 2., 2.],[2., 2., 4., 2., 2.],[2., 2., 2., 4., 2.],[2., 2., 2., 2., 4.]])Second calltensor([[8., 4., 4., 4., 4.],[4., 8., 4., 4., 4.],[4., 4., 8., 4., 4.],[4., 4., 4., 8., 4.],[4., 4., 4., 4., 8.]])Call after zeroing gradientstensor([[4., 2., 2., 2., 2.],[2., 4., 2., 2., 2.],[2., 2., 4., 2., 2.],[2., 2., 2., 4., 2.],[2., 2., 2., 2., 4.]])
注意當我們使用相同的參數第二次調用 backward
時,梯度的值會不同。這是因為在調用 backward
進行反向傳播時,PyTorch 會累加梯度,即計算梯度得到的值會被加到計算圖中所有葉子結點的 .grad
屬性上。因此一般區情況下,為了計算的梯度正確,我們需要再每一步計算梯度前先將已有梯度清零(zero_grad()
)。在實際的訓練中,優化器會幫助我們做這一步。
譯者注:有時也可以通過這一累加特性變相增大 batch_size。
之前我們調用無參數的
backward()
函數。 這本質上等同于調用backward(torch.tensor(1.0))
,這在標量值函數的情況下計算梯度時很有用,例如神經網絡訓練期間的損失。
擴展閱讀
- Autograd Mechanics