第一次看到这门课的第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显卡显存6GBPyTorch 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只列出必须的依赖包并注明版本号比如torch2.1.0、torchvision0.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次作业让人头疼但它也确确实实让人把前面五次作业学的所有知识真正串起来了。