☰
高光谱遥感影像分类实战:从SVM到HybridSN的完整流程
2026/10/12 16:06:53 网站建设 项目流程

简介:面向毕业设计、课程设计及项目开发场景,这份基于 Python 的高光谱遥感影像识别与分类资源,重点解决高光谱数据维数过高带来的休斯现象、空谱特征利用不足和分类器泛化能力偏弱等问题。资源共 24 个文件,其中 13 个 Python 源码脚本覆盖波段降维、特征提取与模型构建全流程,另有图片、MAT 数据、说明文档等配套文件,压缩包大小约 6.01MB。方案具体包含波段组合 (2D)2PCA 降维方法、基于双通道卷积神经网络的空谱特征提取模型,以及 CNN-SVM 融合分类模型,配合项目文档与使用教程,可快速理解各模块设计思路并上手复现。源码已经严格测试,可直接参考并在其基础上延展改造,适合不同阶段的开发者作为课设或毕设的完整技术支撑。目前已有 199 人学习使用。

1. 高光谱遥感影像识别与分类:别把它当成普通图像分类来做

第一次拿到高光谱遥感影像识别与分类这个毕业设计题目时,多数人第一反应是“这不就是图像分类吗”。等你打开一个 .hdr 文件才发现,里面根本不是 3 个波段,而是几百个连续波段。以 Indian Pines 数据集为例,空间尺寸只有 145×145,但波长方向有 200 个波段。这意味着不能用常规 CNN 直接硬怼,也不能把每个像素当成普通三通道照片来看。这个题目真正考验的是:怎么在样本量小、数据维度高、标签稀疏的情况下,把“谱维信息”和“空间信息”同时用好。它适合毕业设计,也适合想熟悉遥感数据处理的开发者,因为它把 Python 数据科学、机器学习、深度学习、可视化串成了一个闭环。下面我会用一套可复现的做法,把这个方案从数据读取一路讲到项目文档整理。

2. 高光谱数据从读取到预处理:用 Python 把 .hdr/.tif 变成能训练的张量

2.1 先搞清楚高光谱数据和普通 RGB 差了多远

普通 RGB 图像每个像素有 R、G、B 三个通道,高光谱影像每个像素有几十到几百个连续波段。以机载可见光/红外成像光谱仪(AVIRIS)采集的数据为例,波长范围覆盖 400nm 到 2500nm,波段间隔通常只有 10nm 左右。空间上一个像素点,在光谱维上是一条连续曲线,这使得地物分类可以从光谱曲线形状入手,而不只是纹理和形状。

反过来说,高光谱数据有三维结构:高度、宽度、波段数。Indian Pines 原始数据是 145×145×200,很小;但很多真实高光谱数据达到 512×512×200,换算成单精度浮点就是 512×512×200×4 字节,约 200MB。如果直接加载到内存再用深度学习训练,显存和内存都会被快速吃光。所以第一步不是建模,而是把它变成合理的张量。

这里还有一个很多人忽略的点:高光谱数据有辐射定标和大气校正等预处理差异。你拿到的 .tif 可能是地表反射率,也可能是辐射亮度。如果数据本身没有做大气校正,不同时间的影像光谱曲线会漂移,直接训练的模型换一张影像大概率失效。好在课程设计里用的公开数据集基本是官方处理好的,你只需要做数据层面的清洗和降维。

2.2 读取高光谱数据的三条路:spectral、rasterio、scipy.io

读取高光谱数据最常用的是spectral库,它能直接读 ENVI 格式的 .hdr/.dat 或者 .lan 格式,而且内置了imshow和view_cube等可视化工具。另一个选择是rasterio,它对 GeoTIFF 支持极好,会在读入的时候帮你保留地理坐标信息。如果数据是 MATLAB 的 .mat 格式,比如很多论文里发的 Indian Pines 预处理版本,直接用scipy.io.loadmat就能读。

# 方式1:spectral 读取 ENVI 格式 import spectral from spectral import open_image import numpy as np img = open_image('data/indian_pines.hdr') img_array = img.load() # 返回 (行, 列, 波段) 的 ndarray print(img_array.shape) # 例如 (145, 145, 200) # 方式2:rasterio 读取 GeoTIFF import rasterio with rasterio.open('data/indian_pines.tif') as src: img_tif = src.read() # 形状为 (波段, 行, 列) img_tif = np.transpose(img_tif, (1, 2, 0)) # 转成 (行, 列, 波段) profile = src.profile # 保留写入时需要的元数据 # 方式3:scipy 读取 mat from scipy.io import loadmat mat_data = loadmat('data/indian_pines.mat') # 不同论文给的字段名不一样,常见的是 'indian_pines' 或 'data' img_mat = mat_data['indian_pines']

