拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

孪生自注意力网络高光谱变化检测Python源码实战

孪生自注意力网络高光谱变化检测Python源码实战

简介:本资源为基于孪生自注意力网络的高光谱图像变化检测系统,面向计算机、人工智能、通信、遥感等专业的在校学生、教师及企业研发人员,可用于课程设计、毕业设计、大作业或初期项目立项演示,帮助理解高光谱变化检测的模型搭建与实验流程。压缩包共138个文件,约174.68MB,包含30个py源码、54个pyc编译文件、18个xml配置、10个mat数据、2个pth权重、2个npy数组及若干png、md、html等,覆盖模型定义、训练推理、数据加载与结果可视化等模块。项目代码完整且功能验证通过,运行稳定,已有176人学习下载。读者可据此掌握孪生自注意力网络在高光谱变化检测中的实现思路,参考数据预处理、网络结构与评估指标写法,并在此基础上进行二次开发或功能扩展。需注意解压后路径与项目名不要含中文,建议重命名为英文后再运行。

1. 孪生自注意力网络做高光谱变化检测:这套 Python 源码到底解决了什么

高光谱图像变化检测这件事,真正难的不是「找出两期影像哪里不一样」,而是「在光谱维度上百来个波段里,把真正的物候变化、地物替换和传感器噪声、配准误差区分开」。传统 CVA、PCA 加阈值那套做法,在波段数一多就容易被噪声带偏,虚警率压不下来。孪生自注意力网络这套思路,本质是让两个时相的高光谱立方体共享同一套特征提取权重,再用自注意力在空间和光谱两个维度上建模长程依赖,最后输出一张变化/未变化的二值图。它适合谁?手里有双时相高光谱数据、想做地物变化监测(城市扩张、植被退化、水体变迁)的遥感方向研究生和算法工程师,尤其是想找一个能跑通、能改、能换数据集的 Python 源码来打底的人。这套源码加数据的组合,价值就在于省掉你从零搭数据加载和训练循环的两周时间,直接进到调参和换场景的阶段。

2. 孪生自注意力网络的结构拆解与选型理由

2.1 为什么是孪生结构而不是单分支拼接

高光谱变化检测的输入天然是成对的:时相 T1 和时相 T2 各一个 H×W×B 的立方体,B 通常上百。最粗暴的做法是把两个立方体在通道维拼成 H×W×2B 丢进一个网络,但这样有个隐患——网络会学到「T1 在前、T2 在后」这种位置先验,一旦你交换输入顺序,输出就变了,这在工程上叫顺序敏感,是血泪经验里最常见的翻车点之一。

孪生结构的做法是两个分支共享权重,各自提取特征,再在特征层做差分或拼接。共享权重保证了「同一个地物在两期影像里被映射到同一个特征空间」,差分才有物理意义。常见做法是:

import torch import torch.nn as nn class SiameseBackbone(nn.Module): def __init__(self, in_bands=103, feat_dim=64): super().__init__() # 共享权重的特征提取分支,两个时相各走一次 self.encoder = nn.Sequential( nn.Conv2d(in_bands, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.Conv2d(64, feat_dim, kernel_size=3, padding=1), nn.BatchNorm2d(feat_dim), nn.ReLU(inplace=True), ) def forward(self, x1, x2): f1 = self.encoder(x1) # T1 特征 f2 = self.encoder(x2) # T2 特征 return f1, f2

这段代码的关键点在于self.encoder只定义一次,forward里对 x1、x2 调用同一个模块,PyTorch 会自动共享参数。in_bands要跟你数据的实际波段数对齐,比如 Pavia University 是 103 个波段(去掉噪声波段后),Indian Pines 是 200 个。feat_dim是特征通道数,64 是个稳妥的起点,显存吃紧就降到 32。

提示:如果你的两期影像波段数不一致(比如传感器不同),孪生结构就不能直接共享第一层卷积,需要各自一个投影层先对齐到相同波段数,再进共享编码器。

