autograd 是一个反向自动微分系统,它能根据对 tensor 的操作过程自动构建计算图。具体而言,计算图是一个有向无环图,记录了前向传播过程的全部数据操作,图中的根节点是输出张量(Output Tensor),叶节点是输入张量(Input Tensor)。沿着这个图即可使用链式法则计算得到中间的梯度。下面给出一个例子
- 设 x = [ x 1 , x 2 , x 3 , x 4 ] \pmb{x} = [x_1,x_2,x_3,x_4] x=[x1,x2,x3,x4] 是一个 1 × 4 1\times 4 1×4 的向量,求下式梯度 f ( x ) = 1 4 ∑ i = 1 4 3 ( x i + 2 ) 2 f(\pmb{x})=\frac{1}{4}\sum_{i=1}^43(x_i+2)^2 f(x)=41∑i=143(xi+2)2。注意向量的梯度也将是一个同尺寸的向量
- 可以把计算图如下画出,可见,有 ∂ f ∂ x i = ∂ d ∂ x i = ∂ d ∂ c i ∂ c i ∂ b i ∂ b i ∂ a i ∂ a i ∂ x i = 1 4 ⋅ 3 ⋅ ( 2 x i + 4 ) ⋅ 1 = 1.5 x i + 3 \frac{\partial f}{\partial x_i} =\frac{\partial d}{\partial x_i}= \frac{\partial d}{\partial c_i}\frac{\partial c_i}{\partial b_i}\frac{\partial b_i}{\partial a_i}\frac{\partial a_i}{\partial x_i}=\frac{1}{4}·3·(2x_i+4)·1=1.5x_i+3 ∂xi∂f=∂xi∂d=∂ci∂d∂bi∂ci∂ai∂bi∂xi∂ai=41⋅3⋅(2xi+4)⋅1=1.5xi+3
no-grad 模式和 inference模式等上下文管理器requires_grad 字段来实现。这样可以打断有向无环图中的一些有向边,从而选择性地排除某些子图不参与梯度计算nn.Module 的评估模式方法 nn.Module.eval(),它实际上并不用于禁用梯度计算。但是由于其名称的误导性,经常与上述机制混淆使用requires_grad 实现精确梯度控制Tensor.requires_grad 是 tensor 对象的一个标志变量,默认为 False,它在前向传播和反向传播中都起作用,允许对梯度计算中的子图进行精细排除。
requires_grad=True 时才会记录在计算图中requires_grad=True 的叶张量才会将梯度累积到它们的 .grad 字段中有几种方式可以将 Tensor.requires_grad 设置为 True
torch.ones(2,2,requires_grad=True)a.requires_grad_(True)nn.Parameter 对 tensor 进行包装,如 nn.Parameter(torch.zeros(2,2))。定义神经网络时如果需要优化一个张量(比如定义Transformer的可训练位置编码),通常使用这种方法值得注意的是,尽管每个张量都有这个标志,但设置它只对 leaf tensor 有意义;non-leaf tensor 是有可以记录其计算过程的 .grad_fn 方法,有一部分反向图与之相关的 tensor,它们自动具有 require_grad=True,因为要计算 leaf tensor 的梯度时必须借助相关的 non-leaf tensor 的梯度作为中间结果
设置 requires_grad 是控制模型进行部分梯度计算的主要方法。例如考虑函数:
y
3
=
y
1
+
y
2
=
x
2
+
x
3
y_3 = y_1+y_2=x^2+x^3
y3=y1+y2=x2+x3,有
∂
y
3
∂
x
=
∂
y
1
∂
x
+
∂
y
2
∂
x
=
2
x
+
3
x
=
5
x
\frac{\partial y_3}{\partial x} = \frac{\partial y_1}{\partial x} + \frac{\partial y_2}{\partial x} = 2x+3x = 5x
∂x∂y3=∂x∂y1+∂x∂y2=2x+3x=5x 如果将其中的
y
2
y_2
y2 设置为 requires_grad=False,梯度就无法从
y
2
y_2
y2 往回传播,这时有
∂
y
3
∂
x
=
∂
y
1
∂
x
=
2
x
\frac{\partial y_3}{\partial x} = \frac{\partial y_1}{\partial x}= 2x
∂x∂y3=∂x∂y1=2x 计算图如下

