【深度学习实验】—— YOLOv12 水果目标检测实战:从数据集制作、模型训练到视频推理
文章目录
- 1. 简介
- 2. 环境
- 3. 代码实现
- 3.1 前期准备
- 3.1.1 项目结构与导入库
- 3.1.2 数据集划分
- 3.1.3 VOC 标注转换为 YOLO 格式
- 3.1.4 数据配置与标签可视化
- 3.2 模型建立与训练
- 3.2.1 YOLOv12n 模型结构
- 3.2.2 创建模型与训练入口
- 3.2.3 训练参数与损失函数
- 3.2.4 训练记录与模型保存
- 3.3 视频推理实现
- 4. 模型评估
- 4.1 可视化训练过程
- 4.2 最优模型的验证指标
- 4.3 PR 曲线与混淆矩阵
- 4.3.1 PR 曲线
- 4.3.2 混淆矩阵
- 4.4 验证图像预测效果
- 4.5 视频输出与适用范围
- 5. 总结
1. 简介
本次实验使用YOLOv12n完成四类水果目标检测,串联数据集划分、VOC 标注转换、模型训练、验证和视频推理流程。
目标检测需要同时回答“图像中有什么”和“目标在哪里”。因此,模型既要预测水果类别,也要输出对应的边界框。一张图像可以包含多个目标,评价时需要同时考虑分类和定位质量。
| 项目 | 内容 |
|---|---|
| 模型 | YOLOv12n,训练入口为YOLO("yolo12n.yaml") |
| 任务 | 四类水果目标检测 |
| 类别 | pineapple(菠萝)、snake fruit(蛇皮果)、dragon fruit(火龙果)、banana(香蕉) |
| 数据集 | 200 张图像,594 个标注目标 |
| 数据划分 | 训练集 160 张、验证集 20 张、测试集 20 张 |
| 训练配置 | 2 个 Epoch,batch=2,imgsz=640 |
| 模型参数量 | 2,568,828,约 256.9 万 |
| 已有最优验证指标 | mAP@0.5 为4.216%,mAP@0.5:0.95 为1.562% |
| 视频输出 | 1280×720,25 FPS,共 4,051 帧 |
本次训练只进行了 2 轮,检测指标仍然较低。实验已经完成整个处理流程,但模型还需要进一步训练和验证,才能判断其实际应用价值。
2. 环境
项目使用 Python 脚本运行。当前项目所在虚拟环境为../.venv312,复核到的主要版本如下:
| 环境项 | 版本或配置 |
|---|---|
| 操作系统 | macOS,arm64 |
| Python | 3.12.13 |
| PyTorch | 2.14.0 |
| torchvision | 0.29.0 |
| Ultralytics | 8.4.182 |
| OpenCV | 5.0.0.93 |
| NumPy | 2.5.2 |
| PyYAML | 6.0.3 |
训练权重内部记录的 Ultralytics 版本也是8.4.182。当前from ultralytics import YOLO导入的是虚拟环境中的已安装包。
在项目根目录中,可以使用现有虚拟环境运行脚本:
../.venv312/bin/python train.py../.venv312/bin/python test.py以上是运行入口说明,重新执行训练会生成新的训练目录,本文分析的是已存在的runs/detect/train-2/结果。
3. 代码实现
3.1 前期准备
3.1.1 项目结构与导入库
项目的主要文件如下:
Yolo/ ├── data/ │ ├── images/ # 原始图像 │ ├── annotations/ # VOC XML 标注 │ ├── labels/ # 转换后的 YOLO 标注 │ ├── ImageSets/Main/ # 各数据子集的图像编号 │ ├── split_train_val.py # 划分训练、验证、测试集 │ ├── voc_label.py # 标注转换与图像清单生成 │ ├── ab.yaml # 数据集配置 │ ├── train.txt │ ├── val.txt │ └── test.txt ├── yolov12/ # 附带的模型源码 ├── train.py # 训练入口 ├── test.py # 视频推理入口 ├── yolo12n.pt # 另存的 80 类权重 ├── TestVideo.mp4 ├── TestVideo_result.mp4 └── runs/detect/train-2/ ├── args.yaml ├── results.csv ├── results.png ├── BoxPR_curve.png ├── confusion_matrix.png └── weights/ ├── best.pt └── last.pt训练主要使用ultralytics.YOLO;视频推理还使用cv2读写视频,并使用argparse、pathlib.Path管理参数和路径。
可以通过以下代码检查当前解释器和实际导入的库:
importsysimporttorchimportultralyticsfromultralyticsimportYOLOprint("Python:",sys.version)print("Ultralytics:",ultralytics.__version__)print("导入位置:",ultralytics.__file__)print("CUDA 可用:",torch.cuda.is_available())print("MPS 可用:",torch.backends.mps.is_available())3.1.2 数据集划分
原始数据集包含 200 张 PNG 图像,每张图像对应一个 XML 标注文件。split_train_val.py先将 90% 的样本划入训练与验证集合,再从中抽取 8/9 作为训练集。
核心代码如下,完整实现见data/split_train_val.py:
trainval_percent=0.9train_percent=8/9num=len(total_xml)list_index=range(num)tv=int(num*trainval_percent)tr=int(tv*train_percent)trainval=random.sample(list_index,tv)train=random.sample(trainval,tr)因此,实际得到80% 训练集、10% 验证集、10% 测试集。
对现有划分文件和 YOLO 标签进行统计,结果如下:
| 数据子集 | 图像数 | 菠萝 | 蛇皮果 | 火龙果 | 香蕉 | 目标总数 |
|---|---|---|---|---|---|---|
| 训练集 | 160 | 132 | 157 | 78 | 109 | 476 |
| 验证集 | 20 | 17 | 20 | 11 | 10 | 58 |
| 测试集 | 20 | 14 | 20 | 15 | 11 | 60 |
| 合计 | 200 | 163 | 197 | 104 | 130 | 594 |
三个子集的图像编号没有交集。蛇皮果的标注数量最多,火龙果最少,说明各类别的目标数量并不完全均衡。
这里还需要区分两处随机性:划分脚本使用random.sample(),但没有设置随机种子;训练参数中则记录了seed=0。训练种子不会自动固定另一个脚本的数据划分结果,所以复现本次实验时应保留现有清单文件。
3.1.3 VOC 标注转换为 YOLO 格式
原始 XML 使用xmin、ymin、xmax、ymax描述边界框。YOLO 标签的每一行格式为:
class_id x_center y_center width height其中class_id从 0 开始,其余四个值按图像宽高归一化。
项目中的转换函数如下,保留了原脚本的函数名convent:
defconvent(size,box):dw=1.0/size[0]dh=1.0/size[1]x=(box[0]+box[1])/2.0-1y=(box[2]+box[3])/2.0-1w=box[1]-box[0]h=box[3]-box[2]returnx*dw,y*dh,w*dw,h*dh注意,该函数中box的顺序为(xmin, xmax, ymin, ymax)。中心坐标中的-1是原实现保留的坐标偏移,本文按项目代码记录;迁移其他标注源时,应核对其坐标约定。
转换脚本还会跳过不在类别列表中的对象和difficult=1的对象,并将超出图像右边界、下边界的xmax、ymax截断到图像尺寸。
例如,fruit0.txt包含三个目标:
0 0.38375000000000004 0.5116666666666667 0.5825 0.48333333333333334 1 0.6525 0.6316666666666667 0.09 0.11 2 0.75375 0.8766666666666667 0.2425 0.24000000000000002它们依次表示菠萝、蛇皮果和火龙果。类别顺序必须与ab.yaml完全一致。
准备新数据时,两个脚本的执行顺序为:
../.venv312/bin/python data/split_train_val.py../.venv312/bin/python data/voc_label.py第一步会重新划分数据并覆盖编号清单;第二步按照清单转换标签,并生成data/train.txt、data/val.txt和data/test.txt。
3.1.4 数据配置与标签可视化
本次使用的data/ab.yaml中,有效配置为:
train:./train.txtval:./val.txtnc:4names:["pineapple","snake fruit","dragon fruit","banana"]train和val指定图像清单,nc指定类别数量,names给出类别编号对应的名称。例如,train.txt中的路径形如./images/fruit0.png。
项目虽然生成了测试集清单,但当前 YAML 没有启用test字段,因此已有训练结果对应验证集,不能写成独立测试集成绩。
训练过程保存了标签分布图,可以用来观察类别数量以及边界框的位置、尺寸分布:
3.2 模型建立与训练
3.2.1 YOLOv12n 模型结构
本项目通过模型 YAML 创建网络,再由训练器根据数据配置将检测类别设置为 4。
从训练后best.pt保存的结构可以看到,网络包含卷积、C3k2、A2C2f、上采样、特征拼接和Detect检测头。可以按功能理解为以下几个部分:
| 组成部分 | 作用 |
|---|---|
| Conv、C3k2 | 提取图像特征,并逐步降低空间分辨率 |
| A2C2f | 在相应配置下使用区域注意力等特征处理模块,增强特征表达 |
| Upsample、Concat | 融合不同层级的特征信息 |
| Detect | 基于 P3、P4、P5 三个尺度输出目标检测预测 |
对于 640×640 的输入,三个检测尺度通常对应 80×80、40×40 和 20×20 的特征图。多尺度预测让模型能结合不同分辨率的特征处理大小不同的目标。
训练后模型的参数量实测为2,568,828。这个数值来自本次四类模型的权重,而非其他类别数或其他实现版本的模型介绍。
3.2.2 创建模型与训练入口
项目中的train.py如下:
fromultralyticsimportYOLOif__name__=='__main__':model=YOLO("yolo12n.yaml")model.train(data=r"/Users/bareerah/Learn/ML/Yolo/data/ab.yaml",epochs=2,batch=2,workers=2,)这里使用.yaml创建网络结构。脚本没有显式加载根目录下的yolo12n.pt,因此不能因为该文件存在,或者参数中出现pretrained: true,就把本次训练描述成已经加载该预训练权重的迁移学习。
两类调用应分清:
# 本次训练入口:从 YAML 构建网络model=YOLO("yolo12n.yaml")# 另一种训练方式:显式加载本地权重,本文未用它重做训练model=YOLO("yolo12n.pt")根目录下的yolo12n.pt经检查保存了 80 类名称;视频脚本默认使用的runs/detect/train-2/weights/best.pt保存的是本项目的四类名称。二者用途和类别空间不同。
3.2.3 训练参数与损失函数
结合args.yaml和训练权重内部的train_args,可以整理出以下配置:
| 参数 | 本次记录 | 说明 |
|---|---|---|
epochs | 2 | 训练轮次 |
batch | 2 | 每批图像数量 |
imgsz | 640 | 训练输入尺寸 |
workers | 请求值 2,权重内最终记录 0 | 初始参数和运行时参数存在调整 |
optimizer | 初始为 auto,最终为 AdamW | 自动选择优化器 |
lr0 | 初始配置 0.01,最终记录 0.00125 | 自动模式调整后的初始学习率 |
weight_decay | 0.0005 | 权重衰减配置 |
warmup_epochs | 3.0 | 预热轮次配置,长于本次总训练轮次 |
seed | 0 | 训练随机种子 |
val | true | 训练中进行验证 |
本次记录的主要损失项为:
| 损失项 | 含义 | 配置权重 |
|---|---|---|
box_loss | 边界框定位损失 | 7.5 |
cls_loss | 目标类别预测损失 | 0.5 |
dfl_loss | Distribution Focal Loss,辅助边界框回归 | 1.5 |
训练器负责数据加载、前向传播、损失计算、反向传播、参数更新及验证。调用model.train()后,无需在训练入口中再次手写这些步骤。
配置文件还记录了 HSV 颜色扰动、平移、缩放和水平翻转等增强参数。但配置值不等于每一种增强都在本次短训练中实际生效,例如 Mosaic 还受close_mosaic=10的训练阶段控制。本文不据配置文件推断增强效果。
训练批次可视化如下,可检查进入训练流程的图像及边界框:
3.2.4 训练记录与模型保存
完整的两轮指标保存在runs/detect/train-2/results.csv。下表保留 CSV 中的小数值:
| Epoch | 累计时间/s | 训练 box_loss | 训练 cls_loss | 训练 dfl_loss | Precision | Recall | mAP@0.5 | mAP@0.5:0.95 |
|---|---|---|---|---|---|---|---|---|
| 1 | 113.832 | 3.02740 | 4.66800 | 4.56860 | 0 | 0 | 0 | 0 |
| 2 | 168.661 | 2.99215 | 4.48872 | 4.23091 | 0.00085 | 0.09091 | 0.04216 | 0.01562 |
time是累计时间,不能将两行相加作为总训练时间。最后一行记录的累计时间约为168.7 秒,不包含对整个项目所有步骤耗时的统计。
训练结束后保存了:
runs/detect/train-2/weights/best.pt runs/detect/train-2/weights/last.pt第 2 轮具有本次最高的验证 mAP@0.5:0.95;best.pt和last.pt内部保存的训练指标一致。这里的“最优”只表示本次两轮训练中的相对最优。
3.3 视频推理实现
项目中的test.py是视频推理脚本,不会读取data/test.txt计算测试集指标。
脚本通过命令行指定权重、输入视频和输出视频,默认值分别为:
parser.add_argument('--weights',type=Path,default=root/'runs/detect/train-2/weights/best.pt')parser.add_argument('--source',type=Path,default=root/'TestVideo.mp4')parser.add_argument('--output',type=Path,default=root/'TestVideo_result.mp4')读取视频后,获取原始尺寸和帧率,并使用mp4v编码创建输出文件:
model=YOLO(str(args.weights))cap=cv2.VideoCapture(str(args.source))width=int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))height=int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))fps=cap.get(cv2.CAP_PROP_FPS)out=cv2.VideoWriter(str(args.output),cv2.VideoWriter_fourcc(*'mp4v'),fps,(width,height),)核心处理循环如下:
frames=0whileTrue:success,frame=cap.read()ifnotsuccess:breakresults=model(frame)out.write(results[0].plot())frames+=1model(frame)对一帧图像执行检测,plot()将返回的检测框、类别和置信度绘制到图像上,再由out.write()写入输出视频。没有通过筛选的检测结果时,画面不会出现检测框。
完整脚本还检查输入文件、视频尺寸、帧率和写入器状态,防止输出路径覆盖输入视频或权重,并在finally中释放视频资源。
如果已有输出文件需要保留,可以指定新的输出路径:
../.venv312/bin/python test.py\--weightsruns/detect/train-2/weights/best.pt\--sourceTestVideo.mp4\--outputTestVideo_result_new.mp44. 模型评估
4.1 可视化训练过程
框架保存的训练曲线如下:
从记录中可以看到,三项训练损失均有所下降:
box_loss:3.02740 → 2.99215。cls_loss:4.66800 → 4.48872。dfl_loss:4.56860 → 4.23091。
验证集损失的变化为:
| 指标 | Epoch 1 | Epoch 2 |
|---|---|---|
| 验证 box_loss | 2.91293 | 2.88124 |
| 验证 cls_loss | 4.58430 | 4.47664 |
| 验证 dfl_loss | 4.16822 | 4.17233 |
训练损失下降说明参数更新开始产生作用,但两轮数据不足以判断模型已经收敛,也不足以认定过拟合。结合很低的检测指标,更合理的判断是:模型仍处于训练早期,尚未学到稳定的水果检测能力。
4.2 最优模型的验证指标
best.pt内部保存的验证指标与 CSV 第 2 轮一致:
| 指标 | 原始数值 | 百分比 |
|---|---|---|
| Precision | 0.00085 | 0.085% |
| Recall | 0.09091 | 9.091% |
| mAP@0.5 | 0.04216 | 4.216% |
| mAP@0.5:0.95 | 0.01562 | 1.562% |
这些指标含义不同:
- Precision:预测为目标的结果中,有多少是正确检测。
- Recall:真实目标中,有多少被正确检测出来。
- mAP@0.5:以 IoU=0.5 判定定位匹配,计算各类别 AP 后取平均。
- mAP@0.5:0.95:在 IoU 从 0.5 到 0.95、步长 0.05 的多个阈值下计算平均结果,对定位精度要求更高。
框架汇总的 Precision、Recall 具有类别平均和置信度选点等统计口径,不能简单将它们视为整套数据的单个计数比例。尤其是mAP 不能称为分类准确率,4.216% 也不表示“识别正确了 4.216% 的图片”。
如需重新评估最优权重,可以使用以下代码;本文表格引用的是已有记录,没有将该示例作为一次新评测:
fromultralyticsimportYOLO model=YOLO("runs/detect/train-2/weights/best.pt")metrics=model.val(data="data/ab.yaml",split="val")print(f"Precision:{metrics.box.mp:.6f}")print(f"Recall:{metrics.box.mr:.6f}")print(f"mAP@0.5:{metrics.box.map50:.6f}")print(f"mAP@0.5:0.95:{metrics.box.map:.6f}")若要评估独立测试集,需要先在数据配置中加入test: ./test.txt,再调用split="test"。当前项目没有保存可引用的独立测试集评测成绩。
4.3 PR 曲线与混淆矩阵
4.3.1 PR 曲线
图例中的 AP@0.5 显示值为:
| 类别 | AP@0.5,按图例四舍五入 |
|---|---|
| pineapple | 0.000 |
| snake fruit | 0.000 |
| dragon fruit | 0.169 |
| banana | 0.000 |
| all classes | 0.042 |
当前有效的 AP 主要来自火龙果类别,其余三类尚未形成有效的检测表现。火龙果的 PR 曲线也仅覆盖较低的召回率范围,模型仍会漏掉较多目标。
4.3.2 混淆矩阵
图中横轴为真实类别,纵轴为预测类别。在这张图采用的匹配和筛选条件下:
- 火龙果有 4 个正确匹配,另有 2 个被预测为香蕉。
- 菠萝有 1 个被预测为蛇皮果。
background行记录了大量未匹配到预测框的真实目标,例如 16 个菠萝、20 个蛇皮果、5 个火龙果和 10 个香蕉。background列中存在大量未匹配到真实目标的预测框,四类分别为 1,153、1,143、1,178 和 1,166 个,说明该统计条件下误检很多。
目标检测中的background表示未匹配的真实目标或预测框,并不是数据集新增的第五类。混淆矩阵与 PR/AP 的阈值、匹配及汇总方式也有差异,不能直接用矩阵计数替代前面的指标。
4.4 验证图像预测效果
下面两张图来自同一个验证批次,可对照真实标注和预测输出:
真实标注中包含水果边界框,而该预测批次未显示有效检测框。这与较低的验证指标一致。
这也说明,定量指标和可视化需要结合起来看:损失下降描述训练过程,mAP 衡量检测质量,预测图则能直接揭示漏检等具体问题。
4.5 视频输出与适用范围
对已有输入、输出视频进行元数据检查,结果如下:
| 项目 | 输入视频 | 输出视频 |
|---|---|---|
| 文件 | TestVideo.mp4 | TestVideo_result.mp4 |
| 分辨率 | 1280×720 | 1280×720 |
| 帧率 | 25 FPS | 25 FPS |
| 视频帧数 | 4,051 | 4,051 |
| 视频流时长 | 162.04 秒 | 162.04 秒 |
| 视频编码 | H.264 | MPEG-4 Part 2(mp4v) |
| 音频流 | 有,AAC | 无 |
输入文件的容器总时长为 162.10 秒,与音频流有关;上表对齐比较的是视频流时长。输出帧数、尺寸和帧率与输入一致,支持视频读取和写出流程已完整执行的判断。脚本只写入图像帧,因此输出没有保留原音频。
从已有输出中抽取第 0 秒、第 60 秒和第 120 秒的画面如下:
这三处抽样画面主要是人物场景,没有显示检测框。当前模型的类别是四种水果,人物并不属于其检测范围,因此不能要求该模型输出人物框,也不能用这些画面评价水果识别能力。
现有视频结果适合说明视频处理流程可以运行。要验证水果检测效果,还需要包含对应水果、并具有人工标注或可核对目标的视频。
另外,输出为 25 FPS 只表示文件的播放帧率。项目没有保存完整视频推理耗时,不能据此认定模型能以 25 FPS 实时处理视频。
5. 总结
本次实验完成了从原始 XML 标注到视频推理输出的完整流程,也暴露出当前训练配置和评估素材的局限。
完成四类水果检测的数据准备。200 张图像包含 594 个目标,按 160/20/20 划分为训练、验证和测试集,并完成 VOC 到 YOLO 的标注转换。
完成模型训练与结果保存。本次四类 YOLOv12n 模型有 2,568,828 个参数;训练两轮后,三项训练损失均下降,保存了权重、指标、训练曲线和验证图像。
现有检测能力仍然有限。最优验证 mAP@0.5 为 4.216%,mAP@0.5:0.95 为 1.562%。PR 曲线、混淆矩阵和预测图共同表明,模型仍存在大量漏检和误检。
视频输出验证了工程流程。已有输出保留全部 4,051 帧,但抽样人物画面与水果任务不匹配;输出文件也没有保留原音频,且播放帧率不能代替推理速度测试。
后续应优先完善训练与评估。可以显式加载本地预训练权重,增加训练轮次并观察验证指标;固定数据划分,对相近拍摄场景进一步检查,必要时按拍摄批次分组;完成独立测试集评测,再使用水果视频检验实际效果。
例如,下一轮可以尝试以下训练配置。它是后续实验方案,尚无对应结果:
frompathlibimportPathfromultralyticsimportYOLOif__name__=="__main__":root=Path(__file__).resolve().parent model=YOLO(str(root/"yolo12n.pt"))model.train(data=str(root/"data/ab.yaml"),epochs=100,batch=2,imgsz=640,workers=2,patience=20,seed=0,project=str(root/"runs/detect"),name="fruit_pretrained",)增加轮次和加载权重能提供进一步学习的条件,最终效果仍需由新的验证和测试结果确认。通过这次实验,可以建立起目标检测项目的基本工作流程,并学会结合代码、权重、数值指标和可视化判断模型实际完成到了哪一步。