自动求导#

看这个文章的时候最好先看下这个视频https://www.bilibili.com/video/BV1KA411N7Px?p=2

Autograd简介#

torch.Tensor是这个包的核心类。如果设置它的属性.requires_gradTrue,那么它将会追踪对于该张量的所有操作。当完成计算后可以通过调用.backward(),来自动计算所有的梯度。这个张量的所有梯度将会自动累加到.grad属性。

注意:在 y.backward() 时,如果 y 是标量,则不需要为 backward() 传入任何参数;否则,需要传入一个与 y 同形的Tensor。

要阻止一个张量被跟踪历史,可以调用.detach()方法将其与计算历史分离,并阻止它未来的计算记录被跟踪。为了防止跟踪历史记录(和使用内存),可以将代码块包装在 with torch.no_grad():中。在评估模型时特别有用,因为模型可能具有 requires_grad = True 的可训练的参数,但是我们不需要在此过程中对他们进行梯度计算。

还有一个类对于autograd的实现非常重要:FunctionTensorFunction 互相连接生成了一个无环图 (acyclic graph),它编码了完整的计算历史。每个张量都有一个.grad_fn属性,该属性引用了创建 Tensor自身的Function(除非这个张量是用户手动创建的,即这个张量的grad_fnNone )。下面给出的例子中,张量由用户手动创建,因此grad_fn返回结果是None。

PS:关于这个有一个更深入的可以看看:https://www.youtube.com/watch?v=MswxJw-8PvE 视频中的图:draw.io

import torch
x = torch.randn(3,3,requires_grad=True)
print(f"x.grad_fn:{x.grad_fn}")
x.grad_fn:None

如果需要计算导数,可以在 Tensor 上调用 .backward()。如果Tensor是一个标量(即它包含一个元素的数据),则不需要为 backward()指定任何参数,但是如果它有更多的元素,则需要指定一个gradient参数,该参数是形状匹配的张量。

创建一个张量并设置requires_grad=True用来追踪其计算历史

x = torch.randn(3,4,requires_grad=True)
print(f"x:{x}")
x:tensor([[-0.0405,  2.1663, -1.0016,  2.3026],
        [ 0.5074,  1.2494, -1.0841, -1.3393],
        [ 1.0113,  1.5474,  0.4087,  1.0646]], requires_grad=True)

对这个张量做一次运算:

y = x**2
print(f"y:{y}")
y:tensor([[1.6394e-03, 4.6930e+00, 1.0031e+00, 5.3017e+00],
        [2.5744e-01, 1.5609e+00, 1.1753e+00, 1.7937e+00],
        [1.0227e+00, 2.3945e+00, 1.6706e-01, 1.1334e+00]],
       grad_fn=<PowBackward0>)

y是计算的结果,所以它有grad_fn属性。

print(f"y.grad_fn:{y.grad_fn}")
y.grad_fn:<PowBackward0 object at 0x7f8380e14250>

y进行更多操作

z = y * y * 3
out = z.mean() #求平均值
print(f"z.grad_fn:{z.grad_fn}")
print(f"z:{z}")
print(f"out:{out}")
z.grad_fn:<MulBackward0 object at 0x7f83a10bb850>
z:tensor([[8.0624e-06, 6.6073e+01, 3.0187e+00, 8.4325e+01],
        [1.9882e-01, 7.3093e+00, 4.1440e+00, 9.6524e+00],
        [3.1374e+00, 1.7200e+01, 8.3729e-02, 3.8535e+00]],
       grad_fn=<MulBackward0>)
out:16.583065032958984

.requires_grad_(...)函数可以修改现有张量的requires_grad 标志。如果没有指定的话,默认输入的这个标志是False

a = torch.randn(2, 2) # 缺失情况下默认 requires_grad = False
a = ((a * 3) / (a - 1))
print(f"a.requires_grad:{a.requires_grad}")
a.requires_grad_(True)
print(f"a.requires_grad:{a.requires_grad}")
b = (a * a).sum()
print(f"b.grad_fn:{b.grad_fn}")
a.requires_grad:False
a.requires_grad:True
b.grad_fn:<SumBackward0 object at 0x7f83a10bb850>

梯度#

现在开始进行反向传播,因为out 是一个标量,因此out.backward()out.backward(torch.tensor(1.)) 等价。

求的是最上层x的梯度

