简介:面向计算机视觉或相关专业的课程设计或期末大作业场景,这是一份采用Python与CNN实现道路坑洼检测的完整源码包,特别适合需要快速上手项目实战或借鉴高分作业做法的学习者。整套项目源自作者大三学期的期末作业,经导师指导认可,评审达到98分,代码模块划分清晰,注释也较为完善,可直接作为同类型任务的参考框架。压缩包共含14个文件,其中11个Python脚本覆盖数据预处理、模型构建、训练及预测等核心环节,并涉及AlexNet、LeNet-5等经典网络结构的实现;2个h5文件为已训练好的模型权重,可直接加载复用;1个Markdown文档用来说明整体设计与使用方法,包总体积约10.49MB。目前已有906人学习过本资源,下载后可通过该工程快速理解CNN在道路坑洼检测中的实际应用,并基于现有代码开展二次开发与调优。
1. 道路坑洼检测大作业:CNN方案为什么是拿高分的最短路径
计算机视觉课程的期末大作业里,“基于Python+CNN的道路坑洼检测”是一个高频选题——场景直观、数据好找、效果容易演示,而且恰好落在CNN分类和目标定位两个核心知识点上。很多同学拿到源代码包的第一反应是赶紧跑通,结果卡在环境配置和参数调试上就是一整晚,最后只能提交一个“能跑但讲不清”的版本。
这篇笔记把从数据准备、模型搭建、训练调参到答辩汇报的完整路径拆开讲,代码可以直接抄,参数给的是我反复试过的起点值,不是玄学。适合正在赶大作业的学生,也适合想在报告里多写一两个亮点的从业者——前提是你已经装好了Python3和PyTorch,用CPU跑完全足够,只是比GPU慢上几倍。下面按“数据怎么做、模型怎么搭、参数怎么调、坑怎么躲、分怎么再涨”的顺序走一遍。
2. 数据集与输入管线:坑洼检测任务的第一公里
2.1 数据来源与标注:公开资源、手机实拍、Labelme怎么配合
道路坑洼检测的数据来源一般有三条路:公开数据集、手机或行车记录仪实拍、视频抽帧。公开数据方面,Kaggle上有多个Pothole Detection相关数据集,学术项目也会放出一些坑洼图像集。但直接拿来用有个典型问题——视角和光照不统一。有的数据集是无人机俯拍,有的是行车记录仪视角,有的甚至是从新闻图片里截出来的,混合训练会让模型学得一头雾水。
我的习惯是先用公开数据做一次快速验证:所有图片缩放到224x224,训练一个十几轮的小模型,如果验证准确率连60%都不到,说明数据分布和你的目标场景差异太大,必须掺入实拍图。手机实拍是最可控的来源,也最容易让答辩老师认可。推荐在三种光照下拍摄:顺光、侧光、逆光。坑洼在侧光下阴影最明显,CNN也最容易学到“阴影加纹理断裂”的组合特征;逆光样本太少会导致模型出现系统性误检——把所有阴影都报成坑。每类场景拍30到50张,加上公开数据扩充到正负样本各250张以上,一个轻量CNN就能学到可演示的效果。
标注环节,纯分类任务用文件夹目录就够了:background和pothole两个文件夹各放对应图片。如果你计划做第6章的定位热力图或分割分支,推荐用Labelme做多边形标注,输出JSON后自己写转换脚本。LabelImg也可以,它输出Pascal VOC格式的XML,转掩码时稍微绕一点。我一般直接选Labelme,因为JSON转掩码的代码很成熟,改改路径就能用。
2.2 图像预处理与数据增强:小样本提分的第一个杠杆
图像预处理在坑洼检测里被很多人轻视。不同设备拍出的图片分辨率和曝光差异很大,统一管线是:读取图片、转RGB、缩放、归一化。归一化的均值方差直接沿用ImageNet的(0.485, 0.456, 0.406)即可,虽然你的数据不是ImageNet,但后续如果要换预训练权重做对比实验,这会省掉很多麻烦。
数据增强是这份大作业里性价比最高的操作。坑洼检测的典型增强组合是随机水平翻转、小角度旋转、亮度饱和度抖动、随机裁剪缩放。注意旋转角度别超过15度,坑洼本身有路面朝向,转太多会让模型学到错误的空间先验。下面是用PyTorch写的数据加载器:
import os import cv2 from torch.utils.data import Dataset from torchvision import transforms class PotholeDataset(Dataset): def __init__(self, root_dir, train=True): self.root_dir = root_dir self.train = train self.image_paths = [] self.labels = [] class_names = ['background', 'pothole'] for label, cls in enumerate(class_names): cls_dir = os.path.join(root_dir, cls) for fname in os.listdir(cls_dir): if fname.lower().endswith(('.jpg', '.jpeg', '.png')): self.image_paths.append(os.path.join(cls_dir, fname)) self.labels.append(label) if train: self.transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(10), transforms.ColorJitter(brightness=0.3, contrast=0.3), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) else: self.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 __len__(self): return len(self.image_paths) def __getitem__(self, idx): img = cv2.imread(self.image_paths[idx]) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) label = self.labels[idx] if self.transform: img = self.transform(img) return img, label这里面有一个细节:cv2读图默认是BGR通道,必须手动转成RGB,否则模型会学到错误的颜色分布。RandomRotation(10)表示正负10度区间,亮度抖动0.3是常见起点,如果数据偏暗,可以调到0.5。训练集和验证集共用归一化参数,但验证集不能加随机增强,否则评估结果每次都不一样。
2.3 划分数据集:目录结构、随机种子与文件级划分
数据划分是整份代码里最容易埋雷的环节。常见错误有两种:一种是不固定随机种子,每次运行划分结果不同,训练过程完全不可复现;另一种是按文件名字母序切分,很可能背景图片集中在前半段、坑洼图片集中在后半段,划分出来的训练集和验证集分布严重失衡。
正确做法是按类别分层划分:每个类别内部先随机打乱,再按比例切出训练、验证、测试三份。下面的代码按类别分层做文件级划分,并把结果复制到独立目录中:
import random import shutil from pathlib import Path def split_dataset(src_dir, out_dir, train_ratio=0.7, val_ratio=0.15, seed=42): """按类别分层划分数据集,保证每个类的比例一致""" random.seed(seed) class_names = ['background', 'pothole'] out_dirs = {split: {cls: Path(out_dir) / split / cls for cls in class_names} for split in ['train', 'val', 'test']} for split in out_dirs: for cls in out_dirs[split]: out_dirs[split][cls].mkdir(parents=True, exist_ok=True) for cls in class_names: src_class = Path(src_dir) / cls images = [p for p in src_class.iterdir() if p.suffix.lower() in ('.jpg', '.jpeg', '.png')] random.shuffle(images) total = len(images) n_train = int(total * train_ratio) n_val = int(total * val_ratio) for split_name, split_imgs in [ ('train', images[:n_train]), ('val', images[n_train:n_train + n_val]), ('test', images[n_train + n_val:]) ]: for img in split_imgs: dst = out_dirs[split_name][cls] / img.name shutil.copy2(str(img), str(dst)) if __name__ == '__main__': split_dataset('raw_data', 'dataset', seed=42)这段代码的关键点有三个:先固定seed再shuffle,保证可复现;按类别分别shuffle,避免某一类全部落入训练集;划分粒度是文件而不是样本条目,杜绝同一场景的连续帧被拆到不同集合里。val和test各取15%是我在小样本任务上的经验值,总样本500到1000时这个比例比较稳。还有一条纪律:调参过程中只看val,测试集留到最后 evaluate 一次,反复用测试集调参会让它失效。
3. CNN模型搭建与训练:从零写出一个能答辩的结构
3.1 选型理由:为什么是CNN,而不是Canny边缘检测或YOLO
选型这一步必须先说清一个误区:很多人听到“检测”就想到YOLO,但课程大作业里的“道路坑洼检测”,最主流的做法其实是“图像分类”——判断一张图里有没有坑洼。YOLO的边界框输出是加分项,不是必选项。YOLO的anchor机制、NMS和损失函数对初学者是个黑匣子,调一组能用的参数可能要搭上一整天,小数据集上还容易不收敛,答辩时被追问细节很容易露怯。
传统图像处理方案,比如Canny边缘检测加轮廓分析,在某些光照条件下确实能圈出坑洼,但鲁棒性很差。路面裂缝、车道线、轮胎印乃至井盖边缘,都会产生大量伪轮廓,换一个场景准确率就崩。CNN的价值在于它不依赖手工设计的特征,而是让卷积核在训练中自己学习坑洼的视觉模式——阴影形状、边缘断裂、路面纹理异常的组合。在我做过的项目里,500到1000张的小数据集,一个轻量CNN从头训练就能达到85%以上准确率;用ImageNet预训练权重微调,90%以上是常规水平。
那为什么不直接调torchvision.models.resnet18?可以用,而且效果不错。但大作业的评分点通常包含“对模型结构的理解”。从零搭一个简单的CNN,在报告里画出结构图、说明每层的感受野和参数量,比直接调用ResNet更能体现课程内容的掌握程度。我的建议是:主模型用自己写的轻量CNN,对比实验里再加一个ResNet18做baseline,这样报告既有深度又有广度。
3.2 模型结构:轻量CNN的层设计、感受野与参数说明
下面这个结构是坑洼检测任务上我反复使用的基础版本:四组卷积加一个分类头,输入224x224的RGB图像。设计时考虑三个点:前几层用3x3小卷积核堆叠,等效感受野足够捕捉坑洼的边缘和局部纹理;每层紧跟BatchNorm,在小batch size下训练稳定性明显更好;分类头用AdaptiveAvgPool2d,模型不锁定输入尺寸,答辩现场换测试图不用改网络。
import torch.nn as nn class PotholeCNN(nn.Module): def __init__(self, num_classes=2): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(128, 256, kernel_size=3, padding=1), nn.BatchNorm2d(256), nn.ReLU(inplace=True), nn.MaxPool2d(2), ) self.classifier = nn.Sequential( nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.Dropout(0.5), nn.Linear(256, 128), nn.ReLU(inplace=True), nn.Linear(128, num_classes) ) def forward(self, x): x = self.features(x) x = self.classifier(x) return x参数说明:输入3通道RGB图,经过四次池化,特征图从224降到14,通道数从32升到256。这个结构的总参数量约300万,CPU上单张推理在50毫秒以内。Dropout放在第一个全连接之前,比例0.5,对这个规模的模型不算激进。如果训练准确率接近99%而验证集只有80%,把Dropout提升到0.6,或者给特征层加一点L2正则,过拟合能明显缓解。
3.3 训练主循环:损失函数、优化器与学习率调度
训练部分的工程素养主要体现在三个选择上:二分类用CrossEntropyLoss(内部包含softmax),优化器用AdamW而不是Adam,因为AdamW对weight_decay的处理更规范,小数据集上不容易出现权重衰减失效的问题。学习率从1e-3起步,配合余弦退火调度,通常比固定学习率高出两到三个点的验证准确率。
import torch from torch import nn, optim from torch.utils.data import DataLoader def train_model(model, train_loader, val_loader, epochs=30): device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs, eta_min=1e-5) best_acc = 0.0 for epoch in range(epochs): model.train() running_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() running_loss += loss.item() val_acc = evaluate(model, val_loader, device) scheduler.step() print(f'Epoch {epoch+1:02d} | loss {running_loss/len(train_loader):.4f} | val_acc {val_acc:.4f}') if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), 'best_pothole_model.pth') def evaluate(model, loader, device): model.eval() correct, total = 0, 0 with torch.no_grad(): for images, labels in loader: images, labels = images.to(device), labels.to(device) outputs = model(images) preds = outputs.argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) return correct / total这里三个核心细节容易被忽略。第一,保存模型用state_dict而不是整个model对象,加载时先实例化结构再load_state_dict,代码重构后旧权重依然可用。第二,CosineAnnealingLR的T_max设为总轮数,学习率从1e-3平滑降到1e-5,比阶梯式下降更适合小数据集。第三,evaluate里的model.eval()和torch.no_grad()必须成对出现,漏掉任一个,验证集指标都会虚高。训练中还要留意:如果训练准确率已经很高但验证准确率不再上涨,说明过拟合已经发生,应该用触发“验证集最佳”时保存的权重,而不是最后一个epoch的权重——上面的代码里已经实现了这个逻辑。
4. 训练与调参:从85分到95分,关键在参数和评估
4.1 超参数起点值:一张参数表和一个调整策略
很多人拿到训练代码后第一件事是照抄参数,然后发现loss不降,就开始怀疑代码有问题。实际上,超参数必须和你的数据规模匹配。坑洼检测这个任务,下面这组参数是我验证过比较稳的起点:
| 参数 | 建议起点值 | 调整方向 |
|---|---|---|
| 输入尺寸 | 224x224 | 坑洼在图中占比很小时升到288 |
| batch size | 16(CPU)/ 32(GPU) | 显存不足时降到8,同时学习率减半 |
| 学习率 | 1e-3(AdamW) | 训练震荡就降到3e-4 |
| epochs | 30 | 验证集停止上升就早停 |
| weight_decay | 1e-4 | 过拟合明显时加大到1e-3 |
| Dropout | 0.5 | 过拟合再加到0.6 |
| 优化器 | AdamW | 不推荐SGD,收敛太慢 |
调整策略有一条铁律:一次只动一个变量。有人同时改学习率、batch size和数据增强,结果准确率下降了却不知道是哪个改动导致的。我的习惯是先用这张表跑一遍,记录train_acc和val_acc。如果train_acc低于85%,优先调学习率和epochs;如果train_acc接近99%而val_acc低于90%,优先调Dropout、weight_decay和增强强度。epochs设成30只是起点,实际中验证集往往在第12到20轮就达到峰值,之后继续训练只是让train_loss继续下降,val_acc不再上涨——那是过拟合的开始。报告里一定要说明你选的是验证集最优那个epoch的模型。
4.2 类别不均衡:坑洼样本不够的时候怎么办
道路坑洼检测里正负样本不均衡是常态。外出实拍时,背景图随便拍都是,坑洼图却要专门去找,结果pothole目录只有80张,background却有三四百张。直接用原始分布训练,模型会把所有图片都判成background,准确率照样有70%以上——但这是假指标,掩盖了模型完全没有检测能力的事实。
解决不均衡有四种办法,按性价比排序。第一是给损失函数加类别权重,PyTorch的CrossEntropyLoss直接传weight参数,代码只加几行,效果立竿见影。第二是数据增强时对少数类做额外增强,比如对pothole图多做随机裁剪和亮度变化。第三是过采样少数类样本。下面这段代码演示了类别权重的计算:
from collections import Counter import torch from torch import nn # all_train_labels 是训练集所有样本标签组成的列表 label_counts = Counter(all_train_labels) total = sum(label_counts.values()) num_classes = 2 weights = [total / (num_classes * label_counts[i]) for i in range(num_classes)] criterion = nn.CrossEntropyLoss( weight=torch.tensor(weights, dtype=torch.float32).to(device) )权重计算逻辑是:每类的权重等于总样本数除以(类别数乘该类样本数)。假设background有300张、pothole有100张,两类权重分别是0.5和1.5,坑洼类的梯度贡献被放大三倍。加权重后训练准确率可能会从95%降到90%左右,但验证集上pothole的召回率会明显升上来——这才是我们要的指标。
补充一点:过采样不能直接把重复图片复制进目录再shuffle,因为复制之后再做划分,同一图片的副本可能同时进入训练和验证集,造成数据泄漏。正确顺序是先用第2章的split_dataset划好目录,再对train目录里的少数类别做增强扩展。欠采样(删掉部分background图)在小数据集上不推荐,有效样本变少后模型学不到稳定的背景分布。
4.3 评估与可视化:报告里放什么图才能拿高分
大作业报告里只写一行“准确率93.2%”是最吃亏的呈现方式。评估环节至少要做三件事:混淆矩阵、按类别的精确率与召回率、Grad-CAM热力图。混淆矩阵能暴露模型的失败模式——如果pothole的召回率低于85%,说明漏检严重;如果background被误判为pothole的样本很多,说明模型的假阳性需要压制。
from sklearn.metrics import confusion_matrix, classification_report import matplotlib.pyplot as plt y_true, y_pred = [], [] model.eval() with torch.no_grad(): for images, labels in test_loader: images = images.to(device) outputs = model(images) preds = outputs.argmax(dim=1).cpu() y_true.extend(labels.numpy()) y_pred.extend(preds.numpy()) cm = confusion_matrix(y_true, y_pred) print(classification_report(y_true, y_pred, target_names=['background', 'pothole'])) plt.figure(figsize=(6, 5)) plt.imshow(cm, cmap='Blues') plt.colorbar() plt.xticks([0, 1], ['background', 'pothole']) plt.yticks([0, 1], ['background', 'pothole']) plt.xlabel('Predicted') plt.ylabel('True') plt.tight_layout() plt.savefig('confusion_matrix.png', dpi=200)训练曲线是另一个必备图。在train_model里多收集两个列表,train_loss_list和val_acc_list,每个epoch追加一次,最后用双子图画出来。曲线图的价值在于向老师展示你看到了过拟合的发生过程——训练loss持续下降、验证准确率在第15轮左右见顶,于是你选择了第15轮的权重。报告的结论必须能追溯到图上的现象,而不是空口说“模型效果好”。
5. 避坑指南:道路坑洼检测大作业的5个翻车现场与排查方法
5.1 现象:训练时报CUDA Out of Memory,代码没有CPU回退
大作业的常见配置是实验室机器有GPU但显存只有4GB。模型和数据稍大一点,训练就报CUDA Out of Memory,代码直接崩掉。原因通常是batch size设得太大,或者代码里没有先检测设备再分配参数。
解决方法是先判断设备再设置batch size,同时显存不足时逐级降batch size并同步降学习率。batch size从32降到16再降到8,学习率从1e-3降到5e-4,否则梯度估计的噪声变大,训练容易震荡。更彻底的做法是把图像尺寸从224降到160,特征图尺寸变小,显存占用能省下一半。注意不要在GPU机器上把batch size调到显存刚好装下的程度,训练时显存占用会波动,留出20%余量更稳。
5.2 现象:验证准确率虚高,答辩现场用新图一测就翻车
这是最常见的高分翻车现场:报告里写验证集95%,答辩现场拿手机拍了一张路面照片,模型直接判成background。大概率是数据泄漏——划分时没有按类别分层,或者测试集和训练集混入了同一段视频的相似帧。
连续视频帧之间的相似度极高,如果同一场景的帧被拆到train和test两个集合,测试集评估的就是“见过的数据”而不是泛化能力。解决办法是视频抽帧时至少相隔10帧取一帧,然后按拍摄事件分组划分,确保同一组视频帧只出现在一个集合里。还有一个自查方法:训练结束后从训练集随机抽10张图预测,准确率应接近100%;如果训练集准确率也不高,那是欠拟合而不是数据泄漏。
5.3 现象:模型把路面阴影全部报成坑洼
坑洼检测最典型的误检来源就是阴影。沥青路面上的裂缝阴影、树荫、轮胎印的边缘,在CNN看来和坑洼的视觉特征高度相似。解决这个问题光增加负样本不够,负样本里要专门包含“带阴影的正常路面”这一类。
我在第2章提到逆光拍摄,就是为了制造这类“欺骗样本”。把逆光下的正常路面、树荫下的路面、雨后倒影路面的图片全部归入background,让模型必须区分“阴影加边缘断裂”和“只是阴影”的差异。另一个有效做法是数据增强里加入随机亮度扰动,范围调到0.3以上,强迫模型不依赖绝对亮度而依赖形状特征。预测时可以加一个启发式后处理:坑洼区域的长宽比应该在一定范围内,过大或过小的高亮连通域直接过滤——但这只是治标,报告里要写明它是“启发式后处理”。
5.4 现象:预测脚本与训练时的预处理不一致
训练时用224x224输入,预测时直接用原始分辨率,模型输出的置信度莫名其妙,甚至把坑洼图判成背景。预处理不一致是新手最容易忽略的细节,因为代码不报错,只在结果上悄悄体现。
解决办法是把transform定义成独立的模块,训练脚本和推理脚本都从同一个文件import,而不是各自写一套。尤其注意两点:归一化的均值方差是否一致,以及有没有做RGB通道转换。还有一个隐蔽问题:验证集评估用DataLoader,测试集预测却写了另一个手写循环,两套代码路径的缩放和归一化细节容易出现微小差异。统一入口是根治手段。
5.5 现象:报告里只有accuracy一行字,没有过程可视化
这一条和代码无关,但对分数的影响比想象中大。很多人的训练过程只有控制台输出的loss和一行最终准确率,报告里没有训练曲线、没有混淆矩阵、没有预测样本展示。老师拿到这样的报告,无法判断你是否真正理解了这个任务——他能推断的只是“你跑通了别人写好的代码”。
拿高分不需要额外算力,只需要在每个epoch记录train_loss和val_acc,画一条曲线;在测试集上挑出四到六张典型图片,把预测标签和置信度打在图上;把第4章的混淆矩阵和第6章的热力图放上去。从“我跑了一个模型”到“我分析了一个模型”,分数差距就体现在这些细节里。
提示:这一章的所有排错经验,前提都是你已经在数据划分时固定了随机种子。随机种子不固定,所有曲线对比和验证集指标都不可复现,也就谈不上排查。
6. 从95再往上走:给分类模型加一个弱定位分支
6.1 用Grad-CAM生成热力图,让模型说清楚自己在看哪里
分类模型只输出一个标签,答辩老师最常问的问题就是:“你怎么知道模型是根据哪个区域判定有坑的?”Grad-CAM热力图是这个问题最直接的回应。它的原理是求分类得分对最后一个卷积层特征图的梯度,按通道加权得到空间重要性图,不需要改动模型结构、不需要额外标注。
def grad_cam(model, input_tensor, target_class): """简化版Grad-CAM,返回与输入等宽高的热力图""" model.eval() input_tensor = input_tensor.unsqueeze(0).requires_grad_(True) features = model.features(input_tensor) output = model.classifier(features) score = output[0, target_class] score.backward() grads = input_tensor.grad.abs().mean(dim=1, keepdim=True) cam = (features.detach().mean(dim=1, keepdim=True) * grads).clamp(min=0) cam = torch.nn.functional.interpolate(cam, size=input_tensor.shape[2:], mode='bilinear', align_corners=False) return cam.squeeze().numpy()这个简化版避开了逐通道梯度统计的高计算量,直接用输入梯度做近似,热力图形状已经足够支撑“模型关注的是坑洼区域”这种定性结论。答辩时把原图、预测标签、热力图三张图并排摆在一页slide上,比任何文字都有说服力。注意模型里有Dropout,热力图生成前必须保持model.eval(),否则每次生成的结果都会抖动。
6.2 从分类到定位:加一个分割头输出像素级掩码
如果还想再增一个亮点,可以在训练好的分类模型上另加一个小分支:对输入图片输出二值掩码,标注坑洼的像素位置。不需要重新训练整个网络——冻结特征提取层,只训练解码部分。常用做法是取分类模型里256通道的14x14特征图,用转置卷积逐步上采样到224x224,再接一个1x1卷积输出单通道logits,损失用BCEWithLogitsLoss。训练几百轮后生成的掩码精度谈不上完美,但作为“弱监督定位对比实验”放进报告,已经足以说明你理解了语义分割的基本思路。
6.3 一个真实的边界案例:视频连续帧暴露了井盖误检
我在自己的坑洼检测项目里,最后用行车记录仪视频做了一次连续帧验证。结果模型对排水井盖产生了连续误报——井盖的圆形金属边缘和阴影让模型误判成坑洼。这个现象本身成了答辩时最有价值的素材:我能清楚说明模型的边界在哪里,并给出两个改进方向——增加“井盖”作为第三个负类别重新训练,或者在数据增强里加入圆形纹理的负样本。
这类大作业拿到95分以上,靠的不是代码多花哨,而是每一步都有依据、每个结果都有可视化、每个失败都能解释清楚。我的习惯是在提交前把整个项目从空目录开始重跑一遍,确保不依赖绝对路径、不需要手动修改代码、所有依赖都有记录。走过的坑越多,报告里的分析就越扎实。希望这条路径能帮你少走几天的弯路,祝你答辩顺利。
本文还有配套的精品资源,点击获取