YOLOv5实战:替换主干网络与注意力机制部署全攻略
2026/9/13 17:41:08 网站建设 项目流程

简介:面向计算机、电子信息工程及数学等专业学生,这份压缩包提供了基于YOLOv5的主干网络改进与部署参考资料,涵盖ResNet、ShuffleNet、MobileNet、EfficientNet、HRNet、CBAM、DCN以及TensorRT/Triton/TF Serving等方向,适合课程设计、期末大作业或毕业设计时进行算法对比与工程实现。压缩包共99个文件,以Python源码、YAML配置、说明文档、Dockerfile及少量示例图片组成,整体仅1.34MB,覆盖了模型定义、训练评估、量化部署的关键环节。目前已有3933人学习下载,可见其作为参考资料的实用性。借助结构清晰的目录与注释,读者可以直接查看不同主干对应的配置,结合训练、评估、检测脚本理解改进入口,并按需调整模块;同时,Triton/TF Serving/TensorRT相关文件能帮助快速了解从PyTorch训练到服务化部署的流程,适合具备一定代码基础、希望自行调试与扩展功能的开发者。

1. 从一份改版YOLOv5源码看模型迭代的正确打开方式

拿到这份基于YOLOv5的改进源码包,我第一反应不是看它在COCO上涨了多少点,而是直接翻models下的配置目录。它没有把主干网络写死在C3结构里,而是把ResNet、ShuffleNet、MobileNet、EfficientNet、HRNet全部做成了可切换的yaml,还把CBAM注意力、DCN可变形卷积、TensorRT部署脚本放进同一套工程里。相比网上零散的教程,这种“一个仓库把结构定义、训练、部署串起来”的做法,对做工程落地的人非常友好。你不需要改一堆散落的调用逻辑,只需要改配置、切换权重、重新训练或导出,就能对比不同主干在目标检测任务上的效果。这份资源适合已经跑通原版YOLOv5、想尝试替换主干和算子但不想重写训练管线的开发者,也适合作为课程设计中“模型结构对比实验”的交付基座。

2. 主干网络替换:五个backbone的配置逻辑与选型边界

2.1 先从models下的yaml结构读懂YOLOv5怎么搭骨架

YOLOv5的网络结构完全由yaml文件驱动,这份改进版把主干部分单独抽了出来。看目录下的configs文件夹,里面同时存在model_resnet.yamlmodel_shufflenet.yamlmodel_mobilenet.yamlmodel_efficientnet.yaml等对应文件,每个文件都定义了主干从哪一层开始、在哪一层结束,以及后续neck和head的接入方式。原版YOLOv5的backbone由Conv、C3、SPPF等模块按固定顺序堆叠,换主干本质上是在替换这一整段卷积层组合,但要保证下采样后的特征图stride序列保持一致,否则PANet在拼接时会因为尺度错配而直接报错。

model_resnet.yaml为例,简化后的结构长这样:

# configs/model_resnet.yaml backbone: - [-1, 1, ResNet, [50, [3, 4, 6, 3], True]] # ResNet50,stride从2开始 - [-1, 1, SPPF, [1024]] # 池化后衔接YOLOv5的SPPF head: - [-1, 1, Conv, [512, 3, 2]] # 下采样进入PANet - [-1, 1, BIFPN, [256, 3]] # 颈部特征融合 ...

这里的-1表示输入来自上一层的输出,1是当前模块重复次数,ResNet是注册在models/common.py里的自定义类,中括号内的参数会被透传给这个类。这段配置说明了关键点:YOLOv5不是把完整ResNet网络端到端复制过来,而是只取它的stem + stage2~stage4作为特征提取器,丢弃最后的全局池化和全连接层。替换主干时需要保证它会输出三张不同尺度的特征图,分别对应stride 8、16、32,这样PANet才能按原路径融合。

2.2 ResNet/ShuffleNet/MobileNet/EfficientNet/HRNet的配置差异

把分类网络直接搬来做检测主干时,有三个地方必须重新适配。第一是输入分辨率,YOLOv5默认训练尺寸是640x640,而多数分类预训练权重是在ImageNet的224x224上得到的,直接加载时BatchNorm里的统计量会出现偏差。解法是在模型构建阶段就明确ch=3imgsz=640,并在训练前让模型跑几次前向完成BN统计量校准,也就是源码里warmup阶段做的事。第二是输出通道数,不同主干最后一个输出的channel差异很大:ResNet50为2048,ShuffleNetV2通常是1024,MobileNetV3视配置可能是960或1280,EfficientNet-B0为1280,HRNet-W32为720。所以每个yaml配置里都要有对应的降维层,一般用一个1x1卷积把输出统一到1280或1024,再接SPPF。第三是下采样方式,MobileNet和ShuffleNet大量使用depthwise conv来减参,HRNet则是并行多分辨率分支再融合,这些都会直接影响训练时的显存占用和收敛速度。

