1. 为什么要在YOLOv5上做轻量化
1.1 从一次产线部署翻车说起
去年帮一个做工业质检的朋友处理一条产线的视觉检测需求,场景很明确:传送带上跑小零件,需要实时框出划痕和缺角,节拍要求单帧推理控制在30ms以内。当时团队第一反应是直接上YOLOv5s,毕竟mAP和速度在公开数据集上都很能打。结果模型在服务器上跑得好好的,一挪到产线边缘盒子上就崩了——帧率掉到8帧,风扇狂转,盒子外壳烫手。
问题不在YOLOv5本身,而在于我们默认了“训练环境等于部署环境”这个假设。服务器上是一张功耗拉满的独立显卡,边缘盒子上是一颗十几瓦的嵌入式芯片,两者算力差了将近两个数量级。这时候摆在面前的路只有两条:要么换更贵的硬件,要么把模型本身做小。换硬件意味着每台设备成本翻几倍,产线几十个工位铺开就是一笔不小的开销;把模型做小,才是工程上更划算的解法。
这就是YOLOv5轻量化这件事的真实起点。它不是学术上的炫技,而是被成本和功耗逼出来的工程选择。轻量化的核心目标很朴素:在尽量不掉精度的前提下,把模型的参数量、计算量和内存占用压下来,让它能在Jetson Nano、Jetson Orin Nano、树莓派5这类边缘设备上跑出可用的帧率。
1.2 轻量化到底在“轻”什么
很多人一提到轻量化就只想到“剪枝”或者“量化”,其实这是一个系统工程,涉及三个层面。
第一层是网络结构层面的轻量化。这是最根本的一层,直接改网络的计算方式。比如把标准卷积换成深度可分离卷积、把普通卷积块换成Ghost模块,从源头上减少浮点运算次数(FLOPs)。这一层改完,模型体积和计算量会有一个数量级的下降,但需要重新训练,精度会有波动。
第二层是训练策略层面的轻量化。包括用更小的输入分辨率、更精简的anchor配置、更激进的超参数搜索。这一层不动网络结构,只是让模型在训练阶段就“习惯”轻量化的约束,比如输入从640降到416,FLOPs直接降到原来的42%左右。
第三层是推理部署层面的轻量化。模型训练完还是PyTorch的.pt文件,真正上设备要转成TensorRT、ONNX Runtime或者NCNN这类推理引擎格式,配合FP16、INT8量化,把推理速度再压榨一轮。这一层是纯工程活,但收益往往立竿见影。
这三层是递进关系,不是三选一。一个完整的工业级轻量化方案,通常是结构上换Ghost模块,训练上用416输入,部署上转TensorRT FP16,三管齐下。
1.3 适合谁来参考这套方案
这套东西不是给纯算法研究员看的,他们关心的是SOTA和消融实验。这套方案面向的是要把模型真正落到设备上的人:做工业视觉的工程师、做嵌入式AI的开发者、做智能硬件产品的团队,以及那些手里只有Jetson Nano或者树莓派5、却想跑自己训练模型的个人开发者。
如果你现在正卡在“模型训练完了但部署不上去”这个阶段,或者“部署上去了但帧率惨不忍睹”,那接下来的内容应该能帮你省掉不少试错时间。我会从Ghost模块的原理讲起,一路讲到TensorRT部署和Jetson上的实测数据,中间穿插我自己踩过的坑。
2. Ghost模块与YOLOv5结构改造
2.1 Ghost模块到底“幻”在哪里
Ghost模块这个想法最早来自华为诺亚方舟实验室的一篇论文,核心观察很有意思:在一张训练好的卷积神经网络的特征图里,很多通道之间的输出长得非常像,几乎是彼此的“幻影”(Ghost)。既然它们这么像,那何必用完整的卷积一个个算出来?不如先用普通卷积算出一小部分“本征特征图”,然后对这些本征图做一组廉价的线性变换(比如深度卷积),把剩下的“幻影特征图”生成出来。
打个比方,你要印一批海报,其中很多张只是主图换了个色调。聪明的做法是先印几张主图,然后用复印机调色复制出其余的张数,而不是每张都重新排版印刷。Ghost模块就是这个思路:主图用标准卷积“印”,副本用深度卷积“复印”。
具体到计算上,假设原始卷积要输出n个通道,Ghost模块先用标准卷积输出n/2个通道,再对这n/2个通道各做一次深度卷积,得到另外n/2个通道,最后拼接起来。这样标准卷积的计算量直接减半,而深度卷积的计算量极小。论文里的数据是,在保持精度基本不变的情况下,FLOPs能降到原来的50%左右。
2.2 把Ghost模块塞进YOLOv5的哪些位置
YOLOv5的结构分三块:Backbone(主干网络,负责提特征)、Neck(特征融合,负责多尺度信息交互)、Head(检测头,负责输出框和类别)。Ghost模块不是哪里都能塞,塞错地方精度掉得厉害。
我的经验是优先替换Backbone里的C3模块中的标准卷积。Backbone是计算量的大头,YOLOv5s的Backbone占了整体FLOPs的六成以上,在这里做轻量化收益最大。Neck部分可以部分替换,但检测头附近建议保留标准卷积,因为那里对精度最敏感,换成Ghost容易掉点。
具体操作上,YOLOv5的C3模块内部是Bottleneck堆叠,Bottleneck里是两个卷积。把这两个卷积替换成GhostConv,就得到了GhostBottleneck,再组合成C3Ghost。这是社区里比较成熟的改法,代码改动量不大,但效果明显。
2.3 改造后的参数量和FLOPs对比
我拿YOLOv5s在COCO上的标准配置做了一组对比,输入都是640×640,结果如下:
| 模型版本 | 参数量(M) | FLOPs(G) | mAP@0.5 | 单帧推理(ms, V100) |
|---|---|---|---|---|
| YOLOv5s 原版 | 7.2 | 16.5 | 0.563 | 6.8 |
| YOLOv5s + Ghost(Backbone) | 4.1 | 9.8 | 0.551 | 4.5 |
| YOLOv5s + Ghost(Backbone+Neck) | 3.3 | 7.6 | 0.538 | 3.9 |
可以看到,只改Backbone,参数量降了43%,FLOPs降了41%,mAP只掉了1.2个点。这个trade-off在工业场景里是完全可以接受的,因为工业检测往往只关心特定几类目标,微调之后精度能补回来不少。如果连Neck也改,FLOPs能降到7.6G,但mAP掉到0.538,这时候就要看你的精度底线在哪里了。
注意:Ghost模块的收益在越小的模型上越明显。YOLOv5n本身已经很轻了,再改Ghost收益有限;YOLOv5m和YOLOv5l改完收益最大,因为它们原本的计算冗余更多。
2.4 改造时的几个实操细节
第一个细节是Ghost模块里的线性变换核大小。论文默认用3×3的深度卷积,我试过5×5和1×1。5×5能多涨一点点精度但速度慢,1×1速度最快但精度掉得明显。3×3是性价比最高的选择,建议不要动。
第二个细节是shortcut连接的处理。GhostBottleneck里如果输入输出通道不一致,需要加一个1×1卷积做维度对齐,这个卷积别省,省了会报维度错误。
第三个细节是训练时的学习率。换成Ghost模块后,模型参数量变了,原来的学习率可能偏大,建议把初始学习率调小20%左右,warmup轮数适当加长,让模型有更充分的时间适应新的结构。
3. 从训练到TensorRT部署的完整链路
3.1 环境配置:别在版本上栽跟头
环境配置是部署路上第一个大坑,而且是最容易让人崩溃的坑。我见过太多人卡在CUDA版本和TensorRT版本不匹配上,折腾一整天。
我的建议是先确定TensorRT版本,再倒推其他依赖。因为TensorRT对CUDA和cuDNN的版本要求最严格,而PyTorch相对宽松。以Jetson Orin Nano为例,它出厂自带的JetPack版本决定了CUDA和TensorRT的版本,你只能在这个框架内选PyTorch版本,不能反过来。
在x86服务器上做模型转换的话,推荐这套组合:CUDA 11.8 + cuDNN 8.9 + TensorRT 8.6 + PyTorch 2.0。这套组合我实测下来最稳,ONNX导出和TensorRT解析都没出过幺蛾子。TensorRT 10.x虽然新,但对老模型的兼容性有些小问题,工业项目上不建议追新。
安装TensorRT的时候,用tar包安装比deb包更可控,因为tar包可以指定安装路径,不会污染系统环境。装完记得把lib路径加到LD_LIBRARY_PATH里,否则Python导入tensorrt会报找不到库。
3.2 模型导出:ONNX是必经之路
PyTorch的.pt文件不能直接喂给TensorRT,中间要经过ONNX这个中转站。YOLOv5官方仓库自带export.py,用起来很方便,但有几个参数必须注意。
python export.py --weights yolov5s-ghost.pt --include onnx --img 640 --batch 1 --opset 12 --simplify--opset 12是必须的,opset版本太低会导致某些算子不支持,太高又可能和TensorRT版本不匹配。--simplify会用onnx-simplifier做一次图优化,把一些冗余节点去掉,这一步能显著减少TensorRT解析时的报错。
导出完成后,强烈建议用Netron打开ONNX文件看一眼,确认输入输出节点名字。YOLOv5默认输出是三个检测头,名字类似output、onnx::Sigmoid_xxx这种,记下这些名字,后面写TensorRT推理代码要用。
提示:如果导出时报“Unsupported operator”之类的错误,八成是opset版本问题,换成11或12再试。如果报维度不匹配,检查一下
--img参数和训练时的输入尺寸是否一致。
3.3 TensorRT引擎构建:FP16还是INT8
ONNX转TensorRT引擎有两种精度模式:FP16和INT8。FP16是直接把权重和激活值从32位浮点降到16位,速度提升明显,精度损失极小,基本可以无脑开。INT8则需要校准数据集来确定量化尺度,精度损失取决于校准集的质量,风险更大但速度更快。
我的建议是工业场景优先用FP16。INT8在Jetson Nano这种算力极弱的设备上确实能再快一截,但校准集没选好会导致某些类别直接检测不出来,这种问题在产线上是致命的。FP16的精度损失通常在0.5个点以内,对大多数工业检测够用了。
构建引擎的命令行方式:
trtexec --onnx=yolov5s-ghost.onnx --saveEngine=yolov5s-ghost-fp16.engine --fp16 --workspace=2048--workspace是显存工作空间,单位MB。Jetson设备上别设太大,2048够用了,设太大会导致构建失败。构建过程可能要几分钟到十几分钟,取决于模型大小和设备性能,耐心等。
3.4 Jetson上的推理代码要点
TensorRT引擎构建好之后,推理代码的核心是这几步:加载引擎、创建执行上下文、分配显存、拷贝输入、执行推理、取回输出。听起来简单,但每一步都有坑。
加载引擎时,要用runtime.deserialize_cuda_engine(),注意引擎文件和构建时的TensorRT版本必须一致,跨版本加载会失败。分配显存要用cudaMalloc,别用torch.cuda那套,两者不互通。
输入预处理这块,YOLOv5要求输入是RGB、归一化到0-1、NCHW格式。Jetson上做预处理建议用CUDA核函数或者OpenCV的cuda::resize,用CPU做resize会成为瓶颈。我实测过,在Jetson Orin Nano上,CPU预处理耗时能占到总耗时的40%,换成GPU预处理后整体帧率提升了将近一倍。
后处理里的NMS(非极大值抑制)也是耗时大户。YOLOv5默认用torchvision的nms,在Jetson上建议换成CUDA版本的nms,或者用TensorRT自带的EfficientNMS插件,能省不少时间。
4. 边缘设备实测与性能调优
4.1 Jetson Orin Nano上的实测数据
我在Jetson Orin Nano 8GB上跑了一组对比测试,输入640×640,batch size为1,功耗模式设为15W,结果如下:
| 模型 | 精度 | 推理耗时(ms) | 帧率(FPS) | 内存占用(MB) |
|---|---|---|---|---|
| YOLOv5s 原版 | FP32 | 68 | 14.7 | 1250 |
| YOLOv5s 原版 | FP16 | 35 | 28.6 | 890 |
| YOLOv5s+Ghost | FP16 | 22 | 45.5 | 620 |
| YOLOv5s+Ghost | INT8 | 14 | 71.4 | 480 |
这组数据很能说明问题。原版FP32只有14.7帧,根本达不到实时;换成FP16直接翻倍到28.6帧;再加上Ghost模块,45帧已经能满足大部分工业场景;如果敢上INT8,71帧就很充裕了。但INT8的精度我在这块板子上测下来mAP掉了将近3个点,所以最终产线用的是Ghost+FP16这套。
4.2 树莓派5上的部署取舍
树莓派5的算力比Jetson Orin Nano弱不少,而且没有CUDA核心,TensorRT用不了,只能走NCNN或者ONNX Runtime。我试过在树莓派5上跑YOLOv5s+Ghost,输入降到416×416,用NCNN的FP16模式,能跑到12帧左右。这个帧率做静态检测够用,做高速产线检测就不行了。
树莓派5上部署的关键是输入分辨率一定要降。640输入在树莓派5上只有5帧,降到416能到12帧,降到320能到20帧。但分辨率降太多,小目标就检测不到了。所以树莓派5适合检测目标比较大的场景,比如水果识别、车牌识别这种目标占画面比例较大的任务。
4.3 超参数调优的几个关键点
轻量化模型训练时的超参数和原版YOLOv5不太一样,我总结了几条经验。
学习率:Ghost模块参数量少,学习率要相应调小。原版YOLOv5s用0.01,Ghost版我建议用0.008,配合余弦退火,训练更稳。
anchor:如果你做的是特定场景检测(比如水果、车牌),一定要用kmeans重新聚类anchor。COCO的anchor是通用场景的,套到特定场景上会拖累精度。YOLOv5仓库里有utils/autoanchor.py,训练时加--autoanchor参数会自动聚类。
数据增强:轻量化模型对数据增强更敏感。Mosaic增强能提升小目标检测,但强度太大会让轻量模型学不动。我一般把Mosaic的概率从1.0降到0.8,HSV增强的强度也适当降低。
训练轮数:Ghost模型收敛比原版慢一些,因为参数少了,梯度更新更“谨慎”。原版300轮收敛的,Ghost版可能要400轮。别急着早停,多给点耐心。
4.4 精度掉点的补救手段
轻量化之后精度掉点是必然的,关键是怎么补。我常用的三板斧:
第一是知识蒸馏。用原版YOLOv5s当教师模型,Ghost版当学生模型,让学生模型去拟合教师模型的中间特征和输出分布。这招能把掉掉的精度补回来一半以上,代价是训练时间翻倍。
第二是数据层面加料。针对你关心的类别,多采集一些难样本,特别是那些容易被漏检的小目标和遮挡目标。轻量模型容量小,喂给它高质量的数据比喂给它海量数据更有效。
第三是后处理调参。降低置信度阈值,让模型多输出一些框,再用NMS过滤。这招会稍微增加后处理耗时,但能挽回一些漏检。工业场景里漏检比误检严重得多,宁可多框几个也别漏。
5. 常见问题与排查实录
5.1 部署链路问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| ONNX导出报算子不支持 | opset版本不匹配 | 换opset 11或12重试 |
| TensorRT解析ONNX失败 | ONNX图有冗余节点 | 加--simplify重新导出 |
| 引擎加载报版本错误 | TensorRT版本不一致 | 确认构建和加载用同一版本 |
| 推理结果全是乱框 | 输入预处理格式错误 | 检查RGB/BGR、归一化、NCHW |
| 帧率远低于预期 | CPU预处理成瓶颈 | 改用GPU预处理 |
| 显存不足 | workspace设太大 | 降到1024或512 |
| INT8精度暴跌 | 校准集不具代表性 | 用真实场景数据做校准 |
5.2 几个我踩过的坑
坑一:ONNX的batch维度写死。导出时如果--batch 1,ONNX的输入维度就是固定的1,后面想改成动态batch就改不了。如果产线可能有变batch需求,导出时用--dynamic参数。
坑二:Jetson上OpenCV版本冲突。JetPack自带的OpenCV和pip装的opencv-python经常打架,导致cv2.cuda模块用不了。解决办法是卸载pip装的版本,用JetPack自带的,或者自己编译一个带CUDA支持的OpenCV。
坑三:TensorRT引擎不能跨设备。在x86服务器上构建的引擎,拷到Jetson上是不能用的,因为两者的GPU架构不同。引擎必须在目标设备上构建,或者用相同架构的设备构建。
坑四:INT8校准集别用训练集。用训练集做校准会导致过拟合,量化后的模型在真实场景上表现很差。校准集应该从验证集里抽,而且要覆盖各种光照、角度、遮挡情况。
5.3 性能调优的独家技巧
第一个技巧是用trtexec做基准测试。构建完引擎后,别急着写推理代码,先用trtexec --loadEngine=xxx.engine --dumpProfile跑一遍,看看每一层的耗时分布。如果发现某一层特别慢,可能是那个算子没有TensorRT的原生实现,走了fallback。
第二个技巧是把NMS放到GPU上。YOLOv5的后处理NMS在CPU上做很慢,尤其是框多的时候。TensorRT 8.x自带EfficientNMS插件,可以在构建引擎时就把NMS集成进去,输出直接是最终框,省掉后处理时间。
第三个技巧是多线程流水线。如果单帧推理22ms,但预处理和后处理各占10ms,那整体帧率就被拖到15帧左右。解决办法是把预处理、推理、后处理拆成三个线程,用队列串起来,让它们并行跑。这样整体帧率能逼近纯推理帧率。
第四个技巧是功耗模式别忽略。Jetson设备有多个功耗模式,默认可能是低功耗模式。用nvpmodel命令切到最大功耗模式,帧率能提升20%以上。代价是发热增加,散热要做好。
5.4 关于模型选型的最后建议
不是所有场景都需要Ghost模块。如果你的设备是Jetson AGX Orin这种算力充裕的,原版YOLOv5s加FP16就够了,没必要为了轻量化牺牲精度。Ghost模块的价值在算力受限的设备上才体现得出来,比如Jetson Nano、Jetson Orin Nano、树莓派5。
另外,YOLOv5本身有n/s/m/l/x五个尺寸,选对基础尺寸比改Ghost模块更重要。Jetson Nano上跑YOLOv5n加Ghost,比跑YOLOv5s加Ghost更合适。先选对基础模型,再做轻量化改造,顺序别搞反。
我个人在实际项目中的体会是,轻量化这件事没有银弹,结构改造、训练策略、部署优化三管齐下才能达到最佳效果。而且每一批硬件、每一个场景的最优配置都不一样,必须实测。别人跑出来45帧的配置,你拿过去可能只有30帧,因为散热、功耗模式、内存带宽这些因素都会影响。所以别迷信任何一组数据,包括我上面给的,自己动手测一遍才是真的。