ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

EQTransformer实战:连续波形事件检测与P/S震相拾取调优

EQTransformer实战:连续波形事件检测与P/S震相拾取调优

简介:面向地震学与深度学习交叉研究者的 EQTransformer 工具包,基于带注意力机制的分层深度神经网络完成地震信号检测与 P、S 相位拾取,可同时输出概率和模型不确定性,适用于连续波形处理、震相拾取及模型复现实验。资源包共 92 个文件、约 31.34MB,以 Python 源码和编译缓存为主,配有 RST 文档、IPython Notebook 示例、预训练 H5/HDF5 模型与样例数据,以及 PNG/JPG 可视化图片,另有环境配置、测试和许可证文件,便于直接运行、阅读文档并二次开发。已有 1176 人学习或下载。包内模块覆盖连续地震数据下载、预处理、检测、相位拾取、模型构建与测试,并提供简单的相位关联示例;配合多个 Notebook 演示训练、检测、可视化、下载和关联流程,读者可据此搭建从数据到拾取结果的完整实验链路,理解网络结构与不确定性估计,并在自有数据上迁移预训练模型。

1. 震后十分钟,先要一张能进定位程序的到时表

台网值班的人最怕的不是没数据,是三通道连续波形堆了几十 GB,而定位程序在等一张 P/S 到时表。人工挑震相,一条 24 小时记录顺利时也要十几分钟,赶上噪声大的台站,翻车概率极高。EQTransformer 这个 Python 包解决的就是这一段:喂进连续三通道波形,直接吐事件检测结果和 P、S 相位拾取时刻。它把 Transformer 的全局注意力机制和 LSTM 的时序建模拼进同一个神经网络,用多任务学习让「有没有事件」「P 在哪」「S 在哪」三件事共享一套编码,属于典型的深度学习地震信号检测方案。适合两类人:手上有一批 miniseed 想快速出一版目录的台网/观测业务人员,以及想拿它做迁移学习、换区域重新训练的开发。下面按「模型怎么搭 → 数据怎么喂 → 怎么跑 → 哪里会踩坑 → 怎么改」拆。

2. 模型结构拆解:一个 6000×3 的波形窗口如何走完全程

2.1 输入窗口与三路输出:为什么不拆成三个模型

EQTransformer 的输入张量形状固定为(6000, 3),也就是 100 Hz 采样率下 60 秒、三个分量(通常 Z/N/E)。这个窗口长度不是随手定的:太短盖不住 S 波后续的尾波,太长则池化后的时间分辨率不够,拾取误差会被放大。项目里从预处理、生成器到预测器,6000这个数字贯穿始终,改它等于改整个模型的形状契约,后面会细说。

模型的输出不是一路,而是三路等长的 6000 点概率序列:detector给每个采样点一个「这里属于某个事件」的概率,picker_P和picker_S分别给 P、S 到时的概率。三路共享前面同一套编码器,这是多任务学习的核心收益——检测任务见得多、标签相对可靠,它学到的「这段波形像不像事件」的表征会反哺拾取分支;反过来,精确到采样点的相位标签又逼着编码器保留高频细节。我见过有人把它拆成「一个检测模型 + 两个拾取模型」分别训练,然后抱怨小震漏检变多,本质就是把共享表征这条路堵死了。

代价也很直接:三路 loss 权重必须调。检测分支的标签在整段 60 秒里只有一小段是 1,正负样本极端不均衡;拾取分支的正样本更是只有几个采样点。权重给歪了,模型会退化成「全输出 0」这个局部最优,训练曲线看着在降,实际一个事件都检不出来。

2.2 编码器逐层拆:卷积、双向 LSTM、自注意力各管一段

编码器是「四层卷积 → 双向 LSTM → 若干 Transformer 块」的堆叠。卷积负责局部波形特征提取并降采样,四层池化把 6000 点压到 375 点,相当于每 16 个采样点合成一个时间步;双向 LSTM 在这个 375 步的序列上做前后向时序建模,抓 P 波到 S 波这种「有先后顺序」的包络关系;Transformer 块再叠上去做全局注意力,让模型能同时看到窗口首尾——这一点对区分「前 10 秒的微小事件」和「第 50 秒的噪声尖峰」很关键,纯 LSTM 在长序列上容易把远距离依赖稀释掉。

