
简介针对YOLOv5目标检测算法的改进需求这份资源包集成了ResNet、ShuffleNet、MobileNet、EfficientNet、HRNet等多样主干网络并加入CBAM注意力机制、DCN可变形卷积以及TensorRT推理优化方案同时提供可直接阅读的源码与配套说明文档便于快速上手。资源共99个文件以54个Python源码和25个YAML配置为主辅以Dockerfile、模型图片、使用说明等辅助资料整体压缩包仅1.34MB目录结构在模型定义、训练、推理部署等方面划分清晰便于按需查阅。面向计算机、电子信息、数学等专业的学生可作为课程设计、期末大作业或毕业设计的参考资料要求使用者具备一定的代码基础能够自行阅读、调试和二次开发。目前已有3933人学习下载。通过该资源读者能快速掌握YOLOv5更换主干网络、嵌入注意力机制与可变形卷积的修改思路同时了解TensorRT部署与量化流程源码中包含训练与评估脚本配置文件覆盖多种模型结构资源还附带不同主干网络的yaml配置示例方便对比实验效果适合用于快速搭建和改进检测模型的实验其中还包含服务化部署参考便于将模型落地到实际推理环境。1. 改YOLOv5主干改的到底是一张什么图做目标检测的项目组十有八九会在某一个版本里给 YOLOv5 换过主干网络。默认的 CSPDarknet 在 COCO 上表现均衡但换到自己的数据集上经常出现两种诉求一是小目标多需要更高分辨率的特征图比如 HRNet二是计算资源受控特别是最终要转 TensorRT 上 Orin 这类设备MobileNet、ShuffleNet 这类轻量网络比默认主干更容易吃满硬件的带宽。把 backbone 换掉并不难难点在于 YOLOv5 的后续流程全部建立在特征图通道和尺度对齐之上neck 的 C3 结构、detect 头的 anchor 分配、以及 TensorRT 导出时的算子兼容性都跟主干直接关联。本文就按实际改模型的流程来拆解这条链路从五类主干网络的接入方法说起再讲 CBAM 注意力机制和 DCN 可变形卷积的加装最后落到 TensorRT 推理部署的验证与坑位。2. 给YOLOv5换主干ResNet、ShuffleNet、MobileNet、EfficientNet、HRNet的接入方法2.1 换主干前先理解 YOLOv5 的配置文件结构YOLOv5 的模型结构由models/yolov5s.yaml这类配置驱动YAML 里backbone和head两段决定了整个网络的连接方式。每一行的格式是[from, number, module, args]其中from表示输入来自哪一层number是模块重复次数module是模块类名args是构造参数。parse_model()在models/yolo.py中执行时会把 YAML 里出现的模块名从globals()字典里取出来所以只要我们在common.py或models/common.py中定义好新的主干类并在 YAML 中正确引用就能在不破坏 detect 层的前提下替换整段 backbone。替换主干常见做法有两种。第一种是把新主干的每一层拆开写进 YAML例如把 ResNet 的 stem 和四个 stage 分别映射为若干行Conv和C3的组合这种方式最透明但维护成本高只要某个 stage 重复次数一变YAML 行数就对不上。第二种是在common.py里封装一个完整的ResNetBackbone类内部直接调用 torchvision 的resnet50(pretrainedTrue)对外只输出给 neck 用的三张特征图。实际工程中第二种方式更省事且预训练权重直接就用上了所以我更推荐这种做法。# models/yolov5s_resnet.yaml核心片段 # 注意这里 depth_multiple 和 width_multiple 仍需保留 # 但 ResNetBackbone 内部不使用这两个缩放因子 depth_multiple: 1.0 width_multiple: 1.0 backbone: [[-1, 1, ResNetBackbone, [50, True]],] # 50表示ResNet深度True表示加载预训练权重 head: [[-1, 1, Conv, [256, 1, 1]], [-1, 1, nn.Upsample, [None, 2, nearest]], [[-1, 3], 1, Concat, [1]], [-1, 3, C3, [256, False]], # 16倍下采样特征 # 后续连接保持默认 ]这段配置的关键点是ResNetBackbone的输出不能只有最后一层特征图YOLOv5 的 neck 需要 P3、P4、P5 三个尺度。如果自定义主干只返回out layer4(x)模型会直接报错。所以ResNetBackbone的forward必须返回一个特征图列表且顺序要跟 YAML 中 head 的from索引对应。# common.py 中添加的 ResNetBackbone 示例 import torch.nn as nn from torchvision.models import resnet50, resnet34 class ResNetBackbone(nn.Module): def __init__(self, depth50, pretrainedTrue): super().__init__() if depth 50: base resnet50(pretrainedpretrained) elif depth 34: base resnet34(pretrainedpretrained) else: raise ValueError(Unsupported depth) self.stem nn.Sequential(base.conv1, base.bn1, base.relu, base.maxpool) self.layer1 base.layer1 # stride 4输出通道 64 self.layer2 base.layer2 # stride 8输出通道 128 self.layer3 base.layer3 # stride 16输出通道 256 self.layer4 base.layer4 # stride 32输出通道 512 def forward(self, x): x self.stem(x) c2 self.layer1(x) # P3 候选stride 8 c3 self.layer2(c2) # P4 候选stride 16 c4 self.layer3(c3) # P5 候选stride 32 return c2, c3, c4代码中forward返回三个张量分别对应 8 倍、16 倍和 32 倍下采样这与 YOLOv5 默认 backbone 输出 P3/P4/P5 的节奏一致。这里没有返回layer4的输出因为对大多数检测头来说32 倍下采样再往后接一层 2048 通道的layer4会显著增加计算量而精度提升有限。如果你用的数据集全是小目标可以考虑把layer2和layer3输出的顺序调一下让 P3 对应layer1但 neck 的通道数对齐工作也要跟着改。自depth_multiple和width_multiple对ResNetBackbone无效因为通道数完全由 torchvision 预训练权重决定。这意味着你的模型 FLOPs 会固定在一个水平上不再像默认 YOLOv5s 那样可以便捷缩放。后续如果感觉模型过大要么换更浅的 ResNet18要么在颈部加1x1 Conv压缩通道。2.2 五类主干的通道与stride对照表换主干时最常犯的错是只改 backbone 不改 neck。不同主干的输出通道数差异很大若不调整 head 首层Conv的输入通道模型会在第一行就报 shape 不匹配。下面是五类主干与 YOLOv5 neck 对接时常选的输出组合通道数按主流公开实现的默认配置整理。主干网络P3输出通道P4输出通道P5输出通道stride 对齐方式ResNet5051210242048取 layer2 / layer3 / layer4ResNet342565121024取 layer2 / layer3 / layer4ShuffleNetV2 (1.0x)2324641024取 stage2 / stage3 / stage4MobileNetV3 Large40112960取 stage5 / stage6 / stage7 输出EfficientNet-B040112320取 stage3 / stage5 / stage7 输出HRNet-W183672144取各分辨率分支的最后一层如果 YAML 头部仍写着width_multiple: 0.5而 neck 的Conv层期望的输入通道数是按照默认 CSPDarknet 计算的那么即使forward返回了三个特征图parse_model构建时也会因通道不一致而在训练前报错。我一般建议所有自定义主干替换后先把width_multiple置为 1.0再根据实际通道数手工微调 neck 的中间层。ShuffleNetV2 移植时还有一个额外工作ChannelShuffle算子不是 torch 内置模块需要在common.py里手写。这个操作本身很简单就是重塑通道维度后做转置但在 TensorRT 导出时它不一定被标准插件支持通常要转成 ONNX 后检查算子映射情况。# ChannelShuffle 的标准实现用于 ShuffleNet 主干 class ChannelShuffle(nn.Module): def __init__(self, groups): super().__init__() self.groups groups def forward(self, x): b, c, h, w x.shape x x.view(b, self.groups, c // self.groups, h, w) x x.transpose(1, 2).contiguous() return x.view(b, c, h, w)使用 ShuffleNet 做主干后在 YAML 的对应 stage 输出位置调用ChannelShuffle即可。值得提醒的是torchvision里没有直接可用的 ShuffleNet 分类器输出接口你必须使用包括 group convolution 在内的完整 stage 实现。如果从开源仓库复制要确认通道数变量名与 YOLOv5 的c2、c3、c4一致避免把 last stage 输出误当成 P5。2.3 预训练权重的挂载路径替换主干的预训练权重加载方式与默认权重不同。YOLOv5 官方权重里的 state_dict 键名是model.0.conv.weight这种格式而 torchvision 的预训练模型键名是layer1.0.conv1.weight。直接把torchvision权重加载进 YOLOv5 会提示大量 missing keys这是正常的说明你的自定义主干已经拿到了属于自己的预训练权重但 detect 头依然需要从头训练。# 在 train.py 的 load 逻辑之前手动将主干权重剥离开 import torch def load_backbone_pretrained(model, backbone_state_dict): model_dict model.state_dict() matched {} for k, v in backbone_state_dict.items(): # common.py 中 ResNetBackbone 的层名前缀 new_key model.0. k if new_key in model_dict: matched[new_key] v model_dict.update(matched) model.load_state_dict(model_dict, strictFalse)这段代码的关键逻辑是遍历主干网络导出的state_dict将键名加上model.0.前缀后匹配到 YOLOv5 模型参数。同理MobileNetV3 和 EfficientNet 的 torchvision 权重也这样做。strictFalse允许 neck 和 head 的参数保持随机初始化。有一个常被忽略的细节使用 BatchNorm 的预训练权重加载后如果主干里存在num_batches_tracked这个 buffer加载时会被一同导入这是正确的。但如果你切换了pretrainedFalse重新训练buffer 不会自动归零建议训练前把主干所有 BN 层的num_batches_tracked清零否则前几轮学习率调度会异常。HRNet 预训练权重一般来自分类任务加载后还需检查主干输出的 stride 是否与 YAML 预期一致因为 HRNet 的高分辨率分支可能改变最终输出尺寸。3. 在YOLOv5中加入CBAM注意力模块通道权重与空间权重怎么算3.1 CBAM的结构一个模块里塞进两种注意力CBAM 全称是 Convolutional Block Attention Module它由两个串联的子模块组成。通道注意力子模块对输入特征图分别做全局平均池化和全局最大池化把空间维度压缩成一个通道描述向量再经过两层全连接或 1x1 卷积映射出通道权重最后用 sigmoid 输出 0 到 1 之间的权重系数。空间注意力子模块则是在通道维度上做平均池化和最大池化将两个结果拼成一个双通道的特征图再用卷积生成空间权重。CBAM 能提升检测精度的原因在于它不需要额外设计复杂的网络结构只是在特征图经过时对通道和空间位置做一次可学习的强调。对目标检测来说通道注意力更能影响小目标回召而空间注意力对遮挡场景更有效。将 CBAM 放进 YOLOv5 的 C3 模块或 neck 中不会影响 anchor 分配逻辑也不需要修改 loss。3.2 CBAM模块的完整实现与三个放置位置先在common.py中加入 CBAM 实现然后把它当作普通模块用于 YAML。常见的放置位置有三处默认 C3 模块的残差分支里、骨干输出到 neck 的过渡层后面、以及 detect 头前最后一层。第一处最常用因为训练成本低且结构侵入小。# common.py 中的 CBAM 模块实现 import torch import torch.nn as nn class ChannelAttention(nn.Module): def __init__(self, in_channels, reduction16): super().__init__() self.avg_pool nn.AdaptiveAvgPool2d(1) self.max_pool nn.AdaptiveMaxPool2d(1) hidden max(in_channels // reduction, 8) self.fc nn.Sequential( nn.Conv2d(in_channels, hidden, 1, biasFalse), nn.ReLU(inplaceTrue), nn.Conv2d(hidden, in_channels, 1, biasFalse) ) self.sigmoid nn.Sigmoid() def forward(self, x): avg_out self.fc(self.avg_pool(x)) max_out self.fc(self.max_pool(x)) return self.sigmoid(avg_out max_out) class SpatialAttention(nn.Module): def __init__(self, kernel_size7): super().__init__() self.conv nn.Conv2d(2, 1, kernel_size, paddingkernel_size // 2, biasFalse) self.sigmoid nn.Sigmoid() def forward(self, x): avg_out torch.mean(x, dim1, keepdimTrue) max_out, _ torch.max(x, dim1, keepdimTrue) concat torch.cat([avg_out, max_out], dim1) return self.sigmoid(self.conv(concat)) class CBAM(nn.Module): def __init__(self, in_channels, reduction16, kernel_size7): super().__init__() self.channel_attention ChannelAttention(in_channels, reduction) self.spatial_attention SpatialAttention(kernel_size) def forward(self, x): x x * self.channel_attention(x) x x * self.spatial_attention(x) return xreduction控制通道注意力的压缩比。默认 16 对中等模型是合理值当in_channels只有 40 或 112 时压缩到 2 个通道会丢失表达能力所以我在代码里加了一个max(hidden, 8)的下界保护。kernel_size控制空间注意力的感受野7x7 能覆盖更大的空间范围但也会增加计算量。在 YAML 中接入 C3 内部时可以创建一个C3CBAM包装类或者更简单的方式是在 neck 的 C3 后单独加一层 CBAM以下是一个可选配置head: [-1, 1, CBAM, [512]], # 在 P4 特征图经过 Concat 后加 CBAM [-1, 3, C3, [512, False]],这段配置的含义是将 concat 得到的 512 通道特征图先经过 CBAM 做通道和空间加权再送入 C3。与把 CBAM 放在 C3 内部相比这种“外挂”方式更容易控制消融实验的公平性。放置位置的实际收益差异需要结合训练轮次判断一般来说放在骨干输出后比放在检测头前效果更稳因为检测头前一层特征图分辨率过低空间注意力能提供的空间信息有限。3.3 CBAM的超参数调整与训练注意点训练加 CBAM 的模型时学习率建议比默认调低 10% 到 20%因为 CBAM 的 FC 层参数会在初期放大梯度的波动。如果使用 AMP 混合精度训练CBAM 的 sigmoid 和 pooling 算子通常不会有数值问题但建议前三轮用纯 FP32 跑通一遍。另一个注意点是 BatchNorm 的动量CBAM 放在主路径后会改变后面的 BatchNorm 统计量更新速度若发现训练 loss 不降可以尝试减小主干和 head 的 BN momentum 差值。4. 在C3模块中集成DCN可变形卷积offset学习与部署边界4.1 DCN可变形的采样改进标准 3x3 卷积在每层使用固定网格采样难以适应具有较大形变的检测目标。DCN 在卷积采样位置基础上额外学习一组 offset让采样点能够随目标形状自适应偏移。对细长目标、密集遮挡目标以及不规则形状的集装箱或车辆检测DCN 的收益比常规注意力模块更直观。缺点是训练不稳定且 offset 生成会额外占用显存。YOLOv5 中集成 DCN 的常规做法不是替换整个 backbone而是替换 C3 模块的残差分支中的 3x3 卷积。这种局部替换减少了训练难度也保留了原始预训练参数的大部分价值。若你的主干本身就是 ResNet可以在 ResNet 的 layer3 和 layer4 中把conv2替换为 DeformConv2d这与后续 TensorRT 的导出兼容性需要提前验证。4.2 基于torchvision.ops实现DCNv2模块torchvision 从 0.10 版本开始内置了deform_conv2d算子我们只需实现 offset 预测部分。这里给出一个可直接用于 YOLOv5 的DeformConv2d模块它接受与nn.Conv2d类似的参数并在forward中调用torchvision.ops.deform_conv2d。import torch import torch.nn as nn from torchvision.ops import deform_conv2d class DeformConv2d(nn.Module): def __init__(self, in_channels, out_channels, kernel_size3, stride1, padding1, dilation1, groups1, biasTrue): super().__init__() self.stride stride self.padding padding self.dilation dilation self.groups groups self.kernel_size kernel_size offset_channels 2 * kernel_size * kernel_size * groups self.conv_offset nn.Conv2d( in_channels, offset_channels, kernel_sizekernel_size, stridestride, paddingpadding, biasTrue) self.weight nn.Parameter( torch.empty(out_channels, in_channels // groups, kernel_size, kernel_size)) if bias: self.bias nn.Parameter(torch.empty(out_channels)) else: self.register_parameter(bias, None) self.reset_parameters() def reset_parameters(self): nn.init.kaiming_uniform_(self.weight, a1) if self.bias is not None: nn.init.constant_(self.bias, 0) def forward(self, x): offset self.conv_offset(x) return deform_conv2d( inputx, offsetoffset, weightself.weight, biasself.bias, strideself.stride, paddingself.padding, dilationself.dilation, maskNone)代码里关键是self.conv_offset的输出通道数必须是2 * kernel_size * kernel_size * groups。如果 DCN 只在 C3 的一个分支中使用offset 卷积的初始化权重建议全零这样初始阶段采样网格与普通卷积一致训练稳定性更好。接入 C3 时你可以仿照原版 Bottleneck 写一个BottleneckDCN最后在 YAML 里把C3替换为C3DCN。最简单的接入是在 YAML 中把C3模块换成C3DCN并在common.py中实现它class BottleneckDCN(nn.Module): def __init__(self, c1, c2, shortcutTrue, g1, e0.5): super().__init__() c_ int(c2 * e) self.cv1 Conv(c1, c_, 1, 1) self.cv2 DeformConv2d(c_, c2, 3, 1, 1, groupsg, biasFalse) self.add shortcut and c1 c2 def forward(self, x): return x self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x)) class C3DCN(nn.Module): def __init__(self, c1, c2, n1, shortcutTrue, g1, e0.5): super().__init__() c_ int(c2 * e) self.cv1 Conv(c1, c_, 1, 1) self.cv2 Conv(c1, c_, 1, 1) self.cv3 Conv(2 * c_, c2, 1) self.m nn.Sequential(*(BottleneckDCN(c_, c_, shortcut, g, e1.0) for _ in range(n)))n表示瓶颈模块重复次数训练自己的数据集时建议内置多个不同尺度目标的特征可以适当增加n值但会显著增加显存占用。一个常用参考是C3DCN 中的shortcut在 stride 改变的层级应设为 False否则维度不匹配会直接报错。4.3 DCN集成后的导出边界DCN 在部署阶段是最大的不确定性来源。torchvision 的deform_conv2d会被 ONNX exporter 映射为DeformConv2d算子但 TensorRT 对该算子的支持取决于版本。TensorRT 8.5 前的版本通常不支持之后的部分版本提供了受限支持且只接受固定的偏移量维度。这意味着你训练出的 DCN 权重不一定能顺利导出engine文件。验证方法很简单导出 ONNX 后用onnxruntime跑一次相同输入再尝试用trtexec转引擎若中途报 unsupported operator 错误就需要回退到普通卷积或用 TensorRT 插件方式实现 DCN。在做项目规划时若目标平台确定是 TensorRT建议先在ultralytics的 ONNX export 流程中试跑一次 DCN 模型确认算子兼容性再投入完整训练。5. TensorRT部署从PyTorch权重导出engine文件的完整操作5.1 导出环境的确认与常用命令TensorRT 部署前需要确认几个版本依赖CUDA 版本决定了 TensorRT 版本TensorRT 8.5 和 8.6 的 API 差异会影响 ONNX 解析器行为PyTorch 的 ONNX 导出版本建议在 1.13 到 2.1 之间配合onnxsimplifier消除冗余算子。以下命令以 YOLOv5 官方仓库的export.py为基础如果你的代码版本较新可能需要在环境中直接执行yolo export命令。# 1. 先从 PyTorch 权重导出 ONNX python export.py --weights runs/train/exp/weights/best.pt \ --include onnx \ --img-size 640 \ --opset 12 \ --simplify # 2. 用 trtexec 将 ONNX 转换为 TensorRT engine trtexec --onnxbest.onnx \ --saveEnginebest_fp16.engine \ --fp16 \ --minShapesimages:1x3x640x640 \ --optShapesimages:8x3x640x640 \ --maxShapesimages:16x3x640x640 \ --workspace4096导出时--opset 12是最稳的选择过高或过低都可能导致 DCN 或注意力模块的算子映射失败。--simplify使用 onnxsim 去除 Constant 节点这对减少 TensorRT 解析失败概率很有帮助。trtexec中的minShapes、optShapes、maxShapes三个参数定义了动态 batch 的范围如果你的部署尺寸固定为 640 且 batch 固定也可以不用动态 shape直接固定 batch 导出以减小引擎体积。如果在trtexec阶段报了不支持算子错误第一步不是去改 TRT 配置而是用polygraphy工具定位到具体 op 名称确认它来自 backbone 还是 CBAM。很多自定义主干中的ChannelShuffle或分组卷积算子在 ONNX 中会展开为多个基础算子TensorRT 8.x 对这些算子的支持度不同通常让 ONNX 输出更简单的结构可以解决一部分问题。5.2 Python端engine推理代码模板导出 engine 后推理代码不再依赖 PyTorch 模型而是从序列化的 engine 中恢复网络。这个流程包括创建runtime、反序列化 engine、创建execution_context、分配 device 内存然后每次推理时把输入图片预处理后拷贝到 device 上执行。import tensorrt as trt import numpy as np import pycuda.driver as cuda import pycuda.autoinit class TRTEngine: def __init__(self, engine_path): self.logger trt.Logger(trt.Logger.WARNING) with open(engine_path, rb) as f: runtime trt.Runtime(self.logger) self.engine runtime.deserialize_cuda_engine(f.read()) self.context self.engine.create_execution_context() self.stream cuda.Stream() self._allocate_buffers() def _allocate_buffers(self): inputs, outputs, allocations [], [], [] for i in range(self.engine.num_io_tensors): name self.engine.get_tensor_name(i) shape self.engine.get_tensor_shape(name) dtype trt.nptype(self.engine.get_tensor_dtype(name)) size trt.volume(shape) size size if shape[0] ! -1 else 1 host cuda.pagelocked_empty(size, dtype) device cuda.mem_alloc(host.nbytes) allocations.append(device) if self.engine.get_tensor_mode(name) trt.TensorIOMode.INPUT: self.context.set_input_shape(name, shape) inputs.append((name, host, device)) else: outputs.append((name, host, device)) self.bindings [d for _, _, d in inputs outputs] def infer(self, input_np): input_name, host_in, device_in self.inputs[0] host_in[:] input_np.ravel() cuda.memcpy_htod_async(device_in, host_in, self.stream) self.context.execute_async_v2(bindingsself.bindings, stream_handleself.stream.handle) cuda.memcpy_dtoh_async(self.outputs[0][1], self.outputs[0][2], self.stream) self.stream.synchronize() return self.outputs[0][1].copy()这里的num_io_tensors是 TensorRT 8.5 之后引入的 API旧版本使用num_bindings和binding获取输入输出信息。如果你的 TensorRT 版本较旧需要替换对应的 API 名称。shape[0] ! -1判断动态 batch 维度动态 shape 下需要先set_input_shape再执行否则引擎会报 invalid shape 错误。预处理与训练时保持一致至关重要。YOLOv5 的推理预处理是BGR 图像 resize 到 640x640除以 255 归一化然后转 CHW 顺序。如果你训练时用了letterbox而部署时用普通 resize检测框坐标会系统性偏移。engine模型不保存预处理参数这属于部署代码职责。5.3 FP16和INT8的选择策略精度速度提升参考精度损失参考适用场景FP321x无模型验证或精度优先FP161.5x-2x通常低于 1% mAP大多数 Orin/Xavier 部署首选INT82x-3x2%-5% mAP算力紧张且可接受校准损失FP16 导出成本最低只要在trtexec中加--fp16即可。如果你的模型中含有Sigmoid或SoftmaxFP16 在极端数值下可能出现精度下降可以尝试在 ONNX 模型中将这些激活改为 FP32 计算TensorRT 支持为特定层设置精度域。INT8 需要额外校准数据集校准图片应从训练集中随机抽 500 到 1000 张覆盖白天、夜晚和不同目标尺寸校准步数建议不低于 500 次过少的校准会造成明显的 mAP 波动。对于带 DCN 的模型INT8 的量化误差通常比普通卷积更大不建议先上 INT8优先以 FP16 作为折中。6. 换主干后的三个落地技巧验收方法、权重对齐与回退决策6.1 用val.py同时验收PyTorch和engine版本训练完成先跑一遍官方验证脚本记录 baseline mAPpython val.py --data data/custom.yaml --weights runs/train/exp/weights/best.pt --img 640 --batch 16engine 出来后也要基于同一组验证图片跑推理并对比输出。比较时重点看两个指标检测框坐标偏差和类别置信度。由于 FP16 的数值误差在第三位小数级别只要 mAP 差在 0.5% 以内均可接受。如果 mAP 骤降 2% 以上先用 FP32 engine 复测若 FP32 正常而 FP16 异常问题出在数值敏感层若 FP32 也异常则要回看预处理差异或 anchor 参数不一致。6.2 预训练权重加载时的state_dict键名核对换主干后最隐蔽的问题是引用了错误的预训练权重。加载状态字典时建议在训练脚本中临时打印missing_keys和unexpected_keys的数量。如果 missing keys 集中在model.0.layer1.0.bn1.weight这类属于 torchvision 主干的位置同时 unexpected keys 一片正义模型的model.0.0.conv.weight说明权重已被正确剥离开这是期望的现象。如果两者数量与预期不符多半是主干封装类里用了与 torchvision 不同的属性命名空间需要回到common.py逐一确认层名。6.3 什么时候该回退默认CSPDarknet自定义主干的收益并非总是正向的。当你的数据集与公开预训练数据集分布差异巨大且训练数据少于 5000 张时ResNet 预训练权重的迁移效果很可能弱于 YOLOv5 自带的 COCO 预训练权重。另一个信号是训练曲线如果前 30 轮 mAP 一直处于低位且 loss 振荡强烈不要强撑到 200 轮先回退到默认主干做一个对照组。回退决策的判据应该是“同数据集、同超参数、同训练策略下 50 轮 mAP 差值”而不是只看 backbone 本身的 FLOPs 大小。本文还有配套的精品资源点击获取