ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

【深度学习实验】—— YOLOv12 水果目标检测实战:从数据集制作、模型训练到视频推理

【深度学习实验】—— 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
Python3.12.13
PyTorch2.14.0
torchvision0.29.0
Ultralytics8.4.182
OpenCV5.0.0.93
NumPy2.5.2
PyYAML6.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 标签进行统计,结果如下:

数据子集图像数菠萝蛇皮果火龙果香蕉目标总数
训练集16013215778109476
验证集201720111058
测试集201420151160
合计200163197104130594

三个子集的图像编号没有交集。蛇皮果的标注数量最多,火龙果最少,说明各类别的目标数量并不完全均衡。

这里还需要区分两处随机性:划分脚本使用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,可以整理出以下配置:

参数本次记录说明
epochs2训练轮次
batch2每批图像数量
imgsz640训练输入尺寸
workers请求值 2,权重内最终记录 0初始参数和运行时参数存在调整
optimizer初始为 auto,最终为 AdamW自动选择优化器
lr0初始配置 0.01,最终记录 0.00125自动模式调整后的初始学习率
weight_decay0.0005权重衰减配置
warmup_epochs3.0预热轮次配置,长于本次总训练轮次
seed0训练随机种子
valtrue训练中进行验证

本次记录的主要损失项为:

损失项含义配置权重
box_loss边界框定位损失7.5
cls_loss目标类别预测损失0.5
dfl_lossDistribution 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_lossPrecisionRecallmAP@0.5mAP@0.5:0.95
1113.8323.027404.668004.568600000
2168.6612.992154.488724.230910.000850.090910.042160.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+=1

model(frame)对一帧图像执行检测,plot()将返回的检测框、类别和置信度绘制到图像上,再由out.write()写入输出视频。没有通过筛选的检测结果时,画面不会出现检测框。

完整脚本还检查输入文件、视频尺寸、帧率和写入器状态,防止输出路径覆盖输入视频或权重,并在finally中释放视频资源。

如果已有输出文件需要保留,可以指定新的输出路径:

../.venv312/bin/python test.py\--weightsruns/detect/train-2/weights/best.pt\--sourceTestVideo.mp4\--outputTestVideo_result_new.mp4

4. 模型评估

4.1 可视化训练过程

框架保存的训练曲线如下:

从记录中可以看到,三项训练损失均有所下降:

  • box_loss:3.02740 → 2.99215。
  • cls_loss:4.66800 → 4.48872。
  • dfl_loss:4.56860 → 4.23091。

验证集损失的变化为:

指标Epoch 1Epoch 2
验证 box_loss2.912932.88124
验证 cls_loss4.584304.47664
验证 dfl_loss4.168224.17233

训练损失下降说明参数更新开始产生作用,但两轮数据不足以判断模型已经收敛,也不足以认定过拟合。结合很低的检测指标,更合理的判断是:模型仍处于训练早期,尚未学到稳定的水果检测能力。

4.2 最优模型的验证指标

best.pt内部保存的验证指标与 CSV 第 2 轮一致:

指标原始数值百分比
Precision0.000850.085%
Recall0.090919.091%
mAP@0.50.042164.216%
mAP@0.5:0.950.015621.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,按图例四舍五入
pineapple0.000
snake fruit0.000
dragon fruit0.169
banana0.000
all classes0.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.mp4TestVideo_result.mp4
分辨率1280×7201280×720
帧率25 FPS25 FPS
视频帧数4,0514,051
视频流时长162.04 秒162.04 秒
视频编码H.264MPEG-4 Part 2(mp4v)
音频流有,AAC无

输入文件的容器总时长为 162.10 秒,与音频流有关;上表对齐比较的是视频流时长。输出帧数、尺寸和帧率与输入一致,支持视频读取和写出流程已完整执行的判断。脚本只写入图像帧,因此输出没有保留原音频。

从已有输出中抽取第 0 秒、第 60 秒和第 120 秒的画面如下:



这三处抽样画面主要是人物场景,没有显示检测框。当前模型的类别是四种水果,人物并不属于其检测范围,因此不能要求该模型输出人物框,也不能用这些画面评价水果识别能力。

现有视频结果适合说明视频处理流程可以运行。要验证水果检测效果,还需要包含对应水果、并具有人工标注或可核对目标的视频。

另外,输出为 25 FPS 只表示文件的播放帧率。项目没有保存完整视频推理耗时,不能据此认定模型能以 25 FPS 实时处理视频。

5. 总结

本次实验完成了从原始 XML 标注到视频推理输出的完整流程,也暴露出当前训练配置和评估素材的局限。

  1. 完成四类水果检测的数据准备。200 张图像包含 594 个目标,按 160/20/20 划分为训练、验证和测试集,并完成 VOC 到 YOLO 的标注转换。

  2. 完成模型训练与结果保存。本次四类 YOLOv12n 模型有 2,568,828 个参数;训练两轮后,三项训练损失均下降,保存了权重、指标、训练曲线和验证图像。

  3. 现有检测能力仍然有限。最优验证 mAP@0.5 为 4.216%,mAP@0.5:0.95 为 1.562%。PR 曲线、混淆矩阵和预测图共同表明,模型仍存在大量漏检和误检。

  4. 视频输出验证了工程流程。已有输出保留全部 4,051 帧,但抽样人物画面与水果任务不匹配;输出文件也没有保留原音频,且播放帧率不能代替推理速度测试。

  5. 后续应优先完善训练与评估。可以显式加载本地预训练权重,增加训练轮次并观察验证指标;固定数据划分,对相近拍摄场景进一步检查,必要时按拍摄批次分组;完成独立测试集评测,再使用水果视频检验实际效果。

例如,下一轮可以尝试以下训练配置。它是后续实验方案,尚无对应结果:

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",)

增加轮次和加载权重能提供进一步学习的条件,最终效果仍需由新的验证和测试结果确认。通过这次实验,可以建立起目标检测项目的基本工作流程,并学会结合代码、权重、数值指标和可视化判断模型实际完成到了哪一步。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进