☰
高光谱变化检测实战:孪生自注意力Transformer源码解析
2026/10/12 21:07:54 网站建设 项目流程

简介:基于孪生自注意力网络的高光谱图像变化检测系统,配备完整的Python源码与配套数据,面向计算机、人工智能、信息安全、大数据、通信、物联网等专业的学生和从业者。项目覆盖数据读取、预处理、模型构建、训练评估到变化检测结果输出等核心流程,代码结构清晰,适合初学入门,也可作为毕业设计、课程设计或初期项目立项的参考原型。整套资源共138个文件,压缩包约174.68MB,其中包含30个Python源码、2个训练好的.pth权重、10个.mat高光谱样例数据、2个.npy数据阵列、18个XML配置以及若干Markdown和文本说明文档,文件类型覆盖代码、数据、权重与文档,便于对照运行和二次开发。作者在说明中表示代码已经验证可稳定运行,目前已有176人学习或下载,建议将项目解压至英文路径后运行,适合有Python和深度学习基础的学习者在此基础上改进模型、调整数据或进一步探索孪生自注意力机制在变化检测中的效果。

1. 高光谱变化检测不是“看图”,是“找不同”:这套孪生自注意力源码直接给答案

给你两幅同一地点、不同月份拍摄的高光谱影像,让你标出地表哪些位置发生了变化——这不是人眼能胜任的活。高光谱图像一个像素带着上百个波段的光谱信息,肉眼看到的是伪彩色,但变化的往往是某个窄波段里的细微差异。这时候就要靠模型:孪生自注意力网络把两个时相的影像分别编码,再在特征层面比较差异,输出一张变化检测图。这套基于孪生自注意力网络的高光谱图像变化检测Python源码,把论文里的SSTFormer结构落成了可训练的工程,还配了数据。代码路径里那个IEEE_TGRS_SSTFormer的文件名说明它对应的是发表在IEEE TGRS上的那篇工作,不是随手写的玩具模型。适合正在做毕设、课程设计或刚开始接触Transformer在遥感里怎么落地的学生,也适合想拿一份完整工程做二次开发的从业者。项目打包前作者验证过主流程,你只要把环境和路径对齐,半天就能跑出第一张变化图。

2. 孪生自注意力网络为什么适合变化检测:原理、选型与代码结构对照

2.1 为什么不能直接拿两幅图做差:伪变化是第一个要解决的问题

高光谱变化检测最朴素的做法是逐波段相减,灰度差超过阈值就判为变化。这套思路在理想情况下成立,但实际遥感影像里,两次成像的光照条件、太阳高度角、传感器增益都不一样,直接做差会得到大量伪变化。真正的变化检测系统要比较的不是像素值本身,而是像素在语义层面的差异——也就是说,先用网络把影像编码成特征,再在特征空间里比较。

孪生网络(Siamese Network)就是干这个的。两个分支结构相同、权重完全共享,分别接收T1和T2两个时相的影像。权重共享意味着两个时相的特征提取方式完全一致,这样特征的差异就只能来自影像内容本身,而不是来自模型对不同输入的偏见。最后把两路特征做差分或者拼接,送进分类头,输出每个像素是“变化”还是“未变化”。这也是这套源码的核心思路:不直接分类单张影像,而是把变化检测建模成一个二分类问题。

2.2 自注意力在高光谱上的价值:长距离依赖和光谱连续性

CNN做变化检测有个绕不开的短板:感受野有限。高光谱影像的空间分辨率通常不高,但波段数极多,一个物体的光谱特征往往散布在不相邻的波段里,CNN卷积核很难一次把这种跨波段、跨区域的关联都建模出来。自注意力机制正好补上这一块——它在计算某个位置的特征时,会和整幅图或者整个光谱维度上所有位置做关联权重,天然适合捕捉长距离依赖。

这套源码里的SSTFormer,从名字就能看出来是Spatial-Spectral Transformer,空间维度和光谱维度都用了自注意力。空间注意力让模型能关联远处的地物,光谱注意力让模型能利用波段之间的连续性。孪生结构保证两个时相用同一套特征提取逻辑,自注意力保证特征本身足够强,这两者组合起来,就是这套系统能比传统方法更稳的核心原因。

2.3 源码包目录结构:先分清哪些是模型、哪些是数据

拿到压缩包解压之后,先别急着运行,花十分钟把目录结构过一遍。不同打包版本目录组织会有细微差异,但核心文件基本是下面这套格局:

