文章目录
- 前言
- 链式法则
- 什么是链式法则
- 链式法则和计算图
 
- 反向传播
- 加法节点的反向传播
- 乘法节点的反向传播
- 苹果的例子
 
- 简单层的实现
- 乘法层的实现
- 加法层的实现
 
- 激活函数层的实现
- ReLu层
- Sigmoid层
 
- Affine层/SoftMax层的实现
- Affine层
- Softmax层
 
- 误差反向传播的实现
- 参考资料
前言
上一篇文章深度学习入门(三):神经网络的学习
 中,神经网络参数的学习是通过数值微分求梯度实现的,该方法虽然简单,但也有一个明显的问题就是计算费时。本文介绍一种高效计算参数梯度的方法——误差反向传播法。
注意,误差反向传播法本质上是一种梯度计算的技术,而梯度下降法是一种优化算法。
链式法则
什么是链式法则
先看以下的复合函数:
  
      
       
        
        
          z 
         
        
          = 
         
         
         
           t 
          
         
           2 
          
         
         
        
          t 
         
        
          = 
         
        
          x 
         
        
          + 
         
        
          y 
         
        
       
         z=t^2 \\ t=x+y 
        
       
     z=t2t=x+y
 回忆以前学过的求复合函数的导数的性质:复合函数的导数可以用构成复合函数的各个函数的导数的乘积表示。 因此:
  
      
       
        
         
          
          
            ∂ 
           
          
            z 
           
          
          
          
            ∂ 
           
          
            x 
           
          
         
        
          = 
         
         
          
          
            ∂ 
           
          
            z 
           
          
          
          
            ∂ 
           
          
            t 
           
          
         
        
          ∗ 
         
         
          
          
            ∂ 
           
          
            t 
           
          
          
          
            ∂ 
           
          
            x 
           
          
         
        
          = 
         
        
          2 
         
        
          t 
         
        
          ∗ 
         
        
          1 
         
        
          = 
         
        
          2 
         
        
          ( 
         
        
          x 
         
        
          + 
         
        
          y 
         
        
          ) 
         
        
       
         \frac{\partial z}{\partial x} = \frac{\partial z}{\partial t}* \frac{\partial t}{\partial x}=2t*1=2(x+y) 
        
       
     ∂x∂z=∂t∂z∗∂x∂t=2t∗1=2(x+y)
基于以上思想,神经网络中的链式法则就是:将复杂函数的导数分解为多个简单函数的导数的乘积,使得神经网络中权重的多层梯度计算成为可能。
链式法则和计算图
上例的过程,在计算图里表现出来即:
 
 
反向传播
加法节点的反向传播

乘法节点的反向传播

 可以看到实现乘法节点的反向传播时,要保留正向传播的信号。
苹果的例子

 上图可以理解为:如果苹果的价格和消费税增加相同的值,那么消费税将对最终的价格产生200倍的影响,苹果的个数将对最终的价格产生110倍的影响,苹果的价格将对最终的价格产生2.2倍的影响。
简单层的实现
乘法层的实现
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_price = 100
apple_num = 2
tax_rate = 1.1
# layer
mul_apple_layer = MulLayer()
mul_tax_layer = MulLayer()
# forward
apple_total_price = mul_apple_layer.forward(apple_price, apple_num)
final_price = mul_tax_layer.forward(apple_total_price, tax_rate)
print(final_price) # 220
加法层的实现
class AddLayer:
	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 * 1
		dy = dout * 1
		
		return dx, dy
用上面的乘法层和加法层,实现下面的例子:
 
