简介:一份针对行人检测任务的高质量数据集,内含1000张真实场景图片,覆盖校园、街景、道路及严重遮挡等丰富场景,适用于公共场所监控下的行人检测项目,也可作为监控通用行人数据集的有力补充。整个资源包为1个PDF文件(约4.17MB),PDF中详细介绍了数据集构成、标注规范以及百度网盘下载地址,便于用户快速获取原图。所有图片均通过LabelImg精细标注,并同时提供VOC(xml)、COCO(json)、YOLO(txt)三种标准格式,无论是经典R-CNN系列还是YOLO系列,均可直接读取训练,省去繁琐的格式转换工作。随资源附赠的YOLO11一键式训练脚本覆盖GPU(GPUs)、CPU、Mac(M芯片)三大平台,用户可根据本机环境直接运行,另有博主实测训练日志可供对比调参,尤其适合刚接触目标检测的开发者快速跑通完整流程,也方便研究者做多种数据集的对照试验。当前已有1043人学习下载,对于想要积累行人检测实战经验的学习者而言,值得一试。
1. 这个行人目标检测数据集,为什么值得拿来当 YOLO11 的练手项目
手头有 1000 张行人目标检测图,还配好了 VOC / COCO / YOLO 三种格式标签,外加一个能跨 GPU / CPU / Mac 跑的 YOLO11 一键训练脚本——这个组合在视觉入门里不多见,多数人拿到的要么是裸图,要么只有一种标注格式。它的价值不在规模,而在把“数据格式—训练脚本—硬件适配”这条最容易翻车的链路预先踩平了。1000 张图做正式产线太小,但拿来验证算法流程、跑通部署、给导师或客户看 demo,刚刚好。适合两类人:刚接触目标检测、想在一周内跑通一个完整项目的学生,以及要在本地快速验证模型效果、又不愿意花时间折腾数据格式的工程师。
2. 拿到 1000 张行人图先别训:三种标签格式的核对顺序
2.1 VOC 的 XML:先看 folder 和 bndbox 是否需要归一化
VOC 格式的标签是一个和图片同名的 XML 文件,根节点是 annotation,里面逐个 object 描述目标。object 下的 name 是类别名,bndbox 给出 xmin、ymin、xmax、ymax 四个坐标,单位是像素。很多从网上找的 VOC 数据集会把 folder 写成 VOC2007,但实际图片在别的目录;训练脚本如果依赖 folder 字段定位图片,就会直接报文件不存在。所以拿到标签后我先不看可视化效果,而是用脚本把前 5 个 XML 的 filename 和 bndbox 打出来,确认路径和坐标范围是真实像素还是已经归一化的小数。
import xml.etree.ElementTree as ET from pathlib import Path voc_dir = Path("labels/voc") for xml_path in list(voc_dir.glob("*.xml"))[:5]: root = ET.parse(xml_path).getroot() filename = root.findtext("filename") boxes = [] for obj in root.findall("object"): name = obj.findtext("name") bndbox = obj.find("bndbox") xmin = float(bndbox.findtext("xmin")) ymin = float(bndbox.findtext("ymin")) xmax = float(bndbox.findtext("xmax")) ymax = float(bndbox.findtext("ymax")) boxes.append((name, xmin, ymin, xmax, ymax)) print(filename, boxes)这段代码只做了解析和打印,没有改任何文件,适合在训练前做一次“体检”。关键看两点:一是 bndbox 的四个数是否都大于 1,如果全是 0.3 这种小数,说明不是标准 VOC 像素坐标,后续转 YOLO 时不能直接除以图像宽高;二是图片尺寸要与实际图像一致,因为 VOC 的 XML 里有 width 和 height,但有些标注工具写错了,转换时用 XML 里的尺寸会导致框整体偏移。我一般会同时用 PIL 读一下对应图片的真实宽高,两边比对,差 1 到 2 像素可以忽略,差几十像素就得检查图片是否被缩放或旋转过。
VOC 这种格式本身不要求框的宽高大于 0,但实际行人检测中经常出现 xmin 和 xmax 相等或者 ymin 大于 ymax 的脏数据,通常是因为标注员在打点时手抖,或者导出脚本把坐标取整取错了。这样的框在 YOLO 训练里不会直接报错,但会变成面积为零的负样本,让 loss 出现奇怪波动。为了避免后面排查困难,这段核对脚本可以顺手加一个判断,把 bndbox 中 xmin 小于 xmax 且 ymin 小于 ymax 的框保留下来,不符合的单独输出 warning,不要静默跳过。
2.2 COCO 的 JSON:categories 的 id 不能跳,不然训练时类别对不上
COCO 格式的核心是一个 JSON 文件,包含 images、annotations、categories 三个数组。images 数组记录每张图的 file_name、width、height;annotations 数组记录每个框的 image_id、category_id、bbox、area;categories 数组把类别 id 和名字对应起来。行人数据集通常只有一个 person 类,很多人觉得这太简单了,直接开始训练,结果 mAP 一直是 0,最后发现是 category_id 从 1 开始,而 YOLO 内部要求类别索引从 0 开始,两个标注系统之间差了一位。
更隐蔽的问题是 categories 的 id 不连续。比如有的导出脚本会把 person 定义成 id=1,背景 id=0,后来清理类别时删掉 id=0,但 annotations 里残留了 category_id=0 的框。这种情况在 YOLO 加载时会当作不存在或背景,导致一个行人框被丢弃。我拿到 COCO 标签后做的第一件事不是打开可视化软件,而是统计一下实际用到的 category_id 集合。
import json from pathlib import Path coco_path = Path("labels/coco/annotations.json") data = json.loads(coco_path.read_text()) cat_ids = {c["id"]: c["name"] for c in data["categories"]} used_ids = set(a["category_id"] for a in data["annotations"]) print("categories:", cat_ids) print("used ids:", used_ids)这段代码只输出 categories 定义和实际出现过的 id。正常情况应该一致,比如{1: 'person'}和{1}。如果出现{1: 'person'}但 used_ids 是{0, 1},说明 annotation 里有脏数据,需要先清洗。另一个要核对的是 images 里的 width 和 height,很多 COCO 子集为了兼容旧版本,会把原图缩小后没有同步更新尺寸,结果训练时按旧尺寸归一化标签,推理时按新尺寸加载图片,所有框都偏到左下角。核对方式也很简单,取前 5 张图,用 PIL 读取真实宽高和 JSON 里的宽高做比较,不一致就说明图片被处理过了。
COCO 的 bbox 字段是 x、y、width、height,x、y 是框左上角。它和 VOC 的 xmin、ymin、xmax、ymax 不一样,转换时不要把 width 直接当成 xmax。area 字段本来用于计算 mAP 时按面积分层,但很多导出工具并不认真填,如果你不想在评估时被小目标统计干扰,可以在分析时忽略 area,只在转换 YOLO 格式后自己用框的宽乘高算面积。
2.3 YOLO 的 txt:每行 class_id x_center y_center width height
YOLO 格式最简洁,一张图片对应一个同名 txt,每行五个数字:类别 id、框中心点 x 归一化坐标、中心点 y 归一化坐标、框宽度归一化、框高度归一化。归一化的意思是除以图片宽高,所以五个数里除了类别 id 外,其余全部落在 0 到 1 之间,框的宽高还必须是正数。这个格式看起来简单,实际踩坑率最高,因为很多标注工具导出的 YOLO 坐标是未归一化的像素值,或者把 xyxy 格式的 xmin、ymin 当成了中心点。
from pathlib import Path label_dir = Path("labels/yolo") names = ["person"] for txt_path in list(label_dir.glob("*.txt"))[:5]: for line in txt_path.read_text().strip().splitlines(): parts = line.split() cid = int(parts[0]) x, y, w, h = map(float, parts[1:]) assert cid < len(names), f"class id {cid} out of range in {txt_path}" assert 0 <= x <= 1, f"center x out of [0,1] in {txt_path}" assert 0 <= y <= 1, f"center y out of [0,1] in {txt_path}" assert 0 < w <= 1, f"width not normalized in {txt_path}" assert 0 < h <= 1, f"height not normalized in {txt_path}" print(txt_path.name, cid, x, y, w, h)注意断言只是临时检查,如果数据量超过 10000 张,逐行打印会很烦,可以把 print 改成统计异常文件数量。类别 id 必须和 classes.txt 对应的顺序一致,这个数据集的 classes.txt 里只写了 person,所以正常情况下每一行的第一列都应该是 0;如果你发现第一列是 1,而你只有一个类别,说明导出的 COCO 或 VOC 转换脚本没有处理 id 偏移,需要统一减 1。空白的 txt 文件不要删除,它表示这张图里没有目标,训练时可以提供负样本,防止模型把所有区域都预测成行人。
还有一个容易忽略的点是图片文件名和 txt 文件名必须完全一样,包括后缀前的名称和大小写。Windows 下文件名大小写不敏感,但训练一般跑在 Linux 容器或云服务器上,Img_0001.jpg 和 img_0001.txt 会匹配不上。如果你是从 Mac 上把数据集打包发给别人,Mac 文件系统通常也区分大小写,但很多人没注意这个细节,到 GPU 服务器上一跑就发现训练集图片数量为 0。
2.4 用一个 20 行的 Python 脚本交叉核对三种格式
VOC、COCO、YOLO 三种格式从不同侧面对同一个目标做描述,理论上框的数量和位置应该完全一致。与其分别信任三种标签,不如直接对同一张图做交叉核对:把 VOC 的像素框转成归一化 YOLO 中心点格式,再和 YOLO txt 里的解析结果比较;COCO 的 bbox 也转换成同一形式。框的数量不一致时优先看漏检,坐标不一致时优先看转换时是否忘了除以图片宽高。
import xml.etree.ElementTree as ET from pathlib import Path from PIL import Image def voc_boxes(xml_path): root = ET.parse(xml_path).getroot() boxes = [] for obj in root.findall("object"): bb = obj.find("bndbox") boxes.append((float(bb.findtext("xmin")), float(bb.findtext("ymin")), float(bb.findtext("xmax")), float(bb.findtext("ymax")))) return boxes def yolo_boxes(txt_path, img_w, img_h): boxes = [] for line in txt_path.read_text().strip().splitlines(): _, xc, yc, bw, bh = map(float, line.split()) xmin = (xc - bw / 2) * img_w ymin = (yc - bh / 2) * img_h xmax = (xc + bw / 2) * img_w ymax = (yc + bh / 2) * img_h boxes.append((xmin, ymin, xmax, ymax)) return boxes sample = "img_0001" img = Image.open(f"images/{sample}.jpg") print("VOC :", voc_boxes(f"labels/voc/{sample}.xml")) print("YOLO:", yolo_boxes(f"labels/yolo/{sample}.txt", img.width, img.height))这段代码没有计算 IoU,但打印出的坐标可以直接用肉眼对比。如果你处理的图片超过 1000 张,建议在里面补一个循环,把所有文件名的三种标签逐一读取,统计框数量不一致的文件,再单独抽取 10 个样本检查坐标误差。实际经验是,VOC 和 COCO 之间经常出现个位数像素的舍入差,这是正常的,因为不同导出脚本对 xmax、ymax 是否包含边缘的处理不一样;但 YOLO 归一化坐标反推回像素时如果误差超过 10 个像素,就要怀疑图像尺寸读错了。框数量不一致更严重,通常是 COCO 的 annotations 里漏了某些框,或者 YOLO txt 里有多行重复,这种情况只能回源头标注工具里查,不要指望训练时自动纠错。
这里再补充一个文件层面的检查:三种格式的标签文件命名是否都对应上了图片。最简单的方法是用 Python 的 set 对比图片文件名集合与标签文件名集合,找出多余和缺失的文件。1000 张图的数据集不大,但手工核对很浪费时间,这个交叉检查脚本以后换任何数据集都能复用。
3. YOLO11 一键训练脚本:三平台跑通的最小步骤
3.1 目录结构长什么样:数据、标签、脚本各放哪
拿到数据集后第一件事不是写训练代码,而是把目录结构调整成 YOLO 能认出来的样子。Ultralytics YOLO 默认会从图片路径推导标签路径:如果图片在 images/ 下,标签就在同级的 labels/ 下,并且 txt 文件与图片同名。这个数据集里标签分散在 labels/voc、labels/coco、labels/yolo 三个子目录,训练时直接用 labels/yolo 里内容即可,但要先把它复制成标准的 labels 目录,否则训练脚本会显示训练图片 0 张。
pedestrian_dataset/ |-- images/ # 1000 张行人图片 |-- labels/ | |-- voc/ # VOC XML,核对用 | |-- coco/ # COCO JSON,核对用 | |-- yolo/ # YOLO txt,训练用 |-- train.txt # 训练图片绝对路径列表 |-- val.txt # 验证图片绝对路径列表 |-- pedestrian.yaml # 模型数据配置 |-- train_yolo11.py # 一键训练脚本 |-- requirements.txt # 依赖列表在真正训练前,我会先写一个划分脚本,把 1000 张图按 8:2 拆成训练集和验证集。不要手动复制图片到两个文件夹,最好用生成 train.txt 和 val.txt 的方式,因为 YOLO 的 data.yaml 支持文本列表,也可以把图片按 train/、val/ 子目录组织。文本列表的好处是图片不动,改动划分只改 txt 内容,不会产生大量重复文件。
from pathlib import Path import random random.seed(42) images = sorted(Path("images").glob("*.jpg")) random.shuffle(images) val_count = 200 val_set = set(images[:val_count]) with open("train.txt", "w") as tf, open("val.txt", "w") as vf: for img in images: line = str(img.resolve()) if img in val_set: vf.write(line + "\n") else: tf.write(line + "\n") print("train:", len(images) - val_count, "val:", val_count)固定 random.seed(42) 是很重要的细节。如果不固定种子,每次跑这个脚本生成的验证集都不一样,训练结果就无法横向比较。这里的 val_count 设成 200,是因为 1000 张图如果验证集只有 50 张,mAP 的波动会非常大,一个误检框就可能让指标掉几十个百分点;200 张是一个折中,既保证训练数据足够多,又让验证集能覆盖多种场景。如果你只是做快速实验,也可以把验证集缩到 100 张,但指标只当参考,不要用它做最终发布结论。
3.2 一键脚本做了什么:从检查依赖到调用 train
所谓一键训练脚本,本质就是把原来需要手敲的环境检查、设备选择、模型初始化、参数设置和训练启动打包成一个 Python 文件。这个数据集对应的脚本我一般会做成命令行入口,支持 weights、data、device、epochs、imgsz、batch 这些常见参数,默认值按这个 1000 张行人数据集来设。
import argparse import torch from ultralytics import YOLO parser = argparse.ArgumentParser() parser.add_argument("--weights", default="yolo11n.pt") parser.add_argument("--data", default="pedestrian.yaml") parser.add_argument("--device", default="auto") parser.add_argument("--epochs", type=int, default=50) parser.add_argument("--imgsz", type=int, default=640) parser.add_argument("--batch", type=int, default=16) parser.add_argument("--workers", type=int, default=4) args = parser.parse_args() if args.device == "auto": if torch.cuda.is_available(): device = 0 elif torch.backends.mps.is_available(): device = "mps" else: device = "cpu" else: device = args.device model = YOLO(args.weights) model.train( data=args.data, epochs=args.epochs, imgsz=args.imgsz, batch=args.batch, device=device, workers=args.workers, project="runs/pedestrian", name="yolo11n", )这里最关键的是设备选择逻辑。在 auto 模式下,脚本会优先用 NVIDIA GPU,然后尝试 Mac 的 MPS,最后退回 CPU。很多人以为 torch.cuda.is_available() 为 True 就一定能用 GPU,其实如果显卡驱动和 PyTorch 版本不匹配,这个函数会返回 False,这种情况下脚本会自动落到 CPU,不会报错但会很慢。如果你在 Windows 上看到训练日志里没有 CUDA 字样,可以先在终端敲一句python -c "import torch; print(torch.cuda.is_available())"确认环境,而不是怀疑脚本写错了。yolo11n.pt 是预训练权重,第一次运行时需要联网下载到缓存目录;1000 张图的小数据集强烈建议保留预训练权重,否则从零训练几十张很难收敛。
3.3 GPU / CPU / Mac 三平台分别怎么启动
三平台的启动差异只在 device 和显存相关参数。GPU 服务器上可以直接用--device 0,如果机器有多张卡,先用nvidia-smi看一下哪张卡空闲,再指定卡号。CPU 机器上--device cpu即可,但要控制 workers,不要以为 workers 越大越好,Windows 上 workers 大于 0 时必须在if __name__ == "__main__":保护下执行脚本,否则会递归启动进程。Mac 上优先试--device mps,但小显存机器很容易爆,后面避坑章节会专门讲。
# GPU 机器,单卡 python train_yolo11.py --device 0 --batch 16 --imgsz 640 # CPU 机器,显存不足时用 python train_yolo11.py --device cpu --batch 8 --imgsz 640 --workers 0 # Mac 使用 Metal 加速 python train_yolo11.py --device mps --batch 4 --imgsz 640如果是在远程服务器上跑,建议用 nohup 或 tmux 把训练放到后台,防止 SSH 断开导致训练中断。训练过程中可以随时打开 runs/pedestrian/yolo11n/ 目录下的 results.png 和 results.csv 看曲线,不需要等训练结束。1000 张图加 yolo11n 在小 GPU 上大约 30 到 60 分钟能跑完 50 轮,CPU 上可能要三到五个小时,所以有时间预算的话,先用 GPU 调一轮,确认参数合理再全量训练。
3.4 训练完先看 weight 和 results.csv,别急着吹指标
训练结束后,很多人只看终端最后打印的 mAP,然后直接拿 best.pt 去做 demo。终端显示的是训练结束时的指标,不是验证集上的最佳指标,两者可能差不少。Ultralytics 训练过程中会保存两个权重:last.pt 是最后一轮的权重,best.pt 是验证集指标最好的权重,用的时候应该优先选 best.pt。results.csv 里记录了每一轮的训练损失和验证指标,是排查过拟合和判断训练是否稳定的第一手材料。
import pandas as pd results = pd.read_csv("runs/pedestrian/yolo11n/results.csv") val_cols = [c for c in results.columns if c.startswith("val/")] print(results[["epoch"] + val_cols].tail(5))这段代码读取出最近 5 轮的验证指标。行人数据集只有 person 一个类,主要看metrics/mAP50(B)和metrics/mAP50-95(B)这两列。mAP50 是 IoU 阈值 0.5 下的平均精度,对工程 demo 足够;mAP50-95 更严格,适合衡量模型在小目标和遮挡行人上的真实能力。如果验证集 mAP50 一直在 0.8 以上但 mAP50-95 只有 0.2,说明模型框的位置不够准,下一步应该提高 imgsz 或者检查标注框是否偏大偏小。results.csv 还能帮你判断有没有过拟合:train 损失下降但 val 损失回升,就是把训练轮数拉太多了。
4. 把 mAP 从 0.3 拉到 0.6:行人检测必调的四个参数
4.1 epochs:1000 张图不是越大越好
模型训练中第一个要动的参数是 epochs,很多人觉得反正数据少,直接训 300 轮比较稳。1000 张行人图对 YOLO11 来说属于小样本,300 轮极大概率在 100 轮左右就过拟合,val 损失不再下降,train loss 却持续走低。我一般从 50 轮开始,配合早停参数,让模型在验证指标连续 10 到 15 轮不提升时自动停止,这样即使你设了 300 轮也不会浪费时间。
model.train( data="pedestrian.yaml", epochs=100, patience=15, seed=42, )patience 是 Ultralytics 自带早停的窗口大小,单位是轮。1000 张图训练时指标曲线会有波动,patience 设太短容易提前停在一个局部最优,设太长又失去了早停意义。15 轮是一个常见起点,如果你发现模型在第 40 轮还在明显上升,说明 100 轮不够,可以把 epochs 加到 150;如果第 30 轮就开始反复不涨,说明数据集噪声大,回看是否有标签错位。还要注意早停依赖验证集,如果 data.yaml 只提供了 train 没有 val,早停不会生效,整个训练会按 epochs 跑满。
对于这种单类别的行人检测任务,epochs 的选择也可以参考 loss 曲线。如果 box_loss 降得很慢但 cls_loss 已经很低,说明模型定位能力弱,提升 epochs 帮助不大,应该考虑 imgsz 或数据增强。一个容易踩的误区是拿别人在 COCO 上训练的最好轮数来套自己的数据集,COCO 有十几万张图,1000 张图的收敛速度和它完全不是一个量级,照搬只会得到一个人为拔高的 train loss 和一个在验证集上惨不忍睹的模型。
4.2 imgsz:行人目标小,不要照搬 640
YOLO 官方示例默认 imgsz=640,但对行人检测来说,640 不一定是最优。行人目标的特点是整体形状细长,在画面里可能只占几十个像素,特别是监控视角下的人,宽度可能只有 20 到 30 像素。小目标经过网络多次下采样后特征几乎消失,所以这种情况下适当提高输入分辨率比增加训练轮数更有效。如果原图本身是 1280x720,imgsz 拉到 960 通常能明显提升 mAP50-95;如果原图只有 640x480,强行设 960 只是把图像放大,不会增加细节,还浪费显存。
model.train( imgsz=960, batch=8, )不要孤立地调 imgsz,它和 batch 是连锁的。显存占用几乎按分辨率平方增加,640 到 960 会占用大约 2.25 倍的显存,如果你原来 batch 16,现在可能要降到 8 或 4。在 8GB 显存的笔记本 GPU 上,yolo11n 配 960 加 batch 8 通常能跑,但如果再叠加数据增强和混合精度之前的缓存,也可能爆,这时候优先保 imgsz 而不是 batch,因为 batch 小一点只是 BN 统计不稳,imgsz 不够则是直接丢失小目标信息。反过来,如果是 CPU 训练,960 会让推理速度明显变慢,建议先用 640 把流程跑通,再在 GPU 上做一轮 960 的对比实验。
4.3 batch:GPU 显存和 CPU 内存之间的取舍
batch 的默认值在 YOLO11 里按显存大小有自动调整机制,但自动调整有时候很保守,有时候又过于激进。1000 张图的小数据集,batch 太大并不会带来明显的训练加速,反而会让 BN 统计过于平滑,模型过早收敛。你可以在脚本里手动固定 batch,比如 GPU 8GB 显存设 8,16GB 设 16,CPU 设 4 到 8。注意 batch 是 per GPU 的数值,如果用多卡训练,总 batch 等于 batch 乘以卡数,但这个项目完全没有必要多卡。
model.train( batch=8, cache=False, workers=4, )workers 不是 batch,但经常和 batch 一起被误解。workers 表示数据加载进程数,它影响的是 CPU 读取图片的速度,不会直接占 GPU 显存,但每个 worker 会复制一份数据到内存,如果同时开 8 个 worker 且 cache=True,内存占用会直线上升。1000 张图的数据量不大,用 cache=True 把图片全缓存到内存反而能加快训练,但 1280x720 的 RGB 图片一千张大约要占 2.7GB 内存,如果你的机器只有 8GB 内存,建议 cache=False。训练时如果 GPU 利用率低但 CPU 占用率很高,多半是 worker 太少或者数据读取阻塞,后面避坑章节会单独讲。还有一个和 batch 关系很大的现象是 BN 崩溃:当 batch 很小且没有预训练权重时,BN 层统计量会被少数样本带着跳,loss 突然变成 NaN,这个项目里有预训练权重所以风险低,但如果有人把 weights 换成 yolo11s.pt 且 batch 设为 2,就要小心了。
4.4 数据增强:行人遮挡场景怎么救
小数据集最大的问题是增强策略容易失衡。YOLO11 默认开了 mosaic、flip 等增强,mosaic 可以把四张图拼在一起训练,增加目标尺度和上下文多样性,但在 1000 张图这种小数据上,mosaic 过头会让模型看到大量拼接边界,验证时真实图片反而表现不好。行人遮挡是另一个高频场景,两个行人挨得很近时,默认的随机裁剪会把一个人裁掉一半,标签还是原来的完整框,模型就会被误导。这种情况下我一般会调低 mosaic,并且保持水平翻转。
model.train( mosaic=0.5, fliplr=0.5, hsv_h=0.01, hsv_s=0.5, hsv_v=0.4, )hsv_h、hsv_s、hsv_v 是颜色增强参数,行人检测对颜色变化不需要太敏感,hsv_h 调太大会让肤色和衣服颜色失真,反而不利于泛化。如果你是做夜间行人检测,不要只靠颜色增强,还是需要真实夜间图,增强只能辅助。fliplr=0.5 表示 50% 概率水平翻转,行人左右对称,这个增强是安全的;如果数据集里有文字或方向性标志,才需要关掉。除了这些内置参数,还可以在标注工具里对图片做二次处理,比如把原图放大 1.2 倍再切一部分,生成额外的训练样本,但这不在训练脚本范围内,属于数据扩充。
5. 常见训练事故排查:行人数据集最容易翻车的 5 条踩坑记录
5.1 data.yaml 路径写错,训练集图片数量变 0
现象:启动脚本后日志里打印train: 0 images,或者AssertionError: train: No images in ...,但目录里明明有 1000 张图。
原因:data.yaml 里的 path 字段用了相对路径,例如path: .或path: pedestrian_dataset,而训练脚本是从不同目录启动的,Python 进程的工作目录和项目目录不一致,导致 YOLO 找不到图片。更隐蔽的是,train.txt 里写的图片路径是相对路径,YOLO 会以当前工作目录为基准去解析。
解决:在脚本里用Path(__file__).resolve().parent把项目根目录固定下来,再拼出 data.yaml 的绝对路径,不要相信终端当前目录。
from pathlib import Path BASE_DIR = Path(__file__).resolve().parent data_yaml = str(BASE_DIR / "pedestrian.yaml") model.train(data=data_yaml, ...)判断这个问题的方法是打印出 data_yaml 和 train.txt 的前几行内容,确认路径前缀是否指向真实存在的图片。如果图片在 images/ 下,但 train.txt 里写的是 ./datasets/pedestrian/images/xxx.jpg,也要同步检查。这条坑在第一次跑项目时最容易出现,因为大多数人习惯双击脚本或从 IDE 输出窗口运行,工作目录经常改变。
5.2 标签越界,训练报 IndexError
现象:训练开始后很快抛出IndexError: index 1 is out of bounds for axis 0 with size 1,或者 CUDA error 表面信息,实际堆栈在标签加载。
原因:YOLO 的 txt 第一列是类别 id,当前数据集只有一个 person 类,所以 id 只能是 0。但制作 VOC/COCO 的转换工具可能沿用了 COCO 的 category_id,person 在 COCO 里是 1,转换时没减 1,导致训练脚本在读标签时访问不存在的第二类。
解决:先遍历所有 YOLO txt,检查第一列的最大值,再和 classes.txt 的行数对比。这个检查要在训练前做,不要在报错之后做。
from pathlib import Path bad = [] for txt in Path("labels/yolo").glob("*.txt"): for line in txt.read_text().splitlines(): cid = int(line.split()[0]) if cid != 0: bad.append((str(txt), cid)) print("bad count:", len(bad)) if bad: print(bad[:10])如果确实存在非 0 的 id,用脚本把所有 txt 的第一列改写成 0,前提是你确认这个数据集只有行人类别。如果以后要多类别训练,不要机械改成 0,而是建立类别名字和 id 的映射表。另一个相关问题是坐标值本身超过 1,比如 1.2,这通常是因为归一化时用了错误的图片宽高,处理方式不是截断,而是要回原始标注重新计算。
5.3 GPU 显存溢出,训练跑到一半被杀死
现象:训练正常开始,几分钟后终端弹出RuntimeError: CUDA out of memory.,有时 Intel 核显和 NVIDIA 独显同时存在时,PyTorch 选错 GPU,报错信息里显示的还不是 cuda:0。
原因:imgsz、batch、workers 三个参数叠加,把显存瞬时占满。另一个常见原因是 Windows 笔记本上 PyTorch 虽然装着 CUDA 版,但实际调用的是核显或者被其他程序占用的显存。很多笔记本 GPU 是 RTX 4060 Laptop 这类 8GB 显存卡,实际可用的也就 7GB。
解决:先降到最保守的组合--imgsz 640 --batch 4 --workers 0,确认能跑起来再逐步往上加。workers=0 会去掉数据预取,用 CPU 现读现传,虽然慢但不会造成显存峰值。同时用 nvidia-smi 监控显存,如果进程开始前已有多块显存被占用,就换卡。
nvidia-smi --query-gpu=index,memory.used,memory.total --format=csv python train_yolo11.py --device 0 --imgsz 640 --batch 4 --workers 0如果 batch=4 也爆,可以检查是不是开了 cache=True,把整张数据集都缓存进显存。YOLO 官方文档里 cache 有三种值:False、True、'ram',默认 False,但有人为了加速会设成 'ram',如果设成了 True 在 Ultralytics 里会尝试缓存到显存,小显存直接翻车。我自己的经验是,1000 张图不值得用 cache,提速有限,风险不小。
5.4 Mac 上 MPS 不稳定,训练中断或速度极慢
现象:在 MacBook 上用--device mps训练,有时能跑前几轮,随后提示MPS backend out of memory,有时直接卡死,和 Windows 用 GPU 的表现完全不同。
原因:MPS 是 PyTorch 的 Metal 后端,对部分算子支持还不完整,YOLO11 的一些模块在 MPS 上会退化成低效率实现,甚至触发内存分配问题。1000 张图的数据量虽然小,但数据加载的临时张量也会累积在共享内存里。
解决:如果只是想快速验证数据集和脚本有没有问题,直接用 CPU 跑,最多把 epochs 降到 20。如果一定要用 MPS,把 batch 降到 2、workers 降到 0,并且不要开 cache。还可以在脚本里加一层回退:当 device 是 mps 且出现一次 OOM 时,自动换成 CPU 重启训练,但这种容错逻辑在正式环境里会让结果不可复现,我一般不推荐,只用来救急。
try: model.train(...) except RuntimeError as e: if "MPS" in str(e) or "out of memory" in str(e): print("mps failed, fallback to cpu") model.train(..., device="cpu")这个回退写法的缺点很明显:前一个设备上已经跑了 n 轮,换到 CPU 后权重状态需要重新构建,日志里的曲线也会断裂。所以更靠谱的做法是第一次跑之前先用torch.backends.mps.is_available()确认支持,再用小 batch 测试 3 轮,没问题再正式训练。Mac 不是做目标检测训练的主力平台,跑通脚本和做小规模实验可以,大规模训练还是租一台 GPU 服务器更省时间。
5.5 GPU 利用率只有 20%,训练时间被数据读取拖死
现象:用 nvidia-smi 看训练进程,GPU-Util 一直在 20% 上下跳动,显存占用正常,但 epoch 时间比同配置的 Linux 服务器慢很多。
原因:数据加载线程成了瓶颈。Windows 上 workers 大于 0 时,PyTorch 会启动多个进程读图,但如果图片存储在机械硬盘或网络盘上,每个 worker 都在等 I/O;或者 workers 数量超过 CPU 核数,线程切换开销反而拉低速度。另一个原因是图像解码在 CPU 上完成,而 CPU 本身还在做数据增强和归一化,处理不过来。
解决:先看 CPU 占用率。如果 CPU 已经跑满,就把 workers 从 4 降到 2 或 1;如果 CPU 还有很多空余但 GPU 利用率低,可能是图片读取延迟,可以试试 cache='ram',把图片提前读到内存,但这需要足够内存。还有一个被低估的参数是 rect,它让同 batch 的图片按宽高比分组,减少 resize 造成的计算浪费,但会打乱图片顺序,小数据集上对训练影响不大。
model.train( workers=2, cache="ram", rect=False, )如果你的图片分辨率不统一,还可以先把所有图片缩放到统一尺寸再训练,减少运行时 resize 的波动。1000 张图手动批量 resize 也很简单,用一段 Python 循环 img.resize((640, 640)) 输出到新目录,但注意要同步修改标签归一化坐标,因为原图宽高比值变了,直接将长图压成方形会导致行人变扁。我一般不会在数据集层面强行统一尺寸,而是让 YOLO 的 letterbox 处理,这种预处理在脚本里是自动完成的。
6. 训练结束后的两个收尾技巧:混淆矩阵与 ONNX 导出
6.1 用验证结果里的混淆矩阵判断漏检还是误检
训练结束后不要只盯 mAP,去 runs/pedestrian/yolo11n/ 目录找 confusion_matrix.png。单类行人检测的混淆矩阵只有两行两列加一个 background 类,能直接看出模型是把行人错分成背景,也就是漏检,还是把背景错分成行人,也就是误检。如果 background 行里有很多样本,说明模型的置信度阈值设得太低,推理时把大量背景框留了下来;如果 person 行里真实行人大量落到 background,说明模型特征没学到,需要回看标注质量和输入分辨率。
from ultralytics import YOLO model = YOLO("runs/pedestrian/yolo11n/weights/best.pt") metrics = model.val(data="pedestrian.yaml", split="val", plots=True) print("mAP50:", metrics.box.map50, "mAP50-95:", metrics.box.map)这段代码会重新跑一次验证,并重新生成 plots。有人发现混淆矩阵里的数字总和不是 1,实际上每一行是独立归一化的,background 这一行加起来是 1,其他行也各自归一化,所以不能拿整张表的数字做概率解释。看到 mAP 低但不知道为什么时,先看这个矩阵,比猜参数高效得多。
6.2 导出 ONNX 并用 10 张没见过的图做烟雾测试
验证指标只是数字,部署前我还会把 best.pt 导出成 ONNX,再拿训练时没有见过的 10 张图做一次推理。导出的目的不是立刻上生产,而是确认 PyTorch 模型和部署框架之间的输出一致,避免模型结构里用了自定义算子导致转换后行为不同。
model.export(format="onnx", imgsz=640, dynamic=True)dynamic=True 允许输入尺寸动态变化,方便部署时不用固定 640。导出后可以用 onnxruntime 或直接用 YOLO 的 predict 接口做烟雾测试,把 conf 调成 0.5,看哪些图出现漏检、哪些图出现重复框。我自己的习惯是,每次换数据集都把这 10 张图留在一个单独的 demo 目录里,不参与训练,只用来做最终验收。因为 mAP 是平均指标,但能不能上线,往往取决于一张雨天、逆光、人群拥挤的图上会不会漏检。这套 1000 张行人图加三格式标签和一键训练脚本,正好让我能在半天内把这些事全部验证一遍。希望帮到你。
本文还有配套的精品资源,点击获取