☰
从数据到部署:GTSRB交通标志识别全流程实战与踩坑复盘
2026/10/6 13:01:24 网站建设 项目流程

第一次看到这门课的"第6次作业"时,我盯着题目要求看了很久。这是一门研究生阶段的《深度学习与计算机视觉》课程,前五次作业分别是Python与NumPy基础、线性回归与逻辑回归、多层感知机手写数字识别、CNN实现CIFAR-10分类、训练技巧与正则化调参。整体来看难度是逐步爬坡的,而第六次作业不再有现成的代码框架,要求自己完成一个"从数据到部署"的完整图像分类任务:给定德国交通标志识别基准(GTSRB)数据集,训练一个交通标志多分类模型,测试集准确率不低于90%,同时提交完整的代码、训练日志和实验报告。

说实话这个门槛不算高,90%的准确率在GTSRB上并不夸张,但麻烦在于这道题把以前所有知识点串在了一起:数据加载、预处理、模型设计、训练环节、调参、评估、可复现性,还包括实验报告怎么写才规范。当时踩了不少坑,也走了一些弯路。回过头看,这些坑恰恰是这次作业最有价值的部分,所以把整个复盘的完整过程写出来,给正在做类似课程作业或者第一次接触完整视觉任务的同学做个参考。

1. 一站式拆题:从作业要求到执行计划

作业最怕的不是题目难,而是需求没看懂就开始动手。我先花了大概一个小时把作业要求逐条读了三遍,把里面所有动词都标出来,这里特别说一下拆题的方法:我会把每个技术动词对应成一项具体工程任务,并与课程前五次作业的知识点建立关联。

作业实现主要包含这样几个要求:加载并预处理GTSRB数据集;设计或选择一个合适的模型结构;完成训练与验证流程;在测试集上达到90%以上的准确率;提交可复现代码与实验报告。我当时把这些动词拆成了五个实际任务。

  • 数据工程:下载GTSRB、解析csv标注、处理不同尺寸的图片、划分训练与验证集、设计归一化与数据增强pipeline。
  • 模型搭建:决定是手写CNN还是用预训练模型迁移学习。
  • 训练配置:损失函数、优化器、学习率调度、batch size、epoch数、权重初始化。
  • 评估体系:不只盯着整体准确率,要看混淆矩阵、每个类别的precision/recall,判断瓶颈在哪里。
  • 报告与复现:记录所有实验的超参数、生成图表、固定随机种子、整理README和依赖清单。

拆完之后我意识到,前五次作业正好覆盖了其中每一块,但从来没有一个任务把它们全部连接在一起。第6次作业本质上是一次"综合测评"。

1.1 这份作业里最容易被低估的隐藏考点

如果只看表面要求,你可能会觉得这就是"训练一个CNN然后跑出准确率"。但仔细分析评分标准后会发现,报告里有一项叫做"失败案例分析",要求解释模型在哪些类别上表现差,为什么差,以及如何改进。这意味着光把准确率怼上去还不够,你还得能说清楚模型的行为逻辑。

GTSRB数据集本身有很强的现实挑战:类别有43个,不同类别之间的样本数量差异极大。比如限速标志的图片可能有上千张,而某些罕见警告标志只有一两百张。这种类别不均衡会直接拉低少数类的召回率。更麻烦的是交通标志本身存在视觉相似性,比如限速30、限速50和限速80的圆形标识,区别只在于数字不同,经过缩放和模糊之后很容易混淆。

这些隐藏考点决定了选型和训练策略不能"怎么简单怎么来",也要充分考虑最终评判是否具备解释性。

1.2 按"风险优先"原则排定推进顺序

拆完题后我没有立刻写代码,而是先按风险从高到低排了个执行顺序:第一个要解决的是数据加载和预处理,这个出错会导致后续所有工作白费;第二是模型选型,因为这会决定后面的训练时间和调参策略;第三是建立一个能跑通的小规模baseline,即使准确率只有70%,也可以用来验证整个pipeline没有断裂;最后才是优化准确率和写报告。

