在深度学习中,通常通过梯度下降来优化代求的权重,就需要知道其梯度的方向,而一个模型往往有很多层,也可以看成由很多个函数的嵌套。我们常常需要求导数来进行权重的更新。
深度学习框架通过自动计算导数,即自动微分(automatic differentiation)来加快求导。
实际中,根据设计好的模型,系统会构建一个计算图(computational graph),来跟踪计算是哪些数据通过哪些操作组合起来产生输出。
自动微分使系统能够随后反向传播梯度。
这里,反向传播(backpropagate)意味着跟踪整个计算图,填充关于每个参数的偏导数。
1 链式法则
1.1 标量链式法则
如果: $$y = f(g(h(x)))$$
那么: $$ \frac{dy}{dx} = \frac{dy}{dg} \cdot \frac{dg}{dh} \cdot \frac{dh}{dx} $$
1.2 向量链式法则
当y不是标量时,向量y关于向量x的导数的最自然解释是一个矩阵——雅可比矩阵。 对于高阶和高维的y和x,求导的结果可以是一个高阶张量。
假设有一个向量函数:
$$ \mathbf{y} = \begin{bmatrix} y_1 \\ y_2 \\ \vdots \\ y_m \end{bmatrix} = \mathbf{f}(\mathbf{x}), \quad \mathbf{x} = \begin{bmatrix} x_1 \\ x_2 \\ \vdots \\ x_n \end{bmatrix} \in \mathbb{R}^n $$
- $\mathbf{y} \in \mathbb{R}^m$
- $\mathbf{x} \in \mathbb{R}^n$
那么 雅可比矩阵 (J) 定义为:
$$ J = \frac{\partial \mathbf{y}}{\partial \mathbf{x}}= \begin{bmatrix} \frac{\partial y_1}{\partial x_1} & \frac{\partial y_1}{\partial x_2} & \dots & \frac{\partial y_1}{\partial x_n} \\ \frac{\partial y_2}{\partial x_1} & \frac{\partial y_2}{\partial x_2} & \dots & \frac{\partial y_2}{\partial x_n} \\ \vdots & \vdots & \ddots & \vdots \\ \frac{\partial y_m}{\partial x_1} & \frac{\partial y_m}{\partial x_2} & \dots & \frac{\partial y_m}{\partial x_n} \ \end{bmatrix} \in \mathbb{R}^{m \times n} $$
- 行:对应输出分量 (y_i)
- 列:对应输入分量 (x_j)
- 每个元素 ((i,j)) 表示 (y_i) 对 (x_j) 的偏导
同样的,在遇到嵌套的时候,也可以使用链式法则,只是当y是向量的时候结果就变成了雅可比矩阵
假设有向量函数: $$ \mathbf{y} = \mathbf{f}(\mathbf{u}), \quad \mathbf{u} = \mathbf{g}(\mathbf{x}) $$
- $\mathbf{x} \in \mathbb{R}^n$
- $\mathbf{u} \in \mathbb{R}^m$
- $\mathbf{y} \in \mathbb{R}^p$
想求 $\frac{d \mathbf{y}}{d \mathbf{x}}$。
向量函数的导数用 雅可比矩阵 表示:
$$ \mathbf{J}_{\mathbf{f}} = \frac{\partial \mathbf{f}}{\partial \mathbf{u}} \in \mathbb{R}^{p \times m}, \quad \mathbf{J}_{\mathbf{g}} = \frac{\partial \mathbf{g}}{\partial \mathbf{x}} \in \mathbb{R}^{m \times n} $$
链式法则就变成矩阵乘法:
$$ \frac{\partial \mathbf{y}}{\partial \mathbf{x}} = \mathbf{J}_{\mathbf{f}} \cdot \mathbf{J}_{\mathbf{g}} \quad \in \mathbb{R}^{p \times n} $$
1.3 多元函数的链式法则
f不再是单变量函数,而是多元函数。 $$ y = f(u, v), \quad u=g(x), \quad v=h(x) $$
对 x求导时,要用到 多元函数的全微分:
$$ \frac{dy}{dx} = \frac{\partial f}{\partial u} \cdot \frac{du}{dx} + \frac{\partial f}{\partial v} \cdot \frac{dv}{dx} $$
- 每个中间变量 (u, v) 对 (x) 都有贡献
- 最终梯度是这些贡献的“加和”
其实1.2拓展的向量也是多元变量,这边特意提到是为了说明一些特殊情况也是为了解释后面提到的计算图分离
比如:
$$
y = f(u,x), \quad u = g(x)
$$
y关于u,x有函数关系,u又关于x有函数关系。那么根据上述公式$\frac{dy}{dx}$就可以由以下公式求得。如果画出其计算图可以发现x.grad是有两条路径的。
$$ \frac{dy}{dx} = \frac{\partial y}{\partial u} \cdot \frac{du}{dx} + \frac{\partial y}{\partial x} $$
这也是pytorch中自动求导的重要依据
2 计算图

