
简介这套项目实践资源面向人工智能学习者与智慧交通方向的研究者聚焦如何利用CNN实现交通标志自动识别以解决道路场景中标志检测与分类的实际问题。素材以德国交通标志识别基准GTSRB为基础包含43类标志、约5万张图像的训练与测试样本适合用于图像分类模型的完整训练流程体验。资源压缩包共8个文件容量约310KB涵盖5个Python脚本、2个CSV数据文件及1个XML配置文件其中Python脚本对应数据输入、模型搭建、训练与评估等环节CSV用于存储标签与路径信息XML为工程配置整体目录划分清晰便于快速上手。已有623人学习浏览适合具备基础深度学习知识、希望以小型数据集完整走通CNN识别项目的学习者。文件虽小但代码结构完整覆盖数据预处理到模型测试的常用步骤可作为交通标志识别任务的入门参考。1. 用CNN识别交通标志GTSRB数据集为何撑起智慧交通的第一块拼图路口摄像头把画面里那块圆形红边标志圈出来判断是限速30还是限速80再联动导航给出提醒——这套流程的第一步就是用CNN识别交通标志。GTSRBGerman Traffic Sign Recognition Benchmark是这条路上被引用最多的公开数据集43类真实路面标志图片来自德国街头实拍光照、遮挡、模糊都是真实场景的常态。对正在走人工智能学习路线的人来说这是第一个能端到端跑通的视觉项目数据规模刚好够训一个轻量卷积网络普通笔记本也能跑。人工智能正从尝鲜工具变成日常帮手交通标志识别就是最贴近日常的那类应用。做人工智能课设、大作业或毕设预研的同学以及想快速上手视觉识别的工程师拿它练手最稳。这篇笔记从拆解GTSRB.zip开始覆盖数据预处理、CNN模型选型、PyTorch训练代码、五个经典踩坑点和边缘端部署验证照着跑完就能拿到一组真实的准确率数字。2. 先拆GTSRB.zip数据目录、标签文件与预处理管线拿到GTSRB.zip之后第一件事不是急着定义模型而是把压缩包解开放平把目录结构和标签文件摸清楚。GTSRB的组织方式和大多数图像分类数据集不一样训练集按类别分文件夹每个类别文件夹里还放了一份CSV测试集则全部平铺在一个目录里靠一份汇总CSV记录标签和标志位置。稍不留神加载器就会把路径写错训练集读成乱序、测试集标签对不上。2.1 GTSRB目录结构与CSV字段43个类别文件夹的标签读取方式解压后进入GTSRB根目录会看到Final_Training_Images和Final_Test_Images两个主目录。训练集的完整路径是Final_Training_Images/Images下面按ClassId从00000到00042分成43个文件夹每个文件夹里是同一个类别的PPM图片另有一份GT-00000.csv这样的标签文件。PPM是未压缩位图格式PIL和OpenCV都能直接读但它没有压缩同样的画面比PNG大好几倍这也是后面显存和内存优化里要处理的问题。整体结构如下GTSRB/ Final_Training_Images/ Images/ 00000/ 00001/ ... 00042/ Final_Test_Images/ Images/ GT-final_test.csv先跑一段代码确认结构和CSV字段import os import pandas as pd gtsrb_root GTSRB train_img_root os.path.join(gtsrb_root, Final_Training_Images, Images) # 43个类别文件夹名就是ClassId从00000到00042 class_dirs sorted(os.listdir(train_img_root)) print(类别数:, len(class_dirs), 前5个:, class_dirs[:5]) # 每个类别文件夹里都有GT-xxxxx.csv分号分隔 gt_csv os.path.join(train_img_root, class_dirs[0], GT- class_dirs[0] .csv) df pd.read_csv(gt_csv, sep;) print(df.head()) print(字段:, list(df.columns))这段代码跑完能看到CSV的完整字段Filename、Width、Height、Roi.X1、Roi.Y1、Roi.X2、Roi.Y2、ClassId。Width和Height是原始图片尺寸Roi.X1到Roi.Y2是标志外接矩形在画面里的坐标。GTSRB不是把标志裁好再给你标志可能偏左、偏下或者只占画面一小块直接用整图训练也能跑但按ROI裁剪后网络的注意力会更集中收敛明显更快。测试集的结构相反所有图片平铺在Final_Test_Images/Images里文件名形如00000_00001.ppm前缀是ClassId真正的标签在Final_Test_Images/GT-final_test.csv里。读取方式一条命令就够了test_csv os.path.join(gtsrb_root, Final_Test_Images, GT-final_test.csv) test_df pd.read_csv(test_csv, sep;) print(test_df.head())我习惯把文件夹名作为训练集标签来源再和CSV里的ClassId做一次核验两边不一致时以文件夹名为准。GTSRB官方包一般没问题但从不同渠道下载的重新打包版本偶尔会丢列或改字段核验这一步成本极低能省掉后面标签错位的排查时间。2.2 图片尺寸不统一用ImageOps.fit统一到48×48GTSRB训练集图片尺寸范围很大小的只有二十几像素大的有两百多像素标志在画面里的位置也不固定。如果直接resize成正方形圆形标志会被拉成椭圆三角警告牌的角度也会失真模型很容易把形状学歪。常见做法是先用ROI坐标把标志区域裁出来再做等比缩放加居中裁剪这样网络从一开始就看到一个干净、居中的标志。from PIL import Image, ImageOps def load_gtsrb_image(path: str, size(48, 48), roiNone) - Image.Image: # PPM格式PIL直接支持无需额外解码库 img Image.open(path).convert(RGB) if roi is not None: x1, y1, x2, y2 roi img img.crop((x1, y1, x2, y2)) # 等比缩放至目标尺寸后居中裁剪避免拉伸变形 img ImageOps.fit(img, size, methodImage.LANCZOS) return imgImageOps.fit会先按长边等比缩放再从中心裁出目标尺寸。methodImage.LANCZOS是重采样算法里最锐的一种PPM这种无压缩图像用LANCZOS不会放大本来不存在的压缩伪影如果换成默认的BILINEAR小尺寸标志的边缘会明显发软。48×48是我在GTSRB上常用的输入尺寸。有人喜欢跟风ImageNet的224×224但GTSRB原始图片大多是低分辨率路面实拍224意味着把20像素的模糊标志放大十倍模型学到的是放大后的锯齿而不是标志结构。32×32又太小限速30和80这类内部数字接近的标志会糊成一团。48×48在细节保留和计算量之间最平衡ROI裁剪后建议把ROI外扩几个像素保留圆环外沿对分类有帮助。提示如果机器的内存紧张可以在解压后用脚本把全部PPM转成PNG再入库。PPM未压缩的特性会在DataLoader读取时拖慢速度PNG体积小、解压快训练循环能明显变轻。2.3 数据增强与类别不均衡用transforms和采样器把少数类救回来GTSRB训练集的类别分布很不均匀样本最多的类有上千张最少的只有一两百张。上一节解决的是形状问题这里要解决的是数量问题。数量不足的类别在交叉熵损失下会被多数类带偏决策边界被压缩测试时一遇到光照变化就误判。缓解手段分两支数据增强补足样本采样器调整类别权重。from torchvision import transforms # 训练集增强平移、旋转、亮度抖动 train_transform transforms.Compose([ transforms.RandomAffine(degrees15, translate(0.1, 0.1)), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.Resize((48, 48)), transforms.ToTensor(), transforms.Normalize(mean[0.340, 0.312, 0.321], std[0.198, 0.199, 0.201]), ]) # 验证集/测试集只做标准化不做随机扰动 eval_transform transforms.Compose([ transforms.Resize((48, 48)), transforms.ToTensor(), transforms.Normalize(mean[0.340, 0.312, 0.321], std[0.198, 0.199, 0.201]), ])这里的三通道mean和std是按GTSRB全量图片统计出来的你没时间自己算的话用ImageNet默认的[0.485, 0.456, 0.406]也能跑但GTSRB是德国路面交通标志色彩分布和自然图像差别不小自己统计的均值能让训练更稳。RandomAffine的degrees15表示最大旋转15度translate(0.1, 0.1)表示水平和垂直最多平移10%边长模拟的是车辆颠簸和相机安装角度带来的轻微偏移。ColorJitter模拟早晚光照变化但亮度扰动别超过0.3否则限速牌的底色会被算成特征反而干扰分类。如果增强还不够就用WeightedRandomSampler给少数类加权。每个样本的权重取该类别样本数的倒数样本少的类别自然被抽得更频繁训练循环本身不用改一行from torch.utils.data import WeightedRandomSampler class_counts train_df[ClassId].value_counts().sort_index() sample_weights [1.0 / class_counts[c] for c in train_df[ClassId]] sampler WeightedRandomSampler(sample_weights, num_sampleslen(train_df), replacementTrue)weight越小的类被抽中的概率越大replacementTrue允许同一张图在一个epoch里被重复抽到等价于给少数类做了过采样。num_samples设成len(train_df)相当于每个epoch仍看一遍全部样本的量不会把epoch拉长。3. CNN识别交通标志的模型选型从输入分辨率到网络深度的取舍在深度学习CNN的实践里预处理决定了模型能学到什么网络结构决定了能学多快。GTSRB是个典型的“小数据、少类别、低分辨率”任务模型选型的逻辑和ImageNet分类完全不同。很多人上来就套ResNet反而把简单问题做复杂了。3.1 输入分辨率先于结构定型224×224在GTSRB上是负优化不少教程习惯性套用ResNet的224输入这在GTSRB上并不划算原因有三训练集总量不到4万张43类分类用不到ImageNet级别的感受野224输入会把小图放大近十倍原本20像素的限速牌放大后全是锯齿参数量和计算量随分辨率平方上涨笔记本上训练一轮的时间会从几十秒变成十几分钟。我一般先用表格把输入尺寸和代价的对应关系列出来再决定用哪个。输入尺寸同结构参数量一轮训练耗时笔记本GPU验证集典型acc适用场景32×32约60万快约20秒94%左右快速验证管线通不通48×48约130万适中约40秒96%~97%默认选择64×64约230万慢约一倍97%出头追求极限精度224×224数百万起步非常慢依赖预训练不推荐这个表的数字是普通笔记本GPU上的大致量级机型不同会有浮动但相对关系基本不变。结论是先跑48×48结构和超参数不动如果个别类确实分不开再试64×64。3.2 轻量CNN结构两个3×3卷积堆叠加Dropout分类头网络结构我倾向于三层卷积块加分类头的轻量CNN每个卷积块由两个3×3卷积、BatchNorm、ReLU组成块尾接一个2×2 MaxPool。为什么不用5×5或7×7的大卷积核两个3×3堆叠的感受野等于一个5×5参数量却只有后者的18/25非线性还多一层在小数据集上更不容易过拟合。import torch.nn as nn class TrafficNet(nn.Module): def __init__(self, num_classes43): super().__init__() self.features nn.Sequential( # 第一块输入3通道输出32通道 nn.Conv2d(3, 32, 3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.Conv2d(32, 32, 3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 第二块32 - 64 nn.Conv2d(32, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.Conv2d(64, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 第三块64 - 128 nn.Conv2d(64, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.Conv2d(128, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.Dropout(0.5), nn.Linear(128 * 6 * 6, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.3), nn.Linear(256, num_classes), ) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) return self.classifier(x)以48×48输入计算中间张量三次池化后特征图是6×6×1284608维分类头从4608压到256再映射到43类整个网络参数量在130万上下CPU上做单张推理只要几毫秒。结构上有两个细节Dropout(0.5)放在分类头第一层之前目的是切断分类头对卷积特征的过度依赖全连接第二层前的Dropout降到0.3防止把训练信号也丢掉。3.3 在小数据集上哪些手段真正有效BN、Dropout与预训练模型的取舍选型时经常被问到要不要用GELU、要不要加注意力模块。我的实践结论是在GTSRB这个尺度上这些改进对最终acc的影响通常在1个百分点以内远不如训练策略稳定。BatchNorm在这里真正解决的是梯度震荡——有了BN学习率从1e-3推到3e-3一般也能收敛对新手来说这是很大的容错空间。预训练模型是另一个方向的弯路。ImageNet预训练模型面向224×224自然图像深层特征学的是猫狗和物体的语义交通标志是符号化的图形两者特征空间不匹配。用ResNet50预训练微调GTSRB光是把图片resize到224这一步就把原始分辨率优势抹平了我见过不少人这么跑出来的acc还不如上面的轻量CNN耗时还慢一个数量级。选型逻辑落到一句话让网络容量匹配任务复杂度而不是把最重的模型搬上来。4. 用PyTorch在GTSRB上完整跑通训练数据加载、训练循环与模型保存预处理和网络结构定下来之后剩下就是数据加载、训练循环和推理脚本。下面给出一套能完整跑通的PyTorch实现代码以可读性优先。跑之前把GTSRB.zip解压到项目目录确认路径变量指向正确的根目录。4.1 自定义Dataset与DataLoader把CSV和图片接成训练管线第一步是把CSV和图片路径拼成一张干净的DataFrame。训练集的难点在于类别分散在43个文件夹里每个文件夹一份CSV需要先逐文件夹读取再合并import os import pandas as pd gtsrb GTSRB train_img_root os.path.join(gtsrb, Final_Training_Images, Images) test_img_root os.path.join(gtsrb, Final_Test_Images, Images) records [] for d in sorted(os.listdir(train_img_root)): folder os.path.join(train_img_root, d) if not os.path.isdir(folder): continue df pd.read_csv(os.path.join(folder, GT- d .csv), sep;) df[ClassId] int(d) # 以文件夹名为准覆盖CSV里的标签 df[path] df[Filename].map(lambda f: os.path.join(folder, f)) records.append(df) train_df pd.concat(records, ignore_indexTrue) print(训练样本数:, len(train_df))测试集只有一份CSV在Final_Test_Images/GT-final_test.csv读取后同样能拼出完整路径。接下来把DataFrame喂给自定义Dataset这个类同时兼容训练、验证、测试三份数据区别只在传入的df不同from torch.utils.data import Dataset from PIL import Image class GTSRBDataset(Dataset): def __init__(self, df, transformNone, use_roiTrue): self.df df.reset_index(dropTrue) self.transform transform self.use_roi use_roi def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] img Image.open(row[path]).convert(RGB) if self.use_roi: img img.crop((row[Roi.X1], row[Roi.Y1], row[Roi.X2], row[Roi.Y2])) if self.transform: img self.transform(img) return img, int(row[ClassId])ROI要不要裁、transform用哪套都由外部参数控制避免为三份数据各写一个类。训练集和验证集按类别分层拆分保证每个类在两边比例一致from sklearn.model_selection import train_test_split train_part, val_part train_test_split( train_df, test_size0.1, stratifytrain_df[ClassId], random_state42, ) train_ds GTSRBDataset(train_part, transformtrain_transform) val_ds GTSRBDataset(val_part, transformeval_transform)再包一层DataLoader训练集挂上WeightedRandomSamplerfrom torch.utils.data import DataLoader train_loader DataLoader(train_ds, batch_size64, samplersampler, num_workers2, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size128, num_workers2, pin_memoryTrue)num_workers2让子进程预读图片pin_memoryTrue把CPU侧张量锁页减少拷贝到GPU时的卡顿。这两项加起来能让训练每轮快10%到20%是成本最低的加速手段。4.2 训练循环与早停Adam、学习率衰减与最优模型保存训练循环的核心是前向算交叉熵、反向传播、更新参数每轮结束在验证集上评估一次只在acc创新高时保存模型。Adam配StepLR是GTSRB上最省心的组合import torch import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model TrafficNet(num_classes43).to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler optim.lr_scheduler.StepLR(optimizer, step_size12, gamma0.5) best_val_acc 0.0 patience 0 for epoch in range(50): model.train() running_loss 0.0 for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() logits model(imgs) loss criterion(logits, labels) loss.backward() optimizer.step() running_loss loss.item() * imgs.size(0) scheduler.step() # 每轮结束在验证集上评估 model.eval() correct, total 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) pred model(imgs).argmax(dim1) correct (pred labels).sum().item() total labels.size(0) val_acc correct / total print(fepoch {epoch1:02d} | loss {running_loss/len(train_part):.4f} | val_acc {val_acc:.4f}) # 只在验证集变好时保存天然得到早停 if val_acc best_val_acc: best_val_acc val_acc torch.save(model.state_dict(), traffic_best.pth) patience 0 else: patience 1 if patience 8: print(验证集连续8轮未提升停止训练) break超参数的选择依据如下超参数取值说明batch_size6448×48输入下4GB以上显存都够用初始lr1e-3Adam默认安全起点weight_decay1e-4轻量L2正则防全连接层过拟合step_size / gamma12 / 0.5每12轮学习率减半早停patience8连续8轮无提升就停最大epoch50实际一般在25到35轮收敛最大epoch设成50但很少真正跑满因为早停会在验证集连续8轮不提升时自动终止。训练时盯着loss曲线如果前5轮loss不降先降学习率别急着改网络。4.3 单张推理与置信度输出加载权重后的最小预测代码训练结束后拿到traffic_best.pth接下来是单张推理。把预处理函数和eval_transform接起来再加一个softmax输出置信度def predict_one(model, img_tensor, device): model.eval() with torch.no_grad(): logits model(img_tensor.unsqueeze(0).to(device)) prob torch.softmax(logits, dim1) conf, pred prob.max(dim1) return pred.item(), conf.item() checkpoint torch.load(traffic_best.pth, map_locationcpu) model.load_state_dict(checkpoint) img load_gtsrb_image(test.jpg, size(48, 48), roiNone) tensor eval_transform(img) cls_id, conf predict_one(model, tensor, devicecpu) print(类别ID:, cls_id, 置信度:, round(conf, 3))model.eval()和torch.no_grad()必须成对出现前者切掉Dropout和BatchNorm的训练行为后者禁止梯度追踪。argmax给出类别序号softmax最大值给置信度。部署时如果置信度低于0.6我倾向直接丢弃这次识别而不是硬给一个标签这比反复调阈值更安全。5. GTSRB训练避坑指南五个让识别翻车的经典问题训练管线第一次跑通时acc往往没那么好看。下面这五条是我在GTSRB上反复踩过的坑按现象、原因、解决三部分写方便你对着自己的报错和曲线定位。有些问题表面看像玄学实际上都能落到某个具体的处理环节。5.1 验证集97%、测试集88%预处理不一致现象训练集和验证集acc都到97%一换官方测试集掉到88%掉点集中在小尺寸标志的类别上。原因最常见的是验证集用了和训练集一样的增强transformRandomAffine和ColorJitter在评估时仍然生效把验证分数抬高了另一种常见情况是训练时用ROI裁剪测试时直接喂整图模型看到的目标比例完全不同。预处理不一致是这类掉点的第一嫌疑。解决把train_transform和eval_transform彻底分开验证和测试统一用eval_transformROI裁剪要么三份数据都用要么都不用。改完预处理后重新训一轮再对比测试集acc通常能拉回5个百分点以上。5.2 loss卡在1.0附近不动学习率与BatchNorm的耦合现象前几轮loss从2.0降到1.0左右之后几乎不再变化验证集acc在90%到93%之间徘徊加epoch也没用。原因lr偏大导致后期在小梯度区域震荡配合BatchNorm一起BN的滑动统计量跟着lr波动loss曲线看起来像一条直线。很多人以为模型容量不够其实只是优化器步长不合适。解决把初始lr降到5e-4并加CosineAnnealingLR做平滑衰减。不改结构时最省事的是StepLR每12轮减半。建议先扫一遍lr1e-3、5e-4、1e-4各训10轮看loss走势再定最终组合。5.3 限速30与限速80互相误判低分辨率下的细节丢失现象混淆矩阵里30和80互认的错误最多60和90也常打架按类别统计限速牌的acc明显低于禁令标志。原因48×48下数字区域只有十几个像素反复下采样后数字轮廓糊掉GTSRB里限速牌在强光、逆光、轻微旋转下拍摄数字对比度不稳定。这类问题不是网络容量不够而是输入信息本身丢了。解决输入提到64×64并把ROI外扩几个像素保留圆环让网络同时用边框和数字判别。给限速类单独加随机亮度扰动模拟黄昏和逆光逼模型学数字结构而不是颜色深浅。注意RandomAffine的旋转别超过10度限速牌数字在15度旋转后已经很难用人眼判断。5.4 少数类别被多数类吞掉采样权重与类别权重的二选一现象总体acc能看逐类统计时某些类只有70%多样本多的类接近100%。原因GTSRB类别分布不均衡是官方数据集的客观属性交叉熵损失被多数类主导少数类的决策边界被挤到一边。解决两选一不要叠加。要么用WeightedRandomSampler做采样平衡要么给CrossEntropyLoss传class_weight权重取N_total/(num_classes×N_class)。叠加使用会让少数类在一个epoch里被抽中太多次训练后期开始过拟合少数类acc反而下降。5.5 显存溢出不是batch的错是特征图撑满了现象训练跑到一半报CUDA out of memory把batch_size从128降到64还报。原因PPM是未压缩位图数据加载和传输在内存、显存里都更占资源峰值显存主要由中间特征图决定batch越大中间张量越多。很多人第一反应是降batch但降的是输入缓存特征图占用的显存没有本质改善。解决先把数据统一resize成48×48再存成小图加载训练时用num_workers2、pin_memoryTrue。需要大batch效果时用梯度累积每4个batch累积一次梯度再更新参数等效于batch256显存占用不变。这是实战里最容易忽略但最有效的一招属于血泪经验。6. 把模型端到端验证一遍ONNX导出、帧率测试与逐类指标模型在测试集上的acc达标只是第一步。智慧交通场景里模型最终跑在摄像头背后的边缘设备上要回答两个问题推理够不够快、每类指标是不是都可靠。我习惯把训练好的模型导成ONNX在目标设备上做一次帧率测试和逐类统计不直接上线——这相当于提前买一道后悔药。6.1 ONNX导出与动态batchimport torch model.load_state_dict(torch.load(traffic_best.pth, map_locationcpu)) model.eval() dummy torch.randn(1, 3, 48, 48) torch.onnx.export( model, dummy, trafficnet.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}}, opset_version13, )torch.onnx.export对固定输入尺寸的CNN很友好。dynamic_axes把batch维设为动态方便部署时一次喂一帧还是多帧自由切换。opset_version13是ONNX Runtime和TensorRT都兼容的下限没必要为了追新版本牺牲兼容性。6.2 帧率预算与逐类召回率验证导出后用ONNX Runtime测推理延迟。目标指标很简单25帧行车记录仪视频单帧预算40毫秒推理加前后处理必须压进这个窗口。import onnxruntime as ort import numpy as np import time sess ort.InferenceSession(trafficnet.onnx, providers[CPUExecutionProvider]) img np.random.rand(1, 3, 48, 48).astype(np.float32) # 预热一次排除首次初始化的干扰 sess.run(None, {input: img}) t0 time.perf_counter() for _ in range(100): sess.run(None, {input: img}) t1 time.perf_counter() ms (t1 - t0) / 100 * 1000 print(f单次推理 {ms:.2f} ms, 约 {1000 / ms:.0f} FPS)上面的循环只测了模型推理本身实际项目里要把resize、ToTensor、归一化也算进去。如果模型推理占掉30ms留给前后处理的余量就很紧这时才需要考虑量化或换更轻的结构。先测再决定别凭感觉调。除了帧率上线前还要打印每类的精确率和召回率尤其是召回率——智慧交通里漏掉一个禁行标志比误判一个限速牌严重得多。把混淆矩阵按类别归一化哪些类需要补样本、哪些类需要调增强一目了然。很多人把部署后的模型当黑匣子我只认逐类指标。我最初跑GTSRB时也经历过验证集97%、测试集88%的落差后来发现只是验证集混进了增强模块。那次之后我养成一个习惯任何预处理或结构改动都在官方测试集上完整验证一遍再谈部署这个习惯帮我避开了好几次上线前的翻车。希望帮到你。本文还有配套的精品资源点击获取