智慧型控制 Intelligent Control

自動微分與 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\) 出發,由右往左,每經過一個節點就乘上該節點的局部導數(連鎖律):

x1.5w0.8×u = 1.20b-0.2+n = 1.00tanha = 0.7616(·−t)²L = 0.0684∂L/∂w = 0.3296∂L/∂b = 0.2197∂L/∂n = 0.2197∂L/∂a = 0.5232白字:前向計算的值(由左往右) 紅字:反向傳回的梯度(由右往左,連鎖律逐點相乘)t = 0.5 $$\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 訓練程式的共同骨架,後面的卷積網路、強化學習也一樣,只是換了網路與損失函數。