拓冰建站拓冰建站
首页 / 资讯中心 / 正文

半监督YOLO目标检测框架:用无标注数据提升模型精度

简介目标检测是计算机视觉的核心任务之一传统训练依赖大量人工标注数据成本高且周期长。半监督学习通过利用海量无标注图像结合少量标注样本有效缓解数据瓶颈。其原理是由教师模型生成伪标签再以一致性正则化约束学生模型学习同时通过EMA更新保持教师模型稳定。该技术能显著提升检测器在少标注场景下的精度尤其适用于工业质检、自动驾驶等标注成本高昂的领域。本文围绕YOLO检测器详细梳理了一套半监督训练框架的设计与工程实践涵盖伪标签筛选、动量调度、损失组合等关键细节为实际项目落地提供可复用的参考。 我把前面大半年在项目里反复试错、最终沉淀下来的一套半监督YOLO目标检测框架整理成了zip包分享出来。所谓半监督说直白一点就是标注数据不够但无标注数据管够的情况下怎么把模型效果再往上顶一截。这个框架核心包含师生模型Teacher-Student、伪标签生成、EMA权重更新、一致性正则化以及一整套围绕YOLO检测头做的工程适配。如果你正在做目标检测手头有大量的未标注图像又不想花几周时间去打框那这篇文章值得你仔细看一遍。我会把框架的设计思路、代码结构、训练细节和真实踩过的坑全部摊开来讲。1. 为什么我放弃“堆标注”的路线半监督与YOLO结合的动机1.1 真实项目背景一万张图标注到一半效果却没到预期这个框架不是凭空想出来的。当时我在做一个面向细粒度工业质检的检测项目目标是在产线图片里定位几种外观缺陷。第一批数据就让人头疼质检专家人工标了1200张图每张图平均耗时8分钟前后花了两周多结果训练出来的YOLOv5模型在验证集上mAP只有46.3。这个数字意味着什么意味着在现场根本没法直接上线漏检和误检都肉眼可见。后来另一个渠道弄到了大量未标注的产线图像粗算有一万张以上。当时团队里有两种声音一种说继续招人标注把标注数量堆到5000张预算大概要再烧小十万另一种说要不要试试“先用模型标一批再把高置信度的加进训练集”这种伪标签思路。当时的直觉是纯人工标注这条路既贵又慢而且这类缺陷本身外观差异非常大同一类缺陷在不同光照、不同角度下长得完全不一样标注过程中专家自己都经常意见不一。如果只用1200张标注图模型很难学到那个“类内差异”的边界。而那一万多张无标注数据里恰恰包含了大量人工标注阶段难以覆盖的极端形态。所以真正的问题变成了怎么让模型自己从这些无标注图里学到东西。1.2 半监督学习的核心假设与应用边界半监督学习在目标检测里能起效其实依赖一个非常重要的假设有标注数据和无标注数据来自同一个数据分布。换句话说无标注图像里出现的物体类别、尺度范围、场景风格和有标注图像应该是一致的。我见过很多项目直接拿网络抓图当无标注数据结果类别对不上、分布漂移严重伪标签噪声大得根本没法用。半监督学习的基本原理可以这样理解有标注数据提供了“准确的答案”无标注数据则提供了“丰富的题目”。模型先在少量有标注数据上建立初步认知然后试着给无标注数据一个预测。预测结果中高置信度的部分就被当作新的“答案”回填进训练集让模型在更多“题目”上继续学习。理想情况下模型见过的样本越多学到的特征就越稳定、越泛化。但这也带来了边界条件如果基座模型的初始能力太差第一批生成的伪标签噪声就很高后面就是“越学越歪”。所以半监督框架里对伪标签的置信度筛选、类别平衡处理、教师模型的更新节奏都会直接影响最终效果。后面我会逐个展开。1.3 为什么选YOLO而不是DETR或其他检测器很多人问我做半监督检测为什么偏偏选YOLO而不是DETR、Faster R-CNN这类结构我当时的判断是三个原因第一YOLO的生态足够成熟不管是YOLOv5、YOLOv8还是新一点的版本训练脚本、部署方案、数据增强都现成改动成本低。第二半监督检测最核心的改造点在于伪标签与匹配策略这需要检测头能够灵活地控制正负样本定义而YOLO系列基于anchor或anchor-free的标签分配机制比较直白非常容易插入“额外伪标签监督”这条分支。第三我们的项目最终要在边缘设备上做推理YOLO的部署成熟度和推理速度都更符合实际需求DETR虽然端到端省事但训练收敛慢、工程成本高在工业质检这种对稳定性和时效性要求都很高的场景里性价比反而低。所以我最终选定了以YOLO检测头为底座在训练阶段外挂一个半监督分支。这套设计可以复用到YOLOv5/v8以及后续版本关键逻辑不变。2. 框架核心机制师生模型、伪标签与一致性正则的搭配2.1 整体架构Teacher生成伪标签Student负责学习这个框架的核心结构是Unbiased Teacher思路和STAC、Soft Teacher这些方法是同一脉的。它由两个结构相同的模型组成一个称为Teacher教师模型一个称为Student学生模型。训练流程可以拆成三步用有标注数据正常训练Student这一步和普通YOLO训练完全一样。让Teacher对无标注图像做推理生成经过筛选的伪标签。把无标注图像连同伪标签一起交给Student训练让Student同时从“标准答案”和“伪答案”中学习。但师生模型之间不是静态的Teacher的权重不是固定不变而是通过指数移动平均EMA不断向Student靠近。这样Teacher就能吸收Student在大量有标注数据上学到的能力又不会因为单次迭代的波动产生太剧烈的伪标签变化。简单来说Student一直在“做题”Teacher则像一个经验丰富但决策更稳健的老师他看的题更多给的答案也更稳定。2.2 伪标签生成流程置信度阈值、类别过滤与坐标后处理伪标签是整个半监督流程里最容易翻车的地方。很多人以为“跑一次模型推理把score大于0.5的框存下来”就完事了实际上没那么简单。我在框架里对伪标签生成做了下面几层处理第一置信度阈值。阈值设太高伪标签数量太少无标注数据利用不充分阈值设太低伪标签噪声太大会把错误信息放大。我最终采用的不只是全局固定阈值而是对每个类别分别计算统计量取置信度分布的高分位点作为阈值。这样天然适应了类别间难易度不平衡的问题。第二类别过滤。对于某些容易出现“相似误检”的类别比如把背景纹理误检成缺陷我会额外加一个类别级阈值惩罚项。这个设计来自一个很痛苦的教训我们的背景干扰样本特别多模型经常在无标注图上给出低置信度但类别高度集中的“幻觉框”如果不管Student很快会被带偏。第三坐标后处理。YOLO推理输出要经过NMS去重但NMS本身有很多参数可以调。半监督场景下我建议把IoU阈值稍微调高一点比如0.7防止两个高重叠真实目标被合并成一个而Confidence阈值则按类别动态调整。同时在YOLOv5/v8的原始输出中如果模型开启了多标签属性需要关闭多标签分类因为伪标签任务需要的是互斥类别输出这和真实标注的行为保持一致。2.3 EMA权重更新与动量设计EMA的实现并不复杂每个训练step里Teacher权重的更新方式为teacher_weights decay * teacher_weights (1 - decay) * student_weights这里的decay动量系数是关键。我最早直接用0.999发现训练初期的Teacher能力增长太慢伪标签质量在头几个epoch里跟不上Student的步伐调成0.99后又发现Teacher被Student的短期震荡带着跑伪标签分布很不稳定mAP曲线像锯齿一样。最后的经验是前5个epoch用0.99之后切换到0.999。前期教师权重快速跟上学生后期教师保持“记忆慢、抗波动”的节奏。这个两阶段EMA调度在COCO子集和我们的工业数据上都有效mAP能再提1.5到2个点。这个细节很多论文里不会写但实测非常重要。EMA还有一个工程上的好处因为Teacher是EMA权重它不参与梯度计算不需要为Teacher分配大量显存优化器状态。实际上可以把Teacher的前向推理放在torch.no_grad()下和Student共享同一批数据训练显存只增加原始推理的部分可控。2.4 一致性正则如何真正用进YOLO训练半监督学习的另一个基础操作是一致性正则化。这个听起来玄乎本质上就是同一个图像被做两次不同的随机增强模型在两个视图上的预测应当一致。如果Teacher对强增强后的图像给出了高置信度检测框那么Student在原始弱增强图像上也应该输出差不多的框。YOLO的检测头输出是“框坐标 类别概率 置信度”所以一致性约束可以直接落在分类分支和回归分支的损失上。不过直接对坐标回归结果做一致性约束有一个问题两次数值输出的坐标差异可能不显著但损失很大这会干扰正常回归目标。我最终采用的是对分类分支做KL散度约束对回归分支保留Teacher给出的伪框坐标作为目标用CIoU Loss更新而不是强制Student输出与Teacher完全相同回归值。这样处理既保留了正则化约束的作用又不会过度强硬地惩罚正常的回归偏差。框架中还使用了“弱增强Teacher 强增强Student”的方式Teacher看到的图像是常规的随机翻转、缩放、颜色抖动Student看到的图像在此基础上叠加了更激进的Mosaic、CopyPaste和RandomAffine。这样Student被迫从更难的视角学会鲁棒特征一致性正则化才有额外增益否则两边都看同一幅图、同一种增强学不到什么新东西。3. 框架代码落地目录结构、核心模块与训练流程拆解3.1 项目目录总览打包出来的zip目录大概长这样semi-yolo-framework/ ├── configs/ │ ├── semi_yolov5s.yaml │ └── semi_yolov8s.yaml ├── data/ │ ├── labeled_data.yaml │ └── unlabeled_data.yaml ├── semi_det/ │ ├── models/ │ │ ├── student.py │ │ └── teacher.py │ ├── pseudo/ │ │ ├── generator.py │ │ └── threshold.py │ ├── ema/ │ │ └── momentum_scheduler.py │ ├── losses/ │ │ └── semi_loss.py │ └── trainer/ │ └── semi_trainer.py ├── tools/ │ ├── train.py │ └── evaluate.py └── weights/这个结构不强求完全一致但建议保持几个关键模块独立pseudo/generator.py负责伪标签生成和保存ema/momentum_scheduler.py负责EMA动量调度trainer/semi_trainer.py串联整个训练循环。不要把逻辑混在YOLO官方训练脚本里否则后期调参很难受。3.2 Teacher模型加载与EMA更新实现Teacher模型的初始权重是从预训练权重直接复制过来的不需要额外训练。EMA更新的关键代码我这样写def update_ema_weights(model, ema_model, momentum): with torch.no_grad(): for (name, param), (ema_name, ema_param) in zip( model.named_parameters(), ema_model.named_parameters() ): if param.requires_grad: ema_param.data.mul_(momentum).add_( param.data, alpha1.0 - momentum ) for (name, buffer), (ema_name, ema_buffer) in zip( model.named_buffers(), ema_model.named_buffers() ): ema_buffer.data.copy_(buffer.data)这里有个细节容易被忽略BN层的running_mean和running_var也要同步。很多人只同步了模型参数忘了buffer导致Teacher模型的归一化统计量一直停留在初始状态推理结果偏差很大。我在第一版就踩了这个坑后来查代码才发现EMA更新时没有处理buffers。所以在框架里我对所有buffer做了直接拷贝确保Teacher的BN统计量和Student保持同步。3.3 伪标签生成器从模型输出到训练目标伪标签生成器核心逻辑是对无标注图像做一次Teacher推理然后执行NMS并过滤def generate_pseudo_labels(teacher_model, images, class_thresholds, nms_iou0.7): teacher_model.eval() with torch.no_grad(): preds teacher_model(images) dets non_max_suppression( preds, conf_thresNone, # 分两步过滤 iou_thresnms_iou, multi_labelFalse, max_det300, ) final_labels [] for img_idx, pred in enumerate(dets): keep [] if pred is None or len(pred) 0: continue for det in pred: cls int(det[5]) score float(det[4]) if score class_thresholds[cls]: keep.append(det) if len(keep) 0: continue final_labels.append(keep) return final_labels我特意把conf_thres设置为None先让NMS把物理上重叠最多的边界框去掉再用类别阈值精细过滤。顺序反过来的话有些低置信度但真实的目标框可能先被全局阈值杀掉NMS阶段没有机会保留。这类细节不跑实验很难发现。另外我建议每N个epoch重新生成一次伪标签而不是每个epoch都重新算。伪标签生成要运行全量无标注数据计算成本高如果每个epoch都刷新还会带来标签抖动。我的默认配置是每5个epoch全量重新生成伪标签中间几个epoch用缓存好的伪标签。3.4 Student损失函数有标注损失与无标注损失的组合Student训练的损失函数分成两部分loss_total loss_supervised lambda_semi * loss_unsupervisedloss_supervised和标准YOLO训练完全一样包含box回归损失、objectness损失和分类损失。loss_unsupervised只对无标注图像上被伪标签标识过的位置计算损失背景区域不参与回归分支的反传。lambda_semi这个权重也需要调度。训练初期如果直接给一个很大的权重伪标签噪声会主导训练过程。我采用的做法是前3个epoch把lambda_semi设置为0相当于先让Student在有标注数据上“站稳脚跟”然后线性拉升到0.5或1.0。如果你用的数据集类别数比较多建议从0.3开始调不要直接上1.0。损失组合代码里还有一个易错点伪标签框必须经过与真值相同的标签分配过程。不能直接把伪标签框和Student的预测做loss因为YOLO检测头是一个anchor-based或anchor-free的匹配过程伪标签要先被匹配到具体的特征图位置和anchor上才能参与损失计算。框架里复用了YOLO的assign逻辑对伪标签做匹配而不是手工拿预测框和伪标签做IoU。3.5 训练主循环一个Step里发生的事情以YOLOv5训练循环为参考一个完整的训练step可以这样写for batch in labeled_loader: images_l, labels_l batch images_u, _ next(unlabeled_iter) # 1. Student正常前向和反向 loss_s, loss_items_s student_model(images_l, labels_l) loss_u student_model(images_u, pseudo_labels_u) # 伪标签参与 loss loss_s lambda_semi * loss_u loss.backward() # 2. Student权重更新 optimizer.step() optimizer.zero_grad() # 3. 更新Teacher的EMA权重 momentum ema_scheduler.get_current_momentum() update_ema_weights(student_model, teacher_model, momentum)如果用的是YOLOv8等基于torch训练脚本的版本整个逻辑可以无缝嵌入train循环中。关键在于让Student同时看到有标注和无标注数据而不是用两个模型分开训练。两阶段训练先训练Teacher再蒸馏Student的问题在于Teacher能力到后期不再增长Student的上限被锁死而在线师生训练让Teacher始终跟随Student进步伪标签质量是动态提高的。4. 实验复现与踩坑记录从COCO子集到自定义数据集4.1 实验设置与Baseline对比为了验证框架的有效性我在两个场景上做了实验第一个是COCO标准半监督设定的子集划分第二个是我们自己收集的工业缺陷检测数据集。COCO实验设置为使用COCO train2017中1%的标注数据约1230张剩余约115000张图像作为无标注数据。基座模型是YOLOv5s输入分辨率640x640训练120个epoch。Baseline直接用1%标注数据训练不加入任何无标注数据。方法标注数据量无标注数据量mAP0.5:0.95Baseline只有有标注1230张0张18.2固定伪标签不更新Teacher1230张115000张21.4加EMA两阶段调度1230张115000张23.1半监督YOLO完整框架1230张115000张24.6工业数据实验中标注数据1200张无标注数据6200张基座是YOLOv5m。Baseline mAP是46.3完整框架提升到57.1提升幅度接近11个点。这里最关键的一点是伪标签并不是越多越好过滤掉低置信度样本之后无标注数据的有效利用率反而更高。4.2 最重要的三处踩坑记录踩坑一置信度阈值设太高或者太低特征完全不同。我一开始用全局阈值0.9发现伪标签数量少得可怜很多真实目标被丢弃无标注数据几乎白给。后来调到0.5又发现伪标签里混入了大量误检框模型训练时损失震荡得很厉害mAP反而比Baseline还低。最终的方案是对每个类别单独计算一个阈值运行一次模型统计训练集上每类目标的置信度分布取P75分位数作为初始阈值再根据验证集mAP微调。这个思路比拍脑袋定一个全局值可靠得多。踩坑二EMA动量固定为0.999时训练前期的梯度信号太弱。前面提过动量固定0.999会让Teacher在训练前期学习太慢。伪标签质量跟不上Student的能力成长相当于一直在刷新旧知识而Student早就在新知识上跑得很远了。两阶段调度0.99先用20个epoch再切到0.999是实验对比后最稳定的方案。我甚至在一次实验里把初始动量调成0.98效果也还行但后续mAP平台期来得太早。踩坑三Mosaic增强和伪标签坐标的冲突。YOLO自带的Mosaic增强会把4张图拼在一起这会直接改变图像的坐标空间。如果伪标签是对原始图像生成的而Student训练时用的是Mosaic增强后的图像那伪标签框的位置就对不上了。我当时在第一版框架里没有处理这个问题结果发现无标注损失的回归项很大训练始终不收敛。解决方法是无标注分支用Mosaic时必须同步对伪标签框做相同的拼接变换。最简单的方式是关闭无标注数据的Mosaic增强只保留Student的有标注分支用Mosaic。这样虽然牺牲了一部分数据多样性但换来的是稳定收敛和更低的回归损失。4.3 一个常见误判并不是无标注数据越多越好我一开始的直觉是无标注数据越多半监督增益越大。实验做下来发现不完全对。当无标注数据量从1万张增加到3万张mAP提升只有0.4左右但训练时间翻倍。原因在于这些新数据大多在分布密度较高的区域反复出现边际收益非常低。半监督真正有效的是“补充边界样本”而不是“重复尾部样本”。实操建议是先用框架自带的embedding聚类脚本对无标注数据做一次粗聚类挑选出与当前有标注数据特征距离最大的那部分样本作为无标注池这样训练效率最高。简单说就是优先选择那些“看起来陌生”的样本而不是盲目堆量。5. 工程化细节与调参经验训练速度、显存和稳定性5.1 显存优化伪标签缓存与半精度训练半监督训练比普通训练多一个Teacher前向推理过程显存占用明显上浮。我第一版直接在训练循环里同时跑Teacher和Studentbatch size为16时一块24G的3090直接爆显存。后面做了以下几个优化Teacher前向放在torch.no_grad()下显存占用只有激活值不回传梯度。伪标签不实时生成而是先跑一轮推理把结果序列化保存为.json或.npy文件训练时直接加载。混合精度AMP对Teacher和Student同时开启。因为伪标签生成对精度要求不高FP16推理完全够用。如果显存仍然紧张可以把Teacher的输入分辨率降到Student的一半比如Student用640x640Teacher用416x416。这个操作对伪标签质量的影响很小但显存能省近一半。这些优化做完整个框架单卡可以跑通数据规模再大的时候再考虑分布式。5.2 Batch Size与学习率半监督训练中无标注数据参与梯度计算的比例会影响收敛速度。我推荐的有标注/无标注batch size比例是1:1到1:2。比如有标注batch size是16无标注batch size也是16才能保证伪标签损失在每一步都有足够的样本。学习率的调节和普通YOLO训练逻辑一致但要注意的是增加无标注分支之后模型看到的样本总量变大学习率可以适度上调10%-20%。我的经验是YOLOv5s在COCO子集上从lr0.01调到0.012收敛速度更快没有明显震荡。如果你的模型从预训练权重开始训练建议保持官方学习率先用小批量实验10个epoch观察无标注损失的下降趋势再微调。5.3 有标注与无标注数据使用不同增强这是我这条路径里收益最高的工程策略之一。有标注数据使用标准YOLO增强包括Mosaic、CopyPaste、HSV增强、随机翻转。因为这是模型学习“标准答案”的唯一来源增强越丰富基础特征越稳健。无标注数据建议使用相对温和的增强关掉Mosaic保留随机翻转、缩放和颜色扰动。原因很简单无标注数据的训练信号来自伪标签而伪标签本身已包含Teacher的预测噪声。如果再加上极其激进的几何破坏Student会把“因增强导致的框偏移”学进去回归分支的损失会非常不稳定。在之前实验里无标注分支采用Mosaic后回归损失甚至是正常情况下的3倍。5.4 评估时容易被低估的问题类别不平衡半监督训练还有一个容易忽略的地方伪标签的类别分布往往不均衡。某类样本在无标注数据中占比高生成的伪标签数量就多Student训练时会被这类样本主导一些稀有类虽然置信度高但数量太少学生模型几乎学不到它们。我的处理方式是在统计每个类别伪标签数量之后通过采样策略对无标注数据做类别再平衡。具体做法是每个epoch迭代时按类别比例动态调整无标注数据的采样概率让稀有类别的伪标签在训练中拥有更高的“出场频率”。这一步在COCO这种类别分布相对均匀的数据集上效果一般但在工业数据上非常关键因为缺陷类别天然存在严重不平衡。5.5 跑数小时之后的稳定性检查最后分享一个检查训练是否稳定的技巧如果训练过程中无标注分支的损失一直下降但验证集mAP却不再增长多半是伪标签进入了“自我强化”的状态——Teacher反复确认自己已经确定的框对尚未学会的困难目标视而不见。这时候我通常做两件事一是把lambda_semi临时降为0让Student只学有标注数据跑几个epoch再恢复二是对伪标签生成加入随机Dropout随机丢弃10%的框让Teacher“被迫”寻找新的伪标签候选而不是总盯着那几类高分框。我自己的体会是半监督训练不能完全“无人驾驶”至少每20到30个epoch要检查一次无标注分支的损失曲线和伪标签数量的变化。如果伪标签总量持续下降说明Teacher的置信度整体在降低大概率是数据或增强设置出了问题。这套框架从最初在工业项目里被逼出来的原型到现在整理成zip分享出来中间反复迭代了很多版本。里面最有价值的可能不是具体某个调参技巧而是“如何让模型信任自己又不过度信任自己”这一整套平衡设计——伪标签要信但不能全信Teacher要跟但不能跟太紧无标注数据要用但不能无脑堆量。理解了这个平衡你在自己的数据和场景上遇到新问题时也就知道该往哪个方向去调了。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门