☰
地震波Transformer与对比学习:无标注数据下的储层识别实战拆解
2026/9/30 5:36:45 网站建设 项目流程

简介:这是一份聚焦地质勘探智能化的技术方案文档,系统梳理了Transformer与对比学习在地震波油气储层识别中的联合应用。内容面向石油地质、地球物理及人工智能交叉领域的研究人员与开发者,针对复杂地质条件下储层识别难、传统方法处理大规模地震数据效率低等问题,给出从原理到实践的完整实施路径。文档共二十八页,压缩包内为单个PDF文件,大小2.03MB,支持目录跳转与大纲定位,便于按章节快速查阅。正文依次介绍地震波传播特性、Transformer核心架构(自注意力、多头注意力、位置编码)、对比学习数据增强与损失函数设计、特征提取及模型训练优化,并设置海上、陆地、多区域三类实际勘探案例作验证。读者可获得一套完整的对比学习勘探方案框架、实验设计思路与调优方向,涵盖数据处理、特征提取和评估指标设定,适合用于课题预研、论文参考或项目初探。目前已有五十六人学习。

1. 地震波Transformer在储层识别里到底解决了什么:先从井少、标签缺这个痛点说起

做储层预测的工程师都清楚一件事:真正卡住精度的从来不是反演算法,而是标注数据。一张三维工区的地震数据可能有几十万个CDP道集,但能用来当标签的井往往只有十几口,测井曲线齐全的甚至个位数。传统卷积网络在这种“强输入、弱标签”的场景下学到的特征往往过拟合到井点附近的局部波形,换个构造带泛化能力就断崖式下跌。这也是为什么地震波Transformer配合对比学习这个组合近两年在储层识别里被反复提起——它想解决的核心问题不是“模型更深”,而是“没有标签的时候,模型怎么先学会理解地震相”。

Transformer的优势在于它能建模长距离依赖,地震道在时间轴上的波形变化往往跨越几十甚至上百个采样点才有响应,这刚好是卷积核够不着、Transformer擅长的范围。而对比学习的意义在于:它不依赖井标签,而是靠数据本身构造学习信号,让模型先在大规模无标注地震数据上做预训练,把“什么样的波形组合代表同一套沉积特征”这件事学会。两者组合起来,就是一条“无标注预训练 → 少量井微调 → 复杂储层识别”的路径。这篇文章就把这条路径拆开讲:数据怎么切、架构怎么选、损失函数怎么配、以及在真实工区里最容易翻车的几个坑。

2. 把SEG-Y切出能喂给Transformer的数据块:道序列、像素Patch与样本对构建

2.1 地震数据进Transformer之前的三个前置问题

Transformer吃的是序列或像素块,而SEG-Y里存的是波形振幅,直接堆进去不行。我一般先做三件事:一是解码SEG-Y,把三维数据体按inline/crossline/time三个维度组织好;二是振幅归一化,地震数据的幅值范围在不同工区甚至不同采集批次里都不一样,不做归一化会让位置编码和注意力权重都学出偏差;三是决定基本输入单元。

输入单元的选择直接决定模型看到的是“波形”还是“图像”。常见做法有两种:一种是用单道或多道组合的一维波形序列作为输入,适合做逐道储层参数预测,比如孔隙度、含油气概率;另一种是把inline方向的一个剖面切片作为二维图像输入,适合识别河道、断层、透镜体这类有空间形态的储层结构。用对比学习的方案里,两种都能做,但样本对的构造方式差别很大,后面专门讲。

2.2 代码落地:用Python解码SEG-Y并按时间轴切patch

这里我用的是一个标准的处理流程,假设手上有一个三维SEG-Y数据体,目标是切成可供Transformer训练的样本块。代码用伪井位置做标记,实际使用时替换成工区坐标即可。

import segyio import numpy as np # 打开SEG-Y文件,注意这里用的是真实工区文件路径 with segyio.open("workarea.sgy", ignore_geometry=True) as f: # 读取inline/crossline方向的道数和采样点数 inline_no = len(f.ilines) crossline_no = len(f.xlines) samples_no = len(f.samples) # 将整个三维数据体读入内存,shape为 (inline, crossline, samples) data = f.trace.raw[:].reshape(inline_no, crossline_no, samples_no) # 振幅归一化:不适合用min-max,地震数据含噪声,用百分位数截断更稳 p_low, p_high = np.percentile(data, [1, 99]) data_clip = np.clip(data, p_low, p_high) data_norm = (data_clip - p_low) / (p_high - p_low + 1e-6) # 沿时间轴切patch:这里把每条地震道按256个采样点为一组切片 def cut_trace_patches(trace, patch_size=256, stride=128): patches = [] for start in range(0, len(trace) - patch_size + 1, stride): patches.append(trace[start:start + patch_size]) return np.stack(patches) # 对每条道执行切片,并将所有patch整理成训练集 patch_list = [] for i_idx in range(inline_no): for x_idx in range(crossline_no): trace = data_norm[i_idx, x_idx, :] patch_list.append(cut_trace_patches(trace)) patch_data = np.concatenate(patch_list, axis=0) print(f"total patches: {patch_data.shape}")

