首页 / 资讯中心 / 文章详情

高光谱图像分类实战:Python与CNN从数据到模型

高光谱图像分类实战:Python与CNN从数据到模型 ★ FEATURED ARTICLE
简介这份资源面向具备一定深度学习基础、希望上手高光谱图像分类的开发者与研究者围绕Python与卷积神经网络展开重点解决1D光谱特征、2D空间特征及3D谱空联合特征的学习与分类问题适用于遥感、环境监测、医学成像等场景。压缩包共8个文件以4个py脚本和4个mat数据文件为主脚本分别对应1DCNN、2DCNN、3DCNN三种网络结构及辅助函数mat文件则提供KSC与PaviaU两个高光谱数据集及其标签整体约87.4MB。目前已有3839人学习下载。资源基于TensorFlow-GPU 1.5.0与Keras 2.1.6搭建覆盖数据预处理、特征提取、模型训练、验证测试与混淆矩阵、准确率、召回率、F1分数等评估环节读者可据此复现完整分类流程理解卷积层、池化层、全连接层在高光谱数据上的具体用法并对照脚本调整学习率、批次大小与卷积核数量等超参数快速建立从数据到评估的实践认知。1. 高光谱分类为什么让 CNN 又爱又恨高光谱图像分类这个方向我最早接触是在一个遥感解译的模拟项目里。当时拿到一份公开数据集图像尺寸不大但波段数有 200 多个第一反应是“这不就是把 2D 卷积换成 3D 卷积的事吗”。结果跑了一版基线训练集准确率冲到 99%测试集却惨不忍睹——这就是高光谱分类最经典的坑小样本 高维特征 光谱冗余三者叠加模型极容易过拟合。高光谱图像和普通 RGB 图像的本质区别在于每个像素不是 3 个通道而是几十到几百个连续波段形成一条光谱曲线。这条曲线能反映地物材质比如植被、水体、建筑在特定波段上有明显吸收峰。卷积神经网络在这里的价值是同时提取空间纹理和光谱特征——2D 卷积管空间1D 卷积管光谱3D 卷积两者一起管。但波段多了之后相邻波段高度相关直接全喂给网络参数量爆炸训练样本却往往只有几百个标注像素。所以这篇文章要解决的问题很具体用 Python 和 CNN 把高光谱图像分类跑通并且让测试集精度站得住。适合已经会写 PyTorch 或 TensorFlow 基础代码、但没处理过高光谱数据的读者。我会从数据读取、样本构造、模型选型一路讲到训练参数和排错中间给可直接抄的代码块。先立住一个判断高光谱分类不是“把 RGB 分类代码改个输入通道数”就能交差的样本构造方式比模型结构更决定成败。2. 数据读取与样本构造决定上限的一步2.1 高光谱数据长什么样怎么读进内存常见的高光谱数据集格式有.mat、.tif和.hdr 原始二进制。以最通用的.mat为例里面通常有两个关键变量data是三维数组(H, W, B)labels是二维数组(H, W)每个整数代表一类地物0 通常表示未标注。读进来之后第一件事不是急着切块而是确认波段顺序和无效值。import scipy.io as sio import numpy as np # 读取 .mat 格式高光谱数据 # 常见变量名data / X / imglabels / gt / y mat sio.loadmat(dataset.mat) data mat[data].astype(np.float32) # 形状 (H, W, B) labels mat[labels].astype(np.int32) # 形状 (H, W) print(数据形状:, data.shape) print(标签类别:, np.unique(labels)) # 检查无效值高光谱常见 0 或 -9999 表示背景 invalid_mask (data 0).all(axis2) print(无效像素占比: {:.2%}.format(invalid_mask.mean()))这段代码的逻辑说明data转float32是为了后续归一化不丢精度labels转int32是因为 PyTorch 的交叉熵损失要求标签为int64但中间处理用int32更省内存。参数上invalid_mask用all(axis2)判断“所有波段都小于等于 0”的像素这类像素在部分数据集中是背景不能当训练样本。如果无效像素占比超过 30%说明数据裁剪有问题需要回去检查原始文件。注意有些数据集的波段顺序是(B, H, W)直接按(H, W, B)处理会导致空间维度错乱。读完后打印shape并和数据集说明对照这一步不能省。2.2 样本切块为什么不能按像素单独训练高光谱分类如果按像素单独训练就是把每个像素的光谱向量(B,)喂给 1D 卷积或全连接。这种做法在早期论文里常见但缺点很明显完全丢弃空间信息相邻像素之间的纹理关联没了而且标注样本少的时候每个像素独立看待噪声像素直接污染梯度。主流做法是以标注像素为中心切空间块比如取11×11或15×15的邻域得到一个(11, 11, B)的立方体。这样每个样本既包含中心像素的光谱也包含周围的空间纹理。切块时有两个参数必须调块大小和边界填充方式。import numpy as np def extract_patches(data, labels, patch_size11, remove_zeroTrue): 以标注像素为中心提取空间-光谱块 data: (H, W, B) labels: (H, W) patch_size: 奇数如 11 表示 11x11 remove_zero: 是否剔除全零背景块 返回: patches (N, patch_size, patch_size, B), targets (N,) margin patch_size // 2 # 边界用 reflect 填充避免边缘样本丢失 padded np.pad(data, ((margin, margin), (margin, margin), (0, 0)), modereflect) patches, targets [], [] ys, xs np.where(labels 0) for y, x in zip(ys, xs): patch padded[y:y patch_size, x:x patch_size, :] if remove_zero and (patch 0).all(): continue patches.append(patch) targets.append(labels[y, x]) patches np.stack(patches).astype(np.float32) targets np.array(targets, dtypenp.int64) return patches, targets逻辑说明np.pad用reflect模式而不是补零是因为补零会在边缘引入虚假的“黑色地物”干扰光谱曲线。margin取patch_size // 2保证中心像素始终在块的正中央。remove_zero剔除全零块防止背景噪声进入训练集。参数上patch_size从 11 开始试太小如 5空间信息不足太大如 21会引入过多异类像素尤其在地物边界处。我一般先用 11 跑基线再根据验证集精度微调。2.3 归一化与数据增强两个容易做反的细节高光谱数据的归一化不是简单除以 255。不同波段量纲差异大有的波段反射率在 0 到 1 之间有的在 0 到 10000 之间。常见做法是逐波段标准化即对每个波段单独减均值除标准差。但这里有个坑均值和标准差必须只用训练集算然后应用到验证集和测试集否则就是数据泄露。from sklearn.model_selection import train_test_split # 先切分再算统计量 X_train, X_test, y_train, y_test train_test_split( patches, targets, test_size0.8, stratifytargets, random_state42 ) # 逐波段标准化只在训练集上 fit mean X_train.mean(axis(0, 1, 2), keepdimsTrue) # (1,1,1,B) std X_train.std(axis(0, 1, 2), keepdimsTrue) 1e-8 X_train (X_train - mean) / std X_test (X_test - mean) / std参数说明test_size0.8在高光谱里很常见因为标注样本本来就少训练集可能只占 10% 到 20%。stratifytargets保证每个类别在训练集和测试集里比例一致避免某类样本全被分到一边。keepdimsTrue让均值和标准差的形状保持(1,1,1,B)方便广播。1e-8防止除零。数据增强方面高光谱不能随便用 RGB 那套颜色抖动。可用的增强包括随机水平翻转、随机垂直翻转、随机旋转 90 度。这些操作不改变光谱曲线只改变空间排列对分类是安全的。不要用颜色变换或裁剪会破坏光谱连续性。3. 模型选型2D、3D 还是混合卷积3.1 三种卷积结构的适用边界高光谱 CNN 的模型结构大致分三类1D CNN只处理光谱向量2D CNN把波段当通道处理空间块3D CNN同时在空间和光谱维度卷积。三者的计算量和精度差异很大选型取决于样本量和波段数。结构输入形状参数量适合场景主要缺点1D CNN(B,)小样本极少只关心光谱丢弃空间信息2D CNN(patch, patch, B)中样本中等空间纹理重要光谱维当通道未显式建模3D CNN(patch, patch, B, 1)大样本充足光谱空间联合易过拟合训练慢混合 2D3D先 3D 后 2D较大追求精度有调参经验结构复杂超参多我的经验是标注样本少于 500 时优先用 2D CNN 加光谱注意力不要直接上 3D。3D 卷积在样本少的时候训练集精度和测试集精度能差 20 个点以上血泪经验。如果样本在 1000 以上可以试混合结构先用 3D 卷积压缩光谱维再用 2D 卷积提空间特征。3.2 一个可复现的 2D CNN 基线下面这个模型结构不复杂但在多个公开高光谱数据集上都能跑到可用的基线精度。核心思路先用 1×1 卷积降波段维再用 3×3 卷积提空间特征最后全局池化分类。import torch import torch.nn as nn class HSICNN(nn.Module): def __init__(self, in_bands, num_classes, patch_size11): super().__init__() # 1x1 卷积压缩光谱维减少后续计算量 self.spectral_reduce nn.Sequential( nn.Conv2d(in_bands, 64, kernel_size1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue) ) # 空间特征提取 self.spatial nn.Sequential( nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(128, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.AdaptiveAvgPool2d(1) # 全局池化输出 (N,128,1,1) ) self.classifier nn.Linear(128, num_classes) def forward(self, x): # x: (N, B, patch, patch) x self.spectral_reduce(x) x self.spatial(x) x x.flatten(1) return self.classifier(x)逻辑说明输入需要从(N, patch, patch, B)转成(N, B, patch, patch)因为 PyTorch 的Conv2d要求通道在前。spectral_reduce用 1×1 卷积把 200 多个波段压到 64这一步参数量是B×64比直接 3×3 卷积小得多。AdaptiveAvgPool2d(1)替代全连接前的展平减少参数量也降低过拟合风险。参数上64和128是通道数样本少时可以减半patch_size只影响输入尺寸不影响模型结构。3.3 训练参数怎么设学习率、批次与早停高光谱训练最容易翻车的地方是学习率。因为样本少梯度噪声大学习率设 0.01 经常直接发散。我一般从1e-3开始配合ReduceLROnPlateau验证集损失不降就减半。from torch.utils.data import TensorDataset, DataLoader # 转成 PyTorch 张量注意维度置换 X_train_t torch.tensor(X_train).permute(0, 3, 1, 2) # (N,B,patch,patch) X_test_t torch.tensor(X_test).permute(0, 3, 1, 2) y_train_t torch.tensor(y_train) y_test_t torch.tensor(y_test) train_loader DataLoader(TensorDataset(X_train_t, y_train_t), batch_size32, shuffleTrue) test_loader DataLoader(TensorDataset(X_test_t, y_test_t), batch_size64, shuffleFalse) model HSICNN(in_bandsX_train.shape[-1], num_classeslen(np.unique(y_train))) optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience10 ) criterion nn.CrossEntropyLoss()参数说明batch_size32是样本少时的折中太大梯度更新次数少太小训练不稳定。weight_decay1e-4是 L2 正则高光谱必备不加的话训练集精度很快到 100%。patience10表示验证损失 10 个 epoch 不降就减学习率。早停策略我一般设patience30即 30 个 epoch 验证精度不升就停保留验证集最好的权重。注意验证集不能和测试集混用。高光谱数据量小很多人把测试集当验证集调参最后报的精度是“调出来的”换一组数据就崩。正确做法是训练集、验证集、测试集三分开验证集用于早停和调参测试集只在最后跑一次。4. 避坑与排查高光谱 CNN 的五个典型翻车现场4.1 训练集精度 99%测试集 60%过拟合的三种解法现象训练 loss 一路降到 0.01训练精度 99% 以上但测试精度卡在 60% 到 70%验证 loss 先降后升。原因高光谱标注样本少模型参数量远大于样本量网络直接“背下”了训练集。加上波段间高度相关模型学到的是噪声而非泛化特征。解决第一加Dropout在全连接前设p0.5第二加weight_decay从1e-4试到1e-3第三减小模型通道数把128降到64。三招一起上通常能把训练集和测试集的精度差压到 10 个点以内。4.2 损失变成 NaN归一化和学习率的联合排查现象训练几个 batch 后 loss 突然变成nan梯度爆炸。原因常见两个一是归一化没做某些波段数值极大卷积后激活值溢出二是学习率太大Adam 的更新步长超过数值稳定范围。解决先确认逐波段标准化是否只在训练集上算的再检查输入里有没有inf或nan。然后把学习率降到1e-4加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)。如果还不行把BatchNorm换成GroupNorm小批次下更稳定。4.3 某几类精度极低类别不平衡与光谱混淆现象总体精度 85%但某两类比如草地和农田的召回率不到 50%混淆矩阵里互相错分。原因这两类地物的光谱曲线在大部分波段上几乎重合只在少数窄波段有差异。如果切块时空间邻域混入了对方像素模型更难区分。解决第一检查这两类的样本数是否远少于其他类如果是用WeightedRandomSampler或类别权重加权的交叉熵第二把patch_size从 11 降到 7减少邻域混入第三在模型里加光谱注意力模块让网络自动关注差异波段。常见做法是加一个SEBlock对通道维做注意力。4.4 验证集精度波动大随机种子与数据划分现象同样的代码换一次随机种子测试精度差 5 到 10 个点。原因高光谱数据量小不同的训练/测试划分会导致某些类在测试集里样本极少精度估计方差大。解决不要只跑一次。用StratifiedKFold做 5 折交叉验证报平均精度和标准差。如果标准差超过 3 个点说明数据划分影响太大需要增加样本或改用更稳定的划分方式。固定随机种子只能保证可复现不能降低方差。4.5 推理速度慢3D 卷积的显存与计算优化现象用 3D CNN 训练时显存不够或者推理一张大图要几分钟。原因3D 卷积的计算量是 2D 的kernel_size倍波段数 200 多时中间特征图显存占用极大。解决第一先用 1×1×1 卷积把波段维从 200 降到 32 或 64第二把大图切成不重叠的块批量推理不要整图一次前向第三如果只是分类不是分割用 2D CNN 加光谱降维精度损失通常不到 2 个点速度提升 5 倍以上。5. 进阶技巧用光谱注意力把精度再推一截5.1 为什么加注意力比换模型更划算高光谱分类的精度瓶颈往往不在空间特征而在哪些波段对当前类别有用。比如水体在近红外波段反射率极低植被在红边波段有陡升这些差异集中在少数波段。普通卷积对所有波段一视同仁注意力机制可以让网络自己学波段权重。加一个轻量的通道注意力模块参数量增加不到 5%精度通常能涨 1 到 3 个点比换更深的 3D 网络划算得多。5.2 一个可插入的 SE 模块实现class SEBlock(nn.Module): 通道注意力对光谱维做重加权 def __init__(self, channels, reduction16): super().__init__() self.pool nn.AdaptiveAvgPool2d(1) self.fc nn.Sequential( nn.Linear(channels, channels // reduction, biasFalse), nn.ReLU(inplaceTrue), nn.Linear(channels // reduction, channels, biasFalse), nn.Sigmoid() ) def forward(self, x): # x: (N, C, H, W) b, c, _, _ x.size() y self.pool(x).view(b, c) y self.fc(y).view(b, c, 1, 1) return x * y # 通道加权逻辑说明AdaptiveAvgPool2d(1)把每个通道的空间信息压成一个标量fc先降维再升维Sigmoid输出 0 到 1 的权重最后乘回原特征图。reduction16是压缩比通道数 64 时中间层 4 维参数量极小。插入位置一般在spectral_reduce之后、spatial之前让网络先对光谱通道重加权再提空间特征。5.3 验证注意力是否生效看权重分布加完注意力不能只看精度涨没涨还要确认它真的在学波段权重。方法很简单取一批测试样本前向到 SE 模块把y的权重向量保存下来按类别求平均看不同类别的权重分布是否有差异。# 假设 model 里 se 模块可访问注册 hook 抓取权重 weights {} def hook_fn(module, input, output): weights[se] output.detach().cpu() handle model.se.register_forward_hook(hook_fn) model.eval() with torch.no_grad(): _ model(X_test_t[:64]) handle.remove() # weights[se] 形状 (64, C, 1, 1)按类别平均 se_weights weights[se].squeeze(-1).squeeze(-1) # (64, C) for cls in np.unique(y_test[:64]): idx y_test[:64] cls print(类别, cls, 平均权重前5通道:, se_weights[idx].mean(0).topk(5).indices.tolist())如果不同类别的高权重通道有明显差异说明注意力在起作用如果所有类别权重几乎一样说明模块没学到东西需要检查学习率是否太小或者注意力插入位置不对。5.4 我踩过的注意力坑最早加 SE 模块时我把它放在spectral_reduce之前结果精度反而降了。原因是原始波段数 200 多SE 的fc中间层只有 12 维压缩太狠权重全被压成 0.5 左右等于没加。后来改成先 1×1 卷积降到 64再接 SE精度才涨上去。另一个坑是reduction设成 8 时小样本数据集上过拟合明显验证 loss 波动大改成 16 后稳定。所以注意力模块不是“加上就好”插入位置和压缩比都要根据波段数和样本量调。我现在的习惯是任何高光谱分类任务先跑一个不带注意力的 2D CNN 基线记录测试精度和混淆矩阵然后加 SE只改一个变量对比精度和权重分布。如果精度没涨或者权重分布无差异先查插入位置和reduction不要急着换模型。这套流程帮我省了很多“玄学调参”的时间。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站