2.2 自注意力模块放在哪一层最有效

自注意力在变化检测里的作用是捕捉「一个像素的变化和远处像素的变化是否相关」。比如一条道路的扩建,边缘像素和中心像素的变化是联动的,卷积的局部感受野抓不到这种长程关系,自注意力可以。

但自注意力不是越多越好。全分辨率上做自注意力,H×W 假设是 256×256,注意力矩阵就是 65536×65536,显存直接爆。常见做法是在下采样后的特征图上做,或者用窗口注意力。这套源码里我一般会把自注意力放在编码器之后、差分之前:

class SelfAttention(nn.Module): def __init__(self, dim, num_heads=4): super().__init__() self.num_heads = num_heads self.scale = (dim // num_heads) ** -0.5 self.qkv = nn.Conv2d(dim, dim * 3, kernel_size=1) self.proj = nn.Conv2d(dim, dim, kernel_size=1) def forward(self, x): B, C, H, W = x.shape qkv = self.qkv(x).reshape(B, 3, self.num_heads, C // self.num_heads, H * W) q, k, v = qkv[:, 0], qkv[:, 1], qkv[:, 2] # 各 [B, heads, c, HW] attn = (q.transpose(-2, -1) @ k) * self.scale # [B, heads, HW, HW] attn = attn.softmax(dim=-1) out = (v @ attn.transpose(-2, -1)).reshape(B, C, H, W) return self.proj(out) + x # 残差连接

num_heads=4是注意力头数,dim要能被它整除。self.scale是缩放因子,防止点积过大导致 softmax 梯度消失。最后的+ x是残差,保证注意力模块至少不会让性能变差。这里注意力矩阵是 HW×HW,所以 H、W 不能太大,实践中会把特征图先池化到 64×64 再进这个模块。

2.3 差分与解码:从特征到变化图

两个时相的特征出来后,怎么融合决定了网络对「变化」的敏感度。三种常见做法:

融合方式公式特点适用场景
绝对差|f1 - f2|对亮度变化敏感,简单地物替换类变化
拼接concat(f1, f2)保留两期信息,参数多复杂变化类型
注意力差分Attention(f1, f2)自适应加权,效果最好数据量充足时

源码里一般用绝对差加一层卷积做解码,因为参数量小、训练快。解码就是几层上采样加卷积,最后 1×1 卷积输出 2 通道(变化/未变化),接 softmax 或直接 BCE 损失。

3. 用 Python 源码在本地跑通训练的最小步骤

3.1 环境配置与依赖安装

先把环境弄干净,别在 base 环境里装,翻车概率高。用 conda 建一个:

conda create -n hschange python=3.9 -y conda activate hschange # PyTorch 按你的 CUDA 版本装,这里以 cu118 为例 pip install torch==2.1.0 torchvision==0.16.0 --index-url https://download.pytorch.org/whl/cu118 pip install numpy scipy scikit-learn matplotlib tqdm tensorboard

python=3.9是兼容性最稳的版本,3.11 有些老库会报错。PyTorch 版本跟 CUDA 对齐,nvidia-smi看右上角 CUDA Version,别装错。scikit-learn用来算 OA、Kappa 这些指标,tqdm看进度,tensorboard看 loss 曲线。

注意:如果你只有 CPU,把后面代码里的.cuda()全换成.cpu(),但高光谱数据量大,CPU 训练一个 epoch 可能要几十分钟,不推荐。

3.2 数据加载:高光谱立方体的读取与配对

高光谱数据常见格式是 .mat(MATLAB)或 .tif。ICVL、Pavia、Indian Pines 这些公开数据集多是 .mat。加载逻辑:

import scipy.io as sio import numpy as np import torch from torch.utils.data import Dataset class HSIChangeDataset(Dataset): def __init__(self, mat_path, patch_size=128, stride=64): data = sio.loadmat(mat_path) # 假设 mat 里有 T1、T2、GT 三个键,形状 H×W×B self.t1 = data['T1'].astype(np.float32) self.t2 = data['T2'].astype(np.float32) self.gt = data['GT'].astype(np.int64) # 0/1 标签 self.patch_size = patch_size self.stride = stride self.coords = self._gen_coords() def _gen_coords(self): H, W = self.gt.shape coords = [] for i in range(0, H - self.patch_size + 1, self.stride): for j in range(0, W - self.patch_size + 1, self.stride): coords.append((i, j)) return coords def __len__(self): return len(self.coords) def __getitem__(self, idx): i, j = self.coords[idx] p = self.patch_size x1 = self.t1[i:i+p, j:j+p, :].transpose(2, 0, 1) # B×H×W x2 = self.t2[i:i+p, j:j+p, :].transpose(2, 0, 1) y = self.gt[i:i+p, j:j+p] return torch.from_numpy(x1), torch.from_numpy(x2), torch.from_numpy(y)

patch_size=128是切块大小,太大显存不够,太小上下文不足。stride=64是滑动步长,等于 patch_size 一半可以保证边缘也有覆盖。transpose(2,0,1)把 H×W×B 转成 B×H×W,因为 PyTorch 卷积要求通道在前。标签GT是 0/1 二值图,如果你的数据里变化区域标的是 255,记得先除以 255。

3.3 训练循环与关键超参设置

训练循环没什么玄学,但超参设错就是白跑。核心几个:

import torch.nn as nn from torch.utils.data import DataLoader device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = SiameseChangeNet(in_bands=103, feat_dim=64).to(device) optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4) criterion = nn.CrossEntropyLoss(weight=torch.tensor([1.0, 5.0]).to(device)) # 变化类加权 scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100) dataset = HSIChangeDataset('data/pavia_change.mat') loader = DataLoader(dataset, batch_size=8, shuffle=True, num_workers=4) for epoch in range(100): model.train() total_loss = 0 for x1, x2, y in loader: x1, x2, y = x1.to(device), x2.to(device), y.to(device) optimizer.zero_grad() logits = model(x1, x2) # [B, 2, H, W] loss = criterion(logits, y) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() total_loss += loss.item() scheduler.step() print(f'Epoch {epoch}, Loss {total_loss/len(loader):.4f}')