这段代码里有几个参数值得说明。patch_size=256对应时间轴上的256个采样点,常规地震数据采样率是2ms或4ms,256个点代表约0.5到1秒的时间窗口,这个长度刚好覆盖一个目的层段的反射波形组合。stride=128是重叠步长,重叠的目的是让相邻patch之间有上下文连续性,对比学习里正样本对正是利用这种连续性来构造的。percentile截断而不是min-max归一化,是因为地震数据经常有强振幅异常值,min-max会被极值主导,Transformer对输入尺度比CNN更敏感。

2.3 对比学习样本对的构造:正样本、负样本和硬负样本

对比学习不是凭空来的,它的核心是样本对:正样本对表示“这两个输入应该是同一个东西”,负样本对表示“这两个输入不是同一个东西”。放到地震储层识别场景里,我常用的正样本构造方式有三种:第一种是同一道数据在相邻时间窗口上切出来的patch,因为同一套地层的波形具有纵向连续性;第二种是同一地理位置附近几条相邻道的数据,因为同一个储层在空间上是延展的;第三种是给同一patch做数据增强,比如加轻微的随机噪声、做小幅度的振幅缩放,这类增强能提高模型对采集噪声的鲁棒性。

负样本的选择更讲究,这也是对比学习里最容易翻车的环节。如果负样本直接随机从全工区采样,模型很快就学会用一个偷懒的特征去区分——比如振幅大小,而不是真正的地震相特征。我一般会在负样本里加入“空间近邻但属于不同沉积相”的样本,这类样本在波形上很像、成因上完全不同,逼着模型去学更细的纹理特征。这个策略在MoCo和SimCLR里叫hard negative mining,落地到地震数据上,通常的做法是先跑一次无监督聚类,把地震相粗分成若干类,再从不同类里采样负样本对。

3. 编码器选型与位置编码:Swin、ViT、Point Transformer在地震数据上的取舍

3.1 为什么不能直接套ViT和Swin Transformer

很多从图像转到地球物理的工程师,上来就直接套Vision Transformer做储层识别,然后发现结果还不如一个三层的UNet。原因很简单:地震数据不是自然图像。ViT里预设的归纳偏置是“图像基本结构在空间上是各向同性的”,但地震剖面的物理含义在inline、crossline和时间轴上完全不对称。时间轴是波传播方向,有明确的地质时间意义;inline和crossline是空间方位,横向连续性好但纵向没有强约束。另外地震数据的有效频带窄、信噪比低,直接切成16×16的patch,频谱特征会被严重破坏。

我一般倾向的方案是:如果是逐道预测储层参数,用一维的Transformer encoder配合卷积下采样做特征提取;如果是做剖面级别的岩相识别,用Swin Transformer,因为它的窗口注意力机制对细长条的地层界面更友好,而且计算量比全局自注意力小很多。Point Transformer则适合在层位解释的场景里,把层位离散点当成点云数据处理,这个和本文的储层识别路径稍有差异,但思路相通。

3.2 位置编码这件事在地震数据里比在图像里重要得多

Transformer自己没有位置感,全靠位置编码把顺序信息喂进去。自然图像里位置编码的作用是告诉模型“像素块在哪”,而地震数据里位置编码还额外承担了一个物理意义:深度。时间轴上同样是256个采样点的patch,放在2秒位置和放在3秒位置,走的地层完全不一样,孔隙度、压实程度、含流体情况都不一样。所以位置编码不能只做绝对位置,还要能做相对位置——模型需要知道一个反射特征出现在波峰前还是波谷后。

import torch import torch.nn as nn import math class SeismicPositionalEncoding(nn.Module): def __init__(self, d_model, max_len=512): super().__init__() # 经典正弦位置编码,但对时间轴做了和图像不同的处理: # 这里把频率基调整得更大,因为地震波形变化比自然图像更慢 pe = torch.zeros(max_len, d_model) position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] = torch.sin(position * div_term) pe[:, 1::2] = torch.cos(position * div_term) pe = pe.unsqueeze(0) self.register_buffer('pe', pe) def forward(self, x): # x shape: (batch, seq_len, d_model) return x + self.pe[:, :x.size(1), :]

