
简介本资源面向遥感图像处理与深度学习方向的科研人员、工程师及学生提供基于卷积神经网络的高光谱图像分类完整实现方案涵盖1D-CNN、2D-CNN与3D-CNN三种网络结构帮助读者对比不同维度卷积在光谱与空间特征提取上的表现差异。压缩包共11个文件约229.71MB包含4个Python脚本分别实现三种CNN模型及数据预处理与训练评估函数5个mat格式高光谱数据集与地面真实标签以及1个h5权重文件和1个rar辅助压缩包便于直接复现实验。已有244人学习下载。通过运行这些代码与数据读者可快速搭建分类实验流程理解一维光谱特征、二维空间光谱融合与三维空谱联合卷积的适用场景并依据分类精度与计算开销选择合适网络结构为高光谱图像分析项目提供可复用的代码基础与实验参考。1. 高光谱分类选 1D、2D 还是 3D-CNN先搞清楚你的数据长什么样高光谱图像分类这个方向我见过太多人一上来就纠结网络结构结果数据格式都没搞明白。高光谱数据本质是一个三维立方体空间两个维度加上光谱一个维度比如 Indian Pines 是 145×145 像素、200 个波段Pavia University 是 610×610 像素、103 个波段。卷积神经网络在这里能做什么核心就是利用卷积核在光谱维、空间维或两者同时滑动提取出「哪些波段组合能区分地物」这件事。1D-CNN 只吃光谱向量把每个像素当成一条曲线来分类2D-CNN 把某个波段或几个波段拼成图像做空间纹理提取3D-CNN 直接在立方体上卷同时抓光谱和空间特征。选哪个不是拍脑袋取决于你的标注样本量、计算资源和精度要求。这篇把我自己跑过的高光谱分类方案拆开讲从数据准备到三种 CNN 的实现和调参新手能照着复现熟手能看到边界在哪。2. 数据准备与样本划分高光谱分类绕不开的第一道坎2.1 高光谱数据格式与加载方式高光谱数据常见的存储格式有.matMATLAB 格式、.tifGeoTIFF、.hdr.imgENVI 格式。做分类实验最常用的是.mat因为 Indian Pines、Pavia University、Salinas 这些公开数据集都是这个格式里面通常包含两个关键变量input三维数据立方体和gt二维标签图。用 Python 加载的标准做法import scipy.io as sio import numpy as np # 加载 Indian Pines 数据集 data sio.loadmat(Indian_pines_corrected.mat) gt sio.loadmat(Indian_pines_gt.mat) # 提取数据立方体和标签图 X data[indian_pines_corrected] # shape: (145, 145, 200) y gt[indian_pines_gt] # shape: (145, 145) print(f数据维度: {X.shape}, 标签维度: {y.shape}) print(f类别数: {len(np.unique(y)) - 1}) # 减去背景类 0 print(f数据类型: {X.dtype}, 最大值: {X.max()}, 最小值: {X.min()})这段代码做了三件事加载.mat文件、提取数据和标签、打印基本信息。注意np.unique(y) - 1是因为标签 0 通常代表背景或未标注区域不算入分类类别。打印最大最小值是为了判断是否做了归一化——如果最大值是 65535 这种说明是原始 DN 值后面必须做归一化。2.2 样本划分训练集比例怎么定高光谱分类最大的痛点是小样本。Indian Pines 总共 21025 个标注像素分 16 类有些类只有几十个样本。常见的划分比例是每类取 5%、10%、15% 做训练其余做测试。我一般用每类固定数量而不是固定比例因为比例划分会让小类只剩个位数样本训练根本不稳定。from sklearn.model_selection import train_test_split import numpy as np def split_data(X, y, samples_per_class10): 按每类固定数量划分训练集和测试集 X_flat X.reshape(-1, X.shape[-1]) # (145*145, 200) y_flat y.reshape(-1) # (145*145,) # 去掉背景像素 mask y_flat 0 X_labeled X_flat[mask] y_labeled y_flat[mask] train_idx [] test_idx [] for cls in np.unique(y_labeled): cls_idx np.where(y_labeled cls)[0] np.random.seed(42) np.random.shuffle(cls_idx) train_idx.extend(cls_idx[:samples_per_class]) test_idx.extend(cls_idx[samples_per_class:]) return (X_labeled[train_idx], y_labeled[train_idx], X_labeled[test_idx], y_labeled[test_idx]) X_train, y_train, X_test, y_test split_data(X, y, samples_per_class10) print(f训练集: {X_train.shape}, 测试集: {X_test.shape})samples_per_class这个参数是核心。我一般从 5 开始试5 能跑通再往上加。如果 5 个样本就崩了说明网络结构太复杂或者归一化没做好。np.random.seed(42)是为了结果可复现高光谱分类论文里不固定随机种子基本没法对比。2.3 归一化与 PCA 降维的取舍归一化是必须的常见两种方式Min-Max 归一化到 [0,1]或者按波段做 Z-Score 标准化。我倾向 Min-Max因为高光谱不同波段量纲差异大Z-Score 在某些低方差波段上会放大噪声。PCA 降维在高光谱里很常见把 200 个波段压到 30 左右再送入网络。但注意如果你用 1D-CNN 且样本量够不降维也行如果用 3D-CNN降维几乎是必须的否则参数量爆炸。我一般保留 30 个主成分累计贡献率能到 99% 以上。from sklearn.decomposition import PCA from sklearn.preprocessing import MinMaxScaler # Min-Max 归一化 scaler MinMaxScaler() X_train_norm scaler.fit_transform(X_train) X_test_norm scaler.transform(X_test) # PCA 降维只在训练集上 fit pca PCA(n_components30, whitenTrue) X_train_pca pca.fit_transform(X_train_norm) X_test_pca pca.transform(X_test_norm) print(fPCA 后维度: {X_train_pca.shape}) print(f累计贡献率: {pca.explained_variance_ratio_.sum():.4f})whitenTrue让各主成分方差一致对后续 CNN 训练有帮助。注意 PCA 只能在训练集上 fit测试集用 transform否则数据泄露精度虚高。3. 1D-CNN 实现把每个像素当光谱曲线来分类3.1 1D-CNN 的网络结构设计1D-CNN 的输入是一个光谱向量比如 200 维或 PCA 后的 30 维。卷积核只沿光谱维滑动提取局部波段组合特征。结构一般是Conv1D → BN → ReLU → Conv1D → BN → ReLU → GlobalAvgPool → FC → Softmax。import torch import torch.nn as nn class CNN1D(nn.Module): def __init__(self, in_channels, num_classes): super(CNN1D, self).__init__() self.conv1 nn.Sequential( nn.Conv1d(1, 32, kernel_size7, padding3), nn.BatchNorm1d(32), nn.ReLU() ) self.conv2 nn.Sequential( nn.Conv1d(32, 64, kernel_size5, padding2), nn.BatchNorm1d(64), nn.ReLU() ) self.global_pool nn.AdaptiveAvgPool1d(1) self.fc nn.Linear(64, num_classes) def forward(self, x): # x: (batch, 1, bands) x self.conv1(x) x self.conv2(x) x self.global_pool(x) # (batch, 64, 1) x x.squeeze(-1) # (batch, 64) x self.fc(x) return xkernel_size7是第一层卷积核大小对应一次看 7 个相邻波段。高光谱相邻波段相关性高7 是个经验值太小感受野不够太大容易过平滑。padding3保证输出长度不变。AdaptiveAvgPool1d(1)把整个光谱维压成一个值这样输入波段数变化时网络不用改结构。3.2 训练循环与关键参数from torch.utils.data import DataLoader, TensorDataset # 准备数据 X_train_tensor torch.FloatTensor(X_train_pca).unsqueeze(1) # (N, 1, 30) y_train_tensor torch.LongTensor(y_train - 1) # 标签从 0 开始 train_dataset TensorDataset(X_train_tensor, y_train_tensor) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue) # 模型、损失、优化器 device torch.device(cuda if torch.cuda.is_available() else cpu) model CNN1D(in_channels1, num_classes16).to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) # 训练 for epoch in range(100): model.train() total_loss 0 for xb, yb in train_loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() out model(xb) loss criterion(out, yb) loss.backward() optimizer.step() total_loss loss.item() if (epoch 1) % 20 0: print(fEpoch {epoch1}, Loss: {total_loss/len(train_loader):.4f})lr1e-3是 Adam 的常用起点weight_decay1e-4做 L2 正则小样本场景很重要。batch_size32在几百个训练样本时比较稳太小梯度噪声大太大容易过拟合。标签减 1 是因为 PyTorch 的 CrossEntropyLoss 要求标签从 0 开始。3.3 1D-CNN 的适用边界1D-CNN 只用了光谱信息没用空间信息。在样本极少每类 5 个时它反而比 3D-CNN 稳因为参数少。但它的天花板也明显空间纹理完全没利用同物异谱的问题解决不了。如果你的数据空间分辨率高、地物边界清晰1D-CNN 精度通常比 2D/3D 低 3-8 个百分点。4. 2D-CNN 与 3D-CNN空间信息怎么卷进来4.1 2D-CNN 的输入构造PCA 后取前几个主成分2D-CNN 的思路是把高光谱降维后的前几个主成分当成多通道图像用类似 RGB 分类的方式做。输入是(batch, n_components, H, W)但高光谱分类通常是逐像素分类所以实际做法是取每个像素的邻域 patch比如 11×11 的空间窗口。def extract_patches(X_pca, y, patch_size11): 从 PCA 后的数据中提取空间 patch half patch_size // 2 H, W, C X_pca.shape # 边界填充 X_pad np.pad(X_pca, ((half, half), (half, half), (0, 0)), modereflect) patches [] labels [] for i in range(H): for j in range(W): if y[i, j] 0: patch X_pad[i:ipatch_size, j:jpatch_size, :] patches.append(patch) labels.append(y[i, j]) return np.array(patches), np.array(labels) # 假设 X_pca 已经 reshape 回 (145, 145, 30) X_pca_3d pca.transform(X.reshape(-1, X.shape[-1])).reshape(145, 145, 30) patches, labels extract_patches(X_pca_3d, y, patch_size11) print(fPatch 数据: {patches.shape}) # (N, 11, 11, 30)patch_size11是常见选择太小空间上下文不够太大计算量涨且边界像素 patch 质量差。modereflect做边界填充比补零更合理因为高光谱图像边界像素和内部像素统计特性接近。4.2 3D-CNN 的卷积核设计同时卷光谱和空间3D-CNN 的输入是(batch, 1, D, H, W)卷积核也是三维的比如(3, 3, 3)表示同时看 3 个波段、3 行、3 列。这样一层就能同时提取光谱和空间特征。class CNN3D(nn.Module): def __init__(self, in_bands, num_classes): super(CNN3D, self).__init__() self.conv1 nn.Sequential( nn.Conv3d(1, 8, kernel_size(3, 3, 3), padding(1, 1, 1)), nn.BatchNorm3d(8), nn.ReLU() ) self.conv2 nn.Sequential( nn.Conv3d(8, 16, kernel_size(3, 3, 3), padding(1, 1, 1)), nn.BatchNorm3d(16), nn.ReLU() ) self.global_pool nn.AdaptiveAvgPool3d(1) self.fc nn.Linear(16, num_classes) def forward(self, x): # x: (batch, 1, bands, H, W) x self.conv1(x) x self.conv2(x) x self.global_pool(x) x x.view(x.size(0), -1) x self.fc(x) return x第一层输出通道只给 8第二层 16这是故意的。3D 卷积参数量是 2D 的 kernel_size 倍通道数必须压住。padding(1,1,1)保持维度不变。AdaptiveAvgPool3d(1)把(D, H, W)全压成 1避免全连接层参数爆炸。4.3 三种结构的参数量与精度对比结构输入维度参数量约Indian Pines 10样本/类 OA训练时间/epoch1D-CNN(1, 30)2.5K78-82%0.5s2D-CNN(30, 11, 11)45K85-89%2s3D-CNN(1, 30, 11, 11)12K88-92%5s这张表是我自己在同一台机器上跑出来的量级具体数值会随随机种子和超参波动。关键结论3D-CNN 精度最高但训练最慢1D-CNN 最快但精度最低。2D-CNN 是折中但输入构造比 3D 麻烦。如果计算资源有限2D-CNN 性价比最高。5. 高光谱分类避坑这 5 个翻车点我全踩过5.1 现象训练 loss 正常下降但测试精度只有 20% 左右原因标签没有减 1或者测试集标签里混入了背景类 0。CrossEntropyLoss 遇到标签 0 且类别从 1 开始时会把背景当成一个真实类别学但测试时背景像素又被排除导致预测全乱。解决训练前统一y y - 1并确认测试集里没有 0 标签。用np.unique(y_test)检查一遍确保最小标签是 0。5.2 现象3D-CNN 训练几个 epoch 后 loss 变 NaN原因高光谱原始 DN 值没归一化或者 PCA 后没做白化某些波段方差极大梯度爆炸。3D 卷积对输入尺度比 1D/2D 更敏感。解决输入必须归一化到 [0,1] 或做 Z-Score。PCA 时加whitenTrue。如果还 NaN把学习率降到 1e-4加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)。5.3 现象2D-CNN 精度比 1D-CNN 还低原因patch 提取时边界填充用了补零导致边界像素的 patch 里全是 0网络学到的特征被污染。或者 PCA 主成分数取太少空间纹理信息丢失。解决边界填充改用modereflect或modesymmetric。PCA 主成分数至少保留 20-30 个别只取前 3 个当 RGB 用。5.4 现象换了个数据集精度直接崩到 50%原因不同数据集的波段数、空间尺寸、地物类别数都不同网络结构里的全连接层输入维度写死了。比如 Indian Pines 是 16 类Pavia 是 9 类直接套用会维度不匹配或类别错位。解决把num_classes和in_bands做成参数传入不要硬编码。全连接层前用AdaptiveAvgPool而不是固定尺寸的view这样输入尺寸变化时网络不用改。5.5 现象每次跑结果都不一样论文没法复现原因随机种子没固定包括 Python 的random、NumPy 的np.random、PyTorch 的torch.manual_seed还有 CUDA 的torch.cuda.manual_seed_all。另外 DataLoader 的shuffleTrue也会引入随机性。解决在代码开头统一设种子import random import numpy as np import torch def set_seed(seed42): 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会牺牲一点速度但保证结果可复现。benchmark False同理。如果还不行检查 DataLoader 的num_workers是否大于 0多进程加载数据也会引入不确定性。6. 把精度再往上推 2 个点几个我常用的调参技巧6.1 学习率调度与早停固定学习率在 100 个 epoch 里往往不是最优。我一般用余弦退火scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max100, eta_min1e-5)每个 epoch 后调scheduler.step()。T_max100对应总 epoch 数eta_min1e-5是学习率下限。配合早停验证集 loss 连续 20 个 epoch 不降就停保存验证集精度最高的模型权重。6.2 数据增强高光谱能用的几种方式高光谱数据增强不能随便翻转因为光谱曲线有物理意义。我常用的两种一是加高斯噪声X np.random.normal(0, 0.01, X.shape)二是随机擦除部分波段模拟波段缺失。这两种在样本极少时能把精度提 1-3 个点。6.3 一个容易被忽略的细节测试集评估方式很多人用model.eval()后直接算整体精度OA但高光谱分类更该看每类精度和 Kappa 系数。小类精度往往被大类淹没OA 高不代表模型好用。我习惯输出混淆矩阵看哪些类之间在互相误判再针对性调整。from sklearn.metrics import confusion_matrix, cohen_kappa_score model.eval() with torch.no_grad(): X_test_tensor torch.FloatTensor(X_test_pca).unsqueeze(1).to(device) logits model(X_test_tensor) preds logits.argmax(dim1).cpu().numpy() cm confusion_matrix(y_test - 1, preds) kappa cohen_kappa_score(y_test - 1, preds) oa (preds (y_test - 1)).mean() print(fOA: {oa:.4f}, Kappa: {kappa:.4f}) print(cm)confusion_matrix的对角线是正确分类数非对角线能看出哪两类在混。比如 Indian Pines 里 Corn-notill 和 Corn-mintill 经常互混因为光谱太像。这时候可以考虑加空间后处理比如对分类结果做多数投票滤波。我自己的习惯是每跑完一组实验先把混淆矩阵打出来看再决定下一步是调网络还是调数据。高光谱分类没有一招鲜1D、2D、3D-CNN 各有各的适用场景先跑通 1D 建立 baseline再上 2D/3D 对比比一上来就堆 3D 网络靠谱得多。希望帮到你。本文还有配套的精品资源点击获取