1. 项目概述:为什么我们需要关注requires_grad_?
在PyTorch的日常开发中,无论是刚入门的新手还是有一定经验的开发者,都绕不开一个核心概念:自动微分。而requires_grad属性,正是这个强大引擎的“总开关”。你可能在定义张量时见过它,也可能在模型训练时被它引发的错误困扰过。今天,我们不谈那些泛泛的概念,而是聚焦于一个看似简单、实则暗藏玄机的操作:requires_grad_(True)。
这个带下划线的原地操作符,是PyTorch张量方法中一个非常典型的代表。它直接修改了张量自身的requires_grad属性,将其设置为True。这听起来很简单,但为什么需要它?为什么不直接用requires_grad=True来创建张量?在实际项目中,比如当你加载一个预训练模型,只想微调其中几层时;或者当你构建一个复杂的计算图,需要动态地冻结和解冻部分参数时,requires_grad_()就从一个简单的属性设置器,变成了一个至关重要的流程控制工具。理解它,意味着你能更精细地控制模型的反向传播过程,从而优化内存使用、提升训练效率,甚至实现一些高级的训练技巧。
这篇文章适合所有正在使用PyTorch进行深度学习研究和开发的同行。无论你是正在为“土堆PyTorch”教程中的某个练习而困惑,还是在搭建自己的CNN处理CIFAR-10图像分类任务,亦或是尝试用LSTM构建Seq2Seq模型时遇到了梯度问题,对requires_grad_()的深入理解都将帮助你扫清障碍,写出更健壮、更高效的代码。我们将从最基础的张量属性讲起,逐步深入到计算图、梯度流,并通过大量实际场景的代码示例,让你彻底掌握这个关键操作。
2. 核心概念拆解:张量、计算图与梯度流
要理解requires_grad_(True),我们必须先回到PyTorch自动微分的基石上。这不仅仅是记住一个API的用法,更是要理解其背后整个系统是如何协同工作的。
2.1 张量的requires_grad属性:不仅仅是布尔标志
在PyTorch中,torch.Tensor对象不仅仅是一个存储数据的多维数组。它是一个“智能”的数据结构,额外携带了用于自动微分(Autograd)的元数据。requires_grad就是这些元数据中最重要的一个。
- 默认行为:当你使用
torch.tensor()、torch.randn()等方式创建张量时,如果不显式指定,requires_grad默认为False。这意味着PyTorch的Autograd引擎不会追踪基于这个张量的任何操作。x = torch.randn(3, 3) print(x.requires_grad) # 输出: False - 创建时指定:你可以在创建时就声明需要梯度追踪。
w = torch.randn(3, 3, requires_grad=True) print(w.requires_grad) # 输出: True requires_grad_()的作用:对于已经存在的张量(比如从文件加载的模型参数、或者中间计算结果),如果你想改变它的梯度追踪状态,就需要用到requires_grad_()这个原地(in-place)方法。方法名末尾的下划线在PyTorch约定中通常表示原地操作。y = torch.ones(2, 2) print(y.requires_grad) # 输出: False y.requires_grad_(True) # 原地修改属性 print(y.requires_grad) # 输出: True
注意:
requires_grad_()会返回修改后的张量自身,这方便了链式调用,但更重要的是,它直接修改了原张量,不会创建新的张量。这与y = y.requires_grad(True)(如果该函数存在)有本质区别,后者会创建副本。
2.2 计算图(Computation Graph)的构建与追踪
设置requires_grad=True的根本目的,是为了让PyTorch能够构建一个动态的计算图。每当你对一个requires_grad=True的张量进行操作(加、减、乘、除、矩阵乘法等),PyTorch不仅会计算结果张量,还会在背后默默地记录这个操作(称为Function节点),并将输入张量和输出张量连接起来。
例如:
a = torch.randn(2, 2, requires_grad=True) b = torch.randn(2, 2, requires_grad=True) c = a + b # 加法操作被记录,c是结果张量,其`requires_grad`自动为True d = c * 2 # 乘法操作被记录 loss = d.sum() # 求和操作被记录这段代码会隐式地构建一个从a和b到loss的计算图。loss是一个标量,它是这个计算图的最终输出。
2.3 梯度流:从loss.backward()到.grad属性
计算图构建好后,它的核心用途是在反向传播中计算梯度。当你调用loss.backward()时,PyTorch会沿着计算图从loss开始,反向遍历所有记录的操作(Function节点),根据链式法则计算每一个requires_grad=True的叶子张量(即不是由其他操作生成的原始张量,如这里的a和b)的梯度。
计算出的梯度不会直接显示,而是累积在叶子张量的.grad属性中。
loss.backward() print(a.grad) # 输出a关于loss的梯度 print(b.grad) # 输出b关于loss的梯度 print(c.grad) # 输出通常是None,因为c是非叶子张量,默认情况下梯度计算完后会被释放以节省内存这里的关键在于:只有requires_grad=True的叶子张量,才会在backward()后拥有有意义的.grad值。对于中间张量(如c和d),虽然它们在计算图中,但其梯度通常不被保留。你可以通过retain_grad()方法在中间张量上保留梯度,但这会消耗额外内存。
2.4requires_grad_(True)与requires_grad_(False)的对比
理解了正向追踪和反向传播,我们就能看清requires_grad_()两个方向操作的本质:
x.requires_grad_(True):这相当于为张量x打开了“梯度追踪开关”。此后,任何以x为起点的操作都会被纳入计算图。这对于动态地将一个之前不需要梯度的张量(如固定的输入数据、或暂时冻结的层参数)变为需要训练的参数至关重要。x.requires_grad_(False):这相当于关闭了开关。它有两个主要且常用的效果:- 冻结参数:在微调模型时,我们通常冻结骨干网络,只训练新添加的分类头。对骨干网络的参数调用
.requires_grad_(False),可以确保在backward()时不会计算和更新这些参数的梯度,既节省了计算量,也防止了预训练好的权重被破坏。 - 推理阶段优化:在模型评估(
eval)或部署时,我们不需要梯度。将整个模型或输入数据设置为requires_grad=False,可以显著减少内存开销,因为PyTorch不需要为前向传播构建计算图的历史记录。
- 冻结参数:在微调模型时,我们通常冻结骨干网络,只训练新添加的分类头。对骨干网络的参数调用
一个常见的误区是,认为设置了requires_grad=False,张量就不能参与计算了。完全不是这样。它依然可以参与所有数学运算,只是这些运算不会被梯度追踪。这在需要固定部分输入或使用某些数值常量时非常有用。
3. 核心细节解析与实操要点
掌握了基本原理,我们来看看在实际编码中,requires_grad_()有哪些必须注意的细节和技巧。这些往往是官方文档不会强调,但却是写出正确、高效代码的关键。
3.1 原地操作(In-place Operation)的本质与风险
requires_grad_()是一个原地操作。在PyTorch中,原地操作是指直接修改张量底层数据内容,而不创建新的张量。这通过方法名后的下划线来标识(如add_(),zero_())。
原地操作的优势是节省内存,因为它避免了创建数据副本。但在自动微分上下文中,原地操作需要格外小心,因为它可能破坏计算图。
为什么原地操作危险?考虑以下场景:
x = torch.tensor([1., 2.], requires_grad=True) y = x + 1 # 假设现在对x进行一个原地操作 x.add_(1) # 危险!原地修改了叶子张量x z = y * 2 loss = z.sum() loss.backward() # 这里可能会报错或得到错误的梯度当你调用x.add_(1)时,你直接改变了x的值。然而,计算图中记录的操作y = x + 1是基于旧的x值计算的。这就导致了计算图状态(记录的旧x)与实际张量值(新的x)不一致,Autograd引擎无法正确处理这种不一致,通常会导致错误或 silently 产生错误的梯度。
重要实操心得:对于任何
requires_grad=True的叶子张量,尽量避免使用原地操作修改其数据值。如果你确实需要更新参数,应该使用优化器(如optim.SGD)的step()方法,它会在内部以安全的方式更新参数。requires_grad_()本身只修改元数据(属性),不修改数据值,因此在改变梯度追踪状态时使用原地操作是安全且推荐的。
3.2 叶子张量(Leaf Tensor)与非叶子张量
这是理解梯度存储的关键区分。
- 叶子张量:由用户直接创建的张量(如
torch.tensor,torch.randn),或者通过.detach()从计算图中分离出来的张量。它是计算图的起点。 - 非叶子张量:通过对叶子张量(或其他非叶子张量)进行运算得到的张量。
它们的核心区别在于梯度存储:
a = torch.randn(2, requires_grad=True) # 叶子张量 b = torch.randn(2, requires_grad=True) # 叶子张量 c = a + b # 非叶子张量 loss = c.sum() loss.backward() print(a.grad is not None) # True,叶子张量存储梯度 print(b.grad is not None) # True,叶子张量存储梯度 print(c.grad is not None) # False,非叶子张量默认不存储梯度requires_grad_()对两者的影响:
- 对一个非叶子张量调用
requires_grad_(False)是没有意义的,因为它本身不存储梯度。它的梯度追踪状态由其源张量决定。更常见的做法是使用detach()方法,它会返回一个与当前计算图分离的新张量,且新张量的requires_grad=False。 - 对一个叶子张量调用
requires_grad_(False),会立即将其从梯度计算中排除。后续以它为起点的操作不会被追踪。
3.3detach()vsrequires_grad_(False)
这是两个容易混淆但用途完全不同的操作。
tensor.detach():返回一个新的张量,这个新张量与原始张量共享底层数据存储,但从当前计算图中分离出来。新张量的requires_grad属性为False。原始张量不受影响。a = torch.randn(2, requires_grad=True) b = a.detach() # b是一个新张量,requires_grad=False,与a共享数据 print(a.requires_grad) # True print(b.requires_grad) # False # 对b的操作不会影响a的计算图tensor.requires_grad_(False):原地修改现有张量的属性。这个张量本身还在计算图中(如果它是某个操作的输入),但从此以后,由它参与产生的任何新操作都不会被追踪。a = torch.randn(2, requires_grad=True) a.requires_grad_(False) # 原地修改a自身 print(a.requires_grad) # False # 现在a的requires_grad是False了
如何选择?
- 当你需要将一个中间计算结果作为常数输入到后续计算,且不希望梯度流经它时,使用
detach()。例如,在强化学习中,从目标网络获取稳定目标值。 - 当你想要永久地冻结一个模型参数(叶子张量),使其在后续所有训练中都不被更新时,使用
requires_grad_(False)。这是模型微调中的标准操作。
3.4 梯度累加模式下的特殊考量
在GPU内存紧张时,我们常使用梯度累加(Gradient Accumulation)来模拟更大的批量大小(Batch Size)。基本模式是:多次前向传播和反向传播,但不立即更新参数(optimizer.step()),让梯度在.grad属性中累加,累加一定步数后再更新并清零梯度。
在这种模式下,requires_grad_()的使用要格外小心。不要在训练循环中动态地切换同一个参数的requires_grad状态。例如:
for epoch in range(epochs): for i, data in enumerate(dataloader): # 错误示范:动态切换 if i % 10 == 0: model.some_layer.weight.requires_grad_(True) else: model.some_layer.weight.requires_grad_(False) # ... 前向传播、损失计算、反向传播 optimizer.step() optimizer.zero_grad()这会导致梯度累加的逻辑混乱,因为.grad属性在requires_grad=False时不会被backward()计算,但在切换为True后,之前累加的梯度(可能是0或未定义)依然存在,导致参数更新错误。
正确做法:如果某些层需要按特定模式冻结/解冻(如GAN中交替训练生成器和判别器),应该在每个交替阶段开始前,一次性设置好所有相关参数的requires_grad状态,并确保在切换时清空优化器状态(重新初始化优化器或手动将对应参数的.grad设为None),然后进入该阶段的多个累加步进行稳定训练。
4. 实操过程与核心环节实现
理论说再多,不如动手试一遍。我们通过几个逐渐深入的场景,来看看requires_grad_()如何在实际代码中发挥作用。
4.1 场景一:基础参数冻结与微调
这是最常见的应用场景。假设我们有一个简单的两层网络,我们想冻结第一层,只训练第二层。
import torch import torch.nn as nn import torch.optim as optim # 定义一个简单网络 class SimpleNet(nn.Module): def __init__(self): super().__init__() self.layer1 = nn.Linear(10, 20) self.layer2 = nn.Linear(20, 1) def forward(self, x): x = torch.relu(self.layer1(x)) x = self.layer2(x) return x model = SimpleNet() # 假设layer1是预训练好的,我们需要冻结它 for param in model.layer1.parameters(): param.requires_grad_(False) # 原地冻结参数 # 检查参数状态 for name, param in model.named_parameters(): print(f'{name}: requires_grad={param.requires_grad}') # 输出: # layer1.weight: requires_grad=False # layer1.bias: requires_grad=False # layer2.weight: requires_grad=True # layer2.bias: requires_grad=True # 定义优化器。注意:优化器只应该更新 requires_grad=True 的参数 # 方式一:将模型所有参数传入,优化器内部会自动过滤。 optimizer = optim.SGD(model.parameters(), lr=0.01) # 方式二(更高效、更清晰):显式地只传入需要训练的参数。 trainable_params = filter(lambda p: p.requires_grad, model.parameters()) optimizer = optim.SGD(trainable_params, lr=0.01) # 训练循环 model.train() for epoch in range(10): # 模拟数据 inputs = torch.randn(32, 10) labels = torch.randn(32, 1) optimizer.zero_grad() outputs = model(inputs) loss = nn.MSELoss()(outputs, labels) loss.backward() # 验证梯度:layer1的梯度应为None,layer2的梯度应有值 print(model.layer1.weight.grad) # 输出: None print(model.layer2.weight.grad is not None) # 输出: True optimizer.step()关键点:在定义优化器时,虽然将model.parameters()全部传入也能工作(优化器会跳过requires_grad=False的参数),但显式地传入filter后的可训练参数列表是更好的实践。这使你的意图更清晰,并且在某些复杂场景下能避免潜在问题。
4.2 场景二:动态计算图与条件梯度追踪
PyTorch的动态图特性使得我们可以根据输入数据动态决定是否需要计算梯度。requires_grad_()在这里扮演了关键角色。
def dynamic_forward(x, compute_grad_for_x=False): """ 一个根据参数决定是否对输入x计算梯度的函数。 这在处理混合(需要梯度/不需要梯度)数据时很有用。 """ # 根据函数参数,动态设置输入x的梯度追踪状态 # 注意:为了安全,我们通常在函数的开始处理这个逻辑。 if compute_grad_for_x: x.requires_grad_(True) else: # 如果确定不需要梯度,可以设置为False以节省内存 # 但更常见的做法是,如果外部传入的x本身就不需要梯度,我们就不动它。 # 这里为了演示,我们强制设为False。 x.requires_grad_(False) # 一些计算 w = torch.tensor([[1.0, 2.0], [3.0, 4.0]], requires_grad=True) y = x @ w # 矩阵乘法 z = y.sum() return z # 用例1:不需要x的梯度 x1 = torch.ones(2, 2) # 默认 requires_grad=False out1 = dynamic_forward(x1, compute_grad_for_x=False) out1.backward() # 这里只会计算w的梯度,不会计算x1的梯度 # 尝试打印x1.grad会是None # 用例2:需要x的梯度 x2 = torch.ones(2, 2) # 默认 requires_grad=False out2 = dynamic_forward(x2, compute_grad_for_x=True) out2.backward() print(x2.grad) # 此时x2会有梯度值 print(x2.requires_grad) # True注意事项:在这个例子中,我们在函数内部修改了输入张量x的属性。这属于原地修改传入的参数,在Python中会影响函数外部的原始张量。虽然在这种控制逻辑下是可行的,但在设计API时需要明确告知调用者此函数有副作用。更函数式(无副作用)的做法是,在函数内部根据条件使用x.detach()或保持原样,并返回结果,让调用者自己管理requires_grad状态。
4.3 场景三:梯度检查(Gradient Checking)中的应用
在实现自定义的PyTorch函数(torch.autograd.Function)或复杂的层时,梯度检查是验证反向传播正确性的金标准。其核心思想是与数值梯度进行对比。在这个过程中,我们需要临时操作张量的requires_grad属性。
def gradient_check(layer, input_data, epsilon=1e-7): """ 简单的梯度检查函数。 layer: 要检查的nn.Module或自定义函数。 input_data: 输入张量。 """ # 确保输入需要梯度,以便PyTorch能构建计算图 if not input_data.requires_grad: input_data.requires_grad_(True) # 1. 计算解析梯度 (Analytical Gradient) output = layer(input_data) # 假设我们检查的是输出关于第一个元素的梯度 target_output = output.flatten()[0] target_output.backward() # 反向传播计算梯度 analytic_grad = input_data.grad.clone() # 保存解析梯度 # 重要:清零梯度并为下一次计算做准备 if input_data.grad is not None: input_data.grad.zero_() # 2. 计算数值梯度 (Numerical Gradient) numeric_grad = torch.zeros_like(input_data) flat_input = input_data.view(-1) flat_numeric_grad = numeric_grad.view(-1) for i in range(len(flat_input)): original_val = flat_input[i].item() # f(x + epsilon) flat_input[i] = original_val + epsilon output_plus = layer(input_data).flatten()[0].item() # f(x - epsilon) flat_input[i] = original_val - epsilon output_minus = layer(input_data).flatten()[0].item() # 中心差分公式,更精确 flat_numeric_grad[i] = (output_plus - output_minus) / (2 * epsilon) # 恢复原始值 flat_input[i] = original_val # 3. 比较 diff = torch.abs(analytic_grad - numeric_grad).max().item() print(f"最大梯度差异: {diff}") # 通常差异小于 1e-7 可以认为实现正确 return diff < 1e-6 # 使用示例 layer = nn.Linear(5, 3) test_input = torch.randn(10, 5, requires_grad=False) # 初始不需要梯度 is_correct = gradient_check(layer, test_input) print(f"梯度检查通过: {is_correct}")在这个检查函数中,我们首先确保输入input_data的requires_grad=True,以便进行反向传播求解析梯度。在计算数值梯度时,我们直接修改了input_data的数据(flat_input[i] = ...)。这是一个极其危险的原地操作!因为我们正在对一个requires_grad=True的叶子张量进行数据修改。
为什么这里可以“冒险”?
- 我们在每次扰动后都立即恢复了原始值,保证了在最终状态时,输入数据与最初一致。
- 更重要的是,在计算数值梯度时,我们根本不需要计算图。我们只是单纯地计算函数值。即使因为修改数据破坏了之前为计算解析梯度而构建的计算图,也无所谓,因为那个计算图我们已经用完了(
target_output.backward()已经调用)。 - 在循环结束后,我们调用
layer(input_data)时,会基于当前数据(已恢复为原始值)创建一个全新的计算图,但这不影响我们,因为数值梯度计算已经完成。
尽管如此,在更严谨的实现中,通常会使用.data或.detach().clone()来获取数据的副本进行操作,完全避免对叶子张量的原地修改。这里展示了一种在特定受控环境下“可接受”的用法,但你必须非常清楚自己在做什么。
5. 常见问题与排查技巧实录
即使理解了原理,在实际编码中依然会遇到各种诡异的问题。下面是我在多年实践中总结的与requires_grad_()相关的典型“坑”和解决思路。
5.1 问题一:RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn
这是新手最常见的错误之一。
错误复现:
x = torch.randn(3) # 默认 requires_grad=False y = x * 2 loss = y.sum() loss.backward() # 这里会抛出 RuntimeError!错误原因:你试图对一个从requires_grad=False的张量开始构建的计算图进行反向传播。计算图的所有叶子张量都不需要梯度,因此loss本身也没有关联的梯度函数(grad_fn),Autograd引擎不知道如何计算梯度。
排查与解决:
- 检查源头:回溯你的计算图,找到最初的输入张量。确保你希望训练的参数(如模型参数
model.parameters())或你希望计算梯度的输入,在创建时或通过requires_grad_(True)设置了requires_grad=True。 - 检查冻结操作:如果你在代码中某处对模型参数执行了
param.requires_grad_(False),请确认这是你的本意。可能是无意中冻结了本应训练的参数。 - 检查数据加载:确保你的输入数据(
inputs)通常不需要梯度(requires_grad=False),除非你在做可微的数据增强或某些特殊研究。需要梯度的是模型参数。
5.2 问题二:梯度为None或全零
有时候代码能运行,不报错,但训练不收敛,检查发现某些参数的梯度是None或全是0。
可能原因及排查:
- 参数已被冻结:这是最可能的原因。使用
print(param.requires_grad)检查你怀疑的层参数。确保在训练循环开始前,所有需要更新的参数都是True。 - 计算图被意外截断:
实际上,a = torch.randn(2, requires_grad=True) b = a.detach() # 计算图在这里被切断 c = b * 3 # 由于b是从计算图分离的,c的requires_grad也是False d = c + a # 即使这里又和a相加,d的梯度流也无法通过c回溯到a之前的操作? loss = d.sum() loss.backward() print(a.grad) # 输出是什么?d的梯度能流到a,因为d = c + a创建了一个新的加法操作,a是这个操作的直接输入。所以a.grad不会是None。但b和c的路径确实被截断了。关键在于理解梯度流是沿着操作(Function)传播的,只要从loss到该张量存在一条由可微操作构成的路径,梯度就能到达。 - 使用了不支持的原地操作或Python原生操作:某些原地操作或直接用Python
=赋值会破坏计算图。始终使用PyTorch提供的张量操作。 - 损失函数或网络结构问题:梯度为零可能意味着网络达到了局部极值,或者损失函数设计有问题(例如,对离散数据使用了不适合的连续损失)。这超出了
requires_grad的范畴,但也是排查方向。
5.3 问题三:内存溢出(OOM)与requires_grad
现象:在推理(model.eval())或评估时,内存占用异常高,甚至和训练时差不多。
根因:即使在model.eval()模式下,如果输入张量或中间某个张量的requires_grad=True,PyTorch为了潜在的反向传播,依然会为前向传播的所有中间结果保留计算图信息(用于梯度计算),这消耗了大量内存。
解决方案:
- 使用
torch.no_grad()上下文管理器:这是最推荐、最安全的方式。model.eval() with torch.no_grad(): # 在这个块内,所有计算都不会被追踪梯度 for data in test_loader: inputs, labels = data outputs = model(inputs) # 不会构建计算图,极大节省内存 # ... 后续计算torch.no_grad()不仅节省内存,还能轻微提升前向传播速度。 - 将模型参数和输入设置为
requires_grad=False:虽然可以,但不如torch.no_grad()方便和彻底。torch.no_grad()的优先级更高,即使在它内部操作一个requires_grad=True的张量,也不会被追踪。
5.4 高级技巧:使用torch.inference_mode
在PyTorch 1.9+版本中,引入了torch.inference_mode(),它是比torch.no_grad()更激进的优化。
torch.no_grad():禁用梯度计算,但Autograd引擎仍在运行,只是不记录操作。你仍然可以修改requires_grad=True的张量的数据(虽然危险)。torch.inference_mode():不仅禁用梯度计算,还会完全禁用Autograd引擎。在此模式下,任何输出的张量requires_grad都将为False,并且你不能对requires_grad=True的张量进行原地操作。它带来了比no_grad()更大的性能提升和内存节省。
如何选择:
- 在训练循环中的验证阶段,使用
model.eval()和torch.no_grad()。 - 在纯粹的模型部署和推理场景,且你确认绝对不会进行任何梯度相关操作时,使用
torch.inference_mode()。
5.5 参数冻结与优化器状态管理
当你动态地冻结或解冻参数时(例如,在课程学习或某些GAN训练策略中),优化器的状态管理至关重要。
错误做法:
# 初始状态,所有层都可训练 optimizer = optim.Adam(model.parameters(), lr=0.001) # ... 训练一段时间后,冻结layer1 for param in model.layer1.parameters(): param.requires_grad_(False) # 继续训练... 此时优化器里仍然保存着layer1参数的动量(momentum)等状态信息。即使layer1的参数不再计算梯度,优化器内部为它们维护的状态(如动量缓存)仍然存在,并且会在每次optimizer.step()时被读取(虽然参数值不会更新)。这浪费了内存,也可能在某些算法中引发问题。
正确做法: 在改变参数的requires_grad属性后,最好重新创建优化器,或者手动从优化器中移除对应参数的状态。
# 更健壮的做法:在改变训练参数集后,重建优化器 trainable_params = filter(lambda p: p.requires_grad, model.parameters()) optimizer = optim.Adam(trainable_params, lr=0.001) # 使用新的可训练参数列表重建优化器会丢弃旧状态,确保优化器只管理当前需要训练的参数,这是最清晰、最不容易出错的方式。
理解requires_grad_(True)远不止于记住一个API调用。它关乎你对PyTorch自动微分系统工作流的精细控制。从确保梯度能够正确流动,到高效地冻结和解冻模型部分,再到优化推理性能,这个简单的操作背后连接着计算图、内存管理和训练策略等多个核心环节。我个人的经验是,每当遇到梯度相关的诡异问题时,第一反应就应该是检查相关张量的requires_grad属性,这能解决一大半的困惑。把它当成调试梯度问题的“第一把钥匙”,你的PyTorch编程水平会扎实许多。