文章目录
- 前言
- 链式法则
- 什么是链式法则
- 链式法则和计算图
- 反向传播
- 加法节点的反向传播
- 乘法节点的反向传播
- 苹果的例子
- 简单层的实现
- 乘法层的实现
- 加法层的实现
- 激活函数层的实现
- 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的理论与实践. 人民邮电出版社.