注意
點選此處下載完整的示例程式碼
入門 || 張量 || Autograd || 構建模型 || TensorBoard 支援 || 訓練模型 || 模型理解
Autograd 的基礎知識¶
創建於: 2021 年 11 月 30 日 | 最後更新於: 2024 年 2 月 26 日 | 最後驗證於: 2024 年 11 月 05 日
觀看下面的影片或在 youtube 上觀看,一起學習。
PyTorch 的 Autograd 特性是 PyTorch 靈活且快速構建機器學習專案的關鍵之一。它允許在複雜的計算中快速輕鬆地計算多個偏導數(也稱為梯度)。此操作是基於反向傳播的神經網路學習的核心。
Autograd 的強大之處在於它在執行時動態跟蹤你的計算,這意味著如果你的模型有決策分支,或迴圈長度直到執行時才確定,計算仍能被正確跟蹤,並且你會獲得正確的梯度來驅動學習。這一點,再加上你的模型是使用 Python 構建的,比那些依賴對更嚴格結構化模型進行靜態分析來計算梯度的框架提供了更大的靈活性。
為什麼我們需要 Autograd?¶
機器學習模型是一個函式,具有輸入和輸出。在本次討論中,我們將輸入視為一個 i 維向量 \(\vec{x}\),其元素為 \(x_{i}\)。然後,我們可以將模型 M 表示為輸入的向量值函式:\(\vec{y} = \vec{M}(\vec{x})\)。(我們將 M 的輸出值視為向量,因為一般來說,模型可以有任意數量的輸出。)
由於我們主要在訓練的背景下討論 autograd,我們關注的輸出將是模型的損失。損失函式 L(\(\vec{y}\)) = L(\(\vec{M}\)(\(\vec{x}\))) 是模型輸出的單值標量函式。此函式表示我們的模型預測與特定輸入的理想輸出之間的偏差程度。注意:從此處開始,在上下文清晰的情況下,我們將經常省略向量符號 - 例如,使用 \(y\) 而不是 \(\vec y\)。
在訓練模型時,我們希望最小化損失。在理想的完美模型情況下,這意味著調整其學習權重 - 即函式的引數 - 使所有輸入的損失為零。在現實世界中,這意味著一個迭代過程,透過微調學習權重,直到我們看到對於各種輸入都能獲得可容忍的損失。
我們如何決定微調權重的幅度和方向?我們希望最小化損失,這意味著使其對輸入的第一個導數等於 0:\(\frac{\partial L}{\partial x} = 0\)。
然而,回想一下,損失並非直接來自輸入,而是模型輸出的函式(模型輸出又是輸入的直接函式),\(\frac{\partial L}{\partial x}\) = \(\frac{\partial {L({\vec y})}}{\partial x}\)。根據微分學的鏈式法則,我們有 \(\frac{\partial {L({\vec y})}}{\partial x}\) = \(\frac{\partial L}{\partial y}\frac{\partial y}{\partial x}\) = \(\frac{\partial L}{\partial y}\frac{\partial M(x)}{\partial x}\)。
\(\frac{\partial M(x)}{\partial x}\) 是事情變得複雜的地方。模型的輸出對輸入的偏導數,如果我們使用鏈式法則再次展開表示式,將涉及模型中每個相乘的學習權重、每個啟用函式以及所有其他數學變換的許多區域性偏導數。每個此類偏導數的完整表示式是計算圖中以我們試圖測量其梯度的變數結束的每一條可能路徑的區域性梯度的乘積之和。
特別是,我們對學習權重上的梯度很感興趣 - 它們告訴我們改變每個權重的方向,以使損失函式更接近於零。
由於此類區域性導數的數量(每個都對應於透過模型計算圖的一條獨立路徑)傾向於隨著神經網路深度的增加呈指數級增長,因此計算它們的複雜性也隨之增加。這就是 autograd 的作用所在:它跟蹤每次計算的歷史。PyTorch 模型中計算的每個張量都帶有其輸入張量和用於建立它的函式的歷史記錄。再結合 PyTorch 中旨在作用於張量的函式都內建了計算自身導數的實現,這極大地加速了學習所需的區域性導數的計算。
一個簡單的示例¶
理論講了不少 - 但在實踐中使用 autograd 是怎樣的呢?
讓我們從一個簡單的例子開始。首先,我們將匯入一些庫,以便繪製結果圖
# %matplotlib inline
import torch
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import math
接下來,我們將建立一個包含區間 \([0, 2{\pi}]\) 上均勻分佈值的輸入張量,並指定 requires_grad=True。(與大多數建立張量的函式一樣,torch.linspace() 接受一個可選的 requires_grad 選項。)設定此標誌意味著在隨後的每次計算中,autograd 將在該計算的輸出張量中累積計算歷史。
a = torch.linspace(0., 2. * math.pi, steps=25, requires_grad=True)
print(a)
tensor([0.0000, 0.2618, 0.5236, 0.7854, 1.0472, 1.3090, 1.5708, 1.8326, 2.0944,
2.3562, 2.6180, 2.8798, 3.1416, 3.4034, 3.6652, 3.9270, 4.1888, 4.4506,
4.7124, 4.9742, 5.2360, 5.4978, 5.7596, 6.0214, 6.2832],
requires_grad=True)
接下來,我們將執行計算,並根據其輸入繪製其輸出圖

