PyTorch requires_grad_() 详解:从自动微分原理到模型微调实战
1. 项目概述为什么我们需要关注requires_grad_在PyTorch的日常开发中无论是刚入门的新手还是有一定经验的开发者都绕不开一个核心概念自动微分。而requires_grad属性正是这个强大引擎的“总开关”。你可能在定义张量时见过它也可能在模型训练时被它引发的错误困扰过。今天我们不谈那些泛泛的概念而是聚焦于一个看似简单、实则暗藏玄机的操作requires_grad_(True)。这个带下划线的原地操作符是PyTorch张量方法中一个非常典型的代表。它直接修改了张量自身的requires_grad属性将其设置为True。这听起来很简单但为什么需要它为什么不直接用requires_gradTrue来创建张量在实际项目中比如当你加载一个预训练模型只想微调其中几层时或者当你构建一个复杂的计算图需要动态地冻结和解冻部分参数时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_gradTrue) print(w.requires_grad) # 输出: Truerequires_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_gradTrue的根本目的是为了让PyTorch能够构建一个动态的计算图。每当你对一个requires_gradTrue的张量进行操作加、减、乘、除、矩阵乘法等PyTorch不仅会计算结果张量还会在背后默默地记录这个操作称为Function节点并将输入张量和输出张量连接起来。例如a torch.randn(2, 2, requires_gradTrue) b torch.randn(2, 2, requires_gradTrue) 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_gradTrue的叶子张量即不是由其他操作生成的原始张量如这里的a和b的梯度。计算出的梯度不会直接显示而是累积在叶子张量的.grad属性中。loss.backward() print(a.grad) # 输出a关于loss的梯度 print(b.grad) # 输出b关于loss的梯度 print(c.grad) # 输出通常是None因为c是非叶子张量默认情况下梯度计算完后会被释放以节省内存这里的关键在于只有requires_gradTrue的叶子张量才会在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_gradFalse可以显著减少内存开销因为PyTorch不需要为前向传播构建计算图的历史记录。一个常见的误区是认为设置了requires_gradFalse张量就不能参与计算了。完全不是这样。它依然可以参与所有数学运算只是这些运算不会被梯度追踪。这在需要固定部分输入或使用某些数值常量时非常有用。3. 核心细节解析与实操要点掌握了基本原理我们来看看在实际编码中requires_grad_()有哪些必须注意的细节和技巧。这些往往是官方文档不会强调但却是写出正确、高效代码的关键。3.1 原地操作In-place Operation的本质与风险requires_grad_()是一个原地操作。在PyTorch中原地操作是指直接修改张量底层数据内容而不创建新的张量。这通过方法名后的下划线来标识如add_(),zero_()。原地操作的优势是节省内存因为它避免了创建数据副本。但在自动微分上下文中原地操作需要格外小心因为它可能破坏计算图。为什么原地操作危险考虑以下场景x torch.tensor([1., 2.], requires_gradTrue) 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_gradTrue的叶子张量尽量避免使用原地操作修改其数据值。如果你确实需要更新参数应该使用优化器如optim.SGD的step()方法它会在内部以安全的方式更新参数。requires_grad_()本身只修改元数据属性不修改数据值因此在改变梯度追踪状态时使用原地操作是安全且推荐的。3.2 叶子张量Leaf Tensor与非叶子张量这是理解梯度存储的关键区分。叶子张量由用户直接创建的张量如torch.tensor,torch.randn或者通过.detach()从计算图中分离出来的张量。它是计算图的起点。非叶子张量通过对叶子张量或其他非叶子张量进行运算得到的张量。它们的核心区别在于梯度存储a torch.randn(2, requires_gradTrue) # 叶子张量 b torch.randn(2, requires_gradTrue) # 叶子张量 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_gradFalse。对一个叶子张量调用requires_grad_(False)会立即将其从梯度计算中排除。后续以它为起点的操作不会被追踪。3.3detach()vsrequires_grad_(False)这是两个容易混淆但用途完全不同的操作。tensor.detach()返回一个新的张量这个新张量与原始张量共享底层数据存储但从当前计算图中分离出来。新张量的requires_grad属性为False。原始张量不受影响。a torch.randn(2, requires_gradTrue) b a.detach() # b是一个新张量requires_gradFalse与a共享数据 print(a.requires_grad) # True print(b.requires_grad) # False # 对b的操作不会影响a的计算图tensor.requires_grad_(False)原地修改现有张量的属性。这个张量本身还在计算图中如果它是某个操作的输入但从此以后由它参与产生的任何新操作都不会被追踪。a torch.randn(2, requires_gradTrue) 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_gradFalse时不会被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_gradFalse # layer1.bias: requires_gradFalse # layer2.weight: requires_gradTrue # layer2.bias: requires_gradTrue # 定义优化器。注意优化器只应该更新 requires_gradTrue 的参数 # 方式一将模型所有参数传入优化器内部会自动过滤。 optimizer optim.SGD(model.parameters(), lr0.01) # 方式二更高效、更清晰显式地只传入需要训练的参数。 trainable_params filter(lambda p: p.requires_grad, model.parameters()) optimizer optim.SGD(trainable_params, lr0.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的梯度应为Nonelayer2的梯度应有值 print(model.layer1.weight.grad) # 输出: None print(model.layer2.weight.grad is not None) # 输出: True optimizer.step()关键点在定义优化器时虽然将model.parameters()全部传入也能工作优化器会跳过requires_gradFalse的参数但显式地传入filter后的可训练参数列表是更好的实践。这使你的意图更清晰并且在某些复杂场景下能避免潜在问题。4.2 场景二动态计算图与条件梯度追踪PyTorch的动态图特性使得我们可以根据输入数据动态决定是否需要计算梯度。requires_grad_()在这里扮演了关键角色。def dynamic_forward(x, compute_grad_for_xFalse): 一个根据参数决定是否对输入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_gradTrue) y x w # 矩阵乘法 z y.sum() return z # 用例1不需要x的梯度 x1 torch.ones(2, 2) # 默认 requires_gradFalse out1 dynamic_forward(x1, compute_grad_for_xFalse) out1.backward() # 这里只会计算w的梯度不会计算x1的梯度 # 尝试打印x1.grad会是None # 用例2需要x的梯度 x2 torch.ones(2, 2) # 默认 requires_gradFalse out2 dynamic_forward(x2, compute_grad_for_xTrue) 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, epsilon1e-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_gradFalse) # 初始不需要梯度 is_correct gradient_check(layer, test_input) print(f梯度检查通过: {is_correct})在这个检查函数中我们首先确保输入input_data的requires_gradTrue以便进行反向传播求解析梯度。在计算数值梯度时我们直接修改了input_data的数据flat_input[i] ...。这是一个极其危险的原地操作因为我们正在对一个requires_gradTrue的叶子张量进行数据修改。为什么这里可以“冒险”我们在每次扰动后都立即恢复了原始值保证了在最终状态时输入数据与最初一致。更重要的是在计算数值梯度时我们根本不需要计算图。我们只是单纯地计算函数值。即使因为修改数据破坏了之前为计算解析梯度而构建的计算图也无所谓因为那个计算图我们已经用完了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_gradFalse y x * 2 loss y.sum() loss.backward() # 这里会抛出 RuntimeError!错误原因你试图对一个从requires_gradFalse的张量开始构建的计算图进行反向传播。计算图的所有叶子张量都不需要梯度因此loss本身也没有关联的梯度函数grad_fnAutograd引擎不知道如何计算梯度。排查与解决检查源头回溯你的计算图找到最初的输入张量。确保你希望训练的参数如模型参数model.parameters()或你希望计算梯度的输入在创建时或通过requires_grad_(True)设置了requires_gradTrue。检查冻结操作如果你在代码中某处对模型参数执行了param.requires_grad_(False)请确认这是你的本意。可能是无意中冻结了本应训练的参数。检查数据加载确保你的输入数据inputs通常不需要梯度requires_gradFalse除非你在做可微的数据增强或某些特殊研究。需要梯度的是模型参数。5.2 问题二梯度为None或全零有时候代码能运行不报错但训练不收敛检查发现某些参数的梯度是None或全是0。可能原因及排查参数已被冻结这是最可能的原因。使用print(param.requires_grad)检查你怀疑的层参数。确保在训练循环开始前所有需要更新的参数都是True。计算图被意外截断a torch.randn(2, requires_gradTrue) 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_gradTruePyTorch为了潜在的反向传播依然会为前向传播的所有中间结果保留计算图信息用于梯度计算这消耗了大量内存。解决方案使用torch.no_grad()上下文管理器这是最推荐、最安全的方式。model.eval() with torch.no_grad(): # 在这个块内所有计算都不会被追踪梯度 for data in test_loader: inputs, labels data outputs model(inputs) # 不会构建计算图极大节省内存 # ... 后续计算torch.no_grad()不仅节省内存还能轻微提升前向传播速度。将模型参数和输入设置为requires_gradFalse虽然可以但不如torch.no_grad()方便和彻底。torch.no_grad()的优先级更高即使在它内部操作一个requires_gradTrue的张量也不会被追踪。5.4 高级技巧使用torch.inference_mode在PyTorch 1.9版本中引入了torch.inference_mode()它是比torch.no_grad()更激进的优化。torch.no_grad()禁用梯度计算但Autograd引擎仍在运行只是不记录操作。你仍然可以修改requires_gradTrue的张量的数据虽然危险。torch.inference_mode()不仅禁用梯度计算还会完全禁用Autograd引擎。在此模式下任何输出的张量requires_grad都将为False并且你不能对requires_gradTrue的张量进行原地操作。它带来了比no_grad()更大的性能提升和内存节省。如何选择在训练循环中的验证阶段使用model.eval()和torch.no_grad()。在纯粹的模型部署和推理场景且你确认绝对不会进行任何梯度相关操作时使用torch.inference_mode()。5.5 参数冻结与优化器状态管理当你动态地冻结或解冻参数时例如在课程学习或某些GAN训练策略中优化器的状态管理至关重要。错误做法# 初始状态所有层都可训练 optimizer optim.Adam(model.parameters(), lr0.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, lr0.001) # 使用新的可训练参数列表重建优化器会丢弃旧状态确保优化器只管理当前需要训练的参数这是最清晰、最不容易出错的方式。理解requires_grad_(True)远不止于记住一个API调用。它关乎你对PyTorch自动微分系统工作流的精细控制。从确保梯度能够正确流动到高效地冻结和解冻模型部分再到优化推理性能这个简单的操作背后连接着计算图、内存管理和训练策略等多个核心环节。我个人的经验是每当遇到梯度相关的诡异问题时第一反应就应该是检查相关张量的requires_grad属性这能解决一大半的困惑。把它当成调试梯度问题的“第一把钥匙”你的PyTorch编程水平会扎实许多。