Press "Enter" to skip to content

用张量广播机制实现神经网络反向传播

本站内容均来自兴趣收集,如不慎侵害的您的相关权益,请留言告知,我们将尽快删除.谢谢.

正向传播

 

要想了解反向传播,先要了解正向传播:正向传播的每一步是,用一个或很多输入生成一个输出。

 

反向传播

 

反向传播的作用是计算模型参数的偏导数。再具体一点,反向传播的每一个step就是:已知正向传播的输入本身,和输出的偏导数,求出每个输入的偏导数的过程。

 

反向传播既简单,又复杂:

它的原理很简单:链式法则求偏导。
它的公式又很复杂:因为它的公式看起来真的很复杂。

模型的参数

 

反向传播就是计算模型的参数的偏导数,所以介绍一下模型的参数:

模型里有很多参数,参数的本质是张量,可以把张量看成多维数组,也可以把张量看成一颗树。

张量有形状,张量的偏导数是一个 同样形状
的张量。

线性函数的反向传播

 

线性函数就是 y = wx + b
,我们输入x,w,和 b 就能得到y。y是我们算出来的,这个算y的过程就是正向传播。

 

我们规定字母后面加 .g
表示偏导数,如 y.g
就是y的偏导数, w.g
就是w的偏导数。

 

那幺我们的目的,就是根据 x
, w
, b
y.g
的值,分别算出 w
, x
,和 b
的偏导数,而这个过程,就是反向传播。

 

为了便于说明,我们假设了每个变量的形状: x(1000, 784), w(784, 50), b(50), y(1000, 50)。

 

计算 x.g

 

y = wx + b
x
求偏导 得 w
,即我们要用 w
y.g
计算出 x.g

 

w
的形状是 (784, 50), y.g
的形状跟y相同,是(1000, 50),如何用这两个形状凑出 x.g
的(1000, 784)?

 

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 + b
w
求偏导 得 x
,即我们要用 x
y.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 + b
b
求偏导 得常数 1
,所以直接用形状为(1000, 50)的 y.g
来凑出形状为(50)的 b.g
就可以了。

 

那幺就非常简单了,直接把(1000, 50)消去最开始的那一维就能得到(50),即:

 

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

 

线性函数的反向传播代码

 

已知线性函数的输入是 inp
,输出是 out
,计算过程用到的两个参数是 w
b
,则反向传播的代码如下:

 

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.g
b1.g
w2.g
b2.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))
"""

 

总结

 

借助简单的求导和张量的广播机制,就可以推导实现神经网络的反向传播。

Be First to Comment

发表评论

邮箱地址不会被公开。 必填项已用*标注