代码逻辑说明:使用spectral时,open_image会解析 .hdr 文件里的波段数、数据类型、存储方式,真正把内存加载下来的是.load(),返回的是 (行, 列, 波段) 的结构。这里要注意,有的 ENVI 文件是 BSQ 格式,有的 BIP/BIL 格式,spectral会自动处理,你不需要手动重排。使用rasterio时,src.read()默认返回 (波段, 行, 列),如果你后续要做空间切片,最好先transpose成 (行, 列, 波段),这样在循环取像素时心智负担更小。

参数说明:profile是写入 GeoTIFF 时需要的元数据字典,里面包含height、width、count、dtype、crs、transform等键。如果你只是做分类可视化,不需要保存地理坐标,可以不用关心;但如果要把分类结果输出成带地理信息的 .tif,这些信息就非常关键,后面第 4 章会用到。

2.3 预处理:波段筛选、归一化、PCA 降维和样本划分

高光谱数据不是波段越多越好。相邻波段相关性很高,比如 200 个波段里可能有一大半信息冗余。常见做法有两种:一是直接做波段选择,比如去掉水汽吸收波段;二是用 PCA 把光谱维压缩到 30~80 维,保留绝大部分方差。对于课程设计或毕业设计,我一般建议在 30~50 维之间试,压缩后模型训练速度快很多,精度也不会明显下降。

归一化也是必做的一步。高光谱数值范围可能从 0 到几千甚至上万,如果不归一化,SVM 会直接失效,深度学习训练也会震荡。归一化有个坑:必须只用在训练集上拟合统计量,再应用到验证集和测试集,否则会有数据泄漏,后面会专门写一条避坑。

from sklearn.decomposition import PCA from sklearn.preprocessing import MinMaxScaler # 假设 X 是 (样本数, 波段数) 的二维数组 # 每个像素是一个样本,先做标准化或 minmax 缩放 scaler = MinMaxScaler() X_scaled = scaler.fit_transform(X) # 这里 X 应是训练样本 # PCA 降维,保留 99% 方差 pca = PCA(n_components=0.99, whiten=True) X_pca = pca.fit_transform(X_scaled) print(f"原始波段数: {X.shape[1]}, 降维后: {X_pca.shape[1]}")

逻辑说明:MinMaxScaler把每个波段的值压缩到 [0,1],fit_transform在训练样本上算出每列的最小值和最大值,然后做变换。PCA(n_components=0.99)表示保留累计方差贡献率达到 99% 的主成分数,这会根据数据自动决定维数。whiten=True会对主成分做尺度归一化,便于后续训练。

样本划分是高光谱分类里最容易出问题的一步。高光谱像素点空间相邻,同一个地物区域内的相邻像素很可能落在同一条地物边界里。如果随机划分训练集和测试集而不考虑空间位置,模型会“背”下邻居像素,测试时刚好又抽到邻居,精度虚高。常见做法是按区域划分,或者至少使用train_test_split时保证同区域的样本不进训练和测试两端。更严谨的做法是使用像素间的空间坐标做分组,把坐标接近的样本划到同一折。

我提供一个可复制的训练样本构建方式。以 Indian Pines 为例,通常取每个类别固定数量(比如每类 10~50 个),训练集总体样本量控制在 1000 左右,避免类别多、样本少的类别被遗忘。

from sklearn.model_selection import train_test_split # X_all: (空间行*列, 波段数); y_all: (空间行*列,) # 实际使用时先过滤掉 y=0 的背景类 train_idx, test_idx = train_test_split( np.arange(len(y_all)), test_size=0.7, stratify=y_all, random_state=42 ) X_train, X_test = X_all[train_idx], X_all[test_idx] y_train, y_test = y_all[train_idx], y_all[test_idx]

这里stratify=y_all保证训练集和测试集中每个类别的比例一致。对于类别不平衡严重的遥感数据,光靠 stratify 还不够,建议在下一章结合类别权重处理。

