[入门]2. 自动求导

自动求导(Autograd)

在进行深度学习的过程中,我们时常需要进行求导操作。求导的操作可以自动进行完成,下面我们首先给出一个简单的例子来看一看,求导这一过程是怎么完成的。 我们给出如下的一个数学式子: \[ f = \frac{a * b + c}{d} \] 对f进行求导的公式就如下: \[ \frac{df}{da} = \frac{b}{d}\\ \frac{df}{db} = \frac{a}{d}\\ \frac{df}{dc} = \frac{1}{d}\\ \frac{df}{dd} = -\frac{a * b + c}{d^2} \]

1
import torch
1
2
3
4
5
6
7
8
9
10
a = torch.FloatTensor([2])
b = torch.FloatTensor([4])
c = torch.FloatTensor([6])
d = torch.FloatTensor([8])
print(a.requires_grad, b.requires_grad, c.requires_grad, d.requires_grad)
a.requires_grad = True
b.requires_grad = True
d.requires_grad_(True)
f = (a * b + c) / d
print(a.requires_grad, b.requires_grad, c.requires_grad, d.requires_grad, f.requires_grad)
False False False False
True True False True True
1
2
f.backward()
print(a.grad, b.grad, c.grad, d.grad)
tensor([0.5000]) tensor([0.2500]) None tensor([-0.2188])

1. 设置Tensor

每一个张量都有几个非常重要的属性:

  • requires_grad
  • grad_fn
  • grad

requires_grad是一个布尔值,当它被设置为True时,它的所有操作都会被跟踪。当完成计算后,你可以在最终的计算结果Tensor上调用.backward()函数,会将所有requires_grad为True的Tensor的导数计算出来,计算结果可以通过.grad来访问到。

grad_fn属性会指明创建该Tensor的是一个什么样的操作函数,如果是用户自定义的Tensor,改属性就是None。

设置requires_grad可以通过直接设置,还可以通过requires_grad_(bool)函数来进行。不过在诸如oneszeorsrand等工厂方法中,有一个requires_grad参数可以直接用于指定:

1
2
x = torch.ones(2, 2, requires_grad=True)
print(x)
tensor([[1., 1.],
        [1., 1.]], requires_grad=True)

如果y是通过x创建的,而x的requires_grad为True,那么y的requires_grad也为True。

1
2
3
4
y = x + 2
print(y)
print(y.requires_grad)
print(y.grad_fn)
tensor([[3., 3.],
        [3., 3.]], grad_fn=<AddBackward0>)
True
<AddBackward0 object at 0x00000213D63ABAC8>

2. 求导

现在比如我们有 Tensor a ,它的requires_grad属性也为True。经过一系列运算得到了 f : \(f = f(g(...h(a)...))\),那么只要我们调用f.backward(),就可以把导数计算出来了。

2.1 f 是标量时

在一开始我们给出了一个例子,是若干个标量运算得到标量后求导。现在我们再来看看标量运算结果对矩阵的求导情况:

1
2
3
z = y * y * 3
out = z.mean()
print(z, out)
tensor([[27., 27.],
        [27., 27.]], grad_fn=<MulBackward0>) tensor(27., grad_fn=<MeanBackward1>)

现在来计算\(\frac{d\ out}{d\ x}\)。我们知道 \(o = \frac{1}{4}\sum_i z_i\), \(z_i = 3(x_i+2)^2\) and \(z_i\bigr\rvert_{x_i=1} = 27\)。 所以, \(\frac{\partial o}{\partial x_i} = \frac{3}{2}(x_i+2)\), 于是 \(\frac{\partial o}{\partial x_i}\bigr\rvert_{x_i=1} = \frac{9}{2} = 4.5\)

关于标量对矩阵的求导,大体而言就是标量对矩阵中每个元素分别求导。更多内容可以自行参考Matrix calculus

1
2
out.backward()
print(x.grad)
tensor([[4.5000, 4.5000],
        [4.5000, 4.5000]])

2.2 f 不是标量时

实际上在Pytorch中,不允许 Tensor 对 Tensor 求导,只允许标量 Scalar 对张量 Tensor 求导,求导结果是和自变量同型的 Tensor。 实际上我们需要用到求导一般都是最后算出了Loss,然后调用loss.backward(),再更新参数,所以事实上根本不允许求矩阵之间的导数。

然而实际操作中还是允许调用backward的 Tensor 是一个多维张量的,但是需要传入额外参数:

  • tensor.backward(gradient=None, retain_graph=None, create_graph=False)

传入的这个gradient是一个和tensor同型的张量,**假设 x 经过一番计算得到 y,那么 y.backward(w) 求的不是 y 对 x 的导数,而是 l = torch.sum(y*w) 对 x 的导数 **。

1
2
3
4
5
6
7
W = torch.Tensor([[1, 1, 1], [2, 2, 2]])
W.requires_grad = True
x = torch.Tensor([[1, 3, 5], [2, 4, 6]])
print(W)
print(x)
z = W * x
print(z)
tensor([[1., 1., 1.],
        [2., 2., 2.]], requires_grad=True)
tensor([[1., 3., 5.],
        [2., 4., 6.]])
tensor([[ 1.,  3.,  5.],
        [ 4.,  8., 12.]], grad_fn=<MulBackward0>)
1
2
z.backward(torch.Tensor([[1, 1, 1], [2, 3, 4]]))
print(W.grad)
tensor([[ 1.,  3.,  5.],
        [ 4., 12., 24.]])