解决使用copy.deepcopy()拷贝Tensor或model时报错只支持用户显式创建的Tensor问题

本文探讨了在PyTorch中使用copy.deepcopy()时遇到的问题,即无法拷贝requires_grad=True的非叶子节点Tensor,并提供了相应的解决办法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

模型训练过程中常需边训练边做validation或在训练完的模型需要做测试,通常的做法当然是先创建model实例然后掉用load_state_dict()装载训练出来的权重到model里再调用model.eval()把模型转为测试模式,这样写对于训练完专门做测试时当然是比较合适的,但是对于边训练边做validation使用这种方式就需要写一堆代码,如果能使用copy.deepcopy()直接深度拷贝训练中的model用来做validation显然是比较简洁的写法,但是由于copy.deepcopy()的限制,写model里代码时如果没注意,调用copy.deepcopy(model)时可能就会遇到这个错误:Only Tensors created explicitly by the user (graph leaves) support the deepcopy protocol at the moment,详细错误信息如下:

 File "/usr/local/lib/python3.6/site-packages/prc/framework/model/validation.py", line 147, in init_val_model
    val_model = copy.deepcopy(model)
  File "/usr/lib64/python3.6/copy.py", line 180, in deepcopy
    y = _reconstruct(x, memo, *rv)
  File "/usr/lib64/python3.6/copy.py", line 280, in _reconstruct
    state = deepcopy(state, memo)
  File "/usr/lib64/python3.6/copy.py", line 150, in deepcopy
    y = copier(x, memo)
  File "/usr/lib64/python3.6/copy.py", line 240, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
  File "/usr/lib64/python3.6/copy.py", line 180, in deepcopy
    y = _reconstruct(x, memo, *rv)
  File "/usr/lib64/python3.6/copy.py", line 306, in _reconstruct
    value = deepcopy(value, memo)
  File "/usr/lib64/python3.6/copy.py", line 180, in deepcopy
    y = _reconstruct(x, memo, *rv)
  File "/usr/lib64/python3.6/copy.py", line 280, in _reconstruct
    state = deepcopy(state, memo)
  File "/usr/lib64/python3.6/copy.py", line 150, in deepcopy
    y = copier(x, memo)
  File "/usr/lib64/python3.6/copy.py", line 240, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
  File "/usr/lib64/python3.6/copy.py", line 180, in deepcopy
    y = _reconstruct(x, memo, *rv)
  File "/usr/lib64/python3.6/copy.py", line 306, in _reconstruct
    value = deepcopy(value, memo)
  File "/usr/lib64/python3.6/copy.py", line 180, in deepcopy
    y = _reconstruct(x, memo, *rv)
  File "/usr/lib64/python3.6/copy.py", line 280, in _reconstruct
    state = deepcopy(state, memo)
  File "/usr/lib64/python3.6/copy.py", line 150, in deepcopy
    y = copier(x, memo)
  File "/usr/lib64/python3.6/copy.py", line 240, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
  File "/usr/lib64/python3.6/copy.py", line 161, in deepcopy
    y = copier(memo)
  File "/root/.local/lib/python3.6/site-packages/torch/_tensor.py", line 55, in __deepcopy__
    raise RuntimeError("Only Tensors created explicitly by the user "
RuntimeError: Only Tensors created explicitly by the user (graph leaves) support the deepcopy protocol at the moment

这个错误简单地说就是copy.deepcopy()不支持拷贝requires_grad=True的Tensor(在网络中一般是非叶子结点Tensor, grad_fn不为None),开始以为真的哪个地方Tensor的requires_grad没有按要求设置,熬了几个夜去检查调试网络代码没发现什么线索很郁闷,后来想既然是copy.deepcopy()里报错的,源码也有那就去它里面debug看是拷贝网络的那部分时抛出的Exception吧,折腾了一阵发现里面这个地方加breakpoint比较合适:

   if dictiter is not None:
        if deep:
            for key, value in dictiter:
                key = deepcopy(key, memo)
                value = deepcopy(value, memo)
                y[key] = value
        else:
            for key, value in dictiter:
                y[key] = value

我这个网络的结构是使用的python dict方式定义的,运行时使用注册机制动态创建出来的,既然是dict,这里的key和value就是对应配置文件里的定义网络每层结构的dict的key和value,在这里加bp可以比较清楚地跟踪看到是在哪个地方导致的抛出Exception,结果发现原因是因为有个实现分割功能的head类的内部有个成员变量保存了这层的输出结果Tensor用于后面计算loss,模型每层的输出数据Tensor自然是requires_grad=True,把这个成员变量去掉,改成forward()输出结果,然后在网络的主类里接收它并传入计算Loss的函数,然后deepcopy(model)就不报上面的错了!

另外,显式创建一个Tensor时指定requires_grad=True(默认是False)并不会导致copy.deepcopy()报错,不管这个Tensor是在cpu上还是gpu上,关键是用户自己创建的Tensor是叶子结点Tensor,它的grad_fn是None,在这个Tensor上做切片或者加载到gpu上等操作得到的新的Tensor就不是叶子结点了,pytorch认为requires_grad=Trued的Tensor经过运算得到新的Tensor是需要求导的会自动加上grad_fn而不管这个Tensor是不是网络的一部分,这时再使用copy.deepcopy()深度拷贝新的Tensor时会抛出上面的错误,看完下面的示例就知道了:

>>> t = torch.tensor([1,2,3.5],dtype=torch.float32, requires_grad=True, device='cuda:0')
>>> t
tensor([1.0000, 2.0000, 3.5000], device='cuda:0', requires_grad=True)
>>> x = copy.deepcopy(t)
>>> x
tensor([1.0000, 2.0000, 3.5000], device='cuda:0', requires_grad=True)
>>> t1 = t[:2]
>>> t1
tensor([1., 2.], device='cuda:0', grad_fn=<SliceBackward0>)
>>> x = copy.deepcopy(t1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/python3.8/lib/python3.8/copy.py", line 153, in deepcopy
    y = copier(memo)
  File "/root/.local/lib/python3.8/site-packages/torch/_tensor.py", line 85, in __deepcopy__
    raise RuntimeError("Only Tensors created explicitly by the user "
RuntimeError: Only Tensors created explicitly by the user (graph leaves) support the deepcopy protocol at the moment

>>> t = torch.tensor([1,2,3.5],dtype=torch.float32, requires_grad=True)
>>> t1 = t.cuda()
>>> t1
tensor([1.0000, 2.0000, 3.5000], device='cuda:0', grad_fn=<ToCopyBackward0>)
>>> x = copy.deepcopy(t1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/python3.8/lib/python3.8/copy.py", line 153, in deepcopy
    y = copier(memo)
  File "/root/.local/lib/python3.8/site-packages/torch/_tensor.py", line 85, in __deepcopy__
    raise RuntimeError("Only Tensors created explicitly by the user "
RuntimeError: Only Tensors created explicitly by the user (graph leaves) support the deepcopy protocol at the moment

>>> t = torch.tensor([1,2,3.5],dtype=torch.float32, requires_grad=False)
>>> t
tensor([1.0000, 2.0000, 3.5000])
>>> x = copy.deepcopy(t)
>>> x
tensor([1.0000, 2.0000, 3.5000])
>>> t1 = t[:2]  
>>> t1
tensor([1., 2.])
>>> x = copy.deepcopy(t1)

为何deepcopy()不直接支持有梯度的Tensor,按理要支持复制一个当时的瞬间值应该也没问题,看到https://discuss.pytorch.org/t/copy-deepcopy-vs-clone/55022/10这里这个经常回答问题的胡子哥给了个猜测:

optimizer: AdamW(lr=0.0001, momentum=0.937) with parameter groups 113 weight(decay=0.0), 138 weight(decay=0.05468750000000001), 134 bias(decay=0.0) Image sizes 640 train, 640 val Using 8 dataloader workers Logging results to runs\detect\train10 Starting training for 600 epochs... Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size 0%| | 0/82 [00:00<?, ?it/s]CARA_Block input shape: torch.Size([14, 64, 80, 80]) CARA_Block output shape: torch.Size([14, 64, 80, 80]) CARA_Block input shape: torch.Size([14, 128, 40, 40]) CARA_Block output shape: torch.Size([14, 128, 40, 40]) CARA_Block input shape: torch.Size([14, 256, 20, 20]) CARA_Block output shape: torch.Size([14, 256, 20, 20]) 0%| | 0/82 [00:01<?, ?it/s] Traceback (most recent call last): File "E:\WanHua Industria Parkl\code\YOLOv11\newmodels\traincus.py", line 8, in <module> model.train( File "D:\Anaconda\lib\site-packages\ultralytics\engine\model.py", line 810, in train self.trainer.train() File "D:\Anaconda\lib\site-packages\ultralytics\engine\trainer.py", line 208, in train self._do_train(world_size) File "D:\Anaconda\lib\site-packages\ultralytics\engine\trainer.py", line 393, in _do_train self.optimizer_step() File "D:\Anaconda\lib\site-packages\ultralytics\engine\trainer.py", line 599, in optimizer_step self.ema.update(self.model) File "D:\Anaconda\lib\site-packages\ultralytics\utils\torch_utils.py", line 544, in update v += (1 - d) * msd[k].detach() RuntimeError: The size of tensor a (32) must match the size of tensor b (80) at non-singleton dimension 3
03-15
<think>好的,我现在遇到了PyTorch训练中的RuntimeError,提示张量尺寸不匹配,具体是"The size of tensor a (32) must match the size of tensor b (80)"。这让我有点困惑,需要仔细分析可能的原因和解决方法。 首先,用户问题提到了EMA(指数移动平均)更新,这可能是一个关键点。在PyTorch中,EMA通常用于模型的平滑更新,比如在YOLO等目标检测模型中常见。错误中的两个张量尺寸不同,a是32,b是80,发生在非单例维度上。需要明确这个错误发生在哪个操作步骤,比如是在计算损失、模型前向传播,还是EMA更新过程中。 根据引用中的类似问题,比如引用[2]和引用[4],张量尺寸不匹配通常出现在损失计算模型输出与目标形状不一致。比如MSE损失要求两个张量的维度匹配。但用户提到的是EMA更新中的问题,这可能涉及到模型参数的同步问题。 可能的原因有几个: 1. **模型参数不一致**:在使用EMA,如果EMA模型和当前模型的参数结构不同,可能在更新过程中导致张量形状不匹配。比如,EMA模型可能没有正确初始化,者在更新过程中某些层的参数被错误修改。 2. **输入数据的批处理问题**:不同批次的输入数据可能导致中间层的输出尺寸变化,尤其是在动态调整网络结构的情况下。例如,如果某些层的输出依赖于输入尺寸,而EMA模型和当前模型处理不同尺寸的输入,可能导致张量尺寸不一致。 3. **张量广播机制问题**:在EMA更新过程中,可能进行了张量运算,而两个张量无法通过广播机制对齐尺寸。比如,如果权重参数在更新形状不匹配,就会报错。 接下来,需要根据这些可能的原因逐一排查。 首先检查EMA模型的初始化是否正确。EMA模型应该是当前模型的深拷贝,确保所有参数初始形状一致。如果EMA模型在初始化没有正确复制模型结构,可能在后续更新出现参数形状不匹配。 其次,检查EMA更新的步骤。EMA通常会根据当前模型的参数和EMA模型的参数进行加权平均。例如,每个参数更新可能是这样的:ema_param = decay * ema_param + (1 - decay) current_param。如果在这个过程中,current_param和ema_param的形状不同,就会触发错误。因此需要确认在更新,两个模型的对应参数形状是否一致。 另外,可能是在训练过程中,某些操作意外修改了EMA模型的参数结构。例如,动态修改了网络结构,者在加载模型权重出错,导致EMA模型的某些层参数被改变,从而与当前模型参数形状不匹配。 此外,还要检查输入数据是否有变化。例如,如果不同的批次包含不同尺寸的图像,而模型结构不支持动态调整,可能导致中间层的输出尺寸变化,从而在EMA模型处理出现形状不一致的问题。但这种情况在常见的EMA应用场景中较少见,因为通常输入尺寸是固定的。 解决方法可能包括: 1. **重新初始化EMA模型**:确保EMA模型是当前模型的深拷贝使用`deepcopy`函数,而不仅仅是赋值引用。这可以避免模型结构不同步的问题。 2. **检查EMA更新代码**:确认在更新参数,遍历的是当前模型和EMA模型的对应参数,并且它们的形状一致。可能需要打印出各层的参数形状,对比是否有差异。 3. **验证输入数据的一致性**:确保所有输入数据的尺寸符合模型的要求,避免因输入尺寸变化导致中间层输出尺寸不同。 4. **使用调试工具**:在EMA更新前后,添加打印语句,输出关键张量的形状,定位具体是哪个层哪个参数导致了尺寸不匹配。 例如,在代码中添加: ```python for (name, current_param), (ema_name, ema_param) in zip(model.named_parameters(), ema_model.named_parameters()): print(f"Current param {name} shape: {current_param.shape}") print(f"EMA param {ema_name} shape: {ema_param.shape}") # 进行EMA更新操作 ``` 这样可以看到是否有对应的参数形状不一致。 另外,参考引用[4]中的情况,损失函数计算维度不匹配,但用户问题发生在EMA更新,所以应聚焦于参数更新步骤。可能是在某些情况下,模型结构被动态修改,者在多GPU训练参数同步出现问题。 总之,关键步骤是确保EMA模型的结构与当前模型完全一致,并且在更新过程中所有参数的形状匹配。如果问题依然存在,可能需要检查是否有代码逻辑错误,比如在训练循环中错误地修改了EMA模型的结构。</think>在PyTorch训练过程中出现`RuntimeError: The size of tensor a (32) must match the size of tensor b (80)`的EMA更新问题,通常是因为EMA模型与当前模型的参数形状不一致导致。以下是具体分析和解决方案: --- ### **原因分析** 1. **EMA模型初始化错误** EMA模型未正确初始化为当前模型的深拷贝,导致参数结构不同步。例如,直接赋值引用而非使用`deepcopy`,初始化后模型结构被意外修改[^2][^4]。 2. **参数更新逻辑问题** 在EMA更新过程中,可能因遍历参数的顺序错乱参数名称不匹配,导致张量形状不一致的参数被错误计算(如将卷积层参数与全连接层参数混合更新)[^3]。 3. **动态修改模型结构** 训练过程中动态调整了当前模型的结构(如添加/删除层),但EMA模型未同步调整,导致参数维度不匹配[^1]。 --- ### **逐步解决方案** 1. **正确初始化EMA模型** 使用`copy.deepcopy`确保EMA模型与当前模型完全独立: ```python import copy ema_model = copy.deepcopy(model) # 初始化EMA模型 ``` 2. **验证参数一致性** 在EMA更新前,检查参数形状是否匹配: ```python for (name_current, param_current), (name_ema, param_ema) in zip(model.named_parameters(), ema_model.named_parameters()): assert param_current.shape == param_ema.shape, f"Shape mismatch: {name_current} vs {name_ema}" ``` 3. **修正EMA更新逻辑** 使用`state_dict`直接更新参数,避免遍历顺序错误: ```python def update_ema(ema_model, model, decay=0.999): with torch.no_grad(): ema_state = ema_model.state_dict() model_state = model.state_dict() for key in model_state: ema_state[key].mul_(decay).add_(model_state[key], alpha=1-decay) ema_model.load_state_dict(ema_state) ``` 4. **检查动态结构修改** 若训练中修改了模型结构(如切换分支),需同步更新EMA模型: ```python # 示例:动态修改后重新初始化EMA model.add_new_layer() # 修改模型结构 ema_model = copy.deepcopy(model) # 重新初始化EMA ``` --- ### **验证与调试** - **打印关键参数形状** 在更新前后输出参数形状以定位问题: ```python print("Current model layer1.weight shape:", model.layer1.weight.shape) print("EMA model layer1.weight shape:", ema_model.layer1.weight.shape) ``` - **最小化测试案例** 构造固定输入,验证EMA更新是否触发错误: ```python x = torch.randn(32, 3, 224, 224) # 固定输入尺寸 output = model(x) ema_output = ema_model(x) # 触发错误检查此处 ``` --- ### **相关案例参考** - 引用[2]指出,YOLOv5中张量维度不匹配常因模型结构未对齐,需检查网络输出通道数是否一致[^2]。 - 引用[4]强调损失函数计算的维度对齐,类似逻辑适用于EMA更新中的参数对齐。 ---
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Arnold-FY-Chen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值