
毕设选了“基于CNN的遥感图片识别”这个题目之后我第一时间想的是这跟普通图像分类有啥区别用ResNet跑一下看准确率不就行了真正动手才发现遥感影像和日常照片完全是两个世界——分辨率千差万别、类别严重不均衡、同一个地物在不同影像里尺度能差十倍甚至标签怎么打都够你纠结一整晚。这篇就完整梳理我从数据准备到模型训练、再到最终实验报告的全过程包括每一个环节踩过的坑和怎么绕开它们。整个过程不需要太高的数学门槛但需要你耐得住性子把数据吃透。文章最后会给出一个可以直接改改就用的完整代码框架用Python配合深度学习框架实现供正在做类似地物分类任务的同学直接参考。1. 选题拆解CNN到底在遥感图片识别里做了什么1.1 严格来说这属于什么任务从任务形式上看“沙漠、湖泊、森林”这三类地物的识别就是一个标准的图像分类问题输入一张遥感影像输出它属于哪个类别。但之所以很多同学在这个毕设上翻车是因为没有意识到遥感影像分类和普通图像分类的本质差异。普通图像分类的数据集比如日常拍的照片物体通常占据画面中心背景相对简单而且图片分辨率基本一致。遥感图则完全不同——它是从上往下看的俯视视角地物没有“正立”的概念同一个湖泊在不同影像里可能是湛蓝的矩形、不规则的条带甚至因为云层遮挡只漏出一部分。沙漠的纹理在干旱区和半干旱区也不一样森林从稀疏灌木到密集林区形态悬殊。这意味着模型不能靠单一颜色或者某个位置的像素来识别必须捕捉到更稳定的空间纹理和形态结构。1.2 CNN解决这个问题的核心机制是什么CNN能成为这个任务的主流方案靠的是三件事局部感受野、权值共享、层次化特征提取。打个比方一张遥感图摆在那里传统方法需要人手工设计特征——比如用颜色直方图判断“蓝色面积大的是湖泊”用灰度共生矩阵统计纹理来判断“纹理粗糙的是森林”。但遥感图里沙漠有风沙纹理森林有冠层纹理湖泊有水体边界这些特征靠人一个个去试工作量巨大而且换个数据分布就失效。CNN就不一样了。它的卷积核本质上是滑动窗口扫描全图每一层只关注局部区域然后通过堆叠多层把局部信息逐渐组合成全局语义。底层可能学到的是边缘、颜色块中间层学到的是纹理模式高层才能抽象出“湖泊”“森林”这类语义概念。这种自动学习特征的能力让CNN在处理地物种类多、形态变化大的遥感数据时具备天然优势。而且卷积核的参数在整个图上共享模型需要学习的参数量远小于全连接网络训练起来也更稳。1.3 为什么不用传统机器学习和更强的Transformer我在做方案调研时专门对比过几种路线结论很明确毕设阶段CNNs是性价比最高的选择。传统机器学习路线用OpenCV提取颜色、纹理、形状特征再喂给SVM或随机森林。这条路线的优点是训练快、可解释性强但在三分类且影像分辨率不一致的场景下手工特征很难覆盖所有情况。比如沙漠有时候颜色接近裸土湖泊水位低时边缘会出现大量泥滩这些边界情况靠手工特征很容易误判。视觉Transformer路线ViT在大型数据集上确实效果惊艳但它的训练需要海量数据和相对复杂的调参技巧。毕设的数据量通常只有几千到几万张图ViT很容易欠拟合而且从头训练的收敛速度远慢于CNN。如果你不是想拿它当亮点写论文创新点真心不建议在基础识别任务上死磕Transformer。所以最终方案就是用CNN作为主模型先用经典结构做baseline后续可以再尝试加深网络或者引入注意力模块作为优化方向。提示毕设的重点不是“堆最先进模型”而是把任务做扎实、把分析做完整。先跑通一个稳的baseline再一步步优化这个逻辑比一开始就上SOTA模型更容易写出高质量的论文。2. 数据准备决定项目上限的前置工作2.1 数据从哪来、怎么整理很多同学一上来就搜“遥感数据集”看到数据集包动辄几十GB就头大。实际上这个三分类任务完全不需要猛磕大规模数据集。我当时用的是某高校公开的遥感影像数据集直接从在线地图服务商按经纬度网格抓取了不同地区的影像切片然后手动筛选出了三类地物的样本。如果不想用在线抓取的方案也有一些成熟的小型遥感分类数据集可以选择挑那些包含沙漠、水域、植被类别的就行它们通常已经做了标注省去自己打标签的麻烦。自己整理数据时建议按下面的目录结构放data/ ├── train/ │ ├── desert/ # 沙漠 │ ├── lake/ # 湖泊 │ └── forest/ # 森林 ├── val/ │ ├── desert/ │ ├── lake/ │ └── forest/ └── test/ ├── desert/ ├── lake/ └── forest/这种按类别分类存放的方式可以直接用PyTorch的torchvision.datasets.ImageFolder读取省去写自定义Dataset的麻烦。2.2 图片裁剪与统一尺寸是第一个关键坑遥感影像的原始分辨率极其不统一——有的图是1000x1000有的是500x500如果直接缩放到统一尺寸会发生严重的地物变形。比如一个湖泊图从2000x2000缩到224x224水体面积占比信息虽然还在但细碎的边界纹理已经丢失了。更合理的做法是滑窗裁剪。把大图切成一堆256x256或224x224的小块每块作为一个样本这样既能把分辨率统一又能成倍扩充样本数量。切完之后再用PIL或者opencv做一次中心缩放检查剔除明显无意义的块比如纯云层、纯噪声区域。切图操作建议写个小脚本批量处理千万不要手动用画图工具一个个截批量脚本既快又不易出错。from PIL import Image import os def slide_crop(image_path, output_dir, crop_size256): img Image.open(image_path) img img.convert(RGB) w, h img.size idx 0 for top in range(0, h - crop_size 1, crop_size): for left in range(0, w - crop_size 1, crop_size): box (left, top, left crop_size, top crop_size) crop img.crop(box) crop.save(os.path.join(output_dir, f{os.path.basename(image_path)[:-4]}_{idx}.jpg)) idx 12.3 数据增强该做哪些、不该做哪些遥感图的增强策略和自然图像不太一样。自然图像里常用的水平翻转、垂直翻转在遥感图上完全可以用——因为卫星图没有“上下左右”的方向概念翻转不会破坏语义。这一点比较友好。另一个效果极好的增强是随机旋转90度、180度、270度同样是因为遥感图不存在“倒过来看”的问题。颜色抖动要特别小心。沙漠和湖泊的判定高度依赖颜色特征如果把沙漠的黄色调偏移成偏蓝色或者把湖泊的蓝色调偏移成绿色反而会引入错误的先验。我建议颜色类增强只做轻微亮度、对比度调整不要大幅修改色相饱和度。Scale和RandomResizedCrop也很常用因为遥感图中地物的尺度差异本来就大模型需要适应这种尺度变化。我用的是torchvision.transforms配置大概长这样train_transform transforms.Compose([ transforms.RandomResizedCrop(size224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomVerticalFlip(p0.5), transforms.RandomRotation(degrees90), transforms.ColorJitter(brightness0.3, contrast0.3), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])2.4 类别不均衡是个隐藏杀手我最初的数据里沙漠样本只有600多张湖泊有1800多张森林有3000多张。如果不做处理模型会倾向把所有输入都判成森林因为这样就能拿到约一半的正确率。解决类别不均衡有几种常用思路我组合使用了其中两种一是对少数类做过采样。简单来说就是每次从沙漠类里多取一些样本让三类样本在训练中出现的频率接近。PyTorch中可以用WeightedRandomSampler实现。二是用加权损失函数。给占比较小的类别更高的损失权重迫使模型在训练时更关注这些类。class_weights torch.tensor([2.0, 1.0, 0.6]) # 对应 desert, lake, forest criterion nn.CrossEntropyLoss(weightclass_weights)权重数值不需要太精确数量越少的类权重越大保证整体量级差不多就行。我试过把沙漠的权重拉到3.0效果反而变差因为模型开始把很多湖泊误判成沙漠。权重设置适度就好。3. 模型搭建从零写一个能跑的CNN框架3.1 网络结构设计与选型对比既然标题要求是CNN我不建议直接无脑加载一个预训练ResNet就完事那会缺少“毕设感”。更好的做法是自己写一个轻量CNN作为baseline然后再用ResNet18做对比实验。这样论文里既展示了你自己搭建网络的能力又有预训练模型的性能兜底实验对比也有内容可写。自己设计的CNN结构不需要太复杂4~5个卷积块加全连接层完全够用。核心设计思路是每个卷积层后面接BatchNorm和ReLU加速收敛每两个卷积块后接一个MaxPooling下采样逐步扩大感受野最后用全局平均池化替代Flatten加全连接减少参数量我把结构整理成表格方便对比模型方案参数量训练难度预期准确率适用场景自建浅层CNN(4层)约2M低易过拟合75%-85%验证任务可行性做baselineResNet18约11M适中88%-93%主模型追求稳定高精度ResNet18预训练权重约11M低90%-95%数据量不足时的最佳选择3.2 PyTorch实现关键代码自建CNN代码框架大概长这样import torch import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes3): super(SimpleCNN, self).__init__() self.features nn.Sequential( # Block 1 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 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), # Block 3 nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.Conv2d(128, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Dropout(0.3), nn.Linear(128, num_classes) ) def forward(self, x): x self.features(x) x self.classifier(x) return x在训练函数里除了常规的train_loss.backward()、optimizer.step()之外不要忘了每个epoch记录train_loss、val_loss和val_acc后面做曲线图都要用。3.3 预训练模型到底要不要用这个问题纠结了很久。从纯“深度学习课设”的角度预训练模型算是外挂因为权重是从ImageNet学来的不是你自己训练的。但从工程角度遥感数据样本少尤其是沙漠类只有600张图从头训练很容易过拟合或收敛到局部最优。而在ImageNet上训练过的模型底层已经学会了边缘、纹理、角点这些通用特征这些特征在遥感图上一样有效。我的做法是自建CNN从头训ResNet18加载ImageNet预训练权重然后微调。这样两个模型都有说服力自建CNN证明自己理解网络设计预训练ResNet保证最终准确率和稳定性。两个模型可以在论文里形成对比说明“数据量不足时迁移学习的效果”。加载预训练模型时有一个细节容易出错。默认resnet18(pretrainedTrue)输出的全连接层是1000类需要替换成3类import torchvision.models as models model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, 3)替换之后model.fc这一层是随机初始化的而前面的卷积层是预训练权重。这个时候如果设置全局统一学习率随机初始化的层会跟不上下游学习的速度。更好的做法是把不同层设置不同的学习率——卷积层用小学习率微调全连接层用大学习率从零学param_groups [ {params: model.features.parameters(), lr: 1e-4}, {params: model.fc.parameters(), lr: 1e-3} ] optimizer torch.optim.AdamW(param_groups, weight_decay1e-4)4. 训练与调参从loss曲线崩溃到稳定收敛4.1 超参数初始值怎么定训练之前超参数是最让人头疼的因为网上说法太多。我结合实践给出一个用于遥感三分类比较稳的初始值组合超参数取值说明输入尺寸224x224标准尺寸兼顾细节与显存Batch Size32显存不够就降到16不建议低于8学习率3e-4AdamW先用3e-4起步量级太大会loss震荡Epoch40配合早停一般到30轮左右就稳定了优化器AdamW比SGD收敛快毕设够用Weight Decay1e-4轻微过拟合就加这个标签平滑0.1提升泛化防止模型过自信这个组合不是拍脑袋定的。AdamW的默认学习率是1e-3但对遥感分类这种相对简单的任务来说1e-3经常会导致训练早期loss快速波动降到3e-4之后曲线明显平滑了很多。4.2 训练循环里的监控手段训练不是把数据扔进去就等着看结果。我的习惯是每个epoch结束之后输出一段日志包括当前轮数、训练损失、验证损失、验证准确率和当前学习率。Epoch [10/40] train_loss: 0.3123 val_loss: 0.2891 val_acc: 0.8914 lr: 2.0e-4 Epoch [20/40] train_loss: 0.1152 val_loss: 0.1598 val_acc: 0.9387 lr: 1.0e-4如果看到训练损失在下降但验证损失不降反升那就是过拟合信号需要及时调整不需要傻傻地把40轮全跑完。最好配合模型保存策略每次验证准确率创新低更高就保存一份权重文件后面测试直接用最好的那一份。if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth)4.3 过拟合问题实战处理遥感三分类任务模型很容易过拟合。我遇到的情况是训练集准确率一路飙到99%但验证集却卡在88%左右不动典型的泛化不够。处理手法按优先级排序先加数据增强。数据增强是最温和的抗过拟合手段把增强强度调大一点比如将RandomResizedCrop的scale范围从(0.7, 1.0)改成(0.5, 1.0)强迫模型学习尺度不变性。再加Dropout。在自建CNN的最后一个卷积层后面加一个nn.Dropout2d(p0.2)比只在分类器加Dropout更能抑制过拟合。最后调整Weight Decay。把AdamW的weight_decay从1e-4调到5e-4或者1e-3可以压低权重幅度限制模型的复杂度。如果真的用尽了手段验证集还不涨就要回头检查数据本身了——是不是训练集和验证集来自同一区域导致重叠过多或者某类样本数量太少根本不够学5. 实验评估与可视化别只盯着准确率5.1 准确率不是唯一指标三分类问题中准确率有一定说服力但不够。假设沙漠类只有600张森林类有3000张即便模型把所有沙漠都判错准确率也可能在75%以上。所以必须看每一类的精确率(Precision)、召回率(Recall)和F1分数。sklearn.metrics的classification_report可以直接输出这些指标如果不会看也不要紧只需要记住一点F1分数低的类别就是模型的短板需要专门分析。5.2 混淆矩阵能告诉你模型错在哪混淆矩阵的可读性远高于一堆小数。我训练完看到的第一版混淆矩阵最大的问题出现在沙漠和裸地之间的混淆但题目里只有沙漠、湖泊、森林三类按理说沙漠和湖泊的颜色差异很大不应该混淆。仔细查看误判样本才发现问题出在湖泊水位低的情况下边缘有大量泥滩和干涸区域纹理和颜色都接近沙漠。这个发现提示我可以在预处理时对水体指数相关的通道做增强或者增加泥滩类别的训练样本。这个结论写进毕设论文里是很有说服力的分析内容。5.3 CAM热力图让模型“开口说话”毕设答辩时老师问得最多的就是“你的模型到底学到了什么”解释不清就会显得空洞。Grad-CAM热力图是最直观的回答方式。Grad-CAM的基本原理是把最后一层卷积输出的特征图按类别梯度加权求平均得到一个“模型具体关注了图片哪部分”的热力图。如果模型把一张图判为湖泊热力图应该集中在湖泊水体的区域而不是旁边的森林或裸地。from torchcam.methods import GradCAM cam_extractor GradCAM(model, target_layerlayer4)跑完之后把热力图叠加到原图上保存选几个典型的正确样本和错误样本做对比分析这一页放到答辩PPT里效果非常好。6. 常见问题与排查实录踩过的坑都在这6.1 GPU显存不足训练到一半报CUDA out of memory是最常见的问题。如果是自建CNN32的batch size基本不会爆如果换了ResNet18batch size 32在老旧显卡上就可能爆显存。解决方案优先降低batch size到16或者把输入尺寸从224降到192。如果还不够打开torch.utils.checkpoint以少量计算时间换取显存占用降低。6.2 Loss不下降甚至反弹排查顺序是先确认数据加载没问题——看一个batch的图片和标签是否对应有没有标签错乱再确认模型输出维度是3和数据集的类别数一致然后降低学习率到1e-4试跑10个epoch如果还是不降把增强全部去掉跑一次排除是增强过度导致模型学不到规律。6.3 验证集准确率大幅波动如果每个epoch的验证准确率忽高忽低先怀疑验证集太小。遥感影像裁剪后验证集样本量如果少于200张波动自然大。把裁剪粒度调小或者多裁剪几轮增加样本量。其次怀疑学习率偏高降低学习率再看曲线平滑度。6.4 本地没GPU怎么训练有相当一部分同学手头没有独立显卡。我的经验是优先用云端免费算力资源比如一些大厂提供的Colab类免费Notebook服务足够跑完ResNet18的三分类任务。需要注意的点是把数据集打包成压缩包上传每次运行时解压缓存会节省很多上传时间。另外把代码按模块分开组织避免在一个Notebook里写到几百行后面改参数会让你怀疑人生。最后再分享一点个人实际操作的体会如果你现在正准备着手这个毕设我的建议是不要一开始就陷入模型调参的泥潭而是花更多时间在数据和实验设计上。数据整理得干净、标注正确、划分合理项目就成功了一半。CNN模型本身对这类任务非常成熟真正拉开差距的往往是你对数据分布的认知和实验分析的逻辑而不是哪个模型涨了两三个点。整个项目从零到完成我大概花了三周其中数据处理和可视化分析占了一半时间训练模型本身其实很快。希望这篇记录能帮你少走一点弯路。