print(f"out:{out!r}")
out.backward()
out:tensor(16.5831, grad_fn=<MeanBackward0>)

输出导数\(\frac{d(out)}{dx}\)

print(f"x.grad:{x.grad}")
x.grad:tensor([[-6.6376e-05,  1.0167e+01, -1.0047e+00,  1.2208e+01],
        [ 1.3062e-01,  1.9501e+00, -1.2742e+00, -2.4023e+00],
        [ 1.0342e+00,  3.7052e+00,  6.8284e-02,  1.2066e+00]])

假设某函数从\(\mathbf{f}: \mathbb{R}^{n} \rightarrow \mathbb{R}^{m}\),从\(\mathbf{x} \in \mathbb{R}^{n}\)映射到向量\(\mathbf{f}(\mathbf{x}) \in \mathbb{R}^{m}\),其雅可比矩阵是一\(m \times n\)的矩阵,换句话讲也就是从\(\mathbb{R}^{n}\)\(\mathbb{R}^{m}\)的线性映射,其重要意义在于它表现了一个多变数向量函数的最佳线性逼近。因此,雅可比矩阵类似于单变数函数的导数。

此函数\(\mathbf{f}\)的雅可比矩阵\(\mathbf{J}\)\(m \times n\)的矩阵,一般由以下方式定义:

\(\mathbf{J}=\left[\begin{array}{ccc} \frac{\partial \mathbf{f}}{\partial x_{1}} & \cdots & \frac{\partial \mathbf{f}}{\partial x_{n}} \end{array}\right]=\left[\begin{array}{ccc} \frac{\partial f_{1}}{\partial x_{1}} & \cdots & \frac{\partial f_{1}}{\partial x_{n}} \\ \vdots & \ddots & \vdots \\ \frac{\partial f_{m}}{\partial x_{1}} & \cdots & \frac{\partial f_{m}}{\partial x_{n}} \end{array}\right]\)

矩阵的分量可表示成:

\(\mathbf{J}_{i j}=\frac{\partial f_{i}}{\partial x_{j}}\)

注意:grad在反向传播过程中是累加的(accumulated),这意味着每一次运行反向传播,梯度都会累加之前的梯度,所以一般在反向传播之前需把梯度清零。

# 再来反向传播⼀一次,注意grad是累加的
out2 = x.sum()
out2.backward()
print(f"x.grad:{x.grad}")

out3 = x.sum()
x.grad.data.zero_()
out3.backward()
print(f"x.grad:{x.grad}")
x.grad:tensor([[ 9.9993e-01,  1.1167e+01, -4.6653e-03,  1.3208e+01],
        [ 1.1306e+00,  2.9501e+00, -2.7416e-01, -1.4023e+00],
        [ 2.0342e+00,  4.7052e+00,  1.0683e+00,  2.2066e+00]])
x.grad:tensor([[1., 1., 1., 1.],
        [1., 1., 1., 1.],
        [1., 1., 1., 1.]])

现在我们来看一个雅可比向量积的例子:

x = torch.randn(3, requires_grad=True)
print(f"x:{x}")

y = x * 2
i = 0
while y.data.norm() < 1000:
    y = y * 2
    i = i + 1
print(f"y:{y}")
print(f"i:{i}")
x:tensor([0.6968, 0.2496, 0.4571], requires_grad=True)
y:tensor([1426.9945,  511.0865,  936.1667], grad_fn=<MulBackward0>)
i:10

在这种情况下,y 不再是标量。torch.autograd 不能直接计算完整的雅可比矩阵,但是如果我们只想要雅可比向量积,只需将这个向量作为参数传给 backward:

v = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float)
y.backward(v)

print(f"x.grad:{x.grad}")
x.grad:tensor([2.0480e+02, 2.0480e+03, 2.0480e-01])

也可以通过将代码块包装在 with torch.no_grad(): 中,来阻止 autograd 跟踪设置了.requires_grad=True的张量的历史记录。

print(f"x.requires_grad:{x.requires_grad}")
print(f"(x ** 2).requires_grad:{(x ** 2).requires_grad}")

with torch.no_grad():
    print(f"(x ** 2).requires_grad:{(x ** 2).requires_grad}")
x.requires_grad:True
(x ** 2).requires_grad:True
(x ** 2).requires_grad:False

如果我们想要修改 tensor 的数值,但是又不希望被 autograd 记录(即不会影响反向传播), 那么我们可以对 tensor.data 进行操作。

