误差逆传播算法(BP算法)
本文内容主要参考《机器学习》(清华大学出版社,西瓜书)
1. 算法思想
给定训练集 D = { ( x 1 , y 1 ) , ( x 2 , y 2 ) , … , ( x m , y m ) } , x i ∈ R d , y i ∈ R l D=\{(\pmb{x}_1, \pmb{y}_1), (\pmb{x}_2, \pmb{y}_2), \dots, (\pmb{x}_m, \pmb{y}_m)\},\pmb{x}_i \in R^{d}, \pmb{y}_i \in R^l D={ (xxx1,yyy1),(xxx2,yyy2),…,(xxxm,yyym)},xxxi∈Rd,yyyi∈Rl,即训练集中一共有 m m m个训练数据,每个数据的输入由 d d d个属性描述,输出为 l l l维实值向量。
在下文的神经网络中采用的神经元模型都是上图所示的“M-P神经元模型”。
上图给出了一个拥有** d d d个输入神经元, q q q个隐层神经元, l l l个输出神经元**的多层前馈神经网络结构。
其中,输出层第 j j j个神经元的阈值用 θ j \theta_j θj表示,隐层第 h h h个神经元的阈值用 γ h \gamma_h γh表示。输入层第 i i i个神经元与隐层第 h h h个神经元之间的连接权为 v i h v_{ih} vih,隐层第 h h h个神经元与输出层第 j j j个神经元之间的连接权为 w h j w_{hj} whj。
记隐层第 h h h个神经元接收到的输入是 α h = ∑ i = 1 d v i h x i \alpha_h=\sum_{i=1}^{d}v_{ih}x_i αh=∑i=1dvihxi,输出层第 j j j个神经元接收到的输入是 β j = ∑ h = 1 q w h j b h \beta_j=\sum_{h=1}^{q}w_{hj}b_h βj=∑h=1qwhjbh,其中 b h b_h bh是隐层第 h h h个神经元的输出。假设隐层和输出层神经元都是用 S i g m o i d Sigmoid Sigmoid函数( s i g m o i d ( x ) = 1 1 + e − x sigmoid(x)=\frac{1}{1+e^{-x}} sigmoid(x)=1+e−x1)。
对训练例 ( x k , y k ) (\pmb{x}_k, \pmb{y}_k) (xxxk,yyyk),假定神经网络的输出为 y ^ k = ( y ^ 1 k , y ^ 2 k , … , y ^ l k ) \hat{\pmb{y}}_k=(\hat{y}_1^k,\hat{y}_2^k,\dots,\hat{y}_l^k) yyy^k=(y^1k,y^2k,…,y^lk),即
式 1 : y ^ j k = s i g m o i d ( β j − θ j ) , 式1:\ \hat{y}_j^k=sigmoid(\beta_j-\theta_j), 式1: y^jk=sigmoid(βj−θj),
则网络在 ( x k , y k ) (\pmb{x}_k, \pmb{y}_k) (xxxk,yyyk)上的均方误差为
式 2 : E k = 1 2 ∑ j = 1 l ( y ^ j k − y j k ) 2 . 式2:\ E_k=\frac{1}{2}\sum_{j=1}^l(\hat{y}_j^k-y_j^k)^2. 式2: Ek=21j=1∑l(y^jk−yjk)2.
这里是整理的符号表:
符号名 | 符号含义 |
---|---|
d d d | 输入神经元个数 |
q q q | 隐层神经元个数 |
l l l | 输出神经元个数 |
γ h \gamma_h γh | 隐层第 h h h个神经元的阈值 |
θ j \theta_j θj | 输出层第 j j j个神经元的阈值 |
v i h v_{ih} vih | 输入层第 i i i个神经元与隐层第 h h h个神经元的之间连接权 |
w h j w_{hj} whj | 隐层第 h h h个神经元与输出层第 j j j个神经元之间的连接权 |
α h \alpha_h αh | 隐层第 h h h个神经元接收到的输入 |
b h b_h bh | 隐层第 h h h个神经元的输出 |
β j \beta_j βj | 输出层第 j j j个神经元接收到的输入 |
y ^ j k \hat{y}_j^k y^jk | 输出层第 j j j个神经元的输出 |
网络中有 ( d + 1 ) ∗ q + ( q + 1 ) ∗ l (d+1)*q+(q+1)*l (d+1)∗q+(q+1)∗l个参数需要确定:输入层到隐层的 d ∗ q d*q d∗q个权值, q q q个隐层神经元的阈值,隐层到输出层的 q ∗ l q*l q∗l个权值, l l l个输出神经元的阈值。BP(BackPropagation)是一个迭代学习算法,在迭代的每一轮中采用广义的感知机学习规则对参数进行更新估计,任意参数 v v v的更新估计式为
式 3 : v ← v + Δ v . 式3:\ v \leftarrow v+\Delta v. 式3: v←v+Δv.
下面我们以神经网络图中的隐层到输出层的连接权 w h j w_{hj} whj为例来进行推导。
BP算法基于梯度下降策略,以目标的负梯度方向对参数进行调整。对误差 E k E_k Ek,给定学习率 η \eta η,有
式 4 : Δ w h j = − η ∂ E k ∂ w h j . 式4:\ \Delta w_{hj}=-\eta \frac{\partial E_k}{\partial w_{hj}}. 式4: Δwhj=−η∂whj∂Ek.
注意到 w h j w_{hj} whj先影响到输出层第 j j j个神经元的输入值 β j \beta_j βj,再影响到其输出值 y ^ j k \hat{y}_j^k y^jk,最后影响到 E k E_k Ek,有
式 5 : ∂ E k ∂ w h j = ∂ E k ∂ y ^ j k ∗ ∂ y ^ j k ∂ β j ∗ ∂ β j ∂ w h j . 式5:\ \frac{\partial E_k}{\partial w_{hj}}=\frac{\partial E_k}{\partial \hat{y}_j^k}*\frac{\partial \hat{y}_j^k}{\partial \beta_j}*\frac{\partial \beta_j}{\partial w_{hj}}. 式5: ∂whj∂Ek=∂y^jk∂Ek∗∂βj∂y^jk∗∂whj∂βj.
根据 β j \beta_j βj的定义,显然有
式 6 : ∂ β j ∂ w h j = b h . 式6:\ \frac{\partial \beta_j}{\partial w_{hj}}=b_h. 式6: ∂whj∂βj=bh.
S i g m o i d Sigmoid Sigmoid函数有一个很好的性质:
式 7 : s i g m o i d ′ ( x ) = s i g m o i d ( x ) ∗ ( 1 − s i g m o i d ( x ) ) . 式7:\ sigmoid'(x)=sigmoid(x)*(1-sigmoid(x)). 式7: sigmoid′(x)=sigmoid(x)∗(1−sigmoid(x)).
于是根据式2和式1,
式 8 : g j = − ∂ E k ∂ y ^ j k ∗ ∂ y ^ j k ∂ β j = − ( y ^ j k − y j k ) s i g m o i d ′ ( β j − θ j ) = y ^ j k ( 1 − y ^ j k ) ( y j k − y ^ j k ) 式8:\ g_j=-\frac{\partial E_k}{\partial \hat{y}_j^k}*\frac{\partial \hat{y}_j^k}{\partial \beta_j}\\ =-(\hat{y}_j^k-y_j^k)sigmoid'(\beta_j-\theta_j)\\ =\hat{y}_j^k(1-\hat{y}_j^k)(y_j^k-\hat{y}_j^k) 式8: gj=−∂y^jk∂Ek∗∂βj∂y^jk=−(y^jk−yjk)sigmoid′(βj−θj)=y^jk(1−y^jk)(yjk−y^jk)
将式8和式6代入式5,再代入式4,就得到了BP算法中关于 w h j w_{hj} whj的更新公式
式 9 : Δ w h j = η ∗ g j ∗ b h . 式9:\ \Delta w_{hj}=\eta * g_j * b_h. 式9: Δwhj=η∗gj∗bh.
类似可得
式 10 : Δ θ j = − η ∂ E k ∂ θ j = − η ∂ E k ∂ y ^ j k ∗ ∂ y ^ j k ∂ θ j = − η ( y ^ j k − y j k ) s i g m o i d ′ ( β j − θ j ) = − η ( y ^ j k − y j k ) y ^ j k ( 1 − y ^ j k ) ∗ − 1 = − η g j , 式10:\ \Delta \theta_j=-\eta\frac{\partial E_k}{\partial \theta_j}=-\eta\frac{\partial E_k}{\partial \hat{y}_j^k}*\frac{\partial \hat{y}_j^k}{\partial \theta_j}\\ =-\eta(\hat{y}_j^k-y_j^k)sigmoid'(\beta_j-\theta_j)\\ =-\eta(\hat{y}_j^k-y_j^k)\hat{y}_j^k(1-\hat{y}_j^k)*-1=-\eta g_j, 式10: Δθj=−η∂θj∂Ek=−η∂y^jk∂Ek∗∂θj∂y^jk=−η(y^jk−yjk)sigmoid′(βj−θj)=−η(y^jk−yjk)y^jk(1−y^jk)∗−1=−ηgj,
式 11 : Δ v i h = η e h x i , 式11:\ \Delta v_{ih}=\eta e_h x_i, 式11: Δvih=ηehxi,
式 12 : Δ γ h = − η e h , 式12:\ \Delta\gamma_h=-\eta e_h, 式12: Δγh=−ηeh,
在式11和式12中,
式 13 : e h = − ∂ E k ∂ b h ∗ ∂ b h ∂ α h = − ∑ j = 1 l ∂ E k ∂ β j ∗ ∂ β j ∂ α h s i g m o i d ′ ( α h − γ h ) = ∑ j = 1 l g j w h j s i g m o i d ′ ( α h − γ h ) = b h ( 1 − b h ) ∑ j = 1 l g j w h j . 式13:\ e_h=-\frac{\partial E_k}{\partial b_h}*\frac{\partial b_h}{\partial \alpha_h}\\ =-\sum_{j=1}^{l}\frac{\partial E_k}{\partial \beta_j}*\frac{\partial \beta_j}{\partial \alpha_h}sigmoid'(\alpha_h-\gamma_h)\\ =\sum_{j=1}^{l}g_jw_{hj}sigmoid'(\alpha_h-\gamma_h)\\ =b_h(1-b_h)\sum_{j=1}^{l}g_jw_{hj}. 式13: eh=−∂bh∂Ek∗∂αh∂bh=−j=1∑l∂βj∂Ek∗∂αh∂βjsigmoid′(αh−γh)=j=1∑lgjwhjsigmoid′(αh−γh)=bh(1−bh)j=1∑lgjwhj.
学习率 η \eta η控制着算法每一轮迭代中的更新步长,若太大则容易振荡,太小则收敛速度又会过慢。有时为了精细调节,式9和式10使用 η 1 \eta_1 η1,式11和式12使用 η 2 \eta_2 η2,两者未必相等。
2. 伪代码
对每个训练样例,BP算法执行以下操作:
先将输入示例提供给输入层神经元,然后逐层将信号前传,直到产生输出层的结果;然后计算输出层的误差(第4-5行),再将误差逆向传播至隐层神经元(第6行),最后根据隐层神经元的误差来对连接权和阈值进行调整(第7行)。该迭代过程循环进行,直到达到某些停止条件为止。
输入: 训练集D; 学习率lr
过程:
1: 在(0, 1)范围内随机初始化网络中所有连接权和阈值
2: repeat
3: for all (x_k, y_k) ∈ D do
4: 根据当前参数和式1计算当前样本的输出y_hat_k;
5: 根据式8计算输出层神经元的梯度项g_j;
6: 根据式13计算隐层神经元的梯度向e_h;
7: 根据式9-12更新连接权w_hj, v_ih与阈值theta_j与gamma_h
8: end for
9: until 达到停止条件
输出: 连接权与阈值确定的多层前馈神经网络
3. 累积误差
BP算法的目标是要最小化训练集 D D D上的累计误差
E = 1 m ∑ k = 1 m E k , E=\frac{1}{m}\sum_{k=1}^{m}E_k, E=m1k=1∑mEk,
之前介绍的都是每次仅针对一个训练样例更新连接权和阈值的标准BP算法,也就是说上述算法的更新规则是基于单个的 E k E_k Ek推导而得的。如果类似地推导出基于累积误差最小化的更新规则,就得到了累积误差逆传播算法。
一般来说,标准BP算法每次更新只针对单个样例,参数更新得非常频繁,而且对不同样例进行更新的效果可能出现“抵消”效果。因此为了达到同样的累积误差极小点,标准BP算法往往需要更多次数的迭代,累积BP算法直接针对累积误差最小化,它在读取整个训练集 D D D一遍(读取训练集一遍称为进行了一轮(one epoch)学习)后才对参数进行更新,其参数更新的频率低得多。但在很多任务中,累积误差下降到一定程度之后,进一步下降会非常缓慢,这是标准BP往往会更快得到较好的解,尤其是在训练集 D D D非常大时更加明显。
4. 实例
4.1 Numpy实现两层神经网络
一个全连接ReLU神经网络,一个隐藏层,没有bias(阈值为0, γ h = 0 \gamma_h=0 γh=0, θ j = 0 \theta_j=0 θj=0)。用来从 x x x预测 y y y,使用L2-Loss。
α = W 1 x b = R e L U ( α ) y ^ = W 2 b \alpha=W_1x\\ b=ReLU(\alpha)\\ \hat{y}=W_2b α=W1xb=ReLU(α)y^=W2b
这一实现完全使用numpy来计算前向神经网络,loss和反向传播。
import numpy as np
m, d, p, l = 64, 1000, 100, 10
# 随机创建一些训练数据
x = np.random.randn(m, d)
y = np.random.randn(m, l)
# 随机初始化连接权
w1 = np.random.randn(d, p)
w2 = np.random.randn(p, l)
learning_rate = 1e-6
for it in range(500):
# forward pass
alpha = x.dot(w1) # m * p
b = np.maximum(alpha, 0) # m * p
y_hat = b.dot(w2) # m * l
# compute loss
loss = np.square(y_pred - y).sum()
print(it, loss)
# backward pass
# compute the gradient
grad_y_hat = 2.0 * (y_hat - y)
grad_w2 = b.T.dot(grad_y_hat)
grad_b = grad_y_hat.dot(w2.T)
grad_alpha = grad_b.copy()
grad_alpha[alpha < 0] = 0
grad_w1 = x.T.dot(grad_alpha)
# update weights of w1 and w2
w1 -= learning_rate * grad_w1
w2 -= learning_rate * grad_w2
4.2 PyTorch实现两层神经网络
4.2.1 手动grad
这里使用PyTorch实现的神经网络代码和Numpy实现的几乎没有区别,只是把Numpy的操作换成了PyTorch的操作。
import torch
m, d, p, l = 64, 1000, 100, 10
# 随机创建一些训练数据
x = torch.randn(m, d)
y = torch.randn(m, l)
# 随机初始化连接权
w1 = torch.randn(d, p)
w2 = torch.randn(p, l)
learning_rate = 1e-6
for it in range(500):
# forward pass
alpha = x.mm(w1) # m * p
b = alpha.clamp(min=0) # m * p
y_hat = b.mm(w2) # m * l
# compute loss
loss = (y_pred-y).pow(2).sum().item()
print(it, loss)
# backward pass
# compute the gradient
grad_y_hat = 2.0 * (y_hat - y)
grad_w2 = b.t().mm(grad_y_hat)
grad_b = grad_y_hat.mm(w2.t())
grad_alpha = grad_b.clone()
grad_alpha[alpha < 0] = 0
grad_w1 = x.t().mm(grad_alpha)
# update weights of w1 and w2
w1 -= learning_rate * grad_w1
w2 -= learning_rate * grad_w2
4.2.2 autograd
import torch
m, d, p, l = 64, 1000, 100, 10
# 随机创建一些训练数据
x = torch.randn(m, d)
y = torch.randn(m, l)
# 随机初始化连接权
w1 = torch.randn(d, p, requires_grad=True)
w2 = torch.randn(p, l, requires_grad=True)
learning_rate = 1e-6
for it in range(500):
# forward pass
y_hat = x.mm(w1).clamp(min=0).mm(w2)
# compute loss
loss = (y_pred-y).pow(2).sum() # computation graph
print(it, loss.item())
# backward pass
loss.backward()
# update weights of w1 and w2
with torch.no_grad():
w1 -= learning_rate * w1.grad
w2 -= learning_rate * w2.grad
w1.grad.zero_()
w2.grad.zero_()
4.2.3 nn库
import torch
import torch.nn as nn
m, d, p, l = 64, 1000, 100, 10
# 随机创建一些训练数据
x = torch.randn(m, d)
y = torch.randn(m, l)
model = torch.nn.Sequential(
torch.nn.Linear(d, p),
torch.nn.ReLU(),
torch.nn.Linear(p, l)
)
torch.nn.init.normal_(model[0].weight)
torch.nn.init.normal_(model[2].weight)
# model = model.cuda()
loss_fn = nn.MSELoss(reduction='sum')
learning_rate = 1e-6
for it in range(500):
# forward pass
y_hat = model(x)
# compute loss
loss = loss_fn(y_pred, y) # computation graph
print(it, loss.item())
# backward pass
loss.backward()
# update weights of w1 and w2
with torch.no_grad():
for param in model.parameters():
param -= learning_rate * param.grad
model.zero_grad()
4.2.4 使用optim
使用optim包来更新参数,optim这个包提供了各种不同的模型优化方法,包括SGD+momentum, RMSProp, Adam等等。
import torch
import torch.nn as nn
m, d, p, l = 64, 1000, 100, 10
# 随机创建一些训练数据
x = torch.randn(m, d)
y = torch.randn(m, l)
model = torch.nn.Sequential(
torch.nn.Linear(d, p),
torch.nn.ReLU(),
torch.nn.Linear(p, l)
)
# model = model.cuda()
loss_fn = nn.MSELoss(reduction='sum')
learning_rate = 1e-4
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
for it in range(500):
# forward pass
y_hat = model(x)
# compute loss
loss = loss_fn(y_pred, y) # computation graph
print(it, loss.item())
optimizer.zero_grad()
# backward pass
loss.backward()
# update model parameters
optimizer.step()
4.2.5 自定义nn Modules
定义一个模型,这个模型继承自nn.Modules。如果需要定义一个比Sequential模型更加复杂的模型,就需要定义nn.Modules模型。
import torch
import torch.nn as nn
m, d, p, l = 64, 1000, 100, 10
# 随机创建一些训练数据
x = torch.randn(m, d)
y = torch.randn(m, l)
class TwoLayersNet(torch.nn.Module):
def __init__(self, d, p, l):
super(TwoLayerNet, self).__init__()
# define the model architecture
self.linear1 = torch.nn.Linear(d, p)
self.linear2 = torch.nn.Linear(p, l)
def forward(self, x):
y_pred = self.linear2(self.linear1(x).clamp(min=0))
return y_pred
model = TwoLayersNet(d, p, l)
# model = model.cuda()
loss_fn = nn.MSELoss(reduction='sum')
learning_rate = 1e-4
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
for it in range(500):
# forward pass
y_hat = model(x) # 调用model.forward(x)
# compute loss
loss = loss_fn(y_pred, y) # computation graph
print(it, loss.item())
optimizer.zero_grad()
# backward pass
loss.backward()
# update model parameters
optimizer.step()