上个月我们接到一个园区门禁改造需求,技术清单里最不起眼的一项竟是"口罩佩戴识别"。团队里有人拍胸脯说这是成熟功能,拉个二分类检测模型就够用,结果真实摄像头画面一进来就翻车:口罩拉到下巴、手拿口罩挡脸、背光环境下口罩和皮肤糊成一片,明明是一段再普通不过的入园人流,模型却给出了大量互相矛盾的框。最后让我们把整个流程跑通的,是基于 YOLO26 的口罩佩戴智能识别方案。这篇内容是我们从数据采集、标注、模型训练一路到边缘端部署的完整复盘,适合正在做目标检测落地应用、想复现口罩识别项目,或者准备把视觉模型装到门禁、安防场景里的同学参考。
1. 为什么最终选了 YOLO26:先想清楚三个问题定义
1.1 把"佩戴口罩"拆成三种状态而不是二分类
大多数人对口罩识别的第一反应是"有没有口罩"二分类。但真实需求不是这样。园区门禁关心的往往不是"人脸区域里是否出现口罩纹理",而是"这个人能不能进":规范佩戴可以进,没有佩戴需要提醒,口罩挂在下巴或只罩住嘴露出鼻子,则需要按管理规则单独处理。如果我们只训一个二分类模型,那么"口罩拉在下巴"这种最常见的违规状态,模型大概率会输出"positive"(检测到口罩),标签本身就把问题搞错了。
所以我在项目一开始就把检测目标拆成三类:mask_ok(规范佩戴)、mask_bad(佩戴不规范,比如露出鼻子、口罩挂下巴)、no_mask(未佩戴)。这一改动表面上只是标签数量从 2 变 3,实际上改变了模型的决策边界。后面所有训练和评估都围绕这三类来做,门禁逻辑才能落到具体规则上:no_mask直接拦截,mask_bad提醒纠正,mask_ok放行。
还会有额外的定制需求,比如运营方要求"透明面罩不算口罩"、"N95 和医用外科口罩都算规范佩戴"。这些都必须提前写进标注规范里,模型才会按场景规则去学习,而不是按图像直觉去学习。问题定义清楚之后,算法选型才有讨论基础。
1.2 YOLO26 这代解决了我们哪些历史痛点
选 YOLO26 之前我们对比过两类方案:一类是两阶段检测器,精度确实高,但推理速度和部署复杂度在边缘盒子上撑不住;另一类是近两年讨论较多的 transformer 类检测器,在大目标上表现惊艳,一旦落到远距离密集小目标场景,耗时和显存占用立刻成为突出问题。口罩识别恰恰是小目标为主、需要在普通硬件上实时运行的场景,所以我最终回到 YOLO 系列。
YOLO26 是 YOLO 系列迭代到这一阶段的较新版本,社区里讨论比较集中的改进点是:骨干网络在 C2f 结构基础上加入了轻量注意力分支,检测头沿用解耦设计,同时标签分配策略改成了动态阈值。用我们项目里实际跑下来的感受说,它对"小尺寸人脸+口罩遮挡"这类目标的召回率比前几代明显更稳。把人脸区域按 30 到 60 像素估算,640 输入下约占 6 个左右的网格,放到早期版本里,这种目标在下采样过程中很容易被特征层直接吞掉。
另外,YOLO26 的训练和导出生态比较完整。它保留了预训练权重、迁移学习、ONNX 导出这些成熟链路,这对一个要快速落地的团队非常关键。我们不需要从头设计损失函数和检测头,而是可以把时间压在数据清洗、现场调试这些真正决定上线效果的事情上——阿里团队常说的"80% 的收益来自数据"在我这个项目里完全成立。
2. 数据集的短板会直接变成模型的短板
2.1 数据来源:现场采集为主,公开集补充为辅
很多人做目标检测习惯直接下公开数据集,拿来就用。但口罩识别场景高度依赖摄像头安装角度、光线、画质。公开集里的正面大头照再规范,放到园区入口的俯拍画面里往往全废。所以我们的数据以现场采集为主:在出入口摄像头连续录了一周视频,按 5 到 10 帧随机抽样,覆盖早中晚、晴天雨天、顺光逆光,筛掉大量重复帧之后保留了两万多张可用帧。
公开集只做补充,主要目的是扩充mask_bad这一类样本。因为现场里"不规范佩戴"的比例其实不高,大多数人都戴得规规矩矩,模型很容易把no_mask和mask_bad学成一团。这里我的做法是:从公开的口罩检测数据里手动筛出"口罩在脖子""口罩遮住嘴没遮鼻子""用手把口罩拉下来"这些画面,统一做一次亮度归一化后混入训练集,把三类的数量差距控制在 1.5 倍以内。
还有一个小技巧:把训练初期误检率最高的样本单独存一个"hard set"。每次迭代模型都重新跑一遍这批图,发现哪些框从模糊变得清晰,哪些依然乱报,逐个去补标注或调阈值。这比盲目加数据高效得多。
2.2 标注规范:一套统一到"人脸框"的口径
口罩识别项目最容易犯的标注错误,是拿"口罩的物理位置"去画框。比如一个人规范戴口罩,有人画出半张脸,有人框住整个口罩,mask_bad状态下口罩在脖子,如果按口罩位置画框,模型就会去学"脖子上的口罩",导致人脸区域漏检。
我们团队的统一口径是:一律框"人脸所在区域",口罩区域包含在人脸框内。哪怕口罩遮住了半张脸,边界框也覆盖整个头部皮肤可见区域;如果人脸几乎完全被帽子、衣领或手挡住,无法判断佩戴状态,该帧不参与训练。同时设定最小目标尺寸:人脸区域小于 24×24 像素的样本直接丢弃,因为训练下去只会贡献噪声。
多人协作标注时,不同人的"宽松"和"严格"差异非常大。我们抽查了一轮初标结果,部分标签的框中心偏移超过 15 个像素。解决办法是花半天时间对标注员做标准样张培训,另写一份包含二十几个边界案例的标注规则文档,比如"口罩上方露出眉毛算不算规范""透明面屏怎么标""墨镜遮挡算不算无效样本",把这些都钉死在文档里,才避免反复返工。
2.3 数据增强不是越多越好
YOLO 系列本身集成了 Mosaic、MixUp、HSV 扰动、随机平移缩放等增强策略,默认参数对通用目标检测表现不错,但口罩场景需要重新思考。我最终训练时把 Mosaic 概率开到 0.8,MixUp 只留 0.2,关闭了直角的 90 度旋转。原因很直接:人脸戴口罩是强先验结构,旋转 90 度后"头朝下、口罩跑到额头"的样本在真实场景里根本不存在,只会让模型学出奇怪的纹理关联。
HSV 扰动也做了收敛。口罩的颜色主要是蓝、白、黑,适度变动亮度能模拟不同光环境,但饱和度和色相剧烈抖动会让"口罩区域"的纹理失真,反而把模型带偏。数据增强的要义是保持语义不变、尽量扩展变化,而不是无脑堆强度。我在训练后期还验证过一个现象:始终开启 Mosaic 会让验证集上的小目标精度下降,因为 Mosaic 生成的图片里目标尺度分布和真实场景差距较大,最后 10 个 epoch 关闭 Mosaic 后指标明显回升。
3. 用 YOLO26 训练前,我把这些配置一项项钉死
3.1 环境与依赖:先别急着装最新版
训练环境看起来人人会搭,但往往是翻车重灾区。YOLO26 的训练框架依赖 PyTorch、CUDA、OpenCV 等一整套组件,版本之间偶有兼容问题。我们的经验是:不要追求所有库的最新版本,而是用文档里对应验证过的组合。第一步用 conda 建独立环境,Python 用 3.9 附近稳定版本,PyTorch 装与 CUDA 匹配的预编译版本,其他依赖按 requirements 安装。如果后面导出 ONNX 遇到算子兼容报错,大概率是 PyTorch 和 ONNX 的版本组合问题,调整版本而不是改代码通常更有效。
硬件方面,8GB 显存可以跑通小模型,但训练效率和 batch 上限会受到明显限制。我们最终用了一张 24GB 显存的卡做训练。如果你的机器只有 8GB,建议直接用n或s规格的 YOLO26 权重,关闭大尺寸训练,并把 batch 调到 8 以下。
3.2 一套能直接复现的配置模板
口罩识别项目本质是三分类目标检测。数据配置文件data.yaml长这样:
path: /data/mask_dataset train: images/train val: images/val nc: 3 names: 0: mask_ok 1: mask_bad 2: no_mask关键就在nc: 3和 names 的类别顺序。训练时保证全流程类别顺序一致,导出 ONNX 后后处理才不用额外翻转索引。
训练参数我们最终稳定在这组配置上:
| 参数 | 取值 | 说明 |
|---|---|---|
| img | 640 | 输入分辨率,默认主流尺寸 |
| batch | 16 | 24GB 显存下比较稳 |
| epochs | 180 | 实际第 120 epoch 左右接近收敛 |
| optimizer | SGD | lr0=0.01,weight_decay=0.0005 |
| warmup_epochs | 3 | 防止前期学习率过大导致损失爆炸 |
| mosaic | 0.8 | 训练中期开启 |
| close_mosaic | 10 | 最后 10 个 epoch 关闭 Mosaic |
| mixup | 0.2 | 只做轻量扰动 |
| seed | 42 | 固定种子方便复现 |
这些参数在 YOLO26 的模型配置文件里都能直接对应。SGD 比 AdamW 在中等数据量下收敛更稳定,不会因为自适应学习率把样本量少的mask_bad类推得太激进。
3.3 迁移学习:冻结骨干怎么冻才划算
口罩识别项目没必要从随机初始化开始训练。YOLO26 官方发布的预训练权重已经学过大规模通用物体,底层特征(边缘、纹理、颜色梯度)可以直接复用。我们的做法是:加载预训练权重后,先冻结骨干网络前 10 层,只训练检测头和后面的特征融合层,跑 10 个 epoch;之后再解冻全部层,用较低学习率微调 30 到 50 个 epoch。
为什么不能一开始就全网络微调?口罩场景下训练集规模通常只有几万张,如果从第一个 epoch 就全量更新参数,底层通用特征很容易被少量数据带偏,最后收敛到"只认识这条走廊里的人脸"的过拟合模型。先冻结训练,相当于把前面的知识固定住,只让模型先学会把通用特征映射到三个口罩状态;解冻后再慢慢适配真实画面的光线、角度和画质。
训练命令大致如下:
python train.py --data mask_dataset.yaml \ --weights ckpt/yolo26_m.pt \ --img 640 \ --batch 16 \ --epochs 180 \ --optimizer SGD \ --seed 42训练过程中我习惯盯着runs/exp下的曲线和日志,而不是只看终端打印的 loss 数值。曲线趋势比单点值重要得多,后文会专门讲怎么看。
4. 训练期最容易翻车的地方,我都替你踩了一遍
4.1 Loss 不降:先怀疑这几处
训练前 20 个 epoch 里 loss 如果完全不动,第一反应不应该是调学习率,而是回去检查数据和配置。最容易犯的错是数据配置里路径写错导致图片加载全部失败,但训练流程没有及时报错,日志里显示 loss 一直是同一个值。所以训练脚本跑起来之后,第一件事是点开训练日志确认图片数量正确加载、标注框确实出现,而不是直接干等。
如果数据没问题,loss 还是震荡不降,我会依次检查:warmup 是不是没生效?学习率会不会太大导致梯度来回振荡?batch 太小也会让 loss 曲线像锯齿一样。最有效的调整方式是按顺序来:先把lr0降到 0.001,把batch提到 16 或 32,再把 Mosaic 关掉试试。如果这样 loss 依然不稳定,问题多半在标签上有脏数据,比如类别索引越界、框坐标出现负数、某些图被标注成空框,这些在 YOLO 训练里都会造成隐性伤害。
我还有一个习惯:单独写一小段脚本,把训练集里的图像和对应标签可视化输出几张图。这一步能同时发现问题:标签框有没有跑偏、有没有把整张图都框进去、类别和内容是不是对应。花十分钟可视化,能省下后面三个小时的无效调参。
4.2 NaN 与显存爆炸
训练过程中 loss 直接变成 NaN 是最让人头疼的问题。绝大多数情况是梯度爆炸,罪魁祸首是学习率太大或者某个 batch 里出现了极端样本。我碰到过一次奇怪案例,损失在某个 epoch 后突然 NaN,后来发现是数据集里有十几张全黑的监控帧,模型在黑色图像上输出了奇异梯度。解决了这批帧之后训练恢复正常。
显存溢出(OOM)本身不是 bug,而是资源配置问题。常见的应对思路是减 batch,但我的第一选择是关闭 Mosaic,因为 Mosaic 会一次性拼接四张图,显存占用和计算量明显大于普通样本。如果关掉 Mosaic 还溢,再减半 batch,或者把img从 640 降到 480 做快速验证。这里有一个容易被忽略的点:混合精度(AMP)在显存紧张时能省不少空间,但也可能带来轻微精度损失。如果项目对精度更敏感,建议只在验证阶段开启 AMP,正式训练全程使用 FP32。
训练过程中我建议每隔几个 epoch 就保存一次权重,同时开启自动断电续训。YOLO26 系列都支持从最新 checkpoint 恢复。我吃过大亏:训练到 130 个 epoch 机器被强制重启,没开续训直接从头再来。从此之后,"每 10 个 epoch 保存一次权重"成了团队标配。
4.3 过拟合提前到来:小模型的应对策略
过拟合在口罩识别项目里非常典型,尤其是训练集只有一两万张、模型规格又选得偏大的时候。现象是训练 loss 持续走低,验证集 mAP 在前 50 个 epoch 上涨,之后开始掉点。很多人第一反应是加 dropout,但目标检测模型更有效的工具是增强、早停和数据扩充。
我的做法是:把早停 patience 设成 20 个 epoch,验证集指标连续 20 轮不涨就停。另外一个重要指标是看训练集和验证集之间的 mAP 差距,差距超过 8 个点基本就是在过拟合,这时候优先补mask_bad类别的样本,而不是疯狂调参数。
python train.py ... --patience 20训练结束后,runs/exp目录里会生成混淆矩阵图和精度曲线,这是判断过拟合最直观的方式:如果mask_bad类别被大规模误判成no_mask,说明这个类别的特征还没有真正被模型分开,需要继续补样本而不是加大增强强度。
| 现象 | 可能原因 | 排查顺序 |
|---|---|---|
| loss 恒定不动 | 数据路径错误 / 标签空 | 可视化加载结果 |
| loss 震荡剧烈 | lr 过大 / batch 过小 | 降 lr,调 batch |
| loss 变 NaN | 梯度爆炸 / 极端样本 | 降 lr,清洗黑色帧 |
| OOM 溢出 | Mosaic 显存压力大 | 先关 Mosaic,再减 batch |
| 验证集掉点 | 过拟合 | 补数据,开早停 |
5. 模型不只听 mAP 的话:评估要看这三组数字
5.1 精度、召回、混淆矩阵
训练完第一版模型,mAP@0.5 跑到 0.9 以上,团队有人觉得够了。但打开confusion_matrix.png才发现问题:no_mask的漏检主要指向mask_ok,说明部分人脸在光线下被误判成"规范佩戴";而mask_bad的漏检大量指向no_mask,这是最致命的方向性错误——模型把"不规范佩戴"当成"没戴",门禁逻辑就只能放着人进去。mAP 的漂亮数字掩盖了类别间的不平衡,单独看 mAP 很容易被骗。
评估时必须拆类别看 Precision 和 Recall。我给自己定的标准是:no_mask的 Recall 必须最高,宁可多拦也不能放过;mask_bad对 Precision 的要求更高,否则系统天天把规范佩戴的人拦下来提醒,用不了三天就会被人骂。这会直接影响置信度阈值的选择,比如把no_mask的阈值从 0.25 下调到 0.2,同时把mask_bad的阈值上调到 0.35,虽然整体 mAP 不变,但实际规则效果好了很多。
验证集的构成也要讲究。不能简单随机切分,因为随机切分会让同一摄像头同一时段的帧同时出现在训练集和验证集,造成指标虚高。我按"时段+机位"分层切分,保证验证集里有逆光的、夜间的、不同角度的独立画面,这样评估结果才接近真实部署。
5.2 分辨率与速度的取舍
口罩识别的核心痛点之一是小目标。训练和推理分辨率直接影响两个指标:小目标检出率和推理速度。我们做过一组对比:
| 输入分辨率 | mAP@0.5 | 推理耗时(相对值) | 小目标漏检印象 |
|---|---|---|---|
| 320 | 0.84 | 1.0 | 严重,远距离人脸大量漏检 |
| 480 | 0.90 | 2.2 | 可接受,偶发漏检 |
| 640 | 0.92 | 3.5 | 稳定,夜间场景更好 |
道理很简单:一个人脸框在 640 输入里占 6 个左右网格,降到 320 后只占 3 个网格,再经过下采样,对应特征层可能只剩 1 到 2 个像素,几乎不可能可靠检出。所以我宁可牺牲一点速度,部署时也保留 480 到 640 的分辨率,而不是为了 FPS 盲目降到 320。如果你最后速度还是不够,优先优化推理框架和半精度推理,不要在输入分辨率上过度让步。
6. 部署阶段真正影响上线效果的几个细节
6.1 导出与加速:分辨率固定是第一原则
模型训练完成后要导出成 ONNX 格式部署到边缘设备。导出时最容易犯的错是使用动态尺寸导出,把batch和输入宽高都设成 -1。动态尺寸虽然灵活,但推理性能通常明显下降。我的建议是:部署的输入尺寸固定 640×640,导出时直接固定 batch 为 1,静态尺寸。如果确实要处理不同分辨率画面,在前处理里做 letterbox 缩放,把输入统一到 640 的尺寸,模型本身不需要动态分辨率。
导出成功之后,可以在不同推理后端上做精度对照验证。这里有一个细节:直接跑导出的 ONNX 和原来 PyTorch 的推理结果会有微小差异,因为算子实现不同。上线前要在实际部署设备上重新跑一遍评测集,确认端到端指标没有明显下降,而不是默认导出后效果不变。
部署设备上如果跑不到实时,优先开半精度推理。口罩识别这类任务的精度冗余相对充足,FP16 基本不损失性能,但速度可以提升近一倍。开半精度之前,值得先看目标设备是否支持,不支持的设备强行开启反而会变慢。
6.2 时序抖动:单帧判断会发出错误警报
单帧检测推到现场必定遇到抖动问题。行人低头看手机、转头、手从脸前挥过,都可能造成个别帧误判成no_mask。如果门禁系统按单帧结果弹警报,一天能误报几十次。
我们的解决办法是做一个轻量级的时序平滑逻辑:连续 5 帧里,如果 3 帧判为no_mask才触发拦截;如果当前帧是no_mask但前后几帧都是mask_ok,只当作瞬时扰动忽略。这个策略简单有效,代价是检测结果会滞后半秒,在门禁场景完全可接受。工程上我们给检测结果打一个"标签 + 置信度 + 目标 ID"的结构体,目标 ID 通过相邻帧 IOU 匹配来维护,避免同一目标在连续帧里被反复计数。
时序平滑的阈值要按现场行为调整。入口通道比较窄、行人移动快时,5 帧窗口太大会漏掉快速通过的人;通道宽、人流慢的场景,窗口反而可以更宽。没有一劳永逸的参数,部署后第一个星期就是不断调这个窗口值。
6.3 摄像头安装角度比想象中更影响成绩
这个结论是我在整个项目里最有价值的一条经验:摄像头安装角度几乎决定了模型的天花板。同一套模型,装在 30 度俯拍角度时 mAP 明显高于平视安装。平视安装容易被逆光、反光和遮挡干扰,人脸区域经常只露出一半;俯拍角度能同时拍到额头、眼睛和口罩区域,特征更完整。
现场布置时建议把摄像头安装在距地面 2.5 米左右、向下倾斜 15 到 30 度,让人脸处于画面中上部、高度在整幅画面的三分之一到一半之间。如果门禁上方的摄像头装在 1.5 米平的台子上,模型再准也救不回来。这个环节直接影响训练和部署效果,排期时一定要提前和施工方确认。
另外,夜间环境必须有补光,否则模型在暗光下会把大量人脸当成no_mask。我们调试后发现,红外补光下的模型表现和可见光补光有明显差异,最终训练集里加入了一部分红外样本才让夜间指标回到白天水平。现场调试时带着屏幕去看实时检测结果,比事后翻日志高效得多。
6.4 踩到最后,我发现最大的瓶颈不是模型
我们在这个项目上最耗时间的环节,不是训练参数调优,而是反复确认"什么算规范佩戴"和"边界 case 怎么定义"。模型本身在两周内就收敛了,但现场逻辑和运营规则之间的拉扯持续了整个项目。如果现在有人让我再做一次类似的口罩识别项目,我会把更多时间花在数据采集方案和标注规范上,模型配置反而可以更快定下来。对你来说,如果项目刚起步,我最大的建议也是:先花一个星期去现场盯素材,把摄像头角度、光线、人群行为都记录清楚,再回来跑模型。模型解决的是"看清"的问题,而"看清"的前提,是数据里真的包含你要看清的那些场景。