3 Pytorch中的自动求导
3.1 自动求导的使用
x=torch.arange(4.0,requires_grad=True) #需要使requires_grad为True
print(x.grad) # 默认值是None,得调用backward才会更新
y = 2 * torch.dot(x, x)
y.backward()
print(x.grad) # tensor([ 0., 4., 8., 12.])
y = x.sum()
y.backward()
print(x.grad) # tensor([ 1., 5., 9., 13.])
x.grad.zero_() # 在默认情况下,PyTorch会累积梯度,我们需要清除之前的值
y = x.sum()
y.backward()
print(x.grad) # tensor([1., 1., 1., 1.])
- requires_grad=True
- 调用backward()进行前向传播更新梯度
- 梯度是会累计的,记得清零,(为什么不能自动清理? 因为在图中传播的时候可能会经过多次这个节点,1.3就是一个例子)
3.2 非标量变量的反向传播
正如1.2中提到的,当y不是标量时,向量y关于向量x的导数的最自然解释是一个矩阵。
对于高阶和高维的y和x,求导的结果可以是一个高阶张量。
然而,虽然这些更奇特的对象确实出现在高级机器学习中(包括深度学习中), 但当调用向量的反向计算时,我们通常会试图计算一批训练样本中每个组成部分的损失函数的导数。 这里,我们的目的不是计算微分矩阵,而是单独计算批量中每个样本的偏导数之和。
# 对非标量调用backward需要传入一个gradient参数,该参数指定微分函数关于self的梯度。
# 如果只想求偏导数的和,所以传递一个1的梯度是合适的
x.grad.zero_()
y = x * x
y.backward(gradient = torch.ones(len(x))) # 等价于y.sum().backward()
x.grad
- PyTorch 不直接存雅可比矩阵(效率低),而是用 向量-雅可比乘法 (vector-Jacobian product, VJP)
- 当调用
backward(gradient=v)时会发生: $x.grad = v^T \cdot J_y$
其实可以直接理解为我们传进去的gradient参数,就是雅可比矩阵的每一行(每个输出分量)对最终结果的权重分量,用于计算向量-雅可比乘法(VJP),最终得到输入的梯度。 - 对于标量输出,
v默认是 1,直接得到普通梯度
3.3 分离计算
有时我们希望 将某些计算从计算图中隔离。
例如,y 是 x 的函数,z 又依赖 x 和 y。如果想计算 z 关于 x 的梯度,但把 y 当作常数,不让梯度回传到 y,可以使用 分离操作:
x.grad.zero_()
y = x * x
u = y.detach() # u 与 y 值相同,但不保留 y 的计算图
z = u * x
z.sum().backward() # 只计算 z 关于 x 的梯度,u 被当作常数
x.grad == u
相当于1.3中提到的例子中 $$ \frac{dy}{dx} = \frac{\partial y}{\partial u} \cdot \frac{du}{dx} + \frac{\partial y}{\partial x} $$ 对于y来说u是常数,因此$\frac{\partial y}{\partial u}$就为0,梯度也就不会继续往前传了。
- 这里梯度 不会沿着 u 流回 x
- 相当于把
y从计算图中“切断”,只保留它的数值参与计算 - 简单理解成可以把
y当成一个常数,梯度传到这里就不继续往前传了