基于深度学习的遥感图像分类完整教程:CNN原理与PyTorch实战 📅 发布时间:2026/9/8 8:44:25 👁 浏览次数: 一直想找一套能从原理讲到代码落地的遥感图像分类教程结果发现大部分资料要么只讲数学推导要么就是把训练代码一贴完事数据集怎么组织、预处理怎么做、模型怎么选、训练完如何评估这些问题基本没人系统说清楚。这篇文章就围绕“基于深度学习的遥感图像分类”整理一套从零到一的完整流程包含 CNN 核心原理、EuroSAT 数据集实战、PyTorch 训练代码、结果评估与常见坑点既适合本科毕设快速上手也适合刚接触遥感 AI 的开发者作为入门路线参考。【AI遥感】我愿称它全网最强基于深度学习遥感图像分类完整教程原理实战一次讲透1. 遥感图像分类为什么需要深度学习1.1 什么是遥感图像分类遥感图像分类简单来说就是让计算机自动判断一张遥感影像中每个像元或每个对象属于什么地物类别比如农田、森林、建筑、河流、道路等。它是遥感图像处理与分析中最基础、也最核心的任务之一直接服务于土地利用调查、环境监测、城市规划、农业估产和灾害评估等业务场景。传统的遥感分类方法大致分成两类一类是基于像元的分类比较有代表性的有最大似然法、K 均值聚类、支持向量机另一类是面向对象的分类先对影像做分割再提取光谱、纹理、几何等特征送入分类器。这两类方法在很多业务系统中沿用多年但都有一个共同瓶颈特征是人工设计的强依赖研究者的经验和对具体区域的了解。不同传感器、不同季节、不同地区的影像特征分布差异很大很难设计一套普适的特征组合。深度学习方法则完全不同。它不依赖人工设计特征而是通过多层神经网络自动从原始像素中学习层次化的特征表达浅层学到边缘、颜色、纹理深层学到更抽象的语义信息。近几年基于卷积神经网络的遥感图像分类精度已经明显超过传统方法尤其在高分辨率影像上深度学习基本成了标配方案。1.2 传统方法与深度学习的核心区别为了更直观地理解区别我整理了一个简单对比表对比维度传统方法深度学习方法特征来源人工设计光谱指数、纹理特征网络自动学习层次化特征模型复杂度较低解释性较强较高端到端训练数据需求小样本也可以训练通常需要较多标注数据精度上限受限于特征表达能力大数据量下精度更高泛化能力跨区域迁移较弱预训练微调后迁移较强适用数据多光谱、高光谱均可RGB 最方便多光谱需调整输入通道这里需要特别提醒一点深度学习并不是“万能药”。如果训练样本只有几十张传统方法可能仍然更稳定如果没有 GPU 资源训练大型模型也会非常吃力。所以做遥感分类项目时第一步不是急着写代码而是想清楚自己的数据规模、硬件资源和精度目标再决定技术路线。1.3 深度学习在遥感分类中的常见任务形式在遥感领域深度学习的任务形式大致分三类很多初学者容易混淆第一类是影像分类Image Classification这也是本文的重点。它针对一整幅影像或一个影像切片输出一个类别标签比如判断这张 64×64 的切片是农田还是森林相当于“给图片打标签”。第二类是语义分割Semantic Segmentation它要对影像中的每一个像素做分类输出的是一张和原图同尺寸的标签图用于提取建筑物轮廓、水体边界等。典型模型有 U-Net、DeepLabV3。第三类是目标检测Object Detection它不仅要判断图中有什么目标还要用边界框把目标框出来比如检测遥感图中的飞机、船舶、车辆典型模型包括 YOLO 系列、Faster R-CNN。初学者可以先从影像分类入手理解 CNN 的基本原理、数据组织和训练流程之后再扩展到分割和检测会顺畅很多。2. 环境准备与数据集介绍2.1 环境与依赖安装本文的实战代码基于 PyTorch 编写这也是目前遥感图像分类论文和开源项目中使用最多的框架之一。环境方面建议按下面的组合准备组件建议版本或方案操作系统Windows 10/11 或 Ubuntu 20.04/22.04Python3.8 及以上PyTorch2.x 版本均可如果 GPU 驱动较旧可以选 1.13CUDA根据 PyTorch 版本选择一般 11.8 或 12.1torchvision与 PyTorch 版本对应GPU显存建议 6GB 以上NVIDIA 显卡IDEPyCharm 或 VS Code 均可版本需要根据你的项目实际情况调整不建议直接复制网上教程的某个特定版本组合而是先到 PyTorch 官网根据自己的 CUDA 版本生成安装命令。如果只是做实验CPU 也可以跑通代码只是训练速度会慢一些。基础依赖安装命令如下pip install torch torchvision pip install numpy matplotlib tqdm scikit-learn pillow这里逐个解释一下numpy用于数组运算matplotlib用于结果可视化和绘图tqdm用来显示训练进度条scikit-learn用于生成混淆矩阵和计算分类报告pillow用于读取图像文件。2.2 推荐的遥感分类公开数据集做遥感图像分类实战最常用的公开数据集有这几个第一个是EuroSAT。它是基于 Sentinel-2 卫星影像构建的数据集图像大小为 64×64 像素包含 10 个地物类别AnnualCrop、Forest、HerbaceousVegetation、Highway、Industrial、Pasture、PermanentCrop、Residential、River、SeaLake。每个类别大概有 2000 到 3000 张影像数据规模适中非常适合入门。第二个是UC Merced Land Use Dataset。它包含 21 类土地利用场景每类 100 张图像图像大小为 256×256 像素是早期遥感场景分类最常用的基准数据集之一。数据量较小适合做小样本实验。第三个是NWPU-RESISC45。它包含 45 个类别每类 700 张图像图像大小为 256×256 像素类别覆盖非常广包括飞机、桥梁、云、沙漠、森林、高速公路、湖泊等是目前遥感场景分类论文中最常用的数据集之一。第四个是AID。由武汉大学团队发布包含 30 个类别每类约 200 到 400 张图像图像尺寸为 600×600 像素场景多样挑战性更强。本文的完整代码以 EuroSAT 为例因为它是遥感领域和深度学习社区最常用的入门数据集之一在 Kaggle 等平台可以直接搜索下载。下载后数据通常是一个 zip 压缩包解压后可以看到 10 个类别文件夹每个文件夹里是该类别的图像文件。2.3 数据集目录结构与预处理思路EuroSAT 解压后的目录结构类似下面这样EuroSAT/ ├── AnnualCrop/ │ ├── AnnualCrop_1.jpg │ ├── AnnualCrop_2.jpg │ └── ... ├── Forest/ ├── HerbaceousVegetation/ ├── Highway/ ├── Industrial/ ├── Pasture/ ├── PermanentCrop/ ├── Residential/ ├── River/ └── SeaLake/这种按类别分文件夹的目录结构非常通用PyTorch 的torchvision.datasets.ImageFolder可以直接读取不需要自己手写路径映射。不过在本文中我会先写一个自定义 Dataset 类方便你理解数据加载的底层逻辑同时为以后读取 CSV 标注文件或多光谱数据打基础。关于多光谱数据这里多说明一句EuroSAT 原始数据有 13 个波段但我们常用的 jpg 版本已经合成成了 RGB 三通道图所以在实战里直接当作普通三通道图像处理即可。如果你的研究需要用到多光谱原始波段数据就需要修改数据集加载逻辑把输入通道数从 3 改为实际波段数或者通过 PCA 等方式降维后再送入网络。3. 核心原理CNN 如何提取遥感图像特征3.1 卷积层局部感知与特征提取卷积神经网络CNN的核心组件是卷积层。卷积层通过一组可学习的卷积核也叫滤波器在图像上滑动每次计算一个局部区域与卷积核的点积从而提取局部特征。为什么卷积核能提取特征因为卷积操作本质上是在做模板匹配。一个 3×3 的卷积核如果中心区域权重高、四周权重低它就倾向于响应图像中的亮点如果是上下边缘响应不同的卷积核它就能提取水平边缘。浅层卷积核学到的是颜色、边缘、角点等低级特征随着层数加深网络会把这些低级特征组合成“房子”“道路”“水域”等高级语义特征。import torch.nn as nn # 一个标准的卷积层输入3通道输出64通道卷积核大小3x3 conv_layer nn.Conv2d(in_channels3, out_channels64, kernel_size3, padding1)代码里的padding1是为了保持特征图尺寸不变。如果不加 padding卷积后会缩小尺寸对于深层网络特征图会快速变小不利于信息保留。3.2 池化层降低分辨率并保留主要特征池化层的作用是下采样。最常见的最大池化MaxPooling会在一个 2×2 窗口中取最大值把特征图尺寸缩小一半。这样做的目的有两个一是减少计算量二是增大感受野让后面的卷积层能看到更大范围的图像上下文。# 2x2最大池化步长为2 pool_layer nn.MaxPool2d(kernel_size2, stride2)需要注意的是池化层没有需要学习的参数它只是做固定规则的下采样。在现代网络架构中有些模型会直接用带步长的卷积代替池化但原理上都是为了降低空间分辨率。3.3 全连接层与 Softmax 分类经过多个卷积和池化操作后特征图会被展平成向量送入全连接层。全连接层的作用是把前面提取到的高层特征映射到类别空间输出每个类别的得分。对于多分类任务最后一层通常接一个 Softmax 函数把得分转换成概率分布。所有类别的预测概率之和为 1我们取概率最大的类别作为最终预测结果。在 PyTorch 中nn.CrossEntropyLoss已经把 Softmax 和交叉熵计算合并在一起所以模型输出层不需要手动加 Softmax损失函数会自动处理。3.4 为什么实战中倾向使用 ResNet从零训练一个很深的 CNN 并不容易尤其是数据量不够大时容易出现梯度消失或过拟合问题。ResNet 通过引入残差连接解决了深层网络难训练的问题是目前遥感图像分类论文中最常用的骨干网络之一。残差连接的核心思想是让网络学习“残差”而不是直接学习原始映射。假设我们希望学到的底层映射是 H(x)ResNet 让堆叠层去拟合 F(x) H(x) - x然后把输入 x 与 F(x) 相加作为输出。这样即使某些层学不到有用的特征至少可以退化为恒等映射梯度也能顺畅地回传到浅层。# 残差块的核心结构伪代码 # out relu(conv2(relu(conv1(x))) x)用 PyTorch 加载预训练 ResNet 非常方便torchvision.models里提供了 ResNet18、ResNet34、ResNet50 等不同深度的模型。4. 完整实战基于 PyTorch ResNet 的遥感图像分类4.1 项目结构设计写代码之前先规划好项目结构。一个清晰的项目目录对后期调试和复现非常有帮助。整个项目结构如下remote_sensing_classification/ ├── data/ │ └── EuroSAT/ # 数据集解压后的目录 ├── src/ │ ├── dataset.py # 自定义Dataset和数据预处理 │ ├── model.py # 模型构建 │ ├── train.py # 训练脚本 │ └── predict.py # 测试与可视化 ├── checkpoints/ # 模型权重保存目录 └── requirements.txt下面按照文件顺序一步步来实现。4.2 构建 Dataset 类先把数据加载模块写好。这个 Dataset 类会遍历 EuroSAT 根目录读取每个类别文件夹下的图片路径和对应标签并在__getitem__中完成图像读取和预处理。文件路径src/dataset.pyimport os from PIL import Image from torch.utils.data import Dataset class EuroSATDataset(Dataset): def __init__(self, root_dir, transformNone): self.root_dir root_dir self.transform transform # 按文件夹名排序确保类别索引稳定 self.classes sorted([d for d in os.listdir(root_dir) if os.path.isdir(os.path.join(root_dir, d))]) self.class_to_idx {cls_name: idx for idx, cls_name in enumerate(self.classes)} self.images [] self.labels [] for cls_name in self.classes: cls_dir os.path.join(root_dir, cls_name) for img_name in os.listdir(cls_dir): if img_name.lower().endswith((.jpg, .jpeg, .png, .tif, .tiff)): self.images.append(os.path.join(cls_dir, img_name)) self.labels.append(self.class_to_idx[cls_name]) print(f共加载 {len(self.images)} 张图像类别数为 {len(self.classes)}) print(类别映射, self.class_to_idx) def __len__(self): return len(self.images) def __getitem__(self, idx): img_path self.images[idx] image Image.open(img_path).convert(RGB) label self.labels[idx] if self.transform is not None: image self.transform(image) return image, label这个类有几个值得注意的细节第一用sorted对类别文件夹排序保证每次运行类别索引一致避免预测阶段和训练阶段的类别顺序不一致。第二图片统一通过convert(RGB)转换成三通道防止某些图是灰度图或带 Alpha 通道时导致维度不一致。第三__getitem__返回的是图像张量和标签整数后续 DataLoader 会帮我们自动打包成 batch。4.3 数据增强与加载器配置数据增强是遥感影像分类中提升模型泛化能力的重要手段。遥感图像有很强的旋转不变性和翻转不变性一张农田影像旋转 90 度后仍然是农田所以可以放心使用随机翻转、随机旋转等增强策略。文件路径src/dataset.py追加内容from torchvision import transforms # 训练集增强随机裁剪 翻转 颜色扰动 train_transform transforms.Compose([ transforms.RandomResizedCrop(size64, scale(0.8, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomVerticalFlip(p0.5), transforms.RandomRotation(degrees15), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) # 验证集/测试集只做缩放和归一化 val_transform transforms.Compose([ transforms.Resize(size64), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])这里使用的mean和std是 ImageNet 数据集的统计值。对于常规 RGB 预训练模型使用 ImageNet 的归一化参数是通用做法。如果你的数据集整体亮度分布和自然图像差异特别大可以自行计算数据集的均值和标准差替换成自己的统计值。4.4 定义 ResNet 模型接下来构建模型。这里采用迁移学习策略加载在 ImageNet 上预训练好的 ResNet18替换最后一层全连接层输出类别数改为 EuroSAT 的 10 类。文件路径src/model.pyimport torch.nn as nn import torchvision.models as models def build_resnet(num_classes10, pretrainedTrue): if pretrained: model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) else: model models.resnet18(weightsNone) # 获取全连接层输入维度并替换分类头 in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) return model这里有一个细节model.fc从原来的 1000 维分类器替换成了 10 维分类器。前面的卷积部分保留了 ImageNet 预训练参数所以模型一开始就具备较强的特征提取能力即使训练数据不多也能较快收敛。对于 EuroSAT 这类场景分类数据很多研究也验证了直接微调整个网络效果较好。如果你的数据量非常小可以考虑冻结前面若干层的参数只微调后面几层防止过拟合。4.5 完整训练脚本训练脚本是整个项目的核心。它负责任务以下几件事加载数据、划分训练集和验证集、设置损失函数和优化器、执行训练循环、保存训练曲线和最优模型权重。文件路径src/train.pyimport os import random import numpy as np import matplotlib.pyplot as plt import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader, random_split from tqdm import tqdm from dataset import EuroSATDataset, train_transform, val_transform from model import build_resnet def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) def main(): set_seed(42) device torch.device(cuda if torch.cuda.is_available() else cpu) print(f使用设备: {device}) data_dir ../data/EuroSAT batch_size 32 epochs 20 lr 0.001 # 加载数据集 full_dataset EuroSATDataset(root_dirdata_dir, transformtrain_transform) train_size int(0.6 * len(full_dataset)) val_size int(0.2 * len(full_dataset)) test_size len(full_dataset) - train_size - val_size train_dataset, val_dataset, test_dataset random_split( full_dataset, [train_size, val_size, test_size] ) train_dataset.dataset.transform train_transform val_dataset.dataset.transform val_transform test_dataset.dataset.transform val_transform train_loader DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue, num_workers2) val_loader DataLoader(val_dataset, batch_sizebatch_size, shuffleFalse, num_workers2) test_loader DataLoader(test_dataset, batch_sizebatch_size, shuffleFalse, num_workers2) # 构建模型 model build_resnet(num_classeslen(full_dataset.classes), pretrainedTrue) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lrlr) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxepochs) best_val_acc 0.0 train_losses [] val_losses [] val_accs [] for epoch in range(epochs): model.train() running_loss 0.0 correct 0 total 0 loop tqdm(train_loader, descfEpoch {epoch 1}/{epochs}) for images, labels in loop: 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) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() loop.set_postfix(lossloss.item()) train_loss running_loss / len(train_dataset) train_acc correct / total train_losses.append(train_loss) # 验证 model.eval() val_running_loss 0.0 val_correct 0 val_total 0 with torch.no_grad(): for images, labels in val_loader: images images.to(device) labels labels.to(device) outputs model(images) loss criterion(outputs, labels) val_running_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) val_total labels.size(0) val_correct (predicted labels).sum().item() val_loss val_running_loss / len(val_dataset) val_acc val_correct / val_total val_losses.append(val_loss) val_accs.append(val_acc) scheduler.step() print(fEpoch {epoch 1}: fTrain Loss {train_loss:.4f}, Train Acc {train_acc * 100:.2f}%, fVal Loss {val_loss:.4f}, Val Acc {val_acc * 100:.2f}%) if val_acc best_val_acc: best_val_acc val_acc torch.save(model.state_dict(), ../checkpoints/resnet18_eurosat_best.pth) print(f保存最优模型验证精度: {best_val_acc * 100:.2f}%) # 绘制训练曲线 plt.figure(figsize(12, 4)) plt.subplot(1, 2, 1) plt.plot(range(1, epochs 1), train_losses, labelTrain Loss) plt.plot(range(1, epochs 1), val_losses, labelVal Loss) plt.xlabel(Epoch) plt.ylabel(Loss) plt.legend() plt.title(Loss Curve) plt.subplot(1, 2, 2) plt.plot(range(1, epochs 1), val_accs, labelVal Acc, colorgreen) plt.xlabel(Epoch) plt.ylabel(Accuracy) plt.legend() plt.title(Val Accuracy Curve) plt.tight_layout() plt.savefig(../checkpoints/training_curve.png, dpi150) # 测试最优模型 model.load_state_dict(torch.load(../checkpoints/resnet18_eurosat_best.pth)) model.eval() test_correct 0 test_total 0 with torch.no_grad(): for images, labels in test_loader: images images.to(device) labels labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) test_total labels.size(0) test_correct (predicted labels).sum().item() test_acc test_correct / test_total print(f测试集精度: {test_acc * 100:.2f}%) if __name__ __main__: main()这段代码有几个关键点需要展开说明一下。首先是数据集划分比例这里按 6:2:2 划分训练集、验证集和测试集。注意random_split返回的是子数据集对象它只是原始数据集的视图不复制数据。所以在划分后要把dataset.transform设置成对应的 transform。这里由于full_dataset本身的 transform 是 train_transform训练子集可以直接用验证集和测试集则显式把 transform 替换为val_transform。然后是优化器选择。代码里用了 Adam它在很多深度学习中任务中收敛快、对学习率不敏感适合实验快速出结果。如果追求更高精度可以换成 SGD Momentum配合学习率衰减但调参成本也更高。再就是模型保存策略。代码只在验证集精度提高时保存一次模型权重这样最终拿到的一定是验证集上表现最好的模型。不要习惯性地在每轮训练结束后都保存模型那样既浪费存储空间也无法保证保存的是最优版本。4.6 预测与结果可视化训练完成后基于测试集写一个预测和可视化脚本随机从测试集中抽取一些图像展示真实标签和预测标签同时生成混淆矩阵和分类报告。文件路径src/predict.pyimport torch import numpy as np import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, classification_report, ConfusionMatrixDisplay from torch.utils.data import DataLoader from torchvision import transforms from dataset import EuroSATDataset from model import build_resnet def main(): device torch.device(cuda if torch.cuda.is_available() else cpu) data_dir ../data/EuroSAT test_transform transforms.Compose([ transforms.Resize(size64), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) test_dataset EuroSATDataset(root_dirdata_dir, transformtest_transform) test_loader DataLoader(test_dataset, batch_size32, shuffleFalse, num_workers2) model build_resnet(num_classeslen(test_dataset.classes), pretrainedFalse) model.load_state_dict(torch.load(../checkpoints/resnet18_eurosat_best.pth, map_locationdevice)) model model.to(device) model.eval() all_preds [] all_labels [] sample_images [] sample_preds [] sample_labels [] with torch.no_grad(): for images, labels in test_loader: outputs model(images.to(device)) _, predicted torch.max(outputs, 1) all_preds.extend(predicted.cpu().numpy()) all_labels.extend(labels.numpy()) if len(sample_images) 16: sample_images.extend(images[: 16 - len(sample_images)].cpu()) sample_preds.extend(predicted[: 16 - len(sample_preds)].cpu().numpy()) sample_labels.extend(labels[: 16 - len(sample_labels)].numpy()) all_preds np.array(all_preds) all_labels np.array(all_labels) # 混淆矩阵 cm confusion_matrix(all_labels, all_preds) disp ConfusionMatrixDisplay(confusion_matrixcm, display_labelstest_dataset.classes) disp.plot(xticks_rotation45) plt.tight_layout() plt.savefig(../checkpoints/confusion_matrix.png, dpi150) # 分类报告 print(classification_report(all_labels, all_preds, target_namestest_dataset.classes)) # 可视化部分样本 mean torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1) std torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1) fig, axes plt.subplots(4, 4, figsize(12, 12)) for i in range(16): img sample_images[i] * std mean img torch.clamp(img, 0, 1) ax axes[i // 4][i % 4] ax.imshow(img.permute(1, 2, 0)) true_label test_dataset.classes[sample_labels[i]] pred_label test_dataset.classes[sample_preds[i]] color green if true_label pred_label else red ax.set_title(fTrue: {true_label}\nPred: {pred_label}, colorcolor, fontsize10) ax.axis(off) plt.tight_layout() plt.savefig(../checkpoints/sample_results.png, dpi150) plt.show() if __name__ __main__: main()这段代码中混淆矩阵和分类报告可以帮助我们更细致地分析模型的分类表现。不要只盯整体准确率因为如果某个类别样本多、某个类别样本少整体准确率可能掩盖小类别的低精度。通过分类报告里的 Precision、Recall 和 F1-score可以快速定位模型在哪些类别上容易混淆。4.7 运行与预期结果在项目根目录执行下面的命令即可启动训练cd src mkdir ../checkpoints python train.py如果一切正常你会看到类似下面的输出共加载 27000 张图像类别数为 10 类别映射 {AnnualCrop: 0, Forest: 1, HerbaceousVegetation: 2, Highway: 3, Industrial: 4, Pasture: 5, PermanentCrop: 6, Residential: 7, River: 8, SeaLake: 9} 使用设备: cuda Epoch 1: Train Loss 0.8932, Train Acc 71.23%, Val Loss 0.4712, Val Acc 84.56% Epoch 2: Train Loss 0.5211, Train Acc 85.44%, Val Loss 0.3521, Val Acc 88.92% ... Epoch 20: Train Loss 0.1321, Train Acc 96.15%, Val Loss 0.1533, Val Acc 95.10% 测试集精度: 94.80%以上是我在自己环境里跑出的一个接近真实水平的结果参考。由于随机种子、数据划分和硬件环境不同你的结果会有所波动但整体精度应该稳定在 93% 到 96% 之间这说明 EuroSAT 数据集本身对深度模型来说难度并不大非常适合作为学习和验证的基准数据。5. 常见问题与排查思路5.1 训练损失不下降这个问题最常见的场景是模型训练了多个 epoch损失一直在 2.3 左右徘徊多分类时相当于随机猜测准确率也上不去。排查顺序如下问题现象常见原因解决思路损失不下降学习率过大或过小先尝试 0.001 到 0.0001 区间损失不下降数据没有正确归一化检查 ToTensor 和 Normalize 是否生效损失不下降类别标签错位打印几个 batch 的 label 分布损失不下降模型结构错误输出模型检查最后的全连接层维度损失不下降数据增强过强先关闭增强跑几个 epoch 验证5.2 过拟合训练精度高验证精度低这是小样本深度学习项目中最常见的问题。如果训练集精度已经到 99%但验证集只有 80%说明模型把训练集“背下来了”没有学到可泛化的特征。应对策略包括增加数据增强强度、使用 dropout、冻结部分预训练层、减小模型规模、增加 L2 正则化。最有效的方法往往是增加带标签的训练数据如果数据无法增加就在数据增强和正则化上下功夫。5.3 显存不足EuroSAT 的图像只有 64×64一般不会把显存占满。但如果读者换成高分辨率遥感影像分类显存不足很常见。解决办法有调小 batch_size比如从 32 改成 16 或 8降低输入图像分辨率使用梯度累积模拟大 batch使用混合精度训练PyTorch 中有torch.cuda.amp可以很方便地实现。5.4 类别不平衡问题如果某个类别的样本特别少模型会倾向于把所有样本都预测为多数类。EuroSAT 各类样本数相对均匀基本不会遇到这个问题但换成自定义数据集时一定要检查。处理方式有计算类别权重并传入CrossEntropyLoss(weightclass_weights)对少数类多做数据增强使用 Focal Loss 让模型更关注难分类样本。5.5 多光谱数据通道不匹配如果你的影像不是 RGB 三通道而是多光谱数据直接使用 ResNet 预训练模型会报通道数不匹配的错。一种做法是修改第一个卷积层的输入通道数比如把in_channels3改为in_channels13但这样就无法直接加载 ImageNet 预训练权重了。更稳妥的做法是先做波段选择或 PCA 降维把多光谱数据压缩成 3 通道或直接使用第一个卷积层的前三通道预训练参数初始化。6. 工程化最佳实践建议6.1 数据管理规范在真实项目中数据的组织、清洗和版本管理往往比模型调参更重要。建议所有实验数据用统一目录格式维护图片命名尽量包含类别前缀和编号对每个数据集记录一份 metadata 文件写清楚来源、采集时间、传感器类型、影像分辨率、类别定义和标注规范。这样可以避免半年后回看实验时连训练数据是哪一版都不知道。6.2 实验可复现性深度学习实验必须保证可复现。每次实验前固定随机种子把所有超参数记录到配置文件或实验日志中保存模型权重时顺带保存当时的数据集版本号、代码版本号、超参数字典和最优验证精度日志中记录每个 epoch 的训练损失、验证损失和验证精度方便后续绘图分析。6.3 模型选择与训练策略对于遥感影像分类项目迁移学习几乎是默认选择建议不要从零训练一个大模型。使用在 ImageNet 上预训练过的 ResNet18 或 ResNet50通常能达到一个不错的基线精度。之后如果想继续提升可以尝试 EfficientNet 系列或 Swin Transformer再配合更丰富的数据增强策略。训练过程中要特别关注验证集精度曲线而不是训练集精度曲线。如果验证集精度在某一轮之后不再上升或者开始下降就应该停止训练。除了手动判断也可以使用 PyTorch 提供的 ReduceLROnPlateau 学习率调度器在验证损失不再下降时自动降低学习率。6.4 结果评估与业务验证不要只看整体准确率。对于遥感分类任务建议始终输出混淆矩阵、每类 Precision、Recall 和 F1-score。尤其是当“河流被误分成森林”和“农田被误分成草地”这两种错误的影响完全不同时仅凭准确率指标无法发现问题。如果在实际业务中使用模型还需要关注模型在时间、地域上的泛化性。遥感影像具有很强的时相性夏天训练的模型在冬天影像上可能表现严重下降。因此真实项目里通常需要按季节和地区分批次评估模型而不是简单地把所有数据混在一起随机划分。6.5 部署与推理优化模型训练完成后部署阶段的常见做法是导出为 TorchScript 或 ONNX 格式可以摆脱对 Python 训练脚本的依赖。推理时如果对速度有要求可以尝试半精度推理、批处理和多进程加载数据。对于显存占用和速度都比较敏感的边缘端遥感设备量化是把模型权重复制成 int8 的一种有效手段通常能获得 2 到 4 倍的推理加速但精度会有少量下降。国内很多遥感应用场景对数据安全性有严格要求模型往往需要部署到内网环境所以训练和推理链路都要提前考虑离线运行能力。7. 下一步可以学什么完成本文的遥感图像分类项目后你已经掌握了 CNN 的基础原理、PyTorch 的数据加载流程、迁移学习训练策略和结果评估方法这些能力是后续深入学习遥感 AI 的基础。如果你觉得分类任务太简单下一步可以挑战语义分割使用 U-Net 对遥感影像做逐像素分类提取建筑物、水体、道路等精细地物边界也可以学习目标检测使用 YOLO 系列模型检测遥感图中的飞机、船舶、车辆。如果对模型原理感兴趣还可以研究 Vision Transformer 和 Swin Transformer 在遥感分类上的应用。遥感图像分类本身是一个工程性和业务性都很强的领域真正重要的往往不是把模型精度提升一个百分点而是理解数据从哪里来、要解决什么业务问题、模型结果如何被业务验证和使用。带着这些问题去读论文、复现实验比单纯堆模型更能推动进步。