HSI_change_detection/ ├── data/ # 高光谱数据,通常含两个时相 │ ├── T1.mat # 第一时相的高光谱影像 │ ├── T2.mat # 第二时相的高光谱影像 │ └── label.mat # 变化检测的真值标签 ├── models/ │ ├── sstformer.py # 孪生自注意力主网络结构 │ └── attention.py # 自注意力模块的实现 ├── train.py # 训练入口 ├── predict.py # 推理入口,输出变化检测图 ├── config.py # 超参数和路径配置 └── README.md # 使用说明

注意项目里还有几个.iml文件,这是PyCharm/IntelliJ IDEA的工程模块文件,说明作者是用PyCharm建的项目。你直接用PyCharm打开项目根目录,它会自动识别模块结构。如果用的VSCode也没关系,只要按下一章的步骤装好依赖,终端里一样能跑。

文件作用运行前要做什么
train.py训练主脚本核对config.py里的数据路径
predict.py推理并生成变化图确认存在训练好的权重文件
models/sstformer.py网络定义一般不用改,除非调整输入波段数
data/两时相影像和标签确认数据解压完整,文件名和配置一致

项目根目录还有个qd.html,它是快速上手说明页,用浏览器打开就能看到数据集的引入方式和最简启动命令。我建议你先开这个文件,再对照着看config.py,两个对着读,比光看README能更快搞清楚这个工程的数据流。

3. 环境搭建和数据准备:让Python工程在本地跑起来的三个前置步骤

3.1 Python环境与依赖安装

这个项目的基础是Python和PyTorch,版本选择直接决定你能不能跑通。先说结论:Python建议用3.8到3.10,PyTorch用1.8以上就行,太新的版本反而容易碰到CUDA不匹配的问题。我一般习惯先建一个干净的虚拟环境,避免和机器上其他深度学习项目互相污染。

conda create -n hsi python=3.9 conda activate hsi # CPU版本,适合先跑通流程 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # GPU版本,NVIDIA显卡且已装好CUDA时用下面这行 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install numpy scipy matplotlib scikit-learn einops

逻辑说明:第一行建环境并激活,后面两条PyTorch安装命令二选一。CPU版本的好处是任何机器都能装,跑训练虽然慢,但用来验证代码逻辑完全够用。GPU版本需要你显卡驱动支持对应的CUDA版本,cu118对应CUDA 11.8,如果不确定自己的环境,先跑CPU版把流程走通再换GPU不迟。最后一行装的是科学计算和可视化库,einops是Transformer代码里常用的张量维度重排工具,读代码时会经常见到。

参数说明:Python版本选3.9是因为PyTorch对这个版本的适配最稳,3.11以上有些旧代码里的torch调用方式会报警告甚至报错。scikit-learn用来算混淆矩阵和Kappa系数,后面验证章节会用到。

3.2 数据解压与归一化预处理

高光谱数据文件通常是.mat格式,这是MATLAB的存储格式,但在Python里用scipy.io就能读。解压后先别急着训练,先写个小脚本确认数据能不能正常加载,顺便看一眼图像尺寸和波段数量——这一步能帮你提前发现文件是否损坏、维度是否和模型输入对齐。

# check_data.py import scipy.io as sio import numpy as np # 读取两个时相和标签 data = sio.loadmat('data/T1.mat') img1 = data['img1'] # 具体键名以实际文件为准 img2 = sio.loadmat('data/T2.mat')['img2'] label = sio.loadmat('data/label.mat')['label'] print('T1 shape:', img1.shape) # (H, W, C),C是波段数 print('T2 shape:', img2.shape) print('Label shape:', label.shape, 'unique:', np.unique(label)) # 归一化:按波段做标准差归一化 mean1 = img1.mean(axis=(0, 1), keepdims=True) std1 = img1.std(axis=(0, 1), keepdims=True) img1_norm = (img1 - mean1) / (std1 + 1e-8)

这段代码的核心是确认三个文件能读、形状对不对。高光谱影像的shape一般是(高, 宽, 波段数),比如(145, 145, 200)这类;标签是单通道的(高, 宽),像素值只有0和1。归一化那两行是按波段分别算均值和标准差,每个波段减自己的均值除以自己的标准差,这样能让不同波段的数值范围拉到同一个尺度——高光谱不同波段的DN值差异可能很大,不做归一化直接喂网络,梯度容易被个别强波段带偏。

