误差反向传播法 《深度学习入门 基于Python的理论实现》第五章


layout: post
title: 深度学习入门 基于Python的理论实现
subtitle: 第五章 误差反向传播法
tags: [Machine learning, Reading]


第五章 误差反向传播法

上一章介绍了神经网络的学习,并通过数值微分计算了神经网络的权重参数的梯度,数值微分虽然简单,但是计算起来比较浪费时间。本章使用一个能够高效计算权重参数的梯度的方法--误差反向传播法。
正确理解反向传播法,有两种方法,一种基于计算图,一种基于数学式。后者是比较常见的方法,本章通过计算图来理解。

5.1 计算图

5.1.1 用计算图求解

我们尝试用计算图解决问题,目的是为了让大家熟悉计算图。

问题1:太郎在超市买了 2 个 100 日元一个的苹果,消费税是 10%,请计算支付金额。

计算图通过节点和箭头表示计算过程,节点用圆圈表示,圆圈内是计算的内容。将计算的中间结果写在箭头上方,表示各个节点的计算结果从左向右传递,对于上述问题,求解过程如图所示。


comput_graph_1.png

开始时,苹果的100日元流到 \times2 节点,变成200日元,然后传递到下一个节点,流向 \times1.1 节点,变成220日元。因此答案是220日元。
上图中把 \times2 \times1.1 整体扩起来,不过只用圆圈表示 \times 也是可以的,此时,可以将 2 和 1.1 分别作为变量“苹果的个数”和“消费税”表在外面。

comput_graph_2.png

问题2:太郎在超市买了两个苹果三个橘子,其中,苹果每个100日元。橘子每个150日元,消费税是10%,请计算支付金额。按照上面的思路,求解过程如图所示。

comput_graph_3.png.png

综上,用计算图解题的流程如下:

1.构建计算图

2.在计算图上从左向右计算

第二部的“从左向右计算”是一种正方向上的传播过程,称为正向传播,与之相对应的是反向传播

5.1.2 局部计算

计算图可以通过传递“局部计算”获得最终结果。下面是一个局部计算的例子。

comput_graph_4.png

这个图也很好理解,结论是,无论全局计算有多么复杂,只要按照步骤,都可以最终计算出结果。

5.1.3 为何用计算图解题

优点一:局部计算,无论全局多么复杂的计算,都可以通过局部计算使各个节点致力于简单的计算。
优点二:通过反向传播计算导数。

我们重新思考问题1,假设我们想知道苹果价格的波动会在多大程度上影响最终支付的金额,也就是求“支付金额关于苹果价格的导数”,设苹果价格为 x,支付金额为 L,则相当于求 \frac{\partial L}{\partial x}

现在使用计算图的反向传播求这个导数。

comput_graph_5.png

如上图所示,反向传播使用与正方向相反的箭头(粗线)表示。反向传播传递“局部导数”,将导数值写在下方,这里坠重支付金额关于苹果价格的导数是2.2,也就是说苹果价格每上升1元,最终支付金额会上涨2.2元。

5.2 链式法则

5.2.1 计算图的反向传播

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

comput_graph_6.png

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

5.2.2 链式法则

介绍链式法则之前,先要从复合函数说起,复合函数是多个函数构成的函数,比如 z = (x+y)^2 是由以下两个式子构成的。
z = t^2 \\ t = x + y
链式法则是关于复合函数的导数的性质,定义如下。

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

这就是链式法则的原理,用上面的表达式为例,\frac{\partial z}{\partial x}(z关于x的导数)可以用 \frac{\partial z}{\partial t} (z关于t的导数)和 \frac{\partial t}{\partial x}(t关于x的导数)的乘积表示。数学表示如下:

\frac{\partial z}{\partial x} = \frac{\partial z}{\partial t}\frac{\partial t}{\partial x}

式子中的 {\partial t} 可以相互抵消。

现在我们用链式法则,试着求上面表达式的导数,接下来要求式子中的局部导数(偏导数)

\frac{\partial z}{\partial t} = 2t \\ \frac{\partial t}{\partial x} = 1

于是可以得到:

\frac{\partial z}{\partial x} = \frac{\partial z}{\partial t}\frac{\partial t}{\partial x}=2t \times 1 = 2(x+y)

5.2.3 链式法则和计算图

现在我们尝试将链式法则的计算用计算图表达出来。

comput_graph_7.png

