自动求梯度
前言
神经网络就是寻求一个拟合函数,但是因为参数过多,所以不得不借助每一点的梯度来一点一点的接近最佳的LOSS值,pytorch拥有动态的计算图,存储记忆对向量的每一个函数操作,最后通过反向传播来计算梯度,这可以说是pytorch的核心。
所以深入了解如果利用pytorch进行自动梯度计算非常重要。
深度学习模型的训练就是不断更新权值,权值的更新需要求解梯度。
PyTorch提供的
autograd
包能够根据输⼊
和前向传播过程自动构建计算图,并执行反向传播。
本节将介绍如何使⽤用autograd
包来进行自动求梯度的有关操作。
概念
上一节介绍的 Tensor
是这个包的核心类,如果将其属性 .requires_grad
设置为 True
,它将开始追
踪(track)在其上的所有操作(这样就可以利用链式法则进行梯度传播了)。
完成计算后,可以调用 .backward()
来完成所有梯度计算。此 Tensor 的梯度将累积到 .grad
属性中。
如果不想要被继续追踪,可以调用 .detach()
将其从追踪记录中分离出来,这样就可以防止将来的计算被追踪,这样梯度就传不过去了。
此外,还可以用 with torch.no_grad()
将不想被追踪的操作代码块包裹起来,这种方法在评估模型的时候很常用,因为在评估模型时,我们并不需要计算可训练参数
( requires_grad=True
)的梯度。
Function
是另外一个很重要的类。 Tensor
和 Function
互相结合就可以构建一个记录有整个计算过程的有向无环图(DAG)。
每个 Tensor
都有一个 .grad_fn
属性,该属性即创建该 Tensor
的Function
, 就是说该 Tensor
是不是通过某些运算得到的,若是,则 grad_fn
返回一个与这些运算相关的对象,否则是None
。
下面通过⼀些例子来理解这些概念。
Tensor
创建⼀一个 Tensor
并设置 requires_grad=True
:
x = torch.ones(2, 2, requires_grad=True)
print(x)
print(x.grad_fn)
tensor([[1., 1.],
[1., 1.]], requires_grad=True)
None
再做一下运算操作:
y = x + 2
print(y)
print(y.grad_fn)
输出:
tensor([[3., 3.],
[3., 3.]], grad_fn=<AddBackward>)
<AddBackward object at 0x1100477b8>
注意x是直接创建的,所以它没有
grad_fn
, ⽽而y是通过一个加法操作创建的,所以它有一个为
<AddBackward>
的grad_fn
。
像x这种直接创建的称为叶子节点,叶子节点对应的 grad_fn
是 None
。
print(x.is_leaf, y.is_leaf) # True False
关于复杂度运算操作:
z = y * y * 3
out = z.mean()
print(z, out)
输出:
tensor([[27., 27.],
[27., 27.]], grad_fn=<MulBackward>) tensor(27., grad_fn=
<MeanBackward1>
通过 .requires_grad_()
来用in-place(就地操作)的方式改变 requires_grad
属性:
a = torch.randn(2, 2) # 缺失情况下默认 requires_grad = False
a = ((a * 3) / (a - 1))
print(a.requires_grad) # False
a.requires_grad_(True)
print(a.requires_grad) # True
b = (a * a).sum()
print(b.grad_fn)
输出:
False
True
<SumBackward0 object at 0x118f50cc0>
梯度
因为 out
是一个标量,所以调用 backward()
时不需要指定求导变量:
out.backward() # 等价于 out.backward(torch.tensor(1.))
我们来看看 out
关于 x
的梯度
d
(
o
u
t
)
d
x
\frac{d(o u t)}{d x}
dxd(out):
print(x.grad)
输出:
tensor([[4.5000, 4.5000],
[4.5000, 4.5000]])
我们令 out
为
o
o
o , 因为
o
=
1
4
∑
i
=
1
4
z
i
=
1
4
∑
i
=
1
4
3
(
x
i
+
2
)
2
o=\dfrac{1}{4}\sum_{i=1}^4z_i=\dfrac{1}{4}\sum_{i=1}^43(x_i+2)^2\quad\quad
o=41i=1∑4zi=41i=1∑43(xi+2)2
所以
∂
o
∂
x
i
∣
x
i
=
1
=
9
2
=
4.5
\quad\left.\dfrac{\partial o}{\partial x_i}\right|_{x_i=1}=\dfrac{9}{2}=4.5
∂xi∂o
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}&\cdots&\frac{\partial y_1}{\partial x_n}\\ \vdots&\ddots&\vdots\\ \frac{\partial y_{m}}{\partial x_1}&\cdots&\frac{\partial y_{m}}{\partial x_n}\end{pmatrix}
J=
∂x1∂y1⋮∂x1∂ym⋯⋱⋯∂xn∂y1⋮∂xn∂ym
⽽而 torch.autograd
这个包就是用来计算⼀些雅克比矩阵的乘积的。例如,如果
v
v
v是⼀个标量函数
l
=
g
(
y
⃗
)
l=g\left(\vec{y}\right)
l=g(y)
的梯度:
v
=
(
∂
l
∂
y
1
⋯
∂
l
∂
y
m
)
v=\left(\begin{array}{cc}\frac{\partial l}{\partial y_1}&\cdots&\frac{\partial l}{\partial y_m}\end{array}\right)\quad\quad\quad
v=(∂y1∂l⋯∂ym∂l)
那么根据链式法则我们有
l
l
l关于
x
⃗
\vec{x}
x的雅克比矩阵就为:
v
J
=
(
∂
t
∂
y
1
⋯
∂
t
∂
y
n
)
(
∂
y
1
∂
x
1
⋯
∂
y
n
∂
x
n
⋮
⋱
⋮
∂
y
n
∂
x
1
⋯
∂
y
n
∂
x
n
)
=
(
∂
t
∂
x
1
⋯
∂
t
∂
x
n
)
vJ=\begin{pmatrix}\frac{\partial t}{\partial y_1}&\cdots&\frac{\partial t}{\partial y_n}\end{pmatrix}\begin{pmatrix}\frac{\partial y_1}{\partial x_1}&\cdots&\frac{\partial y_n}{\partial x_n}\\ \vdots&\ddots&\vdots\\ \frac{\partial y_n}{\partial x_1}&\cdots&\frac{\partial y_n}{\partial x_n}\end{pmatrix}=\begin{pmatrix}\frac{\partial t}{\partial x_1}&\cdots&\frac{\partial t}{\partial x_n}\end{pmatrix}
vJ=(∂y1∂t⋯∂yn∂t)
∂x1∂y1⋮∂x1∂yn⋯⋱⋯∂xn∂yn⋮∂xn∂yn
=(∂x1∂t⋯∂xn∂t)
注意:grad在反向传播过程中是累加的(accumulated),这意味着每一次运行反向传播,梯度都会累加之前的梯度,所以一般在反向传播之前需把梯度清零。
# 再来反向传播⼀一次,注意grad是累加的
out2 = x.sum()
out2.backward()
print(x.grad)
out3 = x.sum()
x.grad.data.zero_()
out3.backward()
print(x.grad)
输出为:
tensor([[5.5000, 5.5000],
[5.5000, 5.5000]])
tensor([[1., 1.],
[1., 1.]])
此外,如果我们想要修改 tensor
的数值,但是又不希望被 autograd
记录(即不会影响反向传播),
那么我们可以对 tensor.data
进行操作。
x = torch.ones(1,requires_grad=True)
print(x.data) # 还是⼀一个tensor
print(x.data.requires_grad) # 但是已经是独⽴立于计算图之外
y = 2 * x
x.data *= 100 # 只改变了了值,不不会记录在计算图,所以不不会影响梯度传播
y.backward()
print(x) # 更更改data的值也会影响tensor的值
print(x.grad)
输出为:
tensor([1.])
False
tensor([100.], requires_grad=True)
tensor([2.])