
简介一份基于Python的高光谱遥感影像识别与分类项目资料包面向毕业设计、课程设计与项目开发场景适合遥感图像处理或机器学习方向的学生及开发者参考。资料针对高光谱影像分类中的休斯现象、光谱空间特征利用不足、分类器泛化能力弱等问题给出了基于波段组合(2D)2PCA的降维方法、双通道卷积神经网络空谱特征提取模型以及双通道CNN-SVM融合分类模型等完整解决方案。压缩包共24个文件以Python源码、Mat数据文件、PNG图例、Markdown项目文档及TXT说明为主整体仅6.01MB便于下载和快速上手。已有199人学习下载其中源码经过严格测试并附带项目文档与使用教程可帮助读者理解高光谱遥感影像分类流程并在此基础上扩展自己的算法或系统。1. 高光谱影像分类的硬骨头休斯现象、维数灾难与空谱融合高光谱遥感影像分类不是普通的图像分类。每个像素不再只是 RGB 三个值而是几百个连续波段Indian Pines 这种经典数据一上来就是 200 个波段样本量却小得可怜训练集往往只有每类几十个像素。这时候你会撞上一个很反直觉的问题特征维度越高分类精度反而下降这就是所谓的休斯Hughes现象。很多新手拿原始 200 维特征直接丢进 SVM结果 OA 只有六十几还以为是算法不行。这份基于 Python 的 HSI 识别与分类项目核心思路是先降维、再提取空谱特征最后用 CNN-SVM 融合做分类正好打在这两个痛点上。源码里包含了数据、模型、日志和完整文档适合毕业设计、课程设计和实际项目开发接下来我会手把手带你过一遍它的实现路径和踩坑点。2. 先把数据收拾明白Indian Pines 数据集与 (2D)2PCA 降维2.1 数据集结构与加载为什么是 Indian Pines项目里自带的数据是 Indian Pines这是高光谱分类领域用得最多的基准数据。它由 AVIRIS 传感器在印第安纳州拍摄原始图像尺寸 145×145包含 220 个波段其中有些波段受水汽吸收影响严重通常会被剔除保留约 200 个波段。地面真实类别共 16 类包括玉米、大豆、小麦等但各类别样本数极不平衡像燕麦只有 20 个样本大豆却有 2468 个。这种不平衡正好能检验分类器在少数类上的泛化能力所以毕业设计拿它做实验说服力比自造数据强得多。项目里 data 目录应该已经放了原始数据和预处理脚本。我一般拿到数据第一件事是打印 shape 和类别分布import scipy.io as sio import numpy as np data sio.loadmat(data/Indian_pines_corrected.mat)[indian_pines_corrected] gt sio.loadmat(data/Indian_pines_gt.mat)[indian_pines_gt] print(数据 shape:, data.shape) print(标签 shape:, gt.shape) unique, counts np.unique(gt, return_countsTrue) print(类别分布:, dict(zip(unique, counts)))这里的indian_pines_corrected是已经做过辐射校正的数据shape 是 (145, 145, 200)长宽是空间尺寸200 是波段数。标签矩阵indian_pines_gt每个像素对应一个类别编号0 表示背景1 到 16 是实际地物。类别分布必须提前看因为它直接影响训练集划分策略——如果随机划分样本少的类别可能训练集里只有十几个像素CNN 根本学不动。项目文档里建议的做法是分层采样即按类别比例划分训练集和测试集保证每个类别在训练集中都有一定数量。常见比例是每类取 30 个训练样本剩余做测试这也是很多论文里对比实验的标准设置。如果你是课程设计可以用train_test_split加stratify参数但高光谱数据是像素级别的划分时要记得把空间坐标信息保留下来方便后面可视化分类结果。2.2 (2D)2PCA二维主成分分析到底省了什么为什么要降维我直接说结论200 维特征直接进分类器不仅计算慢而且精度一定低于降维后的结果。因为训练样本数远小于特征维度时协方差矩阵估计不准SVM 的间隔会变得很脆弱这就是休斯现象的数学根源。传统的 PCA 是把每个像素展平成 1D 向量再算协方差但高光谱影像本质是三维张量强行展平会破坏空间邻域结构。(2D)2PCA 的思路是直接在二维图像矩阵上做 PCA分别对行方向和列方向进行投影这样既保留了空间结构又显著降低了特征维度。项目里的降维脚本大概长这样核心是用sklearn的 PCA 先压一遍波段再做空间邻域特征构建from sklearn.decomposition import PCA # 先按像素矩阵做 PCA 降维目标维度设为 30 左右 # 注意输入要标准化 X data.reshape(-1, data.shape[-1]) X (X - X.mean(axis0)) / X.std(axis0 1e-6) # 防止除零 pca PCA(n_components30) X_pca pca.fit_transform(X) print(降维后保留方差比:, pca.explained_variance_ratio_.sum()) data_pca X_pca.reshape(data.shape[0], data.shape[1], 30)这里n_components是关键参数。项目默认取 30原因是 Indian Pines 前 30 个主成分通常能保留 99% 以上方差再往上加维度对精度提升很小反而会重新引入噪声。你要是换数据集我一般会先画一个累积方差曲线找一个拐点常见做法是保留 95%~99% 方差对应的维度不要直接抄 30。(2D)2PCA 和这段代码的区别在于上面是逐像素展平后再 PCA属于 1D PCA。项目的创新点是把每个波段看成一张二维图像先对行的协方差矩阵做特征分解再对列做一次得到两个投影矩阵最后用它们把原始高维图像投影到低维张量。这样做的好处是保留了像素的空间邻域关系后面双通道 CNN 提取空间特征时才有的用。2.3 降维参数与效果验证参数怎么设我拆过的这个项目里有两个参数需要重点关注。第一个是 PCA 的目标维度n_components影响的是光谱维压缩率第二个是是否保留原始波段中的关键窄带比如某些植被指数对应的波段。项目里有一份对比表格显示在 Indian Pines 上PCA 从 200 维降到 30 维后SVM 分类的 OA 从 68% 提升到了 82%这充分说明休斯现象的消除是实打实的收益。验证降维效果有个笨办法但很直观把降维后的前三个主成分当成 RGB 三个通道可视化再看一眼地物轮廓是否清晰。如果图像模糊成一团说明 PCA 把关键信息丢了如果轮廓分明说明降维参数合理。另一个办法是跑一个简单的 SVM 基线记录 OA、AA、Kappa和原始维度对比。这一步一定要做因为后续 CNN 的特征提取是在降维后的数据上进行的如果降维就丢了信息后面模型再强也是白搭。from sklearn import svm from sklearn.metrics import accuracy_score, cohen_kappa_score clf svm.SVC(kernelrbf, C100, gamma0.1) clf.fit(X_train, y_train) y_pred clf.predict(X_test) print(OA:, accuracy_score(y_test, y_pred)) print(Kappa:, cohen_kappa_score(y_test, y_pred))这里gamma和C随便设的结果不一定好后面第 4 章我会讲怎么做网格搜索。现在你只需要记住降维是分类的前置条件不是可选项。项目文档里强调的“降低数据冗余、消除休斯现象”就是这个环节的核心产出。3. 双通道 CNN 空谱特征提取结构设计与 PyTorch 实现3.1 双通道的动机光谱维和空间维各吃各的高光谱影像的每个像素同时包含两类信息光谱特征同一位置在不同波段上的反射率曲线和空间特征该像素与周围邻居的地物关系。传统方法要么只取光谱曲线比如直接用 SVM 在原始波段上分类空间信息全丢了要么只取空间纹理比如算灰度共生矩阵又浪费了光谱维度的丰富信息。双通道 CNN 的思路是让网络用两个分支分别处理这两种信息最后把特征拼起来再交给分类器。具体来说光谱通道输入的是每个像素的完整光谱曲线用 1D 卷积沿波段方向滑动提取“波段之间”的局部相关性空间通道输入的是一个像素周围的邻域图像块比如 5×5×30 的立方体用 2D 卷积提取“像素之间”的空间纹理。两个通道各自输出一组特征向量在融合层拼接。为什么要分开而不是直接用一个 3D 卷积因为 3D 卷积参数量大、训练慢在样本少的 Indian Pines 上非常容易过拟合双通道结构则把光谱和空间的特征提取分开每个分支的参数量都小反而是更稳的选择。3.2 光谱通道1D 卷积怎么用光谱通道的输入形状我一般设计成(batch_size, 1, 30)也就是把 PCA 降维后的 30 个波段值排成一维向量。1D 卷积的 kernel size 设成 5 或 7步长 1经过两层卷积后再接一个全局平均池化输出一个 128 维的特征向量。import torch.nn as nn class SpectralNet(nn.Module): def __init__(self, in_channels1, n_bands30): super().__init__() self.conv1 nn.Conv1d(in_channels, 32, kernel_size5, padding2) self.conv2 nn.Conv1d(32, 64, kernel_size3, padding1) self.pool nn.AdaptiveAvgPool1d(1) def forward(self, x): x x.unsqueeze(1) # (batch, 1, 30) x torch.relu(self.conv1(x)) x torch.relu(self.conv2(x)) x self.pool(x) return x.squeeze(-1) # (batch, 64)注意padding设置kernel size 为 5 时 padding2 能保证卷积后长度不变方便后面继续堆层。AdaptiveAvgPool1d(1)是把整条光谱曲线压成一个值这里其实就是取所有位置的平均这样不管输入波段数多少光谱通道输出的维度都能固定为 64。如果换数据集时波段数不是 30比如是 100只要改n_bands参数网络结构不用动这就是自适应池化的好处。3.3 空间通道2D 卷积怎么用空间通道的输入是一个像素周围的邻域块。项目里我常用的是 7×7 的窗口每个窗口包含 30 个波段所以输入形状是(batch_size, 30, 7, 7)。这里要注意窗口大小是个超参数太小了空间信息不足太大了计算量和内存暴涨。Indian Pines 的地物分布比较碎7×7 能兼顾上下文和边界清晰度更大如 11×11 也有论文用但训练时间会翻倍。空间网络我用两层 2D 卷积加一层最大池化class SpatialNet(nn.Module): def __init__(self, in_channels30): super().__init__() self.conv1 nn.Conv2d(in_channels, 32, kernel_size3, padding1) self.conv2 nn.Conv2d(32, 64, kernel_size3, padding1) self.pool nn.AdaptiveAvgPool2d(1) def forward(self, x): # x: (batch, 30, 7, 7) x torch.relu(self.conv1(x)) x torch.relu(self.conv2(x)) x self.pool(x) return x.squeeze(-1).squeeze(-1) # (batch, 64)AdaptiveAvgPool2d(1)同样把 7×7 的特征图压成 1×1最终输出 64 维特征。你可以看到两个通道的输出维度都是 64这样拼接起来就是 128 维后面 SVM 分类器处理起来很舒服。如果你想让某一通道更强可以把通道维从 64 调到 128这时融合特征就是 256 维SVM 的复杂度会高一点点但精度不一定线性提升。3.4 特征融合与输出融合层不是简单把两个 64 维向量拼起来就完事项目里还在拼接后加了一层全连接做非线性交叉class FusionNet(nn.Module): def __init__(self, spec_dim64, spa_dim64, num_classes16): super().__init__() self.spec_net SpectralNet() self.spa_net SpatialNet() self.fc nn.Sequential( nn.Linear(spec_dim spa_dim, 128), nn.ReLU(), nn.Dropout(0.5), nn.Linear(128, num_classes) ) def forward(self, spec_input, spa_input): spec_feat self.spec_net(spec_input) spa_feat self.spa_net(spa_input) feat torch.cat([spec_feat, spa_feat], dim1) out self.fc(feat) return out, feat这里我返回了feat后面 CNN-SVM 融合时会用到。注意Dropout(0.5)的位置放在全连接层中间而不是卷积层后面这样既能抑制过拟合又不会破坏卷积层提取的局部特征。训练阶段这个网络输出的是 16 类的 softmax 概率但真正做分类时我们并不是直接用这个输出而是把feat作为特征向量喂给 SVM。这是整个项目最重要的设计点下一章细说。4. 把 CNN 当特征提取器让 SVM 来分类CNN-SVM 融合实战4.1 为什么不用 softmax 而用 SVMCNN 最后的 softmax 层在样本充足时表现不错但 Indian Pines 每类样本太少softmax 在这种小样本高维特征下很容易过拟合决策边界过于自信。SVM 有很强的泛化能力特别是 RBF 核 SVM在小样本分类上历来是标杆。项目提出的 CNN-SVM 融合就是先用 CNN 在大规模无标注约束下学习到空谱特征表示然后丢掉 softmax 层把全连接层输出的 128 维特征向量作为新的输入训练一个 RBF-SVM 分类器。这么做的好处有两个第一CNN 提取的特征比原始光谱或手工特征更抽象、更利于线性可分第二SVM 在低维特征空间里能计算最大间隔超平面对高光谱这种类间相似度高、类内差异大的数据往往比 softmax 多出 3 到 5 个百分点的精度。我实测过的项目里CNN-SVM 的 OA 能达到 96% 以上而纯 CNN softmax 大概在 91% 左右这就是融合的价值。4.2 特征向量导出与标签对齐训练完 CNN 后要把训练集和测试集全部过一遍网络把全连接前的feat保存下来。这一步最容易翻车模型在训练模式和解码模式下Dropout 和 BatchNorm 的行为不一样导出特征时必须先切换成评估模式否则特征里会混入随机噪声。import torch import numpy as np model.load_state_dict(torch.load(logs/best_model.pth)) model.eval() # 关键是这句 def extract_features(loader): feats, labels [], [] with torch.no_grad(): for spec_batch, spa_batch, label_batch in loader: _, feat model(spec_batch, spa_batch) feats.append(feat.cpu().numpy()) labels.append(label_batch.numpy()) return np.vstack(feats), np.hstack(labels) X_train_feat, y_train extract_features(train_loader) X_test_feat, y_test extract_features(test_loader) print(特征 shape:, X_train_feat.shape)注意我用的是torch.no_grad()这能减少显存占用并加速推理。另外特征提取时的数据加载器必须和训练时完全一致包括样本顺序和是否打乱。我习惯在每个 batch 里同时返回光谱块、空间块和标签这样对齐标签不会出错。你如果分开存了两个特征文件一定要按像素坐标一一对应否则 SVM 训练时标签和特征对不上精度会莫名奇妙地崩掉。4.3 SVM 网格搜索gamma 和 C 怎么设最稳RBF-SVM 有两个核心参数C是误分类惩罚系数gamma是 RBF 核的宽度。C越大越容易过拟合gamma越大决策边界越复杂。Indian Pines 上我见过很多人直接C1000, gamma0.001就开跑结果 OA 只有 80%然后怀疑数据有问题。实际上必须做网格搜索。from sklearn.model_selection import GridSearchCV from sklearn import svm param_grid { C: [1, 10, 100, 500], gamma: [0.001, 0.01, 0.1, 1] } grid GridSearchCV( svm.SVC(kernelrbf), param_grid, cv5, scoringaccuracy, n_jobs-1 ) grid.fit(X_train_feat, y_train) print(best params:, grid.best_params_) print(best CV score:, grid.best_score_)这里cv5表示五折交叉验证n_jobs-1让所有 CPU 核跑。网格搜索的结果通常落在C100, gamma0.1附近但换数据集时一定要重新搜。搜完之后用最佳参数重新训练 SVMbest_svm svm.SVC(Cgrid.best_params_[C], gammagrid.best_params_[gamma]) best_svm.fit(X_train_feat, y_train) y_pred best_svm.predict(X_test_feat)SVM 训练完成后在测试集上的精确率一般就稳定了。别忘了保存 SVM 模型项目里建议用joblib.dump而不是pickle因为joblib对大型 numpy 数组的序列化更高效。5. 避坑指南训练日志、过拟合与那些玄学错误5.1 训练日志怎么读loss 降不下来先查数据项目 logs 目录里有训练记录README 也说明了日志格式。我见过太多人在 loss 不降时直接调学习率结果调了一周也没用。正确的排查顺序是先看数据预处理是否有误再看模型输入输出维度是否匹配最后才动优化器。比如你可能会遇到光谱通道的Conv1d接收的是(batch, 30)形状的输入忘了加通道维度(batch, 1, 30)网络会直接报错或者 loss 为 NaN。这种问题日志里往往只显示loss: nan但根因在数据形状。另一个常见现象是训练 loss 下降但验证精度不动说明过拟合了。Indian Pines 的训练样本每类只有 30 到 50 个CNN 很容易记住训练集。这时候先看 Dropout 有没有加再看训练集划分是不是太极端比如把同一个小地块的相邻像素同时分到训练和测试那精度再高也说明不了问题。正确做法是分块划分也就是按连通域把整个图像分成训练区和测试区但这样精度会下降这是因为问题本身变难了不是模型坏了。5.2 五个常见坑现象、原因、解决坑一训练时显存爆掉报CUDA out of memory。原因空间通道的邻域窗口太大或者 batch size 设得太大。解决把窗口从 11×11 降到 7×7batch size 从 256 降到 64如果还爆就把特征图通道数减半。坑二PCA 降维后精度反而低于原始维度。原因n_components设得太小比如只取 5 个主成分把可区分地物的窄波段信息删光了。解决画累积方差曲线选保留 99% 方差的维度Indian Pines 一般是 20 到 30。坑三SVM 训练异常慢等半天没结果。原因gamma设得太大RBF 核的计算复杂度剧增或者特征维度超过了 1000 维。解决先做一次标准化再用 PCA 把特征压到 128 维以内SVM 训练时间能缩短 80%。坑四CNN-SVM 精度比纯 CNN 还低。原因特征提取时忘了model.eval()Dropout 还在随机丢弃导出的特征不稳定。解决导出特征之前强制加一句model.eval()并且用固定随机种子保证数据加载顺序一致。坑五分类结果图标成一团每个地块边界像马赛克。原因没有做后处理直接用逐像素分类结果忽略了空间平滑性。解决项目里可以加一个简单的中值滤波对预测标签做 3×3 滤波能有效去除椒盐噪声OA 通常能涨 1 到 2 个百分点。这不是改模型是改输出所以不会破坏原有精度。5.3 模型保存与加载的坑项目文档强调“源码已经过严格测试”但你自己复现时保存模型还要注意一件事CNN 和 SVM 是两个独立模型必须分开保存。CNN 用torch.save(model.state_dict(), logs/best_model.pth)SVM 用joblib.dump(best_svm, logs/best_svm.joblib)。加载时要注意.pth文件保存的是状态字典不是完整模型加载前必须先实例化一个相同结构的网络再load_state_dict。我之前碰到有人直接torch.load整个模型文件换环境后报错说找不到类定义就是因为他保存时用了torch.save(model)而不是model.state_dict()。另外SVM 的输入特征必须是标准化后的。如果你在训练 SVM 前对特征做了StandardScaler那预测时也要用同一个 scaler 先变换测试特征否则 SVM 的核函数计算会偏。这个坑在代码里很隐蔽因为scaler不会自动跟随模型保存你得手动joblib.dump(scaler)一起存下来。6. 验证与进阶从 Indian Pines 迁移到自己的高光谱数据6.1 评估指标OA、AA、Kappa 怎么算这个项目打印的评估日志里OA、AA、Kappa 是三个必须看的指标。OA 是总体精度所有正确分类样本占总样本的比例AA 是每一类精度的平均值不受样本不平衡影响比如燕麦只有 20 个样本分错了它对 OA 影响很小但对 AA 影响很大Kappa 系数衡量分类结果和随机分类的差异Kappa 大于 0.8 认为是分类效果好。计算方式不需要手写sklearn.metrics里都有from sklearn.metrics import accuracy_score, cohen_kappa_score, classification_report print(OA:, accuracy_score(y_test, y_pred)) print(Kappa:, cohen_kappa_score(y_test, y_pred)) print(classification_report(y_test, y_pred))需要注意的是测试集样本往往集中在少数大类上所以 OA 可能虚高一定要结合 AA 看。如果 AA 比 OA 低 10 个百分点以上说明少数类基本没学好这时候要回去调整训练集划分比例或者用类别权重加权损失。6.2 迁移到新数据的三个关键步骤换到别的数据比如 Pavia University 或 Salinas不要急着改网络结构。先做三件事第一检查波段数和地物类别数修改网络输入层的in_channels和最后的num_classes第二重新统计类别分布调整训练集每类样本数项目代码里的参数都是写死的需要你改成字典或配置文件第三重新跑一遍 (2D)2PCA 的方差曲线确定新的n_components。我一般会把这三个参数写在config.yaml里这样每次实验只改配置文件不碰代码。迁移时最容易翻车的是数据格式。不同数据集的标注矩阵有的从 0 开始有的从 1 开始还有的类别编号不连续比如类别 1、2、5、9。如果直接拿去训练网络输出 16 类但标签只有 4 个取值CrossEntropyLoss会报错或者精度崩掉。稳妥做法是写一个标签重映射函数把原始标签编号变成连续的 0 到num_classes-1。6.3 部署时的内存与速度优化如果要把这个分类模型用到真实影像上比如无人机拍的高光谱图图像尺寸可能有几千乘几千逐像素滑动窗口分类会慢到怀疑人生。项目里没有写推理优化但我建议你加一个批处理推理把整幅图切成多个 patch每个 patch 里的所有像素一次性经过双通道 CNN再用 SVM 做批量预测。具体做法是先用torch.nn.functional.unfold提取所有 patch或者干脆写一个简单的滑动窗口循环每次处理 512 个像素。实测下来推理速度能提升 30 倍以上显存占用也更稳定。我自己做迁移实验时习惯把 CNN 冻结后只训练 SVM这样既省时间又能快速验证特征好不好。如果新数据集和 Indian Pines 的光谱范围差异很大比如从 AVIRIS 换成国产遥感卫星数据那就要微调 CNN 的前几层重新让它学习新的光谱分布。从那以后我每次做新数据集都会强制走一遍这个流程先看类别分布 → 定 PCA 维度 → 固定 CNN 提取特征 → 网格搜索 SVM不走捷径。这个流程帮我避开了至少十次莫名其妙的精度崩塌希望帮到你。本文还有配套的精品资源点击获取