x = torch.ones(1,requires_grad=True)
print(f"x.data:{x.data}")  # 还是一个tensor
print(f"x.data.requires_grad:{x.data.requires_grad}")  # 但是已经是独立于计算图之外
y = 2 * x
x.data *= 100 # 只改变了值,不会记录在计算图,所以不会影响梯度传播

y.backward()
print(f"x:{x}")  # 更改data的值也会影响tensor的值 
print(f"x.grad:{x.grad}")

#释放张量
del x
del y
x.data:tensor([1.])
x.data.requires_grad:False
x:tensor([100.], requires_grad=True)
x.grad:tensor([2.])

举个自动求导的例子#

标量变量的反向传播#

PyTorch中,torch.Tensor是存储和变换数据的主要工具。如果你之前用过NumPy,你会发现Tensor和NumPy的多维数组非常类似。然而,Tensor提供GPU计算自动求梯度等更多功能,这些使Tensor更加适合深度学习。

如果将PyTorch中的tensor的requires_grad属性设置为True,它将开始追踪(track) 在其上的所有操作(这样就可以利用链式法则进行梯度传播了)。完成计算后,可以调用backward()来完成所有梯度计算。此tensor的梯度将累积到grad属性中。 下面我们来看一个例子。

import torch

x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = x ** 2 + 2
print(f"y:{y}")
z = torch.sum(y)
print(f"z:{z}")
z.backward()
print(f"x.grad:{x.grad}")
y:tensor([ 3.,  6., 11.], grad_fn=<AddBackward0>)
z:20.0
x.grad:tensor([2., 4., 6.])

\(x=\left[x_{1}, x_{2}, x_{3}\right]\),求\(\begin{array}{l}z=x_{1}^{2}+x_{2}^{2}+x_{3}^{2}+6 \end{array}\)

由求偏导的数学知识,可知:

\(\begin{aligned} \frac{\partial z}{\partial x_{1}} & =2 x_{1} \\ \frac{\partial z}{\partial x_{2}} & =2 x_{2} \\ \frac{\partial z}{\partial x_{3}} & =2 x_{3} \end{aligned}\)

然后把\(x_{1}=1.0, x_{2}=2.0, x_{3}=3.0\)代入得到

\(\left(\frac{\partial z}{\partial x_{1}}, \frac{\partial z}{\partial x_{2}}, \frac{\partial z}{\partial x_{3}}\right)=\left(2 x_{1}, 2 x_{2}, 2 x_{3}\right)=(2.0,4.0,6.0)\)

可见结果与PyTorch的输出一致。

再来个例子

这里将通过一个简单的函数 \(y=x_6+2*x_7\) 来说明PyTorch自动求导的过程

import torch

x6 = torch.tensor(1.0, requires_grad=True)
x7 = torch.tensor(2.0, requires_grad=True)
y = x6 + 2*x7
print(f"y:{y}")
y:5.0

首先查看每个变量是否需要求导

print(f"x6.requires_grad:{x6.requires_grad}")
print(f"x7.requires_grad:{x7.requires_grad}")
print(f"y.requires_grad:{y.requires_grad}")
x6.requires_grad:True
x7.requires_grad:True
y.requires_grad:True

查看每个变量导数大小。此时因为还没有反向传播,因此导数都不存在

print(f"x6.grad.data:{x6.grad.data}")
print(f"x7.grad.data:{x7.grad.data}")
print(f"y.grad.data:{y.grad.data}")
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[17], line 1
----> 1 print(f"x6.grad.data:{x6.grad.data}")
      2 print(f"x7.grad.data:{x7.grad.data}")
      3 print(f"y.grad.data:{y.grad.data}")

AttributeError: 'NoneType' object has no attribute 'data'
x6
tensor(1., requires_grad=True)

反向传播后看导数大小

y = x6 + 2*x7
y.backward()
print(f"x6.grad.data:{x6.grad.data}")
print(f"x7.grad.data:{x7.grad.data}")
x6.grad.data:1.0
x7.grad.data:2.0

导数是会累积的,重复运行相同命令,grad会增加

y = x6 + 2*x7
y.backward()
print(f"x6.grad.data:{x6.grad.data}")
print(f"x7.grad.data:{x7.grad.data}")
x6.grad.data:2.0
x7.grad.data:4.0

