ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

玩手机检测YOLOv8实战:593张图训练全流程与避坑指南

2026/10/5 3:49:43 拓冰建站 浏览量
玩手机检测YOLOv8实战:593张图训练全流程与避坑指南 简介这是一份面向目标检测入门与实战的YOLO系列算法数据集聚焦“手机、玩手机、打电话”三类目标识别场景适用于yolov5、yolov7、yolov8、yolov9、yolov10、yolo11等主流版本可直接用于模型训练、验证与测试。数据集已按常规比例划分并提供yolo格式与voc格式两套标签分别保存在不同文件夹中。yolo标签采用“类别索引、归一化中心坐标、宽高比例”的txt文件记录坐标值均位于0到1之间使用起来直观便捷voc格式的xml文件则便于在标注工具或传统流程中查看与转换。压缩包共1780个文件包含593张jpg图像、593个txt标签、593个xml标签及1个yaml配置文件整体大小约20.46MB轻量易下载适合快速跑通算法流程或进行多版本对比实验。目前已有191人学习下载可用于课堂案例、毕业设计或工程预研省去自行采集、标注与整理数据的环节拿到后即可开始训练。1. 玩手机/打电话检测数据集为什么 593 张图够启动一个 YOLO 项目做行为检测的人拿到这个数据包时最先注意到的是它把「yolo 算法」「手机 / 玩手机 / 打电话」「593 张图像带标签」三个关键词放在了一起。也就是说解压之后你得到的不是一堆零散的照片而是一套已经标好框、可以直接进入训练流程的 YOLO 格式数据集目标只有一类事把“人手里拿着手机”和“人把手机贴在耳边”这两种动作从画面里揪出来。这类需求在实际项目里非常常见课堂里学生低头刷手机、仓库里员工边走边看屏幕、驾驶室司机单手打电话都是要靠检测模型去盯的场景。用公开的 COCO 预训练权重直接跑模型认的是“手机”这个物体不是“玩手机”这个行为所以你需要一份标注粒度正好卡在动作上的数据这份 593 张的压缩包干的就是这个活。很多人会怀疑 593 张图是不是太少。说实话如果目标是做一个要扛住千万级流量的通用检测服务这点数据连热身都不够但如果你的目标是跑通一套行为检测原型、验证算法选型、或者给领导做一个能演示的 Demo593 张带标签图像配合 YOLOv8 的迁移学习已经足够启动项目。刚入门 YOLO 的新手可以把它当练习集把数据配置、训练、验证、导出整条链路跑一遍有经验的工程师则可以用它快速确认“玩手机/打电话”这个检测任务在自己业务场景下的可行性。后面所有章节都围绕这 593 张图展开从解压后的目录结构讲起一路讲到参数怎么设、坑在哪里。2. 解压后的第一步先从目录结构读懂数据集的标注约定2.1 images 与 labelsYOLO 格式的坐标到底怎么存拿到压缩包解压后常见做法是先看顶层目录多数这类数据集的目录结构会分成 images 和 labels 两个目录图像放一边标注放另一边。YOLO 格式的标注不是用一个 JSON 或 XML 文件装下所有框而是一张图像对应一个同名的 .txt 文件比如 IMG_0001.jpg 旁边一定有个 IMG_0001.txt。打开这个 txt里面每行代表一个目标格式固定为五列class_id、x_center、y_center、width、height其中后四列都是相对图像宽高的归一化数值范围在 0 到 1 之间。这里有个容易忽略的点YOLO 的坐标是“中心点 宽高”的归一化表达不是左上角和右下角的像素坐标。比如图像宽 1920、高 1080一个手机框的中心点落在 (960, 540)宽 200、高 400那 txt 里存的就是0 0.5 0.5 0.1042 0.3704。这个设计是为了跟输入尺寸解耦——训练时无论把图缩放到 640 还是 1280标签都不用改。如果你解压后发现标注是 VOC 风格的 XML 文件也就是每个框存的 xmin、ymin、xmax、ymax那需要先做个转换我一般会写一个一次性转换脚本把 XML 里的绝对坐标换算成 YOLO 的归一化中心坐标。import os import xml.etree.ElementTree as ET xml_dir Annotations txt_dir labels os.makedirs(txt_dir, exist_okTrue) class_map {phone: 0, play: 1, call: 2} for xml_file in os.listdir(xml_dir): tree ET.parse(os.path.join(xml_dir, xml_file)) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.findall(object): cls obj.find(name).text if cls not in class_map: continue box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{class_map[cls]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(os.path.join(txt_dir, xml_file.replace(.xml, .txt)), w) as f: f.write(\n.join(lines))这段脚本做了三件事解析 XML 里的图像宽高、读取每个目标的类别和 bndbox 坐标、最后把绝对坐标转成归一化中心坐标写入 txt。转换时最常翻车的是漏了归一化直接把像素值写进 txt训练时 loss 会异常大甚至直接 NaN。另一个翻车点是 class_map 的对应关系XML 里的类别名和你 data.yaml 里的类别序号必须严格一致顺序错了模型不会报错只是把“玩手机”学成了“打电话”。跑完脚本后建议随机抽几张图用后面 2.3 的可视化脚本画框确认而不是直接开训。2.2 用脚本核对 593 张图的标签分布与类别平衡拿到 txt 标签后我习惯先做一次全量统计每张图有几个目标、每个类别出现多少次、哪些图像是空标签。593 张图的量级不大但手工一个个看依然不现实写个小脚本最靠谱。这个脚本要回答三个问题类别 id 的取值是不是从 0 开始连续排列、各类别的样本数量差距是否悬殊、有没有图像完全没有标签。import os from collections import Counter, defaultdict label_dir labels class_counter Counter() per_image_counter defaultdict(int) for f in os.listdir(label_dir): if not f.endswith(.txt): continue path os.path.join(label_dir, f) num_objects 0 with open(path) as fp: for line in fp: line line.strip() if not line: continue parts line.split() cls_id int(parts[0]) class_counter[cls_id] 1 num_objects 1 per_image_counter[num_objects] 1 print(每个类别的目标数量:, dict(class_counter)) print(每张图像的目标数分布:, dict(sorted(per_image_counter.items())))这段代码的逻辑很简单但输出信息量不小。class_counter 告诉你每个类别 id 总共有多少个框如果发现只出现了 0 和 2 而没有 1说明类别 id 有跳号data.yaml 里如果硬写 0、1、2 三个类训练时类别映射就对不上。per_image_counter 统计每张图里目标数量的分布如果大量图像只有 1 个框而少量图像有 5 个以上框说明这个数据集以单人单动作场景为主训练出来的模型在多人场景下可能漏检。593 张图像、每类几百个框的规模做行为检测只能算“看得见”还谈不上“看得稳”后续数据增强必不可少。2.3 画框验证标签和图像真的对得上吗统计标签分布只是第一步标签坐标和图像内容是否真的匹配必须通过可视化确认。这步不能省我见过太多次因为标签坐标的宽高顺序写反、或者归一化时除以了错误的图像尺寸导致训练时 mAP 看起来还行、实际推理时框全部偏移的案例。最简单的做法是用 OpenCV 把 txt 里的坐标画回原图。import cv2 import os import random img_dir images label_dir labels output_dir check_vis os.makedirs(output_dir, exist_okTrue) imgs [f for f in os.listdir(img_dir) if f.endswith(.jpg)] random.seed(42) sample random.sample(imgs, 20) class_names [phone, play, call] colors [(0, 0, 255), (0, 255, 0), (255, 0, 0)] for img_name in sample: img_path os.path.join(img_dir, img_name) label_path os.path.join(label_dir, img_name.replace(.jpg, .txt)) img cv2.imread(img_path) h, w img.shape[:2] if not os.path.exists(label_path): continue with open(label_path) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls_id int(parts[0]) cx, cy, bw, bh map(float, parts[1:]) x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), colors[cls_id], 2) cv2.putText(img, class_names[cls_id], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, colors[cls_id], 2) cv2.imwrite(os.path.join(output_dir, img_name), img)这段脚本随机抽 20 张图把标签框和类别名画上去。需要注意坐标还原时是反归一化中心点坐标乘图像宽、宽高乘图像宽高换算回像素后再算左上角和右下角。跑完后打开 check_vis 目录逐张翻一遍重点看两类问题一是框是不是准确包住了手机和手有没有只框一半或者把脸框进去二是类别是否合理同一个动作是不是被标成了不同类别。画框验证看起来是笨办法但对小数据集来说这一步比任何自动化评估都更能发现问题593 张图多翻几遍也就一顿饭的功夫。3. 用 YOLOv8 训练玩手机检测从 data.yaml 到跑通最小训练3.1 写 data.yaml路径、类别名与 class 序号必须一致数据集整理完后接下来要写一个 data.yaml 把数据喂给 YOLOv8。这份 yaml 的内容包括数据集的根目录、训练集和验证集路径、以及类别名列表格式如下path: /home/user/phone_dataset train: images/train val: images/val names: 0: phone 1: play 2: call这里的 path 是数据集的绝对路径train 和 val 相对于 path 来写。names 的列表顺序就是类别 id 的映射关系names 里第 0 项对应标注里 class_id 为 0 的框第 1 项对应 class_id 为 1依此类推。很多人喜欢把 names 写成字典形式比如names: {0: phone, 1: play}这种写法也可以但排序必须和标签里的 id 一致。一个常见的低级错误是先把玩手机定义为类别 0后来在 yaml 里又把 phone 放在前面模型本身没有任何校验机制只会闷头按顺序学结果就是推理时把“玩手机”的框标成了“手机”。路径问题也是重灾区。建议 path 用绝对路径不要用相对路径。YOLOv8 解析 train 和 val 时是拼在 path 后面的如果你把 path 写成phone_dataset而当前工作目录不在数据集上一级训练会直接报数据集不存在。还有一个容易忽略的细节yaml 文件本身的编码和缩进不要用中文写注释之外的字符串更不要在路径里混入中文或空格Ultralytics 底层虽然是 Python但 OpenCV 读取图像对中文路径支持有问题训练中途报Unable to read image时优先查路径。3.2 训练命令与关键参数imgsz / epochs / batch 怎么定写好了 data.yaml就可以跑训练了。 train 命令是这套流程的核心用的训练脚本是自带的yolo detect train子命令因为它可以把配置、训练、评估一条龙串起来。yolo detect train \ datadata.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ device0这里每个参数都有讲究。model 我一般推荐先用yolov8n.pt也就是 nano 版本它是八个模型里参数量最小的一个官方权重已经用 COCO 预训练过对于 593 张图的小数据集用 nano 起步收敛最快显存占用也最低。epochs 设 100配合早停机制大多数情况下模型会在 50 到 70 个 epoch 内找到最佳权重。imgsz 是输入分辨率640 是 YOLOv8 的默认值如果你的验证集里手机框占比特别小可以尝试提到 960代价是训练速度下降约一半。batch 大小主要看显存16 是一个比较保守的默认值如果显卡只有 8GB 显存别硬上直接把 batch 降到 8 或者 4。训练过程中会在当前目录生成 runs/detect/train 目录里面有 weights/best.pt、weights/last.pt、以及各种曲线图。best.pt 是按照在验证集上的 mAP 指标自动挑选的最优权重last.pt 是最后一个 epoch 的权重。对小数据集来说last.pt 经常已经过拟合所以后续做推理和导出都用 best.pt。训练时还有一个细节如果显存不够不要只调 batch可以同时把 imgsz 降到 480效果比单纯降 batch 更明显。跑之前确认一下 device 参数CPU 机器上把device0删掉但 593 张图用 CPU 训练会非常痛苦建议至少用一块支持 CUDA 的 GPU。3.3 训练日志怎么看box_loss、cls_loss 和 dfl_loss 在说什么YOLOv8 训练时终端每秒刷一行指标box_loss、cls_loss、dfl_loss后面对应的是验证集上的各种 mAP。很多新手只看 mAP 涨得快就开心却忽略了这几个 loss 的含义。box_loss 是回归损失衡量预测框和真实框的位置偏移它降不下去说明框不准cls_loss 是分类损失衡量类别判断的错误它高就说明分类混淆严重dfl_loss 是分布聚焦损失YOLOv8 用来让框的边更贴合目标这个数对手机这种小目标格外敏感。三个 loss 在训练前 20 个 epoch 会快速下降之后开始震荡收窄如果某个 loss 持续上升比如 cls_loss 在 30 个 epoch 后反弹基本可以断定模型开始过拟合。这里要给一个时间点的血泪经验593 张图的小数据集前 20 个 epoch 的 mAP 可能一直在 0.1 到 0.3 之间来回横跳val loss 也不稳定这时候不要急着停。YOLOv8 的迁移学习特性决定了它前期要先把预训练权重里的通用特征“迁移”过来并适应你数据集的类别分布震荡期是正常的。判断训练是否有效的标准不是单看某几个 epoch而是看整体趋势前 30 个 epoch 的 mAP 是否在波动中上行以及 best.pt 和 last.pt 的差距是否越来越大。如果你的训练在 50 个 epoch 后 mAP50 还在稳定上涨说明数据量虽然小但模型还能从中学到东西可以手动把 epochs 往上加这也是小数据集上不用过于迷信默认参数的原因。4. 验证与导出从 best.pt 到可落地的检测权重4.1 用 val 集评估mAP50 和 mAP50-95 在小数据集上的合理区间训练完成后第一步不是急着导出而是先用验证集评估 best.pt 的真实水平。YOLOv8 的 val 子命令会自动读取你 data.yaml 里配置的 val 路径并在终端打印出一张指标表。yolo detect val \ modelruns/detect/train/weights/best.pt \ datadata.yaml \ imgsz640输出里最重要的两个指标是 mAP50 和 mAP50-95。mAP50 是 IoU 阈值在 0.5 时的平均精度mAP50-95 则是在 0.5 到 0.95 之间每间隔 0.05 计算一次再取平均。273 张这种量级、图像总数 593 张的数据集上mAP50 达到 0.7 以上就可以认为模型可用mAP50-95 通常比 mAP50 低 0.1 到 0.2。如果你看到 mAP50 还行、mAP50-95 很低说明模型的框虽然大体位置对但边界不够贴这在手机这种可旋转、可部分遮挡的目标上很常见。另外一个值得关注的输出是混淆矩阵它在 runs/detect/val 目录下能直观看到 phone、play、call 三类之间互相误判的情况比单看 mAP 更能定位问题。小数据集上评估时有个容易误导人的情况如果你只划分了一次验证集而且随机种子固定得不合理验证集里可能某个类别特别多导致 mAP 虚高。我习惯在训练前用 8:2 划分训练集和验证集同时确保两个集合里每个类别的比例基本一致。训练后再看 val 结果时不要只盯着平均指标把 val 目录下的预测结果图翻一遍重点看有没有漏检的、有没有一个手机框被标成两个类别的。4.2 导出 ONNX 与运行推理从 PyTorch 权重到实际检测模型验证通过后接下来是导出和推理。导出这一步是为了让模型脱离 PyTorch 环境运行或者部署到 TensorRT、OpenVINO 这类推理引擎上。YOLOv8 的 export 子命令一行就能完成导出yolo export \ modelruns/detect/train/weights/best.pt \ formatonnx \ imgsz640导出后会生成同名 .onnx 文件。注意 export 时指定的 imgsz 必须和训练时一致否则导出的模型在推理时会先做输入缩放导致检测框整体偏移。导出完成后用 predict 子命令在真实图片上跑一遍yolo predict \ modelruns/detect/train/weights/best.onnx \ sourcetest_images/ \ conf0.25 \ iou0.45conf 是置信度阈值默认 0.25表示低于这个概率的框会被丢掉。玩手机检测场景建议保持 0.25因为手机在画面里通常比较小置信度天然偏低阈值调太高容易漏检。iou 是 NMS 的 IoU 阈值默认 0.45如果画面里手机和人脸离得很近、框之间重叠度高可以试着调到 0.3允许更多重叠框被保留再观察结果。如果需要集成到自己的业务代码里用 Python API 会更方便。下面这段代码从视频流读取帧并逐帧检测from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) results model.predict( sourcedemo.mp4, conf0.25, iou0.45, streamTrue ) for frame_id, r in enumerate(results): boxes r.boxes.xyxy.cpu().numpy() classes r.boxes.cls.cpu().numpy() scores r.boxes.conf.cpu().numpy() for box, cls, score in zip(boxes, classes, scores): x1, y1, x2, y2 box.astype(int) print(f帧{frame_id}: 类别{int(cls)} 置信度{score:.2f} 坐标({x1},{y1})-({x2},{y2}))这段代码的核心是 streamTrue它让模型按帧处理视频而不是一次性把所有帧加载进内存对长视频推理非常友好。boxes 里存的是像素坐标系下的真实坐标直接可用于在原图画框或做业务逻辑如果要在实时告警系统里用可以在这里接入逻辑判断比如连续 N 帧检测到 class2打电话就触发一次告警这也是做驾驶分心检测项目时最常见的集成方式。5. 避坑593 张图训练玩手机检测最常见的 5 个坑5.1 类别标签错位class id 从 0 开始还是从 1 开始现象训练过程完全正常loss 正常下降验证集 mAP 也不低但推理时所有框的类别都是错的最典型的是把所有目标都识别成第一个类。原因标注 txt 里的类别 id 从 1 开始编号而 data.yaml 里 names 从 0 开始定义比如标注里 class_id1 代表“玩手机”模型却把它当成第 0 类去学。解决回到第 2.2 节的统计脚本先确认 txt 文件里类别 id 的取值范围再看 names 列表和它是否一一对应。一个小技巧是先用 50 张图跑 20 个 epoch 做冒烟测试推理一张图看类别名对不对再上全量训练。5.2 小目标手机检不出imgsz 和锚框的取舍现象图像里手机明明清晰可见但模型就是不给框或者把手机框成了人脸的一部分。原因手机在整张图中的像素占比经常低于 5%在 640 分辨率下一个手机可能只有 30×30 像素特征经过多层下采样后所剩无几。解决优先把 imgsz 从 640 提升到 960YOLOv8 对输入尺寸没有强制限制只是推理耗时会上涨其次把模型从 nano 换到 small 版即yolov8s.pt小目标的特征表达明显更强。还有一种两阶段方案先用一个模型检测“人”再把人的区域裁剪放大后输入第二个模型检测手机这对固定摄像头的场景非常有效缺点是做不了单张图的实时推理。5.3 打电话类别样本太少类别不平衡让 mAP 虚高现象验证集 mAP50 有 0.8但单独看“打电话”类别的召回率不到 0.3真实场景里打电话几乎全部漏检。原因593 张图里打电话的样本可能只有几十张训练时模型把“打电话”当成了难例直接把它的置信度压低以换取整体 loss 下降。解决先做类别数量统计如果打电话框不足玩手机的 20%就不要硬训三类把“play”和“call”合并成“hand_phone”一类反而能提升实际可用性如果必须区分可以做离线过采样把打电话的图片复制 3 到 5 份再训练或在 loss 上给类别加权。5.4 标注框偏离框住手机还是框住手现象有些标注框框的是手机本体有些框把整只手和手机都包进去模型学出来的框位置时而偏上时而偏下后处理阶段很难判断“玩手机”这个动作的覆盖范围。原因标注人员在标注时对“目标”的定义不一致这是 593 张图这种小数据集最常见的质量问题因为它没有多人交叉复核。解决在画框验证阶段就抽 20 张图看一遍规定标注规范——我一般约定“手机全身可视时框手机手机被手遮挡时框手和手机的最小外接矩形”然后只保留符合这个规范的图片如果数据集已经训练过也可以把预测结果和标注框画在一起对比找出偏离严重的几类图再决定是否重新标注。5.5 过拟合train loss 还在降val 指标不再涨现象训练到第 40 个 epoch 后train loss 持续下降但 val mAP50 在 0.75 附近震荡不再上升val loss 反而开始抬头best.pt 对应的还是第 35 个 epoch 的权重。原因593 张图的样本量太小模型的容量超过了数据能承载的信息量后期开始“背题”而不是“学规律”。解决这是小数据集上必然出现的现象不用恐慌。优先确认早停机制是否开启Ultralytics 默认会连续 50 个 epoch 没改善就自动停其次是改用更强的数据增强在第 6 章会展开最后是降低模型复杂度从 small 换回 nano或者从第 50 个 epoch 开始手动减小学习率给权重更新做减速让 loss 在极小范围内震荡而不是跳出最优区域。6. 把 593 张图的价值榨干数据增强与迁移学习的进阶组合6.1 迁移学习不要从头训练一个小数据集对于 593 张图的数据集从头训练一个 YOLO 模型几乎注定失败因为你没有足够的样本让 backbone 学会“边缘”“纹理”“形状”这些基础特征。常规做法是使用官方在 COCO 上预训练好的权重比如 yolov8n.pt它会保留对通用目标的特征提取能力训练时只更新检测头和部分骨干参数。这个过程相当于让模型在“已经会看图”的基础上学会把“手机”和“打电话”这两个特定概念对上号。如果你发现训练时 loss 下降极慢先检查是不是把 pretrained 参数写成了 False或者误用了随机初始化权重。6.2 针对性离线增强比调参更能救小数据的方案训练命令里的随机增强对 593 张图来说力度不够我一般会做一层离线增强把数据量扩充到 2000 张以上再进训练。下面这段代码用 albumentations 库对图片和对应的 YOLO 标签同时做变换import albumentations as A import cv2 import os import glob transform A.Compose([ A.RandomBrightnessContrast(p0.5), A.HorizontalFlip(p0.5), A.RandomSizedBBoxSafeCrop(width640, height640, p0.3), ]) image_paths glob.glob(images/*.jpg) for i, img_path in enumerate(image_paths): img cv2.imread(img_path) h, w img.shape[:2] label_path img_path.replace(images, labels).replace(.jpg, .txt) boxes [] class_ids [] with open(label_path) as f: for line in f: parts line.strip().split() class_ids.append(int(parts[0])) cx, cy, bw, bh map(float, parts[1:]) boxes.append([cx * w, cy * h, bw * w, bh * h]) transformed transform( imageimg, bboxesboxes, class_labelsclass_ids, formatcoco ) out_img transformed[image] out_boxes transformed[bboxes] out_labels transformed[class_labels] cv2.imwrite(faug_images/aug_{i}.jpg, out_img) with open(faug_labels/aug_{i}.txt, w) as f: for box, cls in zip(out_boxes, out_labels): x, y, bw, bh box cx (x bw / 2) / out_img.shape[1] cy (y bh / 2) / out_img.shape[0] f.write(f{cls} {cx:.6f} {cy:.6f} {bw / out_img.shape[1]:.6f} {bh / out_img.shape[0]:.6f}\n)这段代码的核心是 albumentations 对 bbox 的同步变换RandomBrightnessContrast 只改图像不改框HorizontalFlip 会同时翻转框的左右位置RandomSizedBBoxSafeCrop 会在裁剪时保证框不被切掉并把框坐标一同缩放。需要注意的是输入给 transform 的 boxes 是像素坐标且 format 标成 coco也就是 [x_min, y_min, width, height]写回文件时再转成 YOLO 的归一化中心点格式。三个变换里作用最大的是 RandomSizedBBoxSafeCrop它会随机缩放目标在画面中的占比等于模拟不同拍摄距离但它只对训练集有效验证集和测试集不能做同样的裁剪增强否则评估指标会失真。6.3 留出法验证593 张图也要讲统计规则最后一次训练和验证我建议把增强后的数据集重新划分而不是沿用最初的 8:2 划分。做法是按类别分层抽样先按每个类别单独拆一个列表再从每个列表里按相同比例抽取 20% 作为验证集剩下的作为训练集。这一步保证了验证集里“打电话”不会只有 5 张图否则 mAP 的波动会让你完全无法判断模型进步与否。然后把模型在初始划分和分层划分两套验证集上各跑一遍取平均值作为最终指标。最后要留一个习惯把验证集里预测错误的图单独放到一个目录比如把漏检的打电话图片、误检成手机的背景图片都挑出来翻一遍这些图比看 10 遍 mAP 曲线更能告诉你下一步该补什么数据——是补光照变化大的样本还是补手遮挡手机的样本。这个习惯我保留了很久每一次都是靠它找到模型真正的短板比盲目叠增强参数有效得多。希望帮到你。本文还有配套的精品资源点击获取