x = torch.tensor(1.0, requires_grad=True)
y1 = x ** 2
with torch.no_grad():
y2 = x ** 3
y3 = y1 + y2
print(x.requires_grad) # True
print(y1, y1.requires_grad) # tensor(1., grad_fn=) True
print(y2, y2.requires_grad) # tensor(1.) False
print(y3, y3.requires_grad) # tensor(2., grad_fn=) True
y3.backward()
print(x.grad) # tensor(2.)
#y2.backward() # 报错: element 0 of tensors does not require grad and does not have a grad_fn
精细梯度控制在模型微调期间比较常用,比如若想在微调时冻结部分预训练模型,只需对不想更新的参数应用
.requires_grad_(False)即可,这样使用这些参数作为输入的计算就不会被记录在向前传递中,它们不再成为计算图的一部分,也就不会在向后传递时更新它们的.grad字段了
另外,requires_grad 也可以在模块级别通过 nn.Module.requires_grad_() 进行设置,这对模块内的所有参数生效 (默认情况下requires_grad=True)
requires_grad 之外,Pytorch 还提供了三种可以影响 autograd 内部梯度计算的模式:默认模式/梯度模式(Grad Mode)、无梯度模式(No-grad Mode) 和 推理模式(Inference Mode),所有这些模式都可以通过python语法中的上下文管理器和装饰器进行切换requires_grad=True 生效的唯一模式,requires_grad 在其他两种模式中总是被设置为 Falserequire_grad=True 的输入,也不会在反向图中记录。有两种常用的进入无梯度模式的方法:使用上下文管理器(with语法)和函数装饰器with torch.no_grad():
do_something()
@torch.no_grad()
def do_something_func():
do_something()
torch.set_grad_enabled(False)
do_something()
torch.set_grad_enabled(True)
def sgd(params, lr, batch_size):
"""小批量随机梯度下降"""
with torch.no_grad():
for param in params: # param 是一个list,如果模型是y=Xw+b,则param=[w,b]
param -= lr * param.grad / batch_size
param.grad.zero_()
torch.nn.init 方法的实现也依赖于无梯度模式,以避免在就地初始化参数时就自动跟踪梯度了with torch.inference_mode():
do_something()
@torch.inference_mode()
def func(x):
do_something()
nn.Module.eval() 实际上不是一种影响 autograd 内部梯度计算的模式,但它有时会被混淆成这样一种机制。 在功能上,module.eval()/ module.train() 与 2.2 节介绍的三种模式完全正交。model.eval() 如何影响模型完全取决于模型中使用的特定模块,以及它们是否定义了任何特定于训练模式的行为。具体而言:model.eval() 的作用是不启用 Batch Normalization 和 Dropout,即
model.train(),在评估模型(验证/测试)时始终使用 model.eval(),以免受到任何潜在导致这些操作的模型更新的影响module.eval(),所有梯度还是会被计算Tensor.requires_grad 参数来打断计算图中的某些边,以实现对梯度计算的精确控制。在微调模型时可以用这种方式冻结某些参数requires_grad=True 生效,会进行计算图构建,这也是默认模式require_grad=True 的输入也不会在反向图中记录,适用于有一些操作无需记录梯度,但需要中间计算结果用于后续(梯度模式下)梯度计算的情况。这种模式下显存占用和计算资源的消耗都大大减少了nn.Module 单独有一种评估模式,它的功能和以上三种梯度计算模式是正交的,它仅影响 Dropout 和 Batch Normalization 的行为模式而和梯度计算无关。在训练应时始终使用 model.train(),在评估模型 (验证/测试) 时应始终使用 model.eval()