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

本文详细介绍了误差逆传播(BP)算法的思想、数学推导和伪代码,以及在神经网络中的应用。通过举例展示了如何在Numpy和PyTorch中实现两层神经网络,包括手动计算梯度和使用自动梯度优化工具。BP算法通过反向传播误差来更新权重,用于训练多层前馈神经网络,减少训练集上的均方误差。
部署运行你感兴趣的模型镜像

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

本文内容主要参考《机器学习》(清华大学出版社,西瓜书)

1. 算法思想

​ 给定训练集D={(x1,y1),(x2,y2),…,(xm,ym)},xi∈Rd,yi∈RlD=\{(\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^lD={(xxx1,yyy1),(xxx2,yyy2),,(xxxm,yyym)},xxxiRd,yyyiRl,即训练集中一共有mmm个训练数据,每个数据的输入由ddd个属性描述,输出为lll维实值向量。

在这里插入图片描述

​ 在下文的神经网络中采用的神经元模型都是上图所示的“M-P神经元模型”。

在这里插入图片描述

​ 上图给出了一个拥有**ddd个输入神经元,qqq个隐层神经元,lll个输出神经元**的多层前馈神经网络结构。

​ 其中,输出层第jjj个神经元的阈值θj\theta_jθj表示,隐层第hhh个神经元的阈值γh\gamma_hγh表示。输入层第iii个神经元与隐层第hhh个神经元之间的连接权vihv_{ih}vih隐层第hhh个神经元与输出层第jjj个神经元之间的连接权whjw_{hj}whj

​ 记隐层第hhh个神经元接收到的输入是αh=∑i=1dvihxi\alpha_h=\sum_{i=1}^{d}v_{ih}x_iαh=i=1dvihxi,输出层第jjj个神经元接收到的输入是βj=∑h=1qwhjbh\beta_j=\sum_{h=1}^{q}w_{hj}b_hβj=h=1qwhjbh,其中bhb_hbh是隐层第hhh个神经元的输出。假设隐层和输出层神经元都是用SigmoidSigmoidSigmoid函数(sigmoid(x)=11+e−xsigmoid(x)=\frac{1}{1+e^{-x}}sigmoid(x)=1+ex1)。

​ 对训练例(xk,yk)(\pmb{x}_k, \pmb{y}_k)(xxxk,yyyk),假定神经网络的输出为y^k=(y^1k,y^2k,…,y^lk)\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^jk=sigmoid(βj−θj), 式1:\ \hat{y}_j^k=sigmoid(\beta_j-\theta_j), 1: y^jk=sigmoid(βjθj),
则网络在(xk,yk)(\pmb{x}_k, \pmb{y}_k)(xxxk,yyyk)上的均方误差为
式2: Ek=12∑j=1l(y^jk−yjk)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.
​ 这里是整理的符号表:

符号名符号含义
ddd输入神经元个数
qqq隐层神经元个数
lll输出神经元个数
γh\gamma_hγh隐层第hhh个神经元的阈值
θj\theta_jθj输出层第jjj个神经元的阈值
vihv_{ih}vih输入层第iii个神经元与隐层第hhh个神经元的之间连接权
whjw_{hj}whj隐层第hhh个神经元与输出层第jjj个神经元之间的连接权
αh\alpha_hαh隐层第hhh个神经元接收到的输入
bhb_hbh隐层第hhh个神经元的输出
βj\beta_jβj输出层第jjj个神经元接收到的输入
y^jk\hat{y}_j^ky^jk输出层第jjj个神经元的输出

​ 网络中有(d+1)∗q+(q+1)∗l(d+1)*q+(q+1)*l(d+1)q+(q+1)l个参数需要确定:输入层到隐层的d∗qd*qdq个权值,qqq个隐层神经元的阈值,隐层到输出层的q∗lq*lql个权值,lll个输出神经元的阈值。BP(BackPropagation)是一个迭代学习算法,在迭代的每一轮中采用广义的感知机学习规则对参数进行更新估计,任意参数vvv的更新估计式为
式3: v←v+Δv. 式3:\ v \leftarrow v+\Delta v. 3: vv+Δv.
下面我们以神经网络图中的隐层到输出层的连接权whjw_{hj}whj为例来进行推导。

​ BP算法基于梯度下降策略,以目标的负梯度方向对参数进行调整。对误差EkE_kEk,给定学习率η\etaη,有
式4: Δwhj=−η∂Ek∂whj. 式4:\ \Delta w_{hj}=-\eta \frac{\partial E_k}{\partial w_{hj}}. 4: Δwhj=ηwhjEk.
注意到whjw_{hj}whj先影响到输出层第jjj个神经元的输入值βj\beta_jβj,再影响到其输出值y^jk\hat{y}_j^ky^jk,最后影响到EkE_kEk,有
式5: ∂Ek∂whj=∂Ek∂y^jk∗∂y^jk∂βj∗∂βj∂whj. 式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∂whj=bh. 式6:\ \frac{\partial \beta_j}{\partial w_{hj}}=b_h. 6: whjβj=bh.
SigmoidSigmoidSigmoid函数有一个很好的性质:
式7: sigmoid′(x)=sigmoid(x)∗(1−sigmoid(x)). 式7:\ sigmoid'(x)=sigmoid(x)*(1-sigmoid(x)). 7: sigmoid(x)=sigmoid(x)(1sigmoid(x)).
于是根据式2和式1,
式8: gj=−∂Ek∂y^jk∗∂y^jk∂βj=−(y^jk−yjk)sigmoid′(βj−θj)=y^jk(1−y^jk)(yjk−y^jk) 式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算法中关于whjw_{hj}whj的更新公式
式9: Δwhj=η∗gj∗bh. 式9:\ \Delta w_{hj}=\eta * g_j * b_h. 9: Δwhj=ηgjbh.
​ 类似可得
式10: Δθj=−η∂Ek∂θj=−η∂Ek∂y^jk∗∂y^jk∂θj=−η(y^jk−yjk)sigmoid′(βj−θj)=−η(y^jk−yjk)y^jk(1−y^jk)∗−1=−ηgj, 式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: Δvih=ηehxi, 式11:\ \Delta v_{ih}=\eta e_h x_i, 11: Δvih=ηehxi,

式12: Δγh=−ηeh, 式12:\ \Delta\gamma_h=-\eta e_h, 12: Δγh=ηeh

在式11和式12中,
式13: eh=−∂Ek∂bh∗∂bh∂αh=−∑j=1l∂Ek∂βj∗∂βj∂αhsigmoid′(αh−γh)=∑j=1lgjwhjsigmoid′(αh−γh)=bh(1−bh)∑j=1lgjwhj. 式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算法的目标是要最小化训练集DDD上的累计误差
E=1m∑k=1mEk, E=\frac{1}{m}\sum_{k=1}^{m}E_k, E=m1k=1mEk,
之前介绍的都是每次仅针对一个训练样例更新连接权和阈值的标准BP算法,也就是说上述算法的更新规则是基于单个的EkE_kEk推导而得的。如果类似地推导出基于累积误差最小化的更新规则,就得到了累积误差逆传播算法

​ 一般来说,标准BP算法每次更新只针对单个样例,参数更新得非常频繁,而且对不同样例进行更新的效果可能出现“抵消”效果。因此为了达到同样的累积误差极小点,标准BP算法往往需要更多次数的迭代,累积BP算法直接针对累积误差最小化,它在读取整个训练集DDD一遍(读取训练集一遍称为进行了一轮(one epoch)学习)后才对参数进行更新,其参数更新的频率低得多。但在很多任务中,累积误差下降到一定程度之后,进一步下降会非常缓慢,这是标准BP往往会更快得到较好的解,尤其是在训练集DDD非常大时更加明显。

4. 实例

4.1 Numpy实现两层神经网络

​ 一个全连接ReLU神经网络,一个隐藏层,没有bias(阈值为0,γh=0\gamma_h=0γh=0θj=0\theta_j=0θj=0)。用来从xxx预测yyy,使用L2-Loss。
α=W1xb=ReLU(α)y^=W2b \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()

您可能感兴趣的与本文相关的镜像

PyTorch 2.6

PyTorch 2.6

PyTorch
Cuda

PyTorch 是一个开源的 Python 机器学习库,基于 Torch 库,底层由 C++ 实现,应用于人工智能领域,如计算机视觉和自然语言处理

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值