简介本资源为基于孪生自注意力网络的高光谱图像变化检测系统面向计算机、人工智能、通信、遥感等专业的在校学生、教师及企业研发人员可用于课程设计、毕业设计、大作业或初期项目立项演示帮助理解高光谱变化检测的深度网络建模思路。压缩包共138个文件约174.68MB包含30个py源码、54个pyc编译文件、18个xml配置、10个mat数据、2个pth权重、2个npy数组及若干png、md、html等覆盖模型定义、训练推理、数据加载与结果可视化等模块。项目代码完整且功能验证通过运行稳定已有176人学习下载。读者可据此掌握孪生自注意力网络的结构设计与高光谱数据处理流程并在此基础上进行二次开发替换数据集或改造网络结构以适配其他变化检测任务。需注意解压后路径与项目名不要含中文建议重命名为英文后再运行。1. 孪生自注意力网络做高光谱变化检测从一份 python 源码和数据包说起高光谱图像变化检测这件事真正难的不是把两张图相减而是同一地点、不同时间拍到的光谱曲线会因为光照、大气、物候发生整体漂移直接做差值会把大量伪变化算进来。孪生自注意力网络这套方案就是拿两个共享权重的分支分别提取双时相特征再用自注意力在空间和光谱维度上重新分配权重让真正发生地物替换的区域被放大、让整体辐射差异被压下去。这份 python 源码加数据包落地的正是一个可训练、可推理、可出变化图的完整链路适合已经会 python、想从传统 CVA、IR-MAD 往深度学习迁移的遥感或图像处理从业者。下面按「数据怎么组织、网络怎么搭、训练怎么调、坑在哪」把这条路走一遍读完你能自己复现一版并判断它值不值得投入。2. 数据准备与双时相样本构造高光谱变化检测的地基2.1 高光谱数据的特点与常见数据集形态高光谱图像和普通 RGB 最大的区别是波段数常见机载传感器能到一百到两百多个波段每个像素是一条连续光谱曲线。变化检测任务里输入是同一区域两个时相的高光谱立方体输出是一张二值或概率变化图。业内常用的公开数据有 ICVL 高光谱数据集、Pavia、Indian Pines 这类做变化检测时更常见的是成对的双时相数据比如同一区域两个年份的航拍高光谱。你拿到的数据包如果是 .mat 格式基本就是 MATLAB 存的多维数组python 用 scipy.io.loadmat 就能读。数据组织上要盯三件事两个时相的空间尺寸必须严格对齐波段数和波长范围要一致标签图是单通道 0/1 掩膜。很多新手翻车就翻在这里——两个时相分辨率差一点或者一个做了辐射定标另一个没做网络再强也救不回来。我一般会先写个检查脚本把 shape、波段数、数值范围、标签正负样本比例全打出来确认没问题再进网络。2.2 用 python 读取 .mat 并构造样本对下面这段是读取和构造双时相样本的最小可用代码假设数据包里是 T1.mat、T2.mat、label.mat 三个文件。import scipy.io as sio import numpy as np # 读取双时相立方体和标签squeeze 去掉多余维度 t1 sio.loadmat(T1.mat)[data].astype(np.float32) # (H, W, B) t2 sio.loadmat(T2.mat)[data].astype(np.float32) # (H, W, B) label sio.loadmat(label.mat)[label].astype(np.uint8) # (H, W) print(T1 shape:, t1.shape, T2 shape:, t2.shape, label shape:, label.shape) print(波段数:, t1.shape[2], 变化像素占比:, label.mean()) # 逐波段归一化避免量纲差异主导注意力权重 def normalize(cube): mean cube.mean(axis(0, 1), keepdimsTrue) std cube.std(axis(0, 1), keepdimsTrue) 1e-6 return (cube - mean) / std t1_n normalize(t1) t2_n normalize(t2) # 按 patch 切样本patch_size 取 11 或 13 是常见做法 def make_patches(a, b, lbl, patch11, stride1): pad patch // 2 a_p np.pad(a, ((pad, pad), (pad, pad), (0, 0)), modereflect) b_p np.pad(b, ((pad, pad), (pad, pad), (0, 0)), modereflect) xs1, xs2, ys [], [], [] H, W lbl.shape for i in range(0, H, stride): for j in range(0, W, stride): xs1.append(a_p[i:ipatch, j:jpatch, :]) xs2.append(b_p[i:ipatch, j:jpatch, :]) ys.append(lbl[i, j]) return np.stack(xs1), np.stack(xs2), np.array(ys) X1, X2, Y make_patches(t1_n, t2_n, label) print(样本对:, X1.shape, X2.shape, 标签:, Y.shape)逻辑上分三步先读三个矩阵并确认维度一致再逐波段做标准化最后按 patch 滑窗切出成对的样本。参数上 patch 取 11 或 13 是经验值太小感受野不够、太大正样本会被稀释stride 训练时取 1 保证样本量推理时可以调大加速。归一化用逐波段均值方差比全局归一化更稳因为不同波段能量差异很大。标签正负比例如果严重失衡后面训练要加类别权重这个先记着。2.3 训练集验证集划分与类别失衡处理高光谱变化检测的标签通常极度失衡变化像素往往只占百分之几。直接随机划分会让验证集里正样本太少指标抖动大。我一般按空间分块划分比如把图切成若干大块整块进训练或验证避免相邻 patch 泄漏。类别权重用 sklearn 的 compute_class_weight 算或者直接在损失里给正样本一个 5 到 20 的权重。这一步不做模型会倾向于全预测为不变准确率看着很高但变化图一片空白这是最典型的翻车现场。3. 孪生自注意力网络结构共享权重分支加注意力聚合3.1 为什么用孪生结构而不是单分支拼接双时相变化检测有两种主流做法一种是把两个时相在通道维拼起来送进单分支网络另一种是孪生双分支。拼接法简单但网络要自己学会区分「这是时相一还是时相二」对权重初始化敏感。孪生结构两个分支共享权重天然保证同一地物在两个时相被映射到同一特征空间差异只来自地物本身变化这正是变化检测想要的。代价是参数量翻倍、显存吃紧所以实际工程里分支不会做太深靠注意力来补感受野。自注意力在这里的作用是重新分配权重。高光谱的波段间相关性很强有些波段噪声大注意力能让网络自动压低这些波段的贡献空间上变化区域往往成片出现自注意力能建模长程依赖把孤立噪点抑制掉。这就是标题里「孪生自注意力」两个词各自的分工。3.2 用 PyTorch 搭一个可训练的孪生自注意力主干下面是一个精简但可跑的结构两个分支共享同一个编码器编码器里嵌一个多头自注意力模块最后做特征差分再分类。import torch import torch.nn as nn class SpectralSpatialAttention(nn.Module): def __init__(self, dim, heads4): super().__init__() self.norm nn.LayerNorm(dim) self.attn nn.MultiheadAttention(dim, heads, batch_firstTrue) def forward(self, x): # x: (B, N, C)N 是空间像素数 h self.norm(x) out, _ self.attn(h, h, h) # 自注意力QKV return x out # 残差连接 class Encoder(nn.Module): def __init__(self, in_band, feat_dim64): super().__init__() self.proj nn.Linear(in_band, feat_dim) self.attn SpectralSpatialAttention(feat_dim) self.mlp nn.Sequential( nn.Linear(feat_dim, feat_dim), nn.ReLU(), nn.Linear(feat_dim, feat_dim)) def forward(self, x): # x: (B, patch, patch, in_band) - (B, N, C) B, P, _, C x.shape x x.view(B, P * P, C) x self.proj(x) x self.attn(x) x x self.mlp(x) return x class SiameseAttentionNet(nn.Module): def __init__(self, in_band, feat_dim64): super().__init__() self.encoder Encoder(in_band, feat_dim) # 两分支共享 self.classifier nn.Sequential( nn.Linear(feat_dim * 2, 64), nn.ReLU(), nn.Linear(64, 2)) def forward(self, x1, x2): f1 self.encoder(x1) # (B, N, C) f2 self.encoder(x2) diff torch.abs(f1 - f2) # 特征差分 feat torch.cat([f1, f2, diff], dim-1) # 全局平均池化到每个样本一个向量 feat feat.mean(dim1) return self.classifier(feat)结构上Encoder 先把波段维投影到 feat_dim再过一个多头自注意力最后接 MLP 加残差。两个时相共用这个 Encoder输出特征做绝对差再和原始特征拼接送分类头。参数上 feat_dim 取 64 是显存和表达力的折中heads 取 4 够用patch 越大 N 越大注意力开销按平方涨所以 patch 别超过 15。分类头输出 2 类配合交叉熵损失。如果你想输出变化图而不是单像素分类把 mean 换成逐像素分类即可但那样显存会明显上升。3.3 损失函数与训练循环的关键参数损失用带类别权重的交叉熵优化器 Adam学习率 1e-3 起步batch size 视显存取 64 到 256。训练循环里要盯两个量训练损失和验证集上的 F1 或 Kappa光看准确率会被失衡标签骗。from torch.utils.data import TensorDataset, DataLoader import torch.optim as optim ds TensorDataset(torch.tensor(X1).permute(0,3,1,2).float(), torch.tensor(X2).permute(0,3,1,2).float(), torch.tensor(Y).long()) loader DataLoader(ds, batch_size64, shuffleTrue) model SiameseAttentionNet(in_bandX1.shape[-1]).cuda() # 正样本权重按失衡比例给这里假设变化占比约 5% weight torch.tensor([1.0, 10.0]).cuda() criterion nn.CrossEntropyLoss(weightweight) opt optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) for epoch in range(50): model.train() total 0 for a, b, y in loader: a, b, y a.cuda(), b.cuda(), y.cuda() # 还原成 (B, patch, patch, band) a a.permute(0,2,3,1); b b.permute(0,2,3,1) opt.zero_grad() out model(a, b) loss criterion(out, y) loss.backward() opt.step() total loss.item() print(fepoch {epoch}, loss {total/len(loader):.4f})参数说明weight_decay 1e-4 抑制过拟合学习率如果损失震荡就降到 5e-450 个 epoch 是常见起点早停看验证 F1。注意 permute 那两行是因为 DataLoader 默认按 (B,C,H,W) 组织而我们的 Encoder 要 (B,H,W,C)这个维度顺序搞错是新手最常见的报错来源。4. 训练调参与推理出图把模型跑成一张变化图4.1 学习率、patch 大小、注意力头数的调参顺序调参别一把抓按影响从大到小来。第一优先是 patch 大小它直接决定感受野和显存11 到 15 之间试先固定其他。第二是学习率1e-3 和 5e-4 各跑一轮看损失曲线震荡就降。第三是注意力头数4 头通常够8 头在小 patch 上收益不明显还费显存。最后才是 feat_dim64 到 128 之间显存够就上 128。我一般会固定随机种子跑三组看验证 F1 的均值和方差方差大的配置直接弃掉别赌。4.2 推理阶段滑窗出图与后处理训练完要出整图变化图做法是全图滑窗每个像素取 patch 预测再拼回原尺寸。重叠区域取平均或投票。model.eval() H, W label.shape patch 11 pad patch // 2 prob_map np.zeros((H, W), dtypenp.float32) t1_p np.pad(t1_n, ((pad,pad),(pad,pad),(0,0)), modereflect) t2_p np.pad(t2_n, ((pad,pad),(pad,pad),(0,0)), modereflect) with torch.no_grad(): for i in range(H): batch_a, batch_b [], [] for j in range(W): batch_a.append(t1_p[i:ipatch, j:jpatch, :]) batch_b.append(t2_p[i:ipatch, j:jpatch, :]) a torch.tensor(np.stack(batch_a)).float().cuda() b torch.tensor(np.stack(batch_b)).float().cuda() out torch.softmax(model(a, b), dim-1)[:, 1] # 变化类概率 prob_map[i] out.cpu().numpy() # 阈值 0.5 出二值图再按需做形态学去噪 binary (prob_map 0.5).astype(np.uint8)逻辑是按行批量推理避免逐像素调用太慢。参数上阈值 0.5 是默认实际可以按验证集上的最佳 F1 点调常见在 0.4 到 0.6 之间。后处理用开运算去掉孤立噪点闭运算填补空洞scipy.ndimage 里都有。出图后一定要和标签叠一起看光看指标不够很多错误是成片的空间结构错误指标反映不出来。4.3 评价指标怎么选才不被失衡标签骗高光谱变化检测别只看 OA总体准确率失衡时 OA 能到 95% 但变化一个没检出。要同时看 F1、IoU、Kappa尤其是变化类的 F1。我习惯把混淆矩阵打出来看漏检和误检各占多少漏检多就降阈值或加正样本权重误检多就升阈值或加正则。这一步是判断模型到底能不能用的关键别跳过。5. 避坑与排查孪生自注意力变化检测的五个血泪现场5.1 现象训练损失一直降但变化图全黑原因基本是类别失衡加阈值默认 0.5模型学会了全预测不变。解决是给正样本加权、监控变化类 F1、推理时按验证集调阈值。这个坑几乎每个人都会踩一次早发现早改。5.2 现象验证指标远好于测试换区域就崩原因是按像素随机划分导致相邻 patch 泄漏训练集和验证集空间上挨着。解决是按空间分块划分或者干脆留一整块区域做验证。高光谱数据空间自相关强这个泄漏比普通图像更严重。5.3 现象显存爆掉batch 只能开到 8原因是自注意力对 patch 内像素数是平方复杂度patch 一大就炸。解决是降 patch 到 11、降 feat_dim 到 64、用梯度累积模拟大 batch或者把注意力改成窗口注意力。别硬堆显存先降复杂度。5.4 现象两个时相波段数不一致直接报错原因是数据来源不同或预处理不一致。解决是取波段交集或者重采样到统一波长。这个必须在进网络前检查网络层不会帮你对齐。5.5 现象推理出图有网格状拼接痕迹原因是滑窗 stride 等于 patch 时边界预测不一致。解决是让 stride 小于 patch 做重叠重叠区取平均。代价是推理变慢但图会干净很多。6. 把这份源码用起来从复现到判断值不值得投入拿到这份 python 源码加数据包第一件事不是改网络而是先跑通默认配置确认数据读取、训练、推理整条链路能出图。跑通之后按这个顺序做验证先用小 patch 和少 epoch 快速跑一轮看损失是否下降再上完整配置看验证 F1最后换一块没见过的区域测泛化。这三步走完你基本能判断这套孪生自注意力方案在你的数据上到底行不行。进阶用法上有两个方向值得试。一是把自注意力换成窗口注意力或线性注意力显存能降一半以上patch 可以开到 21对大区域变化检测帮助明显。二是把特征差分从绝对差换成可学习的差分模块让网络自己决定怎么比较两个时相实测在物候变化明显的区域能压掉一部分伪变化。下面这张表是我常用的配置对照方便你按显存快速选。配置patchfeat_dimheadsbatch显存占用轻量1164464低标准1364464中加强15128832高验证方法上除了 F1 和 Kappa我强烈建议把变化图叠加到假彩色合成图上肉眼过一遍。指标高但图上一片碎斑说明模型学到的是噪声不是地物变化这种模型上线就是灾难。我自己的习惯是每次训练完先存三张图预测概率图、二值图、和标签的对比图一眼就能看出问题在哪。最后说句实在的孪生自注意力这套东西不是银弹它在双时相辐射差异可控、变化区域成片的数据上表现好遇到配准误差大或物候变化剧烈的场景传统方法加后处理有时反而更稳。判断值不值得投入就看你手头的数据质量和变化类型先跑通再决定别一上来就堆大模型。希望帮到你。本文还有配套的精品资源点击获取