墙面污渍识别这个毕设题目,我劝你选它之前先搞明白这几件事
墙面上有水渍、霉菌、脚印、涂鸦,到底算不算“有污渍”?这个问题听起来简单,但真要教给计算机去判断,门道比想象中多得多。我这个项目就是用 Python 写了一套基于 CNN 卷积神经网络的图像分类系统,输入一张墙面照片,模型输出“有污渍”或“无污渍”的判断结果,顺便还能告诉你它把注意力放在了墙面的哪个区域。
这套东西放在毕设里,难度适中、演示效果直观、还能往工程化方向扩展,属于性价比很高的选题方向。无论你是打算直接复现,还是想在这个基础上加自己的改进点,这篇文章从数据准备、网络搭建、训练调优到踩坑记录都会完整覆盖,边写代码边讲原理,把每一步背后的为什么也讲清楚。
适合三类人看:一是正在纠结毕设选题、想找个容易出成果方向的;二是已经定了类似图像分类题目、但不知道怎么下手训练的;三是想了解 CNN 实际落地会遇到哪些细节问题的。看完你就知道这个题目从零到一做完大概要经历什么,哪些环节容易卡住,以及怎么避坑。
1. 这个墙体污渍识别项目到底在解决什么问题
1.1 表面上看是二分类,实际上藏着不少细节
咱们先把题目掰开看。墙体有污渍识别,本质上是一个图像二分类任务:给定一张图片,判断它属于“干净墙”还是“污渍墙”。听起来简单到不能再简单,但仔细想想就知道这里面有一大堆隐藏的坑。
墙面不是均匀的纯色。白墙有阴影、有纹理、有漆面不均匀的痕迹;瓷砖墙有反光、有砖缝;老旧墙面有掉漆、有裂纹。这些都会干扰模型判断。污渍本身也分好多种:有水渍(不规则边界、颜色偏黄褐)、有霉斑(点状或片状、颜色发黑发绿)、有脚印和手印(轮廓比较清晰)、有涂料飞溅(小颗粒分布)、甚至还有不同拍摄角度和光线造成的视觉差异。
如果把所有情况都扔给模型,不加任何处理,模型很容易找到“捷径”——比如因为训练数据里所有“污渍墙”照片都偏暗,模型就学会用亮度做判断,而不是真正学习墙面的纹理特征。这种模型在测试集上可能表现不错,一换成真实场景就崩了。
这个项目从根本上要解决的问题,就是让模型学到的是“污渍本身的视觉特征”,而不是数据收集过程中引入的无关偏差。理解这一点,比急着写模型代码重要得多。
1.2 CNN在这里处于什么位置
为什么选卷积神经网络来做这个事,而不选传统机器学习方法或者更重的 Transformer 模型?
传统方法像 SVM、随机森林做图像分类,通常需要先手工提取特征。比如颜色直方图(污渍区域颜色分布跟正常墙面明显不同)、纹理特征(LBP、Gabor 滤波)、边缘特征(污渍边界通常是不规则的)。手工特征的问题是泛化能力有限,换个光照条件、换个墙面材质,特征分布就变了,你得重新调特征工程。
CNN 的优势在于端到端学习。输入原始图片像素,网络自己从底层边缘纹理特征逐步抽象出高层语义特征(“这块区域像水渍”“这块区域像霉斑”)。卷积核的局部连接和权值共享也大大减少了参数量,让模型在小数据集上也能训练起来。
选 CNN 还有个现实原因:它是毕设里最“稳”的选择。框架支持成熟、资料多、训练时间短、可视化方便,做消融实验、改网络结构、可视化特征图都有现成方法可以借力。你可以在它基础上加注意力机制、换成残差结构、甚至改成轻量级模型做移动端部署——这些都是水到渠成的事。
1.3 项目整体技术栈概览
先把项目用到的东西摊开来看:
- 语言:Python 3.8+
- 深度学习框架:PyTorch(选型理由后面细说)
- 图像处理:OpenCV、Pillow
- 数据增强:torchvision.transforms、Albumentations
- 可视化:Matplotlib、TensorBoard
- 训练平台:CPU 能跑但慢,建议有一个支持 CUDA 的 GPU
整体流程是:数据收集 → 清洗标注 → 数据增强 → 模型构建 → 训练 → 评估 → 可视化 → 导出模型。
每一步表面上看都是常规操作,但实际做的时候,每一步都有自己的坑。我按顺序说说我是怎么做的,以及为什么这么做。
2. 环境搭建与工具选型:为什么我不推荐一上来就装最新的东西
2.1 Python 环境与依赖管理
先说环境,这是很多新手卡住的第一道坎。我的建议是:不要用系统自带的 Python 直接装包,一定要用虚拟环境隔离。用 conda 或者 venv 都行,我个人习惯用 conda,因为处理 OpenCV、PyTorch 这类带二进制依赖的包时,conda 的依赖解析做得比 pip 省心。
conda create -n wall_stain python=3.8 conda activate wall_stain版本选择上,Python 3.8 或 3.9 就好。不用追求最新版,PyTorch 和 OpenCV 对旧版本 Python 的支持反而更稳定。你要是用了 Python 3.12,很容易碰上某些包还没适配的情况,白白浪费时间。
然后是核心依赖:
pip install torch torchvision pip install opencv-python pip install matplotlib numpy pandas pip install albumentations pip install tensorboard这里有个小提醒:PyTorch 的安装命令一定要去官网按你的 CUDA 版本选对应的命令。你要是 CUDA 11.8 却装了 CUDA 12.1 的版本,虽然也能跑,但会弹警告,性能也受影响。看自己 CUDA 版本的方式很简单:
nvidia-smi右上角那行 CUDA Version 就是你的驱动支持的版本,装 PyTorch 的时候照着选就行。
2.2 PyTorch vs TensorFlow:毕设场景选哪个更省事
这个对比几乎每个做毕设的人都会纠结。我的结论很明确:毕设选 PyTorch 更合适,理由有三个。
一是调试方便。PyTorch 是动态计算图,print 中间张量的形状、数值随时都能看。TensorFlow 2.x 虽然也有 eager mode,但涉及 Keras 高层 API 时,有些错误信息绕来绕去,新手容易看懵。调试深度学习代码最重要的就是能随时看到张量在每一层的变化,PyTorch 在这点上体验好太多。
二是资料和社区。现在学术界大部分论文的官方代码都是 PyTorch 实现,图森目标检测、图像分类这些方向的现成代码基本都是 PyTorch。你遇到问题搜到的答案大概率也是 PyTorch 的写法。
三是代码易读性。nn.Conv2d、nn.ReLU、nn.MaxPool2d一层层写下来,跟搭积木一样,理解网络结构非常直观。对毕设答辩来说,这种代码也很好讲:卷积层提取特征,池化层降维,全连接层做分类,逻辑一目了然。
唯一一个选 TensorFlow 的合理场景是:你们实验室或项目组有现成的 TensorFlow 代码要复用。否则不用犹豫,直接 PyTorch。
2.3 硬件层面的心理准备
先说结论:纯 CPU 训练这个项目也能跑,但体验会比较痛苦。
我刚开始用 CPU 跑一个 50 轮的小实验,每轮大约 40 秒,总共半小时左右。听起来好像还能接受,但你要做实验就不是跑一次就完事的:调学习率跑几次、改网络结构跑几次、加数据增强再跑几次、交叉验证再跑几次。积累下来,CPU 训练会严重拖慢你的实验节奏,让你不敢尝试更多可能性。
如果有 NVIDIA 显卡,哪怕是入门级的,体验都会天差地别。用 GPU 跑同样 50 轮只需要几分钟。结合我自己的经历,一句话总结:能用 GPU 就别用 CPU,哪怕去租云服务器也要用 GPU。
图像尺寸也是一个影响因素。如果图片缩小到 128x128 或 96x96,CPU 训练速度可以接受,但会损失一部分精度。这个精度的损失在毕设答辩时可能会被问到,你需要自己衡量。
3. 数据集构建:模型效果的上限在这里决定
3.1 数据从哪来:采集方案对比
深度学习圈有句话叫“垃圾进,垃圾出”。模型效果的上限由数据决定,模型只是逼近这个上限而已。墙面污渍识别这个题目的数据来源,我理过大概几个途径:
首先是自己拍摄采集。找不同光照、不同角度、不同墙面材质拍照,好处是数据真实可控,坏处是要花费不少时间精力。墙面要找齐“干净白墙”“瓷砖墙”“刷漆墙”“老旧水泥墙”,再分别拍摄有污渍和无污渍的状态,工作量不小。再说自己拍的数据量有限,撑不起大规模训练。
其次是网络爬取公开图片。网络上能找到大量墙面装修图、室内设计图、墙面污渍修补案例图等。思路是搜“脏墙”“墙面发霉”“墙壁水渍”“白墙装修”等关键词,然后清洗筛选。爬图的时候注意图片版权问题,用来做学术研究和毕设问题不大,但别用于商业发布。
第三是直接用公开数据集。有些公开数据集包含墙面场景图片,比如室内场景数据集里的墙面部分可以裁剪出来。不过专门针对“墙面污渍”这个具体任务的数据集目前还没有公开的完整版本,需要用通用场景数据自己做标注。
我的方案是混合式:网上收集了大部分,自己拍了一部分补充边缘情况(比如墙角阴影、砖缝较多的大面积瓷砖墙)。总共整理下来大约 6000 张有效图片,其中干净墙 3200 张,污渍墙 2800 张。这个体量训练 CNN 分类任务已经够用,但如果直接上大型模型(ResNet101 这种),会明显感觉数据不够,过拟合风险比较高。
3.2 数据清洗与标注:这里决定模型是否“学歪了”
数据清洗是很多人会忽略的坑。爬来的图质量参差不齐,必须逐类筛查,我把清洗规则整理成表格:
| 需要剔除的类型 | 原因 |
|---|---|
| 黑白图、灰度图 | 颜色信息对污渍识别很重要,灰度图让模型少一个维度的特征 |
| 带水印、带logo的图 | 模型可能把水印跟污渍混淆 |
| 分辨率过低的图 | 边缘信息丢失,污渍轮廓不清晰 |
| 严重模糊的图 | 对焦不准导致纹理信息不可靠 |
| 同一张图被不同渠道重复收录 | 造成数据冗余,加剧过拟合 |
| 有明显人物、家具前景遮挡的图 | 前景干扰严重,模型注意力被带跑偏 |
清洗完再标注。标注方式很简单:建两个文件夹,一个叫clean,一个叫stained,图片各自放进对应文件夹,类别的标签就是文件夹名。这个方案虽然看起来土,但很好用,不用搞什么标注软件。
标注过程中我最深的体会是:模糊样本的处理要一致。比如墙面只有一小块淡黄色痕迹,这算不算污渍?不同人标准不一样,我定的标准是“肉眼在一米距离内能明显看出的痕迹就算污渍”。你也要在标注前定好标准,全程保持一致,不然模型会学到内心分裂的决策边界。
3.3 数据增强策略:老生常谈但细节很多
数据增强是缓解数据量不足最有效的手段。PyTorch 提供了torchvision.transforms,开箱即用。我用了这些:
transform_train = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(degrees=15), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), transforms.RandomAffine(degrees=0, translate=(0.1, 0.1)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])RandomHorizontalFlip配合墙面场景很自然,左右翻转不改变“是否有污渍”的事实。RandomRotation控制在 15 度以内,因为墙面照片歪斜一点是常见的,但转太多会把图像内容都转飞了。ColorJitter模拟不同光照条件——这也是墙面拍摄最常见的变量。RandomAffine的平移模拟拍摄机位偏移。
验证集的变换要保守得多,只做Resize、ToTensor和Normalize,不做随机增强。因为验证集要反映真实分布,加了随机增强会让验证精度波动变大,干扰你判断模型是否真的在收敛。
这里有个容易被忽略的细节:Normalize 的 mean 和 std 用的是 ImageNet 的标准值。你用预训练模型时这是标准做法,因为预训练权重就是在这个归一化参数下训练的。如果你决定不加载预训练权重、从零训练,那均值方差最好重新按自己的数据算。我在实际项目中试过这两种方案,用 ImageNet 参数的收敛速度明显更快。
3.4 数据集划分:不能忽视的“同源图像”问题
划分训练集、验证集、测试集时,我犯过一个典型错误:一开始直接随机划分,结果测试集精度特别高,但拿真实场景的新照片一测就拉胯。
问题出在同源图像上。爬图时同一组室内设计图可能包含连续几张不同角度拍的同一面墙,这些图颜色、光照高度相似。随机划分时,同源的图片会同时出现在训练集和测试集,模型等于提前见过测试题的“同款变形”,精度虚高。
正确做法是按图像来源分组划分。可以先按文件夹或文件名的前缀(同一来源的图片在命名上会有规律)进行分组,然后确保同一组图片全部进入同一个划分集合。具体逻辑是:统计所有图片文件名的前缀模式,把相同来源的放到一组,然后按组来划分而不是按单张图片划分。这样测试集跟训练集的分布差异是真实的,测试精度才可信。
我最终的划分比例是训练集 70%、验证集 15%、测试集 15%。验证集用来做模型选择和早停判断,测试集只在全部实验结束后评测一次,绝不回头调参。
4. CNN 模型结构与实现:从零写网络还是迁移学习
4.1 网络设计思路:先经典后自研
做这个项目你面临一个选择:是自己写一个简单的 CNN 网络,还是直接用 ResNet、VGG 这类经典结构做迁移学习。
我建议是两步走。第一步用经典预训练模型跑通流程,拿到一个基线准确率;第二步再尝试自己精简网络结构或改造经典网络来做优化。这样答辩时有对比实验可讲:预训练模型精度高但参数量大,自己的轻量网络精度接近但计算量小得多,故事的完整性就出来了。
这个道理跟你学做饭一样:先按菜谱做一次标准版吃到基准味道,再自己大胆改良调料比例,有得有失,才有东西可以说。
4.2 主力模型:ResNet18 迁移学习
主力模型选 ResNet18 有三个原因。一是结构相对简单、参数量小(约 1100 万),对毕设数据集体量来说不容易过拟合。二是残差连接让梯度流动顺畅,训练更稳,不需要太多花式技巧。三是有 PyTorch 官方预训练权重,ImageNet 上训练好的通用特征可以直接迁移。
加载预训练模型并改造最后一层分类器是这样的:
import torch import torch.nn as nn import torchvision.models as models model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT) num_features = model.fc.in_features model.fc = nn.Linear(num_features, 2) for param in model.parameters(): param.requires_grad = False for param in model.fc.parameters(): param.requires_grad = True等等,这里有个关键选择:冻结还是微调?上面这段代码把整个骨干网络冻结了,只训练最后一层全连接分类器。这样训练速度快、显存占用小,而且对于跟 ImageNet 场景差异不算特别大的任务,冻结特征提取层通常效果不错。
但墙面污渍特征比较细(霉斑的纹理、水渍的边界分布),底层特征可能不够用。我做完冻结实验后发现验证集最优准确率停在 93% 左右,后面解冻了最后一个残差块做微调,准确率提到了 96%。做法很简单:
# 解冻最后两个残差块 for name, child in model.named_children(): if name in ['layer3', 'layer4']: for param in child.parameters(): param.requires_grad = True微调时学习率要调小,我用的是1e-4,而只训练全连接层时可以用1e-3。这个差别很好理解:全连接层是随机初始化的,需要大一点的学习率快速学习;预训练权重只想让它小幅移动,寻找更适合本任务的局部区域,学习率太大会把学好的特征冲坏。
4.3 训练配置:损失函数、优化器、学习率调度
损失函数用交叉熵损失(CrossEntropyLoss),这是分类任务的标配,PyTorch 里nn.CrossEntropyLoss()内部做了 softmax 和 log 的操作。它有处理类别不均衡的能力,通过给每个类别加权重实现:
weights = torch.tensor([1.0, ratio_clean_to_stained]) criterion = nn.CrossEntropyLoss(weight=weights)如果污渍图像的样本数量比干净墙少,就给污渍类别更高的损失权重,惩罚模型在污渍类上的错误分类更重一些。
优化器选 Adam。SGD 加动量需要更多调参经验,Adam 对学习率的敏感度低一些,更好上手。不过 Adam 默认的学习率1e-3在微调场景下经常偏大,我实际用下来微调阶段1e-4更合适。
学习率调度用了余弦退火CosineAnnealingLR:
import torch.optim.lr_scheduler as lr_scheduler scheduler = lr_scheduler.CosineAnnealingLR(optimizer, T_max=30, eta_min=1e-6)余弦退火的思路是让学习率随着训练轮数按照余弦函数从大平滑衰减到最小。前期学习率大,模型快速收敛到较优区域;后期学习率小,在小范围做精细化调整。对毕设来说,比等间隔下降(StepLR)稳,不用频繁观察曲线手动调整。
训练主循环长这样:
best_val_acc = 0.0 num_epochs = 30 for epoch in range(num_epochs): 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 = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() train_acc = 100.0 * correct / total train_loss = running_loss / total val_acc, val_loss = evaluate(model, val_loader, criterion, device) scheduler.step() print(f"Epoch [{epoch+1}/{num_epochs}] " f"Train Loss: {train_loss:.4f} Train Acc: {train_acc:.2f}% " f"Val Loss: {val_loss:.4f} Val Acc: {val_acc:.2f}%") if val_acc > best_val_acc: best_val_acc = val_acc torch.save(model.state_dict(), "best_model.pth")注意两点。一是scheduler.step()必须在验证集评估之后调用,顺序别搞反。二是只在验证集准确率提升时才保存模型,最后拿到的就是验证集表现最好的那一次权重,而不是最后一轮的权重。很多人直接保存最后一轮,结果后面几轮过拟合了,得到的模型反而不如中间某轮的好。
5. 训练过程与效果优化:曲线不会骗人
5.1 我的实验过程和结果记录
第一次跑 30 轮,训练集准确率一路冲到 99.8%,验证集准确率停在 93% 左右不再涨。这是一个非常典型的过拟合信号:训练集准确率远高于验证集,两者差距越拉越大。模型在背训练样本的细节而非学习泛化特征。
我在第 10 轮左右观察到验证集准确率提升放缓,训练集准确率还在快速增长,就知道这个模型容量对当前数据量来说太大了。缓解方法按优先级来:先加强数据增强,再引入 Dropout,最后考虑减小模型容量。我先在 ResNet18 的全连接层后面加了一个 Dropout:
class ResNet18Dropout(nn.Module): def __init__(self, num_classes=2, dropout_rate=0.5): super().__init__() self.backbone = models.resnet18(weights=models.ResNet18_Weights.DEFAULT) in_features = self.backbone.fc.in_features self.backbone.fc = nn.Sequential( nn.Dropout(p=dropout_rate), nn.Linear(in_features, num_classes) ) def forward(self, x): return self.backbone(x)加了 Dropout + 更强的数据增强后,验证集准确率从 93% 提升到 95.5% 左右。后面把 Dropout 率调低到 0.3(因为 0.5 在微调阶段反而压制了模型的表达),验证准确率达到了 96.2%。第 30 轮结束,测试集最终准确率 95.8%,分类报告如下:
| 类别 | precision | recall | f1-score |
|---|---|---|---|
| 干净墙(clean) | 0.96 | 0.97 | 0.96 |
| 污渍墙(stained) | 0.96 | 0.94 | 0.95 |
从召回率来看,污渍墙类别的召回率 0.94,说明有 6% 的真实污渍图片被漏报成了干净墙。在“实际有污渍但模型说没有”这种场景里,漏报的代价要慎重考虑。如果这个系统用在检测墙体霉变需要人工复查的场景,漏报可能带来维护盲区,这种情况可以通过调整分类阈值让模型更“愿意”判为污渍,提高召回率但会牺牲少量精确率,需要根据实际需求权衡。
5.2 准确率之外的评估维度
分类准确率(Accuracy)在很多场景里是不够的。我建议至少再算三个指标:
- Precision(精确率):预测为“有污渍”的样本中,真正有污渍的比例。这个高意味着模型不会太“神经质”。
- Recall(召回率):真实有污渍的样本中被找出来的比例。这个高意味着漏检少。
- F1-score:两者的调和平均,综合反映模型质量。
计算方式用 sklearn 一行搞定:
from sklearn.metrics import classification_report, confusion_matrix print(classification_report(y_true, y_pred, target_names=['clean', 'stained'])) print(confusion_matrix(y_true, y_pred))混淆矩阵值得看一眼。型录里面哪两类样本最容易互相混淆,看一眼就知道继续提升的方向。我观察下来,误判主要集中在两类图:一类是光照极暗导致细节丢失的墙面,另一类是墙面本身有细腻纹路但又被少量灰尘覆盖的图。这种问题靠调模型难以彻底解决,需要从数据采集端去补强。
5.3 可视化:让答辩评委一眼就懂你做了什么
毕设答辩最看重“可解释性”。你光是说模型准确率 95.8%,评委不一定有直观感受。但你放两张图出来,一张是输入的原图,一张是热力图显示模型在哪些像素区域做出了判断,评委立刻就知道你的模型确实学到了“污渍特征区域”。
用Grad-CAM做热力图可视化,原理不复杂:取最后一个卷积层的输出特征图,结合全连接层的梯度信息反推出每个空间位置对于目标类别的重要程度。PyTorch 实现有现成工具库pytorch-grad-cam:
pip install grad-cam使用方式:
from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget from pytorch_grad_cam.utils.image import show_cam_on_image import cv2 import numpy as np target_layers = [model.backbone.layer4[-1]] cam = GradCAM(model=model, target_layers=target_layers) input_tensor = torch.unsqueeze(images[0], 0).to(device) targets = [ClassifierOutputTarget(1)] # 让模型解释它为什么把这张图判为"污渍" grayscale_cam = cam(input_tensor=input_tensor, targets=targets) grayscale_cam = grayscale_cam[0, :] rgb_img = np.transpose(images[0].cpu().numpy(), (1, 2, 0)) # 这里注意归一化还原 rgb_img = np.clip(rgb_img * np.array([0.229, 0.224, 0.225]) + np.array([0.485, 0.456, 0.406]), 0, 1) visualization = show_cam_on_image(rgb_img, grayscale_cam, use_rgb=True)实际效果里,模型对小片霉斑的关注区域几乎跟人工标注的污渍边界吻合,对大面积水渍的关注区域集中在边缘过渡带上。有几张图模型关注到了非污渍区域(比如墙角和踢脚线阴影),这几张往往就是预测错误的样本——可视化不仅能用在答辩展示,还能反过来帮你迭代数据质量。
5.4 自研轻量网络对比实验
为了体现“对领域的思考”,我还设计了一个轻量 CNN 结构做对比实验,参数量只有 ResNet18 的 1/10 左右,目的是看能不能用更小的模型达到接近的效果。
网络结构是:3 个卷积块(每个含卷积、BN、ReLU 和最大池化)加 1 个自适应池化加全连接层,最后加 Dropout。这是我自己写的一个示例结构,也可以换成其他轻量网络做对比。实际训练发现这个轻量结构大约能达到 91% 左右的验证集准确率,比迁移学习低了 5 个百分点,训练速度快得多,而且显存占用少。虽然最终效果不如 ResNet18,但做对比实验的思路本身是完整可讲的:轻量结构适合部署在资源受限设备上,算是工程化的方向。
6. 常见问题与排查技巧实录:这些坑我替你踩过了
6.1 数据相关的三个经典坑
类别不均衡的影响比想象中大。如果污渍墙图片比干净墙少太多,模型会倾向于把所有图片都判为多数类,准确率看起来很高但实际没用。解决方法是类别加权或过采样少数类。我做测试时把数据手动调成 4:1 不均衡比,发现模型预测直接偏向多数类。加了类别权重后,少数类召回率提升明显。
增强太猛会拖慢收敛。一开始我把RandomRotation设为 45 度,结果模型前期怎么都不收敛。原因很简单:墙面照片旋转超过一定角度已经不符合真实场景了,模型花大量计算去适应这些无意义样本。后面调到 15 度,收敛速度恢复正常。数据增强不是越猛越好,要跟任务场景匹配。
图片 Resize 到太大或太小都难受。太大训练慢,太小污渍细节丢失。我试过 96x96、128x128、224x224 三个尺寸,最终选了 224x224,是识别精度和训练速度的平衡点。测试时发现不同尺寸之间的准确率差距在 3% 左右,这个差异对毕设选题的判断是有意义的。
6.2 训练过程经常出的问题
Loss 变成 NaN。最常见原因是学习率太大导致梯度爆炸。解决办法:降低学习率、检查数据中有没有包含异常值(比如纯黑图、全 255 白图)、在损失函数里加梯度裁剪。我用clip_grad_norm_加了一个梯度上限后,训练稳定多了。
验证集 Loss 波动大、曲线上窜下跳。先看验证集是不是太小了。验证集只有几十张图时,准确率波动 5% 很正常。我把验证集增加到几百张后曲线平滑很多。另一个原因是 batch size 太小,BatchNorm 的统计量不稳定,适当增大 batch size 可以改善。
训练集 Loss 下降但验证集一直不动。多半是模型容量过大而数据量不足。检查一下训练集 Loss 是否过低(比如 0.01 以下),如果是,优先考虑加强数据增强,其次考虑减小模型容量。更直接的方案是用预训练模型 + 冻结底层特征,这是小数据集场景下最有效的手段,效果我也验证过,对收敛速度和最终精度都有明显帮助。
6.3 部署和打包环节的坑
训练好的 PyTorch 模型怎么给别人用。两种方式:把模型结构代码和权重文件一起发;或者导出为 ONNX 格式,方便在其他框架里跑。导出 ONNX 很简单:
dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export(model, dummy_input, "wall_stain_model.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}})第一次导出遇到一个问题:如果模型里包含 Dropout 层,导出前一定要切到评估模式model.eval(),否则 Dropout 行为不确定,导出的模型在推断时表现异常。这个细节我花了好久才发现。
CPU 上跑预测太慢?可以做量化,模型转成半精度推理(model.half()+ 输入也转半精度),加上 batch inference,速度能明显提升。对于墙面污渍识别这种对实时性要求不高的场景,单张图片几百毫秒到一两秒都能接受,不用太追求极致优化。
OpenCV 读图跟 PyTorch 的通道顺序不一致。OpenCV 默认 BGR,PyTorch 默认 RGB。用 OpenCV 读图直接喂给模型,会出现“模型判断完全错乱”的诡异状况。解决办法是读图后强制转成 RGB:
img_bgr = cv2.imread("test.jpg") img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)6.4 一个完整的预测脚本示例
磨刀不误砍柴工,这里给出一个可直接使用的预测脚本,把训练好的模型拿来做单张图片推理:
import torch import cv2 import numpy as np from torchvision import transforms device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = build_resnet18_dropout(num_classes=2) model.load_state_dict(torch.load("best_model.pth", map_location=device)) model.to(device) model.eval() transform = transforms.Compose([ transforms.ToPILImage(), transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) def predict_wall(image_path): img_bgr = cv2.imread(image_path) img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) tensor = transform(img_rgb).unsqueeze(0).to(device) with torch.no_grad(): output = model(tensor) prob = torch.softmax(output, dim=1) clean_prob = prob[0][0].item() stained_prob = prob[0][1].item() return (stained_prob, clean_prob) if __name__ == "__main__": prob_clean, prob_stain = predict_wall("test_images/sample_01.jpg") print(f"干净墙概率: {prob_stain:.4f}") print(f"污渍墙概率: {prob_stain:.4f}")一眼就能看到结果,还能看到模型输出两种类别的概率值。如果两个概率非常接近(比如 0.51 对 0.49),说明模型对这张图没什么把握,这种样本值得单独标注积累成难例集。
结语:做完这个项目的几点体会
项目收尾复盘时,我的感触是:墙面污渍识别本身不难,难的是把数据做好、把实验设计得有条理、把结果讲出道理来。
有几个经验对后来者特别有价值。第一,数据清洗和标注的重要性怎么强调都不过分,这个环节投入的时间和精力,回报率远高于后期调参。第二,冻结预训练模型 + 微调的组合策略,在小数据集上效果稳定,一定要作为首选方案。第三,可视化(Grad-CAM)不仅是答辩利器,它也是你自己理解模型的有效途径——看到模型关注错位置,你就知道下一步该往哪个方向改进。
最后分享一个小技巧:训练过程中把每个阶段的模型权重和对应的验证集指标都完整记录下来,实验记录写得越详细,后面分析和写论文就越轻松。每一个参数改动、每一次数据清洗策略的调整,都记清楚,这会让你的毕设答辩底气完全不一样。