基于深度学习的遥感图像分类完整教程:CNN原理与PyTorch实战
2026/9/8 8:44:20 网站建设 项目流程

一直想找一套能从原理讲到代码落地的遥感图像分类教程,结果发现大部分资料要么只讲数学推导,要么就是把训练代码一贴完事,数据集怎么组织、预处理怎么做、模型怎么选、训练完如何评估,这些问题基本没人系统说清楚。这篇文章就围绕“基于深度学习的遥感图像分类”整理一套从零到一的完整流程,包含 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.04
Python3.8 及以上
PyTorch2.x 版本均可,如果 GPU 驱动较旧可以选 1.13
CUDA根据 PyTorch 版本选择,一般 11.8 或 12.1
torchvision与 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_channels=3, out_channels=64, kernel_size=3, padding=1)

代码里的padding=1是为了保持特征图尺寸不变。如果不加 padding,卷积后会缩小尺寸,对于深层网络,特征图会快速变小,不利于信息保留。

3.2 池化层:降低分辨率并保留主要特征

池化层的作用是下采样。最常见的最大池化(MaxPooling)会在一个 2×2 窗口中取最大值,把特征图尺寸缩小一半。这样做的目的有两个:一是减少计算量,二是增大感受野,让后面的卷积层能看到更大范围的图像上下文。

# 2x2最大池化,步长为2 pool_layer = nn.MaxPool2d(kernel_size=2, stride=2)

需要注意的是,池化层没有需要学习的参数,它只是做固定规则的下采样。在现代网络架构中,有些模型会直接用带步长的卷积代替池化,但原理上都是为了降低空间分辨率。

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.py

import os from PIL import Image from torch.utils.data import Dataset class EuroSATDataset(Dataset): def __init__(self, root_dir, transform=None): 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(size=64, scale=(0.8, 1.0)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomVerticalFlip(p=0.5), transforms.RandomRotation(degrees=15), transforms.ColorJitter(brightness=0.2, contrast=0.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(size=64), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ])

这里使用的meanstd是 ImageNet 数据集的统计值。对于常规 RGB 预训练模型,使用 ImageNet 的归一化参数是通用做法。如果你的数据集整体亮度分布和自然图像差异特别大,可以自行计算数据集的均值和标准差,替换成自己的统计值。

4.4 定义 ResNet 模型

接下来构建模型。这里采用迁移学习策略:加载在 ImageNet 上预训练好的 ResNet18,替换最后一层全连接层,输出类别数改为 EuroSAT 的 10 类。

文件路径:src/model.py

import torch.nn as nn import torchvision.models as models def build_resnet(num_classes=10, pretrained=True): if pretrained: model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) else: model = models.resnet18(weights=None) # 获取全连接层输入维度并替换分类头 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.py

import 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(seed=42): 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_dir=data_dir, transform=train_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_size=batch_size, shuffle=True, num_workers=2) val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False, num_workers=2) test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False, num_workers=2) # 构建模型 model = build_resnet(num_classes=len(full_dataset.classes), pretrained=True) model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=lr) scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs) 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, desc=f"Epoch {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(loss=loss.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(f"Epoch {epoch + 1}: " f"Train Loss {train_loss:.4f}, Train Acc {train_acc * 100:.2f}%, " f"Val 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, label="Train Loss") plt.plot(range(1, epochs + 1), val_losses, label="Val 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, label="Val Acc", color="green") plt.xlabel("Epoch") plt.ylabel("Accuracy") plt.legend() plt.title("Val Accuracy Curve") plt.tight_layout() plt.savefig("../checkpoints/training_curve.png", dpi=150) # 测试最优模型 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.py

import 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(size=64), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) test_dataset = EuroSATDataset(root_dir=data_dir, transform=test_transform) test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False, num_workers=2) model = build_resnet(num_classes=len(test_dataset.classes), pretrained=False) model.load_state_dict(torch.load("../checkpoints/resnet18_eurosat_best.pth", map_location=device)) 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_matrix=cm, display_labels=test_dataset.classes) disp.plot(xticks_rotation=45) plt.tight_layout() plt.savefig("../checkpoints/confusion_matrix.png", dpi=150) # 分类报告 print(classification_report(all_labels, all_preds, target_names=test_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(f"True: {true_label}\nPred: {pred_label}", color=color, fontsize=10) ax.axis("off") plt.tight_layout() plt.savefig("../checkpoints/sample_results.png", dpi=150) 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(weight=class_weights);对少数类多做数据增强;使用 Focal Loss 让模型更关注难分类样本。

5.5 多光谱数据通道不匹配

如果你的影像不是 RGB 三通道,而是多光谱数据,直接使用 ResNet 预训练模型会报通道数不匹配的错。一种做法是修改第一个卷积层的输入通道数,比如把in_channels=3改为in_channels=13,但这样就无法直接加载 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 在遥感分类上的应用。遥感图像分类本身是一个工程性和业务性都很强的领域,真正重要的往往不是把模型精度提升一个百分点,而是理解数据从哪里来、要解决什么业务问题、模型结果如何被业务验证和使用。带着这些问题去读论文、复现实验,比单纯堆模型更能推动进步。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询