前向传播中,变量版本错误

报错信息

RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation

错误原因解释:

在神经网络的训练过程中,我们通常使用反向传播算法来计算损失函数(loss function)对模型参数的梯度。计算梯度是优化这些参数,降低损失函数值的关键步骤。为了找到每个参数的梯度,PyTorch 会追踪并记录在前向传播(即从输入到输出的计算过程)期间对每个参数所进行的操作。

现在,假如你在前向传播期间,有如下一个操作:

python

x = x + 1

这个操作是安全的,因为它创建了一个新的变量来存储 x + 1 的结果,同时原来的 x 不会被更改,它的历史操作记录也会被保留。

然而,如果你改为:

python

x.add_(1)

或者

python

x += 1

这两个操作实际上都在原来的 x 内存位置上直接修改了它的值,这被称为"原地操作"(inplace operation)。在这种情况下,x 的旧值会丢失,这对于反向传播计算梯度是有问题的—因为在计算梯度时,需要前向传播中所有原始值。

现在,当你运行 loss.backward() 并遭遇这个错误时,它实际上是表示在这个损失函数对应的计算图中,某个变量的值已经被原地修改,使得PyTorch 不能再追踪其梯度。由于原地修改,变量的版本已经不同于期望的版本,就会出现版本不匹配的错误。

为了找到并修复这个问题,你可以采用下列步骤:

  1. 启用异常检测。通过添加 torch.autograd.set_detect_anomaly(True),你可以在错误发生时得到更详细的调试信息,这有助于准确找到问题相关的操作。
  2. 检查代码中所有的原地操作。找到所有带有 _ 后缀的操作,以及任何可能改变已有变量值而不创建新变量的代码片段。
  3. 修改这些原地操作。一般来说,你可以创建一个新的变量来存储计算结果,而不直接修改已经存在的变量,比如将 x.add_(1) 改成 x = x + 1
  4. 重新运行代码。在修正了所有原地操作之后,再次执行代码。这时应该不会再遇到同样的错误。

通过这些步骤,你应该可以定位到问题所在,并将其修复。记住,关键是避免在前向传播过程中做任何会干扰或破坏梯度计算所需历史记录的原地操作。

我的实践

使用了torch.autograd.set_detect_anomaly(True)的方法,放在model.train()代码之前,即所有模型训练过程之前,但是仍然无法在控制台的报错信息当中看到应该有的效果,下面是可能具有的效果:

torch.autograd.set_detect_anomaly(True) 是一个调试工具,它会在计算图中的异常发生时提供额外的信息,特别是对于那些不容易观察到的梯度相关问题。

这段代码的作用是让 PyTorch 在执行反向传播(即 .backward() 函数)过程中,如果发现任何梯度计算异常,就会立即抛出错误,并在错误回溯中显示详细信息。这包括变量版本不匹配、梯度计算中的原地操作等问题。

当你启用了这个功能,并且运行你的代码遇到异常时,PyTorch 将会反馈一个错误信息,通常附带一个回溯(traceback)报告,向你展示导致错误的操作来自哪些行代码。通常,这个回溯会列出一系列函数调用,直到遇到执行失败的操作为止。

下面是一个简化的例子,模拟了如何解释回溯报告:

plaintext

RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation ...
Traceback (most recent call last):
  File "<ipython-input-1-...>", line 1, in <module>
    loss.backward()
  ...
  File "script.py", line 12, in <module>
    x = some_function(x)
  ...
  File "script.py", line 27, in some_function
    x += 1  # 这是一个原地操作

在这个例子中,你应该查看文件 script.py 的第 27 行来找到可能改变了 tensor x 的操作。如果你在这个位置发现了一个 += 操作(或其他任何原地操作),这就很有可能是导致问题的根源。你需要修改这些操作,如将 x += 1 更换为 x = x + 1 以避免原地操作。

所以,请重新运行你的代码,并仔细查看报错信息中随后的回溯部分,寻找它提示的特定文件和代码行号,那里将显示调用堆栈以及最后一个有效操作,这有助于确定引发异常的确切位置和原因。根据回溯中的具体信息,你可以检查和修复相关的代码。

最后成功的解决方式:

使用了下面的解决方式中的增加异常捕捉功能。

如果你已经启用了 torch.autograd.set_detect_anomaly(True),但异常的回溯报告(stack trace)并没有提供足够的信息来定位问题,这可能是因为错误信息被限制在了标准的异常输出里,有时可能不够详尽。

在这种情况下,有几个步骤可以尝试获取更多信息:

  1. 检查完整的控制台日志:有时整个回溯在你的控制台是可用的,但由于长的输出被截断或者滚动过快,你可能错过了一些信息。确保你检查了控制台的完整输出。

  2. 增加异常捕捉:你可以尝试在代码中添加一个 try...except 块来捕捉 RuntimeError,然后在异常处理中打印出错误的完整回溯信息。例如:

