高光谱图像分类这几年随着深度学习的普及已经成了遥感圈子里最热门的方向之一。我前阵子接手一个任务基于深度学习做高光谱图像地物分类要求在有限的标注样本下把分类精度提上去还要把整个流程跑通、跑稳。项目做下来从三维卷积、混合卷积到注意力机制从归一化、降维到训练策略调整把相关技术点整个过了一遍踩了不少坑也沉淀了一套可以直接复用的流程。这篇博文就从这个实际项目出发讲讲我是怎么拆任务、选模型、调参数、做评估的也把我踩过的坑一并分享出来。适合刚接触遥感深度学习的人也适合准备拿这个方向做毕设或者横向项目的开发者。1. 内容整体设计与思路拆解1.1 高光谱图像为什么这么难做先说高光谱图像本身。普通相机通常只有三个波段红绿蓝再加一个全色波段就是极限了。高光谱传感器不一样它能在可见光和近红外范围内采集几十甚至上百个连续波段。每个像元都对应一条“光谱曲线”就像给地物拍了一组光谱指纹。传统方法做分类本质上是比较光谱曲线的相似度或者是人工挑选几个关键波段后再跑分类器。但问题在于波段多不一定是好事。几百个波段意味着数据维度极高而高光谱图像的标注样本却通常很少一个场景里逐像元标注的地物区域往往只占整个图像的一小块。这种“高维小样本”的矛盾是传统机器学习方法最头疼的地方。早年大家常用带径向基核的支持向量机或者随机森林先做主成分分析把波段从上百个压到十几个再训练分类器。这套方案在当时算得上主流但有几个绕不过去的短板一是主成分分析会把光谱细节抹掉一部分二是空间邻域关系完全没利用上三是人工调核函数参数很费劲。深度学习天然适合处理这种高维数据。卷积神经网络可以自动学习特征不需要人工筛波段更重要的是三维卷积能同时处理高度、宽度和波段三个维度把“光谱维”和“空间维”联合起来建模。这正好解决了高光谱分类里“既要看光谱曲线又要看周围邻居”的需求。1.2 方案选型背后的逻辑项目立项时我先定了一个原则能不魔改就不魔改优先选择经过公开验证的成熟结构再针对小样本问题做针对性调整。一开始我试过直接套用二维ResNet把每个波段的图像当普通RGB图来处理效果很差。原因很简单二维卷积在处理高光谱数据时要么把波段当通道输入通道数几十上百权重爆炸显存不够要么先用降维把波段压到三五个再输入这样光谱信息基本丢了。无论哪种都违背了高光谱图像“光谱维是关键”这个基本事实。后来我找到了混合卷积结构的思路先做几个三维卷积层让网络在波段方向上也进行滑动卷积直接提取光谱-空间联合特征。三维卷积之后再用二维卷积继续做空间特征提取。这个设计的合理之处在于高光谱数据的光谱维在结构上和空间维是等价的都应该通过四维卷积核进行局部感知。而完全使用三维卷积又会导致参数量和计算量急剧上升所以“三维卷积提取联合特征、二维卷积精修空间特征”就成了实践中最平衡的方案。最终模型结构定为输入一个立体图像块先经过三维卷积块再做维度压缩接着过二维卷积层最后进入全连接分类头。在三维卷积和二维卷积之间我额外加入了一个轻量的波段注意力模块用来给重要波段加权。这个模块的灵感来自SENet的通道注意力思想但放在高光谱场景下它的物理含义更明确某些光谱波段对区分地物确实更重要注意力机制能自动把权重分配出来。2. 数据准备与预处理要点2.1 数据归一化与标签处理先交代一下我用的数据。在实验里我分别用了两个公开的高光谱基准数据集做验证一个是AVIRIS传感器采集的农场景影像另一个是ROSIS传感器采集的城市场景影像。两个数据集的标注类别数不一样光谱波段数也不一样正好可以检验模型的泛化能力。拿到原始数据后第一步永远是归一化。高光谱图像的原始像元值是辐射亮度或反射率范围不稳定有些波段值差异很大直接送入网络训练轻则收敛慢重则梯度爆炸。我采用最常用的最大最小值归一化把每个波段独立缩放到0到1之间。这里要特别注意一个细节归一化参数也就是每个波段的最大值和最小值必须只从训练样本中统计不能在全图上统计完再划分训练集和验证集。如果提前在全图上做了归一化相当于在训练前就“偷看”了验证集和测试集的统计信息最后得出的精度指标会有水分。归一化之后是标签处理。高光谱数据集的标签通常是整数编码比如0代表背景1代表某一类地物2代表另一类。背景像素一般需要单独剔除不参与训练也不参与评估否则会把精度拉得很高失去评判意义。我习惯把类别编号从0开始连续编码并在建索引时忽略标签为0的像素。2.2 邻域图像块采样与降维策略高光谱分类的输入不是单个像元而是以每个带标签像元为中心切出一个小立方体。这个立方体的空间尺寸通常定为11×11或者15×15波段维度保留全部或部分。为什么要切邻域块因为高光谱图像本身空间分辨率有限纯光谱曲线判别能力不足邻域像元的光谱信息能够提供上下文纹理增强分类鲁棒性。这个做法在业内已经是共识几乎所有的深度学习高光谱分类模型都采用这种输入方式。采样阶段需要小心边界像素。图像边缘的像元取不到完整的邻域块解决方法是对图像做边缘反射填充也就是镜像填充再切块。切块之后数据量会非常大。比如一幅145×145的场景剔除背景后标注像素只有一万多个每个像素切一块11×11×波段数的小立方体整体数据规模在可接受范围内。但如果换成较大的场景比如城市数据有几十万标注像元全部切块后内存会爆炸这时需要采用在线采样的方式每个训练批次随机从训练索引中抽取固定数量的块避免一次性把全部样本载入内存。降维是另一个关键决策。我做了两组对照实验一组直接用全部波段另一组先做主成分分析降到30个主成分再训练。结果很有代表性在训练样本充足时保留全部波段的精度略高在训练样本极少比如每类只有20个样本时先降到30个主成分的效果更稳。原因在于主成分分析能压缩冗余的波段信息降低模型拟合难度。所以我最终的策略是默认保留全部波段当出现明显过拟合时才启用PCA预处理。值得注意的是PCA降维时也要在训练像素上拟合统计参数再映射到验证集和测试集不能把整个数据集混在一起做PCA。3. 核心模型设计与实现3.1 三维卷积提取光谱-空间联合特征三维卷积和二维卷积最直观的区别在于卷积核移动的方向。二维卷积核只在图像的宽和高方向滑动三维卷积核额外增加了一个“深度”方向也就是高光谱的波段方向。对高光谱分类来说不同地物的差异往往体现在某几个谱段的连续变化趋势上这种趋势需要同时观察多个相邻波段才能捕捉。三维卷积核在波段维上滑动时可以理解为逐层扫描光谱曲线提取局部光谱模式。这一段是网络构建时最容易翻车的地方一定要把数据张量的维度理清楚。PyTorch中三维卷积的期望输入形状是(batch, channels, depth, height, width)。在高光谱场景中输入图像的尺寸是(height, width, bands)也就是高度、宽度、波段数。放进三维卷积前要把它整理成(batch, 1, bands, height, width)其中1是输入通道数bands作为depth, height和width作为空间维度。我之前见过不少人把波段数直接放在通道位置结果网络能跑但精度一直上不去就是因为维度语义搞错了。三维卷积层的参数设计也需要考虑实际含义。我第一个三维卷积用的卷积核尺寸是(8, 3, 3)意思是波段方向上卷积核覆盖8个波段空间方向覆盖3×3像素卷积核数量为8。这样设置的好处是先在光谱维做较大范围的滑窗充分观察光谱曲线形态再结合小范围的3×3空间感受野兼顾局部邻域信息。之后再用一个尺寸为(5, 3, 3)的三维卷积层继续压缩波段维加深空间特征抽象。三维卷积有个非常现实的问题计算量大参数多显存占用高。特别是输入块的空间尺寸较大时中间特征图的体积会成倍增长。所以我一般控制空间输入尺寸为11×11三维卷积层数不超过两层并在三维卷积之后立即做维度重塑把深度的通道信息合并转给二维卷积去处理。3.2 引入波段注意力与残差结构的实践基线模型纯靠三维卷积加二维卷积堆叠精度已经不差但在小样本实验里还是存在两个问题一是对无用波段过于敏感二是深层网络收敛慢。为此我加入了一个波段注意力模块。思路很简单对三维卷积输出的特征图在波段维上计算每个波段的全局平均响应然后通过两个全连接层得到一个权重向量再把这个权重向量乘回特征图以此增强关键波段、弱化噪声波段。用PyTorch写就是先对特征做全局平均池化得到shape为(batch, channels, depth, 1, 1)的描述符再沿着depth维度计算权重。这个模块参数量很少不会明显拖慢训练但它带来的精度提升在多个数据集上都有体现尤其在包含强噪声波段的数据上提升幅度能达到一个百分点以上。另一个改动是引入残差连接。我的做法是把三维卷积块的输出与输入相加类似ResNet的基本思想。这一项在初始实验里效果不显著但随着训练轮次增加到150轮以上加了残差的网络训练损失下降更平稳最终精度也略高。我分析原因是高光谱数据本身存在很强的低秩特性光谱曲线变化平缓残差学习让网络更多地去拟合变化的部分而不是从头重建整条曲线这降低了学习难度。最终版本的网络结构参数配置如下表这个配置在显存需求与分类精度之间比较平衡。网络层模块具体配置输出特征尺寸输入11×11×BB为波段数1×B×11×11三维卷积块1Conv3d(1, 8, kernel(8,3,3), pad(2,0,0)) BN ReLU8×(B-5)×9×9三维卷积块2Conv3d(8, 16, kernel(5,3,3), pad(0,0,0)) BN ReLU16×(B-8)×7×7波段注意力模块全局均值池化两层全连接Sigmoid16×(B-8)×7×7维度重塑合并通道数与深度数C×7×7二维卷积层Conv2d(C, 64, kernel3, padding1) BN ReLU64×7×7二维卷积层Conv2d(64, 64, kernel3, padding1) BN ReLU64×7×7全局池化AdaptiveAvgPool2d(1)64×1×1分类层全连接层类别数这里的C是重塑后的通道数等于16×(B-8)具体数值依数据集而定。如果波段数很大比如200个波段中间特征图会非常庞大所以通常会在三维卷积块之前先做一次波段方向的PCA降维把波段压到30左右再送入上述网络。3.3 关键网络模块代码实现这里放一段我最终训练用到的核心网络结构定义为了方便复现我把波段注意力模块单独拆出来写。import torch import torch.nn as nn class SpectralAttention(nn.Module): def __init__(self, num_bands, reduction8): super().__init__() self.avg_pool nn.AdaptiveAvgPool3d((None, 1, 1)) self.fc nn.Sequential( nn.Linear(num_bands, num_bands // reduction), nn.ReLU(inplaceTrue), nn.Linear(num_bands // reduction, num_bands), nn.Sigmoid() ) def forward(self, x): # x shape: (batch, channels, depth, height, width) b, c, d, h, w x.size() y self.avg_pool(x) y y.view(b, c, d) y y.mean(dim1, keepdimTrue) # (batch, 1, depth) y y.view(b, d) w self.fc(y) # (batch, depth) w w.view(b, 1, d, 1, 1) return x * w骨干网络的完整定义相对长一些核心点在于第一层三维卷积时给波段方向设置合适的padding避免方向信息被过早压缩。class HybridSpectralNet(nn.Module): def __init__(self, in_bands, num_classes): super().__init__() self.conv3d_1 nn.Sequential( nn.Conv3d(1, 8, kernel_size(8, 3, 3), stride(1, 1, 1), padding(2, 0, 0)), nn.BatchNorm3d(8), nn.ReLU(inplaceTrue) ) self.conv3d_2 nn.Sequential( nn.Conv3d(8, 16, kernel_size(5, 3, 3), stride(1, 1, 1), padding(0, 0, 0)), nn.BatchNorm3d(16), nn.ReLU(inplaceTrue) ) self.attn SpectralAttention(in_bands - 8 4) dummy torch.zeros(1, 1, in_bands, 11, 11) dummy self.conv3d_1(dummy) dummy self.conv3d_2(dummy) c dummy.size(1) * dummy.size(2) h dummy.size(3) w dummy.size(4) self.conv2d_block nn.Sequential( nn.Conv2d(c, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.Conv2d(64, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue) ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(64, num_classes) ) def forward(self, x): # x: (batch, in_bands, 11, 11) x x.unsqueeze(1) x self.conv3d_1(x) x self.conv3d_2(x) x self.attn(x) b, c, d, h, w x.size() x x.view(b, c * d, h, w) x self.conv2d_block(x) x self.classifier(x) return x这段代码里有一个比较“脏”的处理方式用dummy张量在初始化阶段动态计算中间特征图的尺寸。这个方法的好处是不用手算卷积输出的高度、宽度和深度改输入尺寸时不容易出错。但缺点是每次实例化网络时都会有一次空计算会浪费一点点时间。也可以手动推导尺寸公式但我觉得动态计算更省心项目初期调试成本低。4. 训练策略与调参实战4.1 优化器与学习率调整模型结构定下来以后训练策略直接决定最终分类精度的下限。整个项目里我亲测最稳的组合是Adam优化器配合余弦退火学习率调度。初始学习率我设在0.001如果数据集较小或者训练轮次较短初始学习率降到0.0005更安全。为什么不用SGD在小样本高光谱任务中SGD对学习率和动量非常敏感需要手动做很多轮预热和调整而Adam的自适应步长机制能更快找到合适的参数方向上手难度低。而余弦退火调度比固定学习率更好用的原因是训练后期模型接近最优解附近时过大的学习率容易导致参数在最优解周围震荡余弦退火能平滑地把学习率降到很低的数值让最后几个阶段的训练更精调。训练轮次上常规设置是100到150轮。高光谱数据集规模小特征模式相对固定并不需要像ImageNet那样训练几百轮。我在农场景数据上测试过50轮时精度大约能到89%100轮时上升到92%左右150轮以上基本稳定在93%附近再继续训练提升空间很小反而可能开始过拟合。所以我把默认轮次定在120配合早停策略也就是验证集精度连续15轮不再提升时保存当前权重并停止训练。4.2 正则化与小样本对策小样本是高光谱分类最突出的痛点。我做过一个极端实验每类地物只给15个训练样本模型直接崩溃总体精度只有61%。后来我引入了组合拳式的正则化方案把这个数据条件下的精度拉回到78%左右。第一招是数据增强。高光谱图像的相邻空间块之间本身有很强的相似性所以常规的图像旋转和翻转同样适用。我每次训练时以50%的概率对每个输入块做水平翻转和垂直翻转以25%的概率做90度旋转。要注意这些空间变换只作用于空间维度不改变波段顺序否则会破坏光谱连续性。第二招是Dropout。我在全连接层之前加入dropout概率设0.5在二维卷积层之间加概率为0.3的dropout。实践证明深层网络在全连接层附近最容易过拟合这句老话在高光谱里同样成立。第三招是标签平滑。高光谱数据集中不同类别之间光谱相似度可能很高比如两类植物都是绿色的它们的光谱曲线差异很小。模型如果过于自信地将概率分布推向0和1反而会让损失值波动剧烈。标签平滑把one-hot目标改成类别间的软分布能有效抑制这种过度自信增强泛化。我设置平滑系数为0.1在小样本实验里确实能看到验证集精度稳步提升。4.3 评估指标与消融实验设计很多初学者只报一个总体精度这在高光谱分类里是不够的。我一般同时关注总体精度、平均精度和Kappa系数。总体精度看的是全局正确的比例平均精度看的是每个类别单独的精度再取平均Kappa系数则排除随机分类的影响。这三个指标组合起来才能比较全面地反映模型性能。在这个项目里我做了一系列消融实验用来验证每个模块到底有没有用。实验设计如下基线模型仅含两个三维卷积加二维卷积结构不含注意力。基线残差连接在三维卷积块中通过add操作加入输入跳跃连接。基线波段注意力加入注意力模块。完整模型同时加入残差和注意力模块。实验结果显示在相同训练条件下完整模型比基线模型在总体精度上高出1.8个百分点平均精度上高出2.4个百分点。单独去掉注意力模块后精度下降0.9个百分点单独去掉残差连接后精度下降0.3个百分点。这说明注意力机制带来的增益主要集中在对光谱特征的筛选上而残差连接更多是稳定训练。5. 实操过程与核心环节实现5.1 实验流程与数据划分细节整个训练流程我通常按照下面的步骤来走每步都有必须注意的细节。第一步加载数据并完成预处理。将原始数据读取为(height, width, bands)的三维矩阵然后把像素值除以最大值完成归一化。接着统计所有标注像元的坐标按类别建立索引。第二步划分训练集、验证集和测试集。划分是在像元级进行的不是影像级。为了保证随机性我先对每个类别的所有像元编号再按照比例随机抽取。常用比例是每类抽取30%作为训练集20%作为验证集其余50%作为测试集。在小样本场景中训练集比例会降低到每类15到30个样本。另外划分时需要固定随机种子否则每次实验结果都会浮动不利于调参。第三步在线采样图像块。每次训练迭代时从训练集索引中随机选择一批像元位置从其周围取出对应的邻域图像块。这里要重点注意图像块一定是从原始图像上切出来的不能使用已经做过全局增强的数据否则验证集和测试集相当于被污染了。第四步构建模型并初始化权重。网络初始化我直接用PyTorch默认的初始化方式没有额外做特殊初始化和预训练。第五步开始训练循环。每个epoch中遍历若干batch的训练数据计算交叉熵损失反传更新参数。每轮结束后在验证集上计算总体精度保存验证集精度最高的模型权重。第六步测试阶段。加载最优权重在测试集上整体推理一遍计算三组评估指标并生成整个场景的分类图。分类图是逐像元预测得到的因为我训练时用的是像元周围的邻域块所以测试时对全图每个像元也都要切邻域块边缘像元仍然用镜像填充处理。5.2 典型结果对比与解读在农场景数据集上我最终的完整模型取得了总体精度93.2%、平均精度92.6%、Kappa系数0.91的成绩。城市场景数据集上因为标注区域更复杂、类别更多总体精度稍低一些大约为91.5%。作为对比我在同样数据和划分条件下跑了一个带高斯核的支持向量机模型总体精度分别只有83.1%和81.4%。这个结果说明在不做任何精细特征工程的情况下深度学习模型凭借自动特征提取能力已经能稳定碾压传统方法。看一眼类别层面的准确率细项更能量化差距。在农场景数据中深度学习模型对植被类别的识别精度普遍较高对阴影区域和农作物残茬的识别精度低一些。这恰好对应两类困难样本阴影区的光谱亮度整体偏低相邻地物光谱容易混淆农作物残茬与浅色土壤的光谱形状非常接近只靠光谱曲线根本分不开必须利用空间纹理。这也再次说明空间邻域特征的重要性。分类图的后处理同样值得一提。直接逐像元预测的分类图通常会出现椒盐噪声也就是单个像元孤立地属于某一类别零散地分布在其他类别中。在传统工作流里人们会用中值滤波或者多数投票来平滑这类噪声。在深度学习输出后我用一个5×5大小的中值滤波对预测结果做轻量后处理分类图的目视效果改善明显总体精度也提升了约0.2个百分点。如果某些小类地物的面积本身就很小中值滤波可能会抹掉它们所以是否使用后处理要结合应用需求。对需要精细化面积统计的任务我建议保留原始预测输出对需要生成可视化专题图的任务则可以适当做平滑。5.3 环境配置与显存优化记录项目实验跑在一块12G显存的显卡上。高光谱图像数据不大但中间特征图在三维卷积阶段会快速膨胀不经优化很容易爆显存。我第一次把全部波段放入网络时batch size设为32在三维卷积块后特征图体积达到了接近6G导致训练直接中断。为了解决显存问题我做了三处调整。第一将空间输入尺寸从15×15缩小到11×11这一步直接降低了特征图的空间体积。第二将batch size从32减到16再把梯度累积步数设为2它等价于仍按batch size 32的规模更新梯度但显存峰值明显降低。第三对波段数据先做了PCA降维降到30个波段再送入网络特征图体积进一步缩小。经过这三步调整显存峰值稳定在8G左右训练过程不再中断。这里补充一个判断实验资源合理性的心法如果一个深度学习训练任务的显存占用超过显卡总容量的90%说明网络结构和输入尺寸配置可能太冗余了。在高光谱分类这种任务里输入块本身就小一味的堆叠大卷积核和高通道数并不会带来线性提升不如先保证模型能稳定跑完一轮训练再逐步增加容量。6. 常见问题与排查技巧实录6.1 训练时损失不下降或者直接变成NaN这个问题出现的频率很高尤其是在使用自定义数据读取器时。最容易导致NaN的三个原因分别是数据归一化失败、学习率过大、标签中有负值或者类别编号跳跃。我曾遇到过一次因为原始数据中某个波段存在无效值0在归一化后仍然保留为0而未做掩膜处理导致计算损失时出现log 0的情况。排查方式很简单每次训练前打印一批样本值的分布范围确认在0到1之间、没有无效值。另一个容易忽略的点是损失函数与模型输出的匹配。如果使用交叉熵损失函数模型最后一层不要额外加Softmax激活交叉熵在计算时将LogSoftmax和负对数似然合并了。我调试时一度加上Softmax结果损失值变得很大且训练缓慢去掉之后就恢复正常了。6.2 验证集精度高测试集精度低这通常意味着实验流程中存在数据泄漏或者过拟合。最典型的泄漏场景是样本划分前对全图做了归一化统计了全图的最大值和最小值。另一个容易泄漏的地方是数据增强如果切好训练块之后先做了全局增强再划分训练验证测试同一个原始像元可能同时出现在训练集和测试集的变换样本中。我自己踩过的坑是在代码里不小心把所有样本一次性做了随机旋转增强导致验证集精度虚高。解决办法是先划分数据集再在训练循环内部在线做增强。如果确实没有泄漏那就要怀疑模型容量过大了。高光谱数据标注样本有限一个大规模网络在验证集上表现好但在测试集上表现差说明验证集太容易过拟合。可以降低网络层数或者把dropout概率提高甚至直接缩小输入块的空间尺寸让模型更难记住细节。6.3 训练过程波动大精度忽高忽低高光谱分类任务里训练波动是很常见的大多数情况是因为某些类别的训练样本太少导致随机采样的batch分布不稳定。比如一个batch里如果恰好没有包含某个稀有类别反向传播时这个类别的特征就得不到更新下一轮batch里又突然出现梯度方向就会来回震荡。我会在采样器上做文章启用类别平衡采样。也就是每个batch中尽可能均匀地从各个类别抽取样本而不是完全随机地全局采样本。使用分类器后训练损失曲线会更平滑精度波动幅度会从5个百分点缩小到1个百分点以内。另外一个补救手段是提高学习率的warm-up阶段前5个epoch线性增加学习率让模型在最开始阶段不要被噪声极性样本带偏。6.4 小样本过拟合的最后一招迁移学习和自监督预训练当我每类训练样本只有20个左右时即使做了数据增强、正则化和平衡采样模型精度依然有限。这时候我试了迁移学习路线先在波段数量相近的其他高光谱数据集上进行预训练再在目标任务数据上微调最后一层分类器。这个策略有实际效果但限制也很明显必须是同一个传感器或者波段数量相近的数据集才有可比性。另一个我尝试过且效果更好的策略是自监督预训练。思路很简单随机掩膜部分波段让网络学习重建被掩膜的波段。这个过程完全不依赖标注样本可以用全场景的无标签数据来训练。之后再让重建网络承担分类任务的特征提取器微调分类头。这个方向目前在高光谱圈子里还比较前沿但它确实能在极端小样本场景里提升5个百分点左右。如果条件允许后续可以专门做一期内容展开说说。个人实践经验分享做这个项目最大的体会是高光谱分类的难点从来不是模型本身而是如何在小样本、高维度、光谱与空间信息混杂的条件下保持稳定。我的建议是第一次上手时不要急着堆花哨模块先把基线跑通再逐步验证每个模块的增益。我自己最满意的一步是波段注意力模块它成本低、概念清晰而且解决了实际痛点。后续如果项目有更多需求可以考虑把归一化方式改成反射率级别的物理校正或者引入Transformer结构的序列模型从全局角度建模光谱曲线那应该又是一个有意思的新方向。希望这份实战经验能帮你绕开我踩过的坑。