注意这里的关键调整:正弦函数的频率基系数和ViT默认一致,但当数据是地震道序列时,我通常会把频率基调低,让位置编码在时间方向上变化更平缓。原因在于地震道的有效信号分布在低频段,位置编码变化太剧烈会在浅层特征里注入大量高频噪声,干扰自注意力对反射同相轴的感知。代码里的max_len=512对应最长的时间采样数,实际工区采样超过这个值时,要做插值或分块处理,不能直接截断,否则深层储层的信息会丢失。

3.3 网络结构配置参考

我给一个在油气储层识别任务里经过验证的基础配置,适合patch_size=256、通道数为1的地震道输入:

模块参数设置说明
输入嵌入Conv1D 3×3,stride=2,输出维度384先做一次卷积下采样,让序列长度变成128
Transformer层数8层太浅学不到叠前反射特征,太深在井数据少的工区会过拟合
注意力头数每层8头和384维的嵌入维度匹配
FFN中间维度15364倍嵌入维度,和标准Transformer一致
位置编码SeismicPositionalEncoding用上面的自定义实现,频率下调一倍
Dropout0.1预训练阶段可以稍大,微调阶段建议降到0.05

这个配置在A100上预训练约3万条patch序列一次需要大约4小时,如果只有单卡,把层数降到6层,嵌入维度降到256,效果仍然可用。

4. 对比学习预训练:NT-Xent损失、温度系数与正负样本采样策略

4.1 温度系数选不好,预训练模型就是一堆废参数

对比学习最常用的损失是NT-Xent,也就是SimCLR里那个归一化温度标定交叉熵损失。这个损失里有一个最敏感的超参数——温度系数τ。它控制对难负样本的惩罚强度:τ越小,模型越会把注意力集中在和正样本相似的负样本上;τ越大,所有负样本被一视同仁,模型学出的特征会比较粗糙。在自然图像里SimCLR的默认τ是0.07,但地震数据我做过测试,0.07会直接导致训练不收敛,因为地震道之间的相似度天然比ImageNet图片之间更高,负样本区分度太弱,模型在过小的τ下会被难负样本带偏。

import torch import torch.nn.functional as F def nt_xent_loss(z1, z2, temperature=0.15): """ z1, z2: 两个增强视角的特征表示,shape (batch, embed_dim) 返回:NT-Xent对比损失标量 """ z1 = F.normalize(z1, dim=1) z2 = F.normalize(z2, dim=1) batch_size = z1.size(0) # 拼接成2N个样本,对应位置构成正样本对 z = torch.cat([z1, z2], dim=0) # 相似度矩阵,所有样本两两计算余弦相似度 sim = torch.mm(z, z.T) / temperature # 掩码对角线(自己是自己,不算) mask = torch.eye(2 * batch_size, device=sim.device).bool() sim.masked_fill_(mask, float('-inf')) # 计算每个样本的损失,正样本是它的配对样本 loss = 0.0 for i in range(batch_size): pos_idx = i + batch_size loss += -sim[i, pos_idx] + torch.logsumexp(sim[i, :], dim=0) return loss / (2 * batch_size)

这段代码里,temperature=0.15是我在地震道序列上试出来的一个比较稳的起点。如果你用的是剖面级Swin结构,建议用0.2到0.3之间,因为二维patch的高维空间里样本分布更稀疏,温度系数需要适当调大。损失值的走势监控是判断温度系数是否合适的有效手段:如果loss在前500步就掉到0.01以下,说明tau太小,模型在走捷径;如果2000步后loss还大于2.0,说明tau太大,要往下调。

4.2 预训练的训练机制:一个batch里到底放什么

对比学习的batch构成很有讲究。在地震数据里,一个batch里面要同时包含同一位置的增强视角对,也要包含来自不同位置、不同地震相的负样本。这里常见的错误是用随机采样的方式组batch,正样本对间距太远,模型根本学不到空间连续性。我一般会按inline方向的局部窗口来构建batch:取一个20×20的空间窗口,把所有道切出来的patch组成一个batch,这样batch内天然形成空间邻域结构,正负样本比例也合理。

