高光谱分类实战:1D、2D、3D-CNN选型与避坑指南
简介本资源面向遥感图像处理与深度学习方向的科研人员、工程师及学生提供基于卷积神经网络的高光谱图像分类完整实践方案涵盖1D-CNN、2D-CNN与3D-CNN三种网络结构帮助读者对比不同维度卷积模型在光谱与空间特征提取上的表现差异。压缩包共11个文件约229.71MB包含5个mat格式的高光谱数据集与地面真实标签、4个py模型与工具脚本、1个rar辅助压缩包及1个h5训练权重文件覆盖数据加载、预处理、模型训练与评估全流程。已有244人学习下载。读者可借助PaviaU、KSC等经典数据集直接运行对应网络脚本观察一维光谱特征、二维空间光谱融合与三维空谱联合卷积的分类效果并利用辅助函数模块管理实验流程、复现结果为高光谱分类模型选型与优化提供可操作的参考。1. 高光谱分类为什么让 1D、2D、3D-CNN 各占一条赛道高光谱遥感把可见光到短波红外切成上百个连续窄波段每个像素拿到一条近似连续的光谱曲线分类任务就是给每个像素判一个地物类别。卷积神经网络在这里不是单一模型而是三种输入形态1D-CNN 只吃光谱向量2D-CNN 把某个波段或主成分当图像处理3D-CNN 同时卷空间和光谱。很多人第一次跑高光谱分类直接套 2D 卷积网络结果在 Indian Pines 或 Pavia University 上精度卡在 80% 上下换 3D 又显存爆炸。问题不在网络本身而在你喂进去的数据维度决定了卷积核到底在卷什么。这篇笔记按 1D、2D、3D 三条路线拆开讲清各自适合什么数据规模、参数怎么设、代码怎么落地以及我踩过的那些翻车点。适合手里有高光谱数据、想用卷积神经网络做分类但不确定从哪条路切入的从业者。2. 三条路线的输入构造与选型判断2.1 1D-CNN只卷光谱样本少时的保底方案1D-CNN 的输入是一个像素的光谱向量形状为(bands, 1)。卷积核只沿波段方向滑动提取的是吸收峰、反射率突变这类光谱特征。它的优势很直接参数量小训练快对样本量要求低。Indian Pines 只有 10249 个有标签像素分 16 类每类最少只有 20 个样本这种场景下 1D-CNN 往往比 3D 更稳。常见做法是把每个像素的光谱做标准化然后按类别分层采样每类取 10% 做训练、10% 做验证、剩下做测试。输入不需要空间邻域所以不用做 PCA 降维直接把原始波段送进去。我一般会把光谱做逐波段 z-score 标准化因为不同波段的量纲差异很大不标准化的话卷积核会被高幅值波段主导。import numpy as np import scipy.io as sio from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split # 读取 Indian Pines 数据data 形状 (145,145,200)gt 形状 (145,145) data sio.loadmat(Indian_pines_corrected.mat)[indian_pines_corrected] gt sio.loadmat(Indian_pines_gt.mat)[indian_pines_gt] # 展平成像素级样本去掉背景类 0 X data.reshape(-1, data.shape[-1]) # (21025, 200) y gt.reshape(-1) # (21025,) mask y 0 X, y X[mask], y[mask] # 逐波段标准化fit 只在训练集上做避免验证集信息泄漏 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.8, stratifyy, random_state42 ) scaler StandardScaler().fit(X_train) X_train scaler.transform(X_train) X_test scaler.transform(X_test) # 1D-CNN 需要 (样本, 波段, 1) 的形状 X_train X_train[..., np.newaxis] X_test X_test[..., np.newaxis] print(X_train.shape, X_test.shape) # (2064, 200, 1) (8256, 200, 1)这段代码的关键在stratifyy和StandardScaler只 fit 训练集。高光谱类别极不平衡不分层采样的话某些类可能一个训练样本都没有。标准化如果全量 fit验证集的信息会渗进训练精度虚高这是血泪经验。1D-CNN 的网络结构通常三到四层一维卷积每层后面接 BN 和 ReLU最后全局平均池化接全连接分类。卷积核大小一般取 3 到 7太大反而把相邻波段的细节抹平。通道数从 32 起步逐层翻倍到 128 就够再大容易过拟合。2.2 2D-CNN把高光谱当图像先降维再卷空间2D-CNN 的思路是把高光谱的某个波段或降维后的主成分当成灰度图用二维卷积核卷空间纹理。它适合空间分辨率较高、地物有明确纹理差异的数据比如 Pavia University 这种城市地物。直接拿原始波段做 2D 卷积没有意义因为相邻波段高度相关卷积核学不到额外信息常见做法是先做 PCA 降到 15 到 30 个主成分取前三个当 RGB 或者逐主成分送入网络。输入构造有两种一种是逐像素取空间邻域比如以每个像素为中心取 11×11 的窗口窗口内每个主成分作为一个通道输入形状(11, 11, n_components)另一种是整图送入全卷积网络做语义分割。前者适合样本少的情况后者需要更多显存和标注。from sklearn.decomposition import PCA # 对全图做 PCA保留 20 个主成分 h, w, b data.shape X_flat data.reshape(-1, b) pca PCA(n_components20, whitenTrue) X_pca pca.fit_transform(X_flat).reshape(h, w, 20) # 逐像素取 11x11 空间邻域边缘用 reflect 填充 def extract_patches(pca_img, gt, patch_size11): pad patch_size // 2 padded np.pad(pca_img, ((pad, pad), (pad, pad), (0, 0)), modereflect) patches, labels [], [] for i in range(gt.shape[0]): for j in range(gt.shape[1]): if gt[i, j] 0: continue patch padded[i:ipatch_size, j:jpatch_size, :] patches.append(patch) labels.append(gt[i, j]) return np.array(patches), np.array(labels) X_patches, y_patches extract_patches(X_pca, gt) print(X_patches.shape) # (10249, 11, 11, 20)这里whitenTrue让 PCA 各主成分方差归一避免第一主成分主导卷积。窗口大小 11 是经验值太小空间上下文不足太大边缘像素的邻域会包含大量其他类别反而引入噪声。Pavia University 上 9 到 13 都常见Indian Pines 因为空间分辨率低窗口取 7 到 9 更合适。2D-CNN 的网络结构就是标准二维卷积堆叠每层Conv2D BN ReLU最后全局平均池化。注意输入通道数是主成分数而不是 3第一层卷积核要匹配这个通道数。如果显存不够可以把主成分降到 10 个精度损失通常在一个百分点以内。2.3 3D-CNN空间和光谱一起卷精度上限最高但代价最大3D-CNN 的卷积核在空间两个维度和光谱一个维度上同时滑动输入形状(patch_size, patch_size, bands, 1)。它能同时捕捉空间纹理和光谱特征在样本充足时精度通常比 1D 和 2D 高两到五个百分点。代价是参数量和显存占用成倍增长一个 11×11×200 的输入第一层 3D 卷积核如果是 3×3×7参数量就是 2D 的数倍。常见做法是先对光谱做 PCA 降到 30 到 60 个波段再取空间邻域这样输入尺寸从(11,11,200)降到(11,11,40)显存压力小很多。如果不降维batch size 只能设到 8 甚至 4训练极慢。import tensorflow as tf from tensorflow.keras import layers, models def build_3dcnn(input_shape, num_classes): inputs layers.Input(shapeinput_shape) # (11, 11, 40, 1) x layers.Conv3D(32, (3, 3, 7), paddingsame, activationrelu)(inputs) x layers.BatchNormalization()(x) x layers.Conv3D(64, (3, 3, 5), paddingsame, activationrelu)(x) x layers.BatchNormalization()(x) x layers.MaxPooling3D((2, 2, 2))(x) x layers.Conv3D(128, (3, 3, 3), paddingsame, activationrelu)(x) x layers.BatchNormalization()(x) x layers.GlobalAveragePooling3D()(x) x layers.Dropout(0.4)(x) outputs layers.Dense(num_classes, activationsoftmax)(x) return models.Model(inputs, outputs) model build_3dcnn((11, 11, 40, 1), 16) model.summary()第一层卷积核(3,3,7)在光谱方向取 7 是为了覆盖一个完整的吸收特征空间方向取 3 是控制参数量。第二层光谱核降到 5第三层降到 3这是逐步聚合的常见节奏。GlobalAveragePooling3D替代 Flatten 能大幅减少全连接层参数Dropout 0.4 到 0.5 是必须的否则训练集精度能到 99% 而测试集只有 85%。提示3D-CNN 的 batch size 从 16 起步试显存不够就降到 8再不够就降 PCA 维度不要先砍空间窗口空间上下文对精度影响比光谱维度大。3. 训练配置、参数调优与精度验证3.1 学习率、优化器和损失函数的实际取值三条路线我都用 Adam 起步学习率 1e-3配合ReduceLROnPlateau在验证损失不降时减半。1D-CNN 收敛快通常 50 个 epoch 就够2D 和 3D 需要 100 到 200 个 epoch。损失函数用稀疏交叉熵因为标签是整数类别而不是 one-hot省一步转换。类别不平衡是高光谱的常态Indian Pines 里某些类只有几十个样本。常见做法是给损失函数加类别权重权重取该类样本数的倒数再归一化。Keras 里用class_weight参数传入字典即可。不加权的话模型会偏向多数类少数类召回率极低。from sklearn.utils.class_weight import compute_class_weight import numpy as np classes np.unique(y_train) weights compute_class_weight(balanced, classesclasses, yy_train) class_weight dict(zip(classes, weights)) model.compile( optimizertf.keras.optimizers.Adam(1e-3), losssparse_categorical_crossentropy, metrics[accuracy] ) callbacks [ tf.keras.callbacks.ReduceLROnPlateau(monitorval_loss, factor0.5, patience10), tf.keras.callbacks.EarlyStopping(monitorval_loss, patience20, restore_best_weightsTrue) ] history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs200, batch_size16, class_weightclass_weight, callbackscallbacks )balanced模式自动按样本数反比算权重比自己手算省事。EarlyStopping的restore_best_weightsTrue很重要否则拿到的可能是过拟合后的权重。patience 设 20 是因为高光谱验证损失波动大设太小会提前停。3.2 评价指标不能只看总体精度高光谱分类论文里常报总体精度、平均精度和 Kappa 系数。总体精度在类别不平衡时会虚高一个把所有像素判成多数类的模型也能有 70% 以上的总体精度。平均精度是各类精度的平均对少数类更敏感。Kappa 系数衡量的是比随机猜测好多少低于 0.6 基本不可用。我一般三个都报并且单独看混淆矩阵。如果某个类的召回率低于 60%先查这一类是不是样本太少再查它的光谱是不是和别的类高度重叠。Indian Pines 里玉米和大豆的混淆是经典问题光谱几乎重合这时候空间信息就很重要2D 和 3D 的优势在这类场景才体现出来。from sklearn.metrics import classification_report, cohen_kappa_score y_pred model.predict(X_test).argmax(axis1) print(classification_report(y_test, y_pred, digits4)) print(Kappa:, cohen_kappa_score(y_test, y_pred))classification_report直接给出每类的精确率、召回率和 F1比只看一个总体数字有用得多。Kappa 用cohen_kappa_score算注意传入的是原始标签不是 one-hot。3.3 三条路线的精度与资源对比在 Indian Pines 上我跑过的典型结果是1D-CNN 总体精度 88% 到 92%2D-CNN 90% 到 94%3D-CNN 94% 到 98%。Pavia University 上 3D 能到 99% 以上但训练时间是 1D 的五到八倍。显存占用方面同样 batch size 161D 不到 1GB2D 约 2GB3D 在 PCA 降到 40 维后约 6GB不降维直接爆 12GB。路线输入形状参数量级训练时间适合场景1D-CNN(bands, 1)十万级快样本极少、只关心光谱2D-CNN(patch, patch, PCs)百万级中空间纹理明显、样本中等3D-CNN(patch, patch, PCs, 1)千万级慢样本充足、追求精度上限选型判断很简单样本每类少于 50 个先上 1D空间分辨率高且地物有纹理差异上 2D样本每类超过 200 个且显存够上 3D。不要一上来就 3D调参成本太高。4. 高光谱分类避坑五条踩坑记录4.1 现象验证集精度 99%测试集只有 70%原因空间邻域提取时训练集和测试集的窗口有重叠同一个像素的邻域信息同时出现在训练和测试里造成信息泄漏。高光谱空间分辨率低相邻像素高度相关这个问题特别隐蔽。解决按空间块划分训练测试集而不是按像素随机划分。具体做法是把整图切成若干不重叠的块整块分给训练或测试。如果必须按像素划分训练集和测试集之间留至少一个 patch_size 的缓冲带。4.2 现象3D-CNN 训练 loss 一直不降卡在 2.3 附近原因输入没有做标准化原始反射率值在 0 到 1 之间但分布极偏某些波段接近 0卷积核梯度消失。另外 PCA 白化后如果没再标准化各主成分量纲也不一致。解决PCA 之后再做一次逐通道 z-score 标准化或者直接用StandardScaler对每个主成分单独处理。3D 输入还要检查光谱维度的顺序Keras 的 Conv3D 默认通道在最后如果数据是(patch, patch, bands)要手动加一维。4.3 现象1D-CNN 在 Pavia University 上精度只有 75%原因Pavia University 的空间分辨率是 1.3 米地物纹理信息很强只用光谱丢掉了大量判别信息。1D-CNN 在这类数据上天然吃亏不是调参能解决的。解决换 2D 或 3D或者把 1D 提取的光谱特征和空间特征拼接。常见做法是用 1D 卷光谱、2D 卷空间最后特征融合但这已经超出纯 1D 的范畴。4.4 现象训练到 50 个 epoch 后验证精度开始下降原因过拟合。高光谱有标签样本少3D-CNN 参数量大很容易记住训练集。Dropout 设太小或者没加BN 的 momentum 默认 0.99 在样本少时统计量不准。解决Dropout 加到 0.4 以上加 L2 正则化BN 的 momentum 调到 0.9。数据增强也有效对高光谱可以做随机翻转、加高斯噪声、光谱抖动但注意翻转后标签不变光谱抖动幅度不要超过 5%。4.5 现象同一份代码换台机器跑精度差三个百分点原因随机种子没固定。权重初始化、数据打乱、Dropout 都带随机性不固定种子的话结果不可复现。另外 GPU 的 cuDNN 某些算子是非确定性的。解决固定numpy、random、tensorflow的种子设置tf.config.experimental.enable_op_determinism()。如果还差检查两台机器的 TensorFlow 版本和 CUDA 版本是否一致不同版本的 BN 实现有差异。5. 把三条路线串起来特征融合与迁移的进阶做法单一路线跑通之后真正能提精度的是融合。我常用的做法是双分支一个 1D 分支卷原始光谱一个 2D 分支卷 PCA 后的空间邻域两个分支各自全局池化后拼接再接全连接分类。这样既保留了完整光谱信息又利用了空间纹理。在 Indian Pines 上比纯 3D 高一个百分点左右训练时间只有 3D 的一半。def build_hybrid(bands, patch, pcs, num_classes): spec_in layers.Input(shape(bands, 1)) x1 layers.Conv1D(64, 5, activationrelu, paddingsame)(spec_in) x1 layers.BatchNormalization()(x1) x1 layers.GlobalAveragePooling1D()(x1) spat_in layers.Input(shape(patch, patch, pcs)) x2 layers.Conv2D(64, 3, activationrelu, paddingsame)(spat_in) x2 layers.BatchNormalization()(x2) x2 layers.GlobalAveragePooling2D()(x2) x layers.Concatenate()([x1, x2]) x layers.Dropout(0.4)(x) out layers.Dense(num_classes, activationsoftmax)(x) return models.Model([spec_in, spat_in], out)两个分支的输入要分别构造光谱分支用原始波段空间分支用 PCA 后的邻域。拼接前各自全局池化保证特征维度对齐。这个结构在样本中等时最划算比纯 3D 省显存比纯 1D 精度高。另一个方向是迁移学习。高光谱有标签样本少可以先用无标签数据做自监督预训练比如掩码光谱重建再拿有标签数据微调。我试过在 Pavia University 上预训练、Indian Pines 上微调小样本类别的精度能提三到五个百分点。但预训练的数据要和目标任务的光谱范围接近跨传感器迁移效果会打折。验证融合模型有没有真的提升不能只看总体精度。我会固定随机种子跑五次取平均和标准差标准差超过 1.5 个百分点的结果不敢用。另外看每类召回率如果提升只集中在多数类说明融合没解决核心问题。我自己的习惯是新数据先跑 1D 建立基线再跑 2D 看空间信息值不值得加最后决定要不要上 3D 或融合。不要跳过基线直接上复杂模型否则精度不涨你都不知道是数据问题还是模型问题。希望帮到你。本文还有配套的精品资源点击获取