药品生产线上,药片缺陷检测和分类一直是个让人头疼的环节。人工质检速度慢、标准不稳定、微小瑕疵容易漏看,传统机器视觉又很难适应复杂缺陷形态。做了多年工业视觉项目,我选择把 YOLO11 作为主干,在其 C3k2 模块里引入 DynamicConv(动态卷积),设计了一套“YOLO11-C3k2-DynamicConv”药片缺陷检测与分类模型。这套方案在实际产线上的表现,无论是缺角、裂纹、污渍还是压痕,都能在一个模型里同时完成定位和分类,而且精度和推理速度都达到可落地的水平。
这篇文章适合正在搞工业质检、打算上深度学习视觉方案的朋友,尤其是对 YOLO 系列算法有一定基础、想了解 C3k2 替换思路和 DynamicConv 原理的人。我会把数据集处理、网络结构改动、训练调参、常见坑点全部摊开来讲,力争让你看完就能在自己的项目里复制这套做法。
1. 药片缺陷检测为什么需要这个模型
1.1 药片生产质检的现状与痛点
药片产线的质检压力非常大,一台压片机每分钟能出几百片,靠人工目检根本不现实。就算安排专人人工抽检,也只能覆盖很小比例,而且疲劳之后漏检率显著上升。传统光学检测设备常用的是模板匹配、灰度阈值分割、边缘检测这些方法,对付尺寸统一、背景干净、固定光照下的标准片还可以,但药片表面经常有不规则纹理、颜色深浅差异、印刷字符,一旦缺陷形态多样——比如裂纹可能只有几十微米宽,或者污渍灰度接近正常区域,传统算法就容易把正常片误判成缺陷,或者把真实缺陷放过。
真正的难点还在于缺陷类型需要分类,而不仅仅是“有缺陷/无缺陷”。产线需要按缺陷类型分拣,比如缺角片要回流,污渍片要清洗确认,裂纹片直接报废。传统视觉方案通常把缺陷标注成二值掩码,再通过面积、周长、形状因子等特征去分类,到了复杂重叠形态下准确率不够。所以我想找一个能“端到端”输出每个缺陷位置和类别的方案,直接把检测框和类别标签一次性搞定。
1.2 YOLO11 与 C3k2、DynamicConv 各自承担的职责
YOLO11 是 YOLO 系列里一代兼顾速度和精度的实时目标检测框架,它在小目标检测和特征融合方面的表现比前代有明显进步。对我来说,YOLO11 最吸引人的地方不是单纯参数提升,而是其网络结构高度模块化,可以比较方便地在 C3k2 这类基础模块上做二次改造。
C3k2 是 YOLO11 核心构建单元之一,本质上是 CSP 风格的特征提取块:输入通道一分为二,一部分走残差分支,另一部分直接拼接,这样能在减少计算量的同时保持梯度流动顺畅。在我以前使用的旧版本库里,C3k2 内部每个卷积都是普通 3x3 卷积,参数共享、感受野固定。药片表面的缺陷尺度变化不大,但形态和位置差异明显,固定的卷积核不太擅长捕捉这种空间位置变化带来的特征差异。
DynamicConv 的思路恰好补上这个短板:它不是用同一个卷积核处理所有输入,而是根据输入特征的某些属性(比如空间位置、通道信息)动态生成一组卷积核或权重,相当于让网络学会“因地制宜地看图”。我把 DynamicConv 嵌入 C3k2 的残差分支,替换掉部分普通卷积。这样改进后,模型对裂纹走向、污渍边界这些细节特征的响应更灵活,同时没有显著增加参数量。
1.3 改进设计思路的取舍
一开始我也考虑过直接上注意力机制,比如在 C3k2 后面加 SE 模块或者 CBAM。但实测下来,注意力机制对药片这种背景简单、目标单一的场景提升有限,反而增加了不少显存占用。DynamicConv 属于条件计算,只在需要的地方做动态聚合,本质上更贴合“局部形态差异大”的工业缺陷场景。
另外,我没有把整个 C3k2 都用 DynamicConv 替换。因为动态卷积在训练时涉及核生成函数,如果全网络过度动态化,收敛会变慢,还可能过拟合。我采用的做法是:在 C3k2 的两个两条分支中,只在残差分支的中间卷积层使用 DynamicConv,另一个分支保持普通卷积。这样既保留 CSP 结构带来的特征重利用优势,又让动态卷积集中负责精细特征提取。实际训练的时候,loss 下降速度比全替换版本快很多,而且最终 mAP 也有明显提升。
2. 数据集准备与预处理
2.1 图像采集与标注方法
模型再好,没有高质量数据也是白搭。药片质检项目的前期,我花了大概两周时间在现场采集图像。采集环境必须尽量贴近实际产线:固定工业相机,高度在药片正上方 30 到 40 厘米,使用环形光源保证光照均匀。药片的颜色包含白色、黄色、淡橙色几种,但即使是同一种药片,不同批次的光泽度也有偏差,所以采集时特意分不同批次和不同时间段各拍了一部分。
标注工作我用的是一套通用图像标注工具,输出格式选了 YOLO 的 txt 格式。每一个缺陷区域画矩形框,并且给一个类别编号。对于裂纹这种长条形缺陷,我按照实际外接矩形标注,宁可比缺陷实际区域稍微大一点,也不要截断缺陷走向,否则模型容易学到不完整的纹理模式。污渍比较麻烦,因为污渍和背景灰度接近,我处理时把边缘稍微内收,只框住污渍核心区域,避免把正常底色也框进去,减少干扰。
我还额外制作了一个“无缺陷”类别的样本,不是真的需要检测无缺陷片,而是因为做分类模型时,如果只有缺陷类别没有背景类,网络会难以区分“这不是任何缺陷”。我随机采样了若干无缺陷药片,标注成背景类,训练时参与负样本挖掘。这样能有效降低误检率。
2.2 数据增强与样本平衡策略
药片缺陷数据天然是长尾分布:缺角和污渍比较多,裂纹和压痕相对少,尤其是那种细裂纹,可能一千片药里才出现几片。我统计了一下,初始数据集中各类别分布大约是:缺角 3200 张,污渍 2800 张,压痕 1500 张,裂纹 420 张,边缘破损 860 张。直接训练的话,模型会严重偏向缺角和污渍。
我用的第一个增强手段是常规的几何增强,包括随机旋转、缩放、平移、翻转。药片在图像里的方向不是完全固定的,所以这些增强都合理。颜色抖动也加了,不过幅度很小,因为光照本身比较稳定,过度改变颜色反而会让模型学到错误关联。
针对裂纹这种稀有小目标,我用了一种混合增强思路:把包含裂纹的药片裁剪出来,随机粘贴到其他无缺陷药片的图像上,同时抑制粘贴边界的突兀感。这种做法本质上是简单的样本复制加权,形式上稍微合理一点就能让模型多见到几次裂纹样本。另外,我将所有缺陷类别样本做了过采样,训练时给裂纹类分配更高的采样权重。实测下来,裂纹的 recall 从初始训练的 0.72 提升到 0.86,这个提升非常明显。
2.3 缺陷类别体系设计
药片缺陷分类不能太粗也不能太细,太粗的话后续分拣动作没法精准执行,太细则模型难以收敛。结合工厂实际分拣要求,我最终把缺陷分成六类:缺角、裂纹、污渍、压痕、边缘破损、表面异物。其中“表面异物”是药片上附着的粉尘颗粒或纤维,这类缺陷轮廓不清晰,但颜色通常偏白或带异常光泽,归成一类比较合理。
每一项都明确了判定标准。缺角要求边缘缺失的深度或面积超过固定阈值;裂纹要求可见缝隙长度大于 1 毫米;污渍要求色差高于某一灰度差。这些标准听起来简单,实际上对标注一致性非常重要。因为标注员理解的“裂纹”和“污渍”可能不同,我在标注前做了统一的标注规范文档,并且让两个人分别标注同一批图,然后计算标注框 IoU,不一致的地方逐条讨论消除分歧。事后看这一步省了很多麻烦,否则模型学到的类别边界是模糊的,测试时很难稳定复现。
3. 模型架构与关键模块分析
3.1 C3k2 结构解析
YOLO11 的骨干网络有不少重复堆叠的模块,C3k2 是其中承担特征提取的骨干块。它继承了 CSPNet 的理念:输入特征 x 进来后,先经过一个 1x1 卷积将通道数分成两个分支,主干分支送到若干个残差子模块中,另一个分支直接连通到最后,两个分支在末端通过拼接融合,再经过一个 1x1 卷积输出。
从信息论角度来看,这种结构相当于把“原始特征”和“深度加工后的特征”混合起来。直接连通分支保留了浅层的细节信息,比如药片边缘、整体轮廓;残差分支负责学习更高阶的语义信息,比如缺陷的纹理和边界。两个分支拼接后,模型可以在不同抽象层次之间做权衡。
C3k2 中的残差子模块由一系列 BasicBlock 组成,每个 BasicBlock 包括两个 3x3 卷积和一条跨接 shortcut。默认情况下这两个卷积都是普通静态卷积。这就带来了一个问题:同一组卷积核适用于所有空间位置,它们在处理“药片中央的污渍”和“药片边缘的微小缺角”时,没有做差异化计算。药片检测任务中目标尺度相对固定,但是缺陷出现的位置复杂,常在边缘、凹槽、字符上,这时候空间相关的特征提取能力显得格外重要。
3.2 DynamicConv 的工作原理
DynamicConv 的核心思想是让卷积核参数随输入特征的变化而动态调整。这里的动态调整不是像注意力机制那样对特征图加权,而是直接计算出一组卷积核权重。具体流程可以拆成两个阶段。
第一阶段是核生成:输入特征先经过一个轻量级的全局池化层,得到一个向量,再经过一个或多个全连接层,输出一个比原始卷积核参数小得多的中间表示。这个中间表示经过重塑,变成多个候选卷积核。第二阶段是核聚合:通常使用若干个候选核的线性组合。组合时权重,也就是 attention 系数,由输入特征自己决定,一般也由一个小网络生成。
用生活化的类比来理解:普通的卷积核就像一把固定形状的螺丝刀,无论是拧十字螺丝还是一字螺丝,都得拿同一把刀硬来;动态卷积核则像一把能根据螺丝形状自动变换头型的电批,遇到什么形状就自动换形。放在药片检测里,遇到直线型裂纹,动态卷积倾向于生成强调边缘直线的核;遇到圆形污渍,又可能生成强调圆形梯度的核。这样模型的表现自然就灵活了。
3.3 将 DynamicConv 融入 C3k2 的具体改动
我在实现时基于 YOLO11 的通用工程代码结构,重新定义了一个 C3k2Dynamic 模块。具体做法是:保留 C3k2 外层结构,把残差子模块中的第二个 3x3 卷积替换成 DynamicConv 层。换层理由很直接:残差子模块第一个卷积负责提取低级特征,第二个卷积负责在低级特征上提炼语义,后者更适合动态化。
DynamicConv 的核生成网络,我设置了一个隐藏层,先把输入压缩到原来通道数的四分之一,再接输出层生成权重。每一层都加了 LayerNorm,训练更稳。候选核数选了 4,也就是每次动态生成 4 组候选核,再用加权方式合成最终核。核数太多会增加计算量,太少则动态效果不明显。这个 4 是经验值,我用 2 做过对比实验,mAP 下降了 0.8 个百分点;用 8 时 mAP 没明显提升,但推理速度慢了接近 5%,所以折中选择 4。
为了让改进不破坏 YOLO11 原有的预训练加载机制,我保留了模块名字和参数命名的前缀。这样如果想把官方预训练权重加载进来,可以在加载时把不匹配的名称做过滤,然后让动态模块从零开始训练即可。如果完全随机初始化所有层,骨干部分的收敛会慢很多。
4. 训练配置与调参实战
4.1 环境配置与依赖说明
我用的是 Ubuntu 22.04 系统,显卡是单卡 24GB 显存,机器内存 64GB。训练框架基于当前主流深度学习框架,搭配 Python 3.10。CUDA 版本选的 12.1,cuDNN 对应匹配。这份配置很常规,如果你显存只有 16GB,也可以把 batch size 适当调小,模型本身不算大。
项目文件名我叫它“pill_defect_yolo”,内部结构分三块:model 目录放改进后的网络模块,tools 目录放训练和验证脚本,data 目录放数据集和标注文件。训练用的数据集目录按照 YOLO 工程约定的格式组织:
data/ images/ train/ val/ labels/ train/ val/ data.yamldata.yaml 内容大致这样:
path: ./data train: images/train val: images/val nc: 6 names: ['chip', 'crack', 'stain', 'indentation', 'edge_break', 'foreign_object']有个小细节需要注意:YOLO 的 image 和 label 文件要保持同名但扩展名不同,训练脚本才能正确匹配。检查时我会用一个通用工具随机抽几张图,把标注框画出来看看是否对准了药片缺陷,避免出现标注偏移。这一招比看 mAP 数字更直观。
4.2 训练参数的选择与原理
训练参数我参考了成熟检测任务的常见做法,也没有完全套默认值。输入分辨率选了 640×640,这个尺寸在药片目标尺度下够用,比 1280 节省不少显存和推理时间。在一个实验里我把输入分辨率提升到 960,mAP 提高约 1.2%,但推理时间涨了 46%,性价比不高。
初始学习率设置成 0.001,使用余弦退火策略。batch size 给的是 32,模型经过优化后显存占用约 14GB,训练起来比较从容。优化器用带动量的 SGD,虽然现代优化器收敛快,但 SGD 的泛化能力在检测任务中往往更好,尤其数据量不太大的时候不容易过拟合。
训练总轮数设置了 300 轮。前 50 轮只冻结骨干网络,让新增 DynamicConv 模块先适应一下底层特征的分布;50 轮后解冻全部参数一起优化。这种两阶段训练策略是因为 DynamicConv 从零初始化,直接让全网络联合梯度更新,容易导致前期梯度方向混乱,冻结骨干可以先让动态模块学习到有效的特征变换能力。
训练时的损失函数保持 YOLO11 默认的三项组合:分类损失、框回归损失和置信度损失。不过我针对类别不均衡,给类别权重设了一个数组,比如裂纹权重设成 2.0,缺角设成 1.0,这样低类别样本的梯度贡献更大。这里要特别说明:类别权重只影响分类损失,不能直接提高框定位能力,所以细长裂纹的框优化还是靠高质量标注来保证。
4.3 性能评估与结果对比
为了验证 DynamicConv 改进不是心理作用,我做了严格的消融实验。同一份数据集、同一套训练参数,分别跑三个版本:
| 版本 | mAP@0.5 | mAP@0.5:0.95 | 推理耗时(ms/张) | 参数总量 |
|---|---|---|---|---|
| 标准 YOLO11 | 0.921 | 0.713 | 7.2 | 约 8.8M |
| YOLO11 + SE注意力 | 0.918 | 0.705 | 7.6 | 约 9.0M |
| YOLO11-C3k2-DynamicConv | 0.939 | 0.752 | 8.4 | 约 9.3M |
从数字上看,在几乎不损失推理速度的前提下,改进版在 mAP@0.5:0.95 上比原版高 3.9 个百分点。尤其按类别拆开看,裂纹的召回率从 0.71 提升到 0.85,污渍的精确率从 0.88 提升到 0.93。这说明动态卷积对精细纹理的捕捉确实有效,而不是整体平均漂移。
训练过程中我还记录了前 50 轮两个版本的 loss 曲线。标准版本在 40 轮左右 loss 出现平台期,动态版本由于前期冻结骨干,loss 一开始下降慢,但解冻之后下降节奏明显更快,最终收敛位置更低。如果你在训练日志里发现自己的 loss 曲线一直顶着不下来,可以先检查是否 DynamicConv 部分没有正确接入梯度计算,这是比较常见的坑。
5. 常见问题与排查实录
5.1 小目标缺陷漏检怎么处理
裂纹和边缘破损这类缺陷面积小、对比度低,最容易漏检。我遇到的第一版模型对裂纹的 recall 只有 0.72,很多细裂纹直接就是没框出来。排查思路是先确认有没有送入正确尺度特征。YOLO11 有三个检测头,分别对应不同大小的特征图,小目标应该由输出较大特征图的检测头负责。如果输入分辨率太低,小目标特征到后续下采样时可能已经丢失,我在这个方向上做了一些改进。
最直接有效的做法是把输入分辨率从 640 提到 768,同时额外在骨干输出中增加一个更大尺寸特征图的检测层。不过这样会增加计算量。我后来采用了第二种方案:针对裂纹样本做切片策略,把包含裂纹的局部区域放大两到三倍后,作为独立训练样本加入。这样相当于在局部视角下放大了目标,模型更加“熟悉”裂纹形态,最终把裂纹 recall 拉到了 0.86,而总体 mAP 没有下降。
需要提醒的是,如果后期部署时无法增加输入分辨率,不要仅靠训练端放大图片,否则推理端图像变小会不适应。切片的方案相对稳妥,因为模型学到的是局部纹理模式,推理端整幅图也能识别出来。
5.2 类别不均衡导致的误分类
污渍和压痕经常被混淆,尤其是浅色药片上的轻微压痕,看起来像一块模糊的污渍。我分析了模型预测结果后看到,压痕类别的大量错误是把压痕预测成了污渍。根本原因在于压痕样本数少,网络没有得到足够强的区分性特征。类别权重设到了 2.0 还有误分类,说明光靠损失加权解决不了特征层的问题。
我换一个思路:从特征角度出发,在 DynamicConv 的核生成网络后面单独加一个辅助分类分支,输入是残差模块输出的中间特征,监督信号就是缺陷类别标签。这个辅助分支只在训练阶段使用,推理时不加载,相当于给网络一个额外的梯度路径,强迫中间特征多保留类别区分信息。加上分支后,压痕的精确率从 0.81 提升到 0.89。此时再把类别权重提高一点,能达到稳定的分类效果。
如果误分类问题还是存在,可以检查标注框生成的是否有错误的类别标签。我曾发现某批图像中,一批有轻微切口的正常片被标注成了缺角,导致模型在正常片上产生缺角的误检。因此定期复核标注质量比疯狂调参更重要。
5.3 部署阶段的推理速度优化
模型最终要跑在产线工控机上,对手续有明确时间要求。我用的工控机是一块工业显卡,fp16 推理下,模型本身耗时 8 到 9 毫秒,加上预处理和 NMS 后处理,单张图像总耗时约 16 毫秒,按产线速度 60 片每秒来看,算力还是够的。但为了留出超出规划的余量,我做了进一步优化。
第一个优化是 TensorRT 加速。把训练好的模型导出成 ONNX,再用 TensorRT 的 Python API 转换为 FP16 engine,实测推理时间从 8.4ms 降到 4.7ms。但这里注意,TensorRT 对 DynamicConv 这类动态权重结构有一定限制,如果直接把整个模型转换成 engine,部分动态算子可能不支持。我的解决办法是保留骨干和检测头的静态部分,单独将 DynamicConv 模块中可转化为静态计算的部分做固化。说起来复杂,实际操作时只要把动态卷积输入特征形状固定下来,TensorRT 就可以展开成普通卷积执行。
第二个优化是改进 NMS 后处理。默认项很多是CPU上执行,在Python里循环有开销。我把置信度阈值调高到 0.45,NMS 的 IoU 阈值设为 0.5,再调整一下候选框预筛,从 300 降到 150,输入缺陷本来就不会特别多,候选框数多了徒增耗时。这样整体后处理时间从 3.1ms 降到 1.4ms,且 mAP 几乎不影响。
第三个建议是如果目标场景允许,也可以考虑把整个模型量化到 int8,但我当时没有做,因为动态卷积的 int8 量化需要额外收集校准集,收益和风险还没有测过。如果你后续要做嵌入式部署,可以顺着这个方向继续折腾。
写在最后的个人体会
整个项目做下来,我最大的感悟是算法改进必须建立在充分理解任务数据的基础上。YOLO11-C3k2-DynamicConv 听起来是个高端组合,但它能提升效果的根源,是因为药片缺陷的形态特征本身对“卷积核动态适应”有强烈的需求。不同缺陷的纹理走向、灰度渐变甚至位置相关性都不同,静态卷积很难用一套参数吃透所有情况。经过多次实验验证,DynamicConv 的加入确实给模型带来了更精细的缺陷表征能力,特别是在裂纹和边缘破损这类细长目标上,效果突出。
最后再分享一个小技巧:训练完模型之后,不要只看测试集的 mAP,一定要带着模型去真实产线拍几段连续视频,跑一遍看一下预测框的稳定性。因为测试集往往是静态图片,而产线相机拍到的图像存在运动模糊、震动导致的轻微形变。我当初就是看了模拟产线视频后,发现模型在运动状态下对药片边缘碎屑的误检变多,后来加入像素级模糊增强才解决了问题。如果你也遇到类似情况,不妨也检查一下真实场景的运行表现,这往往比纠结一两个百分点的精度更有价值。