news 2026/9/30 3:06:09

基于PyTorch的高光谱图像分类实战:2D CNN从入门到跑通

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于PyTorch的高光谱图像分类实战:2D CNN从入门到跑通

1. 项目概述与核心思路拆解

1.1 高光谱图像分类到底在做什么

高光谱图像分类,简单说就是给遥感影像里的每一个像元打标签。普通照片只有R、G、B三个通道,而高光谱图像往往有几十上百个波段,覆盖从可见光到近红外的连续光谱范围。每个像元拿到的不是三种颜色,而是一条完整的光谱曲线,这条曲线就像物质的“指纹”,不同地物(植被、水体、建筑、土壤)的光谱响应完全不同。分类任务的核心,就是让模型学会根据这些光谱-空间特征判断像元属于哪个地物类别。

我拿到的这个项目标题叫“超简单!基于PyTorch的高光谱图像分类2D_CNN网络实战”,用户意图很明确:不搞复杂的3D卷积、不堆Transformer,就用最常见的2D卷积神经网络,把高光谱图像分类这件事跑通。这里有个很容易误解的点——高光谱图像本身是三维数据块(高度×宽度×波段数),怎么用2D CNN处理?答案就在数据预处理上:先做降维或波段选择,把高光谱数据压缩成几个关键通道,再按普通图像分类的思路处理。这个思路不是最先进的,但它是理解高光谱分类的“最短路径”,特别适合第一次接触这个方向的初学者。

1.2 为什么选择2D CNN而不是3D CNN

很多教程一上来就推荐3D CNN,理由是它能同时提取空间和光谱特征,听起来更“完整”。但3D CNN的问题很明显:参数量大、训练慢、对硬件要求高,而且高光谱数据本身波段数多、样本量少,3D卷积很容易过拟合。项目标题里既然强调“超简单”,我理解的核心诉求就是快速跑通、理解全流程,而不是刷SOTA精度。

2D CNN的做法是先把高光谱立方体通过PCA(主成分分析)降维,保留前3个或前若干个主成分,相当于把几十上百个波段压缩成3个信息量最大的“伪彩色通道”,之后整个任务就退化成标准的RGB图像分类问题。这样做的优势在于:可以完全复用PyTorch里成熟的2D卷积模块、预训练权重和大量图像分类的经验,调试门槛低,训练速度快,而且代码量比3D CNN少一个量级。当然代价也明显:PCA压缩会丢失一部分光谱细节,如果地物之间光谱差异特别细微,2D CNN的精度上限就不如3D方法。但对于教学、原型验证和大部分中等难度数据集,这个方案完全够用。

1.3 项目整体流程速览

整个项目可以拆成四个环节:数据准备、预处理、模型搭建、训练评估。数据准备阶段最容易被忽视,高光谱数据集的格式五花八门,有的是.mat文件,有的是.tif,有的还带单独的标签图,第一步就得统一读取逻辑。预处理阶段的核心是PCA降维和样本划分,这里要特别小心训练集和测试集不能混在一起做标准化,否则会有信息泄露。模型搭建阶段我建议用最简单的卷积堆叠结构,先跑通再谈优化。训练评估阶段除了看准确率,还要看每类地物的分类效果,因为高光谱分类最常见的坑是整体准确率好看、小类别却完全没学会。

我在这篇文章里会把每个环节的代码拆开讲,参数选择给出理由,还会把我实际踩过的坑拿出来说。适合人群主要是刚入门深度学习、想拿高光谱数据练手的学生,以及做遥感应用但不太熟悉PyTorch的工程师。你不需要有很强的数学背景,但最好已经会基础的Python和简单的PyTorch张量操作。

2. 环境准备与工具选型

2.1 PyTorch环境搭建的几个关键决定

PyTorch是必须的,但怎么装其实有讲究。我建议用Anaconda创建独立环境,不要直接装在base环境里,否则以后项目多了依赖会打架。创建环境的命令很简单:

conda create -n hs python=3.10 -y conda activate hs

Python版本方面,3.9到3.11都可以,PyTorch官方对3.10的适配最好。接下来装PyTorch,这里有两个方向:CPU版和GPU版。如果机器上没有NVIDIA显卡,或者只有A卡,那就老老实实装CPU版,训练慢但能跑通流程;如果有NVIDIA显卡,装CUDA版。激活环境的命令:

pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118

或者用conda安装:

conda install pytorch torchvision pytorch-cuda=11.8 -c pytorch -c nvidia

我实际测试下来,pip方式更省事,conda经常因为依赖解析卡很久。装完之后务必验证一下CUDA是否可用:

import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else "CPU only")

如果打印出True,说明GPU版本正常工作。有个容易被忽略的细节:PyTorch版本和CUDA驱动版本是两回事。哪怕你系统里装了CUDA 12.x,只要PyTorch是cu118版本,它也能跑,因为PyTorch自带CUDA运行时,系统驱动够新就行。所以别在“到底装哪个CUDA版本”上纠结太久,用nvidia-smi看一眼驱动支持的CUDA版本,选一个比它低的PyTorch CUDA版本就对了。

2.2 硬件配置与WSL方案

高光谱图像分类的数据量其实不大,主流数据集比如Indian Pines、Pavia University,整个文件也就几MB到几十MB,所以对内存和显存的要求都不高。我实测在8GB显存的GTX 1070上,批大小32、图像块尺寸11×11,训练50轮毫无压力。但如果你用的是AMD显卡,比如最近讨论度很高的RX 7900 XTX,情况就特殊了——PyTorch官方对ROCm的支持在Linux下才比较完整,Windows下基本没法直接用。这时候有两个选择:一是装WSL(Windows Subsystem for Linux),在WSL里安装ROCm版本的PyTorch;二是干脆用CPU跑这个小项目。

WSL的坑不少,我简单说下要点:先在Windows里启用WSL2,然后装Ubuntu 22.04发行版,接着在Ubuntu里安装AMD的ROCm驱动,最后再用pip安装PyTorch的ROCm版本。整个过程对新手不太友好,光是驱动版本匹配就能折腾半天。所以我给个务实的建议:如果只是跑这个项目,CPU版完全够用;如果你以后要认真做深度学习,要么换NVIDIA卡,要么老老实实折腾WSL+ROCm。在WSL里创建环境和Windows里没有本质区别,仍然是conda+env那一套,只是显卡调用走的是ROCm而不是CUDA。

另外提一句,无论什么环境,只要代码里用了.cuda()或者to(device),PyTorch都会自动尝试调用GPU。如果GPU不可用,它会报错或者静默回到CPU,所以代码里最好加一句设备判断的逻辑,免得在别人机器上跑的时候出问题。

2.3 数据读取的库选择

处理高光谱数据,除了PyTorch还需要几个辅助库:scipy(读.mat文件)、numpy(矩阵运算)、scikit-learn(PCA降维和评价指标)、matplotlib(可视化)。这些都用pip一条命令装完:

pip install numpy scipy scikit-learn matplotlib

这里我想强调scikit-learn的价值,很多人只拿它当传统机器学习的工具包,但它的decomposition.PCA和metrics.classification_report在深度学习项目里同样非常好用。PCA直接调包比自己写快得多,而且它支持先fit再transform,可以方便地把训练集学到的降维参数应用到测试集上,这个特性我们后面会用到。

3. 数据准备与预处理实操

3.1 以Indian Pines数据集为例

Indian Pines是高光谱分类最经典的benchmark,由AVIRIS传感器在美国印第安纳州拍摄,空间尺寸145×145,包含200个有效波段(原始224个波段去掉吸水带后剩200个),地物类别共16类,包括玉米、大豆、小麦、草地、树林等。这个数据集常用于论文对比,网上很容易找到.mat格式的版本,两个文件:一个存图像数据,一个存标签。

读取方式很简单:

import scipy.io as sio import numpy as np data = sio.loadmat('Indian_pines_corrected.mat')['indian_pines_corrected'] labels = sio.loadmat('Indian_pines_gt.mat')['indian_pines_gt'] print(data.shape) # (145, 145, 200) print(labels.shape) # (145, 145)

data的形状是(高度, 宽度, 波段数),labels是(高度, 宽度)。注意labels里0代表背景(无标签),1到16是16个地物类别。数据准备的第一步就是把背景像素剔除,只保留有标签的样本,这步做不好,后面样本统计全乱。