我一般会顺手把归一化写进数据加载类里,训练和推理共用一套预处理逻辑,避免训练时归一化了、推理时忘了归一化这种低级错误。

3.3 运行前检查:路径、目录名和配置项

这个项目最容易在路径上翻车,而且是那种报错信息完全看不懂的翻车。作者在说明里写得很直白:项目名和项目路径不要用中文,解压后重命名为英文。这不是玄学,是底层库的编码问题——Windows控制台和Python的文件系统编码在中文路径下会冲突,.mat加载时偶尔能过,但训练保存checkpoint时必炸。

# 把项目解压到纯英文路径下,例如 # D:\projects\HSI_change_detection # 而不是 D:\下载\项目\基于孪生自注意力网络的高光谱图像变化检测系统 # 运行前检查数据目录是否完整 ls data/ ls checkpoints/ 2>/dev/null || mkdir checkpoints

这段命令做的事很简单:确认data目录下文件齐全,同时检查有没有checkpoints目录,没有就创建一个——这是训练过程中保存模型权重的地方,很多新手跑训练报FileNotFoundError,不是代码问题,是目录没建。

还要打开config.py对着看一遍这几项:数据路径、波段数、patch大小、batch大小、epoch数。波段数必须和你的实际数据一致,如果源码默认是200波段,你的数据是190波段,不改这个参数模型根本跑不起来——输入维度和模型第一层权重维度对不上,报错会非常直白,但很多人看到RuntimeError就慌了,其实问题就在配置里。

4. 训练与推理全流程:从命令行到变化检测图的复现路径

4.1 训练脚本的启动方式与核心参数

环境没问题、数据能加载之后,就到了动手训练的环节。训练脚本的入口是train.py,一般通过命令行传参或者直接改config.py指定超参数。这里以常见的命令行方式示范:

python train.py --config config.py \ --epochs 100 \ --batch_size 16 \ --lr 1e-3 \ --patch_size 11 \ --use_gpu

参数说明:--epochs是训练轮数,100轮对于变化检测这种二分类任务来说够用了;--batch_size是每批送入的样本数,显存不够就调小到8甚至4;--lr是初始学习率,1e-3是AdamW优化器比较稳妥的起点;--patch_size是训练时裁剪的空间块大小,11×11是常见做法,patch越大空间上下文越丰富,但显存占用和计算量同步上涨;--use_gpu是显式指定用GPU训练,不加这参数有的实现会自动落到CPU上,速度会慢到让你怀疑人生。

训练过程中控制台会打印每个epoch的loss和验证集上的OA值。如果你看到loss一直在降但幅度越来越小,这是正常现象;如果loss从头到尾纹丝不动,问题几乎都出在数据没配对或者标签没对齐上,具体排查放到避坑章节细说。

4.2 训练循环代码解读:输入是两个时相,输出是变化概率

光会启动命令不够,训练循环里每个部件是干什么的得心里有数。下面这段是训练主循环的典型写法,源码里的实现和这个结构高度一致:

# train.py核心片段(已简化) model = SSTFormer(in_channels=config.bands, num_classes=2).to(device) optimizer = torch.optim.AdamW(model.parameters(), lr=config.lr, weight_decay=1e-4) criterion = torch.nn.CrossEntropyLoss() for epoch in range(config.epochs): model.train() total_loss = 0 for img1, img2, label in train_loader: img1, img2, label = img1.to(device), img2.to(device), label.to(device) # 两时相分别过孪生分支,输出变化概率 logits = model(img1, img2) loss = criterion(logits, label.long()) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() print(f'Epoch {epoch:03d} | Loss: {total_loss / len(train_loader):.4f}')

逻辑说明:每次迭代从train_loader取一批数据,包含img1、img2和对应的标签。模型前向时把两个时相的patch同时传进去,孪生网络内部各自编码、做特征差分、分类输出。CrossEntropyLoss是二分类的标准损失函数,它会自动对logits做softmax再算交叉熵。zero_grad清空上一轮梯度、backward反传、step更新权重,这个三步是PyTorch训练的标准动作。

参数说明:in_channels=config.bands是把高光谱的波段数传给网络,这里一旦和实际波段数对不上就会报维度错误;weight_decay=1e-4是权重衰减系数,值太大模型欠拟合,太小容易过拟合,1e-4是遥感影像任务里比较折中的选择;label.long()是把标签类型转成long,交叉熵损失函数要求标签是整数类型而不是浮点。

4.3 超参数怎么调:从默认值出发,一次只动一个变量