python

   try:
       # 这里是可能会导致异常的代码
       loss.backward()
   except RuntimeError as e:
       print(e)
       # 打印详细的回溯
       import traceback
       traceback.print_exc()
  1. 程序输出重定向:有时IDE的控制台可能有限制,你可以通过命令行运行你的脚本,并将输出重定向到文件中,这样就可以保留完整的回溯信息。例如:

sh

   python your_script.py > output_log.txt 2>&1

这条命令运行 your_script.py 并将所有输出(包括错误信息)都重定向到 output_log.txt 文件中。

  1. 检查IDE设置:如果你正在使用IDE(例如PyCharm等),它可能有日志输出的界限设置。你需要检查IDE的设置,确保日志长度等没有限制,或调高这些限制,以便能捕获更多的输出信息。

  2. 静态检查你的代码:逐行或区域检查你的代码,特别关注那些可能进行修改变量的操作。寻找带有 _ 后缀的操作、使用了 inplace=True 参数的函数调用,以及其他可能会在原址修改变量的代码。

尝试这些步骤,以期能够给你提供更多的信息来调试现有的问题。如果依然找不到问题的关键所在,你可能需要分离代码并逐步构建简单的测试用例,逐一排查直至可以复现问题的最小代码集,以此来辅助定位问题的根源。

好像解决了?

使用异常捕捉的方式只能知道变量的类型和长度,比如说给我的信息反馈是[torch.FloatTensor [3]] is at version 6; expected version 0 instead. 

但是通过这样的信息已经能够推测出大概是哪个变量出错了,因为有变量的类型和长度,然后在可疑变量可能发生版本改变的前后使用代码:

print("变量X的版本:", x._version)

来检查,就能够定位到是哪个变量了。

我的问题变量是一个数组

由于这是原地操作,会改变版本计数器,如果class weight被设置为

requires_grad=True

那么它就会会被需要用来计算梯度,所以在反向传播过程中,代码并不知道应该选择哪个版本的class weight。

在这里,我是生成class weight的时候忘记写上不需要梯度了,所以在反向传播loss.backward()的时候,它也被当做模型需要自动去学习调整的参数。

转折

但是,又发现并不是在生成class weight变量时候使得它需要计算梯度,因为在用torch.FloatTensor()生成变量的时候,默认就是不需要计算梯度的。
但是通过修改关于class weight变化的部分代码,改成:
之后,并不会再出现报错。
肯定是class weight的原因。下面真正揭示了class weight本身是不需要计算梯度的,但是它的原地改动仍然影响了反向传播的计算图的原因:

再次查阅你对修改的代码描述,我猜测原因可能是class_weights可能在它创建后被用于构建了某些需要梯度的操作,或者与其他需要梯度的张量有某种关联,尽管它自身不需要梯度。举个例子,如果class_weights被用作某个操作的参数,并且该操作的输出张量要参与梯度计算,那么即使class_weights本身不需要梯度,原地更新class_weights也可能抛出错误,因为这样会影响其它依赖于这些参数的计算节点的梯度

举个具体的例子,如果在构建计算图时,涉及到class_weights的操作后续会用于梯度计算,那么对class_weights的原地修改可能会破坏计算图中的依赖关系,因此会抛出错误。这就解释了为什么将class_weights克隆并且分离后再修改就没有问题:这样做没有破坏已存在的计算图,因为你对它的操作是发生在一个完全独立的数据副本上。

总结

原来的代码片段:

python

# 直接在class_weights上操作,这可能会引起就地(in-place)修改错误,特别是如果class_weights是一个需要梯度的张量
for i in range(len(class_weights)):
    # some operations on class_weights

改动后的代码片段:

python

# 创建了class_weights的副本,并且调用了.clone().detach(),这使得updated_class_weights成为了一个新的张量,它和原来的class_weights不共享内存
updated_class_weights = class_weights.clone().detach()
for i in range(len(class_weights)):
    # operations on updated_class_weights
# ...
class_weights = updated_class_weights

当你在Python中使用clone().detach()时,你实际上是创建了原始数据的一个克隆副本,并且这个副本是从计算图中分离的,也就是说,它不会参与梯度传播,因此对它的任何修改都不会影响原始数据的梯度计算。

现在这个报错RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation意指在梯度计算过程中,如果一个变量被原地(in-place)修改了,那么它可能会损坏反向传播过程中的计算图。

因此,当你将原来的代码改为使用克隆并分离的副本进行操作时,这样的修改不会影响到计算图,因为更新操作是在一个新的、不记录梯度信息的张量上执行的。所以,当你在背景计算时使用.backward(),计算图是完好无损的,不会因为原地修改操作而受到影响。

  • 53
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值