我当时的直觉是,很多同学一上来就追求高精度,结果卡在数据读取环节折腾一晚上。数据没对齐、标签错位这类问题一旦发生,后面训练出来的模型全都是废的。所以这个顺序非常关键。

2. 数据集与训练环境:先看清楚你手里有什么牌

很多人做作业的第一步是import torch,然后就开始搭模型。我建议反过来,先花两三个小时把数据彻底摸清楚。因为模型结构的选择、数据增强的设计、评估指标的定义,全都取决于数据长什么样。

2.1 GTSRB数据集的真实形态

GTSRB全称German Traffic Sign Recognition Benchmark,是交通标志识别领域非常经典的公开数据集。它包含43类交通标志,训练集大约有39209张图片,测试集有12630张,图片来自真实场景拍摄,尺寸不一,从15x15像素到250x250像素以上都有。

这个数据集最大的特点是"不规整":很多图片不是完整的正方形,车牌、树木、建筑物会遮挡标志边缘,光照条件差异大,部分图片严重模糊。更麻烦的是原始标注并不是统一的图片文件名加标签格式,而是通过csv文件记录每个样本的路径和类别id。我第一次加载的时候,图片尺寸一不一致先不说,光是resize策略就想了很久,因为直接暴力缩放到统一尺寸会丢失很多细节信息。

我当时做的第一件事是写了个脚本,统计每个类别的样本数量并画出柱状图。有些类别明显不足,比如第0类和第1类的样本数量接近2000,而第33类甚至只有不到300。这个分布直接决定了我后面要使用类别权重加权的损失函数。

2.2 环境配置与可复现性准备

训练环境我用的是一台RTX 3060 Laptop显卡,显存6GB,PyTorch 2.1版本,CUDA 11.8。这个配置做GTSRB这种规模的任务完全够用,但如果想跑大Batch或大模型,6GB显存会有点紧张,所以batch size和输入分辨率需要提前估算一下。

我按224x224输入、batch size 64估算,一个batch的数据大约需要22422434字节64约等于38MB,加上中间激活值和梯度,ResNet18网络跑forward和backward大概占用2-3GB显存,可以稳妥运行。但如果用更深的模型如ResNet34或EfficientNet,显存占用会显著上升。

在动手之前,我还做了一个很小但非常关键的步骤:固定所有随机种子。包括Python的random、NumPy的random和PyTorch的torch.manual_seed,同时设置torch.backends.cudnn.deterministic为True。这意味着同一个代码版本在不同时间跑出来的结果完全一致。做实验最怕的就是跑了一个通宵出来一个指标,第二天重新跑发现数字变了,你根本分不清这是模型改进还是随机波动。

2.3 数据增强:最容易被低估的免费午餐

在数据集本身样本有限的情况下,数据增强的价值不亚于换一个更大的模型。我在训练阶段使用了这样一组增强组合:随机旋转15度、随机平移10%、随机缩放0.8到1.2倍、随机水平翻转(注意交通标志没有水平翻转对称性问题,部分标志翻转后语义不变,但数字类标志翻转会变成另一类,所以这里我用了条件翻转并非常谨慎)、颜色抖动(调整亮度、对比度、饱和度)、最后是随机擦除。

这里有个细节值得提醒:验证集和测试集绝对不能使用随机增强,只能做确定性的resize和归一化,否则评估结果会受到增强策略的随机性干扰,导致指标虚高或虚低。我当时在验证集上忘了加归一化步骤,导致验证准确率一直卡在84%,这个问题后面单独细说。

3. 模型选型与迭代路径:不迷信大模型

大多数人遇到分类任务的第一反应是"直接上ResNet50或者EfficientNet"。但我的做法是先从一个自己手写的轻量级CNN开始,跑通整个流程之后,再去尝试更强的迁移学习模型,这样每一步的变量是可控的。

3.1 为什么不一开始就用预训练模型

