
简介基于半监督学习训练YOLOv7的完整源码包面向目标检测算法开发者与机器学习初学者旨在解决标注数据稀缺时如何利用大量未标注图像提升检测精度的问题。包内完整呈现伪标签生成、联合训练、一致性正则化、分阶段训练等半监督策略的代码实现并拆分为数据加载、模型结构、损失函数、训练循环、验证评估等模块从伪标签生成到mAP指标评估均有对应脚本除基础训练流程外还包含伪标签置信度处理与时间衰减策略便于对照学习与二次开发。资源共34个文件以Python源码为主26个py辅以4个xml配置、2个txt说明、1张示例图片和1个IDE工程文件RAR包整体仅515KB轻量且结构清晰。目前已有674人学习下载适合想要快速复现半监督YOLOv7训练流程或将其迁移到自定义检测任务的开发者。1. 半监督学习训练YOLOv7标注不够时怎么把检测精度再往上提你手里握着几百张人工标注好的图片另一边是几万张同场景的原始抓拍人工标注又贵又慢直接丢又觉得可惜。这种情况在工业质检、安防和自动驾驶数据采集里太常见了。半监督学习训练YOLOv7干的事情就是把这些无标签图片变成可用信息用已标注数据训练出的模型去给无标签图片打伪标签再把高置信度的伪标签当作监督信号继续训练。这套路径不改变YOLOv7的网络结构只改造训练流程和数据加载方式所以很多基于YOLOv7的半监督源码包比如你看到的那个.rar会同时包含数据划分脚本、伪标签生成脚本和改过的train脚本。这篇文章就用一线工程视角讲清楚半监督训练YOLOv7的原理、代码改法和参数调优不解读某个特定压缩包而是把这类项目通用可复现的部分讲透。2. 半监督目标检测的两种范式伪标签和一致性正则YOLOv7该选哪个2.1 伪标签法在YOLOv7训练里的落地路径伪标签法可以看作是一个“自训练”循环先用有标签数据训练一个基础检测器然后用这个检测器对无标签图片做推理输出框中经过置信度阈值筛选后以YOLO的txt标签格式写进labels_unsupervised目录最终与真实标签混合进下一轮训练。这里的关键在于“阈值筛选”——YOLOv7回归出的分类概率和objectness可以联合排序通常只保留conf 0.7具体值后面讲的检测框。在源码实现上多数压缩包里的做法是先写一个单独的generate_pseudo.py脚本内部调用YOLOv7的detect.py或者直接跑推理目录。我一般建议跳过nms之后的处理直接取模型head的原始输出因为你要的是坐标和置信度不需要做可视化。示例代码如下import torch from utils.datasets import LoadImages from utils.general import non_max_suppression, scale_coords model torch.load(best.pt, map_locationcuda) model.eval() for path, img, im0s, vid_cap in LoadImages(images/unlabeled): img img.to(cuda).float() / 255 with torch.no_grad(): pred model(img)[0] pred non_max_suppression(pred, conf_thres0.7, iou_thres0.45) for det in pred: if det is not None and len(det): det[:, :4] scale_coords(img.shape[2:], det[:, :4], im0s.shape).round() # 按YOLO txt格式写入这段代码的要点先加载训练中保留的teacher模型把无标签图片跑完最后将每个目标的置信度和类别额外拼在标签文件尾部或者单独存一个置信度数组。为什么不直接用txt的5列因为后续训练时可能要根据置信度权重来调节损失或者做阈值调整所以多存一行是有用的。2.2 一致性正则和多视图增强的取舍一致性正则的思路是对无标签图片分别做弱增强和强增强然后要求模型对二者的输出尽量一致。听起来不错但放到YOLOv7上问题很多。首先检测头的输出是一组anchor框的回归量弱增强和强增强后的同一目标其相对anchor的偏移完全不同回归分支的一致性约束很难构造。其次YOLOv7用的是anchor-based结构对平移和尺度变化非常敏感强增强会把框的位置扰动到另一个anchor的归属直接对feature map做MSE loss会迫使模型去模糊化导致训练震荡。市场上常见的半监督检测框架比如Unbiased Teacher其实也主要依赖伪标签一致性正则往往只用在分类置信度上面。YOLOv7的每个输出层已经对每个anchor预测了objectness和class prob我们可以用KLDiv或MSE约束强增强后的分类分布接近弱增强版。这样做不会影响框回归训练稳定很多。但这类改造需要改动forward的返回在源码包中不一定都实现。如果你的目标是不大规模改代码优先坚持伪标签路线一致性正则可放后面作为增强。2.3 综合选型伪标签是主基线EMA模型做稳定器我做半监督YOLOv7训练时始终用伪标签作为主基线两个原因第一实现简单YOLOv7的内部结构完全不用动只需在训练循环中增加无标签数据的损失分支第二检测任务里伪标签的质量通常高于一致性正则因为检测框本身就是具有空间语义的判别结果高置信度框往往比分类置信度更可靠。但伪标签方案的一个重要变体是使用EMA模型生成伪标签。所谓EMA就是维持一个模型权重的滑动平均版本训练阶段不更新它每轮用当前权重和EMA权重加权平均然后每次无标签推理都用EMA模型来打伪标签。这个操作能把伪标签的方差压低避免训练过程中的异常噪声。在YOLOv7源码里train.py本身就有EMA的选项很多版本默认开启我们只需要把生成伪标签的路径从best.pt换成EMA参数即可。下表给出两类方案的对比维度固定teacher伪标签EMA teacher伪标签标签时效性较滞后每轮需手动刷新随训练动态更新实时性高噪声水平若teacher初始不好噪声持续存在平滑掉单轮异常更稳实现成本低只需离线推理需要修改训练循环内推理逻辑收敛速度较快但可能陷入自己错误略慢但精度上限更高因此如果你拿到的是一个已经带EMA训练的YOLOv7源码包可以在EMA更新函数旁边找到伪标签生成接口优先使用它。2.4 如何识别源码包里的“半监督”改造位置拿到一个半监督YOLOv7源码包不要直接去看model/yolov7.py因为网络结构通常没变。重点看train.py和datasets.py的修改点train.py里一般会增加两个损失变量一个是有标签数据的boxclsobj另一个是无标签的objcls对应loss权重分别是l_w和u_wdatasets.py里通常会有一个concat逻辑能把有标签batch和无标签batch的结果拼接成一个batch输入。找到这两个位置整套训练逻辑就清晰了。如果源码包里有.rar里的README里面大概率会画一个pipeline图数据流从两个数据集出发分别进入模型然后再融合损失。一般pipeline展示的路径都要对应这几个关键函数。3. 准备半监督数据集并改造YOLOv7数据管线3.1 数据集目录结构和标注文件怎么放YOLOv7默认读取图片和txt标签时标签文件名必须与图片名相同且每一行格式为class x_center y_center width height。而半监督训练要求无标签图片不要参与常规标签匹配否则会因找不到标签文件而报错。常见的做法是建立一个独立目录树dataset/ ├── images_supervised │ ├── train/ │ └── val/ ├── labels_supervised │ ├── train/ │ └── val/ ├── images_unsupervised └── pseudo_labels └── current/在这套结构里images_unsupervised存放所有无标签图片它们的标签文件并不放在labels_supervised下而是在每次生成伪标签后由脚本写到pseudo_labels/current/文件名与无标签图片名一致内容格式和真实的YOLO标签完全一样。然后train.py的--data配置里会写成train: ./images_supervised/train val: ./images_supervised/val labeled: ./images_supervised unlabeled: ./images_unsupervised pseudo_label_root: ./pseudo_labels/current注意这里额外定义了unlabeled和pseudo_label_root两个字段这不在YOLOv7官方yaml定义内只供半监督版本的数据加载器识别。如果你看到的源码包在数据加载时不认识这些字段说明你还得自己动手加。3.2 在YOLOv7的Dataset类里同时读取有标签和无标签分支YOLOv7官方的LoadImagesAndLabels类负责从路径加载图片及其标签。半监督改造的核心是再写一个LoadUnlabeledImages类它只负责返回图片张量不读取标签路径。然后在训练的DataLoader构造阶段同时实例化这两个数据集并对它们的batch做拼接。from utils.datasets import LoadImagesAndLabels, LoadUnlabeledImages labeled_dataset LoadImagesAndLabels(opt.labeled, label_rootopt.label_root, img_size640, batch_sizeopt.batch_size) unlabeled_dataset LoadUnlabeledImages(opt.unlabeled, img_size640, batch_sizeopt.batch_size, pseudo_rootopt.pseudo_label_root) labeled_loader torch.utils.data.DataLoader(labeled_dataset, batch_sizeopt.batch_size, shuffleTrue) unlabeled_loader torch.utils.data.DataLoader(unlabeled_dataset, batch_sizeopt.batch_size, shuffleTrue)在实际训练循环里通常是交替取两个loader的batch先取一个有标签batch再取一个无标签batch然后拼接在一起走一次forward。代码里可以看是否有torch.cat([imgs_l, imgs_u], 0)和torch.cat([labels_l, labels_u], 0)这样的操作。注意拼接时标注的类别和坐标都属于原始图片尺寸下归一化的坐标只要img_size一致拼接没有问题。但无标签数据的伪标签是在上个检查点模型下生成的所以这里要注意pseudo_label_root下的内容会离线更新训练中的LoadUnlabeledImages每次启动都重新读入这个目录。3.3 生成伪标签并合并到训练批次的pipeline伪标签不能在训练过程每一轮都生成那样太耗时。常见的做法是先训练N个epoch比如前20轮只用有标签数据得到初始模型之后每隔M轮运行一次generate_pseudo.py覆盖更新pseudo_labels/current/然后后续训练中加载新伪标签继续训练。这个周期性刷新机制决定了你需要一个shell或Python侧的调度器。我习惯在训练启动脚本里加一个逻辑块# 伪阶段每隔10轮刷新一次伪标签 for global_epoch in $(seq 1 1 300); do if [ $((global_epoch % 10)) -eq 0 ]; then python generate_pseudo.py --weights ../runs/train/exp/weights/best.pt --source ./images_unsupervised --output ./pseudo_labels/current fi python train_one_epoch.py --epoch $global_epoch done当然更优雅的是直接改train.py内部的循环——在if epoch % 10 0时调用一个推理函数。需要注意每次刷新伪标签后模型看到的是“逐渐变好”的标签这会帮助模型收敛。但也会带来风险如果某个epoch的模型严重退化生成的伪标签会污染后续训练。所以我会在生成伪标签前先在验证集上检查mAP是否比上次刷新时高如果mAP掉了超过5%则跳过本次刷新沿用上一批伪标签。伪标签生成之后训练时还会做一项重要操作把伪标签的置信度信息作为损失权重。部分源码包在loss计算时从标签额外字段读取confidence然后对cls和obj损失做乘权。如果没有这个设计你可以忽略置信度只用高阈值过滤也够用。4. 训练YOLOv7的关键参数阈值、EMA和增强强度怎么调4.1 伪标签置信度阈值先高后低伪标签阈值直接决定无标签数据的监督噪声。阈值太低一堆垃圾框会成为标签模型被带偏阈值太高无标签数据几乎被丢弃半监督就名存实亡。实际调参中我一般把conf_thres初始设到0.8然后每50轮下降0.05最终稳定在0.6左右。原因是训练初期模型还不可靠只用最确定的框随着模型变强逐渐引入更多中等置信度的框来增加数据多样性。从损失曲线来看如果无标签数据的损失在阈值降低后立刻下降说明模型正在顺利学习如果损失不减反增且验证集mAP停滞那大概率说明新增的伪标签大量是错误框。此时需要暂停降低阈值甚至回升0.1。4.2 EMA动量参数对班级平衡的影响YOLOv7源码里的EMA默认动量通常为0.9999。这个值越大EMA模型更新越慢对历史权重的依赖越高生成的伪标签更稳定但也会跟不上模型快速进步的步伐。半监督训练中我推荐设置ema_decay0.999让EMA对新权重有更明显的反应。原因是伪标签要跟随当前模型的表达能力否则你就会一直用半个月前的旧模型给新数据贴标签。如果你发现无标签数据的loss持续上升但伪标签的类别分布已经严重倾斜比如全是某一类检查EMA动量是否过大导致类别均衡被冻结。一个实用的检查方式是每次刷新伪标签时统计类别数量的直方图若某类占比异常超过90%说明EMA模型对“稀有类”的识别能力没有反映到伪标签生成中考虑调低动量或对稀有类单独降低阈值。4.3 强弱增强分配和增强强度在只使用伪标签的半监督模式中无标签图片通常用轻量增强随机翻转、缩放、色域抖动和YOLOv7官方的MOSAIC策略保持相同。但注意不要对无标签图片使用cutout或mixup等破坏语义的增强因为伪标签框可能是错的如果图片已经被强增强破坏错误会被放大。我常用的设置是无标签图片只做flip和轻度HSV变换有标签图片保持正常的mosaic增强。如果你决定在前面加分类一致性正则再考虑对无标签图片做一次强增强如随机90度旋转、随机裁剪。在源码中这通常体现在LoadUnlabeledImages的augment参数上augmentTrue但mosaicFalse。4.4 无标签损失的权重怎么设YOLOv7原版损失是三部分box loss、objectness loss、class loss。半监督改造通常会额外增加一个无标签分支的objectness和class loss并使整体损失为L L_supervised lambda_u * L_unsupervisedlambda_u建议从0.2开始让它先作为辅助信号。如果无标签图片数量是有标签的20倍以上可以逐渐加到1.0。但有一个细节无标签分支不应计算box回归损失因为伪标签的坐标是模型自己预测的去回归它们没有意义。在源码的loss函数里可以通过torch.is_tensor(label_confidence)来区分或者把无标签标签的box_loss乘以mask0。我见过一些半监督实现直接对无标签数据也计算box_loss结果训练发散。务必检查loss代码里对无标签数据是否关闭了box_loss。参数推荐值影响说明conf_thres0.6~0.8 动态伪标签数量与质量平衡ema_decay0.999更新过快过慢都会降精度lambda_u0.2→1.0无标签损失权重先小后大增强模式无标签flipHSV防止语义破坏伪标签刷新间隔10轮太长跟不上模型太短浪费资源标签拼接每轮一个batch交替两种数据平衡更新在训练脚本里这些参数一般会集中在一段配置代码或.yaml文件中。改成半监督后你还要注意batch_size翻倍的问题因为每步同时处理有标签和无标签显存占用是原来的两倍。如果显卡不够将有标签batch和无标签batch各设为原来的一半从而总batch不变。这一步也会影响BN的统计值建议保持总batch不小于16。5. 从源码运行到排错半监督YOLOv7训练的最小可行命令5.1 解压源码并搭建环境假设你解压了那个.rar文件里面通常是train.py、models/、utils/、data/和一组半监督专用的data/unlabeled.yaml。先检查环境YOLOv7需要PyTorch1.8如果你GPU是Ampere或以上架构直接安装官方要求的requirementspip install torch1.8.0 opencv-python4.1.2 matplotlib pandas pyyaml requests tqdm seaborn然后做一次纯有标签的冒烟测试确认原始权重和前向传播没问题。这一步很像嵌入式开发里先烧一个空程序验证硬件不要跳过去。用官方预训练权重跑一个batch若正常再进入半监督配置。5.2 修改配置并运行半监督训练在data/目录下新建halfsup.yaml文件内容除了前面提到的labeled/unlabeled字段外其余和普通YOLOv7配置一样但train和val指向有标签数据。接着运行python train.py --data data/halfsup.yaml --cfg cfg/training/yolov7.yaml --weights yolov7.pt --batch-size 16 --epochs 300 --semisupervised --psi-threshold 0.75 --lambda-u 0.5关键参数说明--semisupervised是半监督开关打开后才能读取unlabeled字段--psi-threshold是伪标签阈值对应当前所述的高置信度过滤--lambda-u是无标签损失权重。如果你的源码包没有这些参数就去查argparse常见名字有--sup-start-epoch有标签预训练轮数和--refresh-interval。运行中你会看到控制台输出的loss行里多了一项Unsup Loss如果一直为0说明数据管线没接到无标签分支。另外务必要在训练过程中定期清理显存因为周期性生成伪标签时会额外加载一次模型推理很容易显存溢出。我习惯将生成脚本独立成一个进程训练结束后隔一帧再执行。5.3 训练日志里怎么看伪标签的质量打开TensorBoard或控制台曲线重点关注三件事。第一无标签损失的绝对值。初期它可能是有标签损失的0.3~0.5倍如果突然掉到0.001以下说明伪标签几乎全被模型“照抄”了但真实性能没有提升。第二有标签验证集的mAP应该每刷新一次伪标签就更新一次若mAP连续两次刷新都在下降立刻停掉训练恢复上一次的checkpoint并调高阈值。第三看伪标签数量日志中每隔一轮打印Pseudo labels: 521 boxes / 100 imgs如果这个数字急剧上升大概率是阈值太低。5.4 常见坑和处置手段无标签图片和伪标签文件名对不上是最常见的报错。YOLOv7的Dataset在读取标签时会自动用图片名替换.jpg为.txt但半监督改造后如果伪标签目录下的文件命名带上了_pseudo后缀就无法匹配。我的做法是在生成脚本里直接保持原名并且在datasets.py里加一个print来打印第一次读到的无标签名先确认路径拼接正确。另一个高频坑是BN层的统计量偏差。半监督训练时有标签和无标签图片的分布可能有差异导致BN在无标签分支上统计漂移。若看到无标签的class loss在某个epoch之后剧烈震荡检查是不是把两个batch拼接进同一个batch但BN统计量仍然按整体计算——最好是保持YOLOv7的batch_normalization参数固定或者让无标签分支的batch单独跑forward且不更新running stats。如果训练过程中显存爆掉优先将--batch-size减半并增大累积梯度步数而不是减少无标签数据量。无标签数据占比减小会让半监督收益锐减得不偿失。6. 进阶技巧动态阈值与不确定性筛选让每一张无标签图都有用固定阈值是半监督YOLOv7的第一版方案但它有一个明显弱点模型对不同类别的把握程度不同。比如“人”类阈值0.7很可靠“猪”类可能0.9才可靠。更好的方式是动态阈值——根据无标签图片所在类别在模型上的平均置信度即时调整阈值下限。实现动态阈值并不复杂你只需要在生成伪标签时读取模型输出的conf信息然后计算一个分类别的平均confimport numpy as np category_conf {i: [] for i in range(num_classes)} for det in all_predictions: for *xyxy, conf, cls in det: category_conf[int(cls)].append(float(conf)) # 动态阈值该类的平均置信度 - 0.2但不能低于0.5 dynamic_thr {} for cls, conf_list in category_conf.items(): if len(conf_list) 0: dynamic_thr[cls] max(0.5, np.mean(conf_list) - 0.2) else: dynamic_thr[cls] 0.7然后对模型输出按类别动态阈值过滤再生成伪标签。这个方法可以在不改变无标签数据量的前提下降低整体误检也让稀有类有更多参与训练的机会。如果嫌动态阈值还是粗糙可以采用TTA投票。所谓投票是对同一张无标签图片做两种不同尺度的推理比如原尺寸和放大1.2倍把两组检测框按置信度加权求和保留在两种尺度下都出现的框。这个思路能过滤掉单尺度下的幻觉框。落地时你可以用YOLOv7官方detect.py里的--augment参数开启Test Time Augmentation然后用nms合并结果。但注意TTA投票的耗时翻倍不宜在每轮刷新伪标签时跑太多图片。我通常只在无标签数据少千张以内且阈值已经很低但噪声依然大时使用。最后一个小技巧是“伪标签利用率监测”。最直接的方法是每隔一段时间在验证集上测试同时记录无标签数据的置信度分布。如果大部分无标签图片的伪标签数量为0说明阈值太高模型在“做减法”如果伪标签数量爆炸说明模型在“自信地胡说”。把阈值写成一个随epoch变化的分段函数比如从0.85线性降到0.6往往能比固定阈值多拿3-5个mAP点。你要做的只是把这个曲线写进generate_pseudo.py不用改网络结构。本文还有配套的精品资源点击获取