《深度学习入门:基于Python的理论与实现》 误差反向传播法
[日]斋藤康毅
误差反向传播法
有两种方法:一种是基于数学式;另一种是基于计算图(computational graph)。
计算图
计算图将计算过程用图形表示出来。这里说的图形是数据结构图,通过多个节点和边表示(连接节点的直线称为“边”)
计算图通过节点和箭头表示计算过程。节点用圆圈表示,节点中是计算方法,边线上是变量。将计算的中间结果写在箭头的上方,表示各个节点的计算结果从左向右传递。
计算图举例:太郎在超市买了2个100日元一个的苹果,消费税是10%,请计算支付金额。
上图从左到右,第一步先100*2
计算出总价为200,第二步 200*1.1
额外加上消费税。这种 “从左向右进行计算”是一种正方向上的传播,简称为正向传播(forward propagation)。正向传播是从计算图出发点到结束点的传播。反向传播(backward propagation)就是从右向左的传播。
计算图的特征是可以通过传递“局部计算”获得最终结果。“局部”这个词的意思是“与自己相关的某个小范围”。局部计算是指,无论全局发生了什么,都能只根据与自己相关的信息输出接下来的结果,例如第一步100*2
计算时不用考虑消费税的计算。
计算图优点
- 局部计算一般都很简单,无论全局的计算有多么复杂,各个步骤只需要完成局部计算,通过传递它的计算结果,可以获得全局的复杂计算的结果,从而简化问题。
- 利用计算图可以将中间的计算结果全部保存起来(比如,计算进行到2个苹果时的金额是200日元、加上消费税之前的金额650日元等)。
- 使用计算图最大的原因是,可以通过反向传播高效计算导数
假设我们想知道苹果价格的上涨会在多大程度上影响最终的支付金额?
即求“支付金额关于苹果的价格的导数”。设苹果的价格为x,支付金额为L,则相当于求$\frac{\partial L}{\partial x}$,这个导数的值表示当苹果的价格稍微上涨时,支付金额会增加多少。计算中途求得的导数的结果(中间传递的导数)可以被共享,从而可以高效地计算多个导数。综上,计算图的优点是,可以通过正向传播和反向传播高效地计算各个变量的导数值。
链式法则(chain rule)
复合函数是由多个函数构成的函数。比如,$z=(x+y)^2$是由$z=t^2$和$t = x + y$构成的。
链式法则是关于复合函数的导数的性质: 如果某个函数由复合函数表示,则该复合函数的导数可以用构成复合函数的各个函数的导数的乘积表示。
$$
\frac{\partial z}{\partial x} = \frac{\partial z}{\partial t}\frac{\partial t}{\partial x}=2t \times 1 = 2(x+y)
$$
$z=(x+y)^2$的计算过程使用计算图表示,正向先进行了x+y后,再对第一步的结果t进行平方得到最终结果z
反向传播的计算顺序是,先将节点的输入信号乘以节点的局部导数(偏导数),然后再传递给下一个节点。上图以对x求偏导数:
- 从右向左第一个节点而言,就是节点输入$\frac{\partial z}{\partial z}$乘以$z=t^2$的导数,即$\frac{\partial z}{\partial z}\frac{\partial z}{\partial t}$ 也就是$1\times 2t=2(x+y)$;
- 下一个节点的输入$\frac{\partial z}{\partial z}\frac{\partial z}{\partial t}$ 乘以$t=x+y$对x的导数,即$\frac{\partial z}{\partial z}\frac{\partial z}{\partial t}\frac{\partial t}{\partial x}$ 也就是$2(x+y)\times 1= 2(x+y)$
根据链式法则,最左边的反向传播的结果$\frac{\partial z}{\partial z}\frac{\partial z}{\partial t}\frac{\partial t}{\partial x}=\frac{\partial z}{\partial z}\frac{\partial z}{\partial t} = \frac{\partial z}{\partial x}$ ,对应“z关于x的导数”。
计算图反向传播是基于链式法则的
反向传播
加法的反向传播
加法反向传播将从上游传过来的输入导数乘以1(因为加法局部计算的导数为1,如上面例子最左侧节点x+y),然后传向下游,所以输入的值会原封不动地流向下一个节点。
乘法的反向传播
乘法的反向传播会将上游的值乘以正向传播时的输入信号的“翻转值”后传递给下游。因为对于乘法计算$z=xy$,如果对x求导数$\frac{\partial z}{\partial x}=y$,所以上游输入的值乘以导数y就是对x的输出。
翻转值表示一种翻转关系:正向传播时信号是x的话,反向传播时则是y;正向传播时信号是y的话,反向传播时则是x。实现乘法节点的反向传播时,要保存正向传播的输入信号。
以之前买苹果的例子,计算图是两个乘法运算,支付金额对苹果的价格的导数是2.2,苹果的个数的导数是110,消费税的导数是200。这可以解释为,如果消费税和苹果的价格增加相同的值,则消费税将对最终价格产生200倍大小的影响,苹果的价格将产生2.2倍大小的影响。不过,因为这个例子中消费税和苹果的价格的量纲不同,所以才形成了这样的结果(消费税的1是100%,苹果的价格的1是1日元)。
各个层的实现
我们将把构建神经网络的“层”实现为一个类。这里所说的“层”是神经网络中功能的单位。比如,负责sigmoid函数的Sigmoid、负责矩阵乘积的Affine等,都以层为单位进行实现。
层的实现中有两个共通的方法forward()对应正向传播,backward()对应反向传播。
简单层的实现
计算图的乘法节点称为“乘法层”(MulLayer),加法节点称为“加法层”(AddLayer)。
首先,生成必要的层,以合适的顺序调用正向传播的forward()方法。然后,用与正向传播相反的顺序调用反向传播的backward()方法,就可以求出想要的导数。计算图中层的实现非常简单,使用这些层可以进行复杂的导数计算
1 | class MulLayer: |
forward()
接收x和y两个参数,将它们相乘后输出。backward()
将从上游传来的导数dout
乘以正向传播的翻转值,然后传给下游。
要注意backward()
的参数中需要输入“关于正向传播时的输出变量的导数”。
激活函数层的实现
激活函数ReLU(Rectified Linear Unit)
ReLU函数及其导数为
$$
y = \begin{cases}
x, & (x \gt 0) \
0, & (x \leq 0)
\end{cases},
\frac{\partial y}{\partial x} = \begin{cases}
1, & (x \gt 0) \
0, & (x \leq 0)
\end{cases}
$$
如果正向传播时的输入x大于0,则反向传播会将上游的值原封不动地传给下游$\frac{\partial L}{\partial y}\times 1 = \frac{\partial L}{\partial y}$。反过来,如果正向传播时的x小于等于0,则反向传播中传给下游的信号将停在此处($\frac{\partial L}{\partial y}\times 0 = 0$ )。
1 | class Relu: |
Relu类有实例变量mask。这个变量mask是由True/False构成的NumPy数组,它会把正向传播时的输入x的元素中小于等于0的地方保存为True,其他地方(大于0的元素)保存为False。如果正向传播时的输入值小于等于0,则反向传播的值为0。因此,反向传播中会使用正向传播时保存的mask,将从上游传来的dout的mask中的元素为True的地方设为0。
sigmoid函数
$$
y(x) = \frac{1}{1+e^{-x}}
$$
计算图正向和反向流程如下
其中最右的节点$y = \frac{1}{x}$的导数为$\frac{\partial y}{\partial x}=-x^{(-1-1)}=-\frac{1}{x^2}=-y^2$
$y = e^x$的导数为$\frac{\partial y}{\partial x} = e^x$,正向的函数为$y = e^{-x}$所以它对x的导数为$e^{-x}$,这个节点反向计算使用上游的输入$-\frac{\partial L}{\partial y}y^2$乘以计算函数的导数$e^{-x}$为$-\frac{\partial L}{\partial y}y^2e^{-x}$
最后一个节点是乘法节点,把上游输入乘以反转的另一个输入,这里是-1,所以最终结果是$\frac{\partial L}{\partial y}y^2e^{-x}$。这个输出还可以进行公式简化得到
$$
\frac{\partial L}{\partial y}y^2e^{-x} = \frac{\partial L}{\partial y}\frac{1}{(1+e^{-x})^2}e^{-x} \
= \frac{\partial L}{\partial y}\frac{1}{(1+e^{-x})}\frac {e^{-x}}{(1+e^{-x})} \
= \frac{\partial L}{\partial y} y(1-y)
$$
从上式可以看出,Sigmoid层的反向传播,只根据正向传播的输出就能计算出来。
1 | class Sigmoid: |
Affine层的实现
神经网络的正向传播中进行的矩阵的乘积运算在几何学领域被称为“仿射变换”。因此,这里将进行仿射变换的处理实现为“Affine层”。
神经元的加权和可以用Y = np.dot(X, W) + B
计算出来。然后,Y
经过激活函数转换后,传递给下一层,这就是神经网络正向传播的流程。
矩阵的乘积与偏置的和的运算用计算图表示
矩阵的乘积(“dot”节点)的反向传播可以通过组建使矩阵对应维度的元素个数一致的乘积运算而推导出来。例如输入矩阵$X=(x_0,x_1,…x_n)$ ,损失函数L对X的偏导数$\frac{\partial L}{\partial X}=(\frac{\partial L}{\partial x_0}, \frac{\partial L}{\partial x_1}, .., \frac{\partial L}{\partial x_n})$ ,可以看出$X$和$\frac{\partial L}{\partial X}$形状相同
因为矩阵的乘积运算要求对应维度的元素个数保持一致,比如,$\frac{\partial L}{\partial Y}$的形状是(3,),$W$的形状是(2, 3)时,可以让$\frac{\partial L}{\partial Y}$和$W^T$乘积,使得$\frac{\partial L}{\partial X}$的形状为(2,),从而推出上图中的公式1。
正向传播时,偏置会被加到每一个数据(第1个、第2个……)上。因此反向传播时,各个数据的反向传播的值需要汇总为偏置的元素。
1 | class Affine: |
Softmax层的实现
神经网络中未被正规化的输出结果(Softmax层前面的Affine层的输出)有时被称为“得分”。神经网络的推理只需要给出一个答案的情况下,因为此时只对得分最大值感兴趣,所以不需要Softmax层。但是在神经网络的学习阶段则需要Softmax层。
Softmax层的反向传播得到了$(y_1-t_1, y_2-t_2,…,y_n-t_n)$,即Softmax层的输出和监督标签的差分。神经网络的反向传播会把这个差分表示的误差传递给前面的层,这是神经网络学习中的重要性质。
神经网络学习的目的就是通过调整权重参数,使神经网络的输出(Softmax的输出)接近监督标签。因此,必须将神经网络的输出与监督标签的误差高效地传递给前面的层。$(y_1-t_1, y_2-t_2,…,y_n-t_n)$正是Softmax层的输出与监督标签的差,直截了当地表示了当前神经网络的输出与监督标签的误差
使用交叉熵误差作为softmax函数的损失函数后,反向传播得到$(y_1-t_1, y_2-t_2,…,y_n-t_n)$这样“漂亮”的结果。实际上,这样“漂亮”的结果并不是偶然的,而是为了得到这样的结果,特意设计了交叉熵误差函数。回归问题中输出层使用“恒等函数”,损失函数使用“平方和误差”,也是出于同样的理由。也就是说,使用“平方和误差”作为“恒等函数”的损失函数,反向传播才能得到$(y_1-t_1, y_2-t_2,…,y_n-t_n)$这样“漂亮”的结果。
1 | # 新的交叉熵损失函数 |
误差反向传播法的实现
OrderedDict是有序字典,“有序”是指它可以记住向字典里添加元素的顺序。因此,神经网络的正向传播只需按照添加元素的顺序调用各层的forward()方法就可以完成处理,而反向传播只需要按照相反的顺序调用各层即可。因为Affine层和ReLU层的内部会正确处理正向传播和反向传播,所以这里要做的事情仅仅是以正确的顺序连接各层,再按顺序(或者逆序)调用各层。
新的两层网络实现
1 | from collections import OrderedDict |
这里还保留了数值微分求梯度的方法numerical_gradient()
,用来确认数值微分求出的梯度结果和误差反向传播法求出的结果是否一致(严格地讲,是非常相近),这个操作称为梯度确认(gradient check)。确认实现的误差反向传播算法是否正确。
1 | def gradient_check(): |
使用新的网络训练MNIST数据,和上一章的程序只是使用的网络类名称不同,其他完全一样
1 | def network_train(): |
小结
通过使用计算图,可以直观地把握计算过程
计算图的节点是由局部计算构成的。局部计算构成全局计算
计算图的正向传播进行一般的计算。通过计算图的反向传播,可以计算各个节点的导数。