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_bands103, feat_dim64): super().__init__() # 共享权重的特征提取分支两个时相各走一次 self.encoder nn.Sequential( nn.Conv2d(in_bands, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.Conv2d(64, feat_dim, kernel_size3, padding1), nn.BatchNorm2d(feat_dim), nn.ReLU(inplaceTrue), ) 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_heads4): super().__init__() self.num_heads num_heads self.scale (dim // num_heads) ** -0.5 self.qkv nn.Conv2d(dim, dim * 3, kernel_size1) self.proj nn.Conv2d(dim, dim, kernel_size1) 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_heads4是注意力头数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 python3.9 -y conda activate hschange # PyTorch 按你的 CUDA 版本装这里以 cu118 为例 pip install torch2.1.0 torchvision0.16.0 --index-url https://download.pytorch.org/whl/cu118 pip install numpy scipy scikit-learn matplotlib tqdm tensorboardpython3.9是兼容性最稳的版本3.11 有些老库会报错。PyTorch 版本跟 CUDA 对齐nvidia-smi看右上角 CUDA Version别装错。scikit-learn用来算 OA、Kappa 这些指标tqdm看进度tensorboard看 loss 曲线。注意如果你只有 CPU把后面代码里的.cuda()全换成.cpu()但高光谱数据量大CPU 训练一个 epoch 可能要几十分钟不推荐。3.2 数据加载高光谱立方体的读取与配对高光谱数据常见格式是 .matMATLAB或 .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_size128, stride64): 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:ip, j:jp, :].transpose(2, 0, 1) # B×H×W x2 self.t2[i:ip, j:jp, :].transpose(2, 0, 1) y self.gt[i:ip, j:jp] return torch.from_numpy(x1), torch.from_numpy(x2), torch.from_numpy(y)patch_size128是切块大小太大显存不够太小上下文不足。stride64是滑动步长等于 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_bands103, feat_dim64).to(device) optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) criterion nn.CrossEntropyLoss(weighttorch.tensor([1.0, 5.0]).to(device)) # 变化类加权 scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max100) dataset HSIChangeDataset(data/pavia_change.mat) loader DataLoader(dataset, batch_size8, shuffleTrue, num_workers4) 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_norm1.0) optimizer.step() total_loss loss.item() scheduler.step() print(fEpoch {epoch}, Loss {total_loss/len(loader):.4f})lr1e-4是 AdamW 的稳妥起点太大 loss 震荡太小收敛慢。weight_decay1e-4防过拟合。CrossEntropyLoss的weight[1.0, 5.0]是因为变化像素通常只占 5%~15%不加权网络会全预测成未变化OA 看着高但 Kappa 接近 0这是最经典的坑。clip_grad_norm_防止梯度爆炸高光谱数据里噪声大梯度容易飞。batch_size8是 128×128 patch 在 8G 显存下的经验值显存大可以加到 16。4. 高光谱变化检测的避坑与排查清单4.1 现象训练 loss 一直不降OA 卡在 85% 不动原因最常见的是标签类别极度不平衡变化像素太少网络学到「全预测未变化」这个退化解。其次是数据没归一化高光谱不同波段量纲差异大有的波段值域 0~1有的 0~10000不归一化网络根本学不动。解决先统计标签里变化/未变化的像素比如果小于 1:10损失函数必须加权或者用 Focal Loss、Dice Loss。归一化用逐波段标准化(x - mean) / stdmean 和 std 在训练集上算别用整个数据集算否则测试集信息泄露。4.2 现象验证集指标很好测试集一塌糊涂原因高光谱数据空间自相关强相邻 patch 高度相似。如果你随机切 patch 再随机分训练测试训练集和测试集的 patch 可能来自同一片区域等于变相泄露。这是高光谱领域最隐蔽的坑。解决按空间分块划分比如图像左上 70% 做训练右下 30% 做测试中间留缓冲带。或者用「按区域留一法」确保训练和测试的地理区域不重叠。指标差距如果超过 10 个点基本就是泄露了。4.3 现象显存溢出报 CUDA out of memory原因自注意力的注意力矩阵是 HW×HWpatch 一大就爆。或者 batch_size 设太大或者没及时释放中间变量。解决三个方向——降 patch_size128 降到 64、降 batch_size8 降到 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)三件套都加上否则两次跑的差异你分不清是改动带来的还是随机性带来的。这个方向不难难的是把变量控制住让每次实验都可复现。希望帮到你。本文还有配套的精品资源点击获取
返回列表