3.2 PCA降维与图像块采样

如前所述,200个波段不能直接塞进2D CNN,先做PCA降维。这里有个细节:PCA要把三维数据reshape成二维矩阵再计算。具体做法:

from sklearn.decomposition import PCA # 把三维展开成二维:像素数×波段数 h, w, bands = data.shape data_2d = data.reshape(h * w, bands) # 只对有标签的像素做PCA拟合 mask = labels.reshape(-1) > 0 pca = PCA(n_components=3) pca.fit(data_2d[mask]) data_pca = pca.transform(data_2d) # 恢复成三维并归一化 data_pca_3d = data_pca.reshape(h, w, -1)

为什么只用有标签的像素来拟合PCA?因为PCA是无监督方法,如果用所有像素拟合,背景像素(通常是零值或噪声)会主导主成分方向,干扰降维结果。n_components选3,一方面是为了让可视化时能对应RGB三个通道,另一方面前3个主成分通常能解释超过90%的方差。如果你发现前3个主成分解释的方差不够,也可以适当增加到5-10个,但那就需要调整模型第一层的输入通道数。

下一步是构建图像块。2D CNN不能只看单个像素,还要看它周围的邻域,这样才能利用空间上下文信息。常见的做法是取每个像素周围p×p的小块,比如11×11,这样一个样本的形状就是(11, 11, n_components)。边界像素的邻域超出图像范围,需要用padding处理,我习惯用镜像填充(reflect模式),因为零填充会让边界样本带上明显的人工痕迹。

3.3 训练集测试集划分与信息泄露陷阱

高光谱分类有两种划分方式:随机划分和按区域划分。随机划分是把所有有标签的像素随机分成训练集和测试集,这样训练测试可能来自同一块地,精度会偏高;按区域划分是把图像分成几块,某些块做训练、某些块做测试,更接近真实遥感应用场景,难度更大。初学阶段建议先用随机划分,代码简单、指标好看,等你理解整个流程后再挑战按区域划分。

随机划分代码如下:

from sklearn.model_selection import train_test_split # X_all是图像块数组,形状为(N, 11, 11, 3),y_all是对应标签 X_train, X_test, y_train, y_test = train_test_split( X_all, y_all, test_size=0.7, stratify=y_all, random_state=42 )

这里我设置了test_size=0.7,意思是70%的样本做测试,30%训练。高光谱分类通常不需要太多训练样本,因为每个像素都是一条光谱曲线,信息密度高,30%已经足够。stratify=y_all保证每个类别在训练集和测试集中的比例和总体一致,防止某个小类别在训练集中恰好没被抽到。

信息泄露陷阱要特别留意:如果先对整个数据集做标准化(减去全局均值、除以全局标准差),再划分训练测试,那么测试集的信息已经通过均值、方差混进了训练过程,评估结果会虚高。正确做法是先划分,再在训练集上计算均值方差,用同一组的参数去归一化测试集。PCA也是同理,上一节特意强调只用训练集有标签的像素拟合PCA,就是为了避免信息泄露。

4. 2D CNN模型搭建与训练

4.1 模型结构设计与参数量分析

2D CNN的结构不需要太复杂,我用的是一个三层卷积加全连接的小网络,参考了许多高光谱分类论文里的基础结构,但把参数做了精简,保证新手也能在普通显卡上快速训练。核心代码如下:

import torch import torch.nn as nn class HyperspectralCNN(nn.Module): def __init__(self, input_channels=3, num_classes=16, patch_size=11): super(HyperspectralCNN, self).__init__() self.features = nn.Sequential( nn.Conv2d(input_channels, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), ) self.classifier = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(128, 256), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(256, num_classes) ) def forward(self, x): x = self.features(x) x = self.classifier(x) return x

这个结构有三个设计要点值得解释。第一,三个卷积层的通道数从32到64到128逐层翻倍,这是图像分类网络的经典做法——浅层提取边缘、纹理等低级特征,通道数少一点没关系;深层提取语义特征,需要更多通道来承载信息。第二,每层卷积后面都跟了BatchNorm和ReLU,BatchNorm能加速收敛、缓解梯度消失,在高光谱这种数据分布差异大的任务上作用尤其明显。第三,分类器用AdaptiveAvgPool2d(1)把特征图压缩成1×1,这样不管输入图像块尺寸怎么变,全连接层的输入维度都是128,代码更灵活。