简化后的骨架长这样,层序和超参按项目里的默认值写:

# EQTransformer/core/models.py 的核心结构(精简示意,保留真实层序) import tensorflow as tf from tensorflow.keras import layers, Model, Input def conv_block(x, filters, kernel_size, pool=2): """卷积 + BN + ReLU + 池化:负责提局部特征并降采样""" x = layers.Conv1D(filters, kernel_size, padding="same")(x) x = layers.BatchNormalization()(x) x = layers.Activation("relu")(x) return layers.MaxPooling1D(pool_size=pool)(x) # 6000 -> 3000 -> ... -> 375 def transformer_block(x, num_heads=4, ff_dim=64, drop=0.1): """多头自注意力 + 前馈,两次残差 + LayerNorm,标准 Transformer 编码块""" attn = layers.MultiHeadAttention(num_heads=num_heads, key_dim=x.shape[-1])(x, x) x = layers.Add()([x, attn]) x = layers.LayerNormalization()(x) ff = layers.Dense(ff_dim, activation="relu")(x) ff = layers.Dense(x.shape[-1])(ff) x = layers.Add()([x, ff]) return layers.LayerNormalization()(x) def define_EQTransformer(input_dim=(6000, 3), cnn_blocks=4, lstm_units=64): inp = Input(shape=input_dim) x = inp # 四层卷积,滤波器数 8/16/32/64,核长 11/9/7/7:先宽后窄,前期看大尺度形态 for f, k in zip([8, 16, 32, 64], [11, 9, 7, 7]): x = conv_block(x, f, k) # 双向 LSTM:375 个时间步上做前后向建模,return_sequences 必须为 True x = layers.Bidirectional(layers.LSTM(lstm_units, return_sequences=True))(x) x = transformer_block(x) x = transformer_block(x) # 三路解码头:stride=2 反卷积堆 4 次,375 * 16 = 6000,回到采样点级别 outs = [] for name in ["detector", "picker_P", "picker_S"]: y = x for f in [32, 16, 8]: y = layers.Conv1DTranspose(f, 7, strides=2, padding="same", activation="relu", name=f"{name}_up{f}")(y) y = layers.Conv1DTranspose(1, 7, strides=2, padding="same", activation="sigmoid", name=name)(y) outs.append(layers.Reshape((input_dim[0],), name=f"{name}_out")(y)) return Model(inputs=inp, outputs=outs, name="EQTransformer")

几个参数值得单独说清。核长 11/9/7/7 的递减设计是血泪经验:第一层大核覆盖约 0.1 秒的波形,够看清 P 波起跳的陡度;后面层核变小是为了在降采样后不糊掉细节。conv_block里的padding="same"保证每层长度严格折半,四层下来正好 6000/16,不会出现张量长度对不齐的低级报错。解码头用Conv1DTranspose而不是简单UpSampling1D,是因为反卷积带可学习参数,能把编码器压掉的相位信息「补」回来,代价是参数量增加。最后一层sigmoid输出 0~1 概率,这个选择直接决定了 loss 只能是二分类交叉熵,换成 softmax 或回归 MSE 都会让收敛变难。

还有一处容易被忽略:layers.Reshape之前必须确保长度恰好 6000。如果你为了省显存把输入改成 3000 点,四层池化只能到 187.5,非整数会直接报错,这时候要么改池化层数,要么把输入补齐到 16 的整数倍——我一般倾向前者,网络结构跟窗口长度绑死是这类模型最常见的改造门槛。

2.3 多头输出与损失权重:多任务学习里的取舍

三路输出决定了整个训练配置的形态:loss 是一个字典,键必须和模型输出层的名字对应,权重分别控制检测和两类拾取的话语权。项目默认给的是检测 0.2、P 0.4、S 0.4——检测偏轻不是笔误,因为检测的标签在整个窗口上是稠密的(每个采样点都有 0/1),梯度贡献本来就大;拾取标签稀疏,需要更高权重才能把梯度拉起来。

