在四中,采取了数值微分的方式求损失函数关于权重参数的梯度,但是比较耗时间,这一节讲一个能够高效计算权重参数的梯度的方法------误差反向传播法。
一,计算图
计算图指的就是将计算过程用图形表示出来。
计算图的表示方式为:计算图通过节点和箭头表示计算过程。节点用○表示,○中是计算的内容。将计算的中间结果写在箭头的上方,表示各个节点的计算结果从左向右传递。
**问题1:**首先来看看一个简单的题: 小明在水果摊买了2个单价100的苹果,消费税为10%,请计算支付金额
表示如下:
如果在○里只写计算方式,可表示如下:
问题2:小明在水果摊买了2个苹果、3个砂糖橘,苹果每个100,砂糖橘每个150,消费税10%,请计算支付金额。
像这种从左向右来金蒜,简称为正向传播
从计算图中可以看到,无论全局是多么复杂的问题,只要各个节点的局部运算正确就可以解决问题
如果说想知道苹果价格的上涨会在多大程度上影响最终的支付金额,也就是说求"支付金额关于苹果的价格的导数",可以通过计算图的反向传播求导数
二,链式法则
在反向传播中,是将局部导数从右到左传递,而传递这个局部导数的原理,是基于链式法则
2.1 链式法则
是复合函数里面的定义,如有以下复合函数:
要求z关于x的导数,可表示为
链式法则的定义如下:如果某个函数由复合函数表示,则该复合函数的导数可以用构成复合函数的各个函数的导数的乘积表示。
2.2 计算图的反向传播
将上式的链式法则用计算图表示出来,如下
如图所示,反向传播的计算顺序是,将反向过程中的节点输入信号乘以节点的局部导数,然后将结果传递给下一个节点。
这时我们重新考虑之前的苹果的价格、苹果的个数、消费税这3个变量各自如何影响最终支付的金额的问题。具体如下:
这里主要是因为在计算图中,如果节点的算法为乘法,那么反向传播就是乘以输入信号的翻转值(对应偏导)
四,简单层的实现
在上面的例子中,我们主要涉及了乘法和加法两种,那么,我们将要实现的计算图的乘法节点和加法节点各自封装成层类
4.1 乘法层
乘法层实现如下:
python
class MulLayer:
#初始化x、y,保存正向传播的输入值
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 # 翻转x和y
dy = dout * self.x
return dx, dy
假如借用这个类来实现买苹果的案例
代码实现如下:
ini
apple = 100
apple_num = 2
tax = 1.1
# layer
mul_apple_layer = MulLayer()
mul_tax_layer = MulLayer()
# forward
apple_price = mul_apple_layer.forward(apple, apple_num)
price = mul_tax_layer.forward(apple_price, tax)
print(price) # 220
# backward
dprice = 1
dapple_price, dtax = mul_tax_layer.backward(dprice)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)
print(dapple, dapple_num, dtax) # 2.2 110 200
4.2 加法层
加法层实现如下:
ruby
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
4.3 案例实现
案例如下
代码来实现如下案例的前向和反向过程
ini
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) # 715
print(dapple_num, dapple, dorange, dorange_num, dtax) # 110 2.2 3.3 165 650
五,激活函数层的实现
结合计算图的思想,将神经网络的的激活函数层也类似实现
5.1 ReLU层
激活函数ReLU如下所示:
求导如下:
用计算图来表示,如下:
代码实现如下:
ruby
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
变量mask是由True/False构成的NumPy数组,正向传播时把x中小于等于0的保存为True,其他大于0的保存为False。
这样,在正向的时候,小于等于0的,输出0,大于0的输出不变
在反向的时候,使用正向的mask,将反向输入的小于等于0的输出0,大于0的输出不变
5.2 Sigmoid层
sigmoid函数表示如下:
用计算图来表示为:
求导为: <math xmlns="http://www.w3.org/1998/Math/MathML"> y ' = y ( 1 − y ) y` = y(1-y) </math>y'=y(1−y)
代码实现为:
ruby
class Sigmoid:
def __init__(self):
self.out = None
def forward(self, x):
out = 1 / (1 + np.exp(-x))
self.out = out
return out
def backward(self, dout):
dx = dout * (1.0 - self.out) * self.out
return dx
六,Affine/Softmax层的实现
6.1 Affine层(全连接)
在Affine层中,输入数据通过一个权重矩阵(W)和一个偏置向量(b)的乘法,再加上偏置向量来计算输出。其数学表示为:Y = np.dot(X, W) + B,用计算图表示如下
计算各自的偏导
再在计算图中加入反向传播,如下图所示
这时的输入X是以单个数据为对象的,那假如我们现在开始考虑N个数据一起进行正向传播
主要是偏置,正向传播时,偏置会被加到每一个数据,所以,反向传播时,各个数据的反向传播的值需要汇总为偏置的元素
代码实现如下:
python
class Affine:
def __init__(self, W, b):
self.W = W
self.b = b
self.x = None
self.dW = None
self.db = None
def forward(self, x):
self.x = x
out = np.dot(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)
return dx
6.2 Softmax-with-Loss 层
softmax函数会将输入值正规化之后再输出,再考虑也包含作为损失函数的交叉熵误差,所以称为"Softmax-with-Loss层"。
假设要进行3类分类,计算图如下:
可以再简化一下
Softmax层将输入(a1, a2, a3)正规化,输出(y1, y2, y3)。 Cross Entropy Error层接收Softmax的输出(y1, y2, y3)和教师标签(t1, t2, t3),从这些数据中输出损失L。
用代码实现:
python
class SoftmaxWithLoss:
def __init__(self):
self.loss = None # 损失
self.y = None # softmax的输出
self.t = None # 监督数据(one-hot vector)
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]
dx = (self.y - self.t) / batch_size
return dx