资讯动态

高光谱图像分类实战:1D/2D/3D CNN选型与避坑指南

发布时间:2026/10/1 7:57:01 来源:尧图企业网站定制
简介本资源面向遥感图像处理与深度学习方向的科研人员、工程师及学生提供基于卷积神经网络的高光谱图像分类完整实现方案涵盖1D-CNN、2D-CNN与3D-CNN三种网络结构帮助读者对比不同维度卷积模型在光谱与空间特征提取上的表现差异。压缩包共11个文件约229.71MB包含5个mat格式的高光谱数据集与地面真实标签、4个py模型与工具脚本、1个rar辅助数据包及1个h5训练权重文件覆盖从数据加载、预处理到模型训练与评估的完整流程。已有244人学习下载。读者可借助PaviaU、KSC等数据集与对应标签直接复现一维、二维、三维卷积分类实验理解各模型在计算量与分类精度之间的权衡并参考工具脚本组织实验流程为高光谱分类模型的设计、优化与验证提供可落地的代码基础。1. 从一份 cnn_3d.zip 说起高光谱分类的三种 CNN 路线怎么选高光谱图像分类这个方向很多人第一次上手时都会卡在同一个地方数据是.mat格式标签是稀疏的_gt.mat模型有 1D、2D、3D 三种卷积路线论文里都写着“效果提升”但真到自己跑的时候连输入张量的维度都对不上。我拿到cnn_3d.zip这个包的时候第一反应不是看代码写得多漂亮而是先确认它到底把哪几条路线跑通了。包里给了1DCNN.py、2DCNN.py、3DCNN.py三个入口配套fun.py做公共函数数据集是PaviaU.mat和KSC.mat两套经典高光谱数据标签分别是PaviaU_gt.mat、KSC_gt.mat另外还有pcasg10.rar和pcasg10_gt.mat这种降维后的版本以及一个已经训练好的PaviaU_1DCNN.h5权重文件。这套东西适合谁适合已经看过卷积神经网络原理、知道卷积核在干什么但还没亲手把高光谱数据喂进模型跑出分类图的人。它不教你反向传播怎么推导它解决的是“我有一份高光谱数据怎么用三种不同维度的 CNN 把它分类并且能对比结果”这个具体问题。2. 三种 CNN 的输入张量到底差在哪1D、2D、3D 的选型逻辑2.1 光谱向量、空间块、光谱-空间立方体高光谱图像和普通 RGB 图像最大的区别在于光谱维度。一张PaviaU.mat里的数据常见尺寸是 610×340×103也就是说每个像素不是一个 RGB 三元组而是一条 103 维的光谱曲线。1D-CNN 的做法最直接把每个像素的光谱曲线当成一个长度为 103 的序列用一维卷积核在光谱维度上滑动提取的是“哪些波段组合起来能区分地物”。它的输入张量形状是(样本数, 波段数, 1)本质上是把光谱当成一维信号处理。2D-CNN 换了个思路。它不再只看单个像素而是以每个像素为中心取一个patch_size × patch_size的空间邻域比如 9×9 或 11×11然后把邻域内每个像素的光谱降维到主成分常见做法是用 PCA 降到 10 到 30 个主成分形成一个(patch_size, patch_size, 主成分数)的三维张量。卷积核在空间两个维度上滑动提取的是“中心像素周围的空间纹理和光谱主成分的联合特征”。这里pcasg10.rar和pcasg10_gt.mat就是干这个的sg通常指 superpixel 或 spectral grouping10指降维后的维度。3D-CNN 最贪心它直接在原始高光谱立方体上做三维卷积卷积核同时在空间两个维度和光谱一个维度上滑动。输入张量是(patch_size, patch_size, 波段数, 1)比如(9, 9, 103, 1)。这样做的代价是参数量和计算量急剧上升但好处是空间和光谱的联合特征不需要人工降维网络自己学。三种路线的对比如下模型输入张量形状卷积维度空间信息光谱信息计算量1D-CNN(样本, 波段, 1)1D忽略完整低2D-CNN(样本, patch, patch, 主成分)2D局部邻域PCA 降维后中3D-CNN(样本, patch, patch, 波段, 1)3D局部邻域完整高2.2 为什么 2D-CNN 常配 PCA而 3D-CNN 不配这里有个容易翻车的点2D-CNN 如果直接把 103 个波段塞进卷积核参数量会爆炸而且波段间高度相关冗余太大。所以常见做法是先对整幅高光谱图像做 PCA保留前 10 到 30 个主成分再切 patch。pcasg10.rar里的10大概率就是保留 10 个主成分。3D-CNN 不配 PCA 的原因不是不能配而是它的设计初衷就是让网络自己学光谱维度的卷积核如果先 PCA 降维光谱维度从 103 降到 103D 卷积在光谱方向上的感受野就只剩 10反而失去了 3D 卷积的意义。但代价是 3D-CNN 训练慢显存占用高PaviaU这种 103 波段的数据patch 取 9batch size 可能只能开到 16 甚至 8。2.3 从fun.py看公共函数的组织方式fun.py这个文件名字很泛但在这种项目里它通常承担几件事加载.mat数据、生成训练和测试样本、切 patch、归一化、计算分类精度。我一般会先打开它确认三个函数的签名load_data、sample_divide、patch或get_patch。如果fun.py里没有这些那三个1DCNN.py、2DCNN.py、3DCNN.py里就会各写一遍维护起来很痛苦。下面是一个典型的fun.py里数据加载和样本划分的逻辑我按常见写法补全你可以对照包里的实际代码看差异import scipy.io as sio import numpy as np from sklearn.model_selection import train_test_split def load_data(dataset_name): # 加载高光谱数据PaviaU.mat 里通常键名是 paviaU data sio.loadmat(f{dataset_name}.mat) label sio.loadmat(f{dataset_name}_gt.mat) # 不同数据集的键名不一样PaviaU 是 paviaUKSC 是 KSC key [k for k in data.keys() if not k.startswith(__)][0] gt_key [k for k in label.keys() if not k.startswith(__)][0] img data[key].astype(np.float32) gt label[gt_key].astype(np.int32) return img, gt def sample_divide(gt, train_ratio0.1): # 按类别分层采样每类取 train_ratio 作为训练样本 train_idx, test_idx [], [] for c in np.unique(gt): if c 0: # 0 通常是背景不参与分类 continue idx np.where(gt c) idx np.array(idx).T tr, te train_test_split(idx, train_sizetrain_ratio, random_state42) train_idx.extend(tr) test_idx.extend(te) return np.array(train_idx), np.array(test_idx)这段代码里有两个参数需要你根据实际情况改train_ratio控制每类训练样本比例常见设 0.05 到 0.2设太小精度上不去设太大测试集不够对比不公平random_state固定随机种子保证你每次跑出来的训练集一样不然调参时精度波动你分不清是模型变了还是数据变了。load_data里那个key的取法是个偷懒写法直接取第一个非__开头的键如果.mat里有多余变量就会取错稳妥做法是显式写data[paviaU]和label[paviaU_gt]。3. 把 PaviaU.mat 跑起来1D/2D/3D 三条路线的实操步骤3.1 1D-CNN 的最小可跑流程1D-CNN 是最容易先跑通的因为它不需要切 patch输入就是每个像素的光谱向量。1DCNN.py里通常先调fun.py的load_data和sample_divide然后把训练样本的光谱取出来形状是(样本数, 103)再扩一维变成(样本数, 103, 1)喂给Conv1D。下面是一个最小可跑的 1D-CNN 结构import numpy as np import tensorflow as tf from tensorflow.keras import layers, models def build_1dcnn(input_shape, num_classes): model models.Sequential([ # 第一层一维卷积32 个卷积核核大小 7在光谱维度上滑动 layers.Conv1D(32, 7, activationrelu, paddingsame, input_shapeinput_shape), layers.BatchNormalization(), layers.MaxPooling1D(2), # 第二层一维卷积64 个卷积核核大小 5 layers.Conv1D(64, 5, activationrelu, paddingsame), layers.BatchNormalization(), layers.MaxPooling1D(2), layers.Flatten(), layers.Dense(128, activationrelu), layers.Dropout(0.4), layers.Dense(num_classes, activationsoftmax) ]) model.compile(optimizeradam, losscategorical_crossentropy, metrics[accuracy]) return model # 假设 img 形状 (610, 340, 103)gt 形状 (610, 340) img, gt load_data(PaviaU) train_idx, test_idx sample_divide(gt, train_ratio0.1) # 取训练样本光谱 X_train img[train_idx[:, 0], train_idx[:, 1], :] # (n, 103) X_train X_train[..., np.newaxis] # (n, 103, 1) y_train gt[train_idx[:, 0], train_idx[:, 1]] - 1 # 标签从 1 开始转成 0 开始 y_train tf.keras.utils.to_categorical(y_train) model build_1dcnn((103, 1), num_classes9) model.fit(X_train, y_train, batch_size64, epochs100, validation_split0.1)这里Conv1D(32, 7)的两个参数分别是卷积核数量和核大小。核大小 7 意味着每次看 7 个相邻波段paddingsame保证输出长度不变。MaxPooling1D(2)把光谱维度减半两层池化后 103 变成 25 左右。Dropout(0.4)是防过拟合的关键高光谱训练样本少的时候不加 dropout 训练精度能到 99%测试精度可能只有 80%。batch_size64对 1D-CNN 来说比较稳显存不够就降到 32。3.2 2D-CNN 的 patch 切取与 PCA 降维2D-CNN 的实操难点不在网络结构而在 patch 切取和 PCA 的先后顺序。常见做法是先对整幅img做 PCA保留前 10 个主成分得到(610, 340, 10)的降维数据然后对每个有标签的像素以它为中心取9×9的邻域如果邻域超出图像边界就用零填充或者镜像填充。下面这段代码是 patch 切取的核心逻辑from sklearn.decomposition import PCA def pca_reduce(img, n_components10): # img 形状 (H, W, B)reshape 成 (H*W, B) 做 PCA H, W, B img.shape flat img.reshape(-1, B) pca PCA(n_componentsn_components) reduced pca.fit_transform(flat) return reduced.reshape(H, W, n_components) def extract_patches(img_reduced, gt, patch_size9): # 对每个有标签像素取 patch边界用零填充 margin patch_size // 2 padded np.pad(img_reduced, ((margin, margin), (margin, margin), (0, 0)), modeconstant) 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)pca_reduce里n_components10对应pcasg10里的 10你可以改成 15 或 20 对比精度。extract_patches里margin patch_size // 2是为了让中心像素对齐np.pad的modeconstant是零填充也可以用reflect镜像填充后者在边界样本上通常更稳。切完 patch 后训练数据形状是(样本数, 9, 9, 10)直接喂给Conv2D。2D-CNN 的网络结构里第一层Conv2D(32, (3, 3))在 9×9 的空间上卷积两层池化后空间维度降到 2×2 左右再Flatten接全连接。3.3 3D-CNN 的显存控制与训练节奏3D-CNN 的代码写起来和 2D 差不多只是Conv2D换成Conv3D输入形状从(9, 9, 10)变成(9, 9, 103, 1)。但跑起来完全是两回事。PaviaU有 103 个波段patch 取 9单个样本就是 9×9×1038343 个浮点数batch size 开到 32 就是 26 万个浮点数加上 3D 卷积核的参数量显存很容易爆。我一般会先把 batch size 设成 8 或 16跑通一个 epoch 看显存占用再往上加。下面是一个 3D-CNN 的典型结构def build_3dcnn(input_shape, num_classes): model models.Sequential([ # 第一层 3D 卷积8 个卷积核核大小 (3,3,7)光谱方向核大一点 layers.Conv3D(8, (3, 3, 7), activationrelu, paddingsame, input_shapeinput_shape), layers.BatchNormalization(), layers.MaxPooling3D((2, 2, 2)), layers.Conv3D(16, (3, 3, 5), activationrelu, paddingsame), layers.BatchNormalization(), layers.MaxPooling3D((2, 2, 2)), layers.Flatten(), layers.Dense(128, activationrelu), layers.Dropout(0.5), layers.Dense(num_classes, activationsoftmax) ]) model.compile(optimizertf.keras.optimizers.Adam(1e-3), losscategorical_crossentropy, metrics[accuracy]) return modelConv3D(8, (3, 3, 7))里核大小(3, 3, 7)是空间 3×3、光谱 7光谱方向核大一点是因为光谱维度有 103空间只有 9。MaxPooling3D((2, 2, 2))在三个维度上都减半两层后 9×9×103 变成 2×2×25 左右。Dropout(0.5)比 1D 和 2D 都高因为 3D-CNN 参数多过拟合风险更大。训练时epochs通常设 200 到 300batch_size设 8 到 16学习率用1e-3起步如果 loss 震荡就降到1e-4。PaviaU_1DCNN.h5这个权重文件说明作者至少把 1D-CNN 训完并保存了你可以用model.load_weights(PaviaU_1DCNN.h5)直接加载省去重新训练的时间。4. 避坑与排查高光谱 CNN 训练里最容易翻车的五件事4.1 标签从 1 开始没转成 0 开始loss 直接 NaN现象模型编译没问题一fit就报InvalidArgumentError或者 loss 变成 NaN。原因PaviaU_gt.mat里的标签是 1 到 9背景是 0而categorical_crossentropy要求标签从 0 开始。解决在to_categorical之前先减 1y gt[...] - 1并且确认num_classes等于实际类别数PaviaU是 9 类KSC是 13 类别写死。4.2 PCA 在切 patch 之后做数据泄漏了现象测试精度高得离谱换一组随机种子精度掉一大截。原因先切 patch 再对每个 patch 单独做 PCA等于把测试集的信息混进了训练集。解决PCA 必须对整幅图像做fit只在训练数据上或者干脆对全图做无监督 PCA因为 PCA 不涉及标签对全图做是常见做法但切 patch 的顺序必须在 PCA 之后。4.3 3D-CNN 显存不够batch size 降了还是 OOM现象batch_size从 32 降到 8 还是ResourceExhaustedError。原因3D-CNN 的参数量集中在第一层卷积核和全连接层Flatten之后的维度可能非常大。解决先算Flatten后的维度如果超过 10000就在Flatten前再加一层MaxPooling3D或者用GlobalAveragePooling3D替代Flatten。另外把PaviaU的 103 个波段用 PCA 降到 30 再跑 3D精度掉得不多显存省一半。4.4 训练集和测试集按像素随机分空间邻域重叠现象2D-CNN 和 3D-CNN 的测试精度比 1D-CNN 高很多但实际预测图上有明显的块状噪声。原因切 patch 时相邻像素的 patch 高度重叠如果训练集和测试集按像素随机分训练 patch 和测试 patch 可能共享大部分邻域像素造成信息泄漏。解决按空间块划分比如把图像分成若干不重叠的区域整块划入训练或测试或者至少保证训练像素和测试像素之间的空间距离大于 patch_size。4.5 忘了做光谱归一化不同数据集精度差异巨大现象PaviaU上精度 95%换KSC直接掉到 70%。原因PaviaU的反射率范围是 0 到 1 左右KSC的数值范围可能完全不同没有归一化时网络对输入尺度敏感。解决在load_data之后加一步逐波段归一化img (img - img.min()) / (img.max() - img.min())或者用StandardScaler对每个波段做零均值单位方差。这一步对 1D-CNN 尤其重要因为 1D-CNN 直接吃原始光谱。5. 从 PaviaU_1DCNN.h5 到 KSC 迁移一个验证模型是否真学到的技巧PaviaU_1DCNN.h5这个权重文件除了直接加载做推理还有一个用法是验证模型到底学到了什么。我一般会做两件事第一把PaviaU训练好的 1D-CNN 在KSC上直接测试不重新训练看精度掉多少。如果掉到随机水平说明模型学的是PaviaU特有的光谱模式没有泛化性如果还能保持 60% 以上说明光谱特征有一定通用性。第二把PaviaU_1DCNN.h5的最后一层Dense去掉用前面的卷积层作为特征提取器对KSC的光谱做t-SNE可视化看类别是否可分。下面是一个加载权重并做迁移验证的代码片段# 加载 PaviaU 上训练好的 1D-CNN model build_1dcnn((103, 1), num_classes9) model.load_weights(PaviaU_1DCNN.h5) # 去掉最后一层取倒数第二层输出作为特征 feature_model models.Model(inputsmodel.input, outputsmodel.layers[-3].output) # 在 KSC 上提取特征注意 KSC 波段数可能不同需要对齐 img_ksc, gt_ksc load_data(KSC) # 如果 KSC 波段数和 PaviaU 不一致这里需要截断或插值 X_ksc img_ksc[gt_ksc 0] # 取所有有标签像素 X_ksc X_ksc[..., np.newaxis] features feature_model.predict(X_ksc, batch_size256) # 用 t-SNE 降到二维看类别分布 from sklearn.manifold import TSNE tsne TSNE(n_components2, random_state42) feat_2d tsne.fit_transform(features)这里的关键参数是model.layers[-3].output索引取决于你的网络层数build_1dcnn里倒数第三层是Dense(128)取它的输出就是 128 维特征。batch_size256在推理时可以开大不占梯度。TSNE的random_state固定后不同次运行结果一致方便对比。如果KSC的波段数和PaviaU不一样比如KSC是 176 波段PaviaU是 103直接迁移会报维度错误常见做法是取前 103 个波段或者对KSC做 PCA 降到 103 维再迁移。从那以后我每次拿到一个新的高光谱 CNN 代码包都强制先跑一遍 1D-CNN 的基线确认数据加载、标签映射、归一化这三步没问题再去碰 2D 和 3D。因为 1D-CNN 最快出错也最容易定位如果 1D 都跑不通2D 和 3D 的报错只会更难查。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