class SeismicPairDataset(torch.utils.data.Dataset): def __init__(self, data_norm, window_size=20, patch_size=256): self.data = data_norm self.ws = window_size self.ps = patch_size # 构建空间窗口的起始位置列表,保证窗口之间有10%的随机偏移 inline_idx, crossline_idx = data_norm.shape[0], data_norm.shape[1] self.windows = [] for i in range(0, inline_idx - window_size, window_size): for j in range(0, crossline_idx - window_size, window_size): self.windows.append((i, j)) def __len__(self): return len(self.windows) def __getitem__(self, idx): i0, j0 = self.windows[idx] # 随机选一个锚点道 i = i0 + torch.randint(0, self.ws, (1,)).item() j = j0 + torch.randint(0, self.ws, (1,)).item() # 正样本:从锚点道附近随机取相邻道或加噪版本 if torch.rand(1) < 0.5: i_pos = min(max(i + torch.randint(-2, 3, (1,)).item(), 0), self.data.shape[0] - 1) trace_pos = self.data[i_pos, j, :] else: trace_pos = self.data[i, j, :] + 0.02 * torch.randn(self.ps) # 负样本:隔几个窗口取,确保空间距离足够远 i_neg = min(i + 5 * self.ws, self.data.shape[0] - 1) j_neg = min(j + 5 * self.ws, self.data.shape[1] - 1) trace_neg = self.data[i_neg, j_neg, :] return (torch.tensor(self.data[i, j, :], dtype=torch.float32), torch.tensor(trace_pos, dtype=torch.float32), torch.tensor(trace_neg, dtype=torch.float32))

上面代码里的0.02 * randn是加高斯噪声做数据增强,幅度控制很关键:太大会破坏反射特征,太小等于没加。负样本隔5个窗口取是为了保证空间上不重叠,避免模型只是靠“两条道物理位置接近”来分辨正负样本,而不是真正学到了储层特征。

4.3 多视角对比:除了空间走向,还能把振幅谱当作第二个视角

只用时间域波形做正样本对,模型学到的特征偏重波形形态。但我有个习惯会加一个频域视角:把同一道数据做短时傅里叶变换,转成时频图后当成第二个增强视角。油气储层的含流体特性在频率域有衰减特征,这个在时间域波形上很难直接显式建模,但通过对比学习把时间域和频域特征拉到同一个嵌入空间,模型在下游微调时只要一层线性层,就能把频域衰减这个隐含特征利用起来。

实现上不复杂,用torch.stft把波形转成时频复数谱,取幅值后做log缩放,再过一个2层卷积映射到和波形嵌入相同的维度。这个做法在含气储层识别上的增益尤其明显,因为气层对高频成分的吸收比水层和干层更剧烈。

5. 避坑指南:5条从地震数据翻车现场总结的实操记录

5.1 对比损失降下去了,但微调结果反而更差

现象是预训练阶段loss曲线很漂亮,从1.8降到0.4,但加载预训练权重做井标签微调后,储层识别的准确率比不用预训练还低两个百分点。

原因出在预训练和微调的数据分布不一致。预训练时我把全工区所有时间段的patch都放进去了,包括浅层未固结沉积、深层成岩段,这些时间深度的波形差异极大;而井数据主要集中在中深层目的层段。模型在预训练阶段学会的是“区分所有地层”,而不是“识别目的层内的储层相”。解决方法是预训练数据要加一个深度范围限制,只取目的层上下100ms窗口内的patch,宁可损失一些数据量,也要保证预训练域和下游任务域对齐。另一个次要点是,如果预训练用了大量低频增强,微调阶段关闭增强后会有domain gap,推荐微调时保留幅度在0.01以内的轻微噪声增强。

5.2 井标签太少,微调阶段疯狂过拟合

一个工区只有8口井参与训练,验证井2口,Transformer编码器在微调阶段参数量太大,训练集损失可以降到0.05,验证集损失却在不断上升。

解决过拟合,第一反应当然是加正则,但我在Transformer微调里验证过:Dropout从0.1加到0.3效果有限,真正有效的是冻结策略。具体做法是把预训练好的Transformer编码器前4层全部冻结,只微调后4层和分类头。因为浅层学到的是通用的波形形态特征,深层才是和储层相相关的语义特征。如果8口井仍然太少,还有一个更激进的选择——完全冻结编码器,只训练一层线性或者一个轻量的MLP做分类。这个做法把可训练参数量降到编码器总参数量的2%以内,8口井就足够训得住。

5.3 正样本对构造不当,模型学到的是“同一位置”而非“同一地层”

这是我踩得最重的坑。最开始我把正样本定义为“空间相邻道”,结果模型在特征空间里把两个相距很近的道聚在一起,却把同一套河道砂体在不同位置的道分开了。问题出在一个基础假设:地震道相似性不等于空间距离相近。由于构造倾角的存在,同一套地层在时间域上并不是水平的,空间相邻道在时间轴上的反射位置有系统性偏移。

