误差反向传播法

计算图的反向传播

假设存在 y=f(x)y = f(x)y=f(x) 的计算,这个计算的反向传播如图5-6所示。

在这里插入图片描述

如图所示,反向传播的计算顺序是,将信号 EEE 乘以节点的局部导数 (∂x∂y)(\frac{\partial x}{\partial y})(yx),然后将结果传递给下一个节点。这里所说的局部导数是指正向传播中 y=f(x)y = f(x)y=f(x)的 导数,也就是 yyy 关于 xxx 的导数 (∂x∂y)(\frac{\partial x}{\partial y})(yx)。比如,假设 y=f(x)=x2y = f(x) = x^2y=f(x)=x2 ,则局部导数为 (∂x∂y)=2x(\frac{\partial x}{\partial y}) = 2x(yx)=2x。把这个局部导数乘以上游传过来的值(本例中为 EEE ),然后传递给前面的节点。

这就是反向传播的计算顺序。通过这样的计算,可以高效地求出导数的值,这是反向传播的要点。

链式法则

定义

如果某个函数由复合函数表示,则该复合函数的导数可以用构成复合函数的各个函数的导数的乘积表示。

示例

z=(x+y)2z = (x + y)^2z=(x+y)2 的导数, 它由两个式子构成,式(5.3) z=t2t=x+y\begin{aligned} z&=t^2 \\ t&=x+y \end{aligned}zt=t2=x+y
求导过程,式(5.4)

∂z∂x=∂z∂t∂t∂x=2t⋅1=2(x+y)\frac{\partial z}{\partial x} = \frac{\partial z}{\partial t}\frac{\partial t}{\partial x} = 2t\cdot1 = 2(x+y)xz=tzxt=2t1=2(x+y)

链式法则和计算图

用计算图表示出来。**2节点表示平方运算
在这里插入图片描述

如图所示,计算图的反向传播从右到左传播信号。反向传播的计算顺序是,先将节点的输入信号乘以节点的局部导数(偏导数),然后再传递给下一个节点。比如,反向传播时,**2 节点的输入是 ∂z∂x\frac{\partial z}{\partial x}xz,将其乘以局部导数 ∂z∂t\frac{\partial z}{\partial t}tz(因为正向传播时输入是ttt、输出是zzz,所以这个节点的局部导数是(∂z∂t\frac{\partial z}{\partial t}tz ),然后传递给下一个节点。另外,图5-7中反向传播最开始的信号在前面的数学式中没有出现,这是因为,所以在刚才的式子中被省略了。

图 5-7 中需要注意的是最左边的反向传播的结果。根据链式法则,∂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}zztzxt=tzxt=xz 成立,对应 “zzz关于xxx的导数”。也就是说,反向传播是基于链式法则的。

把式(5.3)的结果代入到图 5-7 中,结果如图 5-8 所示,∂z∂x\frac{\partial z}{\partial x}xz的结果为 2(x+y)2(x + y)2(x+y)
在这里插入图片描述

反向传播

证明通过了反向传播是基于链式法则成立的。现在介绍反向传播的结构。

加法节点的反向传播

z=x+yz = x + yz=x+y 为对象,观察它的方向传播。z=x+yz = x + yz=x+y 的导数可由下式(解析性的)计算出来。式(5.5),∂z∂x=1∂z∂y=1\begin{aligned} \frac{\partial z}{\partial x}&=1 \\ \frac{\partial z}{\partial y}&=1 \end{aligned}xzyz=1=1
如式(5.5)所示,∂z∂x\frac{\partial z}{\partial x}xz∂z∂y\frac{\partial z}{\partial y}yz同时都等于1。因此,用计算图表示的话,如图5-9所示。

在图5-9中,反向传播将从上游传过来的导数(本例中是∂L∂z\frac{\partial L}{\partial z}zL)乘以1,然后传向下游。也就是说,因为加法节点的反向传播只乘以1,所以输入的值会原封不动地流向下一个节点
在这里插入图片描述

另外,本例中把从上游传过来的导数的值设为 ∂L∂z\frac{\partial L}{\partial z}zL 。这是因为,如图5-10所示,我们假定了一个最终输出值为 LLL 的大型计算图。z=x+yz = x + yz=x+y 的计算位于这个大型计算图的某个地方,从上游会传来 ∂L∂z\frac{\partial L}{\partial z}zL 的值,并向下游传递 ∂L∂x\frac{\partial L}{\partial x}xL∂L∂y\frac{\partial L}{\partial y}yL
在这里插入图片描述

乘法节点的反向传播

z=xyz = xyz=xy 为对象,观察它的方向传播。z=xyz = xyz=xy 的导数可由下式(解析性的)计算出来。式(5.6),∂z∂x=y∂z∂y=x\begin{aligned} \frac{\partial z}{\partial x}&=y \\ \frac{\partial z}{\partial y}&=x \end{aligned}xzyz=y=x
根据式(5.6),可以像图5-12那样画计算图。

