自動微分與 PyTorch
原課程的倒傳遞網路要先手推每一層的梯度公式,網路結構一改,公式就得重推。現在的深度學習框架用自動微分(automatic differentiation)解決這件事:只要寫出前向計算,梯度由程式自動算出。
計算圖 Computational Graph
任何計算都可以拆成一連串基本運算(加、乘、tanh…),畫成計算圖。以一個神經元與平方誤差為例:
$$u = w x,\qquad n = u + b,\qquad a = \tanh n,\qquad L = (a - t)^2$$取 \(x = 1.5\)、\(w = 0.8\)、\(b = -0.2\)、\(t = 0.5\)。前向由左往右算出每個節點的值;反向從 \(\partial L/\partial L = 1\) 出發,由右往左,每經過一個節點就乘上該節點的局部導數(連鎖律):
$$\frac{\partial L}{\partial a} = 2(a - t),\qquad \frac{\partial L}{\partial n} = \frac{\partial L}{\partial a}\,(1 - a^2),\qquad \frac{\partial L}{\partial b} = \frac{\partial L}{\partial n},\qquad \frac{\partial L}{\partial w} = \frac{\partial L}{\partial n}\,x$$每種基本運算只要知道自己的局部導數,框架就能把任意組合的梯度算出來。這種由輸出往輸入的做法稱為反向模式自動微分:不論有多少參數,算出全部梯度的成本只是前向計算的幾倍。倒傳遞演算法就是它用在類神經網路上的特例。
在 PyTorch 中 Autograd in PyTorch
把參數設成 requires_grad=True,PyTorch 就會在前向計算時記下計算圖;呼叫 backward() 時沿圖反向求梯度,結果放在 .grad:
# 自動微分:a = tanh(w·x + b),L = (a − t)²,用 PyTorch 求 ∂L/∂w、∂L/∂b
import torch
x, t = torch.tensor(1.5), torch.tensor(0.5)
w = torch.tensor(0.8, requires_grad=True) # 要求梯度的參數
b = torch.tensor(-0.2, requires_grad=True)
a = torch.tanh(w * x + b) # 前向:PyTorch 一邊算一邊記下計算圖
L = (a - t) ** 2
L.backward() # 反向:沿計算圖用連鎖律算出所有梯度
print(f"a = {a.item():.4f}, L = {L.item():.4f}")
print(f"dL/dw = {w.grad.item():.4f}, dL/db = {b.grad.item():.4f}")
# 和手算的連鎖律比對:dL/dw = 2(a − t)·(1 − a²)·x
dw = 2 * (a - t) * (1 - a ** 2) * x
print(f"手算 dL/dw = {dw.item():.4f}")⬇ 下載 autograd_basic.py(Python 3+PyTorch)
實際執行的輸出:
a = 0.7616, L = 0.0684
dL/dw = 0.3296, dL/db = 0.2197
手算 dL/dw = 0.3296
與上圖手算的結果完全相同。
從 Matlab 工具箱到 PyTorch Mapping
| 原課程(Matlab) | PyTorch | 說明 |
|---|---|---|
newff(PR,[3,1],{'tansig','purelin'}) | nn.Sequential(nn.Linear(2,3), nn.Tanh(), nn.Linear(3,1)) | 每一層 = 線性運算+激活函數;purelin 就是不加激活函數 |
net.IW{1,1}、net.b{1} | net[0].weight、net[0].bias | 權值矩陣的列數=這一層的神經元個數,兩者相同 |
| 輸入矩陣每一行一筆資料 | 輸入張量每一列一筆資料 | PyTorch 的第一個維度是批次 |
net.performFcn = 'mse' | nn.MSELoss() | 分類問題改用 nn.CrossEntropyLoss 等 |
net.trainFcn = 'traingd' | torch.optim.SGD | 也可換成 torch.optim.Adam |
train(net,p,t) | 自己寫訓練迴圈(見下) | 每一步:清梯度 → 前向 → 算損失 → backward() → step() |
sim(net,p) | net(p) | 前向計算 |
範例:重做原課程的倒傳遞網路 Example
倒傳遞網路一頁的附檔,是 Matlab 手冊的 newff 範例:2 個輸入、3 個 tansig 隱藏神經元、1 個線性輸出,用四筆資料訓練。用 PyTorch 改寫,分別以 SGD(對應 traingd)與 Adam 訓練 300 回合:
# 用 PyTorch 重做 Matlab 的倒傳遞範例:newff([-1 2; 0 5], [3,1], {'tansig','purelin'}, 'traingd')
import torch
from torch import nn
torch.manual_seed(0)
p = torch.tensor([[-1., 0.], [-1., 5.], [2., 0.], [2., 5.]]) # 每一列是一筆輸入(Matlab 是每一行)
t = torch.tensor([[-1.], [-1.], [1.], [1.]])
def make_net():
return nn.Sequential(nn.Linear(2, 3), nn.Tanh(), nn.Linear(3, 1)) # tansig = tanh,purelin = 不加函數
for name, Opt, lr in [("SGD(同 traingd)", torch.optim.SGD, 0.05), ("Adam", torch.optim.Adam, 0.05)]:
torch.manual_seed(1)
net = make_net()
opt = Opt(net.parameters(), lr=lr)
loss_fn = nn.MSELoss() # 同 Matlab 的 mse
for epoch in range(1, 301):
opt.zero_grad() # 清掉上一步的梯度
loss = loss_fn(net(p), t) # 前向+誤差
loss.backward() # 倒傳遞
opt.step() # 更新權值
if epoch in (1, 50, 300):
print(f"{name:14s} 第 {epoch:3d} 回合 mse = {loss.item():.2e}")
print(" 訓練後輸出:", [round(v, 3) for v in net(p).squeeze().tolist()])⬇ 下載 netbp_torch.py(Python 3+PyTorch)
實際執行的輸出:
SGD(同 traingd) 第 1 回合 mse = 1.24e+00
SGD(同 traingd) 第 50 回合 mse = 2.10e-03
SGD(同 traingd) 第 300 回合 mse = 8.95e-06
訓練後輸出: [-0.998, -1.002, 0.996, 1.003]
Adam 第 1 回合 mse = 1.24e+00
Adam 第 50 回合 mse = 3.75e-03
Adam 第 300 回合 mse = 2.15e-06
訓練後輸出: [-1.0, -1.0, 1.002, 0.998]
兩者都把四筆資料擬合到誤差 \(10^{-5}\) 以下。訓練迴圈的五個步驟(清梯度、前向、損失、反向、更新)是所有 PyTorch 訓練程式的共同骨架,後面的卷積網路、強化學習也一樣,只是換了網路與損失函數。