这五个主干在实测中的差异可以看这张表:

Backbone名称输出通道(典型值)计算开销排序替换预训练权重难度
ResNet502048
ShuffleNetV21024
MobileNetV3960
EfficientNet-B01280
HRNet-W32720

表格里的“计算开销”不是绝对的,比如EfficientNet-B0参数量不高,但它的Swish激活函数和动态衰减系数会让ONNX导出时多一些操作。实际选型时,先明确部署平台:如果跑Orin或Jetson Nano,优先考虑MobileNetV3与ShuffleNetV2;如果追求精度且显存充足,ResNet50和HRNet更稳。不要一味追新结构,先让主干能顺利过完前向。

2.3 用一段Python检查网络结构是否真的换成功

替换yaml后最容易出的问题是结构定义和预训练权重的shape对不上。我的习惯是先写一段小脚本,打印模型每个检测头的输出尺寸:

import torch from models.yolo import Model cfg_path = 'configs/model_efficientnet.yaml' model = Model(cfg_path, ch=3, nc=80, anchors=3) model.eval() dummy = torch.randn(1, 3, 640, 640) with torch.no_grad(): features = model(dummy) # 三张特征图 for i, feat in enumerate(features): print(f"head {i} output shape: {feat.shape}")

执行这段代码后,如果看到三个输出shape分别是[1, 80, 80, 80][1, 80, 40, 40][1, 80, 20, 20]这样符合预期的维度,说明主干和neck拼接成功。如果某个concat或者bilinear操作报维度错误,说明该backbone的输出通道数和PANet输入层的期望值不一致,需要回到yaml里检查降维层的配置。另一个方法是加载预训练权重时开启严格匹配,打印出未匹配的key列表,凡是出现在列表里的层,基本就是结构定义和权重文件不一致的位置。

提示:替换主干后不要直接拿原版YOLOv5的pth做完整加载,应使用对应主干的分类预训练权重,只加载backbone部分。否则会因为缺C3层而抛出大量unexpected key。

3. CBAM注意力与DCN可变形卷积:在Neck上做小手术

3.1 CBAM的通道与空间权重怎么嵌进YOLOv5的C3模块

CBAM是一个串联的注意力模块,它先对输入特征图分别做通道维度上的全局平均池化和最大池化,通过全连接层生成每个通道的权重系数;再把得到的结果与原始特征相乘,继续在空间维度上对每个位置计算权重。代码里将CBAM封装成一个独立类,放在models/common.py,然后在C3模块的构造函数里增加一个use_cbam开关,控制是否在残差分支的末尾插入。放置位置不同,效果差异明显。如果放在backbone的最后几个Block后面,更多是强化语义特征;如果放在neck的PANet每个stage入口,注意力结果会同时传入上下两条路径,对小目标和遮挡目标更友好。

下面是一个可直接使用的CBAM实现:

