自动微分
Pytorch中所有神经网络的核心是autograd
包,我们先简单的来了解下这个包,然后来训练第一个神经网络。
autograd
包为tensor上所有操作提供了自动微分功能。Pytorch是一个先运行后定义(define-by-run)的网络框架,是一种动态网络图结构,因此代码如何运行决定了如何计算反向传播,并且每次迭代,反向传播都可能不同。
张量(Tensor)
torch.Tensor
是autograd
包的核心,如果把.requires_grad
设置为True
,那么它就会跟踪tensor上的所有操作,计算完之后调用.backward()
自动计算梯度,这个tensor的梯度将会累积到.grad
属性中。
如果要停止tensor追踪操作历史,可以调用.detach()
,这样在以后的计算中阻止计算操作被追踪。
停止追踪操作历史同样还可以将代码块放进with torch.no_grad():
中,这种做法在模型评估时候非常有帮助,因为模型可能包含requires_grad=True
的可训练参数,但是我们并不需要它们的梯度值。
自动微分实现中还有一个非常重要类—— Function
。
Tensor
和Function
是互联的,它们组成一个非循环图,记录了完整的计算过程。每个tensor都有一个.grad_fn
属性指向了创建这个Tensor
的Function
(用户自己创建的Tensor类型除外,它们的grad_fn
为None
)。
如果想要计算Tensor
的导数,可以调用.backward()
。如果tensor是标量(即它包含一个元素),那么不需要为backward()
指定任何参数,如果有多个元素,那么就需要指定梯度参数。
现在我们创建一个tensor并设置requires_grad=True
来追踪计算。
x = torch.ones(2, 2, requires_grad=True)
print(x)
结果:
tensor([[1., 1.],
[1., 1.]], requires_grad=True)
再来做一个tensor操作把:
y = x + 2
print(y)
结果:
tensor([[3., 3.],
[3., 3.]], grad_fn=<AddBackward0>)
y是由一个操作得出来的,因此它有grad_fn
属性:
print(y.grad_fn)
结果:
<AddBackward0 object at 0x00000240692EF470>
在y上再做一些操作:
z = y * y * 3
out = z.mean()
print(z, out)
结果:
tensor([[27., 27.],
[27., 27.]], grad_fn=<MulBackward0>) tensor(27., grad_fn=<MeanBackward1>)
requires_grad
属性默认为False
。
a = torch.randn(2, 2)
a = ((a * 3) / (a - 1))
print(a.requires_grad)
a.requires_grad_(True)
print(a.requires_grad)
b = (a * a).sum()
print(b.grad_fn)
结果:
False
True
<SumBackward0 object at 0x000001D3B315F438>
梯度(Gradients)
现在我们进行反向传播计算梯度,因为out
只包含一个标量,out.backward()
与out.backward(torch.tensor(1.))
等价。
out.backward()
现在计算d(out)/d(x)
print(x.grad)
结果:
tensor([[4.5000, 4.5000],
[4.5000, 4.5000]])
那么4.5这个数值是如何得到的呢,下面是计算过程:
o
u
t
=
1
4
∑
i
z
i
out=\frac{1}{4} \sum_{i} z_i
out=41i∑zi
z
i
=
3
y
i
2
=
3
(
x
i
+
2
)
2
z_i=3y_i^2=3(x_i+2)^2
zi=3yi2=3(xi+2)2
∂
o
u
t
∂
x
i
=
3
2
(
x
i
+
2
)
\frac{\partial out}{\partial x_i} = \frac{3}{2} (x_i+2)
∂xi∂out=23(xi+2)
∂
o
u
t
∂
x
i
∣
x
i
=
1
=
9
2
=
4.5
\frac{\partial out}{\partial x_i} |_{x_i=1} = \frac{9}{2} = 4.5
∂xi∂out∣xi=1=29=4.5
从数学上说,对于函数
y
⃗
=
f
(
x
⃗
)
\vec y=f(\vec x)
y=f(x),
y
⃗
\vec y
y关于
x
⃗
\vec x
x的梯度是一个雅可比矩阵(Jacobian matrix
):
J = ( ∂ y 1 ∂ x 1 ⋅ ⋅ ⋅ ∂ y 1 ∂ x n ⋮ ⋱ ⋮ ∂ y m ∂ x 1 ⋅ ⋅ ⋅ ∂ y m ∂ x n ) J = \begin{pmatrix} \frac{\partial y_1}{\partial x_1} & ··· & \frac{\partial y_1}{\partial x_n}\\ ⋮ & ⋱ & ⋮ \\ \frac{\partial y_m}{\partial x_1} & ··· & \frac{\partial y_m}{\partial x_n} \end{pmatrix} J=⎝⎜⎛∂x1∂y1⋮∂x1∂ym⋅⋅⋅⋱⋅⋅⋅∂xn∂y1⋮∂xn∂ym⎠⎟⎞
一般来说torch.autograd
是一个计算向量-雅可比点积的计算引擎。给定任意一个向量
v
=
(
v
1
v
2
⋅
⋅
⋅
v
m
)
T
v = (v_1 \space v_2 \space ··· \space v_m)^T
v=(v1 v2 ⋅⋅⋅ vm)T,计算
v
T
⋅
J
v^T·J
vT⋅J。如果
v
v
v恰好是一个标量函数
l
=
g
(
y
⃗
)
l=g(\vec y)
l=g(y)的梯度,即
v
=
(
∂
l
∂
y
1
⋅
⋅
⋅
∂
l
∂
y
m
)
T
v = (\frac{\partial l}{\partial y_1} \space ··· \space \frac{\partial l}{\partial y_m})^T
v=(∂y1∂l ⋅⋅⋅ ∂ym∂l)T, 那么根据链式法则,向量-雅可比的点积就是
l
l
l关于
x
⃗
\vec x
x的梯度:
J T ⋅ v = ( ∂ y 1 ∂ x 1 ⋅ ⋅ ⋅ ∂ y m ∂ x 1 ⋮ ⋱ ⋮ ∂ y 1 ∂ x n ⋅ ⋅ ⋅ ∂ y m ∂ x n ) ( ∂ l ∂ y 1 ⋮ ∂ l ∂ y m ) = ( ∂ l ∂ x 1 ⋮ ∂ l ∂ x n ) J^T·v = \begin{pmatrix} \frac{\partial y_1}{\partial x_1} & ··· & \frac{\partial y_m}{\partial x_1}\\ ⋮ & ⋱ & ⋮ \\ \frac{\partial y_1}{\partial x_n} & ··· & \frac{\partial y_m}{\partial x_n} \end{pmatrix} \begin{pmatrix} \frac{\partial l}{\partial y_1} \\ ⋮ \\ \frac{\partial l}{\partial y_m} \end{pmatrix} = \begin{pmatrix} \frac{\partial l}{\partial x_1} \\ ⋮ \\ \frac{\partial l}{\partial x_n} \end{pmatrix} JT⋅v=⎝⎜⎛∂x1∂y1⋮∂xn∂y1⋅⋅⋅⋱⋅⋅⋅∂x1∂ym⋮∂xn∂ym⎠⎟⎞⎝⎜⎛∂y1∂l⋮∂ym∂l⎠⎟⎞=⎝⎜⎛∂x1∂l⋮∂xn∂l⎠⎟⎞
注意: v T ⋅ J v^T·J vT⋅J得到的是一个行向量,我们可以通过计算 J T ⋅ v J^T·v JT⋅v来得到它的列向量。
这种向量-雅可比点积的特性使得计算非标量输出的梯度非常方便。
下面来看一个例子:
x = torch.randn(3, requires_grad=True)
y = x * 2
while y.data.norm() < 1000:
y = y * 2
print(y)
结果:
tensor([-358.4211, -803.5598, 780.2765], grad_fn=<MulBackward0>)
可以看出y
已经不是一个标量了,torch.autograd
不能直接计算出雅可比矩阵,不过我们可以在反向传播的时候传入一个向量作为参数来计算向量-雅可比点积:
v = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float)
y.backward(v)
print(x.grad)
结果:
tensor([5.1200e+01, 5.1200e+02, 5.1200e-02])