
每年到这个时间点总有不少准备毕业设计的同学来问我同一个问题“老师给了一个深度学习的方向让我用 Python 和 PyTorch 训练一个模型识别图片里有没有积水区域这到底该怎么做”这个问题听起来简单但真动手时会发现里面全是细节数据集怎么来、标签怎么打、模型怎么选、训练到底能不能收敛最后还要做出一个像样的系统应付答辩。这篇文章就把这个毕设方向从头到尾拆解一遍把我实际带项目时用到的一套流程、参数和避坑方法全部写出来给准备做这个方向或者正卡在某一步的同学一个完整的参考。这篇内容适合的读者范围很广正在选题的本科生、刚入门 CV 的研究生甚至所有想拿一个真实的图像分类或分割项目练手的人。积水区域识别本身是一个典型的二分类问题稍微升级一步就成了语义分割问题两个版本我在下文都会讲到。你不需要很深的数学基础只要会用 Python、装过 PyTorch就能按着下面的步骤一步步复现。更重要的是我会告诉你哪些地方最容易翻车以及为什么这些地方会翻车——这些才是项目能真正做下去的关键。1. 项目解读与思路选择积水识别到底做什么1.1 一个看似二分类的问题实际有两条技术路线题目里写着“识别是否是积水区域”大部分人第一反应是这不就是给模型一张图让它判断图片里有没有积水吗严格来说这是一个二分类问题输出 0 或 1或者输出“有积水”的概率。这种方案最简单适合毕设的时间安排也容易出效果。但我建议你在写开题报告之前先想清楚一个问题你需要的到底是“这张图里有没有水”还是“水在图片的哪个位置”。如果老师给的题目里出现了“区域”两个字往往隐含着分割的意思。同样是这个毕设题做分类和做分割工作量差了一个量级。分类只需要给每张图打一个标签训练起来快调参也简单分割需要给图片里每一个像素标注是否属于积水工作量主要花在数据标注上但展示出来的效果会好很多答辩时也更有说服力。我通常给同学的建议是如果时间只有三个月左右优先做二分类再在分类模型的基础上用热力图或者滑窗做一个粗糙的定位如果时间充裕或者题目里明确写了“像素级识别”“积水区域提取”那就直接上分割模型。两条路线并不是对立的分类版做扎实了后续把最后一层改成分割输出头也是顺理成章的事。1.2 为什么用深度学习而不是传统图像处理很多人问过这个问题积水不就是水吗用颜色阈值或者边缘检测能不能做说实话传统图像处理不是完全不行但极其脆弱。水的颜色不稳定蓝天倒影进去是蓝的路灯照上去是黄的沙土路面泡了水是暗色的几乎不可能用固定的 RGB 阈值来概括。更麻烦的是积水和柏油路的边界很多时候是渐变的倒影、阴影、水渍、油膜用边缘检测会把这些东西全部圈进来误报率会高到没法用。深度学习模型能做对这件事的核心原因不在于“更聪明”而在于它通过大量数据自动学习了一组高维特征能同时兼顾颜色、纹理、上下文关系。打个比方传统方法像是一个只看颜色的门卫深度学习像是一个认识整条街的老住户判断一片区域像不像积水看的不仅仅是“这地方颜色暗”还包括“周围是路面还是草地”“上方的天空亮度如何”这些综合信息。从毕设的角度来说使用深度学习还有一个实际好处框架成熟预训练模型多哪怕你自己的数据量不大用迁移学习起步也能拿到不错的效果。这不叫偷懒而是工程上合理的做法后面我会专门讲迁移学习的具体操作。1.3 写成毕设的常见加分思路一个只做到“能分类”的毕设在老师眼里大概率只是及格水平。想让项目更有亮点可以考虑加几个方向一是用 Grad-CAM 生成热力图直观展示模型是根据哪些像素判断出积水的这几乎是白送的加分项代码量也不大二是模型轻量化对比 ResNet 和 MobileNet 在速度与精度上的取舍分析不同场景下的适用性这体现的是工程思维三是做实时检测用推理脚本配合摄像头或者视频流动态判断道路积水情况把静态识别变成动态监测。这些扩展方向我在文章最后会详细写先提出来是让你在规划阶段就留好接口不要在数据准备的时候把路堵死。2. 数据是关键积水数据集怎么建、怎么标2.1 数据来源自采、网络与公开数据集怎么搭配图像识别项目里流传着一句话数据决定了模型效果的上限模型只是努力逼近这个上限。积水识别尤其如此因为积水场景非常多样数据不够多样的话模型会学到一堆虚假的规律。数据的第一个来源是自己采集这是我最推荐的方式。不需要专业设备手机摄像头就够在雨后去拍城市道路、小区路面、停车场、农田排水沟注意拍的时候要覆盖不同距离、不同角度。如果条件允许行车记录仪的视频也可以用来抽帧一段视频能抽出大量有效图片。自己采集的好处是数据分布可控你能记录拍摄时的天气和场地情况后期筛选方便。但要注意所有自采数据都要处理隐私问题不要拍到车牌号码、人脸、门牌号等敏感信息可以后期裁剪掉再使用。第二个来源是网络搜集。可以去图片网站搜索“道路积水”“路面水洼”等关键词或者利用开源爬虫工具批量下载。这里有几个注意事项一是尽量避免直接使用带明显水印的图片二是网络图片内容杂必须人工清洗三是来源要整理好写在毕设论文的数据部分注明来源渠道。第三个来源是公开数据集。严格来说专门做“积水识别”的公开数据集很少但你可以用“道路场景分割数据集”来补充这类数据集的标签体系里往往包含“water/pond”之类类别的话最好如果没有可以用其中的道路场景图片通过二次标注来做积水负样本。公开数据的优点在于量足、分布广缺点是很多是国外场景和国内道路状况有差异所以最好只作为补充不能完全替代自采数据。2.2 标注规范先回答清楚“什么是积水”标注是积水识别项目里最容易被低估的环节。很多同学拿着图片就开始画框打标结果数据集的一致性很差模型训练出来准确率很高换一批图片就崩溃。问题往往不出在模型而是标签本身混乱。你需要先定一套明确的标注规则。比如地面能明显看到水体反光且水体区域大于某个像素面积比如整张图的 3%才标为“有积水”反光不明显、只有润湿痕迹的路面统一标为“无积水”雨水落在水面上形成涟漪但水体区域清晰可见的标为“有积水”积雪区域不标泳池、河流这类非道路水体根据你的项目定位决定是否保留。规则定下来之后我的习惯是找一个有经验的人先标 30 张然后全体标注人员一起过一遍统一口径再开始批量标注。分类和分割两类的标注工具不一样二分类只需要在电子表格里记录文件名和标签快得很分割标注要逐像素画掩码推荐用开源标注工具画线闭合多边形导出为 JSON 再转成掩码图片。分割标注非常费时间一张图花十分钟很常见所以在做分割之前先评估一下自己的时间预算。标注完成后一定要做质量抽检。随机抽 10% 的图片对照标注文件和原图人工检查发现边界划错、漏标、标签贴反的图片要退回去重标。这一步会直接影响模型的精度上限我见过太多同学因为标注问题后期疯狂调参回头一看才发现是数据集有几百张错误标注。2.3 数据增强技巧让模型扛住下雨、夜晚和倒影积水识别的数据增强有非常强的现实背景。现实中的积水图片光照条件差异极大晴天强光下有强烈反光阴雨天光线昏暗夜间有路灯的黄色光斑这些都会让模型困惑。如果训练集中晴天照片占了绝大多数模型极有可能学到“画面亮无积水”这种错误规律。基础的数据增强包括随机水平翻转、随机裁剪、随机旋转、颜色抖动。颜色抖动这里要专门做把亮度、对比度、饱和度的随机扰动范围调大一些模拟不同天气条件下的视觉差异。随机遮挡和 Cutout 也对积水识别有效因为积水区域往往是不规则连片的随机擦除一部分可以让模型不依赖某一个局部特征。还有两个针对性更强的增强策略。一是加随机雨滴模糊用高斯模糊模拟镜头上的水雾这能让模型在真实雨天场景里更鲁棒二是 Mixup把有积水和无积水的图片按比例混合标签也对应线性混合这个策略能显著降低模型的过拟合在很多分类任务里都有用。需要注意增强不能过分比如颜色抖动太大会让图片失真模型反而学不到有效特征一般亮度扰动系数在 0.5 到 1.5 之间饱和度类似不要拉到极端。3. 模型与训练从 ResNet 到分割网络的选型实战3.1 二分类选什么ResNet18 还是 MobileNetV3做图像二分类我的首选永远是 ResNet18除非你的电脑实在跑不动。原因很简单ResNet18 结构经典、参数量适中、预训练权重好找在积水识别这种相对简单的任务上效果已经足够好。ResNet 的核心思路是残差连接简单说就是允许网络在训练过程中选择“跳过”一些不必要的层这解决了深网络难训练的问题也意味着你不需要从零调一个复杂的结构。如果你后续要做嵌入式部署或者想体现轻量化的对比分析可以再拿 MobileNetV3 或者 ShuffleNetV2 做对比。MobileNet 系列用深度可分离卷积大幅减少了运算量体积小、速度快代价是精度略降低。在毕设里最常见的操作是用 ResNet18 作为 baseline再用 MobileNetV3 做对比实验整理成一张速度-精度表格这部分内容非常出文章篇幅也很有说服力。还有一种更轻的思路是直接用 torchvision 里现成的分类模型从头训练。但我不建议完全从头训练后面会讲原因。3.2 想输出“哪里积水”直接上 UNet如果决定做分割那就绕不开 U-Net。U-Net 是一种编码器-解码器结构编码器逐步下采样提取特征解码器逐步上采样恢复分辨率中间用跳跃连接把下采样前的细节特征拼回上采样过程这样输出的分割图就能保留较多的边缘细节。积水区域边界模糊、不规则这个特点是 U-Net 能胜任的原因之一。U-Net 的 backbone 可以换成 ResNet34 或者 MobileNetV3这样能利用预训练权重加速收敛。如果你想更进一步DeepLabV3 也是一个常见选项它通过空洞卷积扩大感受野对大面积积水区域的完整性会更好但显存占用也更高。本科毕设建议从 U-Net 起步因为你还需要花大量时间处理标注模型的推倒重来成本太高。3.3 训练超参数与迁移学习细节训练积水分类模型我常用的超参数如下输入图片尺寸 224×224和 ImageNet 预训练一致batch size 一般是 16 或者 32优化器用 Adam初始学习率 1e-3权重衰减 1e-4。如果显存不够把 batch size 降到 8 也可以对应地把学习率调低到 5e-4。这里要说一个关键点图像预处理必须和预训练模型的预处理保持一致。torchvision 里的模型默认期望输入图片按 ImageNet 的均值和标准差做归一化也就是用[0.485, 0.456, 0.406]和[0.229, 0.224, 0.225]这两个固定值。很多人忘记这一步或者搞错顺序导致模型训练半天 loss 不降检查半天发现是输入数据分布不对。迁移学习有两种典型策略。第一种是冻结骨干网络只训练最后的全连接分类层此时学习率可以稍大训练的 epoch 不用太多通常 20 到 30 个就够了第二种是全部参数微调需要更小的学习率比如分类层用 1e-3骨干网络用 1e-4 或 1e-5这样既能微调底层特征又不会破坏预训练学到的通用表示。我建议的做法是分两阶段先冻结 backbone 训练 10 个 epoch 左右让分类头先收敛然后解冻 backbone用小学习率整体微调 20 到 30 个 epoch。这个流程在多个项目里都非常稳。3.4 训练流程示例代码下面给出一段完整的二分类训练流程示例代码基于 PyTorch 的标准写法。这里省略了数据读取类的具体实现重点展示模型构建和训练循环的结构。import torch import torch.nn as nn import torch.optim as optim from torchvision import models, transforms from torch.utils.data import DataLoader, Dataset from PIL import Image import os class WaterDataset(Dataset): def __init__(self, img_dir, labels, transformNone): self.img_dir img_dir self.labels labels # {filename: 0 or 1} self.filenames list(labels.keys()) self.transform transform def __len__(self): return len(self.filenames) def __getitem__(self, idx): img_path os.path.join(self.img_dir, self.filenames[idx]) image Image.open(img_path).convert(RGB) label self.labels[self.filenames[idx]] if self.transform: image self.transform(image) return image, label train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.5, contrast0.5, saturation0.5), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 分类头替换二分类输出为 2 device torch.device(cuda if torch.cuda.is_available() else cpu) model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) num_features model.fc.in_features model.fc nn.Linear(num_features, 2) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) # 假设 train_loader 和 val_loader 已经构建好 for epoch in range(30): model.train() total_loss 0.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() total_loss loss.item() print(fEpoch {epoch1}/{30}, Loss: {total_loss/len(train_loader):.4f}) model.eval() correct, total 0, 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() print(fVal Accuracy: {correct/total:.4f})这里有一个细节值得解释为什么在Resize((256, 256))之后还要RandomResizedCrop(224)因为积水图片中目标区域的大小变化很大随机裁剪相当于模拟了不同距离下看到的路面等于免费又做了一次数据增强。验证集则直接缩放到 224保持评估的稳定性。4. 训练常见问题与效果评估4.1 准确率 99% 却不能用类别不平衡与过拟合这是积水识别项目里最经典的一个坑。你有 1000 张图片850 张没有积水150 张有积水如果不做任何处理直接训练模型完全可能把所有图片都判定为“无积水”因为这样准确率也有 85%。如果你发现训练结束后准确率到了 99%但拿出去实测遇到有积水的图片经常漏掉基本就是这个原因。解决这个问题的标准做法有三个可以组合使用。第一是类别加权损失函数给样本量少的类别更高的权重在 PyTorch 里只需要给CrossEntropyLoss传一个weight参数第二是欠采样或过采样对少的类别重复采样让每个 batch 里正负样本比例尽量接近 1 比 1第三是在评估指标上不要只看 accuracy而是看 precision、recall 和 F1 值。对积水识别场景来说漏报把有积水判成没积水的代价往往比误报更大所以我会更关注 recall 的值。# 类别加权示例假设类别 0 有 850 张类别 1 有 150 张 weights torch.tensor([1.0, 850.0 / 150.0]).to(device) criterion nn.CrossEntropyLoss(weightweights)4.2 用混淆矩阵和可视化判断模型真实水平训练结束后不要只看一个准确率数字就收工。我每次做项目总结都会打印混淆矩阵它能把模型的错误模式暴露出来有很多积水图片被误判成了无积水说明模型对“少量积水”或者“夜间积水”的特征学习不够。经常把“湿路面”误判为积水说明标签规则里对“润湿”和“积水”的边界定义不够清楚可能需要回头修改标注。可视化方面有三个工具很实用。第一个是 Grad-CAM它能生成热力图显示模型判断的依据区域。如果一张有积水图片的预测是对的但热力图集中在图片角落的无关区域说明模型学到了巧合特征要警惕。第二个是直接可视化预测结果把模型的分类概率标注在图片上保存下来抽查最容易被误判的几十张图建立错误的直觉认识。第三个是做错误样本分析表把预测错误、预测正确但置信度低的样本分别归类找出规律。这些内容写进论文里是非常扎实的原始材料。4.3 排查手册损失不降、GPU 不够怎么办我在项目里遇到最多的问题排名第一的是loss 下降得极慢或者一直不降。排查顺序通常是这样的先检查数据预处理确认归一化是否正确再检查标签是否打反了尤其是自己手搓数据集的时候很容易把“有积水”和“无积水”的标签赋值搞反接着检查学习率太大容易震荡发散太小又会龟速收敛最后检查模型输出和损失函数是否匹配二分类的标签必须是 0 和 1而不是 1 和 2。说到 GPU 不够用毕设阶段很多同学的电脑只有 CPU。这种情况下我有几个实测有效的建议第一输入图片尺寸从 224 降到 160 或 128精度损失通常可以接受第二模型从 ResNet18 换成 MobileNetV3-Small第三batch size 设为 8梯度累积设置为 2这样等效于 batch size 16 的训练第四使用免费的在线 GPU 平台跑训练比如 Kaggle 和 Colab这两个平台都能跑 PyTorch注意保存检查点脚本避免平台断线导致训练白跑。5. 从模型到毕设成果推理脚本与演示系统5.1 最简单的模型导出与推理脚本训练完成之后你肯定需要一个能单独运行的推理脚本这也是很多同学容易卡住的地方。核心就是三件事加载模型权重、把图片预处理成和训练时一致的方式、前向推理得到预测结果。import torch from torchvision import models, transforms from PIL import Image device torch.device(cuda if torch.cuda.is_available() else cpu) model models.resnet18() num_features model.fc.in_features model.fc torch.nn.Linear(num_features, 2) model.load_state_dict(torch.load(best_model.pth, map_locationdevice)) model model.to(device) model.eval() transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) def predict(image_path): image Image.open(image_path).convert(RGB) input_tensor transform(image).unsqueeze(0).to(device) with torch.no_grad(): outputs model(input_tensor) probs torch.softmax(outputs, dim1) label torch.argmax(probs, dim1).item() confidence probs[0][label].item() return (有积水 if label 1 else 无积水), confidence if __name__ __main__: result, conf predict(test_water.jpg) print(f识别结果: {result}, 置信度: {conf:.4f})注意保存模型时建议连结构一起保存方便加载。我最推荐的方式是用torch.save(model.state_dict(), best_model.pth)保存权重然后在推理脚本里重新声明模型结构再加载这样结构清晰同理 Jesse。还有一个容易被忽视的点如果训练时的模型结构加了自定义层推理时也要完全一致不能直接加载。如果提示state_dict不匹配优先检查最后的全连接层输出维度是否一致。5.2 做一个可交互的积水识别 Demo毕设答辩的时候一个能现场演示的界面比一百页 PPT 都管用。不要求做多花哨一个最简单的 Web 页面或者本地窗口界面就够。如果你熟悉 Python 生态可以用 Flask 或者 FastAPI 写一个简单的接口然后写一个前端页面支持上传图片并显示识别结果。整套代码量不大后端暴露一个/predict接口接收图片文件调用predict()函数返回 JSON前端就是一个表单加一个图片展示框。如果不想写前端用 Streamlit 可以更快几行代码就能生成一个交互页面import streamlit as st from PIL import Image st.title(道路积水识别系统) uploaded_file st.file_uploader(上传道路图片, type[jpg, png, jpeg]) if uploaded_file is not None: image Image.open(uploaded_file).convert(RGB) st.image(image, caption输入图片) result, conf predict(uploaded_file) st.write(f**识别结果{result}**) st.write(f置信度{conf:.4f})把模型推理封装成函数后这套 Demo 可以在本地直接streamlit run app.py跑起来对于没有任何前端基础的同学来说非常友好。答辩现场直接用浏览器演示能明显提升项目的完成度印象。5.3 时间够的话这些扩展方向更加分如果基础版本已经完成还有时间余量我强烈建议加一个扩展。对我带过的项目来说性价比最高的扩展是加一个积水区域定位功能做法是在分类模型上叠加 Grad-CAM生成热力图叠加到原图上。这个功能不需要额外训练模型代码只有几十行但看上去非常专业答辩老师会认为你理解了模型的可解释性。另一个很实用的扩展是做一个视频检测工具用 OpenCV 读取视频流逐帧调用模型推理连续多帧判定为“有积水”时输出报警。这个方案贴近真实防汛监测场景和“识别是否是积水区域”的题目高度契合。如果你用的是第 3 节里的分割模型还能直接输出积水区域的像素面积估算配上参考比例尺可以得出积水面积的大致范围这在工程上更有应用价值。6. 我个人踩过几次坑之后的体会带过好几个积水识别相关的项目我自己也亲手跑过几版最大的体会是这个题目真正难的不是模型而是数据。很多人把精力全放在调参、换模型上忽略了数据分布、标注一致性、类别平衡这些更底层的问题结果模型在测试集上漂亮一到真实场景就露馅。最后分享一个我实测非常有效的小技巧训练结束后不要急着写论文先拿模型去测试一批“困难样本”——比如夜间积水、镜子一样的反光、很小的水洼。这些样本很可能暴露模型的真实短板也是你接下来优化最有价值的方向。能处理掉这些困难样本你的毕设就不只是“能跑”而是真的有说服力。这个项目的上限其实很高从简单的二分类一路做到分割、部署完全可以做成一套完整的积水监测原型系统祝你顺利。