这里有个很重要的逻辑:预训练模型确实准确率高、训练收敛快,但也会带来两个问题。第一,预训练模型的参数规模大,直接微调时显存占用和训练时间都会增加,在6GB显存的笔记本GPU上需要反复调试batch size和学习率,容易分散精力。第二,课程作业的核心目的之一是检验你有没有真正理解模型结构的设计逻辑,如果上来就torchvision调用现成的ResNet18,最后报告里对"为什么ResNet能解决梯度消失"说不清楚,即使准确率再高也拿不到高分。

所以我决定第一版模型手动写一个轻量级的"学生版"CNN,结构借鉴VGG的思路:卷积层堆叠加最大池化,最后接全局平均池化和全连接层。具体结构是:输入224x224的RGB图像,经过3x3卷积把通道数从3扩展到32,经过ReLU和2x2最大池化后变成112x112;再经过一个3x3卷积把通道数扩到64,池化后56x56;第三个卷积扩展到128,池化后28x28;然后接一个全局平均池化,把特征压缩到128维,最后通过一个有Dropout的全连接层输出43类逻辑。总参数量大约在50万左右。

3.2 从零训练与迁移学习的实测对比

我在同一套数据增强和训练配置下分别跑了手写CNN和ResNet18迁移学习,得到了非常直观的对比。

超参数配置如下:优化器使用AdamW,初始学习率1e-3(迁移学习时降到1e-4),批次大小64,训练15个epoch,学习率采用余弦退火调度。损失函数使用加了类别权重的交叉熵,权重根据每个类别样本数量的倒数归一化得到。

手写CNN大概在第二个epoch之后就开始过拟合,训练集准确率接近100%,验证集却只能爬到89%左右,后面几个epoch几乎没有提升。而ResNet18在前三个epoch中验证准确率就超过了手写CNN的最好成绩,到第12个epoch时已经稳定在96%以上。

这个结果其实在预料之中,因为从零训练一个小网络面对43类高相似度的交通标志,容量明显不足。迁移学习的优势在于ImageNet预训练权重已经给模型提供了丰富的底层特征提取能力,比如边缘、纹理、颜色分布。对于交通标志这种目标集中、背景复杂的场景,这种先验能力非常管用。我把两个模型在测试集上的结果也算了一下,手写CNN是88.7%,ResNet18达到了96.2%,差距非常明显。

3.3 在最终方案中同时保留两个模型的理由

你可能会想,既然ResNet18碾压手写CNN,为什么还要花时间把两个模型都跑完?这里有个经验:完整的实验对比本身就是作业报告的核心内容。如果没有这个对照组,你的报告就只是"我跑了一个模型,准确率96%",没有任何说服力。但有了这个对比,你可以顺着写出一段很好的分析:小的CNN容量不足、迁移学习在目标数据集上的优势、数据增强对两种模型的不同影响。这些讨论比干巴巴堆参数更有价值。

4. 训练阶段真实踩过的坑与排查全过程

这一部分才是这次作业真正让我学到东西的地方。训练过程中一共遇到三个比较大的坑,每一个都花了好几个小时排查。我把完整的排查链路写出来,说不定能让正在踩坑的同学少走些弯路。

4.1 坑一:验证集准确率诡异停滞在84%

在第一个手写CNN训练到第三个epoch时,训练集准确率正常上升,可验证集准确率卡在84%左右,纹丝不动。我一开始怀疑是模型容量不够,于是把网络加宽加了三层,结果毫无变化。这个"完全不受模型容量影响的恒定准确率"很不正常。

顺着这个思路,我开始排查数据读入逻辑。先单独加载了一个验证batch,打印出每张图片的shape和像素分布,发现验证集的tensor数值范围是0到1,而训练集的数值范围是-2到2左右。这正是缺少归一化步骤的症状。我回头检查代码,发现训练阶段的数据pipeline里写了Normalize操作,但验证阶段的pipeline只做了resize和ToTensor,没有加Normalize,两个分支的输入分布完全不一致,模型在验证集上自然乱套。