如图所示,计算图的反向传播从右往左传播信号,反向传播的计算顺序是,先将节点的输入信号乘以节点的偏导数,然后再传递给下一个节点。比如,反向传播时,“**2”节点的输入是 \frac{\partial z}{\partial z},将其乘以局部导数 \frac{\partial z}{\partial z} 因为正向传播的输入是t,输出是z,所以这个节点的局部导数是 \frac{\partial z}{\partial t} 然后传递给下一个节点。
在图中最左边是反向传播的结果,他的计算基于上面的规则,因此有 \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关于x的导数。代入得到 \frac{\partial z}{\partial x} 的结果是 2(x+y)

comput_graph_8.png

5.3 反向传播

5.3.1 加法节点的反向传播

首先看一下加法节点的反向传播这,这里以 z = x + y 为对象,观察反向传播。z = x + y 的导数可以由下式计算出来。

\frac{\partial z}{\partial x} = 1 \\ \frac{\partial z}{\partial y} = 1

上面两个式子都等于1,用计算图表示如下所示。

comput_graph_9.png

comput_graph_10.png

下面来看一个具体的例子。假设有“10 + 5 =15”这样的计算,加法节点的反向传播只是将信号输出到下一个节点。

5.3.2 乘法节点的反向传播

用同样的方式看乘法节点。假设 z =xy 于是有

\frac{\partial z}{\partial x} = y \\ \frac{\partial z}{\partial y} = x

下面看一个具体的例子, 10 \times 5 =50

comput_graph_11.png
comput_graph_12.png

5.3.3 苹果的例子

接着来思考一下苹果的例子。这里要解的问题是苹果的价格、苹果的个数、消费税这3个变量各自如何影响最终支付的金额。这个问题相当于求“支付金额关于苹果的价格的导数”“支付金额关于苹果的个数的导数”“支付金额关于消费税的导数”。


comput_graph_13.png

5.4 简单层的实现

本节用python实现前面的苹果例子,乘法节点称为“乘法层”,加法节点称为“加法层”。

5.4.1 乘法层的实现

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

init() 中会初始化实例变量 x 和 y,它们用于保存正向传播时的输入值。 forward() 接收 x 和 y 两个参数,将它们相乘后输出。backward() 将从上游传 来的导数(dout)乘以正向传播的翻转值,然后传给下游。

comput_graph_13.png

参照上图可以作如下实现。


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)



#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)

5.4.2 加法层的实现


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
comput_graph_14.png

使用之前的方法进行实现。

apple = 100
orange = 150
apple_num = 2
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)
dorange,dorange_num = mul_orange_layer.backward(dorange_price)
dapple,dapple_num = mul_apple_layer.backward(dapple_price)

print(price)
print(dapple_num,dapple,dorange,dorange_num,dtax)

5.5 激活函数层的实现

现在把计算图的思想应用到神经网络。

5.5.1 ReLU层

激活函数ReLU数学表达式如下:

f(x) = \begin{cases} x & x \gt 0 \\ 0 & x \leqslant 0 \end{cases}

可以求出y关于x的导数:

\frac{\partial y}{\partial x} = \begin{cases} 1 & x \gt 0 \\ 0 & x \leqslant 0 \end{cases}

comput_graph_15.png

根据上面的表达式可以得知,正向传播时如果输入x大于零,则反向传播会将上游的值原封不动的传递,当正向传播的值x小于0时,会将信号停留在此处。

下面是是实现:

class Relu:
   def __init__(self):
       self.mask = None

   def forward(self, x):
       self.mask = (x <= 0)
       out = x.copy()
       out[self.mask] = 0 #True的位置变成0 也就是小于0的变成0

       return out

   def backward(self,dout):
       dout[self.mask] = 0
       dx = dout

       return dx

x = np.array([[1,-0.5],[-2.0,3.0]])
print(x)
mask = (x<=0)
out = x.copy()
out[mask] = 0
print(out)

5.5.2 Sigmoid 层

接下来是sigmoid函数的实现

y = \frac{1}{1+exp(-x)}

计算图如下:

comput_graph_16.png

下面简单看一下反向传播的流程:

步骤1

“/”节点表示 y=\frac{1}{x} 导数如下:

\frac{\partial y}{\partial x} = -\frac{1}{x^2} \\ =-y^2

因此如下图所示

comput_graph_17.png

步骤2
下面的节点是加法节点,原封不动的传给下游

comput_graph_18.png

步骤3
“exp”节点表示 y = exp(x), 他的导数由下式表示。

\frac{\partial y}{\partial x} = exp(x)

comput_graph_19.png

步骤4

乘法节点翻转相乘。

comput_graph_20.png