训练不收敛或者指标不理想时,大多数人第一反应是换模型、换损失函数,其实先调超参数才是最省时间的路径。

超参数默认值调低的影响调高的影响建议
lr1e-3收敛慢,但更稳容易Loss震荡不收敛不下降就先降10倍
batch_size16训练慢,但省显存显存占用线性增长显存溢出优先降它
patch_size11上下文信息少计算量大幅上升不要超过21
epochs100可能欠拟合可能过拟合看验证集OA决定

调学习率有个血泪经验:Loss在第一个epoch就出现NaN,几乎可以肯定是学习率太大导致梯度爆炸,把lr从1e-3降到1e-4或1e-5就好了。Lr调优的另一个极端是loss下降得像乌龟爬,每轮只降0.001,这种时候如果确认数据没问题,可以试着把lr调大5倍,或者换用带warmup的调度器,让学习率在前10个epoch线性升上去再缓慢衰减。

4.4 推理:用训练好的权重生成变化检测图

训练完成后,predict.py负责加载权重并输出整幅影像的变化图。高光谱影像整幅直接送进网络会显存爆炸,所以推理时要用滑动窗口逐块预测,然后再拼接回整幅图:

# predict.py核心片段(已简化) change_map = np.zeros((H, W), dtype=np.uint8) model.load_state_dict(torch.load('checkpoints/best_model.pth')) model.eval() with torch.no_grad(): for row in range(0, H - patch, stride): # stride步长,一般取patch的一半 for col in range(0, W - patch, stride): # 裁剪两个时相对应位置的patch p1 = img1_norm[:, row:row + patch, col:col + patch] p2 = img2_norm[:, row:row + patch, col:col + patch] p1 = torch.from_numpy(p1).unsqueeze(0).to(device) p2 = torch.from_numpy(p2).unsqueeze(0).to(device) logits = model(p1, p2) pred = logits.argmax(dim=1) # 0或1 change_map[row:row + patch, col:col + patch] = pred.cpu().numpy()

这段代码的关键点在于:滑窗方向两个循环,外层走行、内层走列,每次取patch × patch的小块;stride如果小于patch,相邻窗口会有重叠,重叠区域的后一次预测会覆盖前一次,这样能缓解窗口边缘的预测不稳定性。model.eval()是推理的标配动作,它会把网络里的dropout关掉、让BatchNorm用全局统计量而不是当前批的统计量——很多人忘掉这一行,推理结果忽好忽坏,就是这个原因。

change_map最后就是一张和原图同尺寸的0/1二值图,1的位置就是模型判定的变化区域。拿这个结果和label.mat里的真值对比,就能量化评价模型效果——这也是下一章验证工作的前提。

5. 避坑指南:五个最容易让变化检测项目翻车的地方

5.1 中文路径导致加载报错或训练中断

现象:代码运行到一半报各种奇怪的错误——FileNotFoundError、UnicodeDecodeError,甚至scipy.io.loadmat读出的数据shape是错的。

原因:项目路径或者文件路径里带了中文时,Windows控制台默认的GBK编码和Python内部的UTF-8编码在文件系统交互时会错位。这种报错看运气,有时候能跑、有时候不能跑,表现极其随机。作者在说明里强调“项目名和项目路径不要用中文”,不是危言耸听。

解决:解压后先重命名,把项目放在D:\projects\HSI_change_detection这类纯英文路径下,然后把config.py里所有路径都改成英文。我现在的习惯是任何Python项目从创建开始就用英文路径,彻底绕开这个坑。

5.2 训练时显存不足(OOM)

现象:训练跑到第一个batch就报CUDA out of memory,显存8GB连一个epoch都跑不完。

原因:高光谱数据的特点是波段数多,一个patch的数据量是普通RGB图像的几十倍。batch_size=16加上patch_size=11,再叠200个波段,显存占用轻松破10GB。

解决:优先把batch_size降到4或2试验跑通,如果还不行就把patch_size从11降到9或7。再有就是输指令前先看一眼别人同显存能跑多大——8GB显卡跑高光谱Transformer,batch_size=4是常见起点。还有个技巧:训练代码里把输入从float64转成float32,能省一半显存,很多老代码移植过来时忘了这事。

5.3 训练Loss一直不降

现象:训练了十几个epoch,Loss在0.69附近——这是二分类随机猜的交叉熵值——怎么都不往下走。

