首页 / 资讯中心 / 文章详情

YOLO13-C3K2-AdditiveBlock:水果质检模型改进与部署实战

YOLO13-C3K2-AdditiveBlock:水果质检模型改进与部署实战 ★ FEATURED ARTICLE
1. 为什么水果质检要折腾到第三版先从一次实际误检说起做水果外观质量检测这件事听起来比工业零件检测简单真正跑起来才发现完全不是那么回事。我在某地的果蔬分拣线上蹲过几天传送带速度调到每秒0.5米每分钟有两百多个果子从相机底下过每个果子都得判断表皮有没有碰伤、腐烂、畸形。工业件的背景是干净的金属和塑料水果却带着反射、阴影、果柄、灰尘还有不同品种天生的颜色差异。这些问题堆在一起普通目标检测模型很容易把“轻微碰伤”漏掉或者把“果皮上的水渍”当成腐烂斑块。第一版我直接用了现成的预训练YOLO模型训练集用的是市面上的公开水果数据集。效果在展示PPT上很好看一上产线就现了原形黄昏时分的检测率直接崩了因为公开数据集的图像大多是电商拍摄的白底棚拍图果子的角度、光照和实际产线完全不同。第二版我加了采集自真实分拣线的数据把背景和光照拉齐但模型在“完好果”和“轻微碰伤果”这两类之间的区分依然不稳定表现为召回率上去了精确率掉得厉害操作员最烦的就是模型把好果子判成坏果子——线下复检的人力成本反而更高。这第三版之所以叫YOLO13-C3K2-AdditiveBlock是我把网络结构做了两处手术一个是把主干部分的关键模块换成C3K2另一个是在检测头之前插入AdditiveBlock。目标很明确——在不大幅增加推理耗时的情况下把“局部微小瑕疵”这一类让前两版翻车的目标捞出来。这篇主要是记录这次改动的完整思路、踩过的坑和最终落地数据给做农业视觉检测的同行参考尤其是那些在“实验室指标很好、产线用不起来”边缘反复横跳的人。2. 网络结构改动拆解C3K2和AdditiveBlock各自负责什么2.1 C3K2模块的通道拆分思路先聊C3K2。熟悉YOLO发展史的人一看这名字就知道它是C3和C2f的混合体。原始的C3模块把特征图沿通道拆成两路一路做Bottleneck堆叠另一路直接短接最后再来一次channel concat。C2f则更进一步把Bottleneck的每一层输出都拼进最终concat列表起到了类似DenseNet的特征复用效果。C3K2的做法是保留了C2f这种“多路输出汇聚”的骨架但把里面的标准Bottleneck替换成了两个不同感受野的分支。K2里的2指的就是内部有两个并行的卷积分支一个用3x3卷积提取局部纹理一个用5x5卷积尽量捕捉稍大范围的表皮损伤。水果碰伤不是工业件上那种明显缺角它往往是一块颜色变深、边界模糊的区域3x3的视野只能看到一小块5x5能连上周围健康区域的纹理判断“这里到底是不是异常”这样对边界模糊的缺陷更友好。我试过把5x5拆成两个3x3串联理论上感受野等价但实际测下来精度略低0.3个点左右的mAP50。原因推测是5x5分支的几个权重参数直接作用于原始特征梯度传播路径更短对细小缺陷的响应更敏感。这个取舍没有绝对的对错但在水果这类类间差异小、类内差异大的数据上保持直接的大核分支是值得的。可参考的配置如下表注意我只在C3K2内部改卷积核心没动外部的CSP结构。C3K2结构参考输入尺寸为示例: 输入特征图 - 1x1 Conv - 通道两路拆分 一路 - 3x3 Bottleneck 分支堆叠2次 另一路 - 5x5 Bottleneck 分支堆叠1次 两路输出 残差支路 - Concat - 1x1 Conv 融合 - 输出2.2 AdditiveBlock加性融合的计算细节AdditiveBlock这名字听着唬人实际解决的是一个很直白的问题特征图在检测头之前做融合时到底用什么方式最好。常规做法是Concat把多层级特征沿通道维度拼起来让后续1x1卷积自行学习权重。但Concat有两个天然劣势通道数翻倍意味着后续卷积计算量更大更深层的问题是多层特征之间的重要性是动态变化的Concat默认它们同等重要要靠大量训练数据把重要性权重“磨”出来。AdditiveBlock的思路是引入加性注意力——不是把通道权重相乘而是算出一组加性的权重偏置再叠加到特征上。用公式表达就是输出的每个位置等于输入特征加上一个由全局上下文生成的偏置项。这样做的计算量比乘性注意力小很适合分拣线上的边缘设备。更关键的是对于水果这种目标背景相对恒定主要是传送带和分拣托盘的颜色加性偏移能让网络快速把注意力集中到“果皮的局部颜色突变”上而不是重新建模复杂背景。我实验里有两个可选插入位置Neck的PAN结构之后以及检测头三个分支之前。实测后者效果好一些分析原因是检测头之前最接近决策边界加性偏置能更直接调整分类置信度。插入在PAN结构中间会把偏置传播多次反而稀释掉原本想要强调的局部细节。这个结论只代表在水果质检数据上的结果换到密集小目标的航拍场景可能不一样但排查坐标时可以按这个思路来。2.3 模块插入位置的选择逻辑网络改造最容易犯的错是“哪里都塞”觉得多挂几个模块总没错。事实上每个模块引入的参数量和推理延迟都是成本而且模块之间可能出现负向相互作用。我在这版里固定了一条原则主干部分只改C3K2检测头前只用AdditiveBlock其他地方保持和原版一致的连接方式。这么做的理由有三层。第一主干是特征提取的源头C3K2在源头把“多尺度局部特征”提取做好后面就不用靠堆模块来补偿。第二AdditiveBlock放在检测头前本质作用是在决策层做一个特征重标定这跟主干的特征提取是解耦的两者分工明确调试时能清楚判断是哪个部分在起作用。第三任何结构改动都要能对应到可解释的“假设”如果另一个模块名字很花哨但无法说清它到底帮模型解决了什么数据分布问题那大概率只是在拟合训练集。我在实验里对比了三种位置方案结果很能说明问题配置mAP50mAP50-95推理耗时毫秒原版模型91.267.56.8仅替换C3K292.669.97.3C3K2 AdditiveBlock在检测头前94.372.17.6C3K2 AdditiveBlock在Neck中93.570.87.7模块插入位置对Neck的干扰是实打实的7.7毫秒的耗时多了0.4毫秒精度却不如放在检测头前。这组数据我建议准备复现的人自己跑一遍因为不同数据集的最优位置可能不同但“模块要插在最能直接影响决策的位置”这条经验是通用的。3. 数据侧才是真正决定上限的地方3.1 采集和标注三套光源条件的教训模型结构改得再多数据不对就是白搭。第一版翻车就是因为数据偏差太大第二版虽然采集了真实产线数据在标注标准上又吃了一亏——不同标注人员对“轻微碰伤”和“完好但表皮斑块”的看法不一致导致模型学到的类别边界是模糊的。第三版我花了一半的项目时间在数据上。采集阶段没有用实验室级别的环形光源而是模拟产线实际环境做了三套配置高位漫射光、自然侧光和低照度。这样做的目的是让模型不要依赖具体的光照方向。我观察到一个有意思的现象同一个碰伤果在三种光源下成像差异非常大侧光下碰伤区域有阴影和立体感模型很容易识别漫射光下碰伤区域几乎和正常表皮融为一体。如果训练集里侧光照片多了模型会学成“依赖阴影判断碰伤”换个光照就失效。标注阶段我定了一个规则所有吃不准的图像单独放一个文件夹两个人同时标标注结果一致的进训练集不一致的单独开一个“难例讨论会”。这个流程看似笨拙却让我拿到了高质量的干净数据。最终训练集里面有接近15%的样本是通过这种“双人核对讨论”筛选出来的这些样本对模型最后提升mAP50的贡献比增加同类型图片数量要大得多。3.2 难例挖掘与类别均衡水果质检数据和常规检测数据有个巨大差异正常果数量远大于缺陷果。如果直接按比例采样模型会严重偏向预测“完好”因为这么做的loss期望更低。我在第二轮迭代中吃过这个亏表现为精确率看着还行召回率惨不忍睹。第三版我用的是两阶段难例挖掘。第一次模型训练完成后我不看精度指标而是把所有验证集假阳性预测错了的和假阴性漏检的图片导出来人工检查找出模型困惑的模式。统计下来的高频难题主要是三类果实的蒂部阴影被当成果皮损伤、运输摩擦导致的非常轻微瘀伤被漏检、相邻果实之间的接触面遮挡。针对这三类问题我做了针对性的数据增强——尤其是模拟接触面遮挡时把两个果实贴得很近并加随机的局部高斯模糊。这些增强不是为了增加图片数量而是精准打击模型最薄弱的能力点。类别均衡我采用了一种动态采样的方式每次训练epoch开始时计算各类别的误差率如果某种缺陷果的误差率一直下不来就按两倍权重从训练集中多采样。实际上就是在自定义的DataLoader里维护每个类别的采样概率代码量不大但对最终mAP50的影响非常显著。这种策略比单纯复制图片更优雅因为不是让模型记住同样的像素而是让它每轮实实在在多看一些难类样本。3.3 增强策略模拟传送带运动模糊传送带上的果子不是静止的相机曝光时间哪怕只有1毫秒高速运行时也会带出轻微的运动模糊。训练集如果用静止的清晰图像训练部署时就总觉得“图像怎么有点糊检测框在抖”。我在第三版训练里加了一组专门模拟运动模糊的增强——对随机选中的图片沿传送带运行方向做3到7个像素的线性模糊。关键是方向必须固定不能是任意方向的模糊因为产线上果子只沿一个方向运动给模型引入非现实的模糊走向反而增加困惑。另外还有一个容易被忽略的细节果子在传送带上是滚动的所以同一个果子的顶部、底部在不同时刻出现在画面里姿态差异很大。我的训练集里刻意加入了同一颗果子翻转多角度的连续帧截图这些数据比模型结构的贡献更实在。轮式托盘的阴影也会间歇性遮挡果实下缘我专门用框标注了这些半遮挡样本而不是把它们从训练集中剔除。半遮挡目标标注虽然麻烦但如果没有这15%左右的遮挡样本部署时一到托盘位置模型就开始漏检。4. 训练与消融怎么证明改动确实有效4.1 训练参数和基线设置训练细节决定了结构改动能否完全发挥效果。第三版的整体配置供参考输入分辨率用的是640x640没有继续加大一方面是因为产线设备用的是常规工业相机图像分辨率不高另一方面是边缘设备对推理时延敏感。优化器用的是带动量的SGD而不是Adam这是目标检测训练的老经验——Adam在这种小模型加中等规模数据的场景下容易收敛到比较尖锐的局部最优泛化不如SGD配合余弦退火好。初始学习率0.01weight decay设成5e-4batch size是32训练200个epoch其中后20个epoch关闭Mosaic增强做精调。我特别想说一下关闭Mosaic增强这个操作。Mosaic在目标检测训练里能大幅提升小目标检测能力但有个副作用四张图拼接后果子的长宽比和真实分布不一样像腐烂这种依赖局部纹理细节的缺陷在Mosaic缩小的过程中很容易丢失关键纹理。所以我在最后阶段关掉Mosaic只保留简单翻转、HSV抖动和运动模糊增强让模型回到原始空间尺度上精调这招让mAP50涨了大约0.8个点。另一个细节是EMA的decay参数我设成0.9998而不是默认的0.999因为训练轮次不算多EMA衰减过快会导致最终权重过于接近最后几轮的状态而那些轮次可能恰好过拟合了某些背景噪声。4.2 三组消融实验的数值变化消融实验的目的是回答一个问题这版改进到底是结构起效还是训练配置起效。我设计了三组对照在原版框架上单独加C3K2、单独加AdditiveBlock、两个模块都加的完整版本。每组都在同样的训练参数下跑用同样的测试集和评估脚本。单独加C3K2之后mAP50从91.2提升到92.6mAP50-95提升2.4个点。这个结果说明5x5感受野的分支确实在处理“边界模糊的碰伤区域”上贡献了额外性能。单独加AdditiveBlock时mAP50提升幅度不算大只有1.1个点但出错类型有了明显变化把完好的果皮误判为损伤的假阳性数量减少了。两个模块叠加之后效果不是简单的加法mAP50进一步到94.3mAP50-95到了72.1。两个模块一个解决特征提取的召回端一个解决决策层的精确端形成互补效果。这份实验里还有一个容易被忽视的数据召回率R和精确率P分别记录了吗记录了的。完整版相比原版召回率从88.9提升到92.4精确率从90.1提升到94.8。通常这两个指标一个涨另一个就会跌能同时上涨说明模型真正学到了“果皮损伤”的泛化特征而非通过调整输出阈值做表面功夫。4.3 容易忽略的训练细节EMA和训练轮次聊一个实战中非常容易踩的细节EMA权重到底该不该用。很多开源代码默认开启EMA大多数人在训练完直接取最后的EMA权重导出模型。但在第三版训练里我发现EMA版本不一定优于非EMA版本——不是EMA机制的问题是EMA的decay值和训练轮次不匹配。我训练200个epoch如果EMA decay设成0.999而不是0.9998那么前150个epoch的模型状态在最终权重里的占比很小相当于模型在150 epoch之后的“高学习率振荡区”学习的稳定性被削减了。具体表现是用0.9998的EMA导出权重比非EMA权重mAP50高0.3用0.999导出反而比非EMA低0.2。这个现象在论坛上被讨论得不多基本要靠自己实验。建议做法是训练过程中每5个epoch都导出一次EMA权重最后在验证集上挑最好的导出不要盲目相信默认参数。训练轮次也不是越长越好。我在训练到180 epoch时发现验证loss已经停止下降但训练loss还在缓慢下降这其实就是过拟合的信号。如果不关Mosaic继续硬撑到300 epoch模型可能会对拼接图的伪边界产生依赖反而在真实单帧数据上掉点。所以训练计划不是排好就锁死要每个epoch观察验证集和训练集的gap决定是否提前进入精调阶段。5. 部署落地从PyTorch到TensorRT的实际链路5.1 ONNX导出与动态尺寸结构改进之后模型部署就是一个全新的坑。PyTorch里跑得很顺的模型导出ONNX时首先要检查算子兼容性。C3K2里用了5x5卷积这个在ONNX里没有问题AdditiveBlock如果只涉及加法和偏置导出也顺利。真正的坑集中在两处一是检测头部分使用的一些后处理算子比如NMS在ONNX里是不导出的只能导出到网络输出层NMS推理阶段自己实现二是动态尺寸支持产线上如果输入画面固定建议直接转固定尺寸能省下不少性能开销。我在导出时走了弯路当时想着要生产环境稳定就把动态轴全部打开结果TensorRT构建Engine的时间从几分钟变成快半小时而且推理耗时比固定尺寸还慢一点。后来想想也对动态尺寸意味着TensorRT要考虑很多种输入形状优化空间会被分散。产线相机分辨率是不变的直接用640x640固定输入性能最稳。导出细节建议如下导出关键配置 torch.onnx.export( model, dummy_input, fruit_det.onnx, input_names[images], output_names[scores, boxes], opset_version11, dynamic_axesNone, # 固定尺寸产线场景推荐 )5.2 Int8量化的精度回退问题边缘设备的算力有限通常会把模型从FP16进一步压到Int8。但目标检测任务对量化比分类任务敏感多了尤其是C3K2里的5x5卷积。我第一轮Int8量化之后mAP50直接从94.3掉到86.5大跌近8个点这在产线场景完全不可接受。排查下来有两个原因。第一个是AdditiveBlock产生的偏置量级分布极不均匀大多数位置的偏置接近0少数地方有较大值Int8均匀量化把这部分动态范围牺牲了。第二个是量化校准集的选取太随便——我直接从训练集拿了一些图做校准这些图和产线图片的分布有差异导致激活值的截断阈值不准。解决办法是对AdditiveBlock的偏置使用per-channel量化策略给偏置单独分配合适的缩放因子而不是和普通卷积特征图用同一个量化参数。这个操作在TensorRT里可以通过调节量化的层级粒度来实现。修改之后Int8模型的mAP50恢复到92.8推理耗时从7.6毫秒降到4.1毫秒。如果项目对精度要求极高可以考虑FP16方案8.9毫秒的耗时段落内通常也能满足产线节拍但Int8的余量更多给了后续增加缺陷检测类别时留出性能空间。5.3 生产线集成传送带速度与判定缓冲区模型部署不是丢一个Engine文件就结束。在分拣线上相机是触发式拍摄当果实进入检测区域时输出一帧画面模型推理完成后再把结果发给执行机构进行分拣。这里存在两个实际问题传送带速度的标定和判定缓冲区的设计。果实从相机画面移动到分拣执行机构中间有物理距离。在传送带固定速度下这段距离对应一个固定时延系统需要在相机拍摄到果实后把检测结果保存到一个环形缓冲区等到果实到达分拣口时再取出结果做动作。环形缓冲区的大小取决于传送带速度和推理耗时的最大波动值。如果推耗时波动较大需要设计一个时间戳匹配机制而不是简单的FIFO队列。因为传送带上的果实数量和实时位置一直在变如果只是先进先出可能推后处理的果实对应的是早先拍摄的图片分拣就会错位。我给产线测试用的缓冲逻辑是这样的每个检测结果带一个相机时间戳分拣执行器根据物理距离计算Tdelay时刻的目标果然后在缓冲区里找对应时间戳的结果。实测这套逻辑稳定运行了连续8小时无错位。这个模块虽然和深度学习无关但是整个系统能否真正跑起来的关键如果忽略它模型性能再好也落不了地。6. 跑完整个项目的几点实在体会6.1 最后悔的决策和庆幸的决策回看整个项目最大的后悔是第二版时花了太多时间在调模型结构上而没有先冷静分析产线光照分布。那时候我总觉得模型不够强反复换各种注意力模块结果都被产线数据狠狠打脸。后来才想明白模型看的是像素光照变了同样的纹理在像素层面完全是两回事。如果第二版就把三套光源的数据采集做起来第三版可能会更早进入结构改动环节。庆幸的决策则是在数据标注流程中引入了“双人核对难例讨论会”。这个流程看起来很慢但它直接保证了标签的一致性。模型结构的调整可以复现数据标注的一致性很难补回来一旦初期标注标准出现混乱后期清洗的代价比重新标注还大。尤其是像水果碰伤这种连续性较强、没有明显边界的类别没有明确标注协议任何模型结构都学不透。6.2 后续还能扩展的方向现在这套YOLO13-C3K2-AdditiveBlock已经在分拣线的固定工位上稳定运行但只覆盖了单一品种、单一成熟度批次。水果和工业件不一样的另一个地方在于不同产区、不同成熟度的果实表皮纹理差异非常大。同一套模型换到另一个品种上可能需要微调或重新采集部分数据。我接下来的计划是收集多个品种的共性缺陷数据训练一个“跨品种”的基座模型然后再用一个轻量的适配器针对具体品种做微调。另外还可以把深度估计任务加进来因为水果质量检测经常需要结合果实的尺寸和形状二维图像只能反映投影面积加一个深度分支能更准确地判断畸形和尺寸分级。不过这些都是后话先把当前这条路走通走稳再考虑增量。如果你也在做类似的农业检测项目别急着追新模型结构先把采集光照、标注协议、训练评估流程这三件事做扎实在这些都没问题的基础上结构改动才是画龙点睛。否则再复杂的模块也只是在错误的数据上做一个精确的错误预测。
阅读完成 · 觉得有帮助?
咨询建站