根据上面的计算,简化后,得到如图所示的图,和上面的计算结果相同。

comput_graph_21.png

进一步作整理:

$$

\frac{\partial L}{\partial y}y^2exp(-x) = \frac{\partial L}{\partial y}\frac{1}{(1+exp(-x))^2}exp(-x) \
=\frac{\partial L}{\partial y}\frac{1}{1+exp(-x)}\frac{exp(-x)}{1+exp(-x)} \
=\frac{\partial L}{\partial y}y(1-y)
$$

因此,我们可以简化如下:

comput_graph_22.png

以下是实现:

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

5.6 Affine/Softamx层的实现

5.6.1 Affine层

上一章讲解了矩阵的乘法,使用Y= np.dot(X, W) + B计算,这里把这个运算表达出来。

comput_graph_23.png
comput_graph_24.png

现在考虑上图的反向传播。对于加法节点来说,原封不动的传递 \frac{\partial L}{\partial Y},对于dot节点,交换乘转置,得到 \frac{\partial L}{\partial Y}W^T。同理得到:

\frac{\partial L}{\partial X} = \frac{\partial L}{\partial Y}W^T \\ \frac{\partial L}{\partial W} = X^T\frac{\partial L}{\partial Y}

转置的概念不再赘述。

comput_graph_25.png

简化上面的表达式,可以得到:

comput_graph_26.png

5.6.2 批版本的Affine层

前面介绍的X是以单个数据为对象的,现在考虑N个数据一起正向传播的情况。

comput_graph_28.png

实现:


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

5.6.3 Softmax-with-Loss 层

最后介绍输出层的softmax函数。手写数字识别的时候,整个神经网络的结构如图所示。

comput_graph_29.png

下面实现Softmax层,考虑到这里也包含交叉熵误差,所以也称为Softmax-with-Loss层。以下是计算图和简化版:

comput_graph_30.png
comput_graph_31.png

实现:



def cross_entropy_error(y,t):
    delta = 1e-7
    return -np.sum(t*np.log(y + delta))

def softmax(x):
    exp_x = np.exp(x-np.max(x))
    sum_exp_x = np.sum(exp_x)

    return exp_x/sum_exp_x

class SoftmaxWithLoss:

    def __init__(self):
        self.loss = None
        self.y = None
        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]
        dx = (self.y - self.t) / batch_size

        return dx

5.7 误差反向传播法的实现

5.7.1 神经网络学习的全貌

前提

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

步骤1(mini-batch)

从训练数据中随机选择一部分数据。

步骤2(计算梯度)

计算损失函数关于各个权重参数的梯度。

步骤3(更新参数)

将权重参数沿梯度方向进行微小的更新。

步骤4(重复)

重复步骤 1、步骤 2、步骤 3。

5.7.2 对应误差反向传播法的神经网络的实现

下面是TwoLayerNet的代码实现:

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['W'],self.params['b2'])

        self.lastlayer = SoftmaxWithLoss()

        def predict(self, x):
            for layer in self.layers.values():
                x = layer.forward(x)

            return x

        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

        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):
            self.loss(x, t)

            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)
x_batch = x_train[:3]

t_batch = t_train[:3]
grad_backprop = network.gradient(x_batch, t_batch)
# 求各个权重的绝对误差的平均值
grad_numerical = network.numerical_gradient(x_batch, t_batch)
for key in grad_numerical.keys():
    diff = np.average( np.abs(grad_backprop[key] - grad_numerical[key]) )
    print(key + ":" + str(diff))

计算后的结果如下:

W1:3.5949338619330523e-10
b1:2.239133814485334e-09
W2:5.190797970067163e-09
b2:1.4008290402101054e-07

结果表明,使用数值微分和误差反向传播法求出的梯度的差非常小。也就是说,反向传播法是正确的。

5.7.4 使用误差反向传播法的学习

实现:


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)

最大的变化是计算速度显著提高。

5.8 小结

本章我们介绍了将计算过程可视化的计算图,并使用计算图,介绍了神经网络中的误差反向传播法,并以层为单位实现了神经网络中的处理。

  • 通过使用计算图,可以直观地把握计算过程。
  • 计算图的节点是由局部计算构成的。局部计算构成全局计算。
  • 计算图的正向传播进行一般的计算。通过计算图的反向传播,可以计算各个节点的导数。
  • 通过将神经网络的组成元素实现为层,可以高效地计算梯度(反向传播法)。
  • 通过比较数值微分和误差反向传播法的结果,可以确认误差反向传播法的实现是否正确(梯度确认)。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容