所以每次计算前需要清除当前导数值避免累积,这一功能可以通过pytorch的optimizer实现

用数学的方式来解题,下面是完整的步骤来解决这个数学问题 \(y = x_1 + 2*x_2\)

  1. 定义变量和方程:设 \(x_1\)\(x_2\) 为变量,\(y\) 为方程。 \(y = x_1 + 2*x_2\)

  2. 给定数值:\(x_1 = 1\)\(x_2 = 2\)

  3. 代入数值计算 \(y\) 的值: \(y = 1 + 2 \cdot 2 = 5\)

    因此,在 \(x_1 = 1\)\(x_2 = 2\) 的点上,\(y\) 的值为 5。

  4. 求解梯度:计算 \(y\)\(x_1\)\(x_2\) 的偏导数。

    \(\frac{\partial y}{\partial x_1} = 1\)

    \(\frac{\partial y}{\partial x_2} = 2\)

    因此,在 \(x_1 = 1\)\(x_2 = 2\) 的点上,\(x_1\) 的梯度为 1,\(x_2\) 的梯度为 2。

非标量变量的反向传播#

同样,先看下面的例子。已知:\(\begin{align}& y_{1}=x_{1}*x_{2}*x_{3} \\& y_{2}=x_{1}+x_{2}+x_{3} \\& y_{3}=x_{1}+x_{2}*x_{3} \\& A=f(y_{1}, y_{2}, y_{3}) \end{align}\)

其中函数\(f(y_{1}, y_{2}, y_{3})\)的具体定义未知,现在求\(x_1,x_2,x_3\)的偏导数:

\(\begin{align} \frac{\partial A}{\partial x_{1}} & =? \\ \frac{\partial A}{\partial x_{2}} & =? \\ \frac{\partial A}{\partial x_{3}} & =? \end{align}\)

根据多元复合函数的求导法则,有:

\(\begin{align}{l} \frac{\partial A}{\partial x_{1}}=\frac{\partial A}{\partial y_{1}} \frac{\partial y_{1}}{\partial x_{1}}+\frac{\partial A}{\partial y_{2}} \frac{\partial y_{2}}{\partial x_{1}}+\frac{\partial A}{\partial y_{3}} \frac{\partial y_{3}}{\partial x_{1}} \\ \frac{\partial A}{\partial x_{2}}=\frac{\partial A}{\partial y_{1}} \frac{\partial y_{1}}{\partial x_{2}}+\frac{\partial A}{\partial y_{2}} \frac{\partial y_{2}}{\partial x_{2}}+\frac{\partial A}{\partial y_{3}} \frac{\partial y_{3}}{\partial x_{2}} \\ \frac{\partial A}{\partial x_{3}}=\frac{\partial A}{\partial y_{1}} \frac{\partial y_{1}}{\partial x_{3}}+\frac{\partial A}{\partial y_{2}} \frac{\partial y_{2}}{\partial x_{3}}+\frac{\partial A}{\partial y_{3}} \frac{\partial y_{3}}{\partial x_{3}} \end{align}\)

上面3个等式可以写成矩阵相乘的形式,如下:

\(\begin{align}\left[\frac{\partial A}{\partial x_{1}}, \frac{\partial A}{\partial x_{2}}, \frac{\partial A}{\partial x_{3}}\right]=\left[\frac{\partial A}{\partial y_{1}}, \frac{\partial A}{\partial y_{2}}, \frac{\partial A}{\partial y_{3}}\right]\left[\begin{array}{ccc} \frac{\partial y_{1}}{\partial x_{1}} & \frac{\partial y_{1}}{\partial x_{2}} & \frac{\partial y_{1}}{\partial x_{3}} \\ \frac{\partial y_{2}}{\partial x_{1}} & \frac{\partial y_{2}}{\partial x_{2}} & \frac{\partial y_{2}}{\partial x_{3}} \\ \frac{\partial y_{3}}{\partial x_{1}} & \frac{\partial y_{3}}{\partial x_{2}} & \frac{\partial y_{3}}{\partial x_{3}} \end{array}\right]\end{align}\)

其中

