BP反向传播算法与Pytorch autograd包解析

本文介绍了反向传播算法在神经网络中的作用,详细解释了链式法则及其在BP算法中的应用,并通过一个三层神经网络的例子展示了如何计算损失对参数的梯度。此外,探讨了Pytorch中的autograd包,阐述了它如何通过构造计算图实现自动求导,最后提供了一个使用autograd包验证反向传播结果的代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

BP(反向传播算法)简介

反向传播算法是如今人工智能优化方法的基础算法之一,如今的梯度下降算法主要思想是从input通过一次前向传播计算output,继而根据output与label计算出loss,再从loss通过反向传播计算网络中参数的梯度,从而对这些参数进行更新。因此反向传播算法在梯度下降算法中起到重要作用。

链式法则

反向传播算法的核心思想就是使用链式法则求出参数的导数,那么,首先给大家介绍一下链式法则。
链式法则是用来求解复合函数的导数的,给大家举一个例子(来自维基百科):
求函数 f ( x ) = ( x 2 + 1 ) 3 f(x)=(x^2+1)^3 f(x)=(x2+1)3 的导数。
g ( x ) = x 2 + 1 g(x)=x^2+1 g(x)=x2+1
h ( g ) = g 3 h(g)=g^3 h(g)=g3 h ( g ( x ) ) = g ( x ) 3 . h(g(x))=g(x)^3. h(g(x))=g(x)3.
f ( x ) = h ( g ( x ) ) f(x)=h(g(x)) f(x)=h(g(x))
f ′ ( x ) = h ′ ( g ( x ) ) g ′ ( x ) = 3 ( g ( x ) ) 2 ( 2 x ) = 3 ( x 2 + 1 ) 2 ( 2 x ) = 6 x ( x 2 + 1 ) 2 . f′(x)=h′(g(x))g′(x)=3(g(x))^2(2x)=3(x^2+1)^2(2x)=6x(x^2+1)^2. f(x)=h(g(x))g(x)=3(g(x))2(2x)=3(x2+1)2(2x)=6x(x2+1)2.
即我们想求 d h d x \frac{dh}{dx} dxdh,可以转化成求 d h d g ∗ d g d x \frac{dh}{dg}*\frac{dg}{dx} dgdhdxdg,其中的 d h d g \frac{dh}{dg} dgdh d g d x \frac{dg}{dx} dxdg都可以使用基本的求导方法简单求得。

链式法则在BP算法中的应用

BP算法的本质是一种帮助我们快速求取loss与神经网络模型参数之间导数的算法,而这便使用到了链式法则,先举个简单的应用链式法则的例子:
存在一个含三个隐层(h1、h2、h3)的全连接神经网络,如下图所示:

在这里插入图片描述
其中 I I I为输入, O O O为输出, W W W为每层的参数,h3经过 S i g m o i d Sigmoid Sigmoid激活函数得到 O O O,即 O = S i g m o i d ( h 3 ) = 1 1 + e − h 3 O=Sigmoid(h_3)=\frac{1}{1+e^{-h_3}} O=Sigmoid(h3)=1+eh31。下面使用 o u t out out来代替 O O O
假设使用均方差损失函数( l o s s = ( o u t − l a b e l ) 2 loss=(out-label)^2 loss=(outlabel)2 (1)),当我们想要求取loss对参数 W W W的导数时,就可以使用链式法则,求取过程如下:

  1. 计算loss对 W 1 W_1 W1的导数:
    d l o s s d W 1 = d l o s s d o u t ∗ d o u t d h 3 ∗ d h 3 d h 2 ∗ d h 2 d h 1 ∗ d h 1 d w 1 \frac{dloss}{dW_1}=\frac{dloss}{dout}*\frac{dout}{dh_3}*\frac{dh_3}{dh_2}*\frac{dh_2}{dh_1}*\frac{dh_1}{dw_1} dW1dloss=doutdlossdh3doutdh2dh3dh1dh2dw1dh1 (2)
    其中 d l o s s d o u t = 2 ( o u t − l a b e l ) \frac{dloss}{dout}=2(out-label) doutdloss=2(outlabel)((1) 式对 o u t out out求导), d o u t d h 3 = e − h 3 ( 1 + e − h 3 ) 2 \frac{dout}{dh_3}=\frac{e^{-h_3}}{(1+e^{-h_3})^2} dh3dout=(1+eh3)2eh3(Sigmoid层的导数), d h 3 d h 2 = W 3 \frac{dh_3}{dh_2}=W_3 dh2dh3=W3, d h 2 d h 1 = W 2 \frac{dh_2}{dh_1}=W_2 dh1dh2=W2, d h 1 d W 1 = I \frac{dh_1}{dW_1}=I dW1dh1=I,将分布计算的结果代入 (2) 式,得:
    d l o s s d W 1 = 2 ∗ o u t ∗ e − h 3 ( 1 + e − h 3 ) 2 ∗ W 3 ∗ W 2 ∗ I \frac{dloss}{dW_1}=2*out*\frac{e^{-h_3}}{(1+e^{-h_3})^2}*W_3*W_2*I dW1dloss=2out(1+eh3)2eh3W3W2I
    因此,使用链式法则可以将复杂导数进行层层分解,转化成简单导数乘积的形式,这样计算机只要知道每种简单函数求导的表达式,即可求得复杂函数(神经网络)的导数,这一点可以在下面pytorch实现自动求导的代码处进行深入理解。
  2. 计算loss对 W 2 W_2 W2的导数:
    d l o s s d W 2 = d l o s s d o u t ∗ d o u t d h 3 ∗ d h 3 d h 2 ∗ d h 2 d W 2 = 2 ( o u t − l a b e l ) ∗ e − h 3 ( 1 + e − h 3 ) 2 ∗ W 3 ∗ h 1 \frac{dloss}{dW_2}=\frac{dloss}{dout}*\frac{dout}{dh_3}*\frac{dh_3}{dh_2}*\frac{dh_2}{dW_2}=2(out-label)*\frac{e^{-h_3}}{(1+e^{-h_3})^2}*W_3*h_1 dW2dloss=doutdlossdh3doutdh2dh3dW2dh2=2(outlabel)(1+eh3)2eh3W3h1 (3)
    其中 d h 2 d W 2 = h 1 \frac{dh_2}{dW_2}=h_1 dW2dh2=h1( h 2 = h 1 ∗ W 2 h_2=h_1*W_2 h2=h1W2)
  3. 计算loss对 W 3 W_3 W3的导数:
    d l o s s d W 3 = d l o s s d o u t ∗ d o u t d h 3 ∗ d h 3 d W 3 = 2 ( o u t − l a b e l ) ∗ e − h 3 ( 1 + e − h 3 ) 2 ∗ h 2 \frac{dloss}{dW_3}=\frac{dloss}{dout}*\frac{dout}{dh_3}*\frac{dh_3}{dW_3}=2(out-label)*\frac{e^{-h_3}}{(1+e^{-h_3})^2}*h_2 dW3dloss=doutdlossdh3doutdW3dh3=2(outlabel)(1+eh3)2eh3h2 (4)
    其中 d h 3 d W 3 = h 2 \frac{dh_3}{dW_3}=h_2 dW3dh3=h2( h 3 = h 2 ∗ W 3 h_3=h_2*W_3 h3=h2W3)

读者可以尝试自己推导并对照。
既然已经使用链式法则将各个参数的表达式求取出来了,那么让我们带入实际数值,完成一次梯度下降(SGD)算法:
假设当前输入 I = 3 , W 1 = 1.1 , W 2 = 1.3 , W 3 = 1.5 , l a b e l = 0.5 I=3,W_1=1.1,W_2=1.3,W_3=1.5,label=0.5 I=3W1=1.1W2=1.3W3=1.5label=0.5

  1. 首先进行前向传播:计算出 h 1 = I ∗ W 1 = 3.3 h_1=I*W_1=3.3 h1=IW1=3.3 h 2 = h 1 ∗ W 2 = 4.29 h_2=h_1*W_2=4.29 h2=h1W2=4.29 h 3 = h 2 ∗ W 3 = 6.435 h_3=h_2*W_3=6.435 h3=h2W3=6.435 o u t = S i g m o i d ( h 3 ) = 0.9984 out=Sigmoid(h_3)=0.9984 out=Sigmoid(h3)=0.9984 l o s s = ( o u t − l a b e l ) 2 = 0.2484 loss=(out-label)^2=0.2484 loss=(outlabel)2=0.2484
  2. 然后可以进行反向传播:将前向过程中计算出的值代入(2)、(3)、(4)式,可以分别计算出 d l o s s d W 1 = 0.0093 \frac{dloss}{dW_1}=0.0093 dW1dloss=0.0093 d l o s s d W 2 = 0.0079 \frac{dloss}{dW_2}=0.0079 dW2dloss=0.0079 d l o s s d W 3 = 0.0068 \frac{dloss}{dW_3}=0.0068 dW3dloss=0.0068