我算一下这个模型的参数量。卷积层参数主要由权重和偏置组成,第一层3×32×3×3=864个参数,第二层32×64×3×3=18432,第三层64×128×3×3=73728。全连接层128×256=32768,256×num_classes=4096(以16类计)。总计约13万个参数,不到0.2MB,相比动辄上千万参数的ResNet轻量太多。这个体量意味着单CPU也能较快训练,GPU更是秒级迭代。

4.2 训练流程与超参数选择

训练流程里最核心的超参数是学习率、批大小、损失函数和优化器。我给出的配置是一套经过验证、适合小数据集的基础组合:

import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset # 转换为PyTorch张量并调整通道顺序为 (N, C, H, W) X_train_tensor = torch.FloatTensor(X_train).permute(0, 3, 1, 2) y_train_tensor = torch.LongTensor(y_train) - 1 # 标签从1开始,减去1变成0-15 X_test_tensor = torch.FloatTensor(X_test).permute(0, 3, 1, 2) y_test_tensor = torch.LongTensor(y_test) - 1 train_dataset = TensorDataset(X_train_tensor, y_train_tensor) test_dataset = TensorDataset(X_test_tensor, y_test_tensor) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=128, shuffle=False) model = HyperspectralCNN(input_channels=3, num_classes=16) device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model.to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-3) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=20, gamma=0.5)

这里有几处新手容易忽略的细节。标签减一是因为原始标签从1到16,而CrossEntropyLoss要求的类别索引必须从0开始,不减的话模型会认为有17类,最后一类永远没有梯度。通道顺序调整为(N, C, H, W)是PyTorch卷积层的硬性要求,如果你之前的数组形状是(N, H, W, C),不转置会直接报错或者得到莫名其妙的结果。Adam优化器对学习率不那么敏感,初始1e-3基本是万金油;StepLR每20轮把学习率减半,能让训练后期收敛更稳定。

训练循环的代码是标准写法,但有几个技巧可以让过程更可控:

epochs = 50 for epoch in range(epochs): model.train() running_loss = 0.0 for inputs, targets in train_loader: inputs, targets = inputs.to(device), targets.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, targets) loss.backward() optimizer.step() running_loss += loss.item() * inputs.size(0) scheduler.step() model.eval() correct = 0 total = 0 with torch.no_grad(): for inputs, targets in test_loader: inputs, targets = inputs.to(device), targets.to(device) outputs = model(inputs) _, predicted = torch.max(outputs, 1) total += targets.size(0) correct += (predicted == targets).sum().item() train_loss = running_loss / len(train_dataset) test_acc = 100.0 * correct / total print(f'Epoch {epoch+1:3d} | Loss: {train_loss:.4f} | Test Acc: {test_acc:.2f}%')

训练循环里我习惯把model.train()和model.eval()分开写,这是必须的,因为BatchNorm和Dropout在训练和推理时行为不同。如果不切模式,训练完直接做测试,BatchNorm会用当前的batch统计量而不是训练时的滚动统计量,结果会有偏差。torch.max(outputs, 1)返回每一行的最大值和对应索引,预测类别就是索引位置。

我用Indian Pines数据实际跑过这个配置,50轮之后测试准确率一般能达到94%-97%,整个训练过程在GTX 1070上大约需要3-5分钟,CPU上大约15分钟左右。如果准确率明显低于这个区间,多半是数据预处理或者标签对齐出了问题,后面会专门说排查方法。

4.3 模型评估与结果可视化

高光谱分类不能只看总体准确率,还要看每个类别的准确率和召回率,否则小类别被牺牲了你都不知道。我推荐用scikit-learn生成分类报告:

from sklearn.metrics import classification_report, confusion_matrix # 收集所有预测结果 y_true_all = [] y_pred_all = [] model.eval() with torch.no_grad(): for inputs, targets in test_loader: inputs = inputs.to(device) outputs = model(inputs) _, predicted = torch.max(outputs, 1) y_true_all.extend(targets.cpu().numpy()) y_pred_all.extend(predicted.cpu().numpy()) print(classification_report(y_true_all, y_pred_all, digits=4))

这份报告会列出每个类别的精确率、召回率、F1分数和样本数。我在实际项目中看到的典型情况是:样本量多的大类(如大豆、玉米)精度很高,样本量少的小类(如石墙、苜蓿)精度可能只有50%-60%,这就是类别不平衡问题。对于高光谱分类,最简单的缓解方案是在训练时给样本量少的类别加权,CrossEntropyLoss支持传入权重向量:

import torch.nn.functional as F # 统计每个类别的样本数 counts = np.bincount(y_train) weights = 1.0 / (counts + 1e-6) weights = torch.FloatTensor(weights).to(device) criterion = nn.CrossEntropyLoss(weight=weights)

加权之后小类别的损失权重变大,模型会稍微更重视它们,整体准确率可能略降,但每类平均精度会提升。对于侧重分类均衡性的应用场景,这个操作很有必要。

分类结果的图像可视化同样重要,可以把模型在整张图上的预测结果做成伪彩色图,和真实标签图对比,一眼就能看出模型在哪些区域出错。做法是先把整个图像的所有像素(包括背景)都提取图像块并预测,然后把预测标签填回二维矩阵:

# 假设整个图像逐像素预测后得到pred_map,形状为(145, 145) # 0表示背景,1-16表示类别 import matplotlib.pyplot as plt fig, axes = plt.subplots(1, 3, figsize=(15, 5)) axes[0].imshow(data_pca_3d / data_pca_3d.max()) axes[0].set_title('PCA RGB') axes[1].imshow(labels, cmap='tab20') axes[1].set_title('Ground Truth') axes[2].imshow(pred_map, cmap='tab20') axes[2].set_title('Prediction') plt.show()

这里有个细节:PCA压缩后的三个通道最大值可能差异很大,直接imshow会偏色,所以先除以各自的最大值归一化。标签图用tab20色图,因为类别数最多16,tab20刚好有足够的颜色区分度。

5. 典型问题排查与性能优化

5.1 维度错误与数据类型问题

2D CNN最常见的报错就是维度不匹配。PyTorth的Conv2d要求输入形状为(N, C, H, W),如果你传入的是(N, H, W, C),会提示“Expected 4D input”或者“got 3D input”。解决方法是记住一个口诀:通道维永远在第二位(batch之后)。而PCA降维后的三维数据是(H, W, C),提取像素块时是(N, H, W, C),所以在送入模型前必须permute(0, 3, 1, 2)。

另一个高频坑是数据类型。numpy数组默认是float64,但PyTorch的Conv2d要求float32或float16,如果不转换直接包裹成Tensor会报类型错误。用torch.FloatTensor而不是torch.Tensor就能避免这个问题,前者强制float32。标签数据则用torch.LongTensor,因为CrossEntropyLoss要求target为整数类型。

还有一个小众但很隐蔽的问题:如果训练集某个类别只有一两个样本,DataLoader在按batch随机采样时可能恰好没抽到它,导致这个类别的模型weights完全没学到东西。解决方法有几个:一是用stratify划分保证每个类别都出现在训练集里;二是加大batch size让采样更均匀;三是干脆用过采样,把稀有类别的样本重复复制几份。对于Indian Pines这类标准数据集,前两种方法通常就足够了。

5.2 显存不足与训练速度慢

显存不足(OOM)在这个项目里不太容易出现,因为图像块尺寸很小,但如果你的patch_size设到了25×25以上,batch_size又设到128,小显存显卡可能吃不消。解决方法按优先级排列:先减小batch_size,比如从64降到32;再减小patch_size;最后才考虑减小模型通道数。实际项目中我一般不会为了省显存而牺牲模型容量,因为Indian Pines的样本量摆在那里,模型再大也大不到哪去。

训练速度慢则要分清瓶颈在数据还是模型。如果是cpu训练,每次迭代都要做numpy和tensor的转换,瓶颈常在DataLoader的数据加载上。可以尝试设置num_workers=2或4,让数据加载并行化:

