简介围绕Python与卷积神经网络的高光谱图像分类项目面向遥感、环境监测及医学成像等领域的研究者和深度学习入门者。资源针对高光谱数据中1D光谱、2D空间及3D谱空联合特征的学习难点提供了从数据预处理到模型训练、验证评估的完整思路可直接参考或扩展用于自己的实验。包体共8个文件压缩包约87.4MB包含4个Python脚本和4个MATLAB数据文件。脚本分别实现1D、2D、3D卷积神经网络模型及数据读取预处理功能数据文件为KSC与PaviaU高光谱数据集及对应标签可直接用于训练和验证。目前已有3839人学习下载。使用者可获得KSC与PaviaU数据集、完整CNN分类代码以及TensorFlow-GPU 1.5.0和Keras 2.1.6环境下的实操范例。代码按模型维度划分清晰便于对比不同特征学习策略对分类性能的影响适合作为课程设计、论文实验或算法调优的起点。1. 高光谱图像分类为什么CNN方案能打破“训练集99%、实测掉档”的怪圈高光谱图像分类的尴尬之处在于训练集准确率能跑到99%以上换到同一区域的另一景影像上精度直接掉到70%以下很多第一次接触这个方向的开发者都会怀疑自己是不是把模型练坏了。其实问题往往不在模型而在数据侧的波段噪声、样本划分和输入组织方式。这份资源是一套完整的Python CNN 高光谱图像分类工程从ENVI格式数据读取、水汽波段剔除、PCA降维到2D/3D卷积网络搭建、训练调参和OA/AA/Kappa指标评估全部覆盖。适合入门遥感高光谱分析的算法工程师、刚转图像分类方向的在校生以及想把公开数据集跑出靠谱基线结果的人。2. 高光谱数据预处理ENVI读取、水汽波段剔除与PCA降维实战2.1 高光谱数据的存储格式与读取陷阱高光谱影像和普通RGB图最大的差别是波段数。常见传感器能输出200个以上连续波段单波段灰度值并排放进三维立方体里形状是(rows, cols, bands)。公开数据集多以ENVI标准组织一个.hdr文本头文件加一个.dat二进制数据文件。.hdr里记录了行列数、波段数、数据类型、波段顺序BSIL/BSQ/BIP读取时我一般直接用spectral库不用自己去解析头文件。from spectral.io import envi import numpy as np # 读取ENVI格式hdr和dat文件名主体一致 img envi.open(dataset_name.hdr, dataset_name.dat) data np.array(img.load()) # 加载为 (rows, cols, bands) print(data.shape, data.dtype) # 例如 (145, 145, 200)这段代码最关键的是img.load()之后的方向。ENVI的波段顺序在头文件里写死了但不同数据集的默认排列可能不一样有的返回波段在前(200, 145, 145)。拿到数据后第一件事就是打印shape确认它是(rows, cols, bands)还是(bands, rows, cols)否则后面所有切片索引都会反。另一个容易忽略的是dtype部分公开数据集是16位整型直接归一化会得到错误范围建议先转成float32再处理。2.2 水汽吸收波段与低信噪比边缘处理高光谱传感器覆盖可见光到短波红外中间有几段大气水汽吸收严重的区间这些波段接收到的有效信号很少基本是噪声放进模型只会增加计算量并干扰特征学习。不同传感器的坏波段位置不一样以常见的VNIR-SWIR融合数据为例1350-1450纳米和1800-1950纳米附近基本不能用边缘波段也常有条带噪声。# 假设原始200个波段剔除水汽吸收段和边缘噪声段 valid_indices list(range(12, 135)) list(range(148, 198)) data_clean data[:, :, valid_indices] print(保留波段数:, data_clean.shape[2])这里的序号按我的项目习惯写的实际使用时你要对着数据集的光谱范围换算成波段索引。判断方法不复杂把每个波段的二维图像显示出来连续多帧出现横条纹或颗粒感明显的位置就是需要剔除的坏波段。这一步做完模型输入从200维降到约170维训练时间和显存占用都能降下来精度通常不会受损。2.3 归一化顺序与PCA降维归一化是所有数据管线里最容易被埋雷的一步。很多初学者直接对整个数据立方体做fit_transform这在单数据集自测时精度好看但换场景就露馅因为验证集的均值方差被训练集统计量污染了。标准做法是先划分训练集再用训练集的统计量去transform全部数据。from sklearn.preprocessing import StandardScaler n_samples data_clean.shape[0] * data_clean.shape[1] flat data_clean.reshape(-1, data_clean.shape[2]).astype(np.float32) # 只在这部分样本上计算均值和方差 train_flat flat[train_mask] # train_mask是预先划分好的样本索引 scaler StandardScaler() scaler.fit(train_flat) data_norm scaler.transform(flat).reshape(data_clean.shape)高光谱波段间高度相关200维输入直接丢给CNN很多维度是冗余的。PCA降维在高光谱分类里几乎是标配操作它把原始波段压缩成互不相关的主成分。我习惯保留99%能量大约对应30到50个主成分具体数目取决于数据本身。from sklearn.decomposition import PCA pca PCA(n_components0.99, whitenTrue) pca.fit(train_flat) # 同样只用训练集拟合 data_pca pca.transform(flat).reshape(data_clean.shape[0], data_clean.shape[1], pca.n_components_) print(降维后波段数:, pca.n_components_)注意PCA拟合也必须只用训练集否则照样是信息泄漏。降维之后的特征图不是物理波段而是统计意义上的主成分后续可视化时要明白这一点不要拿它当真实光谱图去解释。3. 卷积网络选型与搭建2D-CNN和3D-CNN在PyTorch中的实现3.1 为什么CNN能赢过“单像素分类器”高光谱图像分类早期主流做法是SVM或随机森林逐像素提取光谱向量作为输入。这个思路假设像素之间独立但实际地物分布一定有空间连续性——农田边界、城区屋顶、植被覆盖区都表现出明显的邻域相关性。CNN的优势在于用一个局部感受野同时看到“中心像素的光谱特征”和“周围像素的空间纹理”这就是所谓空间-光谱联合特征。我把这种差异理解为SVM是在一维光谱线上做决策CNN是在一个三维光谱立方体上做决策信息量完全不在一个量级。这也是为什么在样本量充足时CNN的OA普遍能比SVM高出8-15个百分点。3.2 2D-CNN基线模型把光谱维当作通道2D-CNN处理高光谱的方式很直观对每个像素取一个邻域patchpatch形状是(patch_size, patch_size, n_bands)把光谱维等价成图像分类里的通道维然后做常规卷积。PyTorch实现里只需要把输入维度从RGB的3通道改成n_bands。import torch.nn as nn class CNN2D(nn.Module): def __init__(self, n_bands, n_classes, patch_size9): super().__init__() self.features nn.Sequential( nn.Conv2d(n_bands, 16, kernel_size3, padding1), nn.BatchNorm2d(16), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(16, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.AdaptiveAvgPool2d(1), ) self.classifier nn.Linear(32, n_classes) def forward(self, x): # x: (batch, n_bands, patch_size, patch_size) feat self.features(x).flatten(1) return self.classifier(feat)这个基线模型只有两层卷积加一个全连接参数量很小不容易在小样本高光谱数据上过拟合。这里特别推荐用AdaptiveAvgPool2d替代Flatten后接全连接因为空间池化能把任意patch尺寸统一成固定特征维度换patch大小时不用改网络结构。BatchNorm在高光谱分类里几乎是必需品数据经过PCA后分布虽然标准化但网络内部每层输出的分布仍在漂移BN能明显加速收敛。3.3 3D-CNN模型同时在空间和光谱维度滑动卷积2D-CNN把光谱维当作通道卷积核在空间维滑动时光谱信息是整体参与加权求和没有显式建模相邻波段之间的关系。3D-CNN则是构造一个(height, width, bands)的三维输入卷积核同时在空间和光谱两个方向滑动直接提取光谱-空间联合特征。import torch.nn as nn class CNN3D(nn.Module): def __init__(self, n_classes, n_bands30, patch_size9): super().__init__() # 输入: (batch, 1, patch_size, patch_size, n_bands) self.conv1 nn.Conv3d(1, 8, kernel_size(3, 3, 7), padding(1, 1, 3)) self.bn1 nn.BatchNorm3d(8) self.conv2 nn.Conv3d(8, 16, kernel_size(3, 3, 5), padding(1, 1, 2)) self.bn2 nn.BatchNorm3d(16) self.conv3 nn.Conv3d(16, 32, kernel_size(3, 3, 3), padding(1, 1, 1)) self.bn3 nn.BatchNorm3d(32) self.pool nn.AdaptiveAvgPool3d(1) self.classifier nn.Linear(32, n_classes) def forward(self, x): x torch.relu(self.bn1(self.conv1(x))) x torch.relu(self.bn2(self.conv2(x))) x torch.relu(self.bn3(self.conv3(x))) x self.pool(x).flatten(1) return self.classifier(x)3D卷积核的光谱维kernel_size不能设太大比如7或5因为PCA降维后波段数通常只有30-50光谱维长度不如空间维充裕设太大容易把高频细节抹掉。padding在光谱维要相应补足保证卷积后尺寸不变。3D模型参数量明显大于2D模型所以中间章后面调参时正则化的优先级会更高。4. 训练策略与超参数Patch尺寸、学习率和损失函数的调参路径4.1 样本划分策略与Patch构建高光谱分类的样本划分直接影响最终结论的可信度。随机划分最容易操作但同一地物连通区域的相邻像素会被同时分到训练集和验证集造成严重的空间信息泄漏得到的指标虚高。我做高光谱实验时默认按“区域分块”划分训练、验证和测试集。import numpy as np def split_by_region(labels, train_ratio0.05, val_ratio0.05): 按连通区域划分避免空间信息泄漏 unique_regions np.unique(labels[labels 0]) perm np.random.permutation(unique_regions) n_train int(len(perm) * train_ratio) n_val int(len(perm) * val_ratio) train_regions perm[:n_train] val_regions perm[n_train:n_train n_val] test_regions perm[n_train n_val:] train_mask np.isin(labels, train_regions) val_mask np.isin(labels, val_regions) test_mask np.isin(labels, test_regions) return train_mask, val_mask, test_mask这段代码的本质是让同一地物大块区域只能出现在一个集合里模型没有机会“记住”邻居像素的标签。要构造模型输入需要以每个标注像素为中心取邻域patchpatch大小是影响精度的关键参数。我一般设9或11patch太小空间上下文不足patch太大则边界像素会被周围异类地物干扰同时样本量膨胀后训练变慢。4.2 超参数配置与调参路径高光谱分类的调参和普通图像分类有差异体现在学习率和正则化上。数据维度高、样本量小学习率稍大就震荡稍小则收敛慢。我通常固定一个基线配置再逐个变量做消融实验。超参数推荐值范围备注patch_size9 ~ 13优先试9效果不稳再加大batch_size32 ~ 128小样本场景64最稳学习率1e-3 ~ 5e-3Adam配合余弦退火优化器Adam比SGD收敛快且对超参不敏感weight_decay5e-4 ~ 1e-33D-CNN必须开2D可小一点Dropout0.3 ~ 0.5加在全连接层前epochs80 ~ 150配合早停防过拟合训练时的优化器我固定用Adam初始学习率1e-3采用余弦退火让学习率在训练后期自然降到接近零。这个组合对CNN高光谱分类来说基本是“后悔药”级别的稳定项很少需要额外手动调学习率。4.3 类别不平衡与加权损失高光谱数据集的类别分布天然不平衡植被和城区像素动辄上万稀有地物可能只有几百个样本。直接训练会让模型偏向多数类稀有类别的AA平均精度被拉低。解决方式有两个一是给损失函数加类别权重二是在采样器里做类别重采样。工程上加权交叉熵实现最直接。import torch import torch.nn as nn def build_class_weights(train_labels, n_classes): 按训练集类别样本数倒数计算权重 counts np.bincount(train_labels, minlengthn_classes).astype(np.float32) weights 1.0 / (counts 1e-6) weights weights / weights.sum() * n_classes # 归一化到均值1 return torch.tensor(weights, dtypetorch.float32) criterion nn.CrossEntropyLoss(weightbuild_class_weights(train_labels, n_classes))权重归一化到均值1的含义是多数类和少数类的总体损失贡献大致平衡模型不会被某个占绝对数量优势的类别绑架。这里有个细节权重只按训练集统计不能引入验证集或测试集信息。如果类别不平衡特别严重还可以把加权交叉熵换Focal Loss做进阶实验但默认场景下加权CE已经能稳住底线。5. 避坑与排查五个高光谱分类项目反复出现的翻车现场5.1 训练集99%验证集60%空间信息泄漏现象模型在训练集上准确率接近完美验证集和测试集却明显偏低且训练曲线一路向下。原因样本划分方式不对。随机split会把同一地块的相邻像素同时分进训练集和验证集模型学到的“地物规律”实际上是“像素间平滑性”测试时遇到不相邻的区域就失效。解决改用按区域分块划分保证同一连通区域的像素只出现在一个集合中。第4章的split_by_region就是为此准备的。检查时把训练集和验证集的地物位置画在两幅图上如果两幅图中同一区域颜色大量重叠说明划分无效。5.2 3D-CNN过拟合严重训练集100%验证集75%现象3D-CNN的训练损失一路降到接近0验证集指标远低于训练集出现明显过拟合。原因3D卷积核参数量比2D-CNN大一个量级高光谱样本量又小模型自由度太高把训练样本的噪声细节也背下来了。解决压缩全连接层维度从我第一次试验时的512降到64Dropout从0.3提高到0.5weight_decay开到1e-3。改完后验证集OA一般能回升5-10个点。如果还压不住优先减少conv3d通道数而不是砍层数因为光谱维的特征抽象少了层数会直接掉精度。5.3 损失震荡不收敛lr背锅现象训练损失曲线像锯齿一样反复波动验证集指标时高时低训练三五十轮仍没有平缓趋势。原因学习率太大或者少数类别样本在mini-batch里忽多忽少导致梯度方向不稳定。解决先把初始学习率降到3e-4做快速验证如果曲线平滑了再逐步调回1e-3配合余弦退火。同时确认采样器是否做了类别重采样如果没有优先在Dataloader里加WeightedRandomSampler这比单纯调lr更治本。5.4 预测图满是椒盐噪声分类结果碎片化现象全图推理出来的分类结果图上单个像素的颜色不断跳动地物边界像被打碎的彩色玻璃。原因推理时对每个测试像素单独取其patch网络只看局部窗口没有利用空间先验边界和异质区域容易被误分。解决推理时同样使用patch-based预测并对相邻patch重叠区域做多数投票。常见做法是把测试图像的每个像素逐点取出patch输入模型得到逐像素概率后再用一个3x3或5x5的窗口做模式滤波预测图会平滑很多。5.5 同一套代码换数据集精度骤降归一化顺序错了现象在数据集A上跑出94%的基线原封不动换到数据集B直接降到80%以下排除模型问题后仍未改善。原因代码里用了整幅图像的均值方差做归一化或者PCA在全部数据上fit。换数据集和标签分布变化后训练集和验证集的统计量被“全局信息”污染模型学到的特征尺度不对。解决严格先划分训练集再fit归一化器和PCA并在pipeline里把scaler和pca保存下来推理时直接用保存的模型transform而不是重新加载整幅图像重新计算。6. 评估指标与工程化技巧从混淆矩阵到可复现实验的最后一里路6.1 指标不能只看OAAA和Kappa必须一起报高光谱分类论文和项目里最常用的三个指标是总体精度OA、平均精度AA和Kappa系数。OA容易被多数类带偏AA把每个类别的精度等权平均能反映稀有类别的表现Kappa则衡量分类结果和随机标注的一致性。三者结合才是一个可信的基线。from sklearn.metrics import confusion_matrix def evaluate_metrics(y_true, y_pred, n_classes): cm confusion_matrix(y_true, y_pred, labelsrange(n_classes)) oa np.trace(cm) / cm.sum() aa np.mean(cm.diagonal() / (cm.sum(axis1) 1e-6)) pe (cm.sum(axis0) * cm.sum(axis1)).sum() / (cm.sum() ** 2) kappa (oa - pe) / (1 - pe) return oa, aa, kappa6.2 实验要可复现随机种子必须钉死高光谱分类实验随机性很大数据划分、Dataloader打乱、权重初始化都会影响结果。我习惯在训练脚本开头把所有能固定的随机源全部锁定每次跑完把配置和指标记录成一份文本方便后面回溯。import random import torch import numpy as np def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)6.3 从实验到交付的特征可视化模型跑稳之后用t-SNE把网络最后一层特征降到二维可视化能直观判断类别分得开不开。这一步在项目汇报时非常加分也能帮你发现那些OA高但特征重叠严重的隐患类别。这份高光谱分类工程包含从数据读取、波段筛选、PCA降维、模型训练到指标评估的完整代码下载后改一下数据路径就能直接跑通适合作为后续实验的起点。从那以后我每次跑高光谱分类实验都会强制走一遍“先检查划分、再确认归一化顺序、最后锁定随机种子”的三步流程再没出现过精度虚高的情况。希望帮到你。本文还有配套的精品资源点击获取