# layer
mul_apple_layer = MulLayer()
mul_orange_layer = MulLayer()
add_apple_orange_layer = AddLayer()
mul_tax_layer = MulLayer()
# forward
apple_price_cur = mul_apple_layer.forward(apple_price, apple_num)
orange_price_cur = mul_orange_layer.forward(orange_price, orange_num)
all_price = add_apple_orange_layer.forward(apple_price_cur, orange_price_cur)
price = mul_tax_layer.forward(all_price, tax_rate)
# backward
d_price = 1
d_all_price, d_tax_rate = mul_tax_layer.barkward(d_price)
d_apple_price_cur, d_orange_price_cur = add_apple_orange_layer.backward(d_all_price)
d_apple_price, d_apple_num = mul_apple_layer.backward(d_apple_price_cur)
d_orange_price, d_orange_num = mul_apple_layer.backward(d_orange_price_cur)
激活函数层的实现
ReLu层
Relu函数的数学表达为:
  
      
       
        
        
          y 
         
        
          = 
         
         
         
           { 
          
          
           
            
             
             
               x 
              
             
            
            
             
              
              
                x 
               
              
                > 
               
              
                0 
               
              
             
            
           
           
            
             
             
               0 
              
             
            
            
             
              
              
                x 
               
              
                < 
               
              
                = 
               
              
                0 
               
              
             
            
           
          
         
        
       
         y=\begin{cases} x & x>0 \\ 0 & x<=0 \end{cases} 
        
       
     y={x0x>0x<=0
 其导数为:
  
      
       
        
         
          
          
            ∂ 
           
          
            y 
           
          
          
          
            ∂ 
           
          
            x 
           
          
         
        
          = 
         
         
         
           { 
          
          
           
            
             
             
               1 
              
             
            
            
             
              
              
                x 
               
              
                > 
               
              
                0 
               
              
             
            
           
           
            
             
             
               0 
              
             
            
            
             
              
              
                x 
               
              
                < 
               
              
                = 
               
              
                0 
               
              
             
            
           
          
         
        
       
         \frac{\partial y}{\partial x}=\begin{cases} 1 & x>0\\ 0 & x<=0 \end{cases} 
        
       
     ∂x∂y={10x>0x<=0
计算图表示如下:
 
 代码实现如下:
class Relu:
	def __init__(self):
		self.mask = None
	
	def forward(self, x):
		# x<=0 的地方保存为True,其它保存为False
		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
		
Sigmoid层
sigmoid函数的数学表达如下:
  
      
       
        
        
          y 
         
        
          = 
         
         
         
           1 
          
          
          
            1 
           
          
            − 
           
           
           
             e 
            
            
            
              − 
             
            
              x 
             
            
           
          
         
        
       
         y = \frac{1}{1-e^{-x}} 
        
       
     y=1−e−x1
图像如下:
 
 导数的计算图为:
 
 
class Sigmoid:
	def __init__(self):
		self.out = None
	
	def forward(self, x):
		out = 1 / (1 + np.exp(-x))
		
		return out
	def backward(self, dout):
		dx = dout * (1 - self.out) * self.out
		
		return dx
Affine层/SoftMax层的实现
Affine层
所谓Affine层指的是:在神经网络的正向传播中,进行矩阵乘积变换的处理,称为“仿射转换”,也称为“Affine层”。
 
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
	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
Softmax层
回顾一下softmax的函数表示:(如果也有softmax和sigmoid记不清的小伙伴,来个口诀吧:sigmoid看自己,softmax看大家,sigmoid是只关于x的计算,而softmax是将最终的输出结果归一化成一个概率值)
  
      
       
        
        
          σ 
         
        
          ( 
         
        
          z 
         
         
         
           ) 
          
         
           i 
          
         
        
          = 
         
         
          
          
            e 
           
           
           
             z 
            
           
             i 
            
           
          
          
           
           
             ∑ 
            
            
            
              j 
             
            
              = 
             
            
              1 
             
            
           
             K 
            
           
           
           
             e 
            
            
            
              z 
             
            
              j 
             
            
           
          
         
        
          , 
         
         
        
          其中  
         
        
          i 
         
        
          = 
         
        
          1 
         
        
          , 
         
        
          2 
         
        
          , 
         
        
          … 
         
        
          , 
         
        
          K 
         
        
       
         \sigma(\mathbf{z})_i = \frac{e^{z_i}}{\sum_{j=1}^{K} e^{z_j}}, \quad \text{其中 } i = 1, 2, \dots, K 
        
       
     σ(z)i=∑j=1Kezjezi,其中 i=1,2,…,K
如下图所示,可以看到最终的输出是通过softmax归一化。
 
 一个问题:为什么神经网络的推理不需要softmax,而学习却需要softmax呢?先思考为什么推理不需要softmax,这个比较简单理解,如果是分类任务,直接选择最后一个Affine输出的值的最大值对应的类作为结果即可,回归任务的输出层无需激活函数。然后思考为什么学习需要softmax,回想前面文章提到的损失函数的计算(交叉熵误差、MSE),都要求输入是一个概率值。
下图展示了softmax层的反向传播的结果,很神奇,我们得到了 
     
      
       
        
        
          y 
         
        
          1 
         
        
       
         − 
        
        
        
          t 
         
        
          1 
         
        
       
      
        y_1-t_1 
       
      
    y1−t1,  
     
      
       
        
        
          y 
         
        
          2 
         
        
       
         − 
        
        
        
          t 
         
        
          2 
         
        
       
      
        y_2-t_2 
       
      
    y2−t2,  
     
      
       
        
        
          y 
         
        
          3 
         
        
       
         − 
        
        
        
          t 
         
        
          3 
         
        
       
      
        y_3-t_3 
       
      
    y3−t3这样漂亮的结果(正好是标签和输出值的误差),这不是巧合,而是为了得到这样漂亮的结果,特地设计了交叉熵误差这样的损失函数。(此处省去了详细的推导过程)
 
def 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
误差反向传播的实现
和上一篇文章两层神经网络的实现一样,这里我们的步骤仍然是:
- mini-batch: 从训练数据中随机选择一部分样本;
- 计算梯度;
- 更新参数;
import sys, os
sys.path.append(os.pardir)
import numpy as np
from common.layers import *
from common.gradient import numerical_gradient
from collections import OrderedDict
class TwoLayerNet:
    def __init__(self, input_size, hidden_size, output_size,
                 weight_init_std=0.01):
        # 初始化权重
        self.params = {}
        self.params['W1'] = weight_init_std * \
                            np.random.randn(input_size, hidden_size)
        self.params['b1'] = np.zeros(hidden_size)
        self.params['W2'] = weight_init_std * \
                            np.random.randn(hidden_size, output_size)
        self.params['b2'] = np.zeros(output_size)
        # 生成层
        self.layers = OrderedDict()
        self.layers['Affine1'] = \
            Affine(self.params['W1'], self.params['b1'])
        self.layers['Relu1'] = Relu()
        self.layers['Affine2'] = \
            Affine(self.params['W2'], self.params['b2'])
        self.lastLayer = SoftmaxWithLoss()
    def predict(self, x):
        for layer in self.layers.values():
            x = layer.forward(x)
        return x
    # x:输入数据, t:监督数据
    def loss(self, x, t):
        y = self.predict(x)
        return self.lastLayer.forward(y, t)
    def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        if t.ndim != 1 : t = np.argmax(t, axis=1)
        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy
    # x:输入数据, t:监督数据
    def numerical_gradient(self, x, t):
        loss_W = lambda W: self.loss(x, t)
        grads = {}
        grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
        grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
        grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
        grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
        return grads
	
	# 在上一篇文章基础上,新增的函数,误差反向传播
    def gradient(self, x, t):
        # forward
        self.loss(x, t)
        # backward
        dout = 1
        dout = self.lastLayer.backward(dout)
        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)
        # 设定
        grads = {}
        grads['W1'] = self.layers['Affine1'].dW
        grads['b1'] = self.layers['Affine1'].db
        grads['W2'] = self.layers['Affine2'].dW
        grads['b2'] = self.layers['Affine2'].db
        return grads
import sys, os
sys.path.append(os.pardir)
import numpy as np
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet
# 读入数据
(x_train, t_train), (x_test, t_test) = \
    load_mnist(normalize=True, one_hot_label=True)
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
iters_num = 10000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1
train_loss_list = []
train_acc_list = []
test_acc_list = []
iter_per_epoch = max(train_size / batch_size, 1)
for i in range(iters_num):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
    # 通过误差反向传播法求梯度
    grad = network.gradient(x_batch, t_batch)
    # 更新
    for key in ('W1', 'b1', 'W2', 'b2'):
        network.params[key] -= learning_rate * grad[key]
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)
    if i % iter_per_epoch == 0:
        train_acc = network.accuracy(x_train, t_train)
        test_acc = network.accuracy(x_test, t_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        print(train_acc, test_acc)
参考资料
[1] 斋藤康毅. (2018). 深度学习入门:基于Python的理论与实践. 人民邮电出版社.



