# 编译时的三路 loss 配置:键名与输出层 name 必须一一对应 model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3), loss={"detector": "binary_crossentropy", "picker_P": "binary_crossentropy", "picker_S": "binary_crossentropy"}, loss_weights={"detector": 0.2, "picker_P": 0.4, "picker_S": 0.4}, metrics=["accuracy"], )

标签侧有个细节决定了模型能不能拾到「人眼级」的精度:训练标签不是把到时位置写成一个孤立的 1、其余全 0,而是用高斯核在到时附近做平滑(项目里对应input_gaussian_sigma这类参数,默认 3 个采样点量级)。原因很实际——人工标注的到时本身有 ±2~3 个采样点的抖动,硬标签等于逼模型去拟合标注误差,训出来的概率峰又尖又抖,做阈值判断时非常玄学。高斯平滑相当于告诉模型「这附近都算对」,概率峰变成一段圆滑的凸起,取 argmax 或取峰中心都稳。

调整权重时我一般按这个顺序试:先把 P、S 权重各降到 0.1 看检测分支能不能单独收敛(验证编码器没问题),再逐步加回去;如果 S 的 loss 死活下不去而 P 正常,八成是窗口里 S 已经跑出 6000 点边界,标签被截断成半个峰,这时候该动的是预处理阶段的窗口对齐策略,而不是继续调权重。这个顺序能省掉大量「改了权重但问题在数据」的无用功。

3. 目录结构与数据流:从 miniseed 到 X_prediction_results.csv

3.1 core / model / utils 三个目录各管什么

包的结构不复杂,但第一次打开还是容易找不到入口。核心逻辑全在EQTransformer/core/,预训练权重和结构描述放在EQTransformer/model/,辅助脚本在EQTransformer/utils/。按职责拆一下更直观:

EQTransformer/ ├── core/ │ ├── models.py # define_EQTransformer:网络结构定义 │ ├── trainer.py # Trainer 类:训练/验证/回调/保存权重 │ ├── generator.py # DataGenerator:从 hdf5 按批取数 + 在线增广 │ ├── mseed_predictor.py # mseed_predictor:连续波形滑窗检测的主入口 │ ├── preprocessing.py # preprocess:原始 miniseed -> 训练用 hdf5 │ ├── postprocessing.py # 滑窗结果去重、合并、按阈值出拾取 │ ├── losses.py # 自定义损失(含类别加权) │ ├── metrics.py # 训练期监控的指标 │ └── plotting.py # 波形 + 概率曲线叠加图 ├── model/ │ ├── EqT_original_model.h5 # 预训练权重 │ └── ... # 配套的结构描述文件(json/yaml) └── utils/ └── ... # 元数据准备等一次性脚本

分工背后的设计思路是「训练态」和「推理态」复用同一份模型定义。models.py只负责搭图,不碰数据;trainer.py负责喂 hdf5;mseed_predictor.py负责喂 miniseed 并按窗口切分。想加自定义 loss 就改losses.py,想换滑窗合并策略就动postprocessing.py,不用去碰预测器主流程。我一般改项目先看mseed_predictor.py,因为它是唯一一个把「读波形 → 归一化 → 滑窗 → 批量预测 → 后处理 → 写 CSV」全串起来的地方,读懂它等于读懂了推理链路。

3.2 训练集 HDF5 的字段约定,最容易被忽略的一环

自己训练时第一道坎不是模型,是数据格式。项目约定训练集是一个 HDF5 文件,里面至少有两组数据:data是波形张量,形状(N, 6000, 3);metadata是复合数据类型的表,一条波形一行,字段里带台站信息、到时采样点索引、拾取状态和权重。字段名错一个字母,生成器就会在取批时报 KeyError,而不是在加载时给你友好提示。

import h5py with h5py.File("sample_train.hdf5", "r") as f: print(f["data"].shape) # (N, 6000, 3):N 条 60 秒三通道波形 meta = f["metadata"] print(meta.dtype.names) # 复合 dtype,字段名就是列名 # 关键字段:p_arrival_sample / s_arrival_sample 是窗口内的采样点索引 print(meta["p_arrival_sample"][:5]) # p_status / s_status 标记这条样本的到时是否可用于训练 print(meta["p_status"][:5])

