从BP反向传播算法到Pytorch autograd
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}
dgdh∗dxdg,其中的
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+e−h31。下面使用
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=(out−label)2 (1)),当我们想要求取loss对参数
W
W
W的导数时,就可以使用链式法则,求取过程如下:
- 计算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=doutdloss∗dh3dout∗dh2dh3∗dh1dh2∗dw1dh1 (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(out−label)((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+e−h3)2e−h3(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=2∗out∗(1+e−h3)2e−h3∗W3∗W2∗I
因此,使用链式法则可以将复杂导数进行层层分解,转化成简单导数乘积的形式,这样计算机只要知道每种简单函数求导的表达式,即可求得复杂函数(神经网络)的导数,这一点可以在下面pytorch实现自动求导的代码处进行深入理解。 - 计算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=doutdloss∗dh3dout∗dh2dh3∗dW2dh2=2(out−label)∗(1+e−h3)2e−h3∗W3∗h1 (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=h1∗W2) - 计算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=doutdloss∗dh3dout∗dW3dh3=2(out−label)∗(1+e−h3)2e−h3∗h2 (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=h2∗W3)
读者可以尝试自己推导并对照。
既然已经使用链式法则将各个参数的表达式求取出来了,那么让我们带入实际数值,完成一次梯度下降(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=3,W1=1.1,W2=1.3,W3=1.5,label=0.5
- 首先进行前向传播:计算出 h 1 = I ∗ W 1 = 3.3 h_1=I*W_1=3.3 h1=I∗W1=3.3、 h 2 = h 1 ∗ W 2 = 4.29 h_2=h_1*W_2=4.29 h2=h1∗W2=4.29、 h 3 = h 2 ∗ W 3 = 6.435 h_3=h_2*W_3=6.435 h3=h2∗W3=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=(out−label)2=0.2484。
- 然后可以进行反向传播:将前向过程中计算出的值代入(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
W1、W2、W3的求导表达式中都涉及
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}
doutdloss∗dh3dout∗dh2dh3,这样在计算过程中就会存在重复。
因此BP算法先从最后一层的节点(loss)开始,使用类似广度优先搜索算法进行梯度传播,将下一层参数与节点对于loss的梯度值计算出来(loss对参数的梯度是梯度下降算法需要的,loss对节点的梯度是计算下一层参数的梯度需要的),直到计算出叶子节点参数的梯度值(叶子节点一般是输入节点,没有参数),还是上面那个例子,在反向传播过程中:
- 计算 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(out−label)=0.9968,该层没有需要更新的参数,因此无需计算loss对参数的梯度。
- 计算 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=doutdloss∗dh3dout=doutdloss∗(1+e−h3)2e−h3=0.9968∗0.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的乘积即可。
- 计算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=dh3dloss∗dh2dh3=dh3dloss∗W3=0.0016∗1.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=dh3dloss∗dW3dh3=dh3dloss∗h2=0.0016∗4.29=0.0068,这样计算的结果与我们上面计算的结果相同(可能会存在部分舍入误差)。
- 计算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=dh2dloss∗dh1dh2=dh2dloss∗W2=0.0024∗1.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=dh2dloss∗dW2dh2=dh2dloss∗h1=0.0024∗3.3=0.0079,这样计算的结果与我们上面计算的结果相同。
- 计算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=dh1dloss∗dW1dh1=dh1dloss∗I=0.0031∗3=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属性。
总结
大家如果在看这篇文章时遇到问题可以在评论区中告诉我,如果文章存在什么问题的话,也欢迎大家指正!