\(\begin{align}{l} {\left[\begin{array}{lll} \frac{\partial y_{1}}{\partial x_{1}} & \frac{\partial y_{1}}{\partial x_{2}} & \frac{\partial y_{1}}{\partial x_{3}} \\ \frac{\partial y_{2}}{\partial x_{1}} & \frac{\partial y_{2}}{\partial x_{2}} & \frac{\partial y_{2}}{\partial x_{3}} \\ \frac{\partial y_{3}}{\partial x_{1}} & \frac{\partial y_{3}}{\partial x_{2}} & \frac{\partial y_{3}}{\partial x_{3}} \end{array}\right]} \\ \end{align}\)

叫作雅可比(Jacobian)式。雅可比式可以根据已知条件求出。

现在只要知道\(\left[\frac{\partial A}{\partial y_1}, \frac{\partial A}{\partial y_2}, \frac{\partial A}{\partial y_3}\right]\)的值,哪怕不知道\(f\left(y_{1}, y_{2}, y_{3}\right)\)的具体形式也能求出来\(\left[\frac{\partial A}{\partial x_1}, \frac{\partial A}{\partial x_2}, \frac{\partial A}{\partial x_3}\right]\)

那现在的问题是怎么样才能求出

\(\left[\frac{\partial A}{\partial y_{1}}, \frac{\partial A}{\partial y_{2}}, \frac{\partial A}{\partial y_{3}}\right]\)

答案是由PyTorch的backward函数的gradient参数提供。这就是gradient参数的作用。

比如,我们传入gradient参数为torch.tensor([0.1,0.2,0.3], dtype=torch.float),并且假定\(x_{1}=1, x_{2}=2, x_{3}=3\),按照上面的推导方法:

\(\begin{align}\left[\frac{\partial A}{\partial x_{1}}, \frac{\partial A}{\partial x_{2}}, \frac{\partial A}{\partial x_{3}}\right]=\left[\frac{\partial A}{\partial y_{1}}, \frac{\partial A}{\partial y_{2}}, \frac{\partial A}{\partial y_{3}}\right]\left[\begin{array}{ccc} x_{2} x_{3} & x_{1} x_{3} & x_{1} x_{2} \\ 1 & 1 & 1 \\ 1 & x_{3} & x_{2} \end{array}\right]=[0.1,0.2,0.3]\left[\begin{array}{lll} 6 & 3 & 2 \\ 1 & 1 & 1 \\ 1 & 3 & 2 \end{array}\right]=[1.1,1.4,1.0]\end{align}\)

我们可以用代码验证一下:

import torch

x1 = torch.tensor(1, requires_grad=True, dtype=torch.float)
x2 = torch.tensor(2, requires_grad=True, dtype=torch.float)
x3 = torch.tensor(3, requires_grad=True, dtype=torch.float)

x = torch.tensor([x1, x2, x3])
y = torch.randn(3)
print(f"y:{y}")
y[0] = x1 * x2 * x3
y[1] = x1 + x2 + x3
y[2] = x1 + x2 * x3
print(f"y:{y}")
y.backward(torch.tensor([0.1, 0.2, 0.3], dtype=torch.float))

print(f"x1.grad:{x1.grad}")
print(f"x2.grad:{x2.grad}")
print(f"x3.grad:{x3.grad}")
y:tensor([ 1.2616, -0.3324,  1.1798])
y:tensor([6., 6., 7.], grad_fn=<CopySlices>)
x1.grad:1.100000023841858
x2.grad:1.4000000953674316
x3.grad:1.0

由此可见,推导和代码运行结果一致。

\(y\)不是标量时,向量\(y\)关于向量\(x\)的导数的最自然解释是⼀个矩阵对于高阶和高维的\(y\)\(x\),求导的结果可以是⼀个高阶张量

然而,虽然这些更奇特的对象确实出现在高级机器学习中(包括深度学习中),但当我们调用向量的反向计算时,我们通常会试图计算一批训练样本中每个组成部分的(这里我的理解是关于非标量\(y\)的各个维度的)损失函数的导数。这里,我们的目的不是计算微分矩阵,而是单独计算批量中每个样本的(这里我的理解是关于非标量\(y\)的各个维度的)偏导数之和。对非标量,调⽤backward,需要传⼊⼀个gradient参数,该参数指定微分函数关于self的梯度

import torch
x = torch.tensor([0., 1., 2., 3.], requires_grad=True)
y = x * x
# 等价于y.sum().backward()
y.backward(torch.ones(len(x)))
print(f"x.grad:{x.grad}")
x.grad:tensor([0., 2., 4., 6.])