
简介面向智慧交通场景的CNN交通标志识别实践项目以GTSRB公开数据集为核心适合深度学习和计算机视觉初学者用于图像分类项目练手也可作为交通标志识别课题的参考实现。压缩包共8个文件包含5个Python脚本负责数据预处理、CNN模型构建、训练与评估、2个CSV数据索引文件及1个XML配置整体仅310KB代码精简便于快速上手。目前已有623人学习下载。资源提供从数据读取、预处理到模型训练与测试的完整流程代码可帮助理解卷积层、池化层、全连接层及损失函数等核心概念通过GTSRB 43类交通标志数据集实践图像分类任务。1. 交通标志识别一个看起来简单、做起来全是细节的 CNN 项目用 CNN 做交通标志识别是人工智能项目实践里出现频率最高的一类大作业和毕设选题数据集多半是 GTSRB.zip。GTSRB 是德国交通标志基准数据集43 个类别、5 万多张真实拍摄的交通标志照片尺寸从 15×15 到 250×250 不等光照、遮挡、运动模糊都是实地采集的原始状态。如果只做数据集的准确率经典 CNN 模型跑到 94% 左右不难再往上每提升 1 个点都要跟过拟合、类别不均衡和预处理细节较劲——这个「收益递减」的过程才是这个项目的真实价值所在。它适合两类人一是需要交人工智能课程设计、毕业设计的学生二是刚入门深度学习、想用一个完整项目走通「数据 → 训练 → 评估 → 部署」全流程的工程师。这篇笔记把我做这个项目的完整思路和踩坑记录写出来按「模型怎么选 → 数据怎么喂 → 训练怎么调 → 哪里会翻车」的顺序展开。2. 模型设计为什么 CNN 适合识别交通标志以及怎么选 Backbone2.1 交通标志识别的难点决定了模型必须长什么样交通标志识别本质上是一个细粒度图像分类问题43 个类别很多类别之间只有细微差别。比如限速 30 和限速 50 的标志内圈数字不同其他完全一样禁令标志都是红圈白底区分点在内部符号。这种任务里模型必须同时抓住「整体形状」和「局部细节」两类特征这正是 CNN 的强项卷积核在浅层学到边缘和颜色块在深层学到「数字 30」和「数字 50」这种语义级模式。GTSRB 的特殊性在于它不只是「分类准确率」的问题。图片来自真实道路环境同一个标志在不同图片里的尺度差异巨大——有的占满整张图有的只有十几个像素。光照变化也很极端逆光、阴影、黄昏下的色彩偏移都会出现。这意味着模型不能只依赖颜色特征还得对边缘和形状有稳定的响应。我在对比实验里发现一个只用 3×3 卷积堆出来的小网络在 GTSRB 上能到 90% 以上但换到光线很暗的测试子集上准确率会掉 10 个百分点以上。原因就是小网络过早把注意力放在颜色上对灰度变化下的形状特征提取不够。2.2 Backbone 选型对比从 LeNet 到 ResNetGTSRB 作为经典数据集社区里尝试过的模型非常多。我做项目时把常见 Backbone 跑了个对比这里直接给结果模型输入尺寸参数量GTSRB 验证集准确率单 epoch 耗时RTX 3060LeNet-5 改32×32约 6 万88%~91%约 20 秒自定义 4 层 CNN32×32约 80 万93%~95%约 40 秒ResNet-1832×32约 1100 万96%~97%约 1.5 分钟ResNet-5048×48约 2300 万97%约 4 分钟LeNet-5 结构太浅在 GTSRB 上只能算「能跑」对复杂背景和遮挡的鲁棒性明显不足。ResNet-18 是性价比最高的选择残差连接解决了深层网络退化问题模型能学到更丰富的特征组合而且 1100 万参数在单卡上训练完全没有压力。ResNet-50 的提升主要来自最后一两个点但对大作业和毕设来说训练时间和显存开销不成比例。我的建议是如果只想把所有流程跑通、拿到一个交代得过去的分数自定义 4 层 CNN 就够了如果想把项目做扎实、在报告里谈迁移学习和模型对比直接上 ResNet-18后面调参空间更大。2.3 自定义 CNN 的推荐结构和 PyTorch 实现对于不想引入预训练权重的场景我一般用下面这个结构它是我多轮实验后确定的一个「配方」在 GTSRB 上稳定能到 93% 以上import torch.nn as nn class TrafficSignCNN(nn.Module): def __init__(self, num_classes43): super().__init__() self.features nn.Sequential( # Block 1: 32x32 - 16x16 nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.Conv2d(32, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # Block 2: 16x16 - 8x8 nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.Conv2d(64, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(64 * 8 * 8, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(256, num_classes), ) def forward(self, x): return self.classifier(self.features(x))这段代码的核心是每个卷积块都采用「双卷积 BN ReLU MaxPool」的结构。两个连续的 3×3 卷积感受野等于一个 5×5 卷积但参数量更少、非线性更强BatchNorm 放在卷积和 ReLU 之间能显著缓解梯度消失训练初期收敛速度快很多。Dropout 加在全连接层之前是防止全连接层过拟合的关键——GTSRB 只有 3.9 万张训练图全连接层参数量一大就容易把训练集背下来。参数量约 80 万训练一轮很快CPU 上也能跑。如果你想更稳妥一点把第一个卷积层的输出通道从 32 改成 64准确率能再涨 1 个百分点左右训练时间会翻倍。输入尺寸统一用 32×32这是 GTSRB 官方评估时的标准尺寸所有预处理代码和后面要用的数据增强策略都围绕这个尺寸来设计。2.4 用迁移学习加载 ResNet-18 的两种姿势如果选择 ResNet-18 路线最常见的做法是加载 ImageNet 预训练权重然后微调。但这里有个细节ImageNet 是 1000 类预训练模型最后的全连接层输出是 1000需要替换成 43 类的全连接层。from torchvision import models # 姿势一只替换分类头冻结特征提取层 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) for param in model.parameters(): param.requires_grad False model.fc nn.Linear(512, 43) # 姿势二全部参与训练微调 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(512, 43) # 然后把整个 model 丢进训练循环姿势一适合数据量小、防止过拟合的场景但 GTSRB 有近 4 万张训练图全部解冻微调并不会过拟合效果反而更好。我对比过两组实验冻结特征提取层时准确率约 95.8%全部微调后到 96.9%。差异原因不难理解ImageNet 的特征更偏向自然图像而交通标志是「平面图形 粗线条 高对比色块」底层特征分布有本质区别微调让模型把底层卷积核重新定向到标志类图形上收益非常明显。3. GTSRB.zip 数据处理解压、目录结构、预处理与数据增强3.1 GTSRB.zip 里到底是什么把 GTSRB.zip 解压后目录结构是这样的GTSRB/ ├── Final_Training/ │ └── Images/ │ ├── 00000/ # 类别 0限速 20 │ │ ├── 00000_00000.ppm │ │ ├── 00000_00001.ppm │ │ ├── ... │ │ └── GT-00000.csv # 类别 0 的标注文件 │ ├── 00001/ # 类别 1限速 30 │ ├── ... │ └── 00042/ # 类别 42 └── Final_Test/ ├── Images/ │ ├── 00000.ppm │ ├── 00001.ppm │ └── ... └── GT-final_test.csv # 测试集标注训练集用文件夹名作为类别标签这点和很多课程里用的 CIFAR-10 不同——不需要额外读标签文件直接按文件夹遍历就行。测试集则统一放在 Final_Test/Images 下文件名是纯数字编号对应的类别在 GT-final_test.csv 里每行是「文件名, 类别ID, 其他信息」。测试集每个类别的图像数量差异很大有的类别只有 30 张有的类别有几百张。这会导致最后计算准确率时大类别对总体准确率的影响更大。报告里我会同时给出「总体准确率」和「每类准确率的均值」两个指标后者更能反映模型对不同类别的公平性。3.2 标准化与尺寸统一为什么推荐双线性插值到 32×32GTSRB 原始图片尺寸从 15×15 到 250×250 都有不能直接喂给 CNN。缩放到固定尺寸是必须的环节这里有两个方案方案一直接 resize 到 32×32。简单粗暴但 15×15 的小图放大到 32×32 会明显模糊纹理信息丢失严重。方案二先把小图用双线性插值放到 32×32再对 32×32 以上的大图做中心裁剪/随机裁剪到 32×32。前者保留了大图的信息后者模拟了小图在真实场景中的局部视角。我用方案二做了实验准确率比方案一高 2~3 个百分点。原因在于大部分 GTSRB 图片的实际尺寸在 30~80 像素之间直接用双线性插值到 32 会把很多大图严重压缩圆圈内的数字细节糊成一团。先裁剪再缩放更合理相当于「模拟摄像头在不同距离下看到的画面」——这也是 GTSRB 数据集设计时就埋好的评估点。实现时直接在 PyTorch 的 Dataset 里处理from PIL import Image import torch from torch.utils.data import Dataset from torchvision import transforms class GTSRBDataset(Dataset): def __init__(self, image_dir, transformNone): self.image_paths [] self.labels [] # 遍历每个类别文件夹 for label in range(43): class_dir os.path.join(image_dir, f{label:05d}) for fname in os.listdir(class_dir): if fname.endswith(.ppm): self.image_paths.append(os.path.join(class_dir, fname)) self.labels.append(label) self.transform transform def __len__(self): return len(self.image_paths) def __getitem__(self, idx): img Image.open(self.image_paths[idx]).convert(RGB) label self.labels[idx] if self.transform: img self.transform(img) return img, label # 训练集增强 标准化 train_transform transforms.Compose([ transforms.RandomResizedCrop(32, scale(0.7, 1.0)), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.340, 0.312, 0.321], std[0.198, 0.193, 0.202]), ]) # 验证集只用缩放 标准化 val_transform transforms.Compose([ transforms.Resize((32, 32)), transforms.ToTensor(), transforms.Normalize(mean[0.340, 0.312, 0.321], std[0.198, 0.193, 0.202]), ])这段代码里最值得关注的是RandomResizedCrop它会随机裁剪一块区域再缩放到 32×32相当于模拟摄像头从不同距离、不同角度拍摄同一个标志的过程是提升模型尺度鲁棒性的关键。ColorJitter的做法是随机调整亮度、对比度和饱和度。交通标志的识别在真实场景中极度依赖颜色但对颜色的依赖又不能过强——黄昏/逆光下红色会变暗蓝色会发灰。数据增强里的 ColorJitter 幅度不能设太大否则模型反而学出「颜色变异」的错误规律。GTSRB 的 RGB 均值/标准差我用的是官方统计值如果不想算直接跑一段代码统计整个训练集的通道均值也能用。3.3 DataLoader 的关键设置num_workers、shuffle、drop_last数据准备好之后DataLoader 的参数直接影响训练速度和模型能否收敛。我每次都会检查这四个参数的设置from torch.utils.data import DataLoader train_loader DataLoader( train_dataset, batch_size128, shuffleTrue, num_workers4, pin_memoryTrue, drop_lastTrue, ) val_loader DataLoader( val_dataset, batch_size256, shuffleFalse, num_workers4, pin_memoryTrue, )shuffleTrue是必须的——如果不打乱模型会按类别顺序学习先学完类别 0 的所有样本再学类别 1训练过程会出现剧烈的 loss 震荡。drop_lastTrue是因为最后一个 batch 如果不足 128 张BatchNorm 的统计量会非常不稳定。pin_memoryTrue在 GPU 训练时能减少 CPU→GPU 拷贝时间显存够的情况下建议都开启。一个通常被忽略的坑num_workers 设得太大会在 Windows 上报 DataLoader worker 相关错误设成 0 虽然稳定但速度慢。Linux 上一般 4~8 都安全Windows 上建议 2。我在 Windows 上跑过一次新手项目num_workers8 直接报错改成 2 就好了。3.4 类别不均衡问题GTSRB 的 43 类数量差异很大GTSRB 训练集中类别 1限速 30有 2220 张图类别 41禁止进入只有 240 张图。如果直接用原始分布训练模型会天然偏向头部类别小类别的召回率很低。两种处理方式加权采样每个 batch 按类别逆频率采样让每个类别出现次数差不多。加权 Loss给每个样本的 loss 乘一个权重类别样本数越少权重越高。我实际测试后发现加权采样对收敛稳定性影响更大加权 Loss 则需要特别小心调权重左右横跳容易导致 loss 不降。推荐做法是训练前期用原始分布跑 20 个 epoch 后如果小类别准确率明显偏低再切换加权采样微调几轮。4. 训练与评估超参数设置、训练循环和结果分析4.1 训练集/验证集划分的坑不能直接按文件随机分GTSRB 官方给的是「训练集 测试集」很多同学直接把 Final_Training 当训练集、Final_Test 当测试集开发过程一直在 Final_Test 上调参——这是完全错误的方式测试集只能最后用一次否则模型的「泛化能力」就是假象。正确做法是从 Final_Training 里再切出 10%~15% 作为验证集。注意不能直接random_split要按类别分层采样。如果不分层某些小类别可能整个类别都没进训练集或者验证集验证集准确率会异常偏高/偏低而且完全无法解释。我在项目里用 StratifiedSplit 按 labels 数组 split保证了每个类别在训练集和验证集中都有比例一致。from sklearn.model_selection import train_test_split # 从 Dataset 中取所有标签 all_labels [train_dataset.labels[i] for i in range(len(train_dataset))] # 保证每个类别在训练/验证集中比例一致 train_idx, val_idx train_test_split( range(len(all_labels)), test_size0.15, stratifyall_labels, random_state42, ) from torch.utils.data import Subset train_subset Subset(train_dataset, train_idx) val_subset Subset(train_dataset, val_idx)这里用 sklearn 的train_test_split配合stratify参数一行代码就搞定了分层划分。手动实现分层很容易出错比如某个类别只有 240 张图按 15% 划分只能分到 36 张进验证集如果你用随机划分可能一个不留。用 Subset 包一层之后直接扔给 DataLoader 就行不用改原来的 Dataset 逻辑。4.2 训练超参数怎么定一份可以直接抄的配置表训练 GTSRB 的推荐超参数我整理成了一张表适合大部分课程设计和毕设场景参数自定义 CNN 推荐值ResNet-18 推荐值说明优化器AdamWAdamWSGDmomentum 也可以但收敛慢学习率1e-31e-3微调时 2e-4学习率过大 loss 震荡过小收敛慢权重衰减1e-41e-4防过拟合ResNet 微调可加大到 5e-4Batch Size128128显存不够就 64影响不大Epoch3030到 20 轮后 loss 基本平了学习率调度StepLR每 10 轮 ×0.3CosineAnnealingLR后期 lr 太小反而过拟合更严重LossCrossEntropyLossCrossEntropyLoss类别均衡后不用改标签平滑0.10.1提升泛化防自信过头让我逐个解释为什么这么选。学习率 1e-3 对自定义 CNN 是安全的起点AdamW 的权重衰减和 Adam 不一致直接正则化所有参数泛化更好。ResNet-18 如果从头训不加载预训练权重学习率可以从 1e-3 开始如果加载了 ImageNet 权重做微调请降到 2e-4否则原有权重会被破坏。这是个和训练时间直接相关的问题——用 1e-3 微调 ResNet 两轮后 loss 就崩了重新加载权重、降到 2e-4 才稳定。学习率调度我用 StepLR每 10 个 epoch 把学习率乘 0.3。优点是可控、效果稳定。CosineAnnealing 在训练后期震荡更小但如果 epoch 次数设置不合理收敛会停在半路。GTSRB 数据集不大30 轮足够让验证集准确率走到平台期。标签平滑是容易被忽略的一个设置把 one-hot 标签从 1 改成 0.9目标值从 0 改成 0.1 的均匀分布。作用是不让模型对训练集的分类边界过于自信实测验证集准确率能稳定提升 0.5%~0.8%。算是性价比极高的一个参数。4.3 完整训练脚本与早停机制训练循环本身不难但有几个细节值得注意。每轮记录训练 loss 和验证集 top-1 准确率并且一定要做「早停」——不是等 loss 完全不降才停而是验证集准确率连续 5 轮不刷新最高记录就停止。def train_one_epoch(model, train_loader, criterion, optimizer, device): model.train() running_loss 0.0 correct 0 total 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() running_loss loss.item() * images.size(0) _, predicted outputs.max(1) total labels.size(0) correct predicted.eq(labels).sum().item() return running_loss / total, 100.0 * correct / total def validate(model, val_loader, criterion, device): model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) _, predicted outputs.max(1) total labels.size(0) correct predicted.eq(labels).sum().item() return 100.0 * correct / total # 训练循环 早停 best_acc 0.0 patience 5 wait 0 for epoch in range(30): train_loss, train_acc train_one_epoch( model, train_loader, criterion, optimizer, device ) val_acc validate(model, val_loader, criterion, device) print(fEpoch {epoch1:02d} | Train Loss: {train_loss:.4f} f| Train Acc: {train_acc:.2f}% | Val Acc: {val_acc:.2f}%) if val_acc best_acc: best_acc val_acc wait 0 torch.save(model.state_dict(), best_model.pth) else: wait 1 if wait patience: print(fEarly stop at epoch {epoch1}, best acc: {best_acc:.2f}%) break这里我每次在验证集准确率创新高时保存模型而不是在最后一轮保存。经验是训练后期的某几个 epoch 模型已经过拟合验证集准确率可能在波动最后一轮的状态往往不是最优的。早停保存最优模型能保证你手里始终拿的是验证集表现最好的那个版本。model.eval()和torch.no_grad()的区别值得强调model.eval()是让 BatchNorm 和 Dropout 切换到推理模式用 running_mean 而不是 batch 统计量no_grad()是关闭梯度计算两者必须同时做。如果不调用model.eval()BN 层在验证阶段仍会更新验证准确率会出现「跳变」这个坑很隐蔽需要单独排查。4.4 评估不能只看 top-1 准确率GTSRB 的官方评估指标就是 top-1 准确率但报告里建议补充三个指标能让项目显得更扎实混淆矩阵找出具体哪些类别之间容易混淆。我做完后发现「限速 50」和「限速 80」经常互相误判原因是数字被缩放后笔画密度接近边缘信息丢失后无法区分。每类召回率找出模型对哪些类别特别差。通常是「禁止通行」和「禁止驶入」这类白底红圈且内部符号极小的标志。不确定度分析对预测概率低于 0.5 的样本单独统计这类样本往往对应极端光照或大幅裁切的情况。混淆矩阵直接作用是可以定位数据增强的方向。比如发现 50/80 混淆严重说明需要更多「数字笔画保留率」高的增强策略如果 3/5限速标志混淆严重说明缩放比例没覆盖到某些中间尺度。这比盲目加模型层数有效得多。5. 避坑指南GTSRB 项目里最常见的 5 个翻车点5.1 PPM 格式打不开PIL 报错现象用 OpenCV 的cv2.imread读取 .ppm 文件返回 None或者用plt.imread读出奇怪的颜色。原因.ppm是 P6 格式二进制 RGBOpenCV 的 imread 对部分介质上的 .ppm 兼容性不稳定而且 OpenCV 默认读成 BGR 通道颜色会偏蓝红互换。解决统一用 PIL 读取并在转换后显式转 RGB。from PIL import Image img Image.open(path).convert(RGB)如果你的环境里 PIL 加载 .ppm 仍然有问题用imageio兜底imageio.imread(path)能稳定读取再转成 PIL Image 或 ndarray。这类问题大多出现在预装环境不全的课程实验机器上提前换掉 imread 是最省时间的做法。5.2 DataLoader 的 worker 报错BrokenPipeError现象Windows 下训练到第一轮结束时报BrokenPipeError: [Errno 32] Broken pipe代码不崩溃但输出刷屏。原因num_workers 0时DataLoader 在子进程加载数据Windows 的多进程数据加载和主进程之间的通信机制与 Linux 不同训练循环结束时会触底碰撞子进程退出时管道断裂。解决有两个层级的规避方法。如果只是报错但程序不终止把 num_workers 改成 0或者在训练代码的if __name__ __main__:保护下运行。如果你的训练逻辑写在 Jupyter Notebook 里建议直接把 num_workers 设成 2不要试图在 Notebook 里搞多进程——Notebook 的循环环境对 IPC 支持很差这是血泪经验。5.3 验证集准确率「跳变」静止不动但 training loss 还在降现象训练 loss 持续下降验证集 top-1 准确率却在一两个点范围内反复横跳不升不降。原因这是最常见的「验证集评估设置错误」——验证阶段没有调用model.eval()BatchNorm 层在验证阶段仍然在用当前 batch 的均值/方差进行归一化精心训练的 BN 统计参数被验证 batch 本身的随机性干扰了。解决严格在验证循环里做三件事——model.eval()、torch.no_grad()、验证结束后model.train()恢复训练模式。如果代码结构里这两者没有分开直接看看有没有with torch.no_grad():包裹整个验证循环。这一段代码我反复吃过亏现在写训练脚本会第一时间把两个函数拆开写。5.4 ResNet 微调后 loss 直接爆炸现象加载 ImageNet 预训练 ResNet-18替换全连接层后直接用 1e-3 学习率训练到第二个 epoch loss 从 0.8 直接飙到 5.0然后一路震荡升天。原因1e-3 的学习率对微调场景过大预训练特征在反向传播时被大幅修改底层卷积核全部失稳。解决微调时学习率降到 2e-4或者直接冻结特征提取层只训练最后的全连接层。还有一个更稳妥的折中特征提取层学习率设为分类头的 0.1 倍用参数分组实现optimizer torch.optim.AdamW([ {params: model.parameters(), lr: 2e-4}, {params: model.fc.parameters(), lr: 2e-3}, ], weight_decay1e-4)5.5 测试集准确率和验证集对不上现象验证集准确率 95%跑官方测试集只有 88%差了 7 个点。原因绝大部分情况是验证集划分方式和官方测试集的数据分布不一致。GTSRB 的 Final_Test 和 Final_Training 采集自不同路段、不同时段本身就存在轻微的 domain shift另一个常见原因是测试集图像不做任何预处理尺寸直接是原始大小没有走 Resize 和 Normalize。解决评估测试集时代码必须和验证集预处理完全一致——同一个 transform 对象、同一个 resize 尺寸、同一套 mean/std。另外我在项目里发现 Final_Test 中真正占比高的是小尺寸图片小于 32×32 的占比不低如果验证集里大图比例偏高验证准确率虚高测试集准确率必然回落。所以划分验证集时要按「尺寸分布」也做一次分层否则这个 7 个点的差距会一直存在。6. 进阶验证从精确率到模型可解释性和部署当模型在验证集和测试集上都稳定在 96% 左右之后下一步值得做的事是把项目从「能跑」升级到「能讲清楚」。首先建议用 Grad-CAM 可视化模型注意力。GTSRB 的类别区分点在数字、内部符号等局部区域可视化结果能直接验证模型「看得对」——如果注意力热力图集中在标志边缘或背景说明模型在利用捷径学习遇到真实场景会翻车。具体做法是用一个 Grad-CAM 库把某个限速标志输入模型查看最后一个卷积层的梯度定位热力覆盖区域。这一步不是可选项我在做答辩演示时两组模型一个注意力集中在数字区域、一个集中在红圈上评委的问题立刻就分出了高下。其次可以用混淆矩阵里的具体混淆对做针对性增强。比如发现 50/80 总是混淆就手动把这两类的图像放大/缩小后各自的样本翻一倍再重新训练。这种定向数据增强的效果比盲目增加随机增强强度更精准而且能让报告里的分析章节有实际案例支撑。最后如果项目时间充裕把模型导出成 TorchScript 或者 ONNX再做一次简单的实时推理演示——用摄像头对准电脑屏幕上的交通标志图片看模型能不能实时识别。这个环节和课程设计演示结合起来效果很好观众能看到模型在真实场景里的反应速度。操作上先用torchscript.jit.trace追踪模型再用OpenCV拉摄像头流每帧做一次前向推理模型输入尺寸仍保持 32×32单帧推理时间在 CPU 上也就 5~10ms。我自己的习惯是把每次实验的超参数和结果记在表格里最后写报告时直接引用这些记录。包括「学习率 1e-3 但是 BN 层没有冻结导致验证集不涨」「测试集没做标准化导致准确率虚低」这类翻车记录也留着——这些经验反而比那个 96% 的准确率更有说服力。做这个项目的最大收获不是模型本身而是建立了一套「改参数 → 观察 → 定位问题 → 再改」的闭环思路。希望帮到你。本文还有配套的精品资源点击获取