乘法的反向传播会将上游的值乘以正向传播时的输入信号的“翻转值”后传递给下游。翻转值表示一种翻转关系,如图5-12所示,正向传播时信号是 xxx 的话,反向传播时则是 yyy ;正向传播时信号是 yyy 的话,反向传播时则是 xxx

在这里插入图片描述

例子

假设有“10 × 5 = 50”这一计算,反向传播时,从上游会传来值1.3。用计算图表示的话,如图5-13所示。

在这里插入图片描述

因为乘法的反向传播会乘以输入信号的翻转值,所以各自可按1.3 × 5 =6.5、1.3 × 10 = 13计算。另外,加法的反向传播只是将上游的值传给下游,并不需要正向传播的输入信号。但是,乘法的反向传播需要正向传播时的输入信号值。因此,实现乘法节点的反向传播时,要保存正向传播的输入信号。

乘法层和加法层的简单实现

# coding: utf-8 

""" 乘法层的类 """
class MulLayer:

    """
     初始化实例变量x和y,它们用于保存正向传播时的输入值
    """
    def __init__(self):
        self.x = None
        self.y = None

    """
    输入参数为x和y,即正向传播时的输入值,将它们相乘后输出
    """
    def forward(self, x, y):
        self.x = x
        self.y = y                
        out = x * y
        return out

    """
     输入参数为dout,即上游传来的导数,将它乘以正向传播的翻转值,然后传给下游
    """
    def backward(self, dout):
        # 翻转x和y
        dx = dout * self.y
        dy = dout * self.x
        return dx, dy
  

""" 加法层的类 """
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


""" 测试代码 """
if __name__ == '__main__':
    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)
    orange_price = mul_orange_layer.forward(orange, orange_num)
    all_price = add_apple_orange_layer.forward(apple_price, orange_price)
    price = mul_tax_layer.forward(all_price, tax)

    # backward
    dprice = 1
    dall_price, dtax = mul_tax_layer.backward(dprice)
    dapple_price, dorange_price = add_apple_orange_layer.backward(dall_price)
    dapple, dapple_num = mul_apple_layer.backward(dapple_price)
    dorange, dorange_num = mul_orange_layer.backward(dorange_price)

    print(price) # 715
    print(dapple_num, dapple, dorange, dorange_num, dtax) # 110 2.2 3.3 165 650

激活函数层的实现

ReLU

