参考文章: Pytorch 自动求导小结

前言:本文主要介绍了 Pytorch 中的自动求导机制,以及如何使用自动求导机制来实现神经网络的前向传播和反向传播。个人一直对自动求导机制比较困惑,市面上的资料比较简陋,或者讲的不太清楚,今天有幸看到参考文章,感觉写得很好,就增添了一些内容和格式,方便自己以后查阅。

一般来说,深度学习的主要步骤是:

  1. 搭建计算图 (相当于输入自己的公式,一般来说是线性公式 $y=\omega x+b$)
  2. 前向传播 (相当于计算公式的值,即 $y$)
  3. 计算损失 (相当于计算 $y$ 与真实值的差距,即 $loss$)
  4. 反向传播 (相当于计算 $loss$ 对 $\omega$ 和 $b$ 的偏导数,即 $\frac{\partial loss}{\partial \omega}$ 和 $\frac{\partial loss}{\partial b}$)
  5. 更新参数 (相当于更新 $\omega$ 和 $b$,即 $\omega=\omega-\alpha \frac{\partial loss}{\partial \omega}$ 和 $b=b-\alpha \frac{\partial loss}{\partial b}$)

标量求导

标量求导是指,对于一个标量函数 $f(x)$,求 $f(x)$ 对 $x$ 的偏导数,即 $\frac{\partial {f(x)}}{\partial {x}}$。其中不涉及向量和矩阵,只涉及标量,算是后续内容的一个引子,目的是先了解求导到底在做什么。

import torch
# 先行定义一个变量 x
# 注意,此处 x 的值是 5.0,必须有这个小数点和后面的 0,因为torch只会对浮点数进行求导,而不是整数
# torch.tensor() 是 torch 中的一个函数,用于创建一个张量
# requires_grad=True 表示需要对这个张量进行求导,如果设为 False,这会在求导过程中忽略这个张量
# 此处定义的 x 是 leaf variable,即叶子变量,因为它是用户自己定义的,而不是通过其他变量计算得到的,
# 且求导的时候只会求目标变量和叶子变量之间的导数
x = torch.tensor(5.0, requires_grad=True)
# 定义一个简单的表达式 y = 2x^2 + x
# 此处的 y 不是叶子变量,因为它是通过其他变量计算得到的
# y 是本次求导的目标函数,我们希望求出 y 对 x 的导数
y = x**2 + x
print(y)
tensor(30., grad_fn=<AddBackward0>)
# 我们可以看一下现在这两个变量是否都能求导数
print(x.requires_grad)
print(y.requires_grad)
True
True
# 接下来我们调用 pytorch 的自动求导函数 backward(),来求出 y 对 x 的导数
y.backward()
# 此时整个链条上的叶子变量的导数已经被求出来了
# 我们可以通过 x.grad 来查看 x 的导数
print(x.grad)
tensor(11.)

我们可以使用手算的方式来验证一下 pytorch 的计算是否正确。

$$ \begin{aligned} f(x) &= x^2 + x \\ \frac{\partial f(x)}{\partial x} &= 2x + 1 \end{aligned} $$

代入 $x=5$,得到 $f(5)=30$,$\frac{\partial f(5)}{\partial 5}=11$。

标量求导是所有求导里面最简单的一部分,并不涉及向量,但是从上面的代码我们可以简单了解一下 pytorch 的自动求导用法,下面介绍多元函数的标量求导。

w = torch.tensor(2.0, requires_grad=True)
# 由于在 pytorch 中,变量的梯度是累加的,所以我们在每次求导之前,都需要将梯度清零
x.grad.zero_()
y = x**2+x
z = w*y

我们可以简单地画出各个变量之间的依赖关系,如下图所示:

  • z

    • y

      • x
    • w

其中,$z$ 依赖于 $y$ 和 $w$,$y$ 依赖于 $x$。我们可以使用 pytorch 的自动求导机制来计算 $z$ 对 $x$ 的偏导数,即 $\frac{\partial z}{\partial x}$。

使用链式求导法则可以很简单的得到:

$$ \begin{aligned} \frac{\partial z}{\partial x} &= \frac{\partial z}{\partial y} \frac{\partial y}{\partial x} \\ &= \frac{\partial z}{\partial y} \frac{\partial y}{\partial x} \\ &= \frac{\partial z}{\partial y} \frac{\partial (x^2 + x)}{\partial x} \\ &= \frac{\partial z}{\partial y} (2x + 1) \\ &= \omega (2x + 1) \\ \end{aligned} $$

# 与上面相同,我们可以看一下现在这三个变量是否都能求导数
z.backward()
# 此时整个链条上的叶子变量的导数已经被求出来了,如果上面不执行梯度清零的操作,那么最后得到的结果会加上 11.0
x.grad
tensor(22.)


向量求导

在 pytorch 中,标量对向量求导是默认的求导方式,即 $\frac{\partial f(x)}{\partial x}$,其中 $x$ 是一个向量,$f(x)$ 是一个标量。这是因为神经网络中使用求导是在反向传播阶段,而反向传播的依据就是损失函数,为了减少工作量,一般来说损失函数得到的值是一个标量,而参数多是呈矩阵的形式,因此这种就是默认的方式。

在本节中,我们假设一个简单的全连接层,输入为 3 维度,输出是 1 维的,另外还有一个偏置 b。这个简单的神经网络的计算公式如下:

$$ \begin{aligned} y &= \omega x + b \\ loss &= (y - y_{true})^2 \end{aligned} $$

其中 $y$ 是神经网络的输出,$y_{true}$ 是真实值,$loss$ 是损失函数,$x$ 是输入,$\omega$ 是权重,$b$ 是偏置。

# 定义各个变量
# X 是输入的数据,因此不需要求导,而 W 和 b 是需要求导的参数,所以 requires_grad=True,我们需要根据求导结果更新其数值
X = torch.randn(3, requires_grad=False)
W = torch.randn(3, requires_grad=True)
y_true = torch.randn(1, requires_grad=True)
b = torch.randn(1, requires_grad=True)
torch.matmul(X, W)
tensor(1.7565, grad_fn=<DotBackward0>)

# 定义一个简单的线性回归模型
y = torch.add(torch.dot(X, W), b)
# 求导
y.backward()
# 然后我们可以查看各个叶子变量的导数
print(W.grad)
print(b.grad)
tensor([ 1.0863, -0.9119, -0.5404])
tensor([1.])

对于上文的结果,我们也可以简单地算一下是否正确

$$ \begin{aligned} y = \omega x+b\\ y = x_1*w_1+x_2*w_2+x_3*w_3+b\\ \frac{\partial{y}}{\partial{w_1}} = x_1\\ \frac{\partial{y}}{\partial{w_2}} = x_2\\ \frac{\partial{y}}{\partial{w_3}} = x_3\\ \frac{\partial{y}}{\partial{b}} = 1\\ \end{aligned} $$

# 我们打印 X 向量的各个值
print(f"x1: {X[0]}, x2: {X[1]}, x3: {X[2]}")
print(f"sum of x: {torch.sum(X)}")
x1: 1.0862804651260376, x2: -0.9118912220001221, x3: -0.5403858423233032
sum of x: -0.3659965991973877

总结

本文简单讲述了 pytorch 中两种典型求梯度的流程,并与手动计算梯度的结果进行比较,对于自动求导,我们还需要注意这几点:

  1. 求导是一次昂贵的操作,因为需要遍历整个计算图,所以在训练过程中,我们需要尽量减少求导的次数,尽量将求导的操作放在循环外面。
  2. 每次求导之后计算图就会被释放,所以如果需要多次求导,需要在每次求导之前重新构建计算图。
  3. 变量的梯度是不断累加的,所以在每次求导之前,需要将梯度清零。
  4. 进行求导操作的时候,还可以传入一个权重向量,用于对最后的梯度进行放缩。如下方所示
x = torch.tensor([1.,2.,3.],requires_grad=True)
y = torch.pow(x,2)

gradient=torch.tensor([1.0,0.1,0.01])
y.backward(gradient)
print(x.grad)
tensor([2.0000, 0.4000, 0.0600])