误差逆传播算法(BP算法)

误差逆传播算法(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)},xxxiRd,yyyiRl,即训练集中一共有 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+ex1)。

​ 对训练例 ( 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=1l(y^jkyjk)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 dq个权值, q q q个隐层神经元的阈值,隐层到输出层的 q ∗ l q*l ql个权值, l l l个输出神经元的阈值。BP(BackPropagation)是一个迭代学习算法,在迭代的每一轮中采用广义的感知机学习规则对参数进行更新估计,任意参数 v v v的更新估计式为
式 3 :   v ← v + Δ v . 式3:\ v \leftarrow v+\Delta v. 3: vv+Δ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=ηwhjEk.
注意到 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: whjEk=y^jkEkβjy^jkwhjβ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)(1sigmoid(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^jkEkβjy^jk=(y^jkyjk)sigmoid(βjθj)=y^jk(1y^jk)(yjky^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=ηgjbh.
​ 类似可得
式 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=ηθjEk=ηy^jkEkθjy^jk=η(y^jkyjk)sigmoid(βjθj)=η(y^jkyjk)y^jk(1y^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=bhEkαhbh=j=1lβjEkαhβjsigmoid(αhγh)=j=1lgjwhjsigmoid(αhγh)=bh(1bh)j=1lgjwhj.
​ 学习率 η \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=1mEk,
之前介绍的都是每次仅针对一个训练样例更新连接权和阈值的标准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()

猜你喜欢

转载自blog.csdn.net/NickHan_cs/article/details/112295451