ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

交通标志图像分类数据集:训练/验证/测试集划分与PyTorch实战指南

交通标志图像分类数据集:训练/验证/测试集划分与PyTorch实战指南 简介面向计算机视觉与自动驾驶场景的图像分类任务提供一套经过整理的交通标志物分类数据集覆盖红绿灯、限速、左右转等43个常见类别。数据按训练集、验证集、测试集三部分存放分别包含31,374、7,835、7,835张图片目录结构可直接配合ImageFolder加载也可作为YOLOv5等框架的分类数据集直接使用省去自行爬取与清洗的步骤。压缩包共2000个文件以1999张JPG图像为主另附1个JSON格式的中文标签字典便于类别名与数值标签之间的快速映射。整体大小约58.4MB轻量易下载。已有333人学习使用适合入门图像分类、交通标志识别实验以及相关课程设计。下载后既可快速开展模型训练也可基于标签字典做类别统计与可视化分析帮助使用者聚焦算法验证而非数据工程。1. 交通标志物图像分类数据集训练集、验证集、测试集解决的是哪件大事很多人拿到一个包含训练集、验证集、测试集的交通标志物图像分类数据集第一反应就是把模型丢进训练集里跑跑完看一眼验证集精度就宣布收工。真正做过落地的人都知道翻车点往往不在模型结构而在数据身上目录没审查、分布没检查、划分逻辑不清楚等训练到中途发现测试集信息早已泄露进训练集整个实验等于白做。交通标志物图像分类数据集解决的是图像分类算法在真实交通标志识别上的标准评测问题适合做自动驾驶感知的前置验证、道路标志审计的自动分类也是把最新的图像分类模型快速落到自己业务里的最小闭环。握着这样一份数据集的人最需要的不是再学一个模型而是搞清楚三份数据分别该怎么用、怎么验收、怎么避开那些让你白白浪费算力的坑。2. 拆解数据集结构目录、标注与类别分布怎么在半小时内审查完在我的工作流里拿到数据集的头半个小时不会碰任何模型代码只做一件事把目录结构和标签对应关系彻底搞清楚。交通标志物数据集最常见的组织方式是三个顶层文件夹分别叫 train、val、test每个文件夹下面再按类别名建子文件夹同类的图片直接放在里面。这套结构看起来简单但它是后面所有训练和评测的地基地基偏了楼盖得越高越危险。2.1 训练集、验证集、测试集在文件夹层面怎么组织训练集是模型真正学习的素材验证集是每个 epoch 结束后用来观察拟合程度的小考测试集是最终验收的大考。三者不能混用尤其不能拿着测试集来回调参。我一般会用一段脚本先把三份数据的类别和样本数扫出来而不是靠文件资源管理器手动数。多数交通标志数据集里图片是 jpg 或 png 格式下面这段代码只统计 jpg 是不够严谨的但作为第一遍快速审查已经够用from pathlib import Path data_root Path(traffic_signs) for split in [train, val, test]: split_dir data_root / split if not split_dir.exists(): print(f警告: {split} 目录不存在) continue total 0 print(f[{split}]) for class_dir in sorted(split_dir.iterdir()): if not class_dir.is_dir(): continue n len(list(class_dir.glob(*.jpg))) total n print(f {class_dir.name}: {n}) print(f 合计: {total})这段代码的逻辑很简单对每个顶层数据集分目录再对目录下的每个子文件夹数一遍图片数量。我刻意加了目录不存在的警告分支是因为很多从网盘或者压缩包解压出来的数据集会多套一层目录比如traffic_signs/train/原始文件名/类别/图片如果不检查后续 ImageFolder 会把那层目录也当成类别名类别数直接翻倍。审查时建议顺手再用find traffic_signs -maxdepth 2 -type d | head -50看一眼目录层级深度发现多包了一层就先整理别急着写训练脚本。这类数据集里验证集的样本数通常远小于训练集这是正常的。但有一点要注意如果训练集有 5 万张测试集只有 500 张说明测试集是留作最终打分的封闭题库它的分布未必和训练集完全一致。审查阶段就要接受这个现实不要在测试集上抱太高期望更不要试图把测试集里的难样本补进训练。2.2 标签从哪来文件夹名、CSV映射与类别索引的对应关系交通标志物数据集里标签最常见的载体有两类一类就是子文件夹名一类是独立的 CSV 或 JSON 标注文件。纯文件夹结构最省事因为 PyTorch 的torchvision.datasets.ImageFolder会自动把子文件夹名映射成类别索引。但省事也藏着坑映射规则是按文件夹名字母顺序排序的不是按你脑子里的语义顺序。比如“speedlimit30”和“speed_limit_30”会被当成两个独立类别哪怕人眼看出来它们是同一个东西。我会在训练前把类别索引打印出来存一份档案from torchvision import datasets import json train_data datasets.ImageFolder(roottraffic_signs/train) print(类别索引:, train_data.class_to_idx) with open(label_map.json, w, encodingutf-8) as f: json.dump(train_data.class_to_idx, f, indent2)这段代码做的事情是把训练集的类别名与数字索引的对应关系固化下来。不要小看这一步很多交通标志数据集提供的是 43 类或 58 类的索引映射一旦你把文件夹重命名重新生成的 class_to_idx 顺序会变之前训练好的模型再推理时预测的类别数字就会错位。保存 label_map.json 就是给未来的自己留一份后悔药。如果数据集附带 CSV 标注一定要把 CSV 里的类别名列和文件夹名逐一比对。常见的不一致包括空格、大小写、中英文括号、编号偏移。我遇到过一份数据CSV 里类别从 1 开始编号文件夹却是从 0 开始训练时模型输出的第 0 类对应文件里的第 1 类整整偏了一位不比对根本发现不了。2.3 类别分布不查就开训后面全在给自己挖坑交通标志数据集的类别分布天然是长尾的。限速标志、停止标志这两种出现频率极高施工标志、特殊天气标志可能少一个数量级。如果你一眼都不看分布直接开训模型大概率会把高频类别学得很好低频类别直接摆烂。更麻烦的是验证集的指标会被高频类拉高给你一种模型已经不错的错觉。我一般会用这样一段代码快速画出分布不需要太精细先看数量级差距from collections import Counter counts Counter() for path, label in train_data.samples: counts[label] 1 for label in sorted(counts): print(f{train_data.classes[label]}: {counts[label]})审查分布时要重点看两个数字样本总量最大的类别和最小的类别差了多少倍。如果差 50 倍以上就要考虑在训练时做类别加权或者对稀有类别做过采样。同时评估指标的选取也要跟着调整整体精度overall accuracy在长尾数据上非常误导我后面会展开说。这里记住一个原则先看分布再定指标最后才谈模型选型。3. 从零跑通分类训练PyTorch 加载数据集的完整脚本与超参选择目录审查通过之后才算进入真正的建模环节。交通标志物图像分类数据集的标准玩法是迁移学习加载一个在 ImageNet-1K 上预训练好的图像分类模型把最后一层全连接换成自己的类别数然后在自己的数据上微调。这个思路对几千张到几万张规模的数据集特别合适所花时间短效果比从零训练稳得多。3.1 用 ImageFolder 加载三份数据transform 每一项都在干什么数据加载的代码几乎是固定模板但 transform 里的每个参数都值得较真。交通标志图片的来源有两种一种已经是裁好的标志特写另一种是带街景背景的完整画面。后者如果直接粗暴 Resize 到 224×224标志物会被拉变形。我常用的稳妥做法是先放大到 256再中心裁剪出 224这样既保留了上下文又避免了过度拉伸。from torchvision import datasets, transforms from torch.utils.data import DataLoader transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_data datasets.ImageFolder(roottraffic_signs/train, transformtransform) val_data datasets.ImageFolder(roottraffic_signs/val, transformtransform) test_data datasets.ImageFolder(roottraffic_signs/test, transformtransform) train_loader DataLoader(train_data, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_data, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) test_loader DataLoader(test_data, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue)这里的 Normalize 用的是 ImageNet-1K 预训练权重的配套均值和标准差不是随便填的。如果你的模型加载了预训练权重输入分布必须和预训练时一致否则前几层卷积的激活值会整体偏移。注意 DataLoader 里验证集和测试集不要开 shuffle否则你自己都说不清评估结果到底是按什么顺序算出来的。num_workers 在本地机器上设 0 最保险在服务器上可以调到 CPU 核数的一半太高了反而容易在数据加载环节卡住。3.2 训练循环里验证集该放在哪个位置早停与过拟合的观测点训练集的 loss 下降是应该的真正用来判断模型好坏的是验证集。很多初学者会把验证集丢在训练循环外面等全部 epoch 跑完才评估一次这样做的代价是你无法知道模型到底在第几个 epoch 开始过拟合。我的习惯是每个 epoch 结束就跑一遍验证集保留验证精度最高的权重而不是保留最后一个 epoch 的权重。import torch import torch.nn as nn import torch.optim as optim from torchvision import models device torch.device(cuda if torch.cuda.is_available() else cpu) model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, len(train_data.classes)) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.SGD(model.parameters(), lr0.001, momentum0.9, weight_decay1e-4) num_epochs 20 best_val_acc 0.0 for epoch in range(num_epochs): model.train() train_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() train_loss loss.item() model.eval() correct total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() val_acc correct / total print(fepoch {epoch1}/{num_epochs} loss{train_loss/len(train_loader):.4f} val_acc{val_acc:.4f}) if val_acc best_val_acc: best_val_acc val_acc torch.save(model.state_dict(), best_model.pth)这套循环里的关键参数是学习率 0.001。这个数值对应的是加载预训练权重后的微调场景配合 SGD 动量 0.9 是图像分类算法里最经典的一套基线配置。如果你是拿小数据集从头训练学习率可以提高到 0.01但收敛会更不稳定。批次大小 32 在单卡上很友好如果你显存够大改成 64学习率可以适当放大一点不过对交通标志这类任务收益不明显。训练过程中如果看到验证集精度连续三个 epoch 不涨甚至下降而训练集的 loss 还在降那就是过拟合信号可以直接早停不用跑满 20 个 epoch。3.3 测试集只准碰一次留在最后评估的纪律测试集是这套数据集里最容易被糟蹋的资源。它的设计初衷是模拟真实世界里的未知数据只允许在最终评估时使用一次。如果你在调参过程中反复用测试集验证效果测试集就慢慢变成了第二个验证集你会在不知不觉中针对测试集过拟合。这个纪律听起来简单执行起来很难。我见过不少人在训练脚本里顺手把 test_loader 加进评估循环每个 epoch 都打印一次测试精度还觉得方便。从实验结果可比性的角度讲这等于把高考题当练习册刷最后得到的测试精度没有参考价值。我自己的做法是训练脚本里只挂验证集测试集单独写一个 evaluate.py只在模型定稿之后运行一次把结果记录到实验日志里。交通标志物数据集里的测试集一般数量不大格外珍贵尤其当你要和别人横向对比识别精度时测试集的口径必须干净。4. 按场景、按时间还是按比例划分保证验证与测试可信的三种分割策略现在很多交通标志物数据集在发布时已经分好了 train、val、test看起来不需要我们再操心划分问题。但当你出于业务需要重新整理数据、合并多个来源或者想自建一套验证集时划分方式就直接决定了实验结论是否可信。这一章的坑比模型结构里的坑更难察觉。4.1 随机划分为什么在交通标志场景里容易“作弊”最自然的划分方式是把所有图片随机打散按比例分到训练集和验证集。如果图片之间是相互独立的这个方案完全没问题。但交通标志物数据集的采集方式通常是车载摄像头录像抽帧一辆车开过一个路口连续 10 帧画面里可能是同一个标志的连续拍摄。随机划分时第 5 帧进了训练集第 6 帧进了验证集这两张图几乎一模一样模型在验证集上看到的所谓新样本其实是训练集见过的画面的孪生帧。这种泄露比文件夹重叠更隐蔽因为图片内容确实不完全相同但背景、光线、拍摄角度高度一致。模型学到的可能是场景记忆而不是标志本身的语义。做这种数据集我一般会先确认每个样本是否带有采集场景编号、拍摄时间或者帧序号字段。如果有按这些字段进行分组划分保证同一个场景的所有帧只出现在一个集合里。sklearn 里的 GroupShuffleSplit 就是为这个场景设计的按视频片段或者采集 session 编号分组而不是按单张图片随机分。4.2 分层划分保持类别比例一份可直接改的 sklearn 脚本如果数据集本身没有强场景关联性那么至少要保证划分后每个类别的比例和原始数据一致。直接用 train_test_split 在类别不平衡时会随机波动小众类别可能全跑到验证集里。更好用的做法是 StratifiedShuffleSplit它按标签分层保证每一类在训练集和验证集中的占比与原始分布尽量一致。import pandas as pd from sklearn.model_selection import StratifiedShuffleSplit from pathlib import Path import shutil df pd.read_csv(labels.csv) # 至少包含 image_path 和 label 两列 splitter StratifiedShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(iter(splitter.split(df, df[label]))) train_df df.iloc[train_idx] val_df df.iloc[val_idx] for split_name, sub_df in [(train, train_df), (val, val_df)]: dest_root Path(traffic_signs_splitted) / split_name for _, row in sub_df.iterrows(): src Path(row[image_path]) dest dest_root / row[label] / src.name dest.parent.mkdir(parentsTrue, exist_okTrue) shutil.copy2(src, dest)这段脚本里的 test_size0.2 是常见选择意味着验证集占总样本 20%。如果你的总数据量只有几千张验证集可以缩到 0.15给训练集多留一点空间。random_state42 固定随机种子保证每次运行划分结果一致这是可复现实验的基本要求。我把划分后的图片用 copy2 复制而不是移动是因为原始标注文件还要留着备用万一划分逻辑写错还能重新来移动文件就不好回退了。4.3 保存划分索引让结果可以被复现的两个小技巧数据划分这个动作看起来一次性实际上在实验中会反复发生。你可能会想调整验证集比例、换随机种子、或者把部分验证集样本挪进训练集。这时候如果每次重新跑划分脚本得到的索引都可能不同之前实验的结果就没了可比性。我的习惯是把划分索引单独保存下来纳入版本管理。import json with open(split_index.json, w) as f: json.dump({ train: train_idx.tolist(), val: val_idx.tolist(), random_state: 42, test_size: 0.2 }, f, indent2)保存索引文件后任何时刻想恢复同一批划分只用读取这份 JSON 里的行号再对齐原表即可。第二个技巧是划分完成后立刻打印每一类的数量分布和原始分布对比一遍确认稀有类没有被漏掉。交通标志数据集里有些类只有几十张样本划分后如果验证集里只剩两三张这类样本的评估结果基本失去统计意义这时候要想清楚是接受这个现实还是针对稀有类单独做评估。5. 交通标志物数据集最常见的五个坑现象、原因与排查方法这部分是我最想写给第一次碰交通标志物数据集的同行看的。以下五个坑我从实际项目中踩过或者帮别人排查过每一条都很典型完全可以对照检查自己的数据集和代码。5.1 类别不均衡验证精度虚高的假象现象训练集精度一路升到 0.97验证集整体精度也不错但换成测试集之后精度掉了十多个百分点而且每次跑结果波动很大。原因交通标志数据集的长尾分布太典型了。整体精度是各类别精度的加权平均高频类别占比高主导了最终数字。模型把高频类全学对整体精度就好看可低频类别几乎全错也没人发现。解决改用 balanced accuracy 或者 macro F1 作为主评估指标同时在验证集上记录各类别的最小精度。计算上就是把每个类别的 recall 先各自算出来再取平均这样稀有类别和常见类别在指标里有相同的发言权。你先按第 2 章的方法看分布如果最大学类别和最小类别的样本数差距超过 50 倍这个坑基本已经埋下了。5.2 尺寸不一致Resize 把所有标志拉成一团现象训练 loss 降得比预期慢验证集 loss 一路抖动模型预测结果里很多标志被互相混淆。原因一套交通标志物数据集里往往混着不同来源的图片有的是从大图上裁出的标志特写有的是完整的街景画面里标志只占一小块。统一 Resize 到 224×224 时特写图被正常缩放完整街景图里那个小标志被缩成几个像素的色块模型根本看不见。解决先判断标志物在原图中的占比。如果标志本身已经是主体随便 Resize 问题不大如果是从大场景里裁出来的建议先加一个中心裁剪或者按标注框裁剪的预处理步骤保证送进模型的主体是标志物而不是马路和天空。我之前在 3.1 里写的 Resize(256) 加 CenterCrop(224) 就是对这类问题的一种缓解方案但它不是万能的标志占比过小时需要做更激进的目标区域裁剪。5.3 PNG 带透明通道训练时突然出现全黑图现象训练过程中 loss 偶尔出现一次巨大尖峰可视化中间结果时发现某张图整体是黑的边缘有锯齿状物体轮廓。原因部分交通标志图片是 RGBA 的 PNG透明通道用 0 值表示。PyTorch 的默认加载器把 RGBA 的透明像素直接当作黑色背景模型看到的是一个黑底上的标志和训练集里其他白底或者真实街景背景的图片分布完全不同。解决在加载图片时先判断通道数对 RGBA 图片做白底合成。一段通用处理如下from PIL import Image def load_rgb(path): img Image.open(path) if img.mode RGBA: bg Image.new(RGB, img.size, (255, 255, 255)) bg.paste(img, maskimg.split()[-1]) return bg return img.convert(RGB)这段代码的思路是把 alpha 通道作为蒙版将彩色内容贴到白色背景上透明区域自然变成白色不参与语义内容。很多和交通标志相关的开源图片素材是从设计稿或图标资源里收集来的PNG 透明底特别常见这个处理不能省。5.4 训练集和测试集重叠查重复样本的正确姿势现象验证集精度极高高到不真实但拿到另一个独立采集的数据上做外部评测时精度大幅缩水。原因原始数据没做去重处理。同一个标志物的图片可能因为采集抽帧、人工整理等原因同时出现在训练集和测试集里模型等于是对见过的图片做记忆输出。解决跑一遍哈希去重把所有图片的 MD5 算出来跨目录比对。轻量做法是只比对文件名和文件大小但更可靠的是按内容哈希from pathlib import Path import hashlib def md5_of_file(path, chunk_size8192): h hashlib.md5() with open(path, rb) as f: for chunk in iter(lambda: f.read(chunk_size), b): h.update(chunk) return h.hexdigest() hashes {} for split in [train, val, test]: for p in (Path(traffic_signs) / split).rglob(*.png): h md5_of_file(p) hashes.setdefault(h, []).append(p) for h, paths in hashes.items(): if len(paths) 1: print(重复样本:, [str(p) for p in paths])查出重复后处理原则是优先删除验证集和测试集里的重复项训练集里的尽量保留因为训练数据多一点不是坏事。极端情况下如果重复比例超过 1%要回头检查一下数据集是否真的按我们想象的方式分好了。5.5 标签偏移从 0 开始还是从 1 开始现象训练结束打印类别数量和类别名发现和原始标注文档对不上比如文档写 43 类模型输出 44 类或者推理时预测的类别数字和真实标志对不上。原因标签编号的起点不一致。ImageFolder 从 0 开始编号有些标注文档从 1 开始也有的是因为同一类标志在文件夹里出现了两种命名写法被算成了两个类。解决训练前打印完整的 class_to_idx逐条核对一遍。对命名不统一的文件夹先清洗把speed limit 30和speedlimit30合并成同一个名字再重新生成索引。另外在推理脚本里永远不要硬编码类别数字而是从 label_map.json 里读取映射关系这样一旦数据更新至少有一个明确的检查点。6. 把测试集用到极致的进阶验证混淆矩阵与错误样本可视化跑完训练只是完成了模型验证的一半真正决定这个交通标志物图像分类数据集能不能上线的是错误分析。我不会停留在打印一个精度数字就结束一定会把混淆矩阵拉出来把预测错的图片一张张看过去。6.1 用混淆矩阵定位易混类别整体精度只告诉你做得好不好不告诉你好在哪里、坏在哪里。用 torchmetrics 可以很轻量地拿到测试集上的混淆矩阵from torchmetrics import ConfusionMatrix confmat ConfusionMatrix(taskmulticlass, num_classeslen(train_data.classes)).to(device) model.eval() with torch.no_grad(): for images, labels in test_loader: preds model(images.to(device)) confmat(preds, labels.to(device)) matrix confmat.compute().cpu().numpy()这个矩阵里对角线越亮代表识别越准非对角线上的亮点就是模型经常搞混的类别对。交通标志里最常见的是“限速 30”和“限速 40”相互误判或者“禁止驶入”和“禁止停车”这种外形接近的三角形标志。看到这些易混对之后下一步不是盲目加训练数据而是先检查这两类在尺寸、颜色、拍摄角度上到底差在哪。6.2 把错误预测的图片存下来逐个看混淆矩阵能告诉你哪些类互相混淆但看不出具体原因。我会把所有预测错误的样例保存到一个 error 目录里按真实类别和预测类别命名然后打开这些图看第一眼直觉。这一步经常能发现标注错误不是模型错是数据本身标错了。from pathlib import Path error_dir Path(errors) error_dir.mkdir(exist_okTrue) model.eval() with torch.no_grad(): for images, labels in test_loader: outputs model(images.to(device)) _, preds torch.max(outputs, 1) for i in range(len(images)): if preds[i].item() ! labels[i].item(): vis images[i].cpu().permute(1, 2, 0) * torch.tensor([0.229, 0.224, 0.225]) torch.tensor([0.485, 0.456, 0.406]) save_path error_dir / ftrue_{labels[i].item()}_pred_{preds[i].item()}_{i}.png # 用 PIL 或 matplotlib 保存 vis 即可看错误图的时候不要只看模型认错的那几张而是对比同类的正确样本找出是光线太暗、遮挡严重还是标志本身已经磨损掉色。这个分析结果会直接告诉你下一轮该做什么是扩充数据、调整预处理还是换一种更关注细节的模型结构。6.3 在测试集上只做一次最终验证所有调参和模型选择都要在验证集上完成测试集只留到最后跑一次这是老生常谈但真正做到的人不多。我会习惯把测试集评估的三个数字同时记录整体精度、balanced accuracy、以及最少类别的单类精度。三个数字一起看才能判断模型是真的可用还是只在大类上好看。做这类数据集项目我个人的习惯是每换一次思路就重新打印一次混淆矩阵不看矩阵就不继续调参。宁可多花三分钟看图也不愿一觉醒来发现模型在测试集上翻车后从头排查。希望帮到你。本文还有配套的精品资源点击获取
返回列表