简介:数据集包含559张三轮车实景图片,配套Pascal VOC与YOLO两种格式的标注文件,由labelImg手工绘制矩形框完成,类别统一为tricycle,共659个标注框。面向计算机视觉入门学习者和目标检测算法研究人员,尤其适合需要快速准备训练数据、验证模型效果的场景。资源包内共1679个文件,以jpg图像、xml标注和txt标注三类为主,其中VOC格式便于使用传统检测工具,YOLO格式可直接接入Darknet、YOLOv5等训练流程,压缩包约25.22MB,轻量易下载。目前已有642人学习下载。数据集中仅对三轮车单类别进行框标注,标注信息准确合理,可直接用于模型训练、精度对比或数据增强实验,省去自行采集和清洗数据的环节。
1. 559 张三轮车数据集:一份能直接喂给 YOLO 的交通类样本库
搞目标检测的人应该都经历过这种尴尬:教程看懂了,代码也跑通了,却找不到一份合适的数据集把手里的模型跑出第一条正常的 loss 曲线。这份三轮车数据集就是用来干这个的——559 张 jpg,559 张同名 Pascal VOC 的 xml,559 张同名 YOLO 的 txt,单类别 tricycle,用 labelImg 手工画框,总共 659 个标注框。平均每张图里有 1 个出头一点的目标,比那种单张单框的玩具数据集要真实,比动辄上万张的公开大集又好上手得多。它没有预置 train/val 路径清单,需要自己动手划分,正好把“如何训练自己的数据集”这套流程完整走一遍。适合想跑通 yolov8/yolov5 训练闭环的入门者,也适合拿真实交通样本做迁移学习微调的人。数据量不大,但标注格式里的坑一个不少。
2. 双格式交付:VOC 的 xml 与 YOLO 的 txt 怎么对齐
2.1 目录结构与文件命名规律
拿到数据集先别急着训练,先认清手里有什么。文件就三类:tricycle_xyxr_xxx.jpg、tricycle_xyxr_xxx.xml、tricycle_xyxr_xxx.txt,同名不同后缀。命名里的tricycle_xyxr是统一的,后面的数字编号从个位数到五百多都有,应该是采集时按顺序落盘的。这样设计的最大好处是省掉了匹配环节,jpg、xml、txt 三件套靠文件名一一对应,后面的检查脚本也能直接用文件名做主键去比对。
| 文件类型 | 数量 | 坐标体系 | 说明 |
|---|---|---|---|
| jpg | 559 | 无 | RGB 原始图片,三通道 |
| xml | 559 | VOC 像素绝对坐标 | Pascal VOC 格式,xmin/ymin/xmax/ymax |
| txt | 559 | YOLO 归一化坐标 | class_id + 中心点 x/y + 宽/高,均除以图片宽高 |
类别上只有 1 个:tricycle,总框数 659,也就是所有标注框都落在这个类上。摘要里特意说明“不包含分割路径的 txt 文件”,意思是没有常见的train.txt、val.txt这种记录图片路径清单的文件——它交付的是干净的标注,划分得自己来。这个设计其实很合理,因为每个项目对训练集/验证集的比例和划分方式要求不一样,预置路径清单反而绑手绑脚。
打开一个 xml 看内部结构,节点就是标准 VOC 字段:annotation根节点,下面挂folder、filename、size、object。object里的name是类别名,bndbox里存的是左上角和右下角的像素坐标。对应 yolo 的 txt 则是每行五个浮点数:class_id x_center y_center width height,其中后四个值全部是相对图片宽高的比例,取值在 0 到 1 之间。这两个体系看着简单,混着用就出事——VOC 给的是绝对像素值,YOLO 给的是相对值,同一个框,在 xml 里写<xmin>512</xmin>,在 txt 里可能就变成0.5333 0.4200 0.1200 0.2500。
2.2 两个格式的坐标换算:手工对一遍更放心
为了确认两份标注描述的是同一个框,常见做法是自己写个小脚本,把两种格式读出来画到同一张图上对比。这步不是浪费时间,新手阶段最容易犯的错就是把x_center当成xmin直接用,结果框全部漂移到左上角。下面这个脚本就是干这个的:
import cv2 import xml.etree.ElementTree as ET def read_voc_box(xml_path): tree = ET.parse(xml_path) root = tree.getroot() boxes = [] for obj in root.findall("object"): name = obj.find("name").text bbox = obj.find("bndbox") xmin = int(float(bbox.find("xmin").text)) ymin = int(float(bbox.find("ymin").text)) xmax = int(float(bbox.find("xmax").text)) ymax = int(float(bbox.find("ymax").text)) boxes.append((name, (xmin, ymin, xmax, ymax))) return boxes def read_yolo_box(txt_path, img_w, img_h): boxes = [] with open(txt_path) as f: for line in f: parts = line.strip().split() cls, cx, cy, bw, bh = (int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4])) xmin = int((cx - bw / 2) * img_w) ymin = int((cy - bh / 2) * img_h) xmax = int((cx + bw / 2) * img_w) ymax = int((cy + bh / 2) * img_h) boxes.append((cls, (xmin, ymin, xmax, ymax))) return boxes img_path = "tricycle_xyxr_341.jpg" img = cv2.imread(img_path) h, w = img.shape[:2] for _, box in read_voc_box("tricycle_xyxr_341.xml"): cv2.rectangle(img, (box[0], box[1]), (box[2], box[3]), (0, 255, 0), 2) for _, box in read_yolo_box("tricycle_xyxr_341.txt", w, h): cv2.rectangle(img, (box[0], box[1]), (box[2], box[3]), (0, 0, 255), 2) cv2.imwrite("check_341.jpg", img)逻辑说明:read_voc_box把 xml 里的object全取出来,坐标原样返回;read_yolo_box则是把归一化的中心点坐标和宽高还原成像素坐标,用的是中心点转左上角的经典公式(cx - bw / 2) * img_w。绿框是 VOC 读出来的,红框是 YOLO 读出来的,如果同一辆三轮车两个框严丝合缝,说明这份 txt 的换算逻辑和 xml 对得上。
参数说明:img_w和img_h必须来自同一张图的真实尺寸,否则框会整体偏移;parts里第一个值转int是类别 id,后面四个转float,如果全部用整数除法,小于 1 的归一化坐标会直接变 0,框全部缩在左上角。我建议抽查至少 10 张不同编号的图,而不是只看一张。
2.3 用前先盘点:文件完整性检查脚本
训练时报“can't open image”或者“no labels found”,十有八九是文件缺失或路径写错,而不是模型的问题。我一般先写一个盘点脚本建立基线,确认三个目录数量一致,且没有配不上对的孤儿文件:
import os from pathlib import Path root = Path("tricycle_dataset") jpg_files = sorted(root.glob("*.jpg")) xml_files = sorted(root.glob("*.xml")) txt_files = sorted(root.glob("*.txt")) print(f"jpg: {len(jpg_files)}, xml: {len(xml_files)}, txt: {len(txt_files)}") name_jpg = {p.stem for p in jpg_files} name_xml = {p.stem for p in xml_files} name_txt = {p.stem for p in txt_files} print("缺xml:", name_jpg - name_xml if name_jpg - name_xml else "无") print("缺txt:", name_jpg - name_txt if name_jpg - name_txt else "无") print("多xml:", name_xml - name_jpg if name_xml - name_jpg else "无") print("多txt:", name_txt - name_jpg if name_txt - name_jpg else "无")检查逻辑分两层:第一层用glob按后缀把所有文件捞出来排序,打印三个数量,正常都是 559;第二层对文件名取stem做集合差,jpg 缺对应 xml 或 txt 会直接打印出来。反过来,xml 和 txt 比 jpg 多出来的部分,说明原始目录里有残留标注文件,训练前最好清理掉,否则打乱后可能出现同名的旧标注覆盖新标注。
参数说明:root指向数据集根目录,脚本默认文件都在一层目录里,如果后续你把图片和标注分到images/、labels/子目录,把glob("*.jpg")改成glob("images/*.jpg")即可。划分完训练集之后,建议对两个子集各自再跑一遍这个脚本,确保增强、剪切、移动文件后三件套依然完整。这种“分完再查一遍”的习惯能挡住后面至少一半的玄学报错。
3. 手工标注复盘:labelImg 的配置习惯与被忽略的边界
3.1 标注工具的配置与操作习惯
这份数据集的标注工具是 labelImg,一个开源标注工具,被用得非常广。很多刚接触目标检测的人以为数据集是从天而降的,实际上像这种小样本数据集,大部分就是拿 labelImg 一框一框画出来的。把工具的行为摸透,后面用起来才不虚。labelImg 默认在PascalVOC模式,左下角显示 1 的地方就是格式切换按钮,点一下会在PascalVOC和YOLO之间切换。这里有个非常经典的坑:如果你先以 VOC 模式画了一部分,中途切到 YOLO 模式再保存,VOC 模式生成的 xml 还在,但下次打开时标注框会读不到,因为两种模式的坐标存储结构完全不一样。
我一般建议拿到这类双格式数据后,把目录里所有文件都假定为已按 labelImg 的默认规则落盘:xml 和 txt 与图片同名、同目录,类别名读的是classes.txt或predefined_classes.txt,名字顺序直接决定 txt 里 class_id 的顺序。对这个数据集来说只有 tricycle 一个类,所以 txt 里所有行的第一个数字都应该是 0,如果出现 1,十有八九是标注时类别列表顺序出了问题。
标注规则本身不复杂:对属于 tricycle 的目标画一个紧贴车体外轮廓的矩形框,从左上角拉到右下角,尽量别把路沿、行人、阴影包进去。难处理的是两类情况:一是三轮车被其他车或树遮挡,这种我一般按可见部分画框,框紧贴可见边缘;二是远处的小目标,整辆车在图中可能只有二三十个像素高,这种也要标,哪怕最后训练时它很难被检出来,漏掉反而会让模型学到“小的不用管”的错误先验。
3.2 框数分布:659 个框背后的标注密度
摘要里统计了每个类别的框数,tricycle 659 框,559 张图,平均每张 1 18 个框。这个密度说明大部分图是单目标,但也有相当一部分图是两辆、三辆三轮车同框。用框数反推标注场景,可以判断这是真实道路采集数据,不是摆拍,因为真实街景里目标分布就是离散的,偶尔扎堆。
这个平均框数还能帮你判断要不要做数据增强。如果目标检测里平均每张只有 1 个目标,模型在训练时正样本数量就少,mAP 容易波动。常见做法是先用 659 这个数估算正样本总量,再决定离线增强的倍数——比如做 2 倍增强,让每张图平均有 2 个以上的样本参与训练,类别不平衡的焦虑会小很多。但注意增强必须同步修改标注文件,这个问题后面专门讲。
另外,659 个框也可以用来做标注质量抽检的基准:如果你自己重新统计一遍,发现某张 xml 里的框数比图上肉眼可见的三轮车少很多,那大概率是漏标了;如果某个框的宽或者高小于 10 像素,那大概率是误标或者说画得太贴边,这类框在缩放、增强后很容易变成无效标注。
3.3 标注质量自查:越界框、极小框与重复框
拿到数据集之后,别急着训练,先写一个质量抽查脚本,重点查三类问题:越界框、极小框、重复框。越界框是标注时手抖拉出了图片边界,YOLO 训练时会以负坐标报错;极小框是画得太极限,缩放后特征完全丢失;重复框是同一个目标被不小心画了两次,造成 loss 计算时对小目标重复计数。
import glob import xml.etree.ElementTree as ET from PIL import Image for xml_path in glob.glob("tricycle_dataset/*.xml"): img_path = xml_path.replace(".xml", ".jpg") img_w, img_h = Image.open(img_path).size tree = ET.parse(xml_path) root = tree.getroot() for obj in root.findall("object"): name = obj.find("name").text bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) if xmin < 0 or ymin < 0 or xmax > img_w or ymax > img_h: print(f"[越界] {img_path} {name} ({xmin:.0f},{ymin:.0f})-({xmax:.0f},{ymax:.0f})") if (xmax - xmin) < 10 or (ymax - ymin) < 10: print(f"[极小] {img_path} {name} w={xmax-xmin:.1f} h={ymax-ymin:.1f}")逻辑说明:逐张读 xml,把 bndbox 坐标与图片实际宽高做比较,越界的直接打印;再算框的宽和高,小于 10 像素的单独标记。这里的 10 像素是一个经验阈值,在 640 的输入尺寸下,10 像素的框小于一个 stride(下采样 32 倍后只有 0.3 个像素),模型基本学不到东西,属于无效标注。
参数说明:脚本用 PIL 读图片尺寸,比 cv2 轻量;glob 路径和 xml 后缀替换成 jpg 时要注意目录结构,如果图片和标注不在同一层,replace会失效。跑完脚本,正常情况应该只有极少数告警,如果告警超过总量的 5%,说明这份标注需要返工,不能硬训。
4. 常见问题排查:从拿到数据到跑通训练的五次翻车
4.1 路径错位:图片与标注“同名不同命”
现象:yolo 训练刚开始就报found no labels,或者FileNotFoundError: xxx.jpg,但手动打开路径明明有文件。
原因:最常见的是文件被移动过,比如你把图片单独复制到images/子目录,但 xml 和 txt 还留在原来的根目录。YOLO 训练器的查找逻辑默认是按训练集图片路径,在相同目录下找同名的.txt,目录一变就找不到。还有一种情况是 xml 里写的filename字段和磁盘上实际文件名大小写不一致,Linux 下这种问题会直接暴露。
解决:建立标准的images/与labels/目录结构,按文件名做映射,不要依赖 xml 里的 filename。我一般会先把所有文件按 stem 排序,再批量移动到对应目录,移动完成后立刻跑一遍第 2.3 节的完整性检查脚本,确保两边数量一致。从那以后我再也没有因为路径问题半夜起来调。
4.2 类别索引错乱:tricycle 被识别成别的类
现象:训练不报错,loss 也正常下降,但预测时框是准的,类别却显示成莫名其妙的标签,或者只显示 class 0/1 的数字。
原因:data.yaml 里的names顺序和 txt 里 class_id 的语义对不上。比如你在 yaml 里写了names: ["tricycle"],但某个 txt 里第一行第一列写的是 1,模型就把 id 为 1 的框当成了第二类。VOC 格式因为有name字段所以不会错,YOLO 的 txt 只存数字,类别顺序一旦错了,模型静默地学到错误映射。
解决:这类纯单类数据集,必须保证所有 txt 每行第一个数字都是 0。批量检查一段:grep -c "^1 " *.txt | grep -v ":0",只要 grep 输出非空,就说明有错乱文件,重新跑一遍标注格式转换即可。检查行首数字这个习惯,我在任何多类别数据集上都会先做,因为这是 yolo 数据里最常见、最隐蔽的黑匣子错误。
4.3 VOC 转 YOLO 坐标除错,框全部漂移
现象:可视化脚本里,YOLO txt 画出来的框和标注对象偏差很大,有的框跑到图片外面,有的框宽高比严重不对。
原因:归一化的时候除了错误的尺寸。最常见的是把x_center除以图片宽度时写成了除以图片高度,或者把width除以了图片宽度、把height也除以了图片宽度。再有一种低级错误是部分边角坐标忘了转 float,整数除法把 0.5 直接干成 0。
解决:坐标换算必须严格按公式:x_center = (xmin + xmax) / 2 / img_w、width = (xmax - xmin) / img_w,高度同理除以img_h。我常用上一章的对比脚本做回归验证,换一张没抽查过的图,绿框红框叠得上才算过。这个步骤枯燥,但它是后续所有训练的地基。
4.4 离线数据增强只增强了图片
现象:做了翻转、平移、缩放增强后,训练 loss 变成 nan,或者验证集 mAP 直接掉到接近 0。
原因:增强是离线做的,只对 jpg 做了处理,生成的新图片没有对应的 txt/xml 标注,或者增强里对图片做了裁切,但标注坐标没跟着一起变换。yolo 在训练时会认为这张增强图“没有目标”,一遍遍把梯度往“无目标”方向推,模型很快就废了。
解决:离线增强必须以“图和标注是同一个变换对象”为前提。翻转时 xmin/xmax 要同步翻转,缩放时坐标和图片尺寸同时乘以系数,裁切还要做坐标裁边。如果不熟悉这套几何变换,我一般建议优先用在线增强(mosaic、mixup 这类由训练框架处理的增强),源码级的同步不容易出错。只有在进度条显示正样本严重不足时,才值得动手做离线增强,并且每生成一张新图都要跑一遍标注越界检查。
4.5 验证集 mAP 为 0,不是模型的锅
现象:训练结束,验证集 mAP 一直显示 0.000,但训练集上的 loss 已经收敛得很好。
原因:十有八九是验证集划分方式出了问题。比如按文件顺序取出最后 20% 当验证集,而采集顺序恰好是按路段排的,验证集全是某一种光照或背景下的难例,模型没见过,mAP 为 0 不意外。另一个常见原因是验证集路径含中文或特殊字符,opencv 读图失败后,标记全部丢弃,评估时所有图片都是“无目标”,mAP 自然为 0。
解决:划分时必须随机打散,用固定 seed 保证可复现,这个我在第 5 章专门讲。路径里不要带中文,项目目录统一用英文字母和数字。你自己可以留一条检查线:验证集里挑 10 张图,手动跑一遍检测,如果模型肉眼可见能框住三轮车,但 mAP 是 0,问题一定出在评估的数据加载上,而不是模型。
5. 三种落地玩法:直接训练、迁移微调与可视化体检
5.1 按帧打散划分:固定 seed,让每次结果可复现
第 4 章说了,按文件顺序划分会翻车,正确做法是随机打散。这里有一个细节容易被忽略:划分的最小单位是“帧”,也就是单张图片,而不是“视频段”或“连续帧批次”。如果采集来源是连续视频抽帧,相邻帧高度相似,把它们全放进训练集、或者全放进验证集,都会让验证结果虚高或虚低。现在这份数据集是独立图片,不涉及这个风险,但顺手把 seed 固定下来仍是好习惯。
import random from pathlib import Path random.seed(42) root = Path("tricycle_dataset") imgs = sorted(root.glob("*.jpg")) random.shuffle(imgs) val_ratio = 0.2 val_cut = int(len(imgs) * val_ratio) val_imgs = imgs[:val_cut] train_imgs = imgs[val_cut:] print(f"train: {len(train_imgs)}, val: {len(val_imgs)}") for img in train_imgs: print(img.name) for img in val_imgs: print(img.name)逻辑说明:先对所有图片名做排序,保证不同机器上glob返回的顺序一致;再shuffle打乱;取前 20% 当验证集,其余当训练集。打印出的文件名清单可以接着去生成 YOLO 需要的train.txt和val.txt路径列表,也可以直接用脚本去移动文件到子目录。
参数说明:seed=42是固定种子,你可以换任何整数,但只要固定,别人跑同一个脚本就会得到完全一样的划分;val_ratio=0.2在 559 张图上划分出约 112 张验证,样本量足够评估又不至于抢训练量。如果你有强迫症想保证验证集里每一类都有分布,可以用按类别统计的 StratifiedSplit,但单类数据集没必要。
5.2 YOLOv8 训练闭环:data.yaml 与第一条曲线
新一代 yolo 系列把训练配置收敛到了一个 yaml 加一条命令行,对新人友好很多。这份数据集因为只有单类,data.yaml 可以写得非常干净:
path: /home/yourname/tricycle_dataset train: images/train val: images/val names: 0: tricyclepath是数据集根目录的绝对路径,train和val是相对path的图片目录,names的 key 是 class_id,从 0 开始,value 是类别名。这个顺序和第 4.2 节讲的坑直接相关:yaml 里0对应tricycle,所以所有 txt 里第一个数字必须是 0。目录结构要先建好:images/train、images/val放图片,labels/train、labels/val放 txt,两者文件名一一对应。
准备完成后,训练命令是:
yolo detect train data=data.yaml model=yolov8s.pt epochs=100 imgsz=640 batch=16参数说明:model=yolov8s.pt表示用小规模的 s 权重作为起点,首次跑流程时建议用 s 而不是 x,显存占用低、迭代快;epochs=100在当前数据量下够用了,新手验证流程可以先用epochs=10跑通再拉长;imgsz=640是标准输入尺寸,如果原图普遍超过 1280,可以尝试imgsz=960,但显存和训练时间会上升。第一个 epoch 的 loss 通常比较大,不要慌,先看前 20 个 epoch 有没有稳定下降趋势,而不是盯着 mAP 看。
5.3 迁移学习:用小数据微调,而不是从零训练
559 张图、659 个框,从头训练一个小模型未必不能收敛,但容易过拟合,尤其体现在验证集 mAP 上蹿下跳。更稳的路线是拿在 COCO 上预训练好的权重来 finetune。COCO 里有车、卡车、自行车这类高度类似的目标,模型的底层特征(边缘、纹理、轮子结构)可以直接复用,这也是model=yolov8s.pt这个参数存在的原因——它会自动下载预训练权重。
yolo detect train data=data.yaml model=yolov8s.pt epochs=100 imgsz=640 batch=16 freeze=10freeze=10表示冻结前 10 层 backbone,让它们在微调时保持 COCO 学到的特征不动。小数据集最容易翻车的就是 backbone 被少量新数据带偏,冻结前几层是常用的后悔药。如果你用的是自己从零训练好的权重,把model参数换成本地权重路径即可,pretrained=True这种写法在旧版 yolo 里常见,新版本直接用model=xxx.pt控制。
我个人的习惯是:先用不冻结的版本跑 30 个 epoch 观察 loss,再对比冻结版本,哪个验证集表现好留哪个。对比是为了确认这个数据集到底是“缺通用特征”还是“缺任务专属特征”,前者冻结有效,后者冻结反而拖后腿。
5.4 可视化体检:把 txt 框画回图片
最后一步体检很朴素:把训练用的 txt 画回原图,肉眼确认标注质量。很多人跳过这步直接训练,然后被 mAP 的忽高忽低折磨。画回图片不需要多复杂的代码,用 opencv 就能批量做:
import glob import cv2 def draw_yolo_boxes(img_path, txt_path, color=(0, 0, 255)): img = cv2.imread(img_path) h, w = img.shape[:2] with open(txt_path) as f: for line in f: parts = line.strip().split() cls, cx, cy, bw, bh = (int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4])) 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), color, 2) cv2.putText(img, f"class:{cls}", (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) return img img_path = "tricycle_xyxr_179.jpg" txt_path = "tricycle_xyxr_179.txt" result = draw_yolo_boxes(img_path, txt_path) cv2.imwrite("vis_179.jpg", result)逻辑说明:读图片宽高,解析 txt,把归一化坐标换算回像素坐标,再画矩形框和类别 id。putText里的max(0, y1 - 5)是防止框贴着图片顶部时文字写到画面外去。
参数说明:color=(0, 0, 255)是红色,BGR 顺序,opencv 里别写反;如果要批量检查,把img_path和txt_path换成 glob 遍历即可。抽检时重点看两个地方:一是有没有把石头、树影当成三轮车;二是两辆三轮车挨得很近时,是一个框包住两辆,还是两个框独立。前者说明标注颗粒度不够,后者才是合格的画法。
6. 吃满 559 张:用尺度分布调 anchor,靠置信度门限收口
数据集只有 559 张,想在有限样本里压榨出更好的检测效果,最值得做的一件事是先统计所有真实框的像素宽高分布,再决定要不要调整模型默认的 anchor。YOLO 系列虽然已经能自适应锚框,但默认设置是在 COCO 上统计出来的,COCO 里包含大量大中型目标,而街景里的三轮车通常是有特定尺寸范围的。
import glob import numpy as np from PIL import Image wh = [] for txt in glob.glob("tricycle_dataset/*.txt"): img_path = txt.replace(".txt", ".jpg") iw, ih = Image.open(img_path).size with open(txt) as f: for line in f: parts = line.split() bw = float(parts[3]) * iw bh = float(parts[4]) * ih wh.append([bw, bh]) wh = np.array(wh) print("框宽高均值:", wh.mean(axis=0)) print("宽高比 p10/p50/p90:", np.percentile(wh[:, 0] / wh[:, 1], [10, 50, 90]))这段脚本遍历所有 txt,把归一化宽高还原成像素宽高,再算宽高比的十分位、中位数和九十分位。如果 p50 在 1.2 到 1.8 之间,说明三轮车多是横向偏宽的目标,默认 anchor 里细长条的比例就浪费了;如果整体框偏小,可以适当把 imgsz 调大,或者在训练时减少输入缩放带来的下采样损失。理解这个分布,比机械地套用 anchor 调参脚本更有用。
训练完成之后,还有最后一个收口动作:调整置信度门限。YOLO 预测时会输出每个框的置信度,默认conf=0.25,在 559 张这类小数据集上,这个门限往往偏低,会带出一堆误检框。跑验证时把门限提到 0.35 或 0.4,同时打印出每个置信度区间里的误检数量,找一个“召回不掉太多、误检又压得住”的点。这个操作不改变模型权重,只改变输出策略,但视觉上对交付结果的影响比调几个 epoch 还明显。
从那以后,我每次拿到新数据集,都会强制先走一遍“文件完整性核对→格式可视化比对→标注质量抽检→随机打散划分→小步训练验证”的流程,再开始正式调参。这套流程看起来又笨又慢,但能挡掉大部分低级翻车,让精力真正花在模型本身。希望这份 559 张的三轮车数据,能帮你把这条路走顺。
本文还有配套的精品资源,点击获取