目錄
1 張量
1)張量的初始化和屬性
2)張量操作
3)使用 NumPy 進行橋接
2 torch.autograd
1)背景
2)在 PyTorch 中的使用
3)Autograd 的微分機制
4)計算圖原理
5)從計算圖中排除
3 神經網絡
1)定義網絡
2)損失函數
3)反向傳播
4)權重更新
本篇是筆者基于官方文檔所做的學習筆記,在完整保留文檔技術要點的同時提供輔助備注,也可作為官方文檔的中文參考譯文閱讀。如需了解如何安裝和運行?PyTorch?可移步《PyTorch 入門學習筆記》。
PyTorch 是由?Meta(原 Facebook)?開源的深度學習框架,是基于 Python 的科學計算包,主要服務于兩大用途:(1)作為 NumPy 的替代方案,支持 GPU 及其他硬件加速器的高性能計算,顯著提升張量運算效率;(2)作為自動微分庫,為神經網絡訓練提供高效的梯度計算與反向傳播支持。
1 張量
張量(Tensor)是 PyTorch 中多維數據的主要載體,類似于 NumPy 的 ndarray,但支持 GPU 加速計算和自動微分功能,是神經網絡中數據和梯度計算的基礎單位。
1)張量的初始化和屬性
張量有以下幾種創建方式:直接用數據創建,用 numpy 數組創建,用另一個張量創建,用常量或隨機值創建。例如:
import torch
import numpy as np# 用數據創建
data = [[1, 2], [3, 4]]
x_data = torch.tensor(data)# 用數組創建
np_array = np.array(data)
x_np = torch.from_numpy(np_array)# 用另一個張量創建
x_ones = torch.ones_like(x_data) # 保持 x_data 的屬性
print(f"Ones Tensor: \n {x_ones} \n")
x_rand = torch.rand_like(x_data, dtype=torch.float) # 覆蓋 x_data 的類型
print(f"Random Tensor: \n {x_rand} \n")# 用常量或隨機值次創建
shape = (2, 3,)
rand_tensor = torch.rand(shape)
ones_tensor = torch.ones(shape)
zeros_tensor = torch.zeros(shape)print(f"Random Tensor: \n {rand_tensor} \n")
print(f"Ones Tensor: \n {ones_tensor} \n")
print(f"Zeros Tensor: \n {zeros_tensor}")
運行結果如下:
張量屬性描述了它們的形狀、數據類型和存儲它們的設備。
tensor = torch.rand(3, 4)print(f"Shape of tensor: {tensor.shape}")
print(f"Datatype of tensor: {tensor.dtype}")
print(f"Device tensor is stored on: {tensor.device}")
運行結果如下:
2)張量操作
PyTorch 提供了超過100種張量操作,涵蓋數學運算、線性代數、數據切片等常見需求。這些操作均可在 GPU 上運行,且速度通常遠快于 CPU。如果您使用的是?Colab,可以通過?Edit ——?Notebook Settings 來分配。
# 將張量移動到 GPU 上(如果支持)
if torch.cuda.is_available():tensor = tensor.to('cuda')print(f"Device tensor is stored on: {tensor.device}")
嘗試一些操作(如果您熟悉 NumPy API,則會發現 Tensor API 使用起來相對更容易):
# 類似 numpy 的標準索引和切片
tensor = torch.ones(4, 4)
tensor[:,1] = 0
print(tensor)# 可以用 torch.cat 將張量沿指定維度連接起來(還有類似功能 torch.stack)
t1 = torch.cat([tensor, tensor, tensor], dim=1)
print(t1)# 計算元素級乘積
print(f"tensor.mul(tensor) \n {tensor.mul(tensor)} \n")
# 也可以這樣寫
print(f"tensor * tensor \n {tensor * tensor}")# 計算兩個張量之間的矩陣乘法
print(f"tensor.matmul(tensor.T) \n {tensor.matmul(tensor.T)} \n")
# 也可以這樣寫
print(f"tensor @ tensor.T \n {tensor @ tensor.T}")# 原地操作,帶有 _ 后綴的操作是原地操作(例如 x.copy_(y) 和 x.t_() 會直接修改 x 的值)
print(tensor, "\n")
tensor.add_(5)
print(tensor)
運行結果如下:
3)使用 NumPy 進行橋接
CPU 上的張量和 NumPy 數組可以共享它們的底層內存位置,更改其中一個將會更改另一個。
# 張量轉換為 NumPy 數組
t = torch.ones(5)
print(f"t: {t}")
n = t.numpy()
print(f"n: {n}")# 張量的變化會反映在 NumPy 數組上
t.add_(1)
print(f"t: {t}")
print(f"n: {n}")# NumPy 數組轉張量
n = np.ones(5)
t = torch.from_numpy(n)# NumPy 數組的修改會同步到張量
np.add(n, 1, out=n)
print(f"t: {t}")
print(f"n: {n}")
運行結果如下:
2 torch.autograd
torch.autograd 是 PyTorch 的自動微分引擎,為神經網絡訓練提供核心支持。
1)背景
神經網絡(Neural Networks, NNs)是由一系列嵌套函數組成的計算模型,這些函數對輸入數據執行運算。這些函數由參數(包括權重和偏置)定義,在 PyTorch 中,這些參數以張量的形式存儲。
神經網絡的訓練過程分為兩個階段:
-
前向傳播(Forward Propagation)
在前向傳播過程中,神經網絡基于輸入數據計算其預測輸出。數據依次經過網絡中的每一層函數,最終生成預測結果。
-
反向傳播(Backward Propagation)
在反向傳播過程中,神經網絡根據預測誤差按比例調整參數。具體做法是從輸出層開始反向遍歷網絡,計算誤差關于各層參數的導數(即梯度),并利用梯度下降法優化參數。
如果把前向傳播就類比成按菜譜做菜,反向傳播則是根據成品味道反推和調整菜譜配方。
2)在 PyTorch 中的使用
來看一個簡單的訓練示例,本例從 torchvision 加載一個預訓練的 resnet18 模型。創建一個隨機數據張量來模擬一張3通道、高寬均為64的圖像,并初始化其對應的標簽為隨機值。預訓練模型中的標簽形狀為(1,1000)。注意:本教程僅適用于 CPU 環境,無法在 GPU 設備上運行(即使將張量移至 CUDA)。
import torch
from torchvision.models import resnet18, ResNet18_Weights
model = resnet18(weights=ResNet18_Weights.DEFAULT)
data = torch.rand(1, 3, 64, 64)
labels = torch.rand(1, 1000)
將輸入數據傳入模型,經過每一層的計算得到預測結果——這就是前向傳播。
prediction = model(data) # 前向傳播
然后計算誤差(損失值),也就是計算預測值與真實標簽之間的差異度量。然后再將這個誤差反向傳播到整個網絡中。調用?.backward()?
方法會觸發反向傳播過程。此時?Autograd 引擎會自動計算每個模型參數的梯度(梯度表示函數在某一點的導數或偏導數,描述了張量隨著其輸入變化的變化率),并將它們存儲在參數的 .grad 屬性中。
loss = (prediction - labels).sum() # 計算損失
loss.backward() # 反向傳播
接下來加載一個優化器,優化器的技術本質是一個能自動調整模型參數的算法,目標是讓預測結果越來越準,通俗點說就是告訴模型怎么改、改多少。這個例子中使用了隨機梯度下降法(SGD),并設置學習率為0.01,動量參數為0.9。我們要將模型的所有參數都注冊到該優化器中。
optim = torch.optim.SGD(model.parameters(), lr=1e-2, momentum=0.9)
然后調用 .step() 方法來執行梯度下降。優化器會根據每個參數存儲在 .grad 屬性中的梯度值來調整這些參數。
optim.step() # 執行梯度下降
以上就是訓練神經網絡所需的所有基本步驟。
3)Autograd 的微分機制
autograd 是如何收集梯度的呢?創建兩個張量?a?和?b,并設置 requires_grad=True。這相當于告訴 autograd:請跟蹤所有對這兩個張量的運算。
import torcha = torch.tensor([2., 3.], requires_grad=True)
b = torch.tensor([6., 4.], requires_grad=True)
接著用 a 和 b 生成新張量 Q:
Q = 3*a**3 - b**2
假設 a 和 b 是神經網絡的參數,Q 是誤差值。在訓練神經網絡時,我們需要計算誤差關于參數的梯度(a 的梯度應為9倍 a 的平方,b 的梯度應為-2倍的 b):
當調用 Q.backward() 時,autograd 會計算梯度并將結果存儲在各個張量的 .grad 屬性中。
由于 Q 是向量,這里需要顯式傳入一個 gradient 參數。這個參數是與 Q 形狀相同的張量,代表 Q 對自身的梯度,即:
等價的做法是將 Q 聚合為標量再隱式調用 backward,例如 Q.sum().backward()。
external_grad = torch.tensor([1., 1.])
Q.backward(gradient=external_grad)
現在梯度已存入 a.grad 和 b.grad 中:
# 驗證梯度是否正確
print(9*a**2 == a.grad) # 檢查 a 的梯度
print(-2*b == b.grad) # 檢查 b 的梯度
運行結果如下:
4)計算圖原理
autograd 通過有向無環圖(DAG)記錄數據(張量)和執行的所有運算(包括生成的新張量)。這個 DAG 由 Function 對象構成:
-
葉子節點:輸入張量(如初始參數)
-
根節點:輸出張量(如損失值)
通過從根節點回溯到葉子節點,autograd 能利用鏈式法則自動計算梯度。
前向傳播,autograd 會同步完成兩件事:
-
執行運算:計算輸出張量
-
維護 DAG:記錄運算對應的梯度函數(grad_fn)
反向傳播,當調用?.backward()?時,autograd 會:
- 從每個 grad_fn 計算梯度
- 將梯度累加到對應張量的 .grad 屬性中
- 通過鏈式法則一直傳播到葉子張量
下圖是本示例中 DAG 的可視化表示,箭頭指向向前傳遞的方向,節點表示每個操作的反向函數。藍色葉子節點表示張量 a 和 b。
5)從計算圖中排除
torch.autograd?會跟蹤所有設置?requires_grad 為 True?的張量的運算,對于不需要梯度計算的張量,將requires_grad 設為?False?即可將其排除在梯度計算圖(DAG)之外。
x = torch.rand(5, 5)
y = torch.rand(5, 5)
z = torch.rand((5, 5), requires_grad=True)a = x + y
print(f"Does `a` require gradients?: {a.requires_grad}")
b = x + z
print(f"Does `b` require gradients?: {b.requires_grad}")
運行結果如下:
在神經網絡中,不計算梯度的參數通常被稱為凍結參數。如果事先知道某些參數不需要計算梯度(通過減少自動微分計算提升性能),那么“凍結”模型的一部分就會非常有用。在微調時,我們通常會凍結模型的大部分參數,僅修改分類器層以適配新的標簽預測。如下是一個小示例(需要先安裝依賴?pip install torchvision )。和之前一樣,先加載一個預訓練的 resnet18 模型,并凍結其所有參數。
from torch import nn, optimmodel = resnet18(weights=ResNet18_Weights.DEFAULT)# 凍結網絡中的所有參數
for param in model.parameters():param.requires_grad = False
假設我們需要在一個包含10個類別的新數據集上對模型進行微調。在 resnet 中,分類器(模型中負責最終分類預測的層,如全連接層)是最后的全連接層?model.fc。可以直接將其替換為一個新的全連接層(默認情況下未被凍結),作為我們的分類器。
model.fc = nn.Linear(512, 10)
現在模型中除?model.fc?的參數外,其余所有參數均被凍結。唯一需要計算梯度的參數是?model.fc?的權重和偏置項。
# 只優化分類器
optimizer = optim.SGD(model.parameters(), lr=1e-2, momentum=0.9)
盡管在優化器中注冊了所有參數,但實際計算梯度(在梯度下降中會被更新)的只有分類器的權重和偏置項。同樣的參數排除功能也可以通過上下文管理器?torch.no_grad()?實現。
3 神經網絡
可以使用?torch.nn?包來構建神經網絡。nn?模塊是基于?autograd?來定義模型并進行微分計算的。一個?nn.Module?包含若干網絡層,以及一個?forward(input)?方法,該方法返回網絡的輸出結果。
下圖是一個用于手寫數字圖像分類的網絡示意圖:
這是一個簡單的前饋神經網絡(常見的神經網絡還有:卷積神經網絡、循環神經網絡等)。它接收輸入數據,依次通過多個網絡層進行處理,最終產生輸出結果。神經網絡的標準訓練流程通常包括以下步驟:
-
定義包含可訓練參數(或稱權重)的神經網絡結構
-
遍歷輸入數據集
-
將輸入數據送入網絡進行前向傳播
-
計算損失值(輸出結果與正確結果的偏差)
-
將梯度反向傳播至網絡各參數
-
使用簡單更新規則調整網絡權重:權重 = 權重 - 學習率 × 梯度(梯度下降算法的核心表達式)
1)定義網絡
import torch
import torch.nn as nn
import torch.nn.functional as Fclass Net(nn.Module):def __init__(self):super(Net, self).__init__()# 輸入圖像通道數1(灰度圖),輸出通道數6,5x5卷積核self.conv1 = nn.Conv2d(1, 6, 5)# 輸入通道數6,輸出通道數16,5x5卷積核self.conv2 = nn.Conv2d(6, 16, 5)# 全連接層(仿射變換):y = Wx + b# 輸入維度16*5*5(16個通道,5x5特征圖),輸出120維self.fc1 = nn.Linear(16 * 5 * 5, 120) # 5*5來自圖像維度# 全連接層:120維輸入,84維輸出self.fc2 = nn.Linear(120, 84)# 輸出層:84維輸入,10維輸出(對應10個類別)self.fc3 = nn.Linear(84, 10)def forward(self, input):# 卷積層C1:1輸入通道,6輸出通道,5x5卷積核# 使用ReLU激活函數,輸出張量尺寸為(N, 6, 28, 28),N是批次大小c1 = F.relu(self.conv1(input))# 下采樣層S2:2x2最大池化,無參數# 輸出張量尺寸為(N, 6, 14, 14)s2 = F.max_pool2d(c1, (2, 2))# 卷積層C3:6輸入通道,16輸出通道,5x5卷積核# 使用ReLU激活函數,輸出張量尺寸為(N, 16, 10, 10)c3 = F.relu(self.conv2(s2))# 下采樣層S4:2x2最大池化,無參數# 輸出張量尺寸為(N, 16, 5, 5)s4 = F.max_pool2d(c3, 2)# 展平操作:無參數,輸出(N, 400)張量s4 = torch.flatten(s4, 1)# 全連接層F5:輸入(N, 400),輸出(N, 120)# 使用ReLU激活函數f5 = F.relu(self.fc1(s4))# 全連接層F6:輸入(N, 120),輸出(N, 84)# 使用ReLU激活函數f6 = F.relu(self.fc2(f5))# 輸出層:輸入(N, 84),輸出(N, 10)output = self.fc3(f6)return output# 實例化網絡
net = Net()
print(net)
運行結果如下:
你只需定義前向傳播函數。反向傳播函數(用于計算梯度)將通過 autograd 自動定義。在前向傳播中,可以使用任何張量運算。
net.parameters() 返回模型的所有可訓練參數:
params = list(net.parameters()) # 返回可訓練參數(權重和偏置)并轉為 list
print(len(params))
print(params[0].size()) # conv1 的權重大小
運行結果如下:
現在嘗試輸入一個隨機的32×32矩陣。注意:該網絡(LeNet)的預期輸入尺寸為32×32。若要將此網絡用于 MNIST 數據集,請將數據集中的圖像尺寸調整為32×32。
input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)
運行結果如下:
將所有參數的梯度緩沖區清零,并使用隨機梯度進行反向傳播:
net.zero_grad()
out.backward(torch.randn(1, 10))
注意:torch.nn 模塊僅支持小批量輸入樣本(不能是單個樣本)。例如,nn.Conv2d 層需要接收一個4維張量,其維度為:樣本數×通道數×高度×寬度。若只有單個樣本,只需使用 input.unsqueeze(0) 來添加一個虛擬的批次維度。
回顧一下目前學到的類:
- torch.Tensor:支持自動求導運算的多維數組,如 backward() ,同時存儲著張量的梯度信息。
- nn.Module:神經網絡模塊。封裝參數的便捷方式,提供將參數移至 GPU、導出、加載等輔助功能。
- nn.Parameter:張量的一種,當被賦值給 Module 的屬性時,會自動注冊為模型參數。
- autograd.Function:實現自動求導運算的前向和反向定義。每個張量運算至少會創建一個Function 節點,該節點會記錄創建該張量的函數并編碼其運算歷史。
2)損失函數
損失函數接受入參 (output, target)?,并計算一個值,該值估計輸出與目標的差距。nn 包下有幾個不同的損失函數,一個簡單的損失函數是:nn.MSELoss,計算輸出和目標之間的均方誤差。例如:
output = net(input)
target = torch.randn(10) # 創建一個假目標
target = target.view(1, -1) # 使其與輸出的形狀相同
criterion = nn.MSELoss()loss = criterion(output, target)
print(loss)
運行結果如下:
現在,如果你反向跟蹤 loss 函數,使用它的 .grad_fn 屬性,你會看到一個如下所示的計算圖:
因此,當我們調用 loss.backward() 時,整個計算圖會相對于神經網絡參數進行微分,圖中所有 requires_grad=True 的張量都會累積梯度值到它們的 .grad 張量中。反向追蹤幾個步驟:
print(loss.grad_fn) # 輸出 MSE 損失函數的梯度計算節點
print(loss.grad_fn.next_functions[0][0]) # 輸出線性層的梯度計算節點
print(loss.grad_fn.next_functions[0][0].next_functions[0][0]) # 輸出 ReLU 激活層的梯度計算節點
運行結果如下:
3)反向傳播
要實現誤差反向傳播,只需執行 loss.backward()。但注意需要清除已有梯度,否則梯度會累積到現有梯度之上。通過如下代碼觀察反向傳播前后 conv1 層偏置的梯度變化:
net.zero_grad() # 將所有參數的梯度緩沖區置零print('conv1.bias.grad 反向傳播之前')
print(net.conv1.bias.grad)loss.backward()print('conv1.bias.grad 反向傳播之后')
print(net.conv1.bias.grad)
運行結果如下:
4)權重更新
實踐中最簡單的更新規則是隨機梯度下降法(簡稱 SGD):
weight = weight - learning_rate * gradient# 可以通過簡單的 Python 代碼實現:
learning_rate = 0.01
for f in net.parameters():f.data.sub_(f.grad.data * learning_rate)
在使用神經網絡時,我們可以使用各種不同的更新規則(SGD、 Nesterov-SGD、 Adam、 RMSProp 等)。可以通過一個小軟件包?torch.optim 來實現所有這些方法:
import torch.optim as optim# 創建優化器
optimizer = optim.SGD(net.parameters(), lr=0.01)# 訓練循環中的操作
optimizer.zero_grad() # 清空梯度緩沖
output = net(input)
loss = criterion(output, target)
loss.backward()
optimizer.step() # 執行更新
注意:觀察梯度緩沖區是如何使用 optimizer.zero_grad()?手動設置為零的(如前面所述,梯度是累積的)。
【本節完&持續更新】