上述过程只是链式法则的简单应用,但是在实际的反向传播过程中,如果向我们上面那样先将所有参数的表达式计算出来,存在着较多冗余,比如loss对 W 1 、 W 2 、 W 3 W_1、W_2、W_3 W1W2W3的求导表达式中都涉及 d l o s s d o u t ∗ d o u t d h 3 ∗ d h 3 d h 2 \frac{dloss}{dout}*\frac{dout}{dh_3}*\frac{dh_3}{dh_2} doutdlossdh3doutdh2dh3,这样在计算过程中就会存在重复。
因此BP算法先从最后一层的节点(loss)开始,使用类似广度优先搜索算法进行梯度传播,将下一层参数节点对于loss的梯度值计算出来(loss对参数的梯度是梯度下降算法需要的,loss对节点的梯度是计算下一层参数的梯度需要的),直到计算出叶子节点参数的梯度值(叶子节点一般是输入节点,没有参数),还是上面那个例子,在反向传播过程中:

  1. 计算 l o s s loss loss o u t out out的导数: d l o s s d o u t = 2 ( o u t − l a b e l ) = 0.9968 \frac{dloss}{dout}=2(out-label)=0.9968 doutdloss=2(outlabel)=0.9968,该层没有需要更新的参数,因此无需计算loss对参数的梯度。
  2. 计算 l o s s loss loss h 3 h_3 h3的导数: d l o s s d h 3 = d l o s s d o u t ∗ d o u t d h 3 = d l o s s d o u t ∗ e − h 3 ( 1 + e − h 3 ) 2 = 0.9968 ∗ 0.0016 = 0.0016 \frac{dloss}{dh_3}=\frac{dloss}{dout}*\frac{dout}{dh_3}=\frac{dloss}{dout}*\frac{e^{-h_3}}{(1+e^{-h_3})^2}=0.9968*0.0016=0.0016 dh3dloss=doutdlossdh3dout=doutdloss(1+eh3)2eh3=0.99680.0016=0.0016,这里便会用到1中计算的 d l o s s d o u t \frac{dloss}{dout} doutdloss,我们实际在这一层只需要计算 d o u t d h 3 \frac{dout}{dh_3} dh3dout部分,然后计算其与 d l o s s d o u t \frac{dloss}{dout} doutdloss的乘积即可。
  3. 计算loss对 h 2 h_2 h2的导数: d l o s s d h 2 = d l o s s d h 3 ∗ d h 3 d h 2 = d l o s s d h 3 ∗ W 3 = 0.0016 ∗ 1.5 = 0.0024 \frac{dloss}{dh_2}=\frac{dloss}{dh_3}*\frac{dh_3}{dh_2}=\frac{dloss}{dh_3}*W_3=0.0016*1.5=0.0024 dh2dloss=dh3dlossdh2dh3=dh3dlossW3=0.00161.5=0.0024,并且此时我们需要计算 l o s s loss loss W 3 W_3 W3的导数: d l o s s d W 3 = d l o s s d h 3 ∗ d h 3 d W 3 = d l o s s d h 3 ∗ h 2 = 0.0016 ∗ 4.29 = 0.0068 \frac{dloss}{dW_3}=\frac{dloss}{dh_3}*\frac{dh_3}{dW_3}=\frac{dloss}{dh_3}*h_2=0.0016*4.29=0.0068 dW3dloss=dh3dlossdW3dh3=dh3dlossh2=0.00164.29=0.0068,这样计算的结果与我们上面计算的结果相同(可能会存在部分舍入误差)。
  4. 计算loss对 h 1 h_1 h1的导数: d l o s s d h 1 = d l o s s d h 2 ∗ d h 2 d h 1 = d l o s s d h 2 ∗ W 2 = 0.0024 ∗ 1.3 = 0.0031 \frac{dloss}{dh_1}=\frac{dloss}{dh_2}*\frac{dh_2}{dh_1}=\frac{dloss}{dh_2}*W_2=0.0024*1.3=0.0031 dh1dloss=dh2dlossdh1dh2=dh2dlossW2=0.00241.3=0.0031,并且此时我们需要计算 l o s s loss loss W 2 W_2 W2的导数: d l o s s d W 2 = d l o s s d h 2 ∗ d h 2 d W 2 = d l o s s d h 2 ∗ h 1 = 0.0024 ∗ 3.3 = 0.0079 \frac{dloss}{dW_2}=\frac{dloss}{dh_2}*\frac{dh_2}{dW_2}=\frac{dloss}{dh_2}*h_1=0.0024*3.3=0.0079 dW2dloss=dh2dlossdW2dh2=dh2dlossh1=0.00243.3=0.0079,这样计算的结果与我们上面计算的结果相同。
  5. 计算loss对 W 1 W_1 W1的导数: d l o s s d W 1 = d l o s s d h 1 ∗ d h 1 d W 1 = d l o s s d h 1 ∗ I = 0.0031 ∗ 3 = 0.0093 \frac{dloss}{dW_1}=\frac{dloss}{dh_1}*\frac{dh_1}{dW_1}=\frac{dloss}{dh_1}*I=0.0031*3=0.0093 dW1dloss=dh1dlossdW1dh1=dh1dlossI=0.00313=0.0093,这样计算的结果与我们上面计算的结果相同。

