误差逆传播算法(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()