PyTorch 入門
把前三課手寫的東西用 PyTorch 再做一遍:張量、自動求導、nn.Module、最佳化器。每一步都和手寫的版本對照,算出的梯度、訓練出的參數完全一樣。
- 約 45 分鐘
- 難度:入門
- 實測:2026-09-14 torch 2.14,CPU,固定隨機種子
程式碼和執行結果保留原樣(簡體中文),與實際執行時完全一致。
前三課我們手寫了線性迴歸、梯度下降和反向傳播。它們能用,但是慢,而且每換一個模型都要重寫很多程式碼。
PyTorch 是目前最常用的深度學習框架。這一課不講它的所有功能,只做一件事:把前三課做過的事用 PyTorch 再做一遍,並且每一步都和手寫的版本對照。你會發現,它做的事和你手寫的完全一樣,只是換了一套更快、更方便的寫法。
安裝(沒有顯示卡也沒關係,這一課和下一個模組全部在 CPU 上執行):
uv add torch
張量和自動求導
PyTorch 最基本的東西是張量(tensor):一個多維的陣列,和 numpy 的陣列很像。一個數是 0 維張量,一串數是 1 維,一張表格是 2 維。
張量有一個 numpy 陣列沒有的能力:自動求導。建立張量時加上 requires_grad=True,PyTorch 就會記下它參與的每一步運算,就像上一課的 Num 那樣:
a = torch.tensor(2.0, requires_grad=True) # requires_grad:记下它参与的运算,以便求导
b = torch.tensor(-3.0, requires_grad=True)
c = torch.tensor(10.0, requires_grad=True)
f = (a * b + c) ** 2
f.backward()
print(f" f = {f.item()},df/da={a.grad.item()}, df/db={b.grad.item()}, df/dc={c.grad.item()}")
== 1. 同一个算式 f = (a × b + c)²,a=2, b=-3, c=10
f = 16.0,df/da=-24.0, df/db=16.0, df/dc=8.0
和上一課手寫的 Num 算出的結果一模一樣:-24、16、8。f.backward() 做的,就是上一課 Num.backward 做的事:從 f 出發,沿著記下的計算過程,用鏈式法則把梯度傳回去,存進每個張量的 .grad 裡。.item() 把只有一個數的張量轉換成普通的 Python 數字。
線性迴歸:四樣東西
用 PyTorch 重寫第 2 課的線性迴歸,會用到 PyTorch 訓練模型的四樣基本東西:
x = torch.tensor((area - area.mean()) / area.std(), dtype=torch.float32).unsqueeze(1) # 标准化,形状 (50, 1)
y = torch.tensor(price, dtype=torch.float32).unsqueeze(1)
model = nn.Linear(1, 1) # 就是 w × x + b
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
loss_fn = nn.MSELoss()
for step in range(100):
loss = loss_fn(model(x), y)
optimizer.zero_grad() # 清空上一步的梯度
loss.backward() # 反向传播,算出每个参数的梯度
optimizer.step() # 按梯度更新参数:p -= lr × p.grad
- 模型:
nn.Linear(1, 1)就是w × x + b,1 個輸入、1 個輸出。它自己建立並管理w和b這兩個參數,並且已經設定好了requires_grad。 - 損失函式:
nn.MSELoss()就是第 1 課的均方誤差。 - 最佳化器:
torch.optim.SGD負責更新參數。optimizer.step()就是第 2 課的w -= lr × dw,只是它會對model.parameters()裡的每一個參數都做一遍。 - 訓練迴圈:清零梯度、反向傳播、更新參數,三行,順序和上一課手寫的完全一樣。
unsqueeze(1) 把形狀從 (50,) 變成 (50, 1),意思是"50 個樣本,每個樣本 1 個特徵"。PyTorch 的層都約定第一個維度是樣本的數量,下面會細說。
== 2. 线性回归 100 步后:损失 143.05,换算回原单位 w=1.2571 b=14.806
和第 2 課手寫的梯度下降結果完全一樣:損失 143.05,w = 1.2571, b = 14.806。
自己的網路:繼承 nn.Module
複雜一點的模型,要自己寫一個類,繼承 nn.Module。用它重寫上一課的異或網路:
class TinyNet(nn.Module):
def __init__(self):
super().__init__()
self.hidden = nn.Linear(2, 4)
self.output = nn.Linear(4, 1)
def forward(self, x):
return torch.tanh(self.output(torch.tanh(self.hidden(x))))
__init__ 裡定義有哪些層,forward 裡寫資料怎麼流過這些層。上一課我們寫了 Neuron 和 Layer 兩個類,現在一個 nn.Linear(2, 4) 就是"一層 4 個神經元,每個 2 個輸入",它把 4 個神經元的計算合在一起,用一次矩陣乘法完成。
nn.Module 會自動找到你在 __init__ 裡定義的所有層的參數,net.parameters() 就能拿到它們。數一數:
== 3. 异或网络,共 17 个参数
和上一課手寫的網路一樣,17 個參數。訓練:
X = torch.tensor([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=torch.float32)
Y = torch.tensor([[-1], [1], [1], [-1]], dtype=torch.float32)
optimizer = torch.optim.SGD(net.parameters(), lr=0.1)
for epoch in range(1, 301):
loss = ((net(X) - Y) ** 2).sum() # 和第 3 课一样:4 个样本的平方误差之和
optimizer.zero_grad()
loss.backward()
optimizer.step()
注意 net(X) 一次就算完了 4 個樣本。上一課我們用一個 for 迴圈一個一個地算,現在把 4 個樣本放進一個形狀為 (4, 2) 的張量,一次矩陣運算全部算完。
第 1 轮 损失 4.5469
第 10 轮 损失 3.7176
第 50 轮 损失 0.5588
第 100 轮 损失 0.0514
第 200 轮 损失 0.0174
第 300 轮 损失 0.0100
训练后的预测: ['-0.953', '+0.964', '+0.944', '-0.943']
同樣學會了異或。損失的變化過程和上一課的數字不完全一樣,因為參數的初始值是隨機的,PyTorch 和我們手寫的版本用了不同的隨機數。但趨勢一樣:前面慢,然後突然下降,最後接近 0。
訓練完做預測時,套上了 torch.no_grad():
with torch.no_grad(): # 只是预测,不需要记录求导信息
print(" 训练后的预测:", [f"{v:+.3f}" for v in net(X).squeeze(1).tolist()])
預設情況下,PyTorch 會為每一步運算記錄求導所需的資訊。只是預測、不需要訓練時,關掉它能省記憶體、更快。
形狀:最容易出錯的地方
用 PyTorch 寫程式碼,大部分的報錯都和張量的形狀有關。一個約定要牢記:第一個維度是一批樣本的數量。
batch = torch.randn(32, 2) # 32 个样本,每个 2 个特征
== 4. 一次喂 32 个样本:输入形状 (32, 2) → 隐藏层 (32, 4) → 输出 (32, 1)
輸入是 32 個樣本、每個 2 個特徵;經過隱藏層變成 32 個樣本、每個 4 個數;最後是 32 個樣本、每個 1 個輸出。樣本的數量在整個過程中保持不變,每一層只改變最後一個維度。
一次處理一批樣本,而不是一個一個處理,是深度學習快的關鍵原因之一。矩陣運算在 CPU 上、特別是在 GPU 上,都是高度最佳化過的,一次算 32 個樣本和算 1 個樣本花的時間差不了多少。
寫程式碼時,在關鍵的地方列印 x.shape,是最有效的除錯方法。
手寫和 PyTorch 對照
| 手寫的版本 | PyTorch |
|---|---|
Num,記下區域性導數 |
張量,requires_grad=True |
Num.backward() |
loss.backward() |
p.grad = 0.0 |
optimizer.zero_grad() |
p.value -= lr * p.grad |
optimizer.step() |
Neuron、Layer 兩個類 |
nn.Linear |
| 均方誤差的函式 | nn.MSELoss() |
| 一個一個樣本地迴圈 | 一批樣本放進一個張量,一次算完 |
PyTorch 沒有做任何魔法。你已經親手寫過它最核心的部分,現在只是換一套更快、更省事的寫法。以後遇到奇怪的訓練問題,比如損失不下降、梯度變成 NaN,你知道底下在發生什麼,就知道該從哪裡查起。
練習
- 把線性迴歸的資料換成沒有標準化的面積(直接用
area),學習率要設成多少才能不發散?和第 2 課的結論一致嗎? - 把
torch.optim.SGD換成torch.optim.Adam,學習率設成 0.01,重新訓練異或網路。損失下降得更快還是更慢? - 故意製造一個形狀錯誤:把異或的輸入
X改成形狀(4, 3)(每個樣本 3 個特徵),執行,讀一讀報錯資訊,看它是怎麼告訴你形狀不匹配的。
自測
1. optimizer.zero_grad()、loss.backward()、optimizer.step() 分別在做什麼?
zero_grad 把所有參數的梯度清零,避免和上一步的梯度累加;backward 從損失出發做反向傳播,算出每個參數的梯度,存在 .grad 裡;step 按照梯度更新每個參數,最簡單的 SGD 就是 p = p - lr × p.grad。
2. 為什麼 PyTorch 約定張量的第一個維度是樣本數量?
深度學習總是一次處理一批樣本,把它們放在一個張量裡用矩陣運算一起算,比一個一個算快得多。約定第一個維度是樣本數量,每一層就只需要處理最後的特徵維度,樣本數量在整個網路裡保持不變。
3. 做預測時為什麼要用 torch.no_grad()?
預設情況下,PyTorch 會為每一步運算記錄反向傳播所需的資訊,這要佔用額外的記憶體和時間。只做預測、不需要訓練時,用 no_grad 關掉它,更省資源、更快。
提問與討論
這一課沒看懂的地方,在這裡問。看到別人的問題,也歡迎你來回答。
提問 +3 點,回答別人 +6 點。內容經審核後公開。
正在載入討論…