Relu 的函数表达式(5.7):y={xx>0,0x≤0y= \begin{cases} x & x>0,\\ 0 & x \le 0 \end{cases}y={x0x>0,x0
它的导数为,式(5.8):∂y∂x={1x>0,0x≤0\frac{\partial y}{\partial x}= \begin{cases} 1 & x>0,\\ 0 & x \le 0 \end{cases}xy={10x>0,x0

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

Sigmoid

数学公式:σ(x)=11+e−xdσdx=σ(x)(1−σ(x))\begin{aligned} \sigma (x) &= \frac{1}{1+e^{-x}} \\ \frac{d\sigma}{dx} &= \sigma(x)(1-\sigma(x)) \end{aligned}σ(x)dxdσ=1+ex1=σ(x)(1σ(x))

式(5.9),f(x)=11+e−xf(x)=\frac{1}{ 1+e^−x}f(x)=1+ex1

导数,f′(x)=f(x)(1−f(x))f'(x)=f(x)(1-f(x))f(x)=f(x)(1f(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

Affine/Softmax 层的实现

Affine

神经网络的正向传播中进行的矩阵的乘积运算在几何学领域被称为“仿射变换”。因此,这里将进行仿射变换的处理实现为“Affine层”。推导公式(5.13), ∂L∂X=∂L∂Y⋅WT∂L∂W=XT⋅∂L∂Y\begin{aligned} \frac{\partial L}{\partial X}&=\frac{\partial L}{\partial Y} \cdot W^T \\ \frac{\partial L}{\partial W}&=X^T \cdot \frac{\partial L}{\partial Y} \end{aligned}XLWL=YLWT=XTYL

在这里插入图片描述

尤其要注意,XXX∂L∂X\frac{\partial L}{\partial X}XL 形状相同,WWW∂L∂W\frac{\partial L}{\partial W}WL 形状相同。
为什么呢?因为矩阵的乘积运算要求对应维度的元素个数保持一致,通过确认一致性,就可以导出式(5.13)。
比如,∂L∂Y\frac{\partial L}{\partial Y}YL 的形状是 (1,3)(1,3)(1,3)WWW 的形状是 (2,3)(2, 3)(2,3) 时,思考 ∂L∂Y\frac{\partial L}{\partial Y}YLWTW^TWT 的乘积,使得 ∂L∂X\frac{\partial L}{\partial X}XL 的形状为 (1,2)(1,2)(1,2)

加上偏置时,需要特别注意。正向传播时,偏置被加到X⋅WX·WXW的各个数据上。比如,N=2N = 2N=2(数据为2个)时,偏置会被分别加到这2个数据(各自的计算结果)上。

import numpy as np

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

Softmax-with-Loss

最后介绍一下输出层的softmax函数。前面我们提到过,softmax函数会将输入值正规化之后再输出。比如手写数字识别时,Softmax层的输出如图5-28所示。

在这里插入图片描述

在图5-28中,Softmax层将输入值正规化(将输出值的和调整为1)之后再输出。另外,因为手写数字识别要进行10类分类,所以向Softmax层的输入也有10个。

推导过程:略

在这里插入图片描述

图5-30的计算图中,softmax函数记为Softmax层,交叉熵误差记为Cross Entropy Error层。这里假设要进行3类分类,从前面的层接收3个输入(得分)。如图5-30所示,Softmax层将输入(a1,a2,a3)(a_1, a_2, a_3)(a1,a2,a3)正规化,输出(y1,y2,y3)(y_1,y_2, y_3)(y1,y2,y3)。Cross Entropy Error层接收Softmax的输出(y1,y2,y3)(y_1,y_2, y_3)(y1,y2,y3)和标签(t1,t2,t3)(t_1,t_2, t_3)(t1,t2,t3),从这些数据中输出损失LLL

图5-30中要注意的是反向传播的结果。Softmax层的反向传播得到了 (y1−t1,y2−t2,y3−t3)(y_1-t_1,y_2-t_2, y_3-t_3)(y1t1,y2t2,y3t3) 这样“漂亮”的结果。由于(y1,y2,y3)(y_1,y_2, y_3)(y1,y2,y3)是Softmax层的输出,(t1,t2,t3)(t_1,t_2, t_3)(t1,t2,t3)是监督数据,所以(y1−t1,y2−t2,y3−t3)(y_1-t_1,y_2-t_2, y_3-t_3)(y1t1,y2t2,y3t3)是Softmax层的输出和标签的差分。神经网络的反向传播会把这个差分表示的误差传递给前面的层,这是神经网络学习中的重要性质。

这样“漂亮”的结果并不是偶然的,而是为了得到这样的结果,特意设计了交叉熵误差函数。
使用“平方和误差”作为“恒等函数”的损失函数,反向传播才能得到(y1−t1,y2−t2,y3−t3)(y_1-t_1,y_2-t_2, y_3-t_3)(y1t1,y2t2,y3t3)这样“漂亮”的结果。

import numpy as np

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

误差反向传播法的实现

神经网络的学习步骤如下所示,

神经网络存在合适的权重和偏置,调整权重和偏置以便拟合训练数据的过程称为“学习”。神经网络的学习分成下面4个步骤:

  1. 步骤1(mini-batch):从训练数据中随机选出一部分数据,这部分数据称为mini-batch。我们的目标是减小mini-batch的损失函数的值。
  2. 步骤2(计算梯度):为了减小mini-batch的损失函数的值,需要求出各个权重参数的梯度。梯度表示损失函数的值减小最多的方向。
  3. 步骤3(更新参数):将权重参数沿梯度方向进行微小更新。
  4. 步骤4(重复):重复步骤1、步骤2、步骤3。

之前介绍的误差反向传播法会在步骤2中出现。

two_layer_net.py

# coding: utf-8
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 = {}
        # 第1层的权重
        self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
        # 第1层的偏置
        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)

        # 生成层
        # 保存神经网络的层的有序字典型变量,以layers['Affine1']、layers['ReLu1']、layers['Affine2']的形式,通过有序字典保存各个层
        # 因此,神经网络的正向传播只需按照添加元素的顺序调用各层的forward()方法就可以完成处理,而反向传播只需要按照相反的顺序调用各层即可。
        # 像这样通过将神经网络的组成元素以层的方式实现,可以轻松地构建神经网络。这个用层进行模块化的实现具有很大优点。
        # 因为想另外构建一个神经网络(比如5层、10层、20层……的大的神经网络)时,只需像组装乐高积木那样添加必要的层就可以了。
        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()

    """ 进行识别(推理)。参数x是图像数据 """
    def predict(self, x):
        for layer in self.layers.values():
            x = layer.forward(x)
        return x

    """
    计算损失函数的值,参数X是图像数据、t是正确解标签
    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'], grads['b1'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
        grads['W2'], grads['b2'] = self.layers['Affine2'].dW, self.layers['Affine2'].db
        return grads

train_neuralnet.py

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 为了导入父目录的文件而进行的设定
import numpy as np
import matplotlib.pyplot as plt
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.numerical_gradient(x_batch, t_batch)
    # 通过误差反向传播法求梯度
    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 | " + str(train_acc) + ", " + str(test_acc))
  

# 绘制图形
markers = {'train': 'o', 'test': 's'}
x = np.arange(len(train_acc_list))
plt.plot(x, train_acc_list, label='train acc')
plt.plot(x, test_acc_list, label='test acc', linestyle='--')
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()

结果

在这里插入图片描述

Logo

脑启社区是一个专注类脑智能领域的开发者社区。欢迎加入社区,共建类脑智能生态。社区为开发者提供了丰富的开源类脑工具软件、类脑算法模型及数据集、类脑知识库、类脑技术培训课程以及类脑应用案例等资源。

更多推荐