lr=1e-4是 AdamW 的稳妥起点,太大 loss 震荡,太小收敛慢。weight_decay=1e-4防过拟合。CrossEntropyLoss的weight=[1.0, 5.0]是因为变化像素通常只占 5%~15%,不加权网络会全预测成未变化,OA 看着高但 Kappa 接近 0,这是最经典的坑。clip_grad_norm_防止梯度爆炸,高光谱数据里噪声大,梯度容易飞。batch_size=8是 128×128 patch 在 8G 显存下的经验值,显存大可以加到 16。

4. 高光谱变化检测的避坑与排查清单

4.1 现象:训练 loss 一直不降,OA 卡在 85% 不动

原因:最常见的是标签类别极度不平衡,变化像素太少,网络学到「全预测未变化」这个退化解。其次是数据没归一化,高光谱不同波段量纲差异大,有的波段值域 0~1,有的 0~10000,不归一化网络根本学不动。

解决:先统计标签里变化/未变化的像素比,如果小于 1:10,损失函数必须加权,或者用 Focal Loss、Dice Loss。归一化用逐波段标准化:(x - mean) / std,mean 和 std 在训练集上算,别用整个数据集算,否则测试集信息泄露。

4.2 现象:验证集指标很好,测试集一塌糊涂

原因:高光谱数据空间自相关强,相邻 patch 高度相似。如果你随机切 patch 再随机分训练测试,训练集和测试集的 patch 可能来自同一片区域,等于变相泄露。这是高光谱领域最隐蔽的坑。

解决:按空间分块划分,比如图像左上 70% 做训练,右下 30% 做测试,中间留缓冲带。或者用「按区域留一法」,确保训练和测试的地理区域不重叠。指标差距如果超过 10 个点,基本就是泄露了。