修正方法很简单,把完全相同的Normalize变换加到验证集的transform里。改完之后验证集准确率直接跳到92%。这个坑的教训是:训练集和验证集的预处理pipeline必须严格一致,差一个Normalize都会让结果失去意义。

4.2 坑二:类别不均衡造成的"假性瓶颈"

ResNet18跑到第8个epoch左右,验证准确率一直在94%徘徊。虽然这个数字已经达标,但我查看了按类别的召回率之后发现,有几类标志的召回率只有60%左右,而整体准确率被大量容易分类的样本掩盖了。

这就是"高准确率下的假性瓶颈"。整体准确率看着还行,但稀有的视觉相似类别表现非常差。我的做法是画出43x43的混淆矩阵,找出所有互相混淆的类别对。结果发现高误判集中出现在:限速30与限速50、圆形禁止标志与部分警告标志。交通标志在缩小到32x32以下时,数字和内侧纹理细节几乎无法辨认,模型只能依赖外圈形状和颜色做判断。

针对这个问题我做了三件事:第一,在损失函数里加类别权重,让少数类样本的错误贡献更高;第二,在数据增强里加入RandomResizedCrop,模拟各种距离下的视觉缩放效果;第三,对混淆严重的类别做了针对性检查,确认标注数据本身没有错位。改完之后,稀有类别的召回率平均提升了8到10个百分点。

4.3 坑三:可复现性比想象中更脆弱

模型最终确定后,我需要重新跑一次完整训练来生成最终提交的日志和指标。结果同一次运行只隔了一天,最终准确率从96.2%掉到了95.4%。代码没改,数据没换,硬件也没变,结果变了。

这个问题最后定位在数据加载器的shuffle机制上。如果DataLoader的shuffle参数设置为True,每次epoch的数据读取顺序都会不同,虽然理论上多次训练取平均后结果应该稳定,但在固定epoch数和固定学习率调度的条件下,单次训练结果会有明显方差。更麻烦的是,我的代码里还用了随机数据增强,每次训练看到的图片都不一样,这本身是好事,但对于复现来说就必须固定增强的随机种子。

所以最终提交版本在训练脚本里做了三处修改:设置全局随机种子、把DataLoader的shuffle改为False并手动用随机索引重排数据集、在数据增强的Random函数前也固定种子。这样模型性能和指标终于可以从头到尾复现。这里提醒一下,如果你是为了学术严谨性需要多次试验取均值,可以用不同的随机种子跑多次再统计均值和方差,但提交给老师的版本务必固定一份种子。

4.4 排查路径的通用模板

三次踩坑让我总结出一个非常通用的排查顺序:先确认数据输入是否正确,再确认模型前向传播是否正确,然后确认损失函数是否正确,最后才是调参。数据问题会伪装成模型问题,模型问题会伪装成损失问题。很多同学看到准确率上不去就立刻调学习率、换模型,结果改了一整天也没变化,原因就是根子在数据管线。

具体来说,遇到指标异常,我建议按以下顺序排查:

  • 检查训练集和验证集的图像预处理是否一致。
  • 打印每个batch的标签分布,确认没有出现标签错位或者类别比例异常。
  • 单独对模型输入做一次可视化,确认resize和归一化后的图片没有变形或信息丢失。
  • 检查损失值是否在正常收敛区间,如果初始损失偏离理论值太多,大概率是输出层或损失函数配置有误。

5. 让作业无可挑剔:实验记录、报告与提交规范

准确率达标只代表完成了一半,另外一半是把你做的所有事情用规范和可读的方式呈现出来。课程作业的评分者需要快速判断你做的东西是否可信、是否可复现,所以实验记录和代码组织方式直接影响最终评价。

5.1 实验记录:让每一次调参都"有据可查"

我在整个训练过程中用CSV文件记录了每一次实验的完整信息:模型结构、输入尺寸、batch size、优化器类型、初始学习率、学习率调度策略、数据增强配置、epoch数、训练集最终准确率、验证集最佳准确率、测试集准确率(在确定最终模型后测试),以及这次实验尝试解决什么问题。

