简介面向深度学习与遥感图像处理场景的高光谱图像分类实践代码包基于卷积神经网络构建一维光谱、二维空间及三维谱空联合特征学习模型帮助研究者掌握从数据预处理到模型训练评估的完整流程。资源共八个文件包含四个Python脚本和四个数据文件压缩后约八十七点四兆字节。脚本分别对应一维、二维、三维卷积网络并包含数据加载与辅助功能数据文件提供两组公开高光谱数据集及其地面真实标签可直接用于实验验证。项目详细介绍了在TensorFlow-GPU与Keras框架下的实现思路涵盖归一化、降噪、混淆矩阵等关键处理环节可支撑课程设计、论文复现或算法对比。已有三千八百三十九人学习下载适合有一定深度学习基础、希望在高光谱图像分类方向快速上手的中高级学习者。1. 高光谱图像分类把上百个波段交给卷积神经网络之前先搞懂它的脾气高光谱图像分类跟普通图像分类完全是两个世界。普通照片每个像素只有RGB三个数值高光谱遥感数据每个像素有上百个波段体积大到能把显存塞爆标注却往往只有几千个像素。我之前拿200波段的影像去套普通分类网络结果显存爆了训练集精度99%验证集精度却原地不动。后来按像素邻域切patch用轻量卷积神经网络重新设计输入通道才把整体精度稳定在93%以上。这套Python工程覆盖了从数据加载、patch采样、CNN建模到分类图生成的完整链路适合刚入门遥感深度学习、想做像素级地物分类实验的开发者。2. 技术栈与数据结构为什么选PythonPyTorch目录里每个文件是干嘛的2.1 高光谱数据的基本形态一张图就是一组连续光谱曲线我在上手之前先花了半天把数据格式摸明白。高光谱图像本质上是一个三维数组形状是高H宽W波段数C。以常见的航空高光谱基准数据为例去掉噪声和水汽吸收严重的波段之后实际参与分类的波段数大约在200个左右。每个波段记录的是地物在某一波长的反射率所以每个像素都对应一条完整的光谱曲线地物类别不同曲线的波形就不同。这意味着分类的核心依据不是“长什么样”而是“光谱指纹”。普通图像分类里那种把整张图缩放到224×224再丢进ResNet的做法在高光谱任务里有两个明显问题一是输入通道数对不上二是整图分类的语义假设不成立。高光谱像素级分类的标准做法是逐像素决策但单看一个像素的频谱又容易受噪声干扰所以就有了patch采样——把目标像素周围一个邻域一起送入网络让CNN兼顾光谱和空间上下文。标签信息通常是一张与影像同样尺寸的整数矩阵每个整数值代表一个地物类别0一般表示未标注背景。整个训练数据量往往只有几千到一万个有效像素这决定了模型不能太深、数据增强要有节制。理解了这几点再去看代码就不会迷路。2.2 目录结构与职责划分这个工程的目录结构很干净。我看代码的习惯是先列一遍文件清单搞清楚数据和模型放在哪里然后再跑train.py。下面是项目压缩包解压后的目录组织方式hyperspectral_cnn/ ├── config.py # 全局配置波段数、patch_size、类别数、训练超参 ├── dataset.py # 读取.mat数据、标准化、patch采样、训练验证集划分 ├── model.py # CNN网络定义 ├── train.py # 训练入口脚本 ├── predict.py # 加载权重生成整图分类结果 ├── utils.py # 通用工具指标计算、可视化、日志 ├── data/ # 原始影像和标签的.mat文件放在这里 └── output/ # 训练好的模型权重和分类图输出目录config.py是所有参数的集中地改patch大小、学习率、batch size都从这里下手。dataset.py负责把.mat文件读成numpy数组再按像素采样。model.py是后面要重点讲的卷积网络。train.py组织训练流程predict.py在训练完成后生成整幅高光谱影像的彩色分类图utils.py里则是OA、AA、Kappa这三个评估指标的计算函数和可视化工具。压缩包里也附了一份说明文档把每个文件的职责和数据格式的约定都写在里面我照着它配环境没有遇到障碍。2.3 环境依赖版本不对会踩到隐形的坑项目的开发语言是Python核心依赖只有numpy、scipy、matplotlib、torch、torchvision这几样没有奇奇怪怪的第三方库。我用conda建了独立环境Python版本选的3.10实际跑下来没什么问题conda create -n hypesian python3.10 conda activate hypesian pip install numpy scipy matplotlib torch torchvisiontorch和torchvision的版本建议选配套的最新稳定版或者至少1.13以上因为训练脚本里用了自动混合精度老版本对AMP的支持不完整。scipy要特别注意如果你手里的.mat是v7.3格式scipy.io.loadmat会直接报错需要换用h5py读取。怎么判断是不是v7.3格式用文本编辑器打开.mat文件开头如果是“MATLAB 5.0 MAT-file”字样就是旧格式loadmat能读如果是HDF5格式的二进制就得走h5py。这条后面避坑章节还会再展开。3. 核心模块拆解一个patch从磁盘到CNN输出的完整链路3.1 数据加载读.mat、转float32、逐波段z-score标准化dataset.py的第一步是把.mat影像和标签读进内存。高光谱数据文件通常几十MB到上百MB读取和类型转换这里就要开始精打细算。我在改这个工程时把原始代码里的float64全部换成了float32显存占用直接减半训练速度也快了不少。# dataset.py 数据加载部分 import numpy as np from scipy.io import loadmat def load_hsi_data(data_path, label_path): # 高光谱原始数据shape为(H, W, C) img loadmat(data_path) data img[data] # 标签矩阵shape为(H, W)0表示背景 gt loadmat(label_path) labels gt[label] # 用float32存储高光谱数据体量大float64会白白多占一倍内存 data data.astype(np.float32) # 逐波段做z-score标准化每个波段独立统计均值和方差 means data.mean(axis(0, 1), keepdimsTrue) stds data.std(axis(0, 1), keepdimsTrue) stds[stds 0] 1e-6 # 防止全零波段导致除零 data (data - means) / stds return data, labels这里的标准化一定要按波段分别算不能把整个三维数组展平成一个标量均值。原因很简单不同波段的能量差异很大数值范围宽的波段如果不在量纲上拉齐训练时梯度会被它们主导模型很难真正学到光谱特征。代码里stds为0时补了一个1e-6处理的是某些数据集的噪声波段全为同一个值的情况这类波段后续在预测阶段基本贡献也接近零。3.2 Patch采样让CNN看到的不止是“一个像素”单像素分类很容易被噪声带偏高光谱任务里最实用的补救办法是取邻域patch。对每个带标签的像素截取它周围5×5的小方块连同中心像素一起作为训练样本。patch_size在config.py里设置我改成7×7试验过精度略有提升但显存压力变大5×5是公认性价比比较高的默认值。# dataset.py patch采样部分 def sample_patches(data, labels, patch_size5): H, W, C data.shape pad patch_size // 2 # 用reflect模式padding图像边缘不会出现生硬的黑色边界 data_pad np.pad( data, ((pad, pad), (pad, pad), (0, 0)), modereflect ) patches, targets [], [] for i in range(H): for j in range(W): if labels[i, j] 0: # 跳过背景像素 continue patch data_pad[i:i patch_size, j:j patch_size, :] patches.append(patch) targets.append(labels[i, j]) # 最终得到(N, patch_size, patch_size, C)在训练前转成(N, C, H, W) return np.stack(patches), np.array(targets)这段循环是初版实现逻辑最清晰适合先把流程跑通。但H和W的嵌套循环在Python里比较慢如果影像尺寸是145×145还勉强能接受遇到上千×上千的影像就会很吃力。我后面把采样改成了向量化版预先用as_strided生成所有滑动窗口的索引表再一次性取数速度提升了大概一个数量级。你可以先跑通再优化不必一步到位。另外注意这里用的是reflect padding而不是zero padding影像边缘的像素不会凭空多出一圈“黑色假区域”生成的分类图边缘也更平滑。3.3 模型定义三层卷积的轻量方案高光谱样本量小网络太深必过拟合。这个项目里用的是三层3×3卷积加一层全连接结构精简但符合这类任务的普遍经验。# model.py 网络定义 import torch.nn as nn class SpectralSpatialCNN(nn.Module): def __init__(self, in_channels200, num_classes16): super().__init__() self.features nn.Sequential( # 第一层200个波段作为输入通道 nn.Conv2d(in_channels, 16, kernel_size3, padding1), nn.BatchNorm2d(16), nn.ReLU(inplaceTrue), # 第二层16 - 32 nn.Conv2d(16, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), # 第三层32 - 64 nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue) ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d((1, 1)), # 把任意尺寸patch池化为1×1 nn.Flatten(), nn.Linear(64, num_classes) ) def forward(self, x): return self.classifier(self.features(x))关键设计有三个。一个是in_channels等于波段数不需要预训练权重从零开始训练即可。二是每层卷积后面紧跟BatchNorm和ReLU高光谱数据虽然做了z-score标准化但网络内部的特征分布还是会漂移BN在这里承担了平滑训练的作用。三是最后的AdaptiveAvgPool2d把特征图压成1×1这让模型不依赖输入patch的具体尺寸推理阶段即使换patch大小也能跑通。全连接层只用了一层因为数据量本身就不大层数再深就是拿论文里的通用网络硬套效果反而差。3.4 训练循环交叉熵、Adam和验证集存档训练脚本逻辑很直白dataloader按batch送数据每个batch算交叉熵损失反向传播更新参数。我用Adam代替了原始配置里的SGD因为Adam对学习率的敏感度低默认1e-3就能收敛不用像SGD那样花时间找学习率。# train.py 训练核心循环 criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) for epoch in range(epochs): model.train() total_loss 0.0 for batch_x, batch_y in train_loader: batch_x batch_x.permute(0, 3, 1, 2).cuda() # (N,H,W,C)-(N,C,H,W) batch_y batch_y.cuda() optimizer.zero_grad() out model(batch_x) loss criterion(out, batch_y) loss.backward() optimizer.step() total_loss loss.item() # 每轮结束后在验证集上算一次OA保存最佳权重 model.eval() with torch.no_grad(): val_oa evaluate(model, val_loader) if val_oa best_oa: best_oa val_oa torch.save(model.state_dict(), output/best_model.pth)weight_decay设成1e-4是给全连接层和卷积核加一点L2约束对抗过拟合。每个epoch在验证集上计算一次总体精度OA只有刷新了纪录的权重才会被保存这是训练深度学习模型的基本习惯——最后一个epoch的模型不一定最好中途往往就有更优的拟合状态。permute这行容易漏高光谱patch在读进来时是(H,W,C)布局PyTorch卷积要求(C,H,W)维度顺序错了会直接报错或者训练出莫名其妙的结果。4. 复现实验从命令行参数到一张完整的分类图4.1 数据准备与目录检查把数据集文件放进data/后第一件事是先把config.py里的参数过一遍。主要看三个值in_channels是否等于数据文件的波段数num_classes是否等于标签矩阵里非背景类别的数量patch_size是否合理。如果这三个值不对训练不会崩但验证集精度会一直上不去属于最难排查的隐性问题。训练前推荐先跑一个冒烟测试直接把epoch设成1batch_size设成4确认数据能完整走一遍前向和反向再开始正式的长时间训练。这样能避免训练了几轮才发现维度错误浪费时间。4.2 训练命令与关键参数环境准备好之后一条命令就能起训练。下面是项目训练入口的标准用法python train.py --patch_size 5 --epochs 200 --batch_size 128 --lr 1e-3各参数的作用整理成了一个表格方便调试时对照参数作用推荐值备注--patch_size每个样本的邻域尺寸5增大能提升精度但显存占用按平方增长--epochs总训练轮数200小样本任务100~300足够--batch_size每批样本数128显存不够就降到64或32--lr学习率1e-3Adam下这个值比较省心训练过程中观察loss曲线和验证集OA的输出。正常情况是loss先快速下降再缓慢收敛验证OA伴随波动上升。如果loss下降但OA纹丝不动说明采样或者标签对不上如果OA不到0.8就开始震荡就要怀疑是模型过深或者学习率大了。4.3 推理与分类图生成训练结束后predict.py会加载output目录下保存的最佳权重对整张影像逐像素做预测。推理时同样要构造邻域patch但不再跳过背景而是把整幅图所有像素都预测一遍最后用matplotlib把预测标签映射成彩色图输出。python predict.py --checkpoint output/best_model.pth --output output/prediction.png跑出来的分类图可以直观检查模型行为。我习惯把预测图和真实标签图并排放在一起看地物边界是否锐利、同一区域是否出现椒盐噪声、类别分布是否合理。这些视觉信号比单纯看精度数字更敏锐比如验证集OA有94%但整图预测里背景区域被大面积误判成某个地物类那多半是训练时背景像素没有参与采样导致的。4.4 三个评估指标OA、AA、Kappa分别在看什么实验类项目最容易被追问的就是指标。这个工程在utils.py里实现了三个经典指标训练日志里会逐个打印。OA是总体精度所有预测正确的像素数除以总像素数最简单也最常用。AA是类别平均精度先按类各自算精度再取平均防止样本量大的类把整体精度拉得太好看。Kappa系数衡量的是预测结果和随机猜测之间的一致性数值越高代表分类结果越可靠。# utils.py 指标计算 from sklearn.metrics import accuracy_score, cohen_kappa_score def compute_metrics(pred, gt): # pred和gt需要提前展平成同长度的一维数组 oa accuracy_score(gt, pred) # 总体精度 per_class [] for c in np.unique(gt): mask gt c per_class.append(accuracy_score(gt[mask], pred[mask])) aa np.mean(per_class) # 类别平均精度 kappa cohen_kappa_score(gt, pred) # Kappa系数 return oa, aa, kappa三个指标要分开看。我见过不少结果是OA 95%但Kappa只有0.8的情况说明类别分布极端不平衡少数类的精度很差。这种情况下后续改模型的时候损失函数可能都要换不能只看OA这一个数。5. 避坑与排查高光谱CNN训练中的五个实战问题这一章直接总结我在复现和改造过程中踩过的坑每一条都是先看到现象再查原因最后给出解决办法。注意这五条是按我实际踩坑的频率排序的前两条大概率你也会遇到。5.1 全图patch一次性展开内存直接被打爆现象执行train.py后内存占用曲线一路上涨过不了多久进程就被系统杀掉终端没有报错只是Python解释器莫名其妙退出任务管理器里看到内存已经占满。原因把整幅影像的所有窗口一次性np.stack成一个大数组。一幅145×145、200波段的数据5×5的patch展开后是大约两万多个样本每个样本5×5×200个float32值总量约几GB还不是最致命的如果换成更大尺寸的影像直接把内存打爆很正常。解决把采样逻辑改成惰性采样在Dataset的__getitem__里按索引临时取patch或者按行分批采样后用DataLoader的num_workers并行加载。我最终用的是后者内存峰值降到了原来的五分之一以下。5.2 训练到一半loss变成NaN现象训练前几十个epoch一切正常突然某一步loss变成NaN之后再也回不到正常值。原因最常见是高光谱数据里存在全零波段或者反射率极大值标准化时虽然做了std补零保护但反向传播中权重更新过大数值溢出到了inf再变成NaN。其次是学习率偏大Adam虽然省心但在小样本高维输入下仍然有数值风险。解决在标准化之后手动检查一遍每个波段的标准差把std接近0的波段直接删掉同时在训练代码里加一个梯度裁剪nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0)。这两步加完NaN再也没出现过。5.3 验证集OA很高整张分类图却全是椒盐噪声现象验证集上OA过了93%模型表现正常但用predict.py生成的全图分类结果里同一片农田内部有大量被错分成其他类别的小碎点看起来像撒了一把胡椒面。原因验证集采样和整图推理的条件不一致。训练时跳过了背景像素但推理时背景区域也参与了预测模型对背景没有见过同类样本于是把背景区域预测成了和它光谱相近的地物。另外单像素patch太小的时候中心像素的局部噪声被放大也会产生椒盐效应。解决让背景像素参与训练虽然背景不进入损失函数的统计或者权重设得很低但要让模型见过这个类型同时把patch_size从5增大到7模型能看到更大的空间上下文碎点会明显减少。这个小改动对整图结果的可视化观感提升很显著。5.4 类别极端不平衡少数类几乎全被吃掉现象训练日志里OA稳步上升但单独看类别精度标记样本只有几十个的那几类精度接近零全部被预测成了大类别。原因高光谱数据标注本身就稀缺有些类别在整个数据里只有几十个像素。交叉熵损失对样本量大的类别天然偏置少样本类别在梯度更新里几乎没有话语权。解决给损失函数的class_weight加上类别频率的倒数或者对少样本类别做过采样复制几份参与训练。我在config里加了一个use_balanced_sampling开关打开后少样本类别的patch会按比例多采样几次少数类的精度能从接近0拉到70%以上。这一步在高光谱任务里几乎是必做的。5.5 换了一台机器同样的代码跑出不同的精度现象同一份代码、同一个数据集、同样的随机种子在自己机器上OA是93%到另一台型号不同的显卡上变成了90%反复跑几次都不太一样。原因深度学习模型对浮点运算顺序敏感不同GPU的算子实现不同结果会有小范围波动。另外BN层的行为受batch size影响换了显卡如果显存变了、最优batch size也变了精度跟着变是正常的。解决训练开始时固定随机种子torch.manual_seed(0)和numpy.random.seed(0)并在config里声明推荐batch size。如果跨度太大先把BN换成GroupNorm试一下结果会容易复现得多。这类问题本质上是训练流程里的玄学记录环境版本比只记录精度数字更负责。6. 二次开发换数据集、换骨干网络还能怎么改6.1 换自己的数据集先改三个参数再跑一次拿到你自己的高光谱数据后不用改代码结构主要工作是适配格式和数据分布。先把影像导出成(H,W,C)的numpy数组或.mat文件标签导出成整数矩阵然后去config.py里改in_channels和num_classes。数据标准化会自动逐波段执行不需要手动干预。之后要做一次像素分布统计打印每个类别的样本数量看看有没有极端不均衡的类。如果有就把use_balanced_sampling打开否则少数类会像第5章说的那样被大类别吞掉。做完这两步你的数据基本就能被现有训练流程吃了剩下的就是调patch_size和epochs看验证OA的变化。6.2 把backbone换成残差结构或Transformer如果你不满足于三层卷积最常见的改造是给网络加上残差连接。我在第二层和第三层卷积之间加了一条shortcut验证OA提升了约1个百分点收敛也跟着顺滑了一些代码改动量很小class ResidualBlock(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch) ) # 输入输出通道不一致时用1×1卷积对齐 self.shortcut None if in_ch ! out_ch: self.shortcut nn.Conv2d(in_ch, out_ch, 1) def forward(self, x): return F.relu(self.conv(x) ( self.shortcut(x) if self.shortcut is not None else x ))在原有的features序列里嵌套两个ResidualBlock替换掉第二部分的三层卷积就是标准的残差CNN。如果你对Transformer感兴趣也可以把最后一层卷积输出的特征图经过位置编码之后送入一个小的Transformer Encoder但高光谱样本量小Transformer层的数量控制在两层以内否则几乎肯定会过拟合。6.3 一个验证技巧波段选择与消融实验改造完模型之后不要只看OA数字。我一般会做一个小消融把200个波段按重要性排序只保留前50个波段重新训练对比全波段的精度。这样能判断新增的模块到底是在利用有效光谱信息还是在拟合数据噪声。波段重要性可以用随机森林的特征重要性跑一版也可以用光谱之间的距离度量近似估算。这个技巧是我从一次失败里学到的——当时加了一个注意力模块兴奋地去跑实验精度涨了一点但去掉注意力模块重跑一遍精度几乎没变我才意识到涨的那一点来自随机波动不是模块的功劳。从那以后我每次改完模型都会强制自己走一遍消融对比固定随机种子跑三次取平均再决定这个改动要不要保留。希望帮到你。本文还有配套的精品资源点击获取