train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=2)

如果是在Windows上,num_workers>0可能会报错或者导致程序卡死,这是Windows多进程的固有问题。我的建议是Windows下就用num_workers=0,Linux下再考虑并行。数据量小的情况下,worker带来的加速很有限,不值得为了这点速度而增加调试成本。

模型训练慢还有一个隐性原因:没把模型和数据放到GPU上。检查代码里是否有model.to(device)和inputs, targets = inputs.to(device), targets.to(device)。漏掉前者模型在CPU上算forward,漏掉后者数据在CPU上,而模型在GPU上,会反复做设备和内存之间的拷贝,速度慢到怀疑人生。

5.3 精度上不去的排查思路

当模型的准确率始终在70%以下徘徊时,问题通常不是模型结构,而是前面的数据处理。我总结了一个排查顺序:先看训练loss有没有下降,如果loss一直不降,说明模型没有学到东西,可能是学习率太大或太小,也可能是标签错误;如果loss下降了但测试准确率低,说明过拟合了,加Dropout或数据增强;如果训练和测试准确率都低,但loss正常,那大概率是数据问题。

数据问题里又分两类。一类是数据预处理出了bug,比如PCA只针对部分像素做了归一化,导致输入分布不一致;另一类是标签错位,比如取图像块时用的坐标和标签坐标对不上。高光谱数据集的坐标系是(height, width),如果你在提取特征时用了(width, height)的顺序,就会出现“图像块内容是一个位置,标签却是另一个位置”的错位,而这种错位从loss曲线上很难看出来。我自己调试时就踩过这个坑,最后是通过可视化预测图和标签图的差异才定位到坐标顺序问题。

还有一个精度陷阱是类别不均衡。Indian Pines里大豆类的样本数可能比石墙类多几十倍,如果模型把所有像素都预测成大豆,总体准确率也能达到70%以上,但这种模型没有实用价值。所以一定要看classification_report,重点关注小类别的F1分数。如果小类别确实学不动,先用类别权重加权,再不行就考虑换成3D CNN或引入更多光谱信息。

5.4 数据增强在2D CNN里的作用边界

很多做自然图像分类的人习惯性地加随机翻转、随机裁剪,但在高光谱分类中,数据增强要非常谨慎。高光谱图像是遥感场景,地物有明确的方向性(建筑、道路、农田的方向都包含语义信息),随机旋转90度或180度在某些数据集上反而会损害精度。我建议只加随机水平翻转和随机垂直翻转,并且要通过实验对比,如果加了增强后精度明显下降,果断去掉。

更有效的一种增强方式是我在项目里常用的“光谱扰动”:对图像块的每个通道加少量高斯噪声,等价于对光谱曲线做轻微偏移,能增强模型对光谱波动的鲁棒性。实现很简单:

if random.random() < 0.5: noise = torch.randn_like(inputs) * 0.01 inputs = inputs + noise

0.01的噪声方差是我试出来的经验值,太小没效果,太大会把真实光谱特征淹没。对于Indian Pines这类干净的标准数据集,不加噪声的baseline已经很高,加噪反而可能干扰训练;但如果你自己采集的数据带有噪声,这个操作能带来明显提升。

6. 项目扩展与进阶方向

6.1 从2D CNN升级到混合CNN

如果把2D CNN跑通了,下一步自然是想办法进一步提高精度。我建议不要直接跳到3D CNN,而是先试“空间-光谱双分支”结构:一路分支用2D CNN提取空间特征,另一路分支把每个像素的光谱曲线(一维向量)单独送入1D CNN提取光谱特征,最后把两个分支的特征拼接在一起分类。这种结构代码量只多了几十行,但精度通常比纯净的2D CNN高一两个百分点,而且能直观展示空间和光谱信息各自的作用。

实现思路:空间分支输入的是PCA降维后的三通道图像块;光谱分支输入的是原始200个波段在该位置的像素向量,形状为(1, 200)。两个分支的输出都经过全局池化变成一维向量,然后cat起来,送入全连接分类器。训练时注意两个分支的学习率可以不同,光谱分支收敛慢,可以设大一点学习率,空间分支收敛快,设小一点,但这种精细调节对初学者来说不是必须的,先用同一套学习率也能跑出合理结果。

