
简介面向计算机视觉课程设计将半监督学习与YOLOv7结合解决标注数据不足时训练效率低的痛点适合需要完成目标检测大作业的本科生或研究生。包内共36个文件以26个Python脚本为主完整覆盖数据标注转换、anchors聚类、半监督训练流程、验证评估与预测推理等关键环节另有4个XML用于配置说明2个TXT提供依赖环境与实现思路压缩包整体仅525KB轻量且便于部署迁移。已有250人学习下载项目曾获导师指导并在答辩中拿到97分高分整体完整、无需修改即可直接运行可作为课程设计或期末大作业的可靠交付模板。使用者可通过清晰的项目结构快速建立半监督目标检测的整体认知也可将自带数据按相同流程替换后开展实验或参考其训练日志分析与评估脚本来优化模型表现兼顾学习与实践双重需求。1. 半监督学习与YOLOv7结合课程设计选型的真正价值在哪课程设计最怕的不是模型训练不起来而是标注数据不够。YOLOv7的完整训练流程网上能搜到一堆但绝大多数教程都假设你有几千张带标签图片。现实是课程设计给的原始素材往往只有一二百张标注图剩下的全是没标签的原始图片。丢掉这些图片模型泛化能力撑不起最后的演示效果全部手动标注时间又不允许。半监督学习的价值恰好在这里它允许你用少量强标注数据加大量弱标注或无标注数据把YOLOv7的检测精度往上再推一档。这篇博文要讲的就是把半监督学习落到YOLOv7源码上的完整路径——数据怎么划分、伪标签怎么生成、阈值怎么设、多轮迭代怎么防止噪声累积。内容覆盖从理论到可运行命令课程设计想要复现或改造照做即可。2. YOLOv7源码训练基线在跑半监督之前先把监督模型的基础打牢2.1 半监督学习的起点为什么是“一只性能还行的预训练模型”半监督检测SSOD的常见做法是自训练Self-training先用有标注数据训练一个教师模型再让教师模型为无标注图片生成伪标签然后把伪标签当真实标签加入下一轮训练。这个流程成立的前提是教师模型本身的预测不能太差否则伪标签噪声会淹没掉真实梯度信号导致第二轮精度不升反降。课程设计场景里我一般会把YOLOv7官方仓库作为实现载体。它的结构稳定、训练脚本完整并且模型定义与推理脚本分离在后期替换教师模型或添加伪标签筛选逻辑时改动面小。首先要确认的是环境满足要求PyTorch 1.8以上、CUDA可用、YOLOv7依赖的包安装完毕。之后第一步不是直接开始半监督而是先把YOLOv7的监督训练跑通得到一个可以正常收敛的基线权重。2.2 用YOLOv7源码跑通第一次训练的最小命令拿到课程设计的数据后先把数据整理成YOLO格式目录结构如下dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ ├── train.txt ├── val.txt └── dataset.yamlimages/train里放训练图片labels/train里放对应的txt标注文件每行是class_id center_x center_y width height坐标值归一化到0到1之间。dataset.yaml中指定路径、类别数和类别名。整理完成后执行训练命令python train.py --data dataset/dataset.yaml --weights yolov7.pt --batch-size 16 --epochs 100 --img 640 --device 0 --project runs/train --name baseline--batch-size根据显存调整6GB显存建议8或1612GB可以到32。--img是训练分辨率640是YOLOv7的默认值课程设计数据如果目标偏小可以改用--img 1280配合较小batch。--weights用的是在COCO上预训练过的yolov7.pt这对小数据集至关重要——哪怕你的类别和COCO完全不同预训练权重的前几层特征提取器仍然有效收敛速度远快于从零训练。训练结束后查看runs/train/baseline/weights/best.pt是否生成。这个文件就是后续所有半监督迭代的基础教师模型。如果best.pt的验证精度在课程设计要求的及格线以上就继续下一步如果连训练都跑不起来先检查dataset.yaml里的路径是否有误、标注txt是否有空文件这两类问题占据了YOLOv7训练失败原因的一半以上。2.3 训练参数的选择依据与常见坑课程设计里的数据集规模通常不大训练参数不宜直接照搬COCO默认配置。--epochs设100轮的原因在于小数据集收敛快通常60轮之后mAP就趋于平稳后续轮次主要消除振荡。--batch-size在显存允许的条件下尽量偏大否则BatchNorm统计量在少量样本上漂移严重导致验证集精度大幅波动。还有一个容易被忽略的参数是--workersWindows环境容易因DataLoader的多进程问题卡死建议Windows下设为0或2Linux下可以按CPU核心数设置。--cache-images参数在数据量小于2000张时推荐开启把图片预加载到内存中每次epoch的读取时间能缩短一半以上。训练结束后的另一个重要动作是记录基线mAP。后续每加入一批伪标签模型精度是否真正提升都要和这个基线数值对比。如果加了伪标签反而掉精度那就说明伪标签筛选策略太松噪声样本过多需要收紧阈值或增加过滤条件。3. 全部数据的职责划分从“有标注/无标注”到“强标注/弱标注/伪标签”3.1 课程设计数据里哪些算有标注哪些算无标注标题里的“全部数据”很容易让人误解为数据越多越好直接全部塞进训练集。实际做法是把整个数据集拆成三个用途不同的部分强标注集、无标注池、验证集。强标注集是课程设计提供或者自己标注的真实标签通常只有几十到几百张无标注池是其余所有图片它们没有标签文件但像素信息是可以参与训练的验证集必须始终是真实标注否则无法客观衡量模型性能。这里有一个关键判断如果课程设计给了你全部图片的标签那么“无标注数据”实际上可以通过随机遮挡、复制粘贴等方式人为构造。但更常见的情况是只给部分标签。我建议把有标签数据按7:2:1或者6:2:2划分较小的那份留作测试集中间那份做验证大头做训练完全没有标签的图片全部归入无标注池。3.2 数据划分脚本训练集、验证集、无标注池一次切好import os import random import shutil random.seed(42) data_root raw_dataset train_img dataset/images/train train_lbl dataset/labels/train val_img dataset/images/val val_lbl dataset/labels/val unlabeled_img dataset/images/unlabeled unlabeled_lbl dataset/labels/unlabeled os.makedirs(train_img, exist_okTrue) os.makedirs(train_lbl, exist_okTrue) os.makedirs(val_img, exist_okTrue) os.makedirs(val_lbl, exist_okTrue) os.makedirs(unlabeled_img, exist_okTrue) os.makedirs(unlabeled_lbl, exist_okTrue) all_images [f for f in os.listdir(os.path.join(data_root, images)) if f.endswith((.jpg, .png, .jpeg))] random.shuffle(all_images) labeled [] unlabeled [] for img in all_images: label_path os.path.join(data_root, labels, img.replace(.jpg, .txt).replace(.png, .txt)) if os.path.exists(label_path) and os.path.getsize(label_path) 0: labeled.append(img) else: unlabeled.append(img) val_split int(len(labeled) * 0.2) for img in labeled[:val_split]: shutil.copy(os.path.join(data_root, images, img), os.path.join(val_img, img)) label_name os.path.splitext(img)[0] .txt shutil.copy(os.path.join(data_root, labels, label_name), os.path.join(val_lbl, label_name)) for img in labeled[val_split:]: shutil.copy(os.path.join(data_root, images, img), os.path.join(train_img, img)) label_name os.path.splitext(img)[0] .txt shutil.copy(os.path.join(data_root, labels, label_name), os.path.join(train_lbl, label_name)) for img in unlabeled: shutil.copy(os.path.join(data_root, images, img), os.path.join(unlabeled_img, img))这段脚本的核心逻辑是先扫描所有图片有对应非空标签的进入标注集无标签或标签文件为空的进入无标注池。标注集内部再做一次划分20%作为验证集。注意最后一行无标注图片不需要复制标签到目标目录直接留空即可后面生成伪标签时会自动填写。3.3 为什么“空标签”是一种数据噪声检查标签文件时经常遇到两类情况一是标签文件存在但内容为空二是图片里目标极小导致标签坐标值异常。空标签文件在YOLOv7训练中就是一张负样本图片如果无标注池里大量存在这种图片教师模型会给它们预测出一些置信度很低的框伪标签生成时会因为不满足阈值而被过滤掉最终这些图片在训练中充当的只是背景样本参与学习的信号有限。但如果无标注池里有一张图片确实包含目标教师模型也给出了高置信度框那么这张图片的伪标签就提供了额外的监督信号。这正是半监督学习的核心收益来源扩充训练图片数量同时通过伪标签增加正样本数量让模型在小样本条件下看到更多目标形态差异。因此在划分数据时我会额外检查如果无标注池里大部分图片的尺寸、光照、目标尺度都和训练集差异很大先做一次简单的分布可视化避免伪标签生成后类别分布严重失衡。4. 伪标签生成与可靠性筛选YOLOv7推理输出如何变成有效训练数据4.1 从YOLOv7的txt输出到YOLO格式标签的转换YOLOv7的detect.py默认输出格式是class_id confidence x1 y1 x2 y2这个格式不能直接当训练标签用。训练标签需要的是归一化的中心点坐标和宽高并且不需要置信度那一列。写一个转换脚本让伪标签生成过程完全自动化import os import glob import numpy as np from pathlib import Path def yolo_txt_to_label(txt_path, img_w, img_h, conf_threshold0.5): valid_lines [] with open(txt_path, r) as f: for line in f.readlines(): parts line.strip().split() if len(parts) ! 6: continue cls_id, conf, x1, y1, x2, y2 map(float, parts) if conf conf_threshold: continue x_center ((x1 x2) / 2) / img_w y_center ((y1 y2) / 2) / img_h width (x2 - x1) / img_w height (y2 - y1) / img_h if width 0 or height 0: continue if x_center 0 or x_center 1 or y_center 0 or y_center 1: continue valid_lines.append(f{int(cls_id)} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) return valid_lines def generate_pseudo_labels(det_txt_dir, images_dir, output_label_dir, conf_threshold0.5): os.makedirs(output_label_dir, exist_okTrue) for det_txt in glob.glob(os.path.join(det_txt_dir, *.txt)): image_name Path(det_txt).stem .jpg image_path os.path.join(images_dir, image_name) if not os.path.exists(image_path): image_path os.path.join(images_dir, Path(det_txt).stem .png) if not os.path.exists(image_path): continue from PIL import Image img Image.open(image_path) img_w, img_h img.size labels yolo_txt_to_label(det_txt, img_w, img_h, conf_threshold) out_path os.path.join(output_label_dir, Path(det_txt).stem .txt) with open(out_path, w) as f: if labels: f.write(\n.join(labels) \n)这段脚本的关键在conf_threshold参数它直接决定伪标签的保真度。阈值太高则生成的伪标签数量太少扩充效果有限阈值太低则大量错检框混入训练集模型会被噪声误导。课程设计场景下我一般从0.5开始如果发现加入伪标签后验证集精度不升反降就提高到0.6或0.7再观察变化。4.2 置信度阈值之外的三个筛选维度只靠置信度阈值筛选伪标签是远远不够的。置信度高只能说明教师模型对某个框有把握不能说明这个框的位置和大小标注是准确的。我在实践中通常还会加三个筛选条件。第一是类别特定阈值。不同类别的检测难度差异很大如果统一用0.5筛选类别A的伪标签可能已经非常干净而类别B的伪标签里混入了大量误检。更好的做法是按类别分别统计置信度分布取每个类别的P90分位数作为该类的阈值。这种做法在课程设计的答辩中也非常好讲因为它体现了对数据分布的实际理解。第二是尺度筛查。小目标的置信度天然偏低如果只按全局阈值过滤小目标伪标签几乎会被全部丢弃。解决办法是将检测框面积小于图片面积0.1%的伪标签单独放宽阈值0.1到0.15保证少样本类别不至于被误伤。第三是空标签约束。如果无标注图片经过推理后一个框都没有检测出来它的标签文件为空。这类图片不应该被直接丢弃它们相当于负样本可以帮助模型减少误检。但如果空标签图片占比过大说明教师模型在当前无标注数据上表现很差此时需要回头检查无标注数据的分布是否与训练集偏差太大。4.3 伪标签的置信度信息如何参与损失计算这是半监督检测的一个进阶话题。基础做法是直接让伪标签和真实标签一样参与YOLOv7的损失计算进阶做法是给伪标签分配一个低于1.0的权重系数。YOLOv7的loss.py中lobj和lcls是最终的类别和定位损失可以在损失加权部分加入一个伪标签权重参数pseudo_weight 0.5 if is_pseudo_label else 1.0 loss (loss_box * pseudo_weight loss_obj * pseudo_weight loss_cls * pseudo_weight) / batch_size这里is_pseudo_label可以按批次来定也可以按某个batch内伪标签框的数量比例来动态计算。比如从训练集中混入的伪标签图片数量不超过30%则pseudo_weight设为0.7否则降到0.4。这样做的好处是前期让模型主要跟随真实标签学习伪标签只起微调作用避免模型被伪标签中带偏。修改loss.py属于对YOLOv7源码的深度改动答辩时可以作为一个亮点展示。如果时间紧张也可以不修改源码而是通过控制训练集中的伪标签图片比例来控制噪声上限这种做法也能达到接近的效果。5. 多轮自训练迭代教师模型如何更新、伪标签如何再生成5.1 自训练的三轮循环流程半监督训练不是一次生成伪标签、一次训练就结束了。标准做法是多轮迭代第1轮用强标注集训练出教师模型A生成第一批伪标签混合后训练出学生模型B第2轮把B当作新的教师模型重新为无标注池生成伪标签再训练出模型C。每一轮迭代伪标签的质量都会提升因为模型对目标的特征感知更强了。课程设计的时间窗口内我建议最多做3轮迭代。轮次再多边际收益非常小而且容易发生过拟合。具体循环如下# Round 1: 用baseline生成伪标签 python detect.py --weights runs/train/baseline/weights/best.pt --source dataset/images/unlabeled --img 640 --conf-thres 0.5 --save-txt --project runs/pseudo_label --name round1 # 转换伪标签格式并合并到训练集 python scripts/convert_pseudo_labels.py --det_dir runs/pseudo_label/round1/labels --img_dir dataset/images/unlabeled --out_dir dataset/labels/unlabeled --conf_threshold 0.5 # Round 2: 混合真实标签和伪标签训练 python train.py --data dataset/dataset_half.yaml --weights runs/train/baseline/weights/best.pt --batch-size 16 --epochs 80 --img 640 --device 0 --project runs/train --name round2dataset_half.yaml里需要把train指向一个合并后的目录里面既包含原训练集图片也包含无标注池图片同时对应的标签目录里既有真实标注也有伪标签生成的文件。第二轮训练的初始权重建议继续用第一轮的best.pt而非COCO预训练权重因为模型已经适应当前数据分布重新从COCO预训练权重起步会丢失掉第一轮学到的域内特征。第二轮生成的伪标签会比第一轮更可靠。按经验第二轮伪标签中置信度大于0.7的比例会比第一轮高5到10个百分点这说明模型对无标注数据的理解在加深。第三轮训练时可以稍微提高conf_threshold只保留最确信的伪标签防止噪声累积。5.2 伪标签混合比例的控制方法训练集里真实标签和伪标签的比例不是越均衡越好。如果伪标签数量远超真实标签模型会被无标注数据主导失去对真实标签分布的拟合能力。常见的比例控制做法有两种。第一种是直接在文件层面控制。将伪标签图片随机抽样使伪标签图片数量不超过真实标签图片数量的1.5倍。例如真实标签有200张图片则从无标注池中随机抽取不超过300张图片的伪标签加入训练集。第二种是动态采样控制。修改YOLOv7的datasets.py中自定义Dataset类的__getitem__方法让它在每个epoch内动态决定当前样本来自真实标注集还是伪标签集class MixDataset(Dataset): def __init__(self, real_img_dir, pseudo_img_dir, real_label_dir, pseudo_label_dir, pseudo_ratio0.3): self.real_images self._load_img(real_img_dir, real_label_dir) self.pseudo_images self._load_img(pseudo_img_dir, pseudo_label_dir) self.pseudo_ratio pseudo_ratio def __len__(self): return len(self.real_images) // (1 - self.pseudo_ratio) def __getitem__(self, idx): if random.random() self.pseudo_ratio: img_path, label_path random.choice(self.pseudo_images) else: img_path, label_path random.choice(self.real_images) return self._load_sample(img_path, label_path)pseudo_ratio建议控制在0.2到0.4之间。课程设计答辩时可以说采用0.3的伪标签混合比例既保证了扩充效果又防止噪声干扰因为真实标注的信号强度必须高于伪标签的信号强度。5.3 迭代轮次间如何对比精度变化每一轮迭代结束后需要在同一个验证集上评估模型。评估时统一使用val.txt对应的真实标注数据不要用伪标签参与评估否则结果没有参考意义。评估命令如下python val.py --data dataset/dataset.yaml --weights runs/train/round2/weights/best.pt --batch-size 16 --img 640 --task val记录每一轮的mAP0.5和mAP0.5:0.95。一个正常的多轮自训练过程精度曲线大致是Round1略高于baselineRound2比Round1高1到3个百分点Round3基本持平或小幅波动。如果Round2比Round1低了说明伪标签噪声已经拖累模型应该回退到Round1的策略降低pseudo_ratio或提高conf_threshold。还有一种情况是验证集精度不变但训练损失下降明显。这说明模型在训练集上过拟合了常见原因是伪标签加入后训练图片数量不变去重没做好或者学习率没有调整。第二轮训练的初始lr建议从第一轮最终lr的十分之一开始比如第一轮cosine衰减结束时lr为0.0001则第二轮设为0.00001。5.4 半监督训练中典型的loss震荡处理伪标签加入后loss曲线经常出现震荡原因是部分伪标签包含不准确的边界框它们在训练初期对梯度的影响较大。解决方式有两种倾向一种是提高伪标签的权重让它帮助模型快速扩展拟合空间适合伪标签质量高的情况另一种是降低伪标签的权重让模型先稳定适合伪标签噪声大的时候。实践经验是train loss和val loss同时震荡时优先降低学习率train loss下降但val loss震荡时优先降低pseudo_ratiotrain loss和val loss都平稳但精度不涨时检查无标注池里是否还有大量未加入训练的数据。如果震荡特别剧烈可以在训练脚本中使用梯度裁剪python train.py --data dataset/dataset_half.yaml --weights runs/train/round1/weights/best.pt --batch-size 16 --epochs 80 --img 640 --device 0 --project runs/train --name round2_fixed --clip-grad 10.0YOLOv7的train.py如果没有暴露--clip-grad参数可以直接在源码的backward()之后加一行torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm10.0)。课程设计答辩里提到这个细节能直接证明你排查过训练不稳定的问题。6. 最终验证与一个比“调高阈值”更有效的伪标签清洗技巧三轮训练结束后除了在测试集上报告mAP还需要做两件事。第一件事是可视化检测结果把detect.py输出的图片整理成网格图覆盖不同光照、不同尺度、不同类别的场景直观展示半监督学习的增益第二件事是统计伪标签的分布变化对比三个轮次生成的伪标签在置信度、目标尺度上的差异这个数据可以直接写进课程设计报告的实验分析部分。python detect.py --weights runs/train/round2/weights/best.pt --source dataset/images/val --img 640 --conf-thres 0.25 --project runs/detect --name final--conf-thres在推理阶段设置0.25即可不需要和训练阶段保持一致。检测结果里频发的漏检位置如果恰好是无标注池中数量较多的场景说明半监督往这个方向扩充得还不够下一轮可以针对性增加该分布的样本比例。最后分享一个比单纯调高阈值更有效的伪标签清洗技巧。伪标签的噪声集中体现在两个位置高度重叠的低置信度框和类别置信度接近的框。前者会让模型对同一目标学习到互相矛盾的标签后者会导致类别混淆。我的做法是在生成伪标签之后额外做一次类别感知的NMS。YOLOv7的detect.py默认已经做了NMS但它是按置信度排序后抑制IoU超过阈值的框不同类别的框即使高度重叠也不会相互抑制。在多类别半监督场景中如果教师模型在一张图上同时预测出“人”和“背包”两个高IoU框它们的语义关系在NMS中不会得到处理。使用类别感知NMS可以有效缓解这种噪声def class_aware_nms(predictions, iou_threshold0.5): # predictions: list of [cls_id, conf, x1, y1, x2, y2] predictions.sort(keylambda x: x[1], reverseTrue) keep [] for pred in predictions: flag True for kept in keep: # 只计算相同类别的IoU if pred[0] ! kept[0]: continue iou compute_iou(pred[2:], kept[2:]) if iou iou_threshold: flag False break if flag: keep.append(pred) return keepclass_aware_nms的执行位置放在转换脚本之前也就是拿到detect.py输出的txt之后、生成训练标签之前。实际使用中它能把伪标签中“一个目标被预测成两类”的噪声样本减少约15%效果比简单把阈值从0.5提到0.6更加精准因为它过滤的是空间冲突而非单纯的低置信度。等这套清洗流程跑完用验证集做最后一次评估记录最终的mAP、每类AP、以及预训练权重和自训练权重的对比曲线课程设计报告需要的核心数据就都齐了。本文还有配套的精品资源点击获取