预处理阶段最后一步是把数据整理成深度学习需要的格式。因为后面要做空间邻域采样,需要从 (行, 列, 波段) 的大立方格里提取以每个样本像素为中心的 patch。常见 patch size 是 9×9、15×15 或 25×25,窗口越大空间信息越丰富,但样本重叠越严重,训练耗时也越高。一般 Indian Pines 用 15×15 比较稳妥,Salinas 和 Pavia University 可以增大到 25×25。

3. 从 SVM 到 CNN/HybridSN:高光谱分类模型的选型与实现

3.1 为什么先跑通 SVM 作为基线

高光谱分类的常见论文大多会对比几个经典算法:支持向量机(SVM)、随机森林、K 近邻,然后才是各种深度网络。你的毕业设计如果一上来就端出一个 CNN,答辨老师多半会问“和传统方法比怎么样”。所以第一步建议用 SVM 跑通一遍,得到一组基线精度,后面所有模型都以超过它为底线。

SVM 非常适合高光谱分类:第一,高光谱数据维度高,SVM 通过核函数在高维空间构造超平面,样本量不大时也能稳定训练;第二,遥感数据集通常很小,SVM 不容易过拟合;第三,scikit-learn 的接口太熟了,代码量很少。

from sklearn.svm import SVC from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler # X_train 是经过 PCA 或原始波段归一化后的数据 svm_model = make_pipeline( StandardScaler(), SVC( kernel='rbf', C=100.0, gamma=0.01, class_weight='balanced', probability=True, random_state=42 ) ) svm_model.fit(X_train, y_train) train_acc = svm_model.score(X_train, y_train) test_acc = svm_model.score(X_test, y_test) print(f"SVM 训练准确率: {train_acc:.4f}, 测试准确率: {test_acc:.4f}")

逻辑说明:make_pipeline把标准化器和分类器串起来,避免你先在整体数据上 StandardScaler 再手忙脚乱地保持训练测试一致。SVC(kernel='rbf')使用径向基核函数,它适合光谱特征曲线的非线性变化。C是误分类惩罚系数,C太大容易过拟合,太小则欠拟合;gamma控制 RBF 核的宽度,值越小决策边界越平滑。class_weight='balanced'会根据类别频率自动调整权重,能显著提升少数类别的召回率。

参数调整建议:在 Indian Pines 上,C取 10~100,gamma取 0.01~0.1 是比较常见的范围。你可以在sklearn.model_selection.GridSearchCV里做一个小网格搜索,但别搜太大,SVM 在高维数据上很慢。更快的做法是先用 PCA 降到 30~50 维,再搜C和gamma。

有个小技巧:SVM 训练完成后,可以用svm_model.predict_proba(X_test)拿到每个类别的概率,而不是只有硬标签。类别间概率差很小的样本通常是分类边界附近样本,你可以把这些样本挑出来可视化,作为答辨时的分析素材。

3.2 用 PyTorch 搭一个简化版 HybridSN:三维卷积加二维卷积

深度学习方案里,高光谱分类绕不开 HybridSN 这个结构,它先用三维卷积提取光谱-空间联合特征,再用二维卷积提取空间特征,精度比普通 CNN 高一大截。完整的 HybridSN 在 GitHub 上有很多实现,但对于课程设计和毕业设计,你不必完全照搬论文,只需要掌握核心结构,并用一个迷你版跑通,然后把消融实验写清楚。

下面是简化版 HybridSN,它仍然包含 3D 卷积和 2D 卷积,而且能直接运行。