import torch import torch.nn as nn class CBAM(nn.Module): def __init__(self, c1, reduction=16, kernel_size=7): super(CBAM, self).__init__() self.mlp = nn.Sequential( nn.Conv2d(c1, c1 // reduction, kernel_size=1, bias=False), nn.ReLU(inplace=True), nn.Conv2d(c1 // reduction, c1, kernel_size=1, bias=False) ) self.spatial_conv = nn.Conv2d(2, 1, kernel_size=kernel_size, padding=kernel_size // 2, bias=False) self.sigmoid = nn.Sigmoid() def forward(self, x): # 通道注意力 avg_pool = torch.mean(x, dim=(2, 3), keepdim=True) max_pool = torch.max(x, dim=2, keepdim=True)[0].max(dim=3, keepdim=True)[0] channel_weight = self.sigmoid(self.mlp(avg_pool) + self.mlp(max_pool)) x = x * channel_weight # 空间注意力 avg_spatial = torch.mean(x, dim=1, keepdim=True) max_spatial = torch.max(x, dim=1, keepdim=True)[0] spatial_weight = self.sigmoid(self.spatial_conv(torch.cat([avg_spatial, max_spatial], dim=1))) return x * spatial_weight

reduction是通道压缩比例,一般取16或8。压缩过小会导致MLP参数量变大,压缩过大则丢失通道间的非线性关系,影响权重表达能力。kernel_size是空间卷积核大小,7x7覆盖范围更大,适合大目标;如果是密集小目标场景,3x3会更稳定。嵌入时我通常把CBAM放在残差分支相加之后、下一个模块之前,这样梯度回传路径更清晰,不会干扰原始残差块的恒等映射。

3.2 DCN可变形卷积的offset学习与采样位置

DCN的核心思想是让卷积核的采样位置不再固定在一个矩形网格上,而是通过额外一层卷积输出每个采样点的偏移量,再对输入特征图进行双线性插值采样。注意,这里的offset是随输入动态变化的,不同目标在不同位置就会产生不同的卷积核采样区域,特别适合目标形变和尺度差异明显的检测任务。这份源码里没有重写采样逻辑,而是直接封装了torchvision.ops.DeformConv2d

一个最小可用的DCN封装如下:

from torchvision.ops import DeformConv2d class DeformableConv(nn.Module): def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, padding=1): super(DeformableConv, self).__init__() self.offset_conv = nn.Conv2d( in_channels, kernel_size * kernel_size * 2, kernel_size=kernel_size, stride=stride, padding=padding ) self.conv = DeformConv2d(in_channels, out_channels, kernel_size=kernel_size, stride=stride, padding=padding) self.relu = nn.ReLU(inplace=True) def forward(self, x): offset = self.offset_conv(x) # 产生每个采样点的x,y方向偏移 return self.relu(self.conv(x, offset))

这里offset_conv的输出通道数是kernel_size * kernel_size * 2,表示3x3卷积核的9个采样点,每个采样点有横纵两个方向的偏移。初始化时要特别注意:offset_conv的权重应该初始化为零,这样初始offset就是0,等价于普通卷积,避免模型一开始就出现不可控的采样位置。训练时,如果整个网络都用DCN,offset层的学习率需要调低到原来的0.1倍,或者把它绑定到普通相关层的参数组中。更稳妥的做法是只在主干最后一个stage和neck部分使用DCN,感受野的适应性已经足够。

3.3 修改后怎么验证参数量和感受野变化

加完CBAM和DCN不能只看最终mAP,要先验证模型复杂度有没有失控。我会用下面的方式统计新增参数量:

def count_params(model): return sum(p.numel() for p in model.parameters() if p.requires_grad) model = Model('configs/model_resnet_cbam_dcn.yaml', ch=3, nc=80) print(f"total params: {count_params(model) / 1e6:.2f}M")

感受野验证上,普通卷积的感受野可以用理论公式计算,但DCN的采样位置是动态的,不能用理论感受野描述。我常用一个简单手段:生成一张全零图片,只在某个位置加一个亮斑,观察网络某一层输出的响应是否扩散到预期区域;多次随机初始化后看扩散范围是否稳定。如果同一个亮斑在两次前向中引起的响应范围差异很大,说明offset学习不充分,需要降低学习率或者训练更长的时间。

4. 训练自己的数据集:超参、anchor和数据增强的配合

4.1 数据布局与data.yaml的坑

把模型结构改完后,接下来是训练。这份源码遵循YOLOv5标准的目录结构,训练前需要把图片和标签按下面的方式组织:

datasets/ mydata/ images/ train/ *.jpg val/ *.jpg labels/ train/ *.txt val/ *.txt

每个txt文件的内容是class x_center y_center width height,所有坐标必须归一化到0~1。这里最常见的坑有两个:一是标签文件里出现空行或多余空格,会让数据加载线程直接卡死;二是框的坐标越界,比如宽度大于1,虽然训练时YOLO不会报错,但loss会出现较大波动。我写的一段预处理脚本片段可以避免大部分格式问题:

def fix_labels(label_path, img_w, img_h): with open(label_path, 'r') as f: lines = f.readlines() fixed = [] for line in lines: parts = line.strip().split() if len(parts) != 5: continue cls, xc, yc, w, h = map(float, parts) xc, w = max(0, min(1, xc)), min(0.5, w) yc, h = max(0, min(1, yc)), min(0.5, h) fixed.append(f"{int(cls)} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}") with open(label_path, 'w') as f: f.write("\n".join(fixed))

data.yaml文件里主要设置trainvalnc三个字段。源码的加载逻辑要求路径必须存在,推荐用相对于项目根目录的路径,不要用绝对路径。如果你复制项目到服务器后发现图片报错,大多数情况是data.yaml里的路径没有同步改。

4.2 hyp.scratch.yaml里值得动的几个超参数

这份改进包的hyp.scratch.yaml配置了基础学习率、动量、权重衰减以及一系列数据增强强度。借用它的默认配置时,我重点关注以下参数:

参数名默认值我的调整建议
lr00.01主干换成ResNet50或HRNet时,最好降到0.003,避免破坏预训练特征
lrf0.2控制最终学习率相对初始学习率的比值,一般不动
momentum0.937默认值稳定,改小通常不涨点
weight_decay0.0005加入DCN后可以降到0.0003,缓解offset层的过拟合
hsv_h0.015提升在逆光和暗光场景下的鲁棒性
fliplr0.5检测左右不对称目标时建议降为0.3
mosaic1.0前80个epoch开启,最后10个epoch可以关闭以稳定真值分布

lr0是最重要的参数。如果你加载的是ImageNet预训练权重,lr0太大容易把主干中的预训练特征“冲掉”,导致前期训练loss不降、验证集mAP震荡;如果从零开始训练,反而可以设置高一些。mosaic需要留个心眼:它虽然能极大丰富图像上下文,但在目标尺度分布极不均匀的数据集上,会引入大量小目标的合成样本,导致最终模型在真实数据的小目标上反而变差。我的做法是训练过半后逐步降低mosaic概率。

4.3 autoanchor与训练命令的实际用法

在启动训练前,YOLOv5会根据数据集重新计算anchor。源码里的autoanchor.py会对训练集的所有标注框做k-means聚类,找到适合当前数据集的先验框大小。换backbone不会改变特征图的stride序列,所以anchor一般不用改;但如果你用了EfficientNet这种使用动态分辨率策略的主干,最好在train.py里设置--noautoanchor先手动检查一次anchor的召回率。

训练命令和原版基本一致:

python train.py \ --cfg configs/model_mobilenet.yaml \ --data data.yaml \ --weights mobilenetv3_pretrained.pt \ --img-size 640 \ --batch-size 32 \ --epochs 100 \ --hyp hyp.scratch.yaml \ --cache-images

--weights参数可以传原版YOLOv5权重,也可以传对应主干的分类权重。源码内部会解析权重文件的key,只加载匹配的层。--cache-images会把全部训练图片加载进内存,适合图片数量不大的数据集,可以显著减少磁盘IO等待。训练过程中如果发现某个类别一直漏检,先检查label文件里该类别的框数量,再看该类别在class_loss中的占比,不要一上来就改anchor或加权重。

提示:训练ResNet或HRNet这类深层主干时,BatchSize设得太小而BN统计量不稳定,建议从16起步;显存不足时优先减少epoch而不是减少batch_size,否则最终mAP会掉很多。

5. TensorRT部署:从PyTorch到engine的完整链路

5.1 为什么要量化,int8与fp16的取舍

训练好的PyTorch模型直接部署到边缘设备上,通常达不到实时帧率。TensorRT会把推理图做层融合、算子替换和显存复用,其中最有实用价值的是精度裁剪。fp16可以把模型体积减半,推理速度提升约1.5~2倍;int8在支持TensorCore的设备上可以再快一倍以上,但会引入量化误差。这份源码里有trt_quant目录,就是为了做PTQ(训练后量化)。选择量化精度时,我建议先在验证集上评估fp16的mAP损失,如果fp16掉点少于0.5%,再考虑int8;否则说明这个改进网络对精度裁剪太敏感,强行int8会让你花大量时间在找量化敏感层上。

5.2 trt_quant与deploy脚本的使用过程

deploy.py脚本负责把PyTorch的pth权重转成TensorRT的engine文件。整个链路是pth → ONNX → TensorRT engine。一个比较完整的转换命令是:

python deploy.py \ --weights runs/train/exp/weights/best.pt \ --cfg configs/model_shufflenet.yaml \ --img-size 640 \ --batch-size 1 \ --int8 \ --calibrator-path data/calibrator.txt

脚本内会先调用torch.onnx.export,导出时设置了opset_version=12,这是TensorRT对ONNX算子支持度较高的一个版本。如果你使用更新的框架,ONNX导出时可能会遇到ReduceSumResize的版本兼容问题,需要手动指定opset。--calibrator-path里的每一行是一张校准图片的绝对路径,校准集不需要标签,但图片内容必须覆盖训练数据的分布。校准集数量建议在200~1000张之间,太少会导致int8量化阈值估计不准,太多又拖慢转换速度。

转换成功后,你会在同目录得到一个.engine文件。推理时使用Python加载:

import tensorrt as trt runtime = trt.Runtime(trt.Logger(trt.Logger.WARNING)) with open("model.engine", "rb") as f: engine = runtime.deserialize_cuda_engine(f.read()) context = engine.create_execution_context()

这里deserialize_cuda_engine需要显卡驱动版本与TensorRT运行库匹配,否则会抛出cuda error。加载engine后,输入数据必须通过cuda.mem_alloc分配到显存,再用context.execute_async异步推理,这套流程和PyTorch的tensor操作完全不同,习惯后会发现显存占用和延迟更加可控。

5.3 TensorRT版本兼容与动态尺寸问题

现在很多人的卡点在Orin上TensorRT版本不匹配。不同版本TensorRT对ONNX算子的支持程度差异很大,特别是DCN的DeformConv自定义节点,原生TensorRT并不支持。如果在yaml里启用了DCN,导出ONNX前需要先把这些层替换为普通卷积,否则会在构建engine时报“op not recognized”。我的做法是保留一份“部署专用配置”,把model_yolo_dcn.yaml中的DCN降级为普通卷积,同时将CBAM保留,因为CBAM在ONNX中可以拆解为标准卷积和sigmoid算子。

动态输入尺寸也是一个容易踩的坑。如果你的检测任务需要支持不同分辨率的输入,必须在导出时定义optimization profile:

profile = builder.create_optimization_profile() profile.set_shape("input", (1, 3, 640, 640), (1, 3, 640, 640), (4, 3, 640, 640)) config.add_optimization_profile(profile)

其中三个shape分别表示最小尺寸、最优尺寸和最大尺寸。最优尺寸的batch数不要设太大,否则TensorRT会为每个尺寸预分配显存,造成浪费。如果你只是在固定端到端分辨率下部署,直接用固定shape就好,能省下内存占用并加快转换速度。

另外,源码里还有triton_server_deploytf_serving_deploy目录。前者适合需要动态batch和多模型管理的场景,后者适合你已经把模型导成tensorflow serving格式的情况。正常情况下,单卡部署只需用deploy.py生成的engine,不要把Triton和服务模型混在一起。

6. 更快验证改进效果:用detector.py做A/B对比的四个技巧

6.1 固定随机种子与eval模式

对比两组模型时,第一件事是固定随机源。detector.py里的推理脚本默认会关闭BatchNorm和Dropout,但如果你用了新主干,某些模块可能带training=True的默认状态,导致结果不可复现。我一般在调用前加上:

import torch torch.manual_seed(0) torch.cuda.manual_seed_all(0) model.eval()

固定种子后,同样的图片在每次推理中得到的预测框坐标和置信度应该完全一致。如果两次结果不同,优先检查模型里是否有多余的随机操作,比如nn.Dropout在eval模式下没有关闭。

6.2 用同一张图对比预测框和置信度

对比base模型和改进模型时,直接分别跑一遍检测结果,再交叠显示:

python detector.py --source test.jpg --weights base.pt --conf 0.25 --save-txt python detector.py --source test.jpg --weights improved.pt --conf 0.25 --save-txt

比较的重点是三个地方:检测框数量是否增加;新增的框置信度是否在0.3~0.6之间;原来就有的框位置是否发生偏移。如果改进模型只是把所有框的置信度普遍抬高0.05,并没有产生新的正确检测,那说明改进模块没有带来实际增益,只是把原有特征图的响应放大了。反过来,如果在低置信度区域多出了大量无意义的框,可能是CBAM加得太多,网络对背景纹理过于敏感。

6.3 统计每层耗时定位backbone还是head的瓶颈

模型结构改了,推理耗时也要对照。detector.py里没有现成的逐层计时工具,但可以自己写个简单包装:

import torch from models.yolo import Model model = Model('configs/model_resnet.yaml', ch=3, nc=80) model.eval().cuda() dummy = torch.randn(1, 3, 640, 640).cuda() with torch.no_grad(): for name, module in model.model.named_modules(): if hasattr(module, 'forward'): t0 = torch.cuda.Event(enable_timing=True) t1 = torch.cuda.Event(enable_timing=True) t0.record() _ = module(dummy) t1.record() torch.cuda.synchronize() print(f"{name}: {t0.elapsed_time(t1):.2f} ms")

注意这里对每个模块重复前向时,输入都是同一个dummy,所以得到的是单层理论耗时。实际推理时层与层之间会重叠执行,但通过这个表能快速看出是backbone里的残差块耗时大,还是neck阶段的concat和卷积耗时大。如果瓶颈在residual block,考虑用MobileNet的depthwise结构替代标准卷积;如果瓶颈在neck,则优先减少PANet的通道数,而不是动backbone。

最后,把改进前后的耗时和mAP放在同一张表里,决定是否值得把某个模块留在正式模型里。对比时统一用同一张卡、同一batch size,并且关闭所有日志输出,不然小的耗时波动会干扰判断。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询