记得深度学习三巨头之一Yann LeCun曾经说过:“深度学习已死,可微编程永生”,就是说深度学习只是一种计算范式,而背后的可微分编程,具有更广阔的应用前景。在这里我们将探索PyTorch中的自动微分技术。
Tensor函数
PyTorch中的自动微分是基于Tensor的,以Tensor作为参数的函数,实际上是将Tensor中的每个元素应用该函数。只讲概念比较抽象,我们来看一个具体的例子。
我们定义x∈R5,定义如下函数:
y=f(x)=⎣⎢⎢⎢⎢⎡f(x1)f(x2)f(x3)f(x4)f(x5)⎦⎥⎥⎥⎥⎤
下面的程序我们以x2为例:
import torch
x = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0])
y = x ** 2
print(y)
在深度学习中,有三种最常见的情况:标量对向量的微分、向量对向量的微分、向量对矩阵的微分,其实还有张量对张量的微分,因为在实际中比较少见,所以这里就不再讨论了。
标量对向量的微分
通常神经网络的代价函数会是一个标量,对于多分类问题,神经网络的输出为一个向量,这时就需要求标量对向量的微分。我们定义如下函数:
y=i=1∑5xi2
我们知道求微分可以采用解析法、数值法和自动微分法,由于这个问题比较简单,我们可以直接应用解析法:
∂xi∂y=2xi,i=1,2,3,4,5
我们定义标量对向量的微分为:
∂x∂y=[∂x1∂y∂x2∂y∂x3∂y∂x4∂y∂x5∂y]∈R1×n,x∈Rn
我们可以通过PyTorch中的自动微分来完成这一任务:
x = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0], requires_grad=True, dtype=torch.float32)
y = torch.sum(x ** 2)
print(y.item())
y.backward()
print('shape:{0}; {1}'.format(x.grad.size(), x.grad))
第1行:
- 第1行:我们在定义Tensor时,如果加入requires_grad,表明我们需要求对其的微分;
- 第2行:计算函数值,在实际应用中就是神经网络的前向传播过程;
- 第4行:调用自动微分,就是实际应用中的神经网络的反向传播过程;
我们可以看到打印的结果与我们用解析法求出的内容一致。
向量对向量微分
实际上向量对向量微分叫做Jacobian矩阵,向量y∈Rm对向量x∈Rn的微分定义为:
∂x∂y=⎣⎢⎢⎢⎡∂x1∂y1∂x1∂y2...∂x1∂ym∂x2∂y1∂x2∂y2...∂x2∂ym............∂xn∂y1∂xn∂y2...∂xn∂ym⎦⎥⎥⎥⎤∈Rm×n
我们假设有如下向量,例如是神经网络某层的输入向量:
zl=⎣⎢⎢⎢⎢⎡1.02.03.04.05.0⎦⎥⎥⎥⎥⎤
经过该层的**函数(sigmoid函数)之后,得到本层的输出向量:
al=⎣⎢⎢⎢⎢⎡z12z22z13z14z15⎦⎥⎥⎥⎥⎤
因为本神经元的输入只会影响本神经元的输出,因此我们的al如上所示。根据解析法可得其Jacobian矩阵为:
∂zl∂al=⎣⎢⎢⎢⎢⎢⎢⎢⎢⎡∂z1l∂a1l00000∂z2l∂a2l00000∂z3l∂a3l00000∂z3l∂a3l00000∂z3l∂a3l⎦⎥⎥⎥⎥⎥⎥⎥⎥⎤
其实际上只有对角线上有值,因此在PyTorch中,为了更高效的处理问题,我们通常不需要原始的Jacobian矩阵,而是将其乘以一个与al同维且元素均为1的向量,得到的结果向量的元素为对角线上的元素。
∂zl∂al⋅⎣⎢⎢⎢⎢⎡1.01.01.01.01.0⎦⎥⎥⎥⎥⎤=⎣⎢⎢⎢⎢⎢⎢⎢⎢⎡∂z1l∂a1l00000∂z2l∂a2l00000∂z3l∂a3l00000∂z3l∂a3l00000∂z3l∂a3l⎦⎥⎥⎥⎥⎥⎥⎥⎥⎤⋅⎣⎢⎢⎢⎢⎡1.01.01.01.01.0⎦⎥⎥⎥⎥⎤=⋅⎣⎢⎢⎢⎢⎢⎢⎢⎢⎡∂z1l∂a1l∂z2l∂a2l∂z3l∂a2l∂z4l∂a4l∂z5l∂a5l⎦⎥⎥⎥⎥⎥⎥⎥⎥⎤
在PyTorch中代码如下所示:
x = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0], requires_grad=True)
y = x**2
y.backward(torch.ones_like(y))
print('y_x:{0}'.format(x.grad))
向量对矩阵求微分
在深度学习中,经常会出现上一层的输入向量zl∈RNl对连接权值Wl∈RNl×Nl−1的微分,公式为:
zl=Wl⋅al−1+bl
其中al−1∈RNl−1,bl∈RNl。
通过解析法,我们可以得到其微分公式为:
∂Wl∂zl=⎣⎢⎢⎡a1l−1a1l−1...a1l−1a2l−1a2l−1...a2l−1............al−1l−1al−1l−1...al−1l−1⎦⎥⎥⎤=⎣⎢⎢⎡(al−1)T(al−1)T...(al−1)T⎦⎥⎥⎤∈RNl×Nl−1
PyTorch代码如下所示:
a_l_1 = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0], requires_grad=True)
W_l = torch.tensor([
[101.0, 102.0, 103.0, 104.0, 105.0],
[201.0, 202.0, 203.0, 204.0, 205.0],
[301.0, 302.0, 303.0, 304.0, 305.0]
], requires_grad=True)
b_l = torch.tensor([1001.0, 1002.0, 1003.0], requires_grad=True)
z_l = torch.matmul(W_l, a_l_1) + b_l
z_l.backward(torch.ones_like(z_l))
print('y_x:{0}'.format(W_l.grad))
''' 打印输出
y_x:tensor([[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.],
[1., 2., 3., 4., 5.]])
'''
上述程序的打印结果与理论分析的结果一致,证明我们的求法是正确的。