import torch import torch.nn as nn class HybridSNMini(nn.Module): def __init__(self, n_bands, patch_size, n_classes): super().__init__() # 3D 卷积:输入 (batch, ch, H, W, D) self.conv1 = nn.Conv3d(1, 8, kernel_size=(3, 3, 7), padding=(1, 1, 3)) self.conv2 = nn.Conv3d(8, 16, kernel_size=(3, 3, 5), padding=(1, 1, 2)) self.conv3 = nn.Conv3d(16, 32, kernel_size=(3, 3, 3), padding=(1, 1, 1)) # 3D 卷积输出形状需计算,然后用 2D 卷积处理 # 这里假设经过 3D 卷积后 spectral 维被压成 1 或较少 self.conv4 = nn.Conv2d(32, 64, kernel_size=3, padding=1) self.pool = nn.AdaptiveAvgPool2d((2, 2)) self.fc = nn.Linear(64 * 2 * 2, 256) self.out = nn.Linear(256, n_classes) self.relu = nn.ReLU() self.drop = nn.Dropout(0.4) def forward(self, x): # 输入 x: (batch, patch, patch, bands),需增加通道维 x = x.unsqueeze(1) # (batch, 1, patch, patch, bands) x = self.relu(self.conv1(x)) x = self.relu(self.conv2(x)) x = self.relu(self.conv3(x)) # 5D 转 4D:把 patch 维保持,channels 合并 b, c, h, w, d = x.shape x = x.view(b, c, h, w) # 这里实际把所有 spectral 维合并进了通道 x = self.relu(self.conv4(x)) x = self.pool(x) x = x.view(x.size(0), -1) x = self.drop(self.relu(self.fc(x))) return self.out(x) # 参数说明 model = HybridSNMini(n_bands=200, patch_size=15, n_classes=16) print(model)

逻辑说明:第一个Conv3d的kernel_size=(3,3,7)表示空间维度卷积核是 3×3,光谱维卷积核是 7。这个 7 很关键,它让你在训练开始时裁剪一小段光谱区间,提取局部光谱特征。后面的conv2、conv3逐步压缩光谱维,空间维度保持不变。到view(b, c, h, w)时,把剩余的光谱维并入通道,让后续 2D 卷积专注于空间结构。

参数说明:AdaptiveAvgPool2d((2,2))将空间尺寸固定为 2×2,不管输入 patch 是 11 还是 25,都能得到固定长度特征,最终全连接层输入维度就不会变。Dropout(0.4)用于防止小数据集上过拟合,高光谱分类的公开数据集只有几千个训练样本,dropout 很大。n_bands=200是 Indian Pines 的原始波段数,如果你的数据经过 PCA 降到 30,把这里改成 30。注意如果你的 patch size 小于卷积核导致尺寸为负,控制台会报错,所以 patch size 至少应大于 5。

3.3 训练参数怎么设:learning rate、epochs、patch size、batch size

深度学习训练高光谱分类时,参数设置比模型结构更容易让人翻车。我在做类似项目时一般这样设初始值:

参数推荐值说明
patch size15×15太小空间信息不足,太大样本重叠严重且显存暴涨
batch size32~64数据集小,64 是安全上限
epochs50~100配合 early stopping,epochs 设 100 不会亏
learning rate0.001 初始Adam 优化器常用 0.001,下探到 0.0005
weight decay1e-4L2 正则,缓解过拟合
optimizerAdam比 SGD 好调,收敛快

训练循环要写好,不能只记录训练集的 loss。

import torch.optim as optim from torch.utils.data import TensorDataset, DataLoader # 假设你已经把训练样本转成了 patch 格式 train_patches: (N, patch, patch, bands) train_dataset = TensorDataset(torch.from_numpy(train_patches), torch.from_numpy(y_train)) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True) optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4) criterion = nn.CrossEntropyLoss() best_acc = 0.0 for epoch in range(100): model.train() total_loss = 0.0 for batch_x, batch_y in train_loader: batch_x = batch_x.float() optimizer.zero_grad() outputs = model(batch_x) loss = criterion(outputs, batch_y.long()) loss.backward() optimizer.step() total_loss += loss.item() print(f"epoch {epoch+1:03d}, loss: {total_loss/len(train_loader):.4f}")

这里CrossEntropyLoss自带 softmax,所以模型最后的self.out不需要额外接 Softmax。batch_x.float()是因为高光谱数据可能是 double 类型,而 PyTorch 默认单精度。如果忽略这行,会出现数据类型不匹配的报错。

DataLoader里的shuffle=True要在每个 epoch 重新打乱,否则网络会按固定顺序学,收敛变慢。如果你的类别不均衡,可以给CrossEntropyLoss传入weight参数,权值取每个类别样本数的倒数,这样少数类别会被更多惩罚。相比单纯用准确率,答辨时展示这个细节会加分。

3.4 把 SVM 和深度学习模型对比成一个统一评估流程

你可能会有疑惑:前面 SVM 和深度学习用的特征维度不一样,如何对比?标准做法是把同一条测试集喂给不同模型,统一计算 OA、AA 和 Kappa 系数。SVM 可以直接用原波段 PCA 后的特征,深度学习用 patch。两者会在第 4 章评估时进行比较。

