交通标志识别这个题目我一开始是真没放在心上。当时选毕设的动机很实在数据集是公开的任务目标一目了然还能做出一个看得见摸得着的Web系统答辩时容易讲。结果做下来才发现从数据集清洗到模型训练再到系统部署每一步都比预想中多出一堆问题。第一次跑通模型时验证集准确率只有91%距离能拿得出手差得很远后来一路调数据增强、调结构、调学习率才爬到98%左右。这篇文章就把整个项目的完整链路拆开讲一遍——为什么这么选题、数据集怎么处理、模型怎么从零搭起来、Web端怎么接、LW文档怎么写到能过盲审中间踩过的坑也会一一列出来给后面选这个方向的学弟学妹当个参考。1. 选题的价值判断为什么交通标志识别适合作为计算机毕业设计1.1 课题本身的意义与工程落地价值交通标志识别Traffic Sign Recognition, TSR是智能驾驶系统里非常基础的一环。车辆在行驶过程中通过摄像头采集道路画面系统需要从画面中找出标志牌并判断它是限速标志、禁止标志、警告标志还是指示标志再把这些信息反馈给驾驶员或者自动驾驶决策模块。在L2/L3级别的辅助驾驶中限速提醒、违章预警这些功能都依赖它。站在毕业设计的角度这个方向的好处是任务边界非常清晰输入是一张图片输出是标志类别比如限速50km/h、禁止左转不像目标检测或者语义分割那样需要同时处理多任务评价指标也非常明确——准确率、混淆矩阵、单帧耗时都是答辩时能直接拿出来的硬指标。1.2 毕业设计场景下的适配性分析很多同学选课题时会纠结做得太偏理论怕做不出来做得太偏应用又怕深度不够。交通标志识别这个题目刚好卡在中间。它既有算法层面的东西——卷积神经网络如何提取边缘、颜色、形状特征也有工程层面的东西——数据怎么清洗、模型怎么部署、前后端怎么对接。相比纯粹训练一个模型然后丢几个准确率数字的算法型毕设系统设计与实现这个定位要求你交付一个完整的闭环登录系统、上传图片、返回识别结果、展示置信度这套流程做下来工作量是肉眼可见的。而且交通标志的类别有明确的中文含义系统界面可以做得非常直观演示效果比抽象的猫狗分类或者手写数字识别要好看得多。1.3 传统方法与深度学习方案的关键差异我在文献综述阶段专门把传统方法翻了一遍。经典路线是颜色分割HOG特征SVM分类器比如先用HSV颜色空间检测红色/蓝色区域把可能的标志牌区域裁剪出来再提取方向梯度直方图特征最后喂给SVM做分类。这套方法在早期研究中效果不差在德国GTSRB数据集上也能做到90%上下但它有硬伤一是依赖手工设计的特征光照一变、遮挡一多鲁棒性就差二是检测和识别分成了两个独立环节误差会累积。深度学习方案直接用卷积神经网络做端到端分类特征是网络自己学出来的颜色、纹理、形状信息在网络里融合得更好。我最终的实测对比也验证了这一点同样的测试集上传统方法91%左右自定义CNN可以做到97%以上。2. 数据集的选型与预处理决定模型上限的地基工程2.1 公开数据集的选择逻辑这个项目里最关键的决策就是数据集选哪个。我对比了三个主流公开数据集数据集来源类别数样本量特点GTSRB德国43约5万张类别覆盖全图像尺寸不统一学术界最常用TT100K中国45常用子集约10万张真实中国场景自带检测框类别严重不均衡LISA美国47约6600张视频帧提取样本量较小我最后选了GTSRB。原因有三点第一43个类别覆盖了限速、禁令、危险、指示等常见大类类别足够丰富能体现分类难度第二它是计算机视觉领域公认的benchmark相关论文多遇到问题容易查资料第三TT100K虽然是中国场景但类别分布极不均匀有的类别只有几十张图训练时很容易出现过拟合处理起来的复杂度不适合毕设周期。如果你坚持用TT100K做中文场景我后面会单独说处理长尾分布的办法。2.2 图片尺寸不统一的问题与归一化策略GTSRB有个容易忽略的坑原始图片尺寸非常不统一小到15x15像素大到250x250像素。直接把这些图片扔进网络是不行的因为卷积层之后的全连接层要求输入尺寸固定。我做了个实验把图片统一缩放到32x32和64x64分别跑了一遍64x64的准确率能高出约3个百分点。原因是交通标志里的文字细节在低分辨率下会被抹掉比如限速30和限速80的差异数字轮廓一旦模糊网络就只能靠猜。所以在预处理阶段我统一将图片Resize到64x64。这里要特别注意缩放算法不要用最近邻插值要用双线性插值cv2.INTER_LINEAR否则会出现严重的锯齿引入额外的噪声。除了尺寸归一化也是必要的。原始图片像素值是0到255的整数直接送入网络会让梯度更新不稳定。我按常见做法把像素缩放到0到1之间并没有做更复杂的标准归一化。在PyTorch的Dataset里每一步的预处理逻辑是固定的先读图、转RGB、Resize、转float32、除以255、转Tensor。下面这段代码是核心部分import cv2 import torch from torch.utils.data import Dataset import os class GTSRBDataset(Dataset): def __init__(self, image_dir, labels_file, transformNone): self.image_dir image_dir self.samples self._load_labels(labels_file) self.transform transform def _load_labels(self, path): # 每行: 文件名, 类别ID samples [] with open(path, r, encodingutf-8) as f: for line in f.readlines()[1:]: # 跳过表头 parts line.strip().split(,) samples.append((parts[0], int(parts[1]))) return samples def __len__(self): return len(self.samples) def __getitem__(self, idx): img_name, label self.samples[idx] img_path os.path.join(self.image_dir, img_name) img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (64, 64), interpolationcv2.INTER_LINEAR) img img.astype(float32) / 255.0 img torch.from_numpy(img).permute(2, 0, 1) # HWC - CHW return img, label2.3 数据增强的力度与使用边界第一版模型在验证集上准确率能到93%但训练集准确率已经接近98%明显是过拟合了。交通标志数据量不算大网络又比较容易记住训练集中的噪声数据增强是必须的。我用torchvision的transforms组合做了一套增强策略transform_train transforms.Compose([ transforms.RandomRotation(10), transforms.RandomAffine(degrees0, translate(0.1, 0.1)), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.RandomHorizontalFlip(p0.0), # 交通标志不能水平翻转必须设为0 ])这里有个极易踩坑的点水平翻转对大多数图像分类任务有效但交通标志不行。比如限速标志里的数字本身就不对称禁止超车这种标志翻转后含义完全不同所以RandomHorizontalFlip一定要关闭。类似的还有90度旋转标志牌很少倒着出现旋转角度控制在±10度以内比较合理。ColorJitter模拟的是不同光照和拍摄设备下的颜色偏差这个对鲁棒性提升很明显。增强力度太大会导致样本失真网络学到的特征偏离真实分布我自己试过把旋转角度调到30度准确率反而下降了。我最后采取的是训练集用增强验证集和测试集只用Resize和归一化保证评估结果可信。3. 模型结构与训练策略准确率从91%到97%的调参过程3.1 为什么选择自定义CNN而非直接迁移ResNet刚开始我也想过直接用ResNet18或者VGG16预训练模型做迁移学习省事。但做了两轮实验之后放弃了原因很具体第一毕业设计用的电脑大概率没有强力GPUVGG16在CPU上训练一轮就要几十分钟反复调参的效率太低第二GTSRB图片是64x64的小图包含的结构信息远没有ImageNet那种复杂场景多用几十层的深度网络属于杀鸡用牛刀还会带来严重的过拟合风险第三答辩时老师大概率会问你网络结构为什么这样设计用自定义CNN可以一条一条讲清楚每个卷积层的意义而搬一个ResNet只能说别人这么设计的、效果好说服力差很多。当然我在论文里也放了和迁移学习方案的对比实验用数据证明在这个小数据集上自定义CNN的性价比更高。3.2 网络结构设计思路与参数量我的自定义CNN结构不复杂核心思路是卷积层逐层加深、特征图逐层减小最后接全连接层分类import torch.nn as nn class TrafficSignNet(nn.Module): def __init__(self, num_classes43): super().__init__() self.conv_layers nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.fc_layers nn.Sequential( nn.Flatten(), nn.Linear(128 * 8 * 8, 512), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(512, num_classes), ) def forward(self, x): return self.fc_layers(self.conv_layers(x))三组卷积层的输出通道分别是32、64、128每次下采样后特征图尺寸从64x64变成32x32再变成16x16最后到8x8。这么设计的目的是第一层卷积提取低级特征边缘、角点、颜色块越往后越抽象组合边缘形成形状比如三角形、圆形、红圈斜杠所以通道数递增是有道理的。在卷积层之间加了BatchNorm它在训练时能把每层的输入分布拉回标准分布模型收敛快很多。最后全连接层前加了Dropout比例为0.5这是缓解过拟合的关键一环。整个模型参数量大约104万单张图片在CPU上推理只要几十毫秒部署到Web后端毫无压力。3.3 训练参数的选择依据与调参曲线训练超参数我记录了一版比较成功的配置优化器用Adam初始学习率1e-3批大小64训练40个epoch学习率在20个epoch时乘以0.1进行衰减。为什么用Adam而不用SGD因为Adam自带自适应学习率对初始学习率的敏感性低很多对第一次做深度学习项目的人来说更友好SGD需要精心设计学习率策略调起来太费时间。学习率衰减是必要的刚开始大学习率让loss快速下降后期小学习率精细收敛避免在最优解附近震荡。我第一次训练时没有做衰减准确率一直在96%左右上不去加了衰减后很快突破97%。训练过程中我从训练集里划出10%作为验证集每个epoch结束后都评估一次。通过观察loss曲线能发现不少问题如果训练loss和验证loss都高说明模型欠拟合需要加深网络或者增加训练轮数如果训练loss一路走低但验证loss升高就是过拟合需要增强数据或加大Dropout比例。我的实验里BatchNorm层对Loss稳定性的帮助非常明显去掉它之后loss曲线波动幅度很大而且准确率低了2个点左右。最终的测试集结果是总准确率97.8%在43个类别里有35个类别的F1值在0.95以上。表现最差的是限速80和限速100之间、部分禁令标志之间因为外观本身就很接近这种混淆也是符合预期的。4. 系统设计与工程实现从模型权重到可用的Web系统4.1 系统的整体架构与模块划分模型训好之后剩下的工作是把模型变成一套能让老师亲手上手点的系统。我的系统采用B/S架构前端是HTMLCSSJavaScript页面后端是Python的Flask框架模型推理模块封装成一个单独的类。为什么用Flask不用Django因为这里后端只需要提供几个接口路由逻辑非常简单Flask足够轻量写起来也快。为什么不用前后端分离毕业设计场景下没必要引入Node.js和Vue那一套服务端渲染页面加上原生Ajax请求逻辑清晰部署也简单。系统的核心模块有四个用户管理模块普通登录验证用户身份、图像上传模块接收客户端图片并校验格式、推理服务模块加载模型、预处理图像、返回Top-3识别结果、结果展示模块展示类别名称、置信度条形图。整个系统跑在本地Flask开发服务器上也支持部署到云服务器。4.2 模型部署与推理接口的实现细节模型训练阶段用的PyTorch部署的时候直接保存为.pth权重文件加载。这里有个小坑如果训练时用了GPU保存的权重也会带着GPU相关的state_dict在没有CUDA的机器上直接torch.load会报错。正确的做法是保存时指定map_locationmodel TrafficSignNet(num_classes43) checkpoint torch.load(best_model.pth, map_locationcpu) model.load_state_dict(checkpoint[model_state_dict]) model.eval()必须强调model.eval()这行虽然训练过网络的都知道但新手经常忘。不切换到eval模式的话BatchNorm层和Dropout层会继续使用训练时的行为导致同一个输入每次推理的结果都不一样。Flask的推理接口实现如下这段代码是系统后端的核心app.route(/predict, methods[POST]) def predict(): file request.files.get(file) if file is None: return jsonify({error: 未接收到文件}), 400 img_bytes file.read() img cv2.imdecode(np.frombuffer(img_bytes, np.uint8), cv2.IMREAD_COLOR) if img is None: return jsonify({error: 图片解析失败}), 400 img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (64, 64), interpolationcv2.INTER_LINEAR) img img.astype(float32) / 255.0 img torch.from_numpy(img).permute(2, 0, 1).unsqueeze(0) with torch.no_grad(): logits model(img) probs torch.softmax(logits, dim1).squeeze(0).cpu().numpy() top_k np.argsort(probs)[::-1][:3] result [ { class_id: int(i), name: class_names[int(i)], probability: float(probs[i]) } for i in top_k ] return jsonify({success: True, predictions: result})我额外做了一步softmax之后返回Top-3的结果这个设计在演示时很加分能让老师直观看到网络对相近类别的置信度分布甚至能解释模型为什么在某些样例上判错。4.3 前端交互与置信度可视化前端页面我保持了简洁风格左侧是上传区域支持拖拽和点击两种方式选择图片后立刻预览右侧是预测结果面板显示三个候选类别以及对应的置信度条形图。条形图是用简单的CSS宽度百分比实现的不需要引入图表库。请求通过fetch发送FormData代码量不多但演示效果很好。async function predictImage(file) { const formData new FormData(); formData.append(file, file); const resp await fetch(/predict, { method: POST, body: formData }); const data await resp.json(); renderResult(data.predictions); }这里有个体验上的细节点原始图片可能很大上传后在线Resize但要在前端把预览图样式限制在固定尺寸避免排版错乱同时应在后端限制文件格式jpg/png/bmp和大小防止用户传一个几十MB的图片把服务拖垮。4.4 系统联调时遇到的几个工程问题联调阶段最让我头疼的问题是模型加载慢。每次启动Flask后第一个请求要等好几秒因为PyTorch在第一次推理时才真正完成部分初始化和内存分配。解决方法是提前加载在Flask应用启动时把模型加载到全局变量而不是在请求函数内加载。另一个是在Windows环境下OpenCV的imread读中文路径会失败虽然这里用的是上传流不是文件路径但如果改成读取本地文件夹做批量测试就会踩这个坑建议后端统一用cv2.imdecode读取字节流彻底避免中文路径问题。5. LW文档毕业论文撰写的结构要点与盲审准备5.1 论文整体结构怎么搭LW文档是这个项目里占比很重的一部分如果说代码决定了你能不能做出系统那么文档直接决定了你能不能毕业。我最终提交的论文结构是七章绪论、相关技术基础、需求分析、系统设计、系统实现、系统测试、总结与展望。这里的核心经验是需求分析和系统设计两章不能糊弄很多老师会重点看这两章。需求分析要写清楚功能性需求用户登录、图片上传、识别展示和非功能性需求准确率指标、响应时间、易用性并且用用例图辅助说明。系统设计要画清楚总体架构图、功能模块图、数据库ER图、关键流程图这部分工作量其实不小建议在开发前先画而不是事后补。5.2 各章节的篇幅分配与写作技巧论文章节建议篇幅写作要点绪论10%-12%研究背景、国内外现状、论文组织结构相关技术基础12%-15%CNN原理、PyTorch框架、Flask框架注意不要照搬教材需求分析12%-15%用例图、功能需求、性能需求系统设计15%-20%架构图、模块设计、数据库设计、模型结构设计系统实现20%核心代码片段和关键界面截图重点讲算法和接口系统测试10%-12%功能测试用例表、准确率测试、图表分析总结与展望5%项目总结、改进方向写相关技术基础那一章时最容易犯的错误是变成百科全书式的堆砌从神经网络历史写到反向传播公式洋洋洒洒几千字但和项目毫无关系。我的建议是只写项目里真正用到的技术并且在介绍每个技术后加一句在本系统中它用在什么地方。例如BatchNorm可以写本系统的网络在每层卷积后引入BN层目的是加快收敛速度、稳定训练过程。这样写作时方向明确盲审老师看起来也不会觉得空洞。5.3 实验数据的呈现方法与图表规范测试部分是答辩老师最喜欢翻的地方。我用三张图呈现实验结果第一张是训练和验证集的loss曲线证明模型收敛且没有严重过拟合第二张是混淆矩阵热力图展示哪些类别容易混淆第三张是不同模型方案的柱状对比图把自定义CNN、传统HOGSVM、迁移学习ResNet放在一起比较。这三张图一放整个论文的工作量感就出来了。另外我还做了一张分类别准确率表列出43个类别中准确率最低的5个类别和可能的原因分析这种自我批评式的细节非常能体现做事的认真程度在我答辩时老师也特别提到了这部分。5.4 答辩高频问题与应对思路答辩环节老师的问题其实有迹可循我把被问到的和听说的问题整理了一下卷积核为什么用3x3而不是5x5回答3x3在相同感受野下参数量更少而且多层3x3堆叠能获得更大的感受野同时引入更多非线性比直接用大卷积核效果更好。BatchNorm的原理是什么回答BN在训练时对每个batch的每个通道做归一化把数据拉回均值为0方差为1的分布同时引入了两个可学习参数缩放和平移来恢复表达能力有效缓解了内部协变量偏移问题模型可以用更大的学习率加速收敛。系统在什么场景下会识别失败回答光照过暗、标志牌被遮挡超过50%、运动模糊严重的情况下都会失败另外类别接近的限速标志之间可能混淆比如限速80和限速100。这个问题要答得诚实承认系统边界会比硬说模型性能很好给老师留下更好的印象。为什么不用YOLO做目标检测识别回答毕设定位是分类识别数据集GTSRB提供的是裁剪好的标志区域没有检测任务的需求如果换成TT100K这种带完整场景图的数据集就有必要引入检测模型了。6. 实测中的踩坑记录与后续优化空间6.1 环境配置阶段的典型问题环境问题看起来不算技术难点但能卡掉不少人。PyTorch安装时务必根据CUDA版本选择合适的安装命令如果电脑没有NVIDIA显卡就老老实实装CPU版不要强行装GPU版然后发现cannot import name cuda。Windows平台下Python建议用3.8或者3.9过高版本的Python在安装旧版torchvision时经常出现依赖冲突。还有一个隐藏比较深的问题装好环境跑训练脚本时报OMP Error或CPU线程相关错误是因为OpenMP运行时与PyTorch冲突在代码开头加上os.environ[KMP_DUPLICATE_LIB_OK]TRUE即可绕过。这些看起来小但对第一次接触深度学习环境的同学来说哪一个都能耗掉半天时间。6.2 训练和部署阶段的针对性避坑清单我在整个项目里踩过的坑按照发生阶段整理了一份清单都是真实记录阶段问题现象根因与解决方案数据加载训练时内存持续上涨num_workers设为0不需要启用多进程数据加载数据加载验证集效果差但训练集正常训练集做了增强验证集也要用相同的Resize和归一化模型训练Loss下降缓慢甚至不下降检查数据是否做了归一化尝试把学习率从1e-3调为5e-4模型训练准确率卡在92%上不去加入学习率衰减step/scheduler检查是否有样本标签错位模型推理同一张图每次结果不同model.eval()没调用Dropout和BN仍在训练模式系统部署第一个请求响应很慢模型在路由函数外全局加载或者用Flask的before_first_request系统部署上传图片返回400OpenCV读帧失败大多数是上传的文件不是有效图片加异常处理6.3 后续可以延伸的方向如果时间和精力允许这个项目还有三条很不错的延伸路线。第一条是引入目标检测把裁剪好的静态图替换为真实的道路场景图用YOLOv5或者Faster R-CNN先检测出标志位置再分类系统从图片识别升级为视频实时识别工程量和技术含量都会上一个台阶。第二条是模型轻量化把自定义CNN替换成MobileNetV3或者ShuffleNet模型体积压缩到几MB级别还能考虑用ONNX Runtime做CPU推理加速这样整个系统可以部署到树莓派之类的嵌入式设备上。第三条是加入难例挖掘机制把模型预测错误的样本收集起来重新放回训练集进行增量训练进一步提升在恶劣光照、运动模糊等困难场景下的鲁棒性。如果是为了论文的工作量任选一条都能增加一到两章的篇幅。这个项目做下来我个人最大的体会是交通标志识别这类课题拿到一个跑得通的模型不是终点把模型变成一套能演示、能测试、能写清楚原理的系统才是完整闭环。如果你正在做或者准备做这个方向先别急着调参把数据集和评价标准吃透后面每一步都会顺很多。特别是模型训练部分一定要保存中间过程的曲线和结果数据不然后面写论文时还要重新跑一遍实验那就真的欲哭无泪了。