快捷方式

學習基礎知識 || 快速入門 || 張量 || 資料集與 DataLoaders || 轉換 || 構建模型 || 自動微分 || 最佳化 || 儲存與載入模型

使用 torch.autograd 進行自動微分

建立日期:2021年2月10日 | 最後更新:2024年1月16日 | 最後驗證:2024年11月5日

訓練神經網路時,最常用的演算法是反向傳播。在該演算法中,根據損失函式相對於給定引數的梯度來調整引數(模型權重)。

為了計算這些梯度,PyTorch 內建了一個稱為 torch.autograd 的微分引擎。它支援對任何計算圖自動計算梯度。

考慮最簡單的單層神經網路,輸入為 x,引數為 wb,以及某個損失函式。它可以透過以下方式在 PyTorch 中定義:

import torch

x = 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)

張量、函式與計算圖

此程式碼定義了以下計算圖

在這個網路中,wb引數,我們需要對其進行最佳化。因此,我們需要能夠計算損失函式相對於這些變數的梯度。為此,我們將這些張量的 requires_grad 屬性設定為 True

注意

你可以在建立張量時設定 requires_grad 的值,也可以稍後使用 x.requires_grad_(True) 方法設定。

我們應用於張量以構建計算圖的函式實際上是 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 0x7f5842ad19c0>
Gradient function for loss = <BinaryCrossEntropyWithLogitsBackward0 object at 0x7f5842ad0610>

計算梯度

為了最佳化神經網路中的引數權重,我們需要計算損失函式相對於引數的導數,即在某些固定值 xy 下,我們需要計算 \(\frac{\partial loss}{\partial w}\)\(\frac{\partial loss}{\partial b}\)。要計算這些導數,我們呼叫 loss.backward(),然後從 w.gradb.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
你可能希望停用梯度跟蹤的原因包括:
  • 將神經網路中的某些引數標記為凍結引數

  • 當你只進行前向傳播時,加快計算速度,因為不對梯度進行跟蹤的張量上的計算會更高效。

更多關於計算圖的資訊

從概念上講,autograd 記錄了資料(張量)和所有執行的操作(以及由此產生的新張量),形成一個由 Function 物件組成的有向無環圖 (DAG)。在這個 DAG 中,葉子節點是輸入張量,根節點是輸出張量。透過從根節點追溯到葉子節點,你可以使用鏈式法則自動計算梯度。

在前向傳播中,autograd 同時完成兩件事:

  • 執行請求的操作以計算結果張量

  • 在 DAG 中維護操作的梯度函式

當在 DAG 的根節點上呼叫 .backward() 時,反向傳播啟動。autograd 隨後:

  • 計算每個 .grad_fn 的梯度,

  • 將它們累加到對應張量的 .grad 屬性中

  • 使用鏈式法則,一直傳播到葉子張量。

注意

DAG 在 PyTorch 中是動態的 需要注意的一點是,圖是每次都從頭開始重建的;在每次 .backward() 呼叫之後,autograd 開始填充一個新圖。這正是允許你在模型中使用控制流語句的原因;如果需要,你可以在每次迭代中改變形狀、大小和操作。

可選閱讀:張量梯度與雅可比乘積

在許多情況下,我們有一個標量損失函式,並且需要計算相對於某些引數的梯度。然而,在某些情況下,輸出函式是一個任意的張量。在這種情況下,PyTorch 允許你計算所謂的雅可比乘積,而不是實際的梯度。

對於一個向量函式 \(\vec{y}=f(\vec{x})\),其中 \(\vec{x}=\langle x_1,\dots,x_n\rangle\)\(\vec{y}=\langle y_1,\dots,y_m\rangle\)\(\vec{y}\) 相對於 \(\vec{x}\) 的梯度由雅可比矩陣給出:

\[J=\left(\begin{array}{ccc} \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{array}\right)\]

PyTorch 不是計算雅可比矩陣本身,而是允許你計算對於給定輸入向量 \(v=(v_1 \dots v_m)\)雅可比乘積 \(v^T\cdot J\)。這透過呼叫帶引數 \(v\)backward 實現。 \(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.]])

請注意,當我們第二次使用相同引數呼叫 backward 時,梯度的值是不同的。這是因為在進行 backward 傳播時,PyTorch 會累加梯度,即計算出的梯度值會新增到計算圖所有葉子節點的 grad 屬性中。如果你想計算正確的梯度,你需要先將 grad 屬性清零。在實際訓練中,最佳化器會幫助我們完成此操作。

注意

之前我們呼叫 backward() 函式時沒有引數。這本質上等同於呼叫 backward(torch.tensor(1.0)),這是在標量值函式(例如神經網路訓練期間的損失函式)的情況下計算梯度的一種有用方法。


文件

訪問 PyTorch 全面開發者文件

檢視文件

教程

獲取初學者和高階開發者深度教程

檢視教程

資源

查詢開發資源並解答你的問題

檢視資源