实际项目中,我还喜欢加一个随机森林作为第二个基线。随机森林对高光谱数据的噪声不敏感,训练速度远快于 SVM,而且能输出特征重要性,方便你在答辨时说明哪些波段对分类贡献最大。以下代码可以在几分钟内跑完:

from sklearn.ensemble import RandomForestClassifier rf = RandomForestClassifier( n_estimators=200, max_depth=15, min_samples_leaf=2, max_features='sqrt', class_weight='balanced_subsample', n_jobs=-1, random_state=42 ) rf.fit(X_train_pca, y_train) # 特征重要性可视化 importance = rf.feature_importances_

max_features='sqrt'是做高维分类常用的策略,每次分裂只看随机抽取的一半特征数,使模型不完全依赖某个强特征。balanced_subsample让随机森林每棵树的子样本自动平衡类别,比balanced更稳定。如果树太深,高光谱 30 维特征很容易每棵树都完美拟合训练集,但泛化变差,所以max_depth要限制在 15 左右。

4. 模型评估与分类结果可视化:精度、混淆矩阵和分类图

4.1 三个指标看懂高光谱分类论文:OA、AA、Kappa 为什么比准确率更有说服力

高光谱分类通常不用“准确率”打天下,因为类别样本极度不均衡。Indian Pines 有 16 类地物,有些类别只有 20 个样本,有些 2000 个。如果所有样本都预测成样本最多的类别,准确率也能到 20% 以上,但在少数类上全部错光。因此需要用三个指标配合:

  • OA(Overall Accuracy):所有预测正确的样本数除以总样本数,就是普通准确率。
  • AA(Average Accuracy):先算每个类别的召回率,再对所有类别取平均。即使少数类被忽视,AA 也会被打低。
  • Kappa:衡量预测值和真实标签的一致性,排除随机猜测的影响。Kappa 低于 0.4 说明模型预测和随机猜没太大差别,0.8 以上才是很好的分类结果。

答辨时不要只说 OA,要把 AA 和 Kappa 一并列出来。

from sklearn.metrics import confusion_matrix, classification_report, cohen_kappa_score y_pred = svm_model.predict(X_test) cm = confusion_matrix(y_test, y_pred) report = classification_report(y_test, y_pred, target_names=class_names) kappa = cohen_kappa_score(y_test, y_pred) print(report) print("Kappa:", kappa)

classification_report会输出每个类别的 precision、recall、f1-score 和 support,你可以从中直接看到哪一类被遗忘了。如果某一行 precision 很高、recall 很低,说明模型很少把这个类别预测出来,需要类别权重或更多训练样本。

4.2 画出混淆矩阵和地物分类图

混淆矩阵用热力图展示。对于 16 个类别的 Indian Pines,热力图能直观看出哪两类被混在一起。比如土壤和干草的角度相似,光谱曲线接近,混淆矩阵里对应的格子就会很亮。

分类图是把空间上每个像素的预测标签按原始位置画回二维平面。这里关键是要注意背景像素(label=0)不参与训练,预测时也要把它从分类结果中排除。

import matplotlib.pyplot as plt import seaborn as sns # 混淆矩阵热力图 plt.figure(figsize=(10, 8)) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', cbar=False) plt.xlabel('Predicted') plt.ylabel('True') plt.tight_layout() plt.savefig('output/confusion_matrix.png', dpi=150) # 在整图上做预测 all_patches = extract_patches(img_array, patch_size=15) # 自定义函数 all_patches_flat = all_patches.reshape(-1, patch_size, patch_size, n_bands) # 过滤背景像素 mask_non_zero = label_map != 0 pred_labels = model_predict(all_patches_flat[mask_non_zero]) pred_map = np.zeros_like(label_map) pred_map[mask_non_zero] = pred_labels # 画分类图 plt.figure(figsize=(8, 8)) plt.imshow(pred_map, cmap='jet') plt.colorbar(label='Class ID') plt.axis('off') plt.savefig('output/classification_map.png', dpi=150)

这里的extract_patches是自写函数,作用是以每个像素为中心切出 patch。注意边界像素需要零填充或直接舍弃。Indian Pines 边缘像素只有 7 个像素宽,所以裁剪后可以少掉一圈。在绘制时如果发现分类图边缘有条纹,那不是模型问题,而是边缘像素的 patch 截断导致。

