赞
踩
计算图就是将计算过程用图形表示出来,这里所说的图形是数据结构图,通过多个节点和边表示(边是用来连接节点的)。
下面我们先来通过一个简单的例子了解计算图的计算过程
假设我们有如下需求:
我们来考虑一个问题:
- 假设我们想知道苹果价格的上涨会在多大程度上影响最终的支付金额,即求“支付金额关于苹果的价格的导数”
- 设苹果价格为 x x x,支付金额为 L L L,则相当于求 ∂ L ∂ x \frac{\partial L}{\partial x} ∂x∂L
图中,反向传播“局部导数”,将导数的值卸载箭头下方,图中红色表示
从右向左(1 -> 1.1 -> 2.2 )
这意味着,如果苹果的价格上涨1块钱,最终的支付金额会增加2.2块钱
关于如何计算的,后面会介绍
同样的我们也可以计算出“支付金额关于苹果个数的导数”、“支付金额关于消费税的导数”
反向传播的计算顺序:
- 将信号 E E E 乘以节点的局部导数 ∂ y ∂ x \frac{\partial y}{\partial x} ∂x∂y
- 传递给下一个节点
通过这样的计算,可以高效地求出导数的值
z = t 2 t = x + y z = t^2 \\ t = x+y z=t2t=x+y
反向传播的计算顺序:
- 将节点的输入信号乘以节点的局部导数(偏导数),然后传递给下一个节点
比如,反向传播时,“**2”节点的输入时 ∂ z ∂ z \frac{\partial z}{\partial z} ∂z∂z,将其乘以局部导数 ∂ z ∂ t \frac{\partial z}{\partial t} ∂t∂z- 然后再将上一步的输出 ∂ z ∂ z ∂ z ∂ t \frac{\partial z}{\partial z} \frac{\partial z}{\partial t} ∂z∂z∂t∂z 作为下一节点的输入,同样乘以局部导数 ∂ t ∂ x \frac{\partial t}{\partial x} ∂x∂t
根据链式法则:
- ∂ z ∂ z ∂ z ∂ t ∂ t ∂ x = ∂ z ∂ t ∂ t ∂ x = ∂ z ∂ x \frac{\partial z}{\partial z} \frac{\partial z}{\partial t} \frac{\partial t}{\partial x} = \frac{\partial z}{\partial t} \frac{\partial t}{\partial x} = \frac{\partial z}{\partial x} ∂z∂z∂t∂z∂x∂t=∂t∂z∂x∂t=∂x∂z
对应于“ z z z 关于 x x x 的导数”
这里以 z = x + y z = x+y z=x+y 为对象来说明
∂
z
∂
x
=
1
\frac{\partial z}{\partial x} = 1
∂x∂z=1
∂
z
∂
y
=
1
\frac{\partial z}{\partial y} = 1
∂y∂z=1
如图, ∂ z ∂ x = 1 , ∂ z ∂ y = 1 \frac{\partial z}{\partial x} = 1 ,\frac{\partial z}{\partial y} = 1 ∂x∂z=1,∂y∂z=1
反向传播将上游传过来的导数乘以1,然后传向下游
也就是说,因为加法节点的反向传播只乘以1,所以输入的值会原封不动地流向下一个节点
class AddLayer:
def __init__(self):
pass
# 正向传播
def forward(self, x, y):
out = x + y
return out
# 反向传播
def backward(self, dout):
dx = dout * 1
dy = dout * 1
return dx, dy
这里以 z = x y z = xy z=xy 为对象来说明
∂
z
∂
x
=
y
\frac{\partial z}{\partial x} = y
∂x∂z=y
∂
z
∂
y
=
x
\frac{\partial z}{\partial y} = x
∂y∂z=x
乘法节点的反向传播需要正向传播时的输入信号值,因此,实现乘法节点的反向传播时,需要保存正向传播的输入信号
乘法节点的反向传播会乘以输入信号的翻转值
class MulLayer: def __init__(self): self.x = None self.y = None # 正向传播 def forward(self, x, y): self.x = x self.y = y out = x * y return out # 反向传播 def backward(self, dout): dx = dout * self.y dy = dout * self.x return dx, dy # 测试 apple = 100 # 苹果价格 apple_num = 2 # 苹果个数 tax = 1.1 # 消费税 mul_apple_layer = MulLayer() # 创建乘法器对象 mul_tax_layer = MulLayer() # 创建乘法器对象 # forward apple_price = mul_apple_layer.forward(apple, apple_num) # 2个苹果的价格 price = mul_tax_layer.forward(apple_price, tax) # 支付金额 # backward dprice = 1 dapple_price, dtax = mul_tax_layer.backward(dprice) dapple, dapple_num = mul_apple_layer.backward(dapple_price) print("price:", int(price)) print("dApple:", dapple) print("dApple_num:", int(dapple_num)) print("dTax:", dtax)
输出为:
price: 220
dApple: 2.2
dApple_num: 110
dTax: 200
结果与上图中的反向传播的结果一样
apple = 100 apple_num = 2 orange = 150 orange_num = 3 tax = 1.1 # layer mul_apple_layer = MulLayer() mul_orange_layer = MulLayer() add_apple_orange_layer = AddLayer() mul_tax_layer = MulLayer() # forward apple_price = mul_apple_layer.forward(apple, apple_num) # (1) orange_price = mul_orange_layer.forward(orange, orange_num) # (2) all_price = add_apple_orange_layer.forward(apple_price, orange_price) # (3) price = mul_tax_layer.forward(all_price, tax) # (4) # backward dprice = 1 dall_price, dtax = mul_tax_layer.backward(dprice) # (4) dapple_price, dorange_price = add_apple_orange_layer.backward(dall_price) # (3) dorange, dorange_num = mul_orange_layer.backward(dorange_price) # (2) dapple, dapple_num = mul_apple_layer.backward(dapple_price) # (1) print("price:", int(price)) print("dApple:", dapple) print("dApple_num:", int(dapple_num)) print("dOrange:", dorange) print("dOrange_num:", int(dorange_num)) print("dTax:", dtax)
输出为:
price: 715
dApple: 2.2
dApple_num: 110
dOrange: 3.3000000000000003
dOrange_num: 165
dTax: 650
由导数可知,
- 如果正向传播时的输入x大于0,则反向传播会将上游的值原封不动地传递给下游(乘以1)
- 如果正向传播时的输入x小于等于0,则反向传播传递给下游的信号将停在此处(乘以0)
class Relu: def __init__(self): self.mask = None def forward(self, x): self.mask = (x <= 0) out = x.copy() out[self.mask] = 0 return out def backward(self, dout): dout[self.mask] = 0 dx = dout return dx
会将输入中小于等于0的,改为0,大于0的保留
ReLu层的作用就像电路开关一样,正向传播时,有电流通过的话,就将开关设为ON;没有电流通过的话,就将开关设为OFF。
反向传播时,开关为ON的话,电流会直接通过;开关为OFF的话,则不会有电流通过。
数学表达式为:
y
=
1
1
+
e
−
x
y = \frac{1}{1 + e^{-x}}
y=1+e−x1
计算图如下:
其中,除了我们之前介绍过的“×”和“+”节点之外,还多了“exp”和“/”节点。
“exp”节点会进行 y = e x p ( x ) y = exp(x) y=exp(x)的计算,“/”节点会进行 y = 1 x y = \frac{1}{x} y=x1的计算
对于“/”节点, y = 1 x y = \frac{1}{x} y=x1
导数为:
∂
y
∂
x
=
−
1
x
2
=
−
y
2
\frac{\partial y}{\partial x} = -\frac{1}{x^2} = -y^2
∂x∂y=−x21=−y2
对于“exp”节点, y = e x p ( x ) y = exp(x) y=exp(x)
导数为:
∂
y
∂
x
=
e
x
p
(
x
)
\frac{\partial y}{\partial x} = exp(x)
∂x∂y=exp(x)
反向传播如下图:
其中
- ∂ L ∂ y y 2 e x p ( − x ) = ∂ L ∂ y y ( 1 − y ) \frac{\partial L}{\partial y}y^2exp(-x) = \frac{\partial L}{\partial y} y(1-y) ∂y∂Ly2exp(−x)=∂y∂Ly(1−y)
上图可转化为:
python实现
import numpy as np def sigmoid(x): return 1 / (1 + np.exp(-x)) class Sigmoid: def __init__(self): self.out = None def forward(self, x): out = sigmoid(x) self.out = out return out def backward(self, dout): dx = dout * (1.0 - self.out) * self.out return dx
正向传播时将输出保存在变量out中,然后,反向传播时,使用该变量进行计算
此文中我们介绍了神经网络的内积,神经网络的正向传播中,为了计算加权信号的总和,使用了矩阵的乘积运算
Y
=
n
p
.
d
o
t
(
X
,
W
)
+
B
Y = np.dot(X, W) + B
Y=np.dot(X,W)+B
计算图如下:
以矩阵为对象的反向传播
∂
L
∂
X
=
∂
L
∂
Y
⋅
W
T
\frac{\partial L}{\partial X} = \frac{\partial L}{\partial Y} \cdot W^T
∂X∂L=∂Y∂L⋅WT
∂
L
∂
W
=
X
T
⋅
∂
L
∂
Y
\frac{\partial L}{\partial W} = X^T\cdot\frac{\partial L}{\partial Y}
∂W∂L=XT⋅∂Y∂L
其中 W T W^T WT为 W W W的转置
与前面的不同之处在于输入X的形状为(N,2),方向传播时,注意矩阵的形状,就可以和前面一样推到出 ∂ L ∂ X , ∂ L ∂ W \frac{\partial L}{\partial X} ,\frac{\partial L}{\partial W} ∂X∂L,∂W∂L
此外要注意:
- 正向传播时,偏置B被加到X·W的各个数据上;
- 反向传播时,各个数据的反向传播的值需要汇总为偏置的元素
上例中,假设数据有2个(N=2),偏置的反向传播会对这2个数据的导数暗元素进行求和
class Affine: def __init__(self, W, b): self.W =W self.b = b self.x = None self.original_x_shape = None # 权重和偏置参数的导数 self.dW = None self.db = None def forward(self, x): # 对应张量 self.original_x_shape = x.shape x = x.reshape(x.shape[0], -1) self.x = x out = np.dot(self.x, self.W) + self.b return out def backward(self, dout): dx = np.dot(dout, self.W.T) self.dW = np.dot(self.x.T, dout) self.db = np.sum(dout, axis=0) dx = dx.reshape(*self.original_x_shape) # 还原输入数据的形状(对应张量) return dx
上图表示:
假定了一个3类别分类的神经网络,从前面的层输入的是 ( a 1 , a 2 , a 3 ) (a_1, a_2, a_3) (a1,a2,a3),softmax层输出 ( y 1 , y 2 , y 3 ) (y_1, y_2, y_3) (y1,y2,y3)。此外,监督标签是 ( t 1 , t 2 , t 3 ) (t_1, t_2, t_3) (t1,t2,t3)
Cross-Entropy-Error层输出的是损失 L L L
Softmax-with-Loss层的反向传播的结果为 ( y 1 − t 1 , y 2 − t 2 , y 3 − t 3 ) (y_1-t_1, y_2-t_2, y_3-t_3) (y1−t1,y2−t2,y3−t3)
上图中多个“log”节点
“log”节点, y = l o g x y = log\ x y=log x
导数:
∂ y ∂ x = 1 2 \frac{\partial y}{\partial x} = \frac{1}{2} ∂x∂y=21
- “×”节点:反向传播时将正向传播的输入值翻转,乘以上游传过来的导数后,传递给下游
- “+”节点:将上游传来的导数原封不动地传递给下游
Cross-Entropy-Error层的反向传播的结果为:
(
−
t
1
y
1
,
−
t
2
y
2
,
−
t
3
y
3
)
(-\frac{t_1}{y_1},-\frac{t_2}{y_2},-\frac{t_3}{y_3})
(−y1t1,−y2t2,−y3t3)
是传递给softmax层反向传播的输入
通过结果我们可以发现,Softmax层反向传播的输出是输出标签和监督标签的差分
神经网络学习的目的就是通过调整权重参数,使神经网络的输出(Softmax层的输出)接近监督标签,而前面的结果直截了当地表示了神经网络的输出与监督标签的误差。
神经网络的反向传播会把这个差分表示的误差传递给前面的层,这是神经网络学习中的重要性质
# softmax函数 def softmax(x): if x.ndim == 2: x = x.T x = x - np.max(x, axis=0) y = np.exp(x) / np.sum(np.exp(x), axis=0) return y.T x = x - np.max(x) # 溢出对策 return np.exp(x) / np.sum(np.exp(x)) # 交叉熵误差 def cross_entropy_error(y, t): if y.ndim == 1: t = t.reshape(1, t.size) y = y.reshape(1, y.size) # 监督数据是one-hot-vector的情况下,转换为正确解标签的索引 if t.size == y.size: t = t.argmax(axis=1) batch_size = y.shape[0] return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size class SoftmaxWithLoss: def __init__(self): self.loss = None self.y = None # softmax的输出 self.t = None # 监督数据 def forward(self, x, t): self.t = t self.y = softmax(x) self.loss = cross_entropy_error(self.y, self.t) return self.loss def backward(self, dout=1): batch_size = self.t.shape[0] if self.t.size == self.y.size: # 监督数据是one-hot-vector的情况 dx = (self.y - self.t) / batch_size else: dx = self.y.copy() dx[np.arange(batch_size), self.t] -= 1 # 反向传播时,要将传播的值除以批的大小后,传递给前面的层的是单个数据的误差 dx = dx / batch_size return dx
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。