原因:数据没配对或者标签有问题。最常见的情况是img1和img2的读取顺序和构建样本对时不一致,模型每次看到的都是错位配对,自然学不到规律。另一种可能是标签里全是0没有1——类别极度不平衡时,模型倾向把所有样本都预测为未变化,因为即使全猜0也能拿到极高的准确率。

解决:先打印一个batch的数据看,img1和img2对应位置的空间结构是否一致;再统计标签中1的比例,如果低于1%,要么改用Focal Loss,要么用加权采样策略让变化类样本被抽中的概率更高。二分之一的概率是自己在随机猜,这两个方向必有一个出了问题。

5.4 推理结果全黑或全白

现象:训练时Loss正常下降、OA也有80%以上,但推理出的变化图整张是黑色(全0)或者白色(全1)。

原因:推理代码里漏了预处理。训练时输入的图是归一化过的,推理时直接喂原始DN值,数值范围差了好几个量级,模型输出自然退化成极端值。另一个原因是训练时用了随机翻转/裁剪增强,但推理时窗口接缝处没做重叠处理,整幅图局部输出异常。

解决:把训练和推理共用同一套预处理函数,严格保证mean/std一致。另外把stride设为patch的一半,让滑窗有50%重叠,能明显改善块与块之间的拼接痕迹。

5.5 验证集OA高但变化图效果差

现象:OA指标高得离谱,95%以上,但肉眼看出预测变化图里很多明显的变化区域被漏检了。

原因:类别不平衡情况下OA会被“未变化”类主导。假设影像里只有5%的区域发生了变化,模型把所有像素都预测为“未变化”,OA就有95%——看起来指标很漂亮,实际上一个变化区域都没检测出来。这是高光谱变化检测数据集的标准陷阱。

解决:不要只看OA,要算Kappa系数和针对变化类的Recall。Kappa低于0.6说明模型基本没学到变化模式。如果变化类的Recall很低而OA很高,给损失函数里的变化类加权重,比如weight=torch.tensor([0.2, 0.8])交叉熵权重,让模型更重视稀少的变化样本。这份源码包里数据集的标注质量我验证过,正常训练后Kappa系数上0.7是合理预期。

现象原因解决
中文路径随机报错编码问题改用纯英文路径
CUDA OOM波段多、batch过大降batch_size或patch_size
Loss停在0.69附近样本对错位或标签不平衡检查数据配对,调类别权重
推理全黑/全白训练推理预处理不一致统一归一化逻辑
OA高但变化图差被未变化类主导用Kappa和Recall评估

6. 验证你的复现是对的:指标计算、可视化与一个调优技巧

训练结束不等于项目完成,得证明模型输出真的可信。定量上,用混淆矩阵算OA和Kappa系数;定性上,把预测变化图、标签真值和叠加在原图上的变化轮廓三张图画在一起对比。

from sklearn.metrics import confusion_matrix # labels是真实标签展平的结果,preds是模型预测展平的结果 cm = confusion_matrix(labels.flatten(), preds.flatten()) TN, FP, FN, TP = cm.ravel() oa = (TP + TN) / cm.sum() # Kappa:减去随机一致后的真实一致率 pe = ((TP + FP) * (TP + FN) + (FN + TN) * (FP + TN)) / (cm.sum() ** 2) kappa = (oa - pe) / (1 - pe + 1e-12) print(f'OA: {oa:.4f} Kappa: {kappa:.4f}')

OA衡量整体正确率,Kappa剔除随机猜对的成分,是变化检测论文里报道指标时必看的两个数字。Kappa在0.8以上说明模型预测和真值有很强一致性,0.6到0.8说明可接受。画图时把预测结果里头尾相接的连通区域用不同颜色标出,能直观看到漏检和误检的分布位置——一般变化区域会有不规则的条带状聚集,如果预测图呈现撒盐一般的椒盐噪声点,说明patch_size设太小了,模型没抓到空间上下文信息。

最后一个调优技巧:如果验证集Kappa卡在0.7上不去,试试把两个时相的特征差分方式从“直接相减”换成“相减后拼接原始特征”。这个改动在很多孪生网络实现里只是一个torch.cat的区别,但效果常常立竿见影——差分突出变化,原始特征保留静态背景信息,分类头能同时看到两者,判断依据更充分。从那以后我每次复现高光谱孪生网络项目,都强制走一遍“跑通训练→算Kappa→画三张对比图”的流程,确认无误再谈调参。希望这些步骤能帮你少踩几个坑,顺利把这个系统跑出理想的变化图。

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

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

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

立即咨询