6.2 注意力机制与Transformer的简单接法

注意力机制近年来也杀进了高光谱分类领域。最朴素的接法是在2D CNN的深层特征图后面加一个通道注意力模块(类似SENet的squeeze-and-excitation)。通道注意力的思想是让网络自动学习哪些特征通道更重要,从而放大有用特征、抑制无用特征。对于高光谱图像,PCA降维已经做了一次通道筛选,再叠加通道注意力相当于第二次筛选,理论上能让分类器更聚焦于判别性特征。

代码实现如下:

class ChannelAttention(nn.Module): def __init__(self, in_channels, reduction=16): super(ChannelAttention, self).__init__() self.avg_pool = nn.AdaptiveAvgPool2d(1) self.fc = nn.Sequential( nn.Linear(in_channels, in_channels // reduction), nn.ReLU(inplace=True), nn.Linear(in_channels // reduction, in_channels) ) self.sigmoid = nn.Sigmoid() def forward(self, x): b, c, _, _ = x.size() y = self.avg_pool(x).view(b, c) y = self.fc(y).view(b, c, 1, 1) return x * self.sigmoid(y)

把ChannelAttention插在每层卷积之后,就能以很小的代价换取几个点的精度提升。至于Transformer,我的建议是初学者先不要碰。ViT类模型需要大量数据才能训练好,高光谱数据集的样本量通常只有几千到几万,在标准数据集上很容易大幅过拟合。如果你想全用Transformer,那必须扩充训练样本(比如用生成数据或跨数据集预训练),这对初学者来说是一条更长的路。

6.3 把单模型做成分类器集成

另一个简单有效的进阶方向是模型集成。训练3个不同的模型(比如不同随机种子、不同patch_size、不同卷积核大小),推理时对3个模型的输出概率取平均,再判断类别。集成能平滑单模型的随机误差,通常能把精度提升1-2个百分点。代价是训练时间变为原来的3倍,但代码改动很小:

probs_list = [] for model in models: model.eval() with torch.no_grad(): probs = F.softmax(model(inputs), dim=1) probs_list.append(probs) avg_probs = torch.stack(probs_list).mean(dim=0) _, predicted = torch.max(avg_probs, 1)

集成对于类别不平衡问题也有一定帮助,因为不同模型在个别类别上的偏好可能不一样,平均之后偏见会被削弱。缺点是需要保存多个模型文件,推理速度变慢。对于小数据集来说,集成的收益大于代价,我比较推荐。

6.4 部署到实际场景的注意事项

如果这个项目要真正部署到遥感业务系统中,有两个问题必须在训练阶段就考虑。第一个是推理效率:实际遥感图像可能非常大,比如10000×10000像素,逐像素提取图像块并预测会非常慢。解决方案是使用滑窗批量推理,或者直接用全卷积网络,让模型一次处理一大块图像,大幅减少重复计算。全卷积网络本质上是把最后的全连接层换成1×1卷积,这样网络可以接受任意尺寸的输入,输出对应的分割图。

第二个是类别定义:标准数据集(Indian Pines)的类别是固定的,但实际业务中地物类别经常变化,今天分的可能是农作物种类,明天分的可能是建筑物损坏程度。设计模型时最好把分类头做成可配置的,num_classes作为参数传入。另一个实用技巧是在训练阶段就把背景类纳入模型,让模型学会区分“已知地物”和“未知区域”,否则推理时模型会把每个像素都强行归到某个已知类别里。

我在实际项目中还发现,高光谱分类模型在实验室数据和野外真实场景数据之间的精度落差很大。原因很直接:实验室数据经过严格的辐射定标和大气校正,光谱干净;野外数据受光照、云影、传感器噪声影响,光谱曲线多了很多随机波动。如果需要部署,建议在训练数据里混入一些真实场景的样本,或者对实验室数据做更激进的光谱增强,提高模型的泛化能力。

7. 从实战中沉淀的几点体会

这个项目做下来,最大的感触是“高光谱图像分类”名字听着高大上,拆开看每一步都是通用深度学习的组合应用。PCA降维是sklearn的标准操作,2D CNN是PyTorch的入门组件,训练流程和任何图像分类项目没有本质区别。真正的难点从来不在模型,而在数据理解:你得知道波段是什么意思、标签分布什么样、空间上下文有多重要,这些领域知识比调参更能决定最终效果。

最后分享一个小技巧:训练过程中把每个epoch的测试准确率和loss记录下来,画成曲线图,观察变化趋势能帮你做出很多判断。如果训练集准确率稳定上升、测试集准确率开始下降,说明开始过拟合了,应该提前停止或者在更早的epoch取checkpoint。如果准确率曲线震荡很大,可能是学习率太高,降一个量级试试。这些曲线比任何复杂的自动化调参工具都直观,也最适合新手建立对模型训练过程的“手感”。

对刚接触这个方向的朋友,我建议不要一开始就追新模型新技术,先把这个2D CNN流程从头到尾跑明白,改一改patch_size看效果怎么变,加一加噪声看鲁棒性怎么变。等你亲手感觉到每一个操作带来的变化,高光谱图像分类的大门就算真正迈进去了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 3:06:09

Mamba实战指南:从线性序列建模到视觉与点云落地

简介&#xff1a;本资源是一份面向人工智能方向研究生、算法工程师及NLP/序列建模学习者的论文汇报PPT&#xff0c;系统解读Mamba模型的核心思想与技术突破。PPT完整覆盖研究背景&#xff08;传统Transformer注意力效率瓶颈与SSM建模局限&#xff09;、解决方案&#xff08;选择…

作者头像 李华
网站建设 2026/9/30 3:05:50

计算机网络排错实战PPT:从OSI模型到Wireshark抓包

简介&#xff1a;本资源是一份系统讲解计算机网络基础知识的PPT课件&#xff0c;面向高校计算机类专业学生、IT初学者及网络入门从业者&#xff0c;旨在帮助读者建立清晰的网络知识框架&#xff0c;掌握核心概念与典型设备原理。课件内容覆盖网络概述、OSI与TCP/IP体系结构、IP…

作者头像 李华
网站建设 2026/9/30 3:05:48

Windows上跑Linux:VirtualBox安装Ubuntu虚拟机完整避坑指南

最近身边好几个朋友都来问我同一个事&#xff1a;想在 Windows 上体验一把 Linux&#xff0c;装 VirtualBox 虚拟机跑个 Ubuntu 到底行不行、麻不麻烦。答案当然是行&#xff0c;而且这是目前门槛最低、成本为零的一套组合——VirtualBox 免费开源&#xff0c;Ubuntu 社区版镜像…

作者头像 李华
网站建设 2026/9/30 3:05:26

轻量论坛搭建全攻略:从选型到上线的Flarum实战复盘

最近趁工作间隙&#xff0c;我终于把打磨了一个多月的论坛给搭上线了。起因其实特别朴素——团队里的讨论散落在好几条即时通讯群里&#xff0c;有价值的内容隔几天就被刷得无影无踪&#xff0c;搜索引擎也捞不回来。我翻来覆去想了很久&#xff0c;与其继续在别人的平台上挤位…

作者头像 李华
网站建设 2026/9/30 3:03:29

DeepSeek V3 + AnythingLLM:本地知识库搭建与避坑指南

简介&#xff1a;这份PDF教程面向希望借助DeepSeek V3与AnythingLLM搭建个人知识库的开发者与效率工具爱好者&#xff0c;解决从零接入大模型、本地化组织碎片知识并实现智能问答的完整流程问题。资源包共1个PDF文件&#xff0c;约590KB&#xff0c;内容以图文步骤形式呈现&…

作者头像 李华
网站建设 2026/9/30 3:03:13

SAP后台作业实战:SM36配置、BAPI批处理与监控排查

1. 后台作业的本质与适用场景1.1 什么是后台作业&#xff0c;为什么要用它在SAP系统里&#xff0c;“启用后台作业”绝对是日常开发和运维逃不开的话题。说白了&#xff0c;后台作业就是让ABAP程序在系统后台自动执行&#xff0c;不需要用户一直守着屏幕、点着按钮等结果。你提…

作者头像 李华