字段的含义要掰开看。p_arrival_sample是 P 到时在 6000 点窗口里的相对索引,不是绝对时间戳;如果你的预处理把波形截取起点搞错了,这个索引会整体偏移,训出来的模型拾取结果系统性偏早或偏晚,而且 loss 曲线看不出异常——这种系统性偏差最耗时间。p_status/s_status这类标记位用来区分「人工标注」「自动标注」「无标注」,生成器会据此决定这条样本要不要参与对应分支的 loss 计算,如果把无标注样本的 status 全填成 1,等于往训练集里灌噪声。snr字段在做数据筛选时很有用,想只保留高信噪比样本训练一个「干净版」模型时,直接按这一列卡阈值就行。

data里的数值范围也要留意。项目在预处理阶段对每条波形做去均值、去趋势,并按标准差归一化(预测时对应normalization_mode参数),所以 hdf5 里存的是已经归一化过的浮点序列。如果你拿原始 counts 值直接塞进 hdf5,训练初期梯度会爆到 nan,这时候别急着怀疑学习率——九成是归一化这一步漏了。

3.3 滑窗、重叠与归一化:预处理阶段埋的雷

连续波形预测走的是滑窗:从一条几小时的记录上,按 6000 点窗口、overlap(默认 0.3)的步进切出大量窗口,批量送进模型,再把每个窗口的概率序列映射回绝对时间轴。重叠 0.3 的含义是每次前进 4200 点,相邻窗口有 1800 点重合,这样落在窗口边界上的事件不会被切掉——一个事件的 P 波如果正好在窗口最后 100 点,不重叠的话 S 波就丢了,模型只能拾到一个 P。

from EQTransformer.core.preprocessing import preprocess # 原始 miniseed -> 训练用 hdf5(参数以本机 help(preprocess) 为准) preprocess( input_dir="mseed_raw", # 原始连续波形目录 output_dir="hdf5_out", # 输出 hdf5 + 元数据表 chunk_size=6000, # 与模型输入长度严格一致,不要随手改 )

chunk_size是这个调用里最不能乱动的参数,因为它同时被三处引用:模型输入层形状、生成器的批切分、预测器的滑窗步长。改一处不改其余两处,轻则报形状不匹配,重则静默出错——比如预测时窗口是 3000 点但模型期望 6000,某些版本会自动补齐零,检出的概率曲线在尾部被拉平,你会以为是「模型对小震不敏感」,其实是补零补出来的假象。

归一化模式也值得单独确认。std是按整条波形标准差归一化,对增益变化大的台站比较稳;但遇到那种一段平静一段强震的记录,整段标准差会被强震段拉高,平静段的小事件归一化后被压得几乎看不见。我的做法是对长记录先按固定长度分段再归一化,虽然多一步,但能明显改善小事件检出。这类调整不需要改模型,属于纯数据侧的收益。

4. 跑起来:环境、权重、三条命令

4.1 环境安装:TensorFlow / Keras 版本是生死线

这个包依赖 TensorFlow 的 Keras 接口,附带 obspy(读 miniseed)、h5py、pandas、matplotlib、scikit-learn。安装本身一条命令,但真正的坑在版本:项目成型于 Keras 2.x 时代,权重文件是旧格式.h5,里面还带自定义层。如果你装了最新版 TensorFlow(Keras 3 那一支),load_model十有八九直接抛错,或者更糟——加载成功但层序被重排,预测结果全是乱的。

# 建议先建独立环境,别往系统 Python 里装 python -m venv eqt-env source eqt-env/bin/activate # 先装 TensorFlow 2.x(Keras 2 分支),再装包本体 pip install "tensorflow==2.15.*" pip install EQTransformer # 装完立刻验证,别等跑预测时才发现 python -c "import tensorflow as tf; print(tf.__version__); import EQTransformer; print('ok')"

版本这一行不是保守,是踩过太多次。Keras 3 改了序列化格式和部分层的默认行为,MultiHeadAttention的key_dim推断逻辑也变过,旧权重加载后即使不报错,输出分布也会偏。稳妥做法是让 TensorFlow 停在 2.x、Keras 停在 2.x,并在环境里显式装tf-keras兼容包,遇到ModuleNotFoundError: keras之类的提示时它是第一顺位怀疑对象。Windows 上还多一条:路径里带中文或空格可能导致 h5py 读写失败,把工程目录放在纯英文路径下能省掉半天排查。

