用張量廣播機制實現神經網絡反向傳播

正向傳播

要想了解反向傳播,先要了解正向傳播:正向傳播的每一步是,用一個或不少輸入生成一個輸出。html

反向傳播

反向傳播的做用是計算模型參數的偏導數。再具體一點,反向傳播的每個step就是:已知正向傳播的輸入自己,和輸出的偏導數,求出每一個輸入的偏導數的過程。python

反向傳播既簡單,又複雜:數組

  • 它的原理很簡單:鏈式法則求偏導。
  • 它的公式又很複雜:由於它的公式看起來真的很複雜。

模型的參數

反向傳播就是計算模型的參數的偏導數,因此介紹一下模型的參數:網絡

  • 模型裏有不少參數,參數的本質是張量,能夠把張量當作多維數組,也能夠把張量當作一顆樹
  • 張量有形狀,張量的偏導數是一個一樣形狀的張量。

線性函數的反向傳播

線性函數就是 y = wx + b,咱們輸入x,w,和 b 就能獲得y。y是咱們算出來的,這個算y的過程就是正向傳播。函數

咱們規定字母后面加 .g 表示偏導數,如 y.g 就是y的偏導數,w.g 就是w的偏導數。測試

那麼咱們的目的,就是根據 x, w, by.g 的值,分別算出 w,x,和b的偏導數,而這個過程,就是反向傳播。ui

爲了便於說明,咱們假設了每一個變量的形狀: x(1000, 784), w(784, 50), b(50), y(1000, 50)。code

計算 x.g

y = wx + bx 求偏導 得 w,即咱們要用 wy.g 計算出 x.ghtm

w 的形狀是 (784, 50),y.g的形狀跟y相同,是(1000, 50),如何用這兩個形狀湊出 x.g 的(1000, 784)?blog

emmm,很簡單,就是這樣,而後那樣,就好了。看玩笑的。。其實就是 y.g 中間加一維,變成 (1000, 1, 50) ,而後再跟 w 搞一下,獲得一個 (1000, 784, 50) 的形狀,再把最後一維消去,就獲得 (1000, 784) 的形狀了。

即:
x.g = (y.g.unsqueeze(1) * w).sum(dim=-1)

計算 w.g

同理咯,y = wx + bw 求偏導 得 x,即咱們要用 xy.g 計算出 w.g

x 的形狀是 (1000, 784),y.g的形狀跟y相同,是(1000, 50),如何用這兩個形狀湊出 w.g` 的(784, 50)?

先將 x 最後加一維,變成 (1000, 784, 1),再將 y.g 中間加一維,變成 (1000, 1, 50),這倆搞一下,變成 (1000, 784, 50),再把開頭的那一維消去,就變成 (784, 50)了。

即:
w.g = (x.unsqueeze(-1) * y.g.unsqueeze(1)).sum(dim=0)

計算 b.g

y = wx + bb 求偏導 得常數 1,因此直接用形狀爲(1000, 50)的y.g來湊出形狀爲(50)的b.g就能夠了。

那麼就很是簡單了,直接把(1000, 50)消去最開始的那一維就能獲得(50),即:

b.g = y.g.sum(0)

線性函數的反向傳播代碼

已知線性函數的輸入是 inp,輸出是 out,計算過程用到的兩個參數是 wb,則反向傳播的代碼以下:

def back_lin(inp, w, b, out):
    inp.g = (out.g.unsqueeze(1) * w).sum(dim=-1)
    w.g = (inp.unsqueeze(-1) * out.g.unsqueeze(1)).sum(dim=0)
    b.g = out.g.sum(0)

relu函數的反向傳播

relu函數表示起來很簡單,就是 max(x, 0),可是在 pytorch 中這樣寫是行不通的,因此用這面這個函數表示:

def relu(x):
    return x.clamp_min(0)

其反向傳播表示爲:

def back_relu(inp, out):
    return (inp > 0).float() * out.g

mse函數的反向傳播

mse函數用代碼表示爲:

def mse(pred, target):
    return (pred.squeeze(dim=-1)-target).pow(2).mean()

其反向傳播則是:

def back_mse(pred, target):
    return 2. * (pred.squeeze(dim=-1) - target).unsqueeze(dim=-1) / pred.shape[0]

測試

假設咱們的模型結果爲:輸入一個x,進行一次線性變換,再通過一次relu,而後再通過一次線性變換獲得結果。

先隨機生成 輸入、輸出和各個參數:

# 僞造輸入和答案
import torch
torch.manual_seed(0)
input_ = torch.randn(1000, 784).requires_grad_(True)  # 輸入
target = torch.randn(1000)  # 答案
# 建立其它參數
w1 = torch.randn(784, 50).requires_grad_(True)
b1 = torch.randn(50).requires_grad_(True)
w2 = torch.randn(50, 1).requires_grad_(True)
b2 = torch.randn(1).requires_grad_(True)

正向傳播獲得模型的輸出:

l1 = input_ @ w1 + b1
l2 = relu(l1)
output = l2 @ w2 + b2
loss = mse(output, target)

反向傳播:

back_mse(output, target)
back_lin(l2, w2, b2, output)
back_relu(l1, l2)
back_lin(input_, w1, b1, l1)

此時 w1.gb1.gw2.gb2.g均已求出。

而後用pytorch自帶的反向傳播求一下梯度:

# 先保存一下手動求的梯度
w1g = w1.g.clone()
b1g = b1.g.clone()
w2g = w2.g.clone()
b2g = b2.g.clone()

input_ = input_.clone().requires_grad_(True)
w1 = w1.clone().requires_grad_(True)
b1 = b1.clone().requires_grad_(True)
w2 = w2.clone().requires_grad_(True)
b2 = b2.clone().requires_grad_(True)

l1 = input_ @ w1 + b1
l2 = relu(l1)
output = l2 @ w2 + b2
loss = mse(output, target)

loss.backward()

此時對比一下咱們手動求得的梯度和調用系統函數求得的梯度,發現兩者是相等的:

def is_same(a, b):
    return (a - b).max() < 1e-4

is_same(w1g, w1.grad), is_same(b2g, b2.grad), is_same(w2g, w2.grad), is_same(b2g, b2.grad)
"""輸出
(tensor(True), tensor(True), tensor(True), tensor(True))
"""

總結

藉助簡單的求導和張量的廣播機制,就能夠推導實現神經網絡的反向傳播。

相關文章
相關標籤/搜索