深度学习入门--误差反向传播法
上一节我们学习的是通过数值微分来计算损失函数关于权重参数的梯度,这种方法也称为:随机梯度下降法(SGD)。
随机梯度下降法的优点是比较简单,容易理解,缺点是计算比较耗费时间。还有一种计算损失函数关于权重参数的梯度的方法:误差反向传播法
误差反向传播法的优点是计算速度快,能够高效地计算权重参数的梯度;缺点是不太容易理解。
关于误差反向传播法的理解,大概有两种方式:
- 基于数学式
- 基于计算图
基于数学式的方式过于复杂,一大堆数学式看着头晕。而计算图则十分便于理解。
计算图
计算图是利用数据结构图,通过多个节点和边将计算过程表示出来。
一个简单的计算图的示例:上面这个计算图表示的是:购买2个10元的苹果,消费税是10%,计算其总金额。
用计算图求解问题的一般步骤如下:
- 构建计算图
- 在计算图上,从左至右进行计算
这种计算方法称为正向传播:即是从计算图出发点到结束点的传播。
既然有正向传播,那就有反向传播。计算图的反向传播就是理解误差反向传播法的关键。
计算图的特征是:可以通过传递“局部计算”来获得最终结果。
计算图解题的优点:
- 局部计算简单
- 中间结果保存
- 可以通过反向传播高效计算导数
链式法则
计算图的反向传播可以高效计算局部导数的原理是基于链式法则的。链式法则的具体内容是大学本科【高等数学】里面的知识,这里不再赘述。
链式法则和计算图的反向传播有着密切的关系,因为,反向传播的计算顺序是:先将节点的输入信号乘以节点的偏导数,然后再传递给下一个节点。
计算图的反向传播的一个简单的示例:反向传播
反向传播有两种类型:
- 加法节点的反向传播
- 乘法节点的反向传播
加法节点的反向传播:
加法节点的反向传播是将上游的值原封不动的输出到下游。乘法节点的反向传播
乘法的反向传播会将上游的值乘以正向传播时的输入信号的“互换值”。
互换值表示一种互换关系,比如正向传播时,输入信号为x和y。则输入信号x的互换值为y;输入信号y的互换值为x。如上所示,乘法节点的反向传播需要正向传播时的输入信号值,因此,实现乘法节点的反向传播时,要保存正向的输入信号。
加法层的Python实现
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
乘法层的Python实现
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
激活函数ReLU的计算图的Python实现
在神经网络的层的实现中,一般假定forward()
和backward()
的参数是numpy数组
class ReLU:
def __init__(self):
self.mask = None
def forward(self, x):
self.mask = (x<=0) # 当x小于0时,mask为true
out = x.copy()
out[self.mask] = 0 # 将mask中为true的地方设为0
return out
def backward(self, dout):
dout[self.mask] = 0
dx = dout
return dx
激活函数Sigmoid的计算图的Python实现
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
Affine层的计算图的Python实现
在神经网络的正向传播中,为了计算加权信号的总和,我们使用了矩阵的乘积运算:np.dot()
。这样一来,神经元的加权和可以用Y = np.dot(X, W) + B
计算出来。这种计算所在的层称为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 out
def backward(self, dout):
dx = np.dot(dout, self.w.T) # self.w.T表示矩阵W的转置
self.dw = np.dot(self.x.T, dout)
self.db = np.dot(dout, axis=0) # 列方向
return dx
Softmax-with-Loss层的计算图的Python实现
损失函数有两种:
- 交叉熵误差
- 平方和误差
输出层的激活函数也有两种:
- softmax函数
- 恒等函数
一般情况下,使用交叉熵误差作为softmax函数的损失函数;使用平方和误差作为恒等函数的损失函数。
Softmax-with-Loss层的实现:
class SoftmaxWithLoss:
def __init__(self):
self.loss = None # 损失
self.y = None # softmax的输出
self.t = None # 监督数据(one-hot表示)
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
神经网络学习的全貌
神经网络中有合适的权重和偏置,调整权重和前置以便拟合训练数据的过程称为学习,神经网络的学习主要是以下四大步骤:
- 步骤1(mini-batch):从训练数据中随机选择一部分数据
- 步骤2(计算梯度): 使用误差反向传播大计算损失函数关于各个权重参数的梯度
- 步骤3(更新参数):将权重参数沿梯度方向进行微小的更新
- 步骤4(重复): 重复步骤1、2、3。
使用误差反向传播法的学习
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("训练精度:"+ str(train_acc)+ " | " + "测试精度: " +str(test_acc))
训练结果如下:
训练精度:0.16325 | 测试精度: 0.1616
训练精度:0.9031833333333333 | 测试精度: 0.9074
训练精度:0.9242833333333333 | 测试精度: 0.9269
训练精度:0.9374833333333333 | 测试精度: 0.9369
训练精度:0.9443166666666667 | 测试精度: 0.9408
训练精度:0.9516166666666667 | 测试精度: 0.9474
训练精度:0.95615 | 测试精度: 0.9524
训练精度:0.9610666666666666 | 测试精度: 0.9581
训练精度:0.9646333333333333 | 测试精度: 0.9589
训练精度:0.9675333333333334 | 测试精度: 0.9623
训练精度:0.9706333333333333 | 测试精度: 0.9644
训练精度:0.9712666666666666 | 测试精度: 0.9638
训练精度:0.9729666666666666 | 测试精度: 0.9664
训练精度:0.9754 | 测试精度: 0.9679
训练精度:0.9768666666666667 | 测试精度: 0.9682
训练精度:0.9772666666666666 | 测试精度: 0.9697
训练精度:0.9784333333333334 | 测试精度: 0.9688
可以看出,训练数据的识别精度和测试数据的识别精度在不断提高,这说明神经网络在不断地学习。
其次,训练数据的识别精度和测试数据的识别精度基本吻合,说明这次的学习没有发生过拟合的现象。
每天学习一点点,每天进步一点点。