资讯详情 小样本高光谱图像分类:Python深度学习实战与避坑指南
📅 2026/10/11 22:38:03
简介本资源面向遥感图像处理与计算机视觉方向的研究者、研究生及工程师提供一套在小样本条件下实现高光谱图像分类的深度学习Python源码与项目说明。针对高光谱数据训练样本稀缺、传统算法泛化能力受限的痛点项目整合卷积神经网络、迁移学习与数据增强等策略构建从数据预处理、网络模型搭建到训练验证与结果评估的完整流程并支持Indian Pines、PaviaU、Salinas等常用数据集。压缩包共36个文件约62.16MB包含6个py源码文件、10个npy数据文件、6张png结果图、5个xml配置及mat、pt权重、xlsx记录等覆盖数据读取、网络定义、训练脚本与评估模块。项目说明文档详细讲解代码使用方法、模型选择依据与实验分析便于复现与扩展。目前已有395人学习下载适合希望快速上手小样本高光谱分类、对照源码理解网络设计与评估指标的读者参考。1. 小样本高光谱分类为什么你的模型总在 20 个样本上翻车高光谱图像分类这个方向坑不在模型深不深而在标注样本少得可怜。一幅 Indian Pines 或 PaviaU 影像动辄上百个波段可每类能拿到的标注像素常常只有几十个有的稀有地物甚至个位数。你拿 ResNet 硬怼训练集准确率能冲到 99%测试集一塌糊涂——这就是典型的小样本条件下的高光谱图像分类困境。这个标题要解决的核心问题很明确在每类只有 5、10、15 个标注样本的极端设定下用 Python 把深度学习分类流程跑通并且让精度别崩。适合谁做遥感解译、农业估产、矿物填图的研究生和一线算法工程师手里有 ENVI 格式的影像和少量地面真值想快速验证一个 baseline。热搜里「深度学习」「小样本」「高光谱图像分类」「python 源码」这几个词恰好对应了技术栈、问题设定、任务类型和落地语言下面按这个顺序拆。2. 高光谱数据与小样本设定先把输入和评价口径钉死2.1 高光谱立方体的数据结构与降维必要性高光谱图像是一个三维立方体空间维度 H×W光谱维度 B常见 B 在 100 到 220 之间。直接把这个立方体喂给网络参数量爆炸不说波段间高度冗余还会让模型学到一堆噪声。我一般先做两件事一是把无效波段水汽吸收带、信噪比极低的波段剔掉Indian Pines 常用 200 个波段里的 200 个有效波段PaviaU 用 103 个二是做主成分分析PCA降到 30 维左右保留 95% 以上方差。这一步不是可选项是必选项降维后的数据既省显存又稳精度。import numpy as np from sklearn.decomposition import PCA def load_hsi(data_path, gt_path, n_components30): # data: H×W×B 的 float32 立方体gt: H×W 的标签图0 表示未标注 data np.load(data_path).astype(np.float32) gt np.load(gt_path).astype(np.int32) h, w, b data.shape # 展平成 (H*W, B)只对已标注像素做 PCA 拟合避免未标注区域干扰 flat data.reshape(-1, b) labeled_mask gt.reshape(-1) 0 pca PCA(n_componentsn_components, whitenTrue) pca.fit(flat[labeled_mask]) reduced pca.transform(flat).reshape(h, w, n_components) return reduced, gt这段代码的关键在pca.fit只用了已标注像素。很多人图省事对全图拟合结果未标注区域的地物分布把主成分方向带偏小样本下这个偏差会被放大。whitenTrue让各主成分方差归一化后续网络对尺度不敏感收敛更稳。n_components设 30 是经验值降到 15 会丢细节升到 60 在小样本下容易过拟合建议在 20 到 40 之间用验证集扫一遍。2.2 小样本划分每类 5/10/15 个样本怎么切才不泄漏小样本实验最忌讳的是随机切分导致空间泄漏。高光谱图像里相邻像素光谱几乎一样如果你随机抽训练样本测试集里紧挨着训练样本的像素会被“偷看”精度虚高十几个点。正确做法是按空间块划分或者至少保证训练和测试样本之间有不小于 patch 半径的间隔。常见做法是每类随机选 n 个n5,10,15作为训练剩余标注像素做测试但抽样时记录坐标剔除与训练样本空间距离小于 patch 半径的测试像素。import numpy as np def sample_per_class(gt, n_per_class, patch_radius4, seed42): rng np.random.RandomState(seed) train_idx, test_idx [], [] for c in np.unique(gt): if c 0: continue idx np.argwhere(gt c) rng.shuffle(idx) train_idx.extend(idx[:n_per_class]) # 测试样本剔除与训练样本空间距离过近的像素 for p in idx[n_per_class:]: if np.min(np.abs(idx[:n_per_class] - p).sum(axis1)) patch_radius: test_idx.append(p) return np.array(train_idx), np.array(test_idx)patch_radius要和后面取 patch 的半径一致取 4 意味着 9×9 的邻域。seed固定是为了可复现小样本实验不固定种子跑三次能差 5 个点审稿人一问就露馅。每类样本数 n 建议至少跑 5、10、15 三档只报一档说明不了方法的鲁棒性。2.3 评价指标OA、AA、Kappa 各自在说什么小样本下只看总体精度 OA 会被大类主导。比如 Indian Pines 里玉米免耕类占了大头模型全猜这一类 OA 也能有 30% 多。必须同时报平均精度 AA 和 Kappa 系数。AA 是各类精度的算术平均稀有类表现差会立刻拉低它Kappa 衡量的是“比随机猜好多少”小样本下 Kappa 低于 0.7 基本说明模型没学到东西。我一般还会打印混淆矩阵看哪几类在互相混这比一个数字有用得多。指标含义小样本下的警戒线OA总体正确率低于 70% 需检查流程AA类平均精度与 OA 差距大于 15% 说明类不平衡严重Kappa一致性系数低于 0.7 模型不可用3. 深度模型选型从 3D-CNN 到孪生网络的最小可用方案3.1 为什么小样本下 3D-CNN 容易过拟合3D-CNN 直接对空间-光谱联合卷积理论上最契合高光谱数据但参数量大。一个两层 3D 卷积加全连接参数量轻松过百万而每类只有 10 个样本时训练样本总数可能不到 200参数量和样本量差了三四个数量级过拟合是必然的。血泪经验是小样本下要么把 3D 卷积核压到很小比如 3×3×3 只做一层要么干脆用 PCA 降维后的 2D 卷积加光谱注意力把参数量控在十万以内。import torch import torch.nn as nn class SmallHSINet(nn.Module): def __init__(self, in_bands30, n_classes16, patch_size9): super().__init__() # 先用 1×1 卷积压缩光谱再空间卷积参数量远小于 3D 卷积 self.spectral nn.Sequential( nn.Conv2d(in_bands, 64, 1), nn.BatchNorm2d(64), nn.ReLU() ) self.spatial nn.Sequential( nn.Conv2d(64, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(), nn.AdaptiveAvgPool2d(1) ) self.fc nn.Linear(128, n_classes) def forward(self, x): # x: (B, bands, patch, patch) x self.spectral(x) x self.spatial(x).flatten(1) return self.fc(x)这个网络参数量约 12 万在每类 10 样本下比标准 3D-CNN 稳得多。spectral用 1×1 卷积做波段加权等价于可学习的光谱降维spatial用两层 3×3 卷积加全局池化避免全连接层堆参数。AdaptiveAvgPool2d(1)把空间维度压成 1进一步减参。如果你的类别数不是 16改n_classes即可patch 大小改了要同步调patch_size参数虽然这里没直接用到但取 patch 时要用。3.2 数据增强小样本下最划算的精度来源小样本下数据增强的性价比远高于换模型。高光谱常用的增强有随机翻转、随机旋转 90 度、加高斯噪声、光谱抖动。注意别用色彩抖动那套高光谱的波段有物理含义乱改波段顺序会破坏光谱曲线。我一般用翻转加旋转再加一个幅度很小的光谱噪声标准差 0.01能把每类 10 样本的有效多样性提升两三倍。import torchvision.transforms as T import torch train_transform T.Compose([ T.RandomHorizontalFlip(), T.RandomVerticalFlip(), T.RandomRotation(90), ]) def add_spectral_noise(x, std0.01): # x: (B, bands, patch, patch) return x torch.randn_like(x) * stdRandomRotation(90)只转 90 度的整数倍避免插值引入虚假光谱。add_spectral_noise的std别超过 0.02再大就把光谱曲线淹了。增强只在训练时做验证和测试保持原样这个开关别搞反否则精度虚高。3.3 迁移学习与元学习什么时候值得上如果你手头有别的高光谱数据集比如用 PaviaU 预训练迁到 Indian Pines迁移学习能再涨几个点。做法是先在源域训一个同结构网络把spectral和spatial的权重冻结只微调fc。元学习如 MAML、ProtoNet在每类 1 到 5 个样本时优势明显但实现复杂度高训练不稳定我一般作为对比方法而不是首选。判断标准很简单每类样本大于 10先用数据增强加轻量网络小于 5再考虑元学习。4. 训练流程与参数配置把复现精度稳在可接受区间4.1 优化器、学习率与 batch 的搭配小样本下 Adam 比 SGD 稳学习率从 1e-3 起步配合余弦退火。batch size 别设大每类 10 样本时总训练样本可能就一两百batch 设 16 或 32 即可设 128 一个 epoch 才走一两步根本训不动。权重衰减用 1e-4太大欠拟合太小过拟合。训练轮数 200 到 300早停看验证集 AA连续 30 轮不涨就停。import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR model SmallHSINet(in_bands30, n_classes16).cuda() optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max200, eta_min1e-5) criterion nn.CrossEntropyLoss() for epoch in range(300): model.train() for x, y in train_loader: x, y x.cuda(), y.cuda() x add_spectral_noise(x) optimizer.zero_grad() loss criterion(model(x), y) loss.backward() optimizer.step() scheduler.step()CosineAnnealingLR的T_max设成总轮数eta_min别设 0留一点学习率防止后期完全不动。CrossEntropyLoss默认带类别权重如果类极不平衡可以传weight参数给稀有类加权但小样本下加权容易让模型偏向稀有类反而拉低 OA建议先不加看混淆矩阵再决定。4.2 取 patch 的边界处理与归一化高光谱分类通常以每个像素为中心取一个 patch比如 9×9边缘像素取不满怎么办常见做法是镜像填充或补零。补零会在边缘引入虚假的低值区域镜像填充更自然。归一化用训练集的均值和标准差别用全图统计量否则测试信息泄漏。import numpy as np def extract_patches(data, gt, indices, patch_size9): r patch_size // 2 padded np.pad(data, ((r, r), (r, r), (0, 0)), modereflect) patches, labels [], [] for (i, j) in indices: patch padded[i:ipatch_size, j:jpatch_size, :] patches.append(patch.transpose(2, 0, 1)) # 转成 (bands, h, w) labels.append(gt[i, j]) return np.array(patches), np.array(labels)modereflect是镜像填充比constant补零稳。transpose(2,0,1)把波段维提前符合 PyTorch 的通道优先格式。归一化在取完 patch 后做用训练集算出的mean和std测试集复用同一组值。4.3 训练日志与早停看什么指标决定停别只盯 lossloss 降不代表精度涨。每个 epoch 后在验证集上算 OA、AA、Kappa存最优模型。早停条件用 AA因为 AA 对稀有类敏感小样本下更能反映真实能力。日志里同时打印学习率确认余弦退火在正常工作。best_aa 0.0 patience, wait 30, 0 for epoch in range(300): # ... 训练 ... model.eval() with torch.no_grad(): preds model(val_x.cuda()).argmax(1).cpu().numpy() aa compute_aa(val_y, preds) if aa best_aa: best_aa, wait aa, 0 torch.save(model.state_dict(), best.pth) else: wait 1 if wait patience: breakcompute_aa按类算召回率再平均别用 sklearn 的accuracy_score糊弄。patience设 30 是经验值小样本下验证集本身小波动大设 10 容易误停。5. 避坑与排查小样本高光谱分类的 5 个翻车现场5.1 精度虚高随机切分导致的空间泄漏现象每类 10 样本OA 跑到 95% 以上换一组种子掉到 70%。原因随机抽训练测试样本相邻像素光谱几乎相同测试集被训练集“污染”。解决按 2.2 节的空间块划分测试样本与训练样本空间距离大于 patch 半径重跑后 OA 会回落到合理区间这才是真实水平。5.2 全猜大类类不平衡没处理现象AA 远低于 OA混淆矩阵显示稀有类全被预测成大类。原因交叉熵被大类主导小样本下稀有类梯度被淹没。解决先看混淆矩阵确认再考虑给稀有类加权重或者用 Focal Loss。但注意加权别过头否则大类精度崩OA 也保不住。我一般先试过采样稀有类到每类 20 个比直接改 loss 稳。5.3 显存爆了patch 取太大或 batch 设太高现象CUDA out of memory。原因patch 取 15×15 甚至 21×21波段没降维batch 又设 64。解决先把 PCA 降到 30 维patch 压到 9×9batch 降到 16。如果还不够用混合精度训练torch.cuda.amp能省一半显存精度几乎不掉。5.4 训练不收敛学习率太大或归一化没做现象loss 震荡或直接 NaN。原因学习率 1e-2 起步或者数据没归一化像素值在 0 到 10000 之间。解决学习率降到 1e-3数据做 z-score 归一化检查输入x.mean()和x.std()是否接近 0 和 1。如果用了 PCA 白化这一步通常已经做了但取 patch 后要再确认一遍。5.5 复现不了种子没固定或环境差异现象同一份代码两次跑差 5 个点。原因numpy、torch、cuda 的随机种子没全固定或者 cudnn 的 benchmark 模式导致卷积算法不确定。解决在开头固定np.random.seed、torch.manual_seed、torch.cuda.manual_seed_all并设torch.backends.cudnn.deterministic True。这会慢一点但小样本实验可复现比快几秒重要得多。6. 进阶技巧用光谱注意力把每类 5 样本的 AA 再抬 3 个点小样本下最值钱的技巧不是换更深的网络而是让模型学会“看哪些波段”。高光谱里有些波段对特定地物敏感有些全是噪声光谱注意力就是给每个波段学一个权重。实现上用一个 squeeze-excitation 模块全局池化后过两层全连接输出波段权重再乘回特征。这个模块参数量极小几十个样本也能训起来。class SpectralAttention(nn.Module): def __init__(self, bands, reduction4): super().__init__() self.se nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(bands, bands // reduction, 1), nn.ReLU(), nn.Conv2d(bands // reduction, bands, 1), nn.Sigmoid() ) def forward(self, x): # x: (B, bands, h, w) w self.se(x) return x * w把SpectralAttention插在spectral卷积之后、spatial之前让波段权重在空间卷积前生效。reduction4是压缩比波段 30 时中间层 7 个通道参数量可忽略。验证方法训练完打印w.mean(dim(0,2,3))看哪些波段权重大和已知的地物敏感波段对比如果权重集中在 700nm 附近的红边说明模型学到了物理意义不是瞎拟合。我自己的习惯是每做一个新数据集先跑一遍不加注意力的 baseline记下 AA再加注意力看涨不涨。涨了保留不涨说明这个数据集波段冗余本来就低别硬加。小样本实验最怕自欺欺人每类 5 个样本时AA 涨 3 个点可能就是随机波动我一般跑 5 组不同种子取平均标准差超过 2 个点就不下结论。这套流程从数据加载到注意力模块代码量不大但每一步的坑都踩过一遍才敢写出来。希望帮到你。本文还有配套的精品资源点击获取