发布时间:2024-12-02 16:01
相信所有熟悉DL的人都会非常熟练的运用如下的三行代码
optimizer.zero_grad()
loss.backward()
optimizer.step()
但是我也相信真正能够理解其中机制的人很少,尤其是backward()这一步到底是怎么运行的。
首先但凡学过DL的人都知道深度学习是利用梯度下降法,通过求梯度来进行更新。在看书上的理论部分时其实很好懂,其实就是一个求导然后求梯度,然后利用梯度下降更新梯度。
但是每次只靠这三行代码,就看到试验的准确率越来越高时,总觉得神经网络真正执行的样子和书上写的样子不完全一样,所以博主也是花了一个下午的时间去研究了一下backward在神经网络内部的运行机制。
直接上实例。
import torch as t
from torch.autograd import Variable as V
a = v(t.tensor([2., 3.]), requires_grad=True)
#这里要注意设置tensor的时候不能设成整数,像t.tensor([2,3])就是错的,
#因为torch只能对浮点数做反向传播,所以要在整数后面加一个小数点将整形变为浮点型
b = a + 3
c = b * b * 3
out = c.mean()
out.backward()
print(a.grad)
这样用代码看有点抽象,让我们用数学表达式写出来就会很好理解。我们这里传入了参数( x 1 = 2 , x 2 = 3 \\large x_1 =2,x_2=3 x1=2,x2=3)。这里要注意一个地方就是requires_grad=True,因为variable函数中这个地方默认为False,所以我们要先将这里设置为True才能对其进行求梯度。
a = ( x 1 , x 2 ) a=(\\large x_1, x_2) a=(x1,x2)
b = ( x 1 + 3 , x 2 + 3 ) b=(\\large x_1+3,x_2+3) b=(x1+3,x2+3)
c = ( 3 ∗ ( x 1 + 3 ) 2 , 3 ∗ ( x 2 + 3 ) 2 ) c=(\\large 3*(\\large x_1+3)^2,3*(x_2+3)^2) c=(3∗(x1+3)2,3∗(x2+3)2)
o u t = ( 3 ∗ ( x 1 + 3 ) 2 + 3 ∗ ( x 2 + 3 ) 2 ) 2 out=\\frac {\\large(3*( x_1+3)^2+3*(x_2+3)^2)}{\\large2} out=2(3∗(x1+3)2+3∗(x2+3)2)
∂ o u t ∂ x 1 = 3 ( x 1 + 3 ) ∣ x 1 = 2 = 15 \\frac{\\large\\partial out}{\\large\\partial x_1} =3(x_1+3)|x_1=2=15 ∂x1∂out=3(x1+3)∣x1=2=15
∂ o u t ∂ x 2 = 3 ( x 2 + 3 ) ∣ x 2 = 3 = 18 \\frac{\\large\\partial out}{\\large\\partial x_2}=3(x_2+3)|x_2=3=18 ∂x2∂out=3(x2+3)∣x2=3=18
我们通过数学式子的定义,可以得到对 x 1 \\large x_1 x1和 x 2 \\large x_2 x2求导之后得到的梯度分别为15和18。
那我们这时将a的梯度打印出来看看结果是否和我计算的一样。
可以看到结果确实是一样的(除了18那个地方被水印挡住了)。所以我们可以知道的是backward()的反向传播操作确实是按照我们数学上的梯度求导来的。
上面那个例子还是过于简单了,所以下面我决定搭建一个最简单的 M L P MLP MLP,其中输入神经元个数是1,隐藏层个数是2,输出层的个数也是1,我们用这个例子来看一个反向传播是怎么在神经网络中进行的。
首先我们先搭建好一个非常简易的 M L P MLP MLP
class Net(nn.Module):
def __init__(self, in_dim, out_dim):
super(Net,self).__init__()
self.linear1 = nn.Linear(in_dim,2)#常数2指隐藏层的神经元个数为2
self.linear2 = nn.Linear(2,out_dim)
def forward(self,x):
out = self.linear1(x)
out = self.linear2(out)
return out
我们直接自己设定一个非常简单的 i n p u t = 2 input=2 input=2,并且我们看一下把 2 2 2作为 i n p u t input input会得到什么样的结果
我们可以看到前向传播的结果,也就是 o u t p u t = 0.0318 output=0.0318 output=0.0318
在进行前向传播和反向传播之前我们先将网络中的所有权重打印出来看一眼。
这里为了方便大家理解这里的权重,我自己画了一个图(很丑,但很通俗易懂)
我们可以从这个“很丑,但是通俗易懂”的图里很直观的看出输入和输出的值。还有神经网络中的所有权值。
好了,接下来到了我们的重头戏,我们对这个简单的 M L P MLP MLP做反向传播之后会得到什么样的梯度呢。
结果是这样的,是不是乍一看根本不知道这个梯度是怎么来的,确实是这样的,好像单从代码上看不好理解,那我们就继续从数学的角度来看。在看之前,请允许我向大家展示另一个非常丑但是依旧通俗易懂的图
可以看到,这个图和上面那个图唯一的区别就在于我给每个权重取了一个名字,取名字的原因就在于方便我们接下来写数学公式。
我们的 i n p u t = 2 , o u t p u t = 0.0318 input=2,output=0.0318 input=2,output=0.0318
所以可以很容易得到以下的公式
o u t p u t = i n p u t ∗ w 1 ∗ w 3 + i n p u t ∗ w 2 ∗ w 4 \\large output=input*w_1*w_3+input*w_2*w_4 output=input∗w1∗w3+input∗w2∗w4
我们对 i n p u t input input求导,可以得到 ∂ o u t p u t ∂ i n p u t = ( w 1 ∗ w 3 + w 2 ∗ w 4 ) = ( − 0.2377 ∗ 0.4739 + 0.3578 ∗ ( − 0.2122 ) = − 0.1886 ) \\frac{\\large \\partial output}{\\large \\partial input}=(\\large w_1*w_3+\\large w_2*w_4)=(-0.2377*0.4739+0.3578*(-0.2122)=-0.1886) ∂input∂output=(w1∗w3+w2∗w4)=(−0.2377∗0.4739+0.3578∗(−0.2122)=−0.1886)
所以我们可以看到,这神经网络中的反向传播也是符合我们的梯度运算的。
到这里可能大家觉得好像这个内容也挺简单的嘛,但是我举的例子有一个问题,就是在实际项目中,我们不可能存在一维的输入。所以最后我想和大家再讨论一下当我们的输入维度大于1的时候我们神经网络是怎么进行反向传播的呢。
我们依旧举一个简单的 i n p u t = ( 2 , 3 ) input=(2,3) input=(2,3),这里的输入不再是一维的而是二维的(也就是说数据的特征有两个)
我们把这时网络相关的参数都打印下来给大家看一眼。
我们的输出为 − 1.3992 -1.3992 −1.3992。
权重变成了这样
这里可能大家对权重的变化有些不李姐,没关系,我依旧准备了一张图帮大家李姐。
这里为了方便大家李姐,我在输入的神经处并列又画了一个神经元,这是为了表示此时这一个数据具有两个特征。我们这时将每个权重对应一下,依旧可以得到一个数学公式。
o u t p u t = x 1 ∗ w 1 ∗ w 6 + x 1 ∗ w 3 ∗ w 5 + x 2 ∗ w 2 ∗ w 6 + x 2 ∗ w 4 ∗ w 5 output=x_1*w_1*w_6+x_1*w_3*w_5+x_2*w_2*w_6+x_2*w_4*w_5 output=x1∗w1∗w6+x1∗w3∗w5+x2∗w2∗w6+x2∗w4∗w5
其中 x 1 x_1 x1代表数据的第一个特征, x 2 x_2 x2代表数据的第二个特征。这样我们分别对 x 1 x_1 x1和 x 2 x_2 x2求偏导就可以得到梯度。
这里就不替大家算了(打Katex数学公式挺麻烦的),不过最后算出来结果后肯定也是和代码算出来的是一样的。
其实今天突然想研究一下BP只是单纯的因为在跑A2C的时候对其中的reward更新有点疑惑,所以就自己尝试研究了一下BP中的机制。一开始研究的目的只是想知道BP是怎么运行的,但是当我看到神经网络中的权重的时候,我好像突然明白之前听到过的“梯度消失”和“梯度爆炸”到底代表的是什么意思。也许我们有时候只是想研究一个点,但很可能研究的过程中,我们可以拓展到一个面。