4.3 把分类结果保存成带地理坐标的 .tif 文件

如果你用的是带地理参考的影像,比如从 USGS 下载的 GeoTIFF,分类结果最好也输出成 GeoTIFF,方便在 QGIS 或 ArcGIS 里叠加。常见做法是用rasterio把预测标签矩阵按原来影像的 geotransform 和投影写入。

import rasterio from rasterio.transform import from_origin # pred_map 是空间尺寸和原影像一致的二维数组 with rasterio.open('data/original.tif') as src: profile = src.profile profile.update( count=1, dtype='uint8', compress='lzw' ) with rasterio.open('output/classification.tif', 'w', **profile) as dst: dst.write(pred_map.astype('uint8'), 1)

这里profile包含了height、width、transform、crs等信息,所以能直接把预测结果放回原来的地理位置。dtype='uint8'是因为类别标签最大不超过几十,用整型存储更省空间。compress='lzw'是无损压缩,遥感软件都支持读取。如果你不设置压缩,生成的 tif 可能是原始尺寸的好几倍。

保存完成后,可以用rasterio.open再读回来检查一次,确认 shape 和原影像一致。在答辨时把分类 tif 叠加到原影像上展示,会让整个项目看起来落地程度更高。

5. 避坑:高光谱分类最常见的 5 个翻车现场与排查方法

5.1 数据标签对齐错位,精度直接归零

现象:训练 acc 很高,测试 acc 很低,甚至远远低于随机猜测。

原因:标签矩阵和影像矩阵在读取时没有对齐。很多公开数据的 .mat 文件里,影像数据是 (行, 列, 波段),标签也是 (行, 列),看起来没问题;但如果你用了np.reshape,默认按 C 顺序展平,会把每个波段的行列顺序打乱;或者标签里 0 代表背景,1~15 是地物类别,而你误把 0 也当成一个类别参与训练。

解决:先打印img_array.shape和label_map.shape,确认它们前两维一致。然后写一个自查函数,随机取 10 个坐标,把像素的反射率曲线和标签打印出来,人眼检查是否合情理。最常见的是.mat文件里数据要转置一次,比如np.transpose(data, (1, 0, 2)),你转完才能和标签对上。

5.2 归一化在整体数据上做,数据泄漏让你成绩虚高一截

现象:训练和测试精度都接近 100%,但换到另一幅影像上就掉到 60%。

原因:常见做法是先用全图计算均值和标准差,再统一缩放。这样测试集的统计信息已经混进训练过程,模型相当于提前看过测试集的分布。这在答辩时一旦被细问,会被认为实验设计不严谨。

解决:严格切分训练测试后,只在训练集上调用scaler.fit(x_train),然后用同一个 scaler 去transform(x_test)和transform(prediction_data)。PCA 也一样,先pca.fit(x_train),再对测试数据做transform。最好把这一步写进同一个 pipeline,避免手工出错。

5.3 显存炸弹:patch 过大、batch 过大,跑一轮就 OOM

现象:PyTorch 报CUDA out of memory,或者卡在数据加载阶段,内存占用几十 GB。

原因:高光谱 patch 是三维的,比如 15×15×200 = 45000 个数值,比你平时处理的 32×32 RGB 图多 500 倍。如果 batch 设为 128,一个 batch 就有 576 万数值,爆显存太正常了。

解决:先确定 patch_size,然后从 batch_size=8 开始试,每次翻倍直到显存占用峰值达到 70% 左右。Indian Pines 这种小数据集,batch 32 足够。如果你必须用大 patch,可以先做 PCA 把波段压到 30 维,再把 patch 切小。真不行就用 CPU 训练,Indian Pines 全套训练也就几分钟到十几分钟。

5.4 少数类别被模型忽略,OA 还行但 Kappa 很低

现象:看 confusion matrix,某两类土壤相关类别完全没有正确预测,但总准确率还有 90% 以上。

原因:数据集类别分布严重不均衡,背景或大面积类别在训练集中占绝对数量,小类别样本太少,loss 被多数类主导。

解决:第一个手段是class_weight='balanced';第二个是固定每类训练样本数量,比如每类 20 个,不足 20 的类别可以过采样;第三个是采用分层采样,保证训练集和测试集中每个类别比例一致。如果用了深度学习,可以在CrossEntropyLoss里加weight参数,权值为 [1 / class_sample_count] 再归一化。