4.2 用预训练权重跑检测:mseed_predictor 参数逐个过

推理入口是mseed_predictor,一行调用就能从 miniseed 目录跑到 CSV。第一次跑建议只放一两个台站、一小时的波形,先确认链路通,再上全量——直接用几天的连续数据跑,中途因为某个台站缺分量报错,前面几十分钟的计算全白费。

from EQTransformer.core.mseed_predictor import mseed_predictor mseed_predictor( input_dir="mseed_data", # 待检测的 miniseed 目录(可递归) input_model="EqT_original_model.h5", # 预训练权重,路径指向包内 model 目录 stations_json="stations_list.json", # 台站与通道元数据,缺了会读不到三分量 output_dir="output", detection_threshold=0.3, # 事件检测概率阈值 P_threshold=0.3, # P 拾取概率阈值 S_threshold=0.3, # S 拾取概率阈值 overlap=0.3, # 滑窗重叠比例,边界事件的后悔药 number_of_plots=10, # 只画前 N 条,画太多 IO 拖慢整体 plot_mode="time", # 按时间轴出图 batch_size=500, # 显存不够就往下压 normalization_mode="std", )

参数里三个阈值是业务效果的主开关,别当成一次性配置。detection_threshold调低会捡回小震但误检上升,调高则目录变干净但漏震;P_threshold/S_threshold影响的是拾取点是否被保留,跟检测阈值是「与」的关系——三路都得过线,这条拾取才会进结果表。我一般先用默认 0.3 跑一遍当基线,再看误检集中在哪些台站,按台站单独配阈值比全局调更有效。batch_size是显存和速度的平衡点,6000×3 的窗口不算大,但几百条一批的中间激活值很占显存,OOM 时先降它而不是降窗口长度。

stations_json是最容易漏的参数。它提供台站坐标、通道方位等元数据,波形文件里虽然有台站名,但分量顺序和朝向不一定可靠,缺了这份元数据,模型可能把 E 分量当 Z 分量喂进去,检测概率会莫名整体偏低。这份文件的格式是「台站字典的列表」,常见写法:

[ { "network": "XX", "station": "ABC", "channels": ["HHZ", "HHN", "HHE"], "coords": [100.0, 30.0, 500.0] } ]

列表里每项对应一个台站,channels的顺序要和波形里的分量顺序一致,coords是经纬度和高程。批量准备这份文件时用脚本从台站目录生成,别手写——台站一多,漏一个通道就是一次静默的错误结果。

4.3 输出目录里有什么:CSV、报告、图

跑完先看output/下有没有预测结果表。这张表是整个流程的交付物,一条记录对应一次拾取,典型列包括台站/台网标识、事件时间、事件在窗口内的索引、检测概率、P 到时与 P 概率、S 到时与 S 概率。

import pandas as pd df = pd.read_csv("output/X_prediction_results.csv") # 二次过滤:三路概率都过线才留下,相当于把「与」逻辑再收紧一档 df = df[(df.detection_probability >= 0.3) & (df.p_probability >= 0.3) & (df.s_probability >= 0.3)].copy() # 时间列统一转成 datetime,方便和台网目录、定位程序对齐 df["p_arrival_time"] = pd.to_datetime(df["p_arrival_time"]) df["s_arrival_time"] = pd.to_datetime(df["s_arrival_time"]) print(len(df), df["station"].nunique())

过滤这一步看着多余,其实是必须的:预测器内部虽然有阈值,但后处理合并重叠窗口时保留的是「先到的那条」,某些情况下会出现检测概率高、拾取概率略低于阈值但仍被写进表的记录。再筛一遍能保证送进关联器(把多个台站的拾取拼成同一个事件)的数据是干净的。时间列务必转 datetime 再参与后续计算,字符串直接相减得到的是类型错误或者天知道什么东西。

除 CSV 之外,输出目录里通常还有一份文本报告(记录处理了多少文件、跳过多少、耗时)和若干张波形叠加概率曲线的图。图不要只当装饰,它是排查利器:概率曲线在事件位置有明显双峰(P 一个、S 一个)说明拾取正常;如果只有一个宽峰,多半是 P 和 S 挨得太近,或者窗口里 S 被截断;如果整段曲线都在 0.3 附近抖动,那是归一化或数据质量的问题,不是模型的问题。number_of_plots设成 10~20 够用,设成几百会让整个流程卡在 matplotlib 上。

4.4 自己训练:Trainer 的参数与关键超参

想换区域、换台网特性,预训练权重往往不够用,得自己训。入口是Trainer类,调用形式是「实例化 + 一次 train」,参数分成四类:数据、结构、优化、增广。下面这份配置是能跑通的起点:

from EQTransformer.core.trainer import Trainer trainer = Trainer() trainer.train( input_hdf5="sample_train.hdf5", input_dimention=(6000, 3), # 注意拼写就是 dimention,别手改成 dimension cnn_blocks=4, # 卷积块数,改这个要同步改解码头 lstm_blocks=1, batch_size=64, epochs=200, patience=3, # 验证 loss 连续 3 轮不降就早停 learning_rate_scheduler=True, # 余弦/阶梯衰减,后期收敛更稳 augmentation=True, # 在线增广,小数据集必开 loss_weights={"detector": 0.2, "picker_P": 0.4, "picker_S": 0.4}, train_valid_test_split=[0.85, 0.05, 0.10], output_name="my_eqt", model_dir="my_model", log_dir="my_log", )

input_dimention这个拼写是项目里的原样,写对才能被接受,这种小地方翻车过不止一次。train_valid_test_split里验证集只占 5%,看着少,但地震波形样本相关性很强,同一事件的多个台站记录如果分散到训练和验证两侧,验证 loss 会虚低——所以切分最好按事件划分而不是按样本随机划分,否则你看到的「验证集准确率 0.95」是假的。增广开关在小数据集上不能省,项目内置了加噪声、丢通道、随机平移、幅度缩放等策略,其中随机平移(把事件在窗口里左右挪)对拾取任务收益最大,等于零成本扩充了到时位置分布。

训练过程的产物是权重文件加日志。日志里重点看三条曲线:总 loss、三个分支各自的 loss、验证集指标。总 loss 平稳下降但 P 分支不降,说明权重或标签有问题;训练 loss 降验证 loss 升,那是过拟合,先减 cnn_blocks 或加 dropout,别急着加数据。200 轮不是硬指标,有早停的情况下大多数数据集在几十轮内就收敛了。

5. 避坑与排查:五个让结果整体出错的细节

5.1 数据与元数据类的坑

现象:预测跑完 CSV 是空的,或者事件数少得离谱,但日志显示文件都处理成功了。原因:stations_json里的台站名、通道名和 miniseed 里的实际命名对不上(大小写、台网代码前缀差异),预测器按元数据找不到对应通道,直接跳过该文件。解决:先用 obspy 读一个文件打印stats.network、stats.station、stats.channel,再逐字段比对 json,别靠肉眼猜。批量场景写个脚本做集合差集,一次把所有不匹配的台站列出来。

现象:训练 loss 前几轮就变成 nan。原因:hdf5 里存的是未归一化的原始 counts,数值量级在 1e4 以上,梯度直接爆。解决:检查data数据集的数值范围,正常应该在 -5~5 之间;超了就回到预处理阶段补上去均值、去趋势和标准差归一化,别在训练脚本里临时除一个常数糊弄过去。

5.2 模型加载与运行类的坑

现象:load_model报未知层、或者加载成功但预测概率全在 0.5 附近。原因:Keras 3 与旧.h5权重不兼容,或者自定义层(多头注意力、自定义损失)没通过custom_objects注册。解决:把 TensorFlow 固定在 2.x + Keras 2.x 分支,加载时显式传入项目里的自定义对象;确认不了就先只加载结构文件、再load_weights,两步走能定位到底是结构问题还是权重问题。

现象:跑长记录时内存一路涨到被系统杀掉。原因:一次性把所有滑窗结果留在内存里再后处理,几小时的三分量数据切出上千个窗口,激活值加起来很可观。解决:降batch_size,把输入目录拆成按天或按台站分批跑,输出 CSV 追加合并。这类批处理脚本我一般写成可断点续跑的,跑一半挂了不至于从头再来。

现象:同一条波形跑两次,拾取结果差了几十个采样点。原因:增广在推理阶段没关掉,或者随机种子没固定。解决:推理走mseed_predictor而不是自己搭生成器,确需自建流程时把增广标志位显式置 False 并固定随机种子;结果不可复现的模型在生产环境里基本没法用。

5.3 结果解读类的坑

现象:目录里事件数量是人工目录的三倍,全是误检。原因:检测阈值用了默认值,但目标区域的背景噪声水平(微震、海洋波浪、人文噪声)和预训练数据差异很大,模型把持续性噪声当成了事件。解决:先抽十几个误检样本画图看形态,如果都是同一类噪声,说明是数据域差异,不是阈值问题;此时要么提高检测阈值并加最短事件间隔约束,要么拿本地数据做微调。单纯调阈值压误检,通常会把小震一起压掉。

6. 二次开发:换区域数据比换模型划算得多

拿到这个包之后最想做的事通常是「换到我这片区域」。我的建议很明确:优先微调,不要从头训练。预训练权重在大规模波形上学到的低层特征(P 波起跳的陡度、S 波的能量包络)是通用的,从头训需要的数据量和算力大概率超出个人能承受的范围。

具体做法是冻结编码器、只放开三路解码头和最后一个 Transformer 块,用较小的学习率(1e-4 量级)在本地数据上跑十几轮:

import tensorflow as tf # compile=False 先拿结构,随后手工编译,避免旧优化器状态干扰 base = tf.keras.models.load_model("EqT_original_model.h5", compile=False) # 冻结前段编码器,只放开解码头(末尾十几层) for layer in base.layers[:-12]: layer.trainable = False base.compile( optimizer=tf.keras.optimizers.Adam(1e-4), # 微调学习率比预训练小一个量级 loss={"detector": "binary_crossentropy", "picker_P": "binary_crossentropy", "picker_S": "binary_crossentropy"}, loss_weights={"detector": 0.2, "picker_P": 0.4, "picker_S": 0.4}, )

判断微调是否有效的验证方法要说清:留出至少 20 个人工标注事件做测试集,且这些事件的台站不能出现在训练集里。只看训练 loss 下降没有意义,多任务模型很容易在本地小数据上过拟合到「记住这几个台站」。真正该看的是拾取误差的分布——P 波残差中位数能不能压到 0.05 秒以内,S 波在 0.1 秒以内,以及不同震级段的检出率是否均衡。如果小震检出率明显偏低而大震正常,说明微调把模型往「高信噪比偏好」上带偏了,增广里的加噪声比例要往上提。

另一个高性价比的改造方向是后处理和阈值策略,而不是动网络。把拾取结果送进关联器之前,加一层按台站自适应的阈值:用该台站一段时间的历史预测概率分布,取 95 分位作为动态阈值,比全局固定 0.3 稳定得多。再就是合并重叠窗口时的去重规则,项目默认留先到的那条,我一般改成「同一事件窗内 P 概率最高的那条」,实测能减少重复事件。这些都是纯 Python 逻辑,改起来风险低、验证快,比重新训练划算。

还有一个我常被问到的点:能不能加一个分量或者改成两分量输入。技术上可以,但输入形状是贯穿模型定义、生成器、预测器、权重的硬约束,改完预训练权重就废了,只能从头训。除非你的台网真的没有三通道数据,否则不值得。

踩过的坑里印象最深的一次,是改完预处理脚本忘了同步chunk_size,训练集窗口是 6000 点、预测时按 3000 点切,模型自动补齐零之后 S 波概率峰整个被拉平,我盯着图看了半天怀疑是权重加载错了,最后才发现是两行配置没对齐。从那以后我每次改窗口长度,都强制走一遍「打印模型输入形状 → 打印 hdf5 的 data.shape → 跑一条波形看输出长度」这三步核对,多花两分钟,能省掉一整天。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进