记得深度学习三巨头之一Yann LeCun曾经说过:“深度学习已死,可微编程永生”,就是说深度学习只是一种计算范式,而背后的可微分编程,具有更广阔的应用前景。在这里我们将探索PyTorch中的自动微分技术。
Tensor函数
PyTorch中的自动微分是基于Tensor的,以Tensor作为参数的函数,实际上是将Tensor中的每个元素应用该函数。只讲概念比较抽象,我们来看一个具体的例子。
我们定义
x
∈
R
5
\boldsymbol{x} \in R^{5}
x∈R5,定义如下函数:
y
=
f
(
x
)
=
[
f
(
x
1
)
f
(
x
2
)
f
(
x
3
)
f
(
x
4
)
f
(
x
5
)
]
y=f(\boldsymbol{x})=\begin{bmatrix} f(x_{1}) \\ f(x_{2}) \\ f(x_{3}) \\ f(x_{4}) \\ f(x_{5}) \end{bmatrix}
y=f(x)=⎣⎢⎢⎢⎢⎡f(x1)f(x2)f(x3)f(x4)f(x5)⎦⎥⎥⎥⎥⎤
下面的程序我们以
x
2
x^{2}
x2为例:
import torch
x = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0])
y = x ** 2
print(y)
# 输出结果为:tensor([ 1., 4., 9., 16., 25.])
在深度学习中,有三种最常见的情况:标量对向量的微分、向量对向量的微分、向量对矩阵的微分,其实还有张量对张量的微分,因为在实际中比较少见,所以这里就不再讨论了。
标量对向量的微分
通常神经网络的代价函数会是一个标量,对于多分类问题,神经网络的输出为一个向量,这时就需要求标量对向量的微分。我们定义如下函数:
y
=
∑
i
=
1
5
x
i
2
y = \sum_{i=1}^{5} x_{i}^{2}
y=i=1∑5xi2
我们知道求微分可以采用解析法、数值法和自动微分法,由于这个问题比较简单,我们可以直接应用解析法:
∂
y
∂
x
i
=
2
x
i
,
i
=
1
,
2
,
3
,
4
,
5
\frac{ \partial{y} }{ \partial{x_{i}} } = 2x_{i}, \quad i=1,2,3,4,5
∂xi∂y=2xi,i=1,2,3,4,5
我们定义标量对向量的微分为:
∂
y
∂
x
=
[
∂
y
∂
x
1
∂
y
∂
x
2
∂
y
∂
x
3
∂
y
∂
x
4
∂
y
∂
x
5
]
∈
R
1
×
n
,
x
∈
R
n
\frac{ \partial{y} }{ \partial{ \boldsymbol{x} } } = \begin{bmatrix} \frac{ \partial{y} }{ \partial{x_{1}} } & \frac{ \partial{y} }{ \partial{x_{2}} } & \frac{ \partial{y} }{ \partial{x_{3}} } & \frac{ \partial{y} }{ \partial{x_{4}} } & \frac{ \partial{y} }{ \partial{x_{5}} } \end{bmatrix} \in R^{1 \times n}, \quad \boldsymbol{x} \in R^{n}
∂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)) # 打印y对x的微分值
# 打印结果:shape:torch.Size([5]); tensor([ 2., 4., 6., 8., 10.])
第1行:
- 第1行:我们在定义Tensor时,如果加入requires_grad,表明我们需要求对其的微分;
- 第2行:计算函数值,在实际应用中就是神经网络的前向传播过程;
- 第4行:调用自动微分,就是实际应用中的神经网络的反向传播过程;
我们可以看到打印的结果与我们用解析法求出的内容一致。
向量对向量微分
实际上向量对向量微分叫做Jacobian矩阵,向量
y
∈
R
m
\boldsymbol{y} \in R^{m}
y∈Rm对向量
x
∈
R
n
\boldsymbol{x} \in R^{n}
x∈Rn的微分定义为:
∂
y
∂
x
=
[
∂
y
1
∂
x
1
∂
y
1
∂
x
2
.
.
.
∂
y
1
∂
x
n
∂
y
2
∂
x
1
∂
y
2
∂
x
2
.
.
.
∂
y
2
∂
x
n
.
.
.
.
.
.
.
.
.
.
.
.
∂
y
m
∂
x
1
∂
y
m
∂
x
2
.
.
.
∂
y
m
∂
x
n
]
∈
R
m
×
n
\frac{ \partial{\boldsymbol{y}} }{ \partial{\boldsymbol{x}} } = \begin{bmatrix} \frac{\partial{y_{1}}}{\partial{x_{1}}} & \frac{\partial{y_{1}}}{\partial{x_{2}}} & ... & \frac{\partial{y_{1}}}{\partial{x_{n}}} \\ \frac{\partial{y_{2}}}{\partial{x_{1}}} & \frac{\partial{y_{2}}}{\partial{x_{2}}} & ... & \frac{\partial{y_{2}}}{\partial{x_{n}}} \\ ... & ... & ... & ... \\ \frac{\partial{y_{m}}}{\partial{x_{1}}} & \frac{\partial{y_{m}}}{\partial{x_{2}}} & ... & \frac{\partial{y_{m}}}{\partial{x_{n}}} \\ \end{bmatrix} \in R^{m \times n}
∂x∂y=⎣⎢⎢⎢⎡∂x1∂y1∂x1∂y2...∂x1∂ym∂x2∂y1∂x2∂y2...∂x2∂ym............∂xn∂y1∂xn∂y2...∂xn∂ym⎦⎥⎥⎥⎤∈Rm×n
我们假设有如下向量,例如是神经网络某层的输入向量:
z
l
=
[
1.0
2.0
3.0
4.0
5.0
]
\boldsymbol{z}^{l}=\begin{bmatrix} 1.0 \\ 2.0 \\ 3.0 \\ 4.0 \\ 5.0 \end{bmatrix}
zl=⎣⎢⎢⎢⎢⎡1.02.03.04.05.0⎦⎥⎥⎥⎥⎤
经过该层的激活函数(sigmoid函数)之后,得到本层的输出向量:
a
l
=
[
z
1
2
z
2
2
z
1
3
z
1
4
z
1
5
]
\boldsymbol{a}^{l}=\begin{bmatrix} z_{1}^{2} \\ z_{2}^{2} \\ z_{1}^{3} \\ z_{1}^{4} \\ z_{1}^{5} \\ \end{bmatrix}
al=⎣⎢⎢⎢⎢⎡z12z22z13z14z15⎦⎥⎥⎥⎥⎤
因为本神经元的输入只会影响本神经元的输出,因此我们的
a
l
\boldsymbol{a}^{l}
al如上所示。根据解析法可得其Jacobian矩阵为:
∂
a
l
∂
z
l
=
[
∂
a
1
l
∂
z
1
l
0
0
0
0
0
∂
a
2
l
∂
z
2
l
0
0
0
0
0
∂
a
3
l
∂
z
3
l
0
0
0
0
0
∂
a
3
l
∂
z
3
l
0
0
0
0
0
∂
a
3
l
∂
z
3
l
]
\frac{ \partial{\boldsymbol{a}^{l}} }{ \partial{\boldsymbol{z}^{l}} }=\begin{bmatrix} \frac{ \partial{a}_{1}^{l} }{ \partial{z}_{1}^{l} } & 0 & 0 & 0 & 0 \\ 0 & \frac{ \partial{a}_{2}^{l} }{ \partial{z}_{2}^{l} } & 0 & 0 & 0 \\ 0 & 0 & \frac{ \partial{a}_{3}^{l} }{ \partial{z}_{3}^{l} } & 0 & 0 \\ 0 & 0 & 0 & \frac{ \partial{a}_{3}^{l} }{ \partial{z}_{3}^{l} } & 0 \\ 0 & 0 & 0 & 0 & \frac{ \partial{a}_{3}^{l} }{ \partial{z}_{3}^{l} } \end{bmatrix}
∂zl∂al=⎣⎢⎢⎢⎢⎢⎢⎢⎢⎡∂z1l∂a1l00000∂z2l∂a2l00000∂z3l∂a3l00000∂z3l∂a3l00000∂z3l∂a3l⎦⎥⎥⎥⎥⎥⎥⎥⎥⎤
其实际上只有对角线上有值,因此在PyTorch中,为了更高效的处理问题,我们通常不需要原始的Jacobian矩阵,而是将其乘以一个与
a
l
\boldsymbol{a}^{l}
al同维且元素均为1的向量,得到的结果向量的元素为对角线上的元素。
∂
a
l
∂
z
l
⋅
[
1.0
1.0
1.0
1.0
1.0
]
=
[
∂
a
1
l
∂
z
1
l
0
0
0
0
0
∂
a
2
l
∂
z
2
l
0
0
0
0
0
∂
a
3
l
∂
z
3
l
0
0
0
0
0
∂
a
3
l
∂
z
3
l
0
0
0
0
0
∂
a
3
l
∂
z
3
l
]
⋅
[
1.0
1.0
1.0
1.0
1.0
]
=
⋅
[
∂
a
1
l
∂
z
1
l
∂
a
2
l
∂
z
2
l
∂
a
2
l
∂
z
3
l
∂
a
4
l
∂
z
4
l
∂
a
5
l
∂
z
5
l
]
\frac{ \partial{\boldsymbol{a}^{l}} }{ \partial{\boldsymbol{z}^{l}} } \cdot \begin{bmatrix} 1.0 \\ 1.0 \\ 1.0 \\ 1.0 \\ 1.0 \end{bmatrix}=\begin{bmatrix} \frac{ \partial{a}_{1}^{l} }{ \partial{z}_{1}^{l} } & 0 & 0 & 0 & 0 \\ 0 & \frac{ \partial{a}_{2}^{l} }{ \partial{z}_{2}^{l} } & 0 & 0 & 0 \\ 0 & 0 & \frac{ \partial{a}_{3}^{l} }{ \partial{z}_{3}^{l} } & 0 & 0 \\ 0 & 0 & 0 & \frac{ \partial{a}_{3}^{l} }{ \partial{z}_{3}^{l} } & 0 \\ 0 & 0 & 0 & 0 & \frac{ \partial{a}_{3}^{l} }{ \partial{z}_{3}^{l} } \end{bmatrix} \cdot \begin{bmatrix} 1.0 \\ 1.0 \\ 1.0 \\ 1.0 \\ 1.0 \end{bmatrix}= \cdot \begin{bmatrix} \frac{ \partial{a}_{1}^{l} }{ \partial{z}_{1}^{l} } \\ \frac{ \partial{a}_{2}^{l} }{ \partial{z}_{2}^{l} } \\ \frac{ \partial{a}_{2}^{l} }{ \partial{z}_{3}^{l} } \\ \frac{ \partial{a}_{4}^{l} }{ \partial{z}_{4}^{l} } \\ \frac{ \partial{a}_{5}^{l} }{ \partial{z}_{5}^{l} } \end{bmatrix}
∂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))
# 输出:y_x:tensor([ 2., 4., 6., 8., 10.])
向量对矩阵求微分
在深度学习中,经常会出现上一层的输入向量
z
l
∈
R
N
l
\boldsymbol{z}^{l} \in R^{N_{l}}
zl∈RNl对连接权值
W
l
∈
R
N
l
×
N
l
−
1
W^{l} \in R^{N_{l} \times N_{l-1}}
Wl∈RNl×Nl−1的微分,公式为:
z
l
=
W
l
⋅
a
l
−
1
+
b
l
\boldsymbol{z}^{l}=W^{l} \cdot \boldsymbol{a}^{l-1} + \boldsymbol{b}^{l}
zl=Wl⋅al−1+bl
其中
a
l
−
1
∈
R
N
l
−
1
\boldsymbol{a}^{l-1} \in R^{N_{l-1}}
al−1∈RNl−1,
b
l
∈
R
N
l
\boldsymbol{b}^{l} \in R^{N_{l}}
bl∈RNl。
通过解析法,我们可以得到其微分公式为:
∂
z
l
∂
W
l
=
[
a
1
l
−
1
a
2
l
−
1
.
.
.
a
l
−
1
l
−
1
a
1
l
−
1
a
2
l
−
1
.
.
.
a
l
−
1
l
−
1
.
.
.
.
.
.
.
.
.
.
.
.
a
1
l
−
1
a
2
l
−
1
.
.
.
a
l
−
1
l
−
1
]
=
[
(
a
l
−
1
)
T
(
a
l
−
1
)
T
.
.
.
(
a
l
−
1
)
T
]
∈
R
N
l
×
N
l
−
1
\frac{ \partial{ \boldsymbol{z}^{l} } }{ \partial{W^{l}} } = \begin{bmatrix} a_{1}^{l-1} & a_{2}^{l-1} & ... & a_{l-1}^{l-1} \\ a_{1}^{l-1} & a_{2}^{l-1} & ... & a_{l-1}^{l-1} \\ ... & ... & ... & ... \\ a_{1}^{l-1} & a_{2}^{l-1} & ... & a_{l-1}^{l-1} \\ \end{bmatrix} = \begin{bmatrix} (\boldsymbol{a}^{l-1})^{T} \\ (\boldsymbol{a}^{l-1})^{T} \\ ... \\ (\boldsymbol{a}^{l-1})^{T} \end{bmatrix} \in R^{ N_{l} \times N_{l-1} }
∂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.]])
'''
上述程序的打印结果与理论分析的结果一致,证明我们的求法是正确的。