[<matplotlib.lines.Line2D object at 0x7f1316776380>]
讓我們仔細看看張量 b。當我們列印它時,會看到一個指示符,表明它正在跟蹤其計算歷史
print(b)
tensor([ 0.0000e+00, 2.5882e-01, 5.0000e-01, 7.0711e-01, 8.6603e-01,
9.6593e-01, 1.0000e+00, 9.6593e-01, 8.6603e-01, 7.0711e-01,
5.0000e-01, 2.5882e-01, -8.7423e-08, -2.5882e-01, -5.0000e-01,
-7.0711e-01, -8.6603e-01, -9.6593e-01, -1.0000e+00, -9.6593e-01,
-8.6603e-01, -7.0711e-01, -5.0000e-01, -2.5882e-01, 1.7485e-07],
grad_fn=<SinBackward0>)
這個 grad_fn 提示我們,當我們執行反向傳播步驟並計算梯度時,需要計算此張量所有輸入的 \(\sin(x)\) 的導數。
讓我們執行更多計算
tensor([ 0.0000e+00, 5.1764e-01, 1.0000e+00, 1.4142e+00, 1.7321e+00,
1.9319e+00, 2.0000e+00, 1.9319e+00, 1.7321e+00, 1.4142e+00,
1.0000e+00, 5.1764e-01, -1.7485e-07, -5.1764e-01, -1.0000e+00,
-1.4142e+00, -1.7321e+00, -1.9319e+00, -2.0000e+00, -1.9319e+00,
-1.7321e+00, -1.4142e+00, -1.0000e+00, -5.1764e-01, 3.4969e-07],
grad_fn=<MulBackward0>)
tensor([ 1.0000e+00, 1.5176e+00, 2.0000e+00, 2.4142e+00, 2.7321e+00,
2.9319e+00, 3.0000e+00, 2.9319e+00, 2.7321e+00, 2.4142e+00,
2.0000e+00, 1.5176e+00, 1.0000e+00, 4.8236e-01, -3.5763e-07,
-4.1421e-01, -7.3205e-01, -9.3185e-01, -1.0000e+00, -9.3185e-01,
-7.3205e-01, -4.1421e-01, 4.7684e-07, 4.8236e-01, 1.0000e+00],
grad_fn=<AddBackward0>)
最後,讓我們計算一個單元素輸出。當你對一個沒有引數的張量呼叫 .backward() 時,它期望呼叫張量只包含一個元素,就像計算損失函式時一樣。
tensor(25., grad_fn=<SumBackward0>)
每個與我們的張量一起儲存的 grad_fn 都可以讓你透過其 next_functions 屬性一直回溯到其輸入。我們可以看到,對 d 的此屬性進行深入探究,會顯示所有先前張量的梯度函式。注意,a.grad_fn 報告為 None,表明這是函式的輸入,本身沒有歷史記錄。
print('d:')
print(d.grad_fn)
print(d.grad_fn.next_functions)
print(d.grad_fn.next_functions[0][0].next_functions)
print(d.grad_fn.next_functions[0][0].next_functions[0][0].next_functions)
print(d.grad_fn.next_functions[0][0].next_functions[0][0].next_functions[0][0].next_functions)
print('\nc:')
print(c.grad_fn)
print('\nb:')
print(b.grad_fn)
print('\na:')
print(a.grad_fn)
d:
<AddBackward0 object at 0x7f12e5ab6230>
((<MulBackward0 object at 0x7f12e5ab6200>, 0), (None, 0))
((<SinBackward0 object at 0x7f12e5ab6200>, 0), (None, 0))
((<AccumulateGrad object at 0x7f12e5ab6230>, 0),)
()
c:
<MulBackward0 object at 0x7f12e5ab6200>
b:
<SinBackward0 object at 0x7f12e5ab6200>
a:
None
有了所有這些機制,我們如何獲取導數呢?你可以在輸出上呼叫 backward() 方法,並檢查輸入的 grad 屬性以檢視梯度
out.backward()
print(a.grad)
plt.plot(a.detach(), a.grad.detach())