解决的办法是在构造正样本对之前,先做一个基于互相关的时移校正:计算相邻道在时间轴上的局部时差,把时差补偿后再算相似度。另一种做法是直接用层位解释结果当作坐标变换的依据,把地震数据从时间域转换到地层域,保证同一个层位在相同的时间位置。很多工区有现成的层位解释结果,这一步处理成本不高,但收益非常大。

5.4 反演得到的储层概率图像“盐巴”一样很碎,没有地质连续性

对比学习预训练加微调后,输出的储层概率体热图在空间上异常破碎,单点预测概率跳变剧烈,完全不像是连续的地质体。

这个问题的根源是模型对逐道独立预测,没有空间上下文约束。Transformer编码器学习的是道内特征,对道间空间关系建模能力弱。解决思路是在微调阶段改造成果成结构:在Transformer输出后面接一个条件随机场或者简单一点的“空间平滑卷积头”,输出层用3×3×3的卷积核在三维上做平滑约束。还有一种做法是在训练损失里加一个空间平滑正则项,衡量相邻体素预测概率的差异。我的经验是加3×3的卷积输出头最简单有效,只需增加约0.1%的参数量,但概率图的连续性改善非常明显。

5.5 GPU显存耗尽,batch size只能设到4,模型完全训不动

三维SEG-Y数据体加载进显存是重灾区。很多工程师直接把整个inline剖面读进内存,或者把patch_size设得过大。patch_size=256已经不算小,但如果在Transformer输入阶段把3D体素块直接作为输入,显存消耗是2D情况的几十倍。我处理这个问题有两个习惯:一是输入阶段只吃道序列或2D剖面,3D的空间关系通过窗口attention来建模,而不是一次性把所有体素都喂进自注意力;二是梯度累积技术,batch_size设4,梯度累积步数设8,等效batch size就能到32,显存占用不变,训练稳定性明显提升。

6. 从预训练到储层概率体:伪井验证、盲井检验与部署技巧

6.1 伪井验证:没有井的地方怎么判断模型学对了

没有足够的真实井的时候,伪井验证是唯一的量化手段。做法是从已知井出发,把井曲线做插值外推,在无井区域构造合成伪井,用伪井的储层解释结果作为验证标签,看模型输出概率是否一致。这个做法的局限是伪井不能完全替代真实井,因为它和外推所用的已知井共享同样的测井响应模式,会掩盖模型在未知区域的系统性偏差。所以伪井验证只用来做训练过程的早停和超参选择,最终验收一定要用未参与训练的盲井。我一般会留至少1口井完全不参与微调,等所有实验做完才动它,否则你对模型的评价永远是被乐观偏差污染过的。

6.2 模型导出与推理速度:别让对比学习的前向计算拖住生产节奏

预训练和微调都在PyTorch里完成后,要落生产还要做一步模型精简。Transformer编码器8层384维的配置,在三维工区全道集上推理,单张A100大约每秒处理8000道。如果工区有500万道,全量推理要10分钟以上。我一般会做两件事:一是把模型转成ONNX格式,去掉训练相关的分支,推理速度能提升30%到50%;二是做量化,把权重从FP32压到FP16,显存占用减半,速度再提升约40%。如果生产环境只有CPU,那就要考虑把层数从8层修剪到4层,精度损失通常在2到3个百分点以内,但推理速度能快一个数量级,这种舍换在生产上是划算的。

6.3 一个管用的技巧:把对比学习的嵌入特征直接当属性体导出

这个技巧是我在一次项目里被逼出来的。当时甲方想要一版“不考虑井数据干扰”的原始地震相分类图,我直接把预训练模型产出的128维对比学习嵌入特征,用PCA降到3个通道,当成三个“新地震属性”送给解释工程师用。他们意外发现,这套无监督属性在刻画河道砂体边界时比传统的相干体、曲率属性更清晰。这背后的逻辑是:对比学习强制模型把相似地震相映射到相近嵌入,边界处嵌入向量变化陡峭,刚好就相当于一个“学出来的不连续性检测器”。如果你的工区本身井少、解释程度低,这个方案值得先跑一版,成本低,还能给后续井位部署提供一个不依赖先验的参考图。我自己后来的项目里,这版无监督属性体几乎成了交付的标配物,甲方反馈也一致说它的空间连续性比传统属性好。

对比学习不是一个万能的银弹,它的适用前提是数据自洽性足够好。在后续做相似项目时,我坚持一个习惯:预训练阶段绝不看井标签,微调阶段绝不碰测试井,验证阶段永远先看概率体的地质合理性再谈数值指标。希望这篇拆解对你在地震波Transformer和对比学习的落地评估上有所帮助。

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

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

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

立即咨询