这张表看起来工作量很大,但实际每行只需要在训练脚本里加几行log代码就能自动生成。它的价值在做实验对比时非常明显。有一次我想验证"增加模型宽度是否能提升性能",后来翻记录发现两周前已经做过几乎一样的实验,结论是不行,省下了大半天时间。好记性不如烂笔头,做深度学习实验尤其如此。

5.2 代码整理:从能跑到可复现的提交标准

训练代码写完之后,我没有直接打包上传,而是按照工程规范重新整理了一遍。目录结构大概是这样的:

  • 根目录下放置README.md、requirements.txt和主训练脚本train.py。
  • 数据下载与预处理逻辑单独放在data_utils.py里。
  • 模型定义统一放在models.py里,支持通过命令行参数切换CNN还是ResNet18。
  • 训练过程使用argparse解析参数,所有超参数都支持命令行传入,不硬编码在脚本内部。
  • requirements.txt只列出必须的依赖包,并注明版本号,比如torch==2.1.0、torchvision==0.16.0、numpy、pandas、matplotlib、scikit-learn。

这样处理最大的好处是,评分者只需要运行一条命令就能加载数据、训练模型、输出指标,而不是在Jupyter Notebook里逐个单元格手动运行。同时,代码的可读性也大幅度提升,如果代码里全是硬编码的随机数字、没有注释、所有过程都堆在一个几千行的文件里,别人根本没耐心看下去。

5.3 最终提交前的事后自检清单

提交前两天,我特意做了一次"模拟评分":假设自己是老师,从零开始复现我的代码,检查整个流程是否顺畅。我在一个干净的conda虚拟环境里按requirements.txt安装依赖,然后依次运行train.py和evaluate.py,确认不用修改任何路径和参数就能复现报告里的数字。

以下是我最终提交前自检的项目,供参考:

  • 代码是否能在一台全新机器上直接运行,不依赖个人绝对路径;
  • 数据集下载是否有注释或说明,是否需要手动下载放至特定位置;
  • 训练日志是否完整记录每个epoch的loss与accuracy;
  • 报告里每张图是否有对应的脚本和实验记录;
  • 随机种子是否固定,同一份代码多次运行结果是否可复现;
  • 实验报告里是否包含失败案例分析和后续改进思路。

这些检查每一项看起来都很琐碎,但恰恰是这些细节决定了这份作业是"能跑的代码"还是"可信赖的工作"。

6. 那些代码之外的真实体会

第6次作业的技术内容基本就是以上这些,但代码之外还有一些体会,我觉得比具体API操作更重要。

第一个体会是,完整跑通一个像样的视觉任务,远比想象中花时间,但又远比想象中有价值。很多人以为模型训练就是点一下run然后等结果,实际上数据清洗、预处理对齐、实验记录这些环节,占掉了整个作业大概六成的时间。但这些环节做的功课,在以后做科研和工程时会全部回报给你。

第二个体会是"对照实验是说服力的来源"。不是说你跑出一个高准确率就完事了,你得能解释这个准确率是怎么一步步达到的。从手写CNN到迁移学习,从普通交叉熵到类别加权交叉熵,每一个变化都有记录和原因,报告尤其是答辩环节才不会心虚。

第三个体会是关于时间管理的小技巧。深度学习训练经常要等好几个小时,这段时间最好别干等着,可以一边写报告的实验设置部分,一边做下一个模型选型的调研。我这次作业能在一个星期内完成,很大程度上是因为训练等待时间被充分利用来写报告和整理代码,而不是刷短视频。

最后再分享一个小建议。如果你也是第一次独立完成一个完整的视觉分类作业,不妨从一开始就养成记录每个实验的习惯,哪怕是失败的实验也记录原因和现象。等到写报告的时候你会发现,这些记录不只是凑字数的素材,更是帮你看清整个调优路径的最佳线索。第6次作业让人头疼,但它也确确实实让人把前面五次作业学的所有知识真正串起来了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询