tensor([ 2.0000e+00, 1.9319e+00, 1.7321e+00, 1.4142e+00, 1.0000e+00,
5.1764e-01, -8.7423e-08, -5.1764e-01, -1.0000e+00, -1.4142e+00,
-1.7321e+00, -1.9319e+00, -2.0000e+00, -1.9319e+00, -1.7321e+00,
-1.4142e+00, -1.0000e+00, -5.1764e-01, 2.3850e-08, 5.1764e-01,
1.0000e+00, 1.4142e+00, 1.7321e+00, 1.9319e+00, 2.0000e+00])
[<matplotlib.lines.Line2D object at 0x7f12e5cc5240>]
回顧一下我們到達這裡所採取的計算步驟
像我們計算 d 那樣新增一個常數,不會改變導數。剩下的就是 \(c = 2 * b = 2 * \sin(a)\),其導數應該是 \(2 * \cos(a)\)。看看上面的圖,這就是我們看到的結果。
請注意,只有計算圖的葉子節點才會計算梯度。例如,如果你嘗試 print(c.grad),你會得到 None。在這個簡單的例子中,只有輸入是葉子節點,因此只有它計算了梯度。
訓練中的 Autograd¶
我們簡要了解了 autograd 的工作原理,但當它用於其預期目的時是什麼樣的呢?讓我們定義一個小模型,並檢查它在一個訓練批次後如何變化。首先,定義一些常量、我們的模型以及一些輸入和輸出的替身
BATCH_SIZE = 16
DIM_IN = 1000
HIDDEN_SIZE = 100
DIM_OUT = 10
class TinyModel(torch.nn.Module):
def __init__(self):
super(TinyModel, self).__init__()
self.layer1 = torch.nn.Linear(DIM_IN, HIDDEN_SIZE)
self.relu = torch.nn.ReLU()
self.layer2 = torch.nn.Linear(HIDDEN_SIZE, DIM_OUT)
def forward(self, x):
x = self.layer1(x)
x = self.relu(x)
x = self.layer2(x)
return x
some_input = torch.randn(BATCH_SIZE, DIM_IN, requires_grad=False)
ideal_output = torch.randn(BATCH_SIZE, DIM_OUT, requires_grad=False)
model = TinyModel()
你可能會注意到的一件事是,我們從未為模型的層指定 requires_grad=True。在 torch.nn.Module 的子類中,我們假定希望跟蹤層權重的梯度以進行學習。
如果我們檢視模型的層,可以檢查權重的數值,並驗證尚未計算任何梯度
print(model.layer2.weight[0][0:10]) # just a small slice
print(model.layer2.weight.grad)
tensor([ 0.0920, 0.0916, 0.0121, 0.0083, -0.0055, 0.0367, 0.0221, -0.0276,
-0.0086, 0.0157], grad_fn=<SliceBackward0>)
None
讓我們看看執行一個訓練批次後會有什麼變化。對於損失函式,我們將使用我們的 prediction 和 ideal_output 之間的歐幾里得距離的平方,我們將使用一個基本的隨機梯度下降最佳化器。
optimizer = torch.optim.SGD(model.parameters(), lr=0.001)
prediction = model(some_input)
loss = (ideal_output - prediction).pow(2).sum()
print(loss)
tensor(211.2634, grad_fn=<SumBackward0>)
現在,讓我們呼叫 loss.backward(),看看會發生什麼
loss.backward()
print(model.layer2.weight[0][0:10])
print(model.layer2.weight.grad[0][0:10])
tensor([ 0.0920, 0.0916, 0.0121, 0.0083, -0.0055, 0.0367, 0.0221, -0.0276,
-0.0086, 0.0157], grad_fn=<SliceBackward0>)
tensor([12.8997, 2.9572, 2.3021, 1.8887, 5.0710, 7.3192, 3.5169, 2.4319,
0.1732, -5.3835])
我們可以看到,每個學習權重的梯度都已計算出來,但權重保持不變,因為我們還沒有執行最佳化器。最佳化器負責根據計算出的梯度更新模型權重。
optimizer.step()
print(model.layer2.weight[0][0:10])
print(model.layer2.weight.grad[0][0:10])
tensor([ 0.0791, 0.0886, 0.0098, 0.0064, -0.0106, 0.0293, 0.0186, -0.0300,
-0.0088, 0.0211], grad_fn=<SliceBackward0>)
tensor([12.8997, 2.9572, 2.3021, 1.8887, 5.0710, 7.3192, 3.5169, 2.4319,
0.1732, -5.3835])
你應該看到 layer2 的權重已經改變了。
關於這個過程,重要的一點是:在呼叫 optimizer.step() 後,你需要呼叫 optimizer.zero_grad(),否則每次執行 loss.backward() 時,學習權重的梯度都會累積
print(model.layer2.weight.grad[0][0:10])
for i in range(0, 5):
prediction = model(some_input)
loss = (ideal_output - prediction).pow(2).sum()
loss.backward()
print(model.layer2.weight.grad[0][0:10])
optimizer.zero_grad(set_to_none=False)
print(model.layer2.weight.grad[0][0:10])
tensor([12.8997, 2.9572, 2.3021, 1.8887, 5.0710, 7.3192, 3.5169, 2.4319,
0.1732, -5.3835])
tensor([ 19.2095, -15.9459, 8.3306, 11.5096, 9.5471, 0.5391, -0.3370,
8.6386, -2.5141, -30.1419])
tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
執行上面的單元格後,你應該會看到,在多次執行 loss.backward() 後,大多數梯度的幅度會大得多。如果在執行下一個訓練批次之前未能將梯度歸零,將導致梯度以這種方式爆炸,從而導致錯誤和不可預測的學習結果。
開啟和關閉 Autograd¶
在某些情況下,你需要精細控制是否啟用 autograd。根據情況不同,有多種方法可以做到這一點。
最簡單的方法是直接更改張量上的 requires_grad 標誌
tensor([[1., 1., 1.],
[1., 1., 1.]], requires_grad=True)
tensor([[2., 2., 2.],
[2., 2., 2.]], grad_fn=<MulBackward0>)
tensor([[2., 2., 2.],
[2., 2., 2.]])
在上面的單元格中,我們看到 b1 有一個 grad_fn(即跟蹤的計算歷史),這是我們期望的,因為它派生自一個啟用了 autograd 的張量 a。當我們使用 a.requires_grad = False 顯式關閉 autograd 時,計算歷史不再被跟蹤,正如我們在計算 b2 時所看到的。
如果你只需要暫時關閉 autograd,更好的方法是使用 torch.no_grad()
tensor([[5., 5., 5.],
[5., 5., 5.]], grad_fn=<AddBackward0>)
tensor([[5., 5., 5.],
[5., 5., 5.]])
tensor([[6., 6., 6.],
[6., 6., 6.]], grad_fn=<MulBackward0>)
torch.no_grad() 也可以用作函式或方法的裝飾器
tensor([[5., 5., 5.],
[5., 5., 5.]], grad_fn=<AddBackward0>)
tensor([[5., 5., 5.],
[5., 5., 5.]])
有一個對應的上下文管理器 torch.enable_grad(),用於在 autograd 未啟用時將其開啟。它也可以用作裝飾器。
最後,你可能有一個需要跟蹤梯度的張量,但你想要一個不跟蹤的副本。為此,我們可以使用 Tensor 物件的 detach() 方法 - 它會建立一個張量副本,該副本與計算歷史分離。
tensor([0.0670, 0.3890, 0.7264, 0.3559, 0.6584], requires_grad=True)
tensor([0.0670, 0.3890, 0.7264, 0.3559, 0.6584])
我們之前為了繪製一些張量圖時就這樣做了。這是因為 matplotlib 期望輸入是 NumPy 陣列,而對於 requires_grad=True 的張量,PyTorch 張量到 NumPy 陣列的隱式轉換是未啟用的。建立一個分離的副本可以讓我們繼續進行。
Autograd 和就地操作¶
到目前為止,此 Notebook 中的每個示例都使用了變數來捕獲計算的中間值。Autograd 需要這些中間值來執行梯度計算。因此,在使用 autograd 時必須謹慎使用就地操作。這樣做可能會破壞在 backward() 呼叫中計算導數所需的資訊。如果你嘗試對需要 autograd 的葉子變數執行就地操作,PyTorch 甚至會阻止你,如下所示。
注意
以下程式碼單元格會丟擲執行時錯誤。這是預期結果。
a = torch.linspace(0., 2. * math.pi, steps=25, requires_grad=True)
torch.sin_(a)
Autograd 效能分析器¶
Autograd 詳細跟蹤你計算的每一步。這樣的計算歷史,結合時序資訊,會是一個方便的效能分析器 - autograd 內建了這項功能。這裡有一個快速使用示例
device = torch.device('cpu')
run_on_gpu = False
if torch.cuda.is_available():
device = torch.device('cuda')
run_on_gpu = True
x = torch.randn(2, 3, requires_grad=True)
y = torch.rand(2, 3, requires_grad=True)
z = torch.ones(2, 3, requires_grad=True)
with torch.autograd.profiler.profile(use_cuda=run_on_gpu) as prf:
for _ in range(1000):
z = (z / x) * y
print(prf.key_averages().table(sort_by='self_cpu_time_total'))
/var/lib/workspace/beginner_source/introyt/autogradyt_tutorial.py:485: FutureWarning:
The attribute `use_cuda` will be deprecated soon, please use ``use_device = 'cuda'`` instead.
------------------------- ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------
Name Self CPU % Self CPU CPU total % CPU total CPU time avg Self CUDA Self CUDA % CUDA total CUDA time avg # of Calls
------------------------- ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------
cudaEventRecord 54.48% 8.843ms 54.48% 8.843ms 2.211us 0.000us 0.00% 0.000us 0.000us 4000
aten::div 22.89% 3.715ms 22.89% 3.715ms 3.715us 7.605ms 50.42% 7.605ms 7.605us 1000
aten::mul 22.53% 3.656ms 22.53% 3.656ms 3.656us 7.479ms 49.58% 7.479ms 7.479us 1000
cudaDeviceSynchronize 0.10% 16.120us 0.10% 16.120us 16.120us 0.000us 0.00% 0.000us 0.000us 1
------------------------- ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------
Self CPU time total: 16.230ms
Self CUDA time total: 15.084ms
效能分析器還可以標記程式碼的單個子塊,按輸入張量形狀細分資料,並將資料匯出為 Chrome 追蹤工具檔案。有關 API 的完整詳細資訊,請參閱文件。
進階主題:更多 Autograd 細節和高階 API¶
如果你有一個輸入為 n 維、輸出為 m 維的函式 \(\vec{y}=f(\vec{x})\),完整的梯度是一個包含每個輸出對每個輸入導數的矩陣,稱為雅可比矩陣 (Jacobian):
如果你有第二個函式 \(l=g\left(\vec{y}\right)\),它接受 m 維輸入(即與上面輸出具有相同維度),並返回一個標量輸出,你可以將其對 \(\vec{y}\) 的梯度表示為一個列向量 \(v=\left(\begin{array}{ccc}\frac{\partial l}{\partial y_{1}} & \cdots & \frac{\partial l}{\partial y_{m}}\end{array}\right)^{T}\) - 這實際上就是一個單列的雅可比矩陣。
更具體地說,想象第一個函式是你的 PyTorch 模型(可能有很多輸入和輸出),第二個函式是損失函式(以模型的輸出為輸入,損失值為標量輸出)。
如果我們用第一個函式的雅可比矩陣乘以第二個函式的梯度,並應用鏈式法則,我們會得到
注意:你也可以使用等效的操作 \(v^{T}\cdot J\),得到一個行向量。
結果列向量是第二個函式對第一個函式輸入的梯度 - 或者在我們模型和損失函式的情況下,是損失對模型輸入的梯度。
``torch.autograd`` 是計算這些乘積的引擎。這就是我們在反向傳播過程中累積學習權重上的梯度的方式。
因此,backward() 呼叫也可以接受一個可選的向量輸入。這個向量代表一組張量上的梯度,它們將乘以先前的 autograd 跟蹤張量的雅可比矩陣。讓我們用一個小向量來嘗試一個具體的例子
x = torch.randn(3, requires_grad=True)
y = x * 2
while y.data.norm() < 1000:
y = y * 2
print(y)
tensor([ 299.4868, 425.4009, -1082.9885], grad_fn=<MulBackward0>)
如果我們現在嘗試呼叫 y.backward(),會得到一個執行時錯誤,並提示梯度只能為標量輸出隱式計算。對於多維輸出,autograd 期望我們提供這三個輸出的梯度,以便將其乘以雅可比矩陣
v = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float) # stand-in for gradients
y.backward(v)
print(x.grad)
tensor([1.0240e+02, 1.0240e+03, 1.0240e-01])
(注意,輸出梯度都與 2 的冪有關 - 這是重複加倍操作的預期結果。)
高階 API¶
autograd 上有一個 API,可以讓你直接訪問重要的微分矩陣和向量操作。特別是,它允許你計算特定函式在特定輸入下的雅可比矩陣和海森矩陣 (Hessian)。(海森矩陣類似於雅可比矩陣,但表示所有偏二階導數。)它還提供了計算與這些矩陣的向量乘積的方法。
讓我們計算一個簡單函式在兩個單元素輸入下的雅可比矩陣
def exp_adder(x, y):
return 2 * x.exp() + 3 * y
inputs = (torch.rand(1), torch.rand(1)) # arguments for the function
print(inputs)
torch.autograd.functional.jacobian(exp_adder, inputs)
(tensor([0.7212]), tensor([0.2079]))
(tensor([[4.1137]]), tensor([[3.]]))
仔細觀察,第一個輸出應該等於 \(2e^x\) (因為 \(e^x\) 的導數就是 \(e^x\)),第二個值應該為 3。
當然,你可以使用高階張量來執行此操作
inputs = (torch.rand(3), torch.rand(3)) # arguments for the function
print(inputs)
torch.autograd.functional.jacobian(exp_adder, inputs)
(tensor([0.2080, 0.2604, 0.4415]), tensor([0.5220, 0.9867, 0.4288]))
(tensor([[2.4623, 0.0000, 0.0000],
[0.0000, 2.5950, 0.0000],
[0.0000, 0.0000, 3.1102]]), tensor([[3., 0., 0.],
[0., 3., 0.],
[0., 0., 3.]]))
方法 torch.autograd.functional.hessian() 的工作方式相同(假設你的函式是二次可微的),但返回所有二階導數的矩陣。
如果你提供了向量,還有一個函式可以直接計算向量-雅可比積
def do_some_doubling(x):
y = x * 2
while y.data.norm() < 1000:
y = y * 2
return y
inputs = torch.randn(3)
my_gradients = torch.tensor([0.1, 1.0, 0.0001])
torch.autograd.functional.vjp(do_some_doubling, inputs, v=my_gradients)
(tensor([-665.7186, -866.7054, -58.4194]), tensor([1.0240e+02, 1.0240e+03, 1.0240e-01]))
方法 torch.autograd.functional.jvp() 執行與 vjp() 相同的矩陣乘法,只是運算元順序顛倒。方法 vhp() 和 hvp() 對向量-Hessian 積執行相同的操作。
有關更多資訊,包括函式式 API 文件中的效能說明,請參閱 函式式高階 API 文件
指令碼總執行時間: ( 0 分鐘 0.519 秒)