4.3 现象:显存溢出,报 CUDA out of memory

原因:自注意力的注意力矩阵是 HW×HW,patch 一大就爆。或者 batch_size 设太大,或者没及时释放中间变量。

解决:三个方向——降 patch_size(128 降到 64)、降 batch_size(8 降到 4)、在注意力模块前加一次池化把 HW 降到 1/4。还可以用torch.cuda.empty_cache()在每个 epoch 后清一次,但治标不治本。混合精度训练torch.cuda.amp能省一半显存,值得加。

4.4 现象:换了自己的数据集,波段数对不上报错

原因:源码里in_bands写死了,比如按 103 写的,你的数据是 200 波段,第一层卷积就报维度不匹配。

解决:把in_bands改成你的实际波段数,同时检查数据加载里有没有硬编码的波段索引(比如data[:, :, :103]这种截断)。如果两期波段数不同,加一个 1×1 卷积投影层对齐。另外注意有些数据集有坏波段(全 0 或全噪声),要提前剔除,否则归一化时 std 为 0 会出 NaN。

4.5 现象:Kappa 系数算出来是负数或大于 1

原因:Kappa 的计算公式里用了混淆矩阵,如果预测全是一类,分母可能为 0,或者标签不是 0/1 而是 0/255,混淆矩阵维度对不上。

解决:先确认标签唯一值,np.unique(gt)看一下,是 [0,1] 还是 [0,255]。是 255 就除以 255。Kappa 计算用 sklearn 的cohen_kappa_score(y_true.flatten(), y_pred.flatten()),别自己手写,手写容易在分母为 0 时出问题。

5. 把源码用起来:换数据集、调注意力头数和验证指标的进阶技巧

源码跑通只是起点,真正要投入这个方向,得会改。第一个进阶动作是换数据集。公开的高光谱变化检测数据集不多,常见的有 Pavia University 双时相、Indian Pines 模拟变化、以及一些高分辨率高光谱数据。换数据时,除了改in_bands,还要注意空间分辨率——不同数据集 GSD 不同,patch_size 要按地物尺度调,比如 1m 分辨率下 128 像素覆盖 128 米,城市变化够用,但 30m 分辨率下 128 像素覆盖近 4 公里,patch 里可能混了好几种变化类型,这时候要么降 patch_size,要么加多尺度特征。

第二个进阶是调自注意力的头数和位置。num_heads从 4 调到 8,能捕捉更细粒度的光谱-空间联合模式,但参数量和显存都涨。我的习惯是先固定 4 头跑一版 baseline,记录 OA、Kappa、F1,然后只改num_heads到 8 再跑,如果 Kappa 提升小于 1 个点,就不值得,因为推理速度会慢 30% 左右。注意力放的位置也有讲究:放在编码器中间层,感受野适中;放在最后,特征图小、算得快但可能丢失细节;放在差分之后,是让网络「关注变化区域之间的关系」,这个位置在变化检测里往往最有效,值得试。

第三个进阶是验证方法。别只看 OA,高光谱变化检测里 OA 会被未变化类主导,虚高。必须同时看 Kappa、F1(变化类的)、以及 ROC 曲线下的 AUC。我一般会画一张变化检测结果叠加在原图上的可视化,肉眼看一下边缘是否贴合、有没有大片漏检。有个小技巧:把预测概率图存成 .tif,用 QGIS 打开叠加原图,比 matplotlib 看得清楚得多。

最后一个习惯,也是我踩坑踩出来的:每次改完超参或结构,一定固定随机种子,torch.manual_seed(42)、np.random.seed(42)、torch.cuda.manual_seed_all(42)三件套都加上,否则两次跑的差异你分不清是改动带来的还是随机性带来的。这个方向不难,难的是把变量控制住,让每次实验都可复现。希望帮到你。

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

返回列表