简介:基于ResNet的遥感图像分类识别项目,主要面向遥感图像分析学习者和深度学习初学者,利用残差网络解决高分辨率、多光谱影像中建筑物、道路、水体等复杂地物的自动分类问题,在地物识别、土地利用分析等场景具有实践价值,也适合课程设计或入门项目参考。压缩包共10个文件,以7个Python脚本为主,将数据集加载、残差网络构建、训练、推理及GUI操作等模块分离;2个Markdown文档补充环境搭建、代码说明与使用步骤,另附1个结果记录txt,整体仅14KB,结构紧凑但流程完整。目前已有147人浏览学习。通过运行代码可直观了解从数据预处理、数据增强到模型评估的完整管线,文档对关键步骤进行了说明,能帮助降低上手门槛;源码模块划分清晰,便于进一步修改网络结构、调整训练策略或迁移到其他遥感场景。
1. 遥感图像分类识别:用 ResNet 把地物判读从「玄学」变成可复现
先抛个结论:遥感图像分类识别这件事,难的不是训练,而是把一整条链路跑通。你从网上下一个带源码的 ResNet 遥感分类项目,解压之后发现文件十几二十个,不知道先打开哪个、先跑哪条命令,这是最常见的卡点。这个资源就是为此准备的——它把数据加载、网络构建、训练、推理、GUI 预测打包成了一整套可运行工程,属于那种「拿到手就能在本地把流程走一遍」的完整代码包。它适用的任务很具体:识别遥感影像里的地物类别,比如建筑、道路、水体、农田、裸地、植被这一类。适合三类人:正在入门深度学习的开发者,做地信或遥感数据处理、想快速拿到一个分类 Bench 的工程师,以及课程设计或论文实验需要对比结果的研究生。下面我按「原理先立住、再一步步复现、最后讲坑」的顺序把它拆开。
2. 项目结构拆解与 ResNet 原理:先看懂代码再动手
这一章解决「这是什么、为什么选它、怎么运行」三个问题。资源包里的文件一眼看去容易乱,但实际每个文件都有明确分工,先弄清每个文件的定位再动手,能避免很多白跑的弯路。
2.1 资源包里到底有什么:逐文件定位
先看这份代码包的完整文件清单,我按功能把它们分了个组:
| 文件 | 定位 | 作用 |
|---|---|---|
| README.md | 入口文档 | 项目说明、快速开始方式 |
| help_doc.md | 辅助文档 | 环境搭建、模块依赖说明 |
| get_logger.py | 公共工具 | 日志打印与落盘配置 |
| rs_dataset.py | 数据层 | 遥感图像读取、预处理、数据增强 |
| res_network.py | 模型层 | ResNet 网络定义,含残差块结构 |
| trian_resnet.py | 训练入口 | 模型训练主脚本(注意文件名拼写) |
| infer.py | 推理脚本 | 加载权重进行单图或多图预测 |
| tst.py | 测试脚本 | 批量测试评估,输出分类结果 |
| gui.py | 可视化 | 图形界面加载模型、选图预测 |
| results.txt | 输出记录 | 训练或推理结果的文本保存 |
从这个清单能看出,它不是某个论文的残缺复现,而是一条完整的工程链:数据入口(rs_dataset)→ 网络定义(res_network)→ 训练(trian)→ 评估(infer/tst)→ 交互(gui)。我的习惯是先看 README 和 help_doc,再跑一遍 infer 确认环境没问题,最后才碰训练脚本。直接从训练开始的人,最容易因为漏装依赖而浪费时间。
2.2 ResNet 为什么适合遥感图像:从残差块说起
ResNet 在 2015 年提出,核心创新就是残差块和跳跃连接。传统 CNN 如 VGG,靠堆叠卷积层加深网络,但当层数超过十几层时,梯度回传会因连乘效应变得极小或极大,导致训练退化——这种现象在遥感图像上尤其明显,因为遥感影像分辨率高、纹理复杂,浅层模型根本学不出有效特征,可盲目加深又训不动。
残差块的数学表达为:输出 = F(x) + x。其中 x 是输入,F(x) 是一组卷积、批归一化、激活函数的组合结果。它把网络的优化目标从「直接学习 H(x)」变成了「学习残差 H(x) - x」。这个改动的价值在于:如果某层不需要额外变换,网络可以学出让 F(x)=0 的参数,输出退化为恒等映射 x,梯度可以直接通过跳跃连接传回浅层,从根上缓解了「网络越深越难训」的问题。遥感图像分类恰好需要这种能力——地物的边缘、纹理属于低层特征,建筑物、水体、道路这类语义概念属于高层特征,需要很深的网络才能把两者融合。ResNet 的多阶段设计(早期卷积抓边缘,后期卷积抓语义)天然贴合这个需求。
2.3 环境搭建与运行路径
虽然 help_doc.md 里写了环境教程,我还是把最常用的搭建命令整理出来,先在命令行确认 Python 和显卡状态:
# 建议用 conda 建独立环境,避免污染系统 Python conda create -n resnet_rs python=3.8 -y conda activate resnet_rs # 安装 PyTorch 全家桶,CUDA 版本按本机驱动选择 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 图像处理与基础库 pip install numpy opencv-python pillow matplotlib搭建完推荐按这个顺序走流程:先打开 help_doc.md 确认模型权重路径和数据路径,再运行python infer.py看能不能出预测结果,如果推理正常说明环境和模型文件都完整,然后再用训练脚本从头训。这个顺序能在半小时内确认「代码包本身没问题」,之后再改动参数就不会有「到底是我写错了还是环境缺东西」的困惑。需要说明的是,有的代码包会用到pandas、scikit-learn这类辅助库,遇到ModuleNotFoundError时缺什么装什么即可,不一定要照着 help_doc 全量安装。
3. 数据流水线与网络实现:把遥感图变成模型输入
这一章深入代码的实现层,是最值得逐行读的部分。遥感图像和自然图像最大的差异是:单张影像尺寸大、地物尺度变化大、多光谱通道多。数据层处理不好,模型再先进也学不出东西。
3.1 rs_dataset.py 解读:裁剪、归一化与数据增强
几乎所有的 PyTorch 数据脚本都是Dataset类加DataLoader的结构,这个项目里的rs_dataset.py也不例外。核心逻辑在__getitem__方法里,我按常见写法还原它的骨架:
import torch from torch.utils.data import Dataset from PIL import Image import numpy as np class RSDataset(Dataset): def __init__(self, img_paths, labels, transform=None): self.img_paths = img_paths # 图像路径列表 self.labels = labels # 类别标签列表 self.transform = transform # 预处理与增强管线 def __len__(self): return len(self.img_paths) def __getitem__(self, idx): # 遥感图像可能是 16bit 深度的 TIFF,注意转成 8bit 或直接归一化 image = Image.open(self.img_paths[idx]).convert("RGB") label = self.labels[idx] if self.transform: image = self.transform(image) return image, torch.tensor(label, dtype=torch.long)这里有几个对遥感特别重要的点。第一,Image.open()之后convert("RGB")是必须的——部分遥感图是单通道灰度,不转会报通道数错误。第二,标签用torch.long,因为CrossEntropyLoss不接受 float 标签。第三,真正的预处理逻辑通常在 transform 里,常见做法是:
# 训练阶段的组合:先放大后裁剪 + 随机翻转 + 归一化 transform_train = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop((224, 224)), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])Resize((256, 256))后再RandomCrop((224, 224))是一种常规操作:先放大到比目标尺寸大一些,再随机裁剪,相当于引入了位置偏移的数据增强。Normalize的 mean 和 std 是 ImageNet 的统计值,遥感图像虽然和自然图像有差异,但大多数代码包仍然采用这套参数,实测收敛速度会比不归一化快很多。如果你的遥感图是多光谱(比如 8 通道 Sentinel 数据),就不能直接convert("RGB"),需要按通道读取后单独处理,这是后话。
3.2 res_network.py 解读:残差块的 forward 写法
res_network.py里定义的是 ResNet 本体。核心组件是残差块,无论是BasicBlock还是Bottleneck,forward 写法都遵循一个模式:
import torch.nn as nn import torch.nn.functional as F class BasicBlock(nn.Module): def __init__(self, in_channels, out_channels, stride=1, downsample=None): super(BasicBlock, self).__init__() self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(out_channels) self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False) self.bn2 = nn.BatchNorm2d(out_channels) self.downsample = downsample def forward(self, x): identity = x out = F.relu(self.bn1(self.conv1(x)), inplace=True) out = self.bn2(self.conv2(out)) if self.downsample is not None: # 通道数变化或特征图尺寸减半时,需要对输入做映射 identity = self.downsample(identity) out += identity # 跳跃连接:残差相加 out = F.relu(out, inplace=True) return out这段代码最关键的是identity = x和out += identity这行相加操作。如果只是普通卷积堆叠,网络学到的是x -> H(x)的映射;有了这条捷径后,每层模块只需要学H(x) - x的残差。downsample参数决定了当通道数翻倍或步长为 2 时,输入如何对齐到输出的形状——通常用1x1卷积实现,这就是很多同学看到的conv1x1直接连接。整个ResNet50由四个 stage 组成,各 stage 的残差块数量是[3, 4, 6, 3],最后的全连接层输出数量要改成你的类别数。遥感分类里常见的设定是 6 类(建筑、道路、水体、农田、植被、裸地),对应最后一层nn.Linear(2048, 6)。如果资源里预训练权重是 1000 类 ImageNet 的,你想微调就只替换这一层,其他层参数原样加载。
3.3 数据加载参数怎么设
DataLoader的参数设置直接影响训练速度和显存占用,这里给出我常用的配置:
from torch.utils.data import DataLoader train_loader = DataLoader( train_dataset, batch_size=32, shuffle=True, num_workers=4, pin_memory=True, drop_last=False )batch_size可调性最高:显存 8G 以下用 16,8G 以上用 32,再大就得配合梯度累积。num_workers在 Windows 上建议设 0 或 2,设高了容易报 DataLoader worker 崩溃;Linux 服务器上 4 到 8 没问题。pin_memory=True能减少 CPU 到 GPU 的传输时间,只要内存够就建议开。drop_last在最后一轮批次不足时会导致 BatchNorm 统计量抖动,遥感类别多、数据量大的时候一般不开。
还有个容易被忽略的点:遥感原图往往非常大,比如 6000x4000 像素的卫星影像,直接整图进网络会撑爆显存。常见做法是切块——把大幅影像切成 224x224 或 256x256 的小 patch,然后对这些 patch 做分类。rs_dataset.py如果输入的是切好的图块路径,那说明数据预处理已经帮你做了这步;如果输入的是大图路径,你要么自己在脚本里加切块逻辑,要么先把图离线切好。
4. 训练与推理调优:trian_resnet.py 和 infer.py 的实操路线
数据层和网络结构都弄清楚后,接下来就是真正的训练与推理。这一章我会把训练脚本的关键配置逐项讲清楚,同时教你怎么从results.txt里判断模型到底练得怎么样。
4.1 训练脚本的关键配置
trian_resnet.py里最核心的是优化器、损失函数、学习率策略和模型保存。常见做法是:
import torch.optim as optim model = res_network.ResNet50(num_classes=6) criterion = nn.CrossEntropyLoss() # 优化器选 SGD 还是 Adam,遥感分类里 SGD 收敛更稳 optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=1e-4) # 学习率动态下降,每 10 个 epoch 缩小 10 倍 scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1) best_acc = 0.0 for epoch in range(30): model.train() running_loss = 0.0 for images, labels in train_loader: images = images.to(device) labels = labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * images.size(0) scheduler.step() # 每个 epoch 后保存最优模型 val_acc = evaluate(model, val_loader) if val_acc > best_acc: best_acc = val_acc torch.save({ 'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'best_acc': best_acc, }, 'best_model.pth')这段代码有几个值得说明的细节。第一个是全连接层替换:如果代码里没做迁移学习,你应该把num_classes改成匹配遥感数据集的实际类别数。第二个是关键配置表,我直接给出推荐值:
| 超参数 | 推荐值 | 说明 |
|---|---|---|
| batch_size | 32 | 显存小就降为 16,配合梯度累积 |
| 初始学习率 | 0.01(SGD)/ 0.001(Adam) | 加载预训练权重时用 0.001 更稳 |
| 优化器 | SGD + momentum 0.9 | Adam 收敛快但精度上限一般不如 SGD |
| weight_decay | 1e-4 | 防止过拟合,遥感小数据集尤其重要 |
| epoch | 30 起步 | 看验证集 acc 不再上升就早停 |
| 学习率策略 | StepLR(step=10, gamma=0.1) | 后期用小学习率精调 |
第三个是模型保存格式:上面用的是完整 checkpoint,包含model_state_dict和optimizer_state_dict,好处是中断训练后可以恢复断点继续跑。如果你只想保存权重用于推理,用torch.save(model.state_dict(), 'model_weights.pth')就够了,文件更小、加载也简单。需要恢复训练时只能加载完整 checkpoint 的格式。
4.2 训练日志怎么看
get_logger.py的作用是让训练过程输出的信息既打到控制台,又写入日志文件。运行训练脚本后,你会看到类似下面的输出:
[2025-06-01 10:23:01] Epoch [1/30] Loss: 1.7823 Acc: 18.25% [2025-06-01 10:27:45] Epoch [2/30] Loss: 1.3210 Acc: 42.37% [2025-06-01 10:32:10] Epoch [3/30] Loss: 0.9862 Acc: 61.08%判断训练是否正常,先看 loss 的下降曲线。CrossEntropyLoss的初始值大约等于类别数的自然对数,如果是 6 类任务,初始 loss 在 1.79 左右是正常的。如果第一个 epoch 后 loss 低于 0.1 或 acc 突然冲到 99%,那大概率是数据泄漏或标签错乱,后面会细说。我一般会记录「每 5 个 epoch」的验证集 acc,看它是否仍在上升。如果超过 5 个 epoch 持续不涨,就调整学习率或增加数据增强。Early Stopping可以简单实现:定义patience为 10,连续 10 个 epoch 验证集 acc 没创新高,就停止训练并恢复最优权重。
4.3 推理与评估:从 results.txt 读结论
训练完成后,infer.py负责加载权重对单张或多张图像做预测。标准的推理流程如下:
import torch from PIL import Image from torchvision import transforms device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = res_network.ResNet50(num_classes=6) checkpoint = torch.load('best_model.pth', map_location=device) # 注意:如果保存的是完整 checkpoint,要取 model_state_dict 字段 model.load_state_dict(checkpoint['model_state_dict']) 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]) ]) image = Image.open('test_area.png').convert('RGB') image = transform(image).unsqueeze(0).to(device) with torch.no_grad(): outputs = model(image) probs = torch.softmax(outputs, dim=1) # 转成概率分布 pred = torch.argmax(probs, dim=1).item() print(f"预测类别索引: {pred}, 置信度: {probs[0][pred].item():.4f}")推理阶段有两点极易踩坑。第一,一定要加with torch.no_grad(),否则模型计算图会被保留,显存迅速耗尽,尤其是在批量测试时。第二,model.eval()和model.train()的区别对 BatchNorm 层影响极大——eval 模式用全局移动平均统计量,train 模式用当前 batch 统计量,忘记切模式会导致推理结果严重偏移。加载权重时,如果checkpoint是字典格式,必须用checkpoint['model_state_dict']而不是直接传给load_state_dict,这是新手最容易报错的地方。
results.txt一般记录的是测试集上的评估结果。建议至少看三项:总体准确率、每个类别的召回率、混淆矩阵。遥感图像分类最常见的坑是类别不均衡——水体样本多、建筑物样本少,模型会把所有不确定的图块都判成水体,总体准确率看着不错,但建筑物类别的召回率可能只有 20%。所以在看results.txt时,别只盯着总 acc,逐类看 precision 和 recall 才能发现问题。
5. 踩坑记录:遥感分类里最容易翻车的五个真实问题
这套代码包我在不同环境、不同数据集上跑过多次,以下五个问题是最高频的翻车现场。每一条都是「现象 → 原因 → 解决」的完整链路,建议你在碰到同类问题时直接对照排查。
5.1 现象:跑 trian_resnet.py 直接 ModuleNotFoundError
明明按照 help_doc 装了依赖,但一执行训练脚本,报ModuleNotFoundError: No module named 'cv2'或'torchvision'。原因是环境混乱——你pip install到的包和运行脚本时激活的 conda 环境不是同一个 Python 解释器。排查方法:在命令行分别执行which python和pip list | grep torch,确认 pip 安装的包归属当前环境。解决:先conda activate resnet_rs,再执行python -m pip install torch torchvision numpy opencv-python,用python -m pip的方式能确保包装进当前解释器。注意,torch 和 torchvision 必须装匹配版本,比如 torch 2.0 配 torchvision 0.15,否则会出现undefined symbol的诡异报错。
5.2 现象:训练 loss 几乎不下降,或者第一轮就降到一个极低值
先看初始 loss。如果 6 类任务初始 loss 在 0.005 附近,那大概率是模型没有正确初始化——例如全连接层之前就接了Softmax,而损失函数又用了带Softmax的CrossEntropyLoss,导致梯度被双熟练层吞掉。正确的做法是:网络最后一层输出未归一化的 logits,CrossEntropyLoss内部自带 Softmax,两者不能重复。如果初始 loss 正常但完全不见下降,常见原因是学习率过大导致梯度在小范围内震荡,或者 BatchNorm 层在model.train()和model.eval()之间切换出了问题。解决:先用 0.001 学习率跑 5 个 epoch 确认 loss 能降,再调大或调小;同时检查优化器初始化时是否传入了model.parameters(),一个常见的低级错误是把model本身而非参数迭代器传给SGD。
5.3 现象:CUDA out of memory
遥感图像分辨率高,如果直接用原图尺寸进入网络,显存很容易爆掉。我之前处理过 12000x12000 的大规模影像,单张图砸进去显存直接不够。解决有两个方向:一是把输入尺寸降下来——统一 resize 到 512x512 或切块到 224x224,绝大多数遥感分类任务并不需要原分辨率;二是调整DataLoader参数——batch_size从 32 降到 16 或 8,num_workers从 4 降到 2。如果降 batch 后训练速度变慢,可以用梯度累积模拟大 batch:
accumulation_steps = 4 # 相当于 batch_size = 8 * 4 = 32 optimizer.zero_grad() for i, (images, labels) in enumerate(train_loader): outputs = model(images) loss = criterion(outputs, labels) / accumulation_steps loss.backward() if (i + 1) % accumulation_steps == 0: optimizer.step() optimizer.zero_grad()这种方式在显存受限时很实用,代价是训练时间会增加,且 BatchNorm 的统计量会随 batch 大小变化,需要适当调低学习率。
5.4 现象:推理结果全部是同一个类别
模型训练完成且准确率不低,但推理时所有图都判成「水体」或「裸地」。原因有两个,需要对号入座。第一个是类别不均衡:训练集中某一类占比超过 70%,模型学到的是「全猜这一类就能拿到不错的 loss」,对策是在损失函数里加类别权重,CrossEntropyLoss(weight=class_weights),class_weights按类别样本数的倒数计算,或直接用torch.nn.CrossEntropyLoss(weight=torch.tensor([...]))传入。第二个是推理预处理和训练不一致,比如训练时做了归一化、推理时忘了归一化,导致输入分布完全偏移。解决:打印一条推理样本的预测分布,若softmax输出的每个类别概率都接近均匀,则大概率是预处理不一致;若某一类概率接近 1,则是类别不均衡或数据泄漏。
5.5 现象:gui.py 加载模型失败,窗口打开后闪退
GUI 模块报错集中在两类。一类是FileNotFoundError,因为图形界面默认加载best_model.pth,而你没训练过、或模型保存到了其他路径。解决:在 gui.py 里加一个显式的模型路径选择,或者在加载前检查路径存在性。另一类是num_classes不匹配——GUI 脚本里写死了num_classes=1000(因为参考了预训练权重),但你的模型是 6 类,加载权重时维度对不上直接报错。解决:把res_network.py里的num_classes改成与训练时一致的数值,两侧保持一致。GUI 本质是调用已经训练好的模型做单张可视化预测,不要指望它能帮你训练。
6. 进阶用法:把 GUI 变成调试台,再做三个能提升模型精度的改造
最后这一章不讲泛泛而谈的优化建议,只讲我自己拿来提高结果可信度的三个具体动作。
第一个动作是把gui.py从「演示工具」变成「调试台」。GUI 除了能选图、跑预测、显示类别名之外,最重要的是能看到置信度数值。我在做多类别遥感图预测时,遇到预测置信度普遍低于 0.7 的情况,直接放弃优化网络,先检查数据。因为这种表现通常意味着输入图块里混入了大量类别边界区域——比如一个图块里既有一半建筑又有一半道路,模型输出自然会模棱两可。解决办法是在数据切块时设置重叠率(overlap),让每个地物目标尽量完整出现在至少一个图块中。这是个数据切块层面的技巧,比调网络结构见效快得多。
第二个动作是在残差块里加入 SE 模块,提升关键通道的权重。SE 模块的全称是 Squeeze-and-Excitation,它对每个通道做一个全局平均池化,再用两层全连接拟合出通道权重,把重要的特征通道放大、不重要的压缩。实现代码很短:
class SEBlock(nn.Module): def __init__(self, channels, reduction=16): super(SEBlock, self).__init__() self.fc1 = nn.Linear(channels, channels // reduction) self.fc2 = nn.Linear(channels // reduction, channels) def forward(self, x): b, c, _, _ = x.size() w = F.adaptive_avg_pool2d(x, 1).view(b, c) w = F.relu(self.fc1(w)) w = torch.sigmoid(self.fc2(w)).view(b, c, 1, 1) return x * w.expand_as(x)然后把SEBlock插入到残差块中两个卷积之后、跳跃连接相加之前即可。这个改动对遥感图像尤其有效:比如水体在近红外波段有很强的特征,SE 模块会自动放大相关通道的响应,让分类更稳。代价是参数量略有增加,训练时间多约 10%,但精度提升通常在 2 到 5 个百分点,属于性价比很高的改动。
第三个动作是训练前固定随机种子。这条看起来不起眼,却是在做算法对比时最重要的「后悔药」。PyTorch 里很多操作是随机的——参数初始化、数据加载打乱顺序、Dropout,每一步都会影响最终结果。如果不固定种子,同一套代码同一份数据跑两遍,得到的 val_acc 可能差 2% 以上,这时候你根本分不清是模型改了还是随机性导致的波动。我每次跑训练脚本前都会强制执行这一段:
import random import torch import numpy as np def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False set_seed(42)需要注意的是,cudnn.deterministic = True会让卷积算子选择固定算法,可能有微小的速度损失,但对结果的可复现性来说值得。从那以后我每次跑遥感图像训练,都强制走一遍「固定种子 → 检查初始 loss → 小学习率跑通 5 个 epoch → 再调参」的流程,翻车率直线下降。希望帮到你。
本文还有配套的精品资源,点击获取