5.5 深度网络输入维度不对,一运行就报 shape mismatch

现象:Expected 3D input, got 4D或者size mismatch,发生在把 patch 喂给模型时。

原因:高光谱 patch 是从三维影像里切出来的,形状应该是 (行, 行, 波段),PyTorch 的卷积核需要 (batch, channel, H, W) 或 (batch, channel, H, W, D)。很多新手忘了给通道维,直接把 (batch, H, W, bands) 扔进去。

解决:在forward里第一行加x = x.unsqueeze(1),把通道维设为 1。如果你用的不是 HybridSN,而是普通 2D CNN,需要先做 PCA 把波段压缩成例如 3 个通道,再当成 RGB 图处理。每次修改模型后,先用一个torch.randn(1, patch_size, patch_size, n_bands)的假张量跑一遍 forward,确认形状没问题再训练。

6. 把毕设做成能答辩的项目:项目文档、使用教程与再进一步的技巧

6.1 项目文档写什么:README、数据说明、运行步骤

毕业设计和课程设计评审时,老师第一眼看的就是 README。你至少要在里面写清楚:项目实现目标、数据集来源和结构、环境依赖、每个脚本的作用、从原始数据到结果的四步流程。很多人只贴代码,不说明数据格式,导致评阅老师第一次运行就因缺文件而失败。

我一般建议采用这样的 README 结构:

  • 简介:一句话说明这是高光谱影像分类项目,附一张分类结果图。
  • 数据集:注明数据来源、原始波段数、类别数和每个类别样本数,不需要完整目录但至少要写清 Indian Pines 有 16 类。
  • 环境依赖:Python 版本、spectral、rasterio、numpy、scikit-learn、torch、matplotlib。最好列出pip install -r requirements.txt。
  • 快速开始:三个命令,第一个转数据,第二个训练 SVM 基线,第三个训练深度学习模型。
  • 脚本说明:每个 py 文件对应哪一步,输入输出是什么。
  • 结果:三个指标、混淆矩阵图、分类图。

这样的文档不需要长篇大论,但要保证换一台新电脑也能按步骤跑通。

6.2 使用教程怎么组织:从环境安装到第一次出图的完整流程

使用教程和 README 是两回事。README 是静态说明,使用教程要带着新手走一遍。你应该包含一个可执行的关键步骤清单:安装 miniconda、创建虚拟环境、安装依赖、下载数据集、运行数据预处理脚本、运行训练脚本、查看输出。不要默认读者已经会配环境,很多课程设计同学卡在第一行pip install spectral上。

在教程里我建议截图展示,但如果只是文档,至少要把每个命令和预期输出写出来。比如运行数据读取脚本后,应该看到(145, 145, 200)这样的输出。给用户一个判断依据,他才知道自己是否成功了。

环境安装还常遇到spectral安装失败,这是因为有些镜像源缺少这个库的对应版本。建议用pip install -q spectral rasterio先装,确认没有报错再继续。rasterio在 Windows 上也比较容易出现依赖问题,但用 wheel 包安装通常是可行的。

6.3 让模型精度再提升一步的 3 个方向

如果你做完基础版本还有时间,往下面三个方向挑一个做深入,整个项目的完成度会上一个档次。

第一个是空间-光谱联合特征增强。我前面给出的 HybridSN 是简化版,你可以尝试把 3D 卷积核调大,或者引入 SE 注意力模块。不要盲目堆层数,高光谱数据量小,浅层比深层更容易训练。

第二个是主动学习或半监督扩展。高光谱标注数据贵,你可以先用小量训练集训练模型,再用预测概率最高的样本加入训练集。这个方向在毕业设计里属于加分项,因为它展示了你对数据成本问题的思考。

第三个是可视化分析。除了分类图,可以画出每类的平均光谱曲线,或者在 PCA 降维后对不同类别做 scatters 图,解释类别可分性。这不需要额外调模型,但答辨时很有说服力。

最后分享一个我的个人习惯:每次实验前,先记下随机种子、patch 大小、波段维数、训练样本量这四件事。高光谱分类实验中,这四个变量任何一个变了,结果都会明显波动。如果不在文档里写清楚,你第二天可能完全无法复现自己的结果。及时记录各种参数组合,比事后花三天重新调参划算得多。希望这个方案能帮你在毕设和课设里少走弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询