上述过程就是完整的BP算法!

Pytorch中的autograd

Pytorch是目前比较成熟的深度学习框架,拥有出色的性能以及简易的上手难度。Pytorch中的自动微分包autograd可以帮助我们自动实现梯度的反向传播,其原理也是我们上述的BP算法,在这里我想简单地分析一下Pytorch autograd包的代码。
首先简单介绍一下pytorch的基本数据结构:
在此我介绍的是pytorch1.0版本之后推荐使用的数据结构:tensor,每个tensor存在三个关键的属性:1.data保存着这个tensor的实际数据。2.grad:如果这个tensor的required_grad属性=True,那么这个节点在反向传播过程中,pytorch会计算出这个节点与loss之间的导数(后面会介绍这个计算过程的实现)。3.grad_fn:一个指向Function对象的指针,在前向传播时生成用于反向传播过程中的梯度计算
Pytorch autograd是通过构造计算图来实现反向传播的,因此autograd包中首先实现了两个基础类别:Node、Edge,分别用来保存计算图的节点与边,因此前向传播的过程就是一个DAG(有向无环图)生成的过程,而反向传播的过程就是根据这个DAG图的拓扑顺序对参数求梯度的过程。
pytorch autograd包中还有一种基本的类别:Function,该类用来记录节点的操作,在pytorch代码中是这样介绍的:
Every operation performed on :class:Tensor s creates a new function object, that performs the computation, and records that it happened. The history is retained in the form of a DAG of functions, with edges denoting data dependencies (input <- output). Then, when backward is called, the graph is processed in the topological ordering, by calling :func:backward methods of each :class:Function object, and passing returned gradients on to next :class:Function s.
大概意思就是这个Function是用来记录DAG图中,input到output的操作。
并且pytorch将所有基本Function类的导数的表达式实现在了其backward方法中,这样在前向传播过程中保存的grad_fn(即Function)便可以在反向传播时计算导数。
Pytorch中每个需要更新的参数(tensor)都要指定requires_grad=True属性,在前向传播过程中pytorch会得到所有与required_grad=True进行运算的节点的值,以及生成requires_grad=True节点父节点的grad_fn属性,属性,举个例子,如下是神经网络的一层全连接实例,其中a为输入,w为参数,b(=a*w)为输出:
在这里插入图片描述
w为参数,一般requires_grad=True(需要被更新),输入节点一般requires_grad=False,因此在前向传播计算到这一层时,因为w的requires_grad=True,因此w的父节点b的grad_fn指向一个Function对象,这个Function对象的backward方法可以用来计算b对w的导数( d b d w \frac{db}{dw} dwdb)。
当前反向传播到这一层时,可以使用节点b的grad_fn属性,得到节点b对w的导数,并且我们在上一层已经计算出loss对b的导数,由此我们可以使用链式法则轻松计算出loss对w的导数,从而在pytorch实现反向传播过程。
顺便一提,pytorch在前向传播中保存的计算图实际上就是所有节点的值以及requires_grad=True节点的父节点的grad_fn属性,这样pytorch才能在反向传播过程中计算出loss对每一个参数的梯度。

使用pytorch的autograd包对示例求证

在介绍链式法则时,我给大家举了一个小例子,在上一节中也向大家介绍了pytorch的反向传播实现方法,现在就让我们使用pytorch的autograd包对我们之前的示例进行计算,看看与我们手动计算的结果是不是一样的:
代码如下,读者可以自行尝试运行(pytorch 1.0版本可以正常运行):

I = torch.Tensor([3])
label = torch.Tensor([0.5])
W1 = torch.tensor([1.1], requires_grad=True)
W2 = torch.tensor([1.3], requires_grad=True)
W3 = torch.tensor([1.5], requires_grad=True)
h1 = I*W1
h2 = h1*W2
h3 = h2*W3
O = torch.sigmoid(h3)
loss = nn.functional.mse_loss(O, label)
print("h1: ", h1, " h2: ", h2, " h3: ", h3, " O: ", O, " loss:", loss)
loss.backward()
print("W1.grad: ", W1.grad, " W2.grad: ", W2.grad, " W3.grad: ", W3.grad)

代码运行结果:
代码运行结果
可以看到实际运行结果与我们之前计算的结果一致,也可以看到在前向传播过程中,节点的grad_fn属性。

总结

大家如果在看这篇文章时遇到问题可以在评论区中告诉我,如果文章存在什么问题的话,也欢迎大家指正!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值