☰
周报(10.12-10.16)
2026/10/16 6:34:49 网站建设 项目流程

目录

一、本周计划

二、完成情况

2.1 论文本身

2.1.1 研究问题

2.1.2 核心方法

2.1.3 实验与结果(论文中的结果)

2.1.4 核心贡献与局限

2.2 InversionNet核心代码学习

2.2.1 network.py

2.2.1.1 Conv2DwithBN类

2.2.1.2 ConvBlock类

2.2.1.3 ConvBlock_Tanh

2.2.1.4 InversionNet类

2.2.1.5 Discriminator(判别器)

2.2.2 dataset.py

2.2.2.1 init初始化函数

2.2.2.2 load_every(self,batch)函数

2.2.2.3 __getitem__(self, idx)函数

2.2.3 test.py

2.2.3.1 evaluate()核心评估函数

2.2.3.2 模拟地震数据损坏场景(噪声+缺失道)

2.2.3.3 指标计算模块

2.2.4 train.py

2.2.4.1 train_one_epoch()

2.2.4.2 evaluate()

2.2.4.3 main(args)

三、存在问题

四、下一步计划

一、本周计划

通过研读论文《 InversionNet: An Efficient and Accurate Data-Driven Full Waveform Inversion》,在OpenFWI的数据集上复现InversionNet基线模型,研读网络结构代码。

二、完成情况

2.1 论文本身

2.1.1 研究问题

全波形反演(FWI)是地球物理和医学成像等领域的核心技术,用于从地震信号中推断地下速度结构。传统 FWI 被表述为优化问题,存在三大瓶颈:计算成本极高、病态性严重、容易陷入局部极小(环跳问题)。本文提出用深度学习直接学习反演算子,绕过传统迭代优化。

2.1.2 核心方法

InversionNet = CNN编码器-解码器 + 条件随机场(CRF)

1.编码器(卷积压缩)

  • 输入为地震数据(32道 × 1000时间步),时间维度远大于空间维度

  • 先用非方形卷积核(如 7×1、3×1)逐步压缩时间维度:1000→500→250→125→32

  • 时间维度压缩到位后,切换为标准方形卷积(3×3)在两个方向同时降维

  • 最终压缩为 512×1×1 的高维特征向量

2.解码器(转置卷积上采样)

  • 通过转置卷积(4×4核,步长2)逐步上采样:1→2→4→8→16→32

  • 每级上采样后跟一个 3×3 普通卷积做细化

  • 最终通过 1×1 回归层输出速度模型,并用 Tanh 激活约束输出范围

  • 由于卷积取整,输出略大于目标尺寸,最后中心裁剪

3.条件随机场(CRF)后处理

  • CNN 用像素级损失(L1)训练,无法捕捉地层边界的结构一致性

  • 在 CNN 输出上叠加局部连接 CRF:每个节点只与 d×d 窗口内的邻居连接

  • CRF 的一元势来自 CNN 预测值,二元势基于特征相似度和空间距离

  • 用均值场近似推理,复杂度为 O(cd²n),其中 c 为迭代次数

  • CRF 参数通过梯度上升学习

2.1.3 实验与结果(论文中的结果)

数据集:

  • FlatVel:36,000 个 100×100 速度模型(平坦地层)

  • CurvedVel:50,000 个 100×150 速度模型(弯曲地层,更具挑战性)

  • 合成地震数据用交错网格有限差分法 + 完美匹配层边界条件生成

主要发现:

  • CNN 模型在所有指标上显著优于物理驱动方法,MAE 从物理方法的 200-400 级别降到 10 级别

  • 添加 CRF 后进一步提升,尤其在层内一致性方面(d=20 时效果最佳)

  • 意外发现:普通 CNN 优于残差网络(ResNet 模块反而降低性能)

  • 模型约 3000 万参数,单卡 GTX 1080 Ti 训练 30 个 epoch,单次推理仅需毫秒级

  • 物理方法需要 154 个节点的 HPC 集群运行 1000 次迭代

PS:这里的实验结果是文章中作者自述的,我还没有自己跑通代码。

2.1.4 核心贡献与局限

贡献:

  • 首次提出完全端到端的 CNN-CRF架构用于 FWI,无需知道前向算子

  • 非方形卷积设计巧妙处理了地震数据的非对称维度

  • CRF后处理有效增强了地质结构的一致性

局限:

  • 对训练数据分布外场景的泛化能力有限

  • 训练集为合成数据,实际野外数据应用仍有差距

  • 未将物理方程显式融入学习过程(后续可结合 Physics-informed ML)

  • 不确定性量化尚未涉及

2.2 InversionNet核心代码学习

2.2.1 network.py

Pytorch实现的深度学习网络代码,包括基础卷积模块、生成器网络、判别器网络和模型注册字典

2.2.1.1 Conv2DwithBN类
class Conv2DwithBN(nn.Module): //in_fea 输入通道 out_fea 输出通道 //bn = true 控制要不要加入批量归一化 relu_slop = 0.2 LeakyReLU负半轴斜率0.2 def __init__(self, in_fea, out_fea, kernel_size=3, stride=1, padding=1, bn=True, relu_slop=0.2, dropout=None): super(Conv2DwithBN,self).__init__() layers = [nn.Conv2d(in_channels=in_fea, out_channels=out_fea, kernel_size=kernel_size, stride=stride, padding=padding)] //卷积层 if bn: layers.append(nn.BatchNorm2d(num_features=out_fea)) //归一化层 layers.append(nn.LeakyReLU(relu_slop, inplace=True)) //激活函数 if dropout: layers.append(nn.Dropout2d(0.8)) //随机丢弃层 PS:函数入参是dropou=None,但开启之后固定Dropout2d(0.8),不是Dropout2d(dropout),也就是说传任何值,dropout比率都是0.8 self.Conv2DwithBN = nn.Sequential(*layers) def forward(self, x): return self.Conv2DwithBN(x)

顺序:Conv2d卷积 —>(可选BN)—> LeakyReLU激活 —>(可选Dropout2d)

  • 卷积层:负责提取特征,改变通道数和图的尺寸

  • 归一化层(BN):默认开启,让数据分布更稳定,训练起来更容易收敛

  • LeakyReLU激活函数:给网络加入非线性化能力

  • Dropout层:可选,训练时随机丢弃一部分神经元,防止模型过拟合

2.2.1.2 ConvBlock类
class ConvBlock(nn.Module): def __init__(self, in_fea, out_fea, kernel_size=3, stride=1, padding=1, norm='bn', relu_slop=0.2, dropout=None): super(ConvBlock,self).__init__() layers = [nn.Conv2d(in_channels=in_fea, out_channels=out_fea, kernel_size=kernel_size, stride=stride, padding=padding)] if norm in NORM_LAYERS: layers.append(NORM_LAYERS[norm](out_fea)) layers.append(nn.LeakyReLU(relu_slop, inplace=True)) if dropout: layers.append(nn.Dropout2d(0.8)) self.layers = nn.Sequential(*layers) def forward(self, x): return self.layers(x)

和Conv2DwithBN对比核心差异

  1. 归一化不再简单是布尔bn=True/False,用norm字符串参数,可以选bn/in/ln三种归一化

  2. 内部序列的变量名改为self.layers,对应权重key就是layers

  3. 其他逻辑点一样,dropout只是开关,概率硬编码为0.8

2.2.1.3 ConvBlock_Tanh
class ConvBlock_Tanh(nn.Module): def __init__(self, in_fea, out_fea, kernel_size=3, stride=1, padding=1, norm='bn'): super(ConvBlock_Tanh, self).__init__() layers = [nn.Conv2d(in_channels=in_fea, out_channels=out_fea, kernel_size=kernel_size, stride=stride, padding=padding)] if norm in NORM_LAYERS: layers.append(NORM_LAYERS[norm](out_fea)) layers.append(nn.Tanh()) self.layers = nn.Sequential(*layers) def forward(self, x): return self.layers(x)

卷积+(可选bn)+ Tanh激活

Tanh输出范围[-1,1],用作生成器网络的最后一层,把输出约束在固定区间

后面采用两个上采样模块:

DeconvBlock:使用nn.ConvTranspose2d转置卷积做上采样,转置卷积容易出现棋盘格伪影

  • 优点:一步完成上采样+卷积,参数少

  • 缺点:容易产生棋盘伪影

ResizeBlock:先用Upsample插值放大特征图,在卷积,属于“上采样+卷积”,缓解棋盘格伪影,和旧版ResizeConv2DwithBN一一对应,是新版实现。

  • 先插值把特征图尺寸拉大,再用普通卷积融合特征

  • 普通卷积不会带来不均匀重叠,从根源规避棋盘伪影

  • 缺点:插值部分是固定操作,这部分不可学习

什么是棋盘格伪影(Checkerboard Artifact)?

出现场景:主要来自转置卷积(ConvTranspose2d,也叫反卷积)做上采样放大特征图的时候

产生原因:不均匀重叠(Uneven Overlap)

转置卷积是把小特征图变大,当卷积核大小、步长搭配不合适时:

输出特征图上有的像素位置会被卷积核多次覆盖,接受很多输入信息,旁边相邻像素被卷积核覆盖次数少,拿到的信息少

像素值交替出现一强一弱,二维空间形成黑白交错,像棋盘格子一样的周期性条纹噪声,就是棋盘伪影

2.2.1.4 InversionNet类

代码最核心的全波形反演网络,标准的编码器-解码器(encoder-decoder)结构,也叫U型网络

class InversionNet(nn.Module): def __init__(self, dim1=32, dim2=64, dim3=128, dim4=256, dim5=512, sample_spatial=1.0, **kwargs): super(InversionNet, self).__init__() #编码器:一堆卷积,尺寸越变越小,通道越变越多 self.convblock1 = ConvBlock(5, dim1, kernel_size=(7, 1), stride=(2, 1), padding=(3, 0)) self.convblock2_1 = ConvBlock(dim1, dim2, kernel_size=(3, 1), stride=(2, 1), padding=(1, 0)) self.convblock2_2 = ConvBlock(dim2, dim2, kernel_size=(3, 1), padding=(1, 0)) self.convblock3_1 = ConvBlock(dim2, dim2, kernel_size=(3, 1), stride=(2, 1), padding=(1, 0)) self.convblock3_2 = ConvBlock(dim2, dim2, kernel_size=(3, 1), padding=(1, 0)) self.convblock4_1 = ConvBlock(dim2, dim3, kernel_size=(3, 1), stride=(2, 1), padding=(1, 0)) self.convblock4_2 = ConvBlock(dim3, dim3, kernel_size=(3, 1), padding=(1, 0)) self.convblock5_1 = ConvBlock(dim3, dim3, stride=2) self.convblock5_2 = ConvBlock(dim3, dim3) self.convblock6_1 = ConvBlock(dim3, dim4, stride=2) self.convblock6_2 = ConvBlock(dim4, dim4) self.convblock7_1 = ConvBlock(dim4, dim4, stride=2) self.convblock7_2 = ConvBlock(dim4, dim4) self.convblock8 = ConvBlock(dim4, dim5, kernel_size=(8, ceil(70 * sample_spatial / 8)), padding=0) #解码器:一堆反卷积,尺寸越变越大,通道越变越少 self.deconv1_1 = DeconvBlock(dim5, dim5, kernel_size=5) self.deconv1_2 = ConvBlock(dim5, dim5) self.deconv2_1 = DeconvBlock(dim5, dim4, kernel_size=4, stride=2, padding=1) self.deconv2_2 = ConvBlock(dim4, dim4) self.deconv3_1 = DeconvBlock(dim4, dim3, kernel_size=4, stride=2, padding=1) self.deconv3_2 = ConvBlock(dim3, dim3) self.deconv4_1 = DeconvBlock(dim3, dim2, kernel_size=4, stride=2, padding=1) self.deconv4_2 = ConvBlock(dim2, dim2) self.deconv5_1 = DeconvBlock(dim2, dim1, kernel_size=4, stride=2, padding=1) self.deconv5_2 = ConvBlock(dim1, dim1) self.deconv6 = ConvBlock_Tanh(dim1, 1)
def forward(self,x): # Encoder Part x = self.convblock1(x) # (None, 32, 500, 70) x = self.convblock2_1(x) # (None, 64, 250, 70) x = self.convblock2_2(x) # (None, 64, 250, 70) x = self.convblock3_1(x) # (None, 64, 125, 70) x = self.convblock3_2(x) # (None, 64, 125, 70) x = self.convblock4_1(x) # (None, 128, 63, 70) x = self.convblock4_2(x) # (None, 128, 63, 70) x = self.convblock5_1(x) # (None, 128, 32, 35) x = self.convblock5_2(x) # (None, 128, 32, 35) x = self.convblock6_1(x) # (None, 256, 16, 18) x = self.convblock6_2(x) # (None, 256, 16, 18) x = self.convblock7_1(x) # (None, 256, 8, 9) x = self.convblock7_2(x) # (None, 256, 8, 9) x = self.convblock8(x) # (None, 512, 1, 1) # Decoder Part x = self.deconv1_1(x) # (None, 512, 5, 5) x = self.deconv1_2(x) # (None, 512, 5, 5) x = self.deconv2_1(x) # (None, 256, 10, 10) x = self.deconv2_2(x) # (None, 256, 10, 10) x = self.deconv3_1(x) # (None, 128, 20, 20) x = self.deconv3_2(x) # (None, 128, 20, 20) x = self.deconv4_1(x) # (None, 64, 40, 40) x = self.deconv4_2(x) # (None, 64, 40, 40) x = self.deconv5_1(x) # (None, 32, 80, 80) x = self.deconv5_2(x) # (None, 32, 80, 80) x = F.pad(x, [-5, -5, -5, -5], mode="constant", value=0) # (None, 32, 70, 70) x = self.deconv6(x) # (None, 1, 70, 70) return x

编码器部分:压缩总结

  • 输入:5通道的地震数据,尺寸1000 x 70

  • 经过8组卷积,一步步下采样

  • 输出:512通道,尺寸1 x 1,把一整张地震图的信息,压缩成一个512维的特征向量,包含了所有地下结构的信息

层级作用输入—>输出通道尺寸变化
convblock1初步下采样5—->321000 x 70—>500 x 70(高度减半,宽度不变)

convblock2_1

convblock2_2

第2组下采样+细化32—>64500 x 70—>250 x 70

convblock3_1

convblock3_2

第3组下采样+细化64—>128250 x 70—>125 × 70

convblock4_1

convblock4_2

第4组下采样+细化128—>256125 x 70—>63 × 70

convblock5_1

convblock5_2

第5组宽高同步下采样258—>12863 x 70—>32 × 35(宽度开始减半)

convblock6_1

convblock6_2

第6组同步下采样128—>25632 × 35—>16 × 18

convblock7_1

convblock7_2

第7组同步下采样256—>25616 × 18—>8 × 9
convblock8压缩256—>5128 × 9—>1 × 1(用同尺寸卷积核一步压成单点)

解码器部分:扩写还原

  • 输入:1 x 1的512维特征向量

  • 经过5组反卷积+卷积,一步步上采样放大

  • 中间穿插了一个裁剪操作,把80 x 80裁成70 x 70

  • 输出:用Tanh激活的卷积层输出1通道的结果,尺寸70 x 70,最后得到重建的地下速度模型图

层级作用输入—>输出通道尺寸变化

deconv1_1

deconv1_2

初始铺展特征512—->5121 x 1—>5 x 5(单点特征铺开成小图)

deconv2_1

deconv2_2

第2次放大+细化512—>2565 x 5—>10 x 10(尺寸翻倍,通道减半)

deconv3_1

deconv3_2

第3次放大+细化256—>12810 x 10—>20 × 20

deconv4_1

deconv4_2

第4次放大+细化128—>6420 x 20—>40 × 40

deconv5_1

deconv5_2

第5次放大+细化64—>3240 x 40—>80 × 80

F.pad(负参数)

裁剪边缘通道不变80 × 80—>70 × 70(裁掉四周各5像素,匹配目标尺寸)
deconv6(ConvBlock_Tanh)最终输出32—>170 × 70—>70 × 70(Tanh激活把数值限定在[-1,1])
2.2.1.5 Discriminator(判别器)

是一个纯编码器结构的二分类网络,用来判断输入的图是真实的还是生成的,和IversionNet是一对搭档,组成对抗性网络(GAN)

InversionNet:生成者,负责根据地震数据画地下速度模型

Discriminator:鉴别者,负责分辨一张图是真实的地下模型,还是生成者画出来的

举个例子:学画画的新手和画作鉴别师

InversionNet=学画画的新手,目标是画出和大师真迹一模一样的画,但最开始的时候他只会画一个轮廓,细节一眼看出来很假是模仿的

Discriminator=画作鉴别师,他见过大量的大师画作的真迹,专门负责判断一幅画是真迹还是新手模仿的赝品

训练的过程:一开始的时候,新手画得很粗糙,鉴别师一眼就能看出来是假的,告诉新手哪里哪里不对,新手根据反馈进行修改,画得越来越像,同时鉴别师见过的仿品也多了,鉴别的眼光也更加毒,能挑出更细的毛病。反复训练很多次后,新手画的几乎和真迹一模一样,鉴别师也只能靠猜,这样这个新手的画画水平就被接受了。

对应到地震反演里,为什么这么做?

如果只用InversionNet来生成地下模型,很容易输出模模糊糊/轮廓大概对但细节全错的情况,比如地层的边界不清晰、速度的分布不符合真实地质规律。

加上判别器一起训练之后,判别器见过大量的真实的地下地质模型,会专门挑生成结果的毛病,生成器为了骗过判别器,就必须去学习真实地质的细节,包括纹理、分布规律等,让自己把细节画对,画清晰,结果就是比只会单个网络的更清晰、更符合真实的地下地质规律。

class Discriminator(nn.Module): def __init__(self, dim1=32, dim2=64, dim3=128, dim4=256, **kwargs): super(Discriminator, self).__init__() self.convblock1_1 = ConvBlock(1, dim1, stride=2) self.convblock1_2 = ConvBlock(dim1, dim1) self.convblock2_1 = ConvBlock(dim1, dim2, stride=2) self.convblock2_2 = ConvBlock(dim2, dim2) self.convblock3_1 = ConvBlock(dim2, dim3, stride=2) self.convblock3_2 = ConvBlock(dim3, dim3) self.convblock4_1 = ConvBlock(dim3, dim4, stride=2) self.convblock4_2 = ConvBlock(dim4, dim4) self.convblock5 = ConvBlock(dim4, 1, kernel_size=5, padding=0) def forward(self, x): x = self.convblock1_1(x) x = self.convblock1_2(x) x = self.convblock2_1(x) x = self.convblock2_2(x) x = self.convblock3_1(x) x = self.convblock3_2(x) x = self.convblock4_1(x) x = self.convblock4_2(x) x = self.convblock5(x) x = x.view(x.shape[0], -1) return x

输出尺寸=⌊(输入尺寸 + 2 * 填充值 - 卷积核尺寸)/ 步长 ⌋ + 1

算的方法:

1.算输出高度

输入高度 = 70

输出高度 = ⌊ (70 + 2 * 1 - 3)/ 2⌋ + 1 = 35

2.输出宽度

输入宽度 = 70,参数和高度一样,输出宽度 = 35

3.输出通道数

输出通道数由out_fea参数直接决定,例如,dim1 = 32,故输出通道为32

4.批次维度

批次维度与输入保持一致

所以经过第一个后形状为(None, 32, 35, 35)【张量格式:[批次数量, 通道数, 高度, 宽度]】

2.2.2 dataset.py

PyTorch的torch.utils.data.Dataset自定义数据集类FWIDataset,用来读取地震反演任务的npy格式数据

2.2.2.1 init初始化函数
def __init__(self, anno, preload=True, sample_ratio=1, file_size=500, transform_data=None, transform_label=None): if not os.path.exists(anno): print(f'Annotation file {anno} does not exists') self.preload = preload self.sample_ratio = sample_ratio self.file_size = file_size self.transform_data = transform_data self.transform_label = transform_label with open(anno, 'r') as f: self.batches = f.readlines() if preload: self.data_list, self.label_list = [], [] for batch in self.batches: data, label = self.load_every(batch) self.data_list.append(data) if label is not None: self.label_list.append(label)

参数解释

anno:注释文件( txt 路径),txt 每一行存npy数据文件路径,标签路径

preload:是否一次性把全部数据集加载进内存,True全部读入内存,False用到哪个 npy 才去磁盘读取

sample_ratio:地震数据下采样比例

file_size:每一个 npy 文件里面包含多少个样本

transform_data:对输入地震数据做的预处理(Compose 组合)

transform_label:对标签(速度场)做预处理

2.2.2.2 load_every(self,batch)函数

解析annotation txt 中的一行文本,加载一组data(地震数据)和 label(速度场标签)

def load_every(self, batch): batch = batch.split('\t') data_path = batch[0] if len(batch) > 1 else batch[0][:-1] data = np.load(data_path)[:, :, ::self.sample_ratio, :] data = data.astype('float32') if len(batch) > 1: label_path = batch[1][:-1] label = np.load(label_path) label = label.astype('float32') else: label = None return data, label

::self.sample_ratio是numpy步长切片,对第三个维度做下采样

2.2.2.3 __getitem__(self, idx)函数
def __getitem__(self, idx): batch_idx, sample_idx = idx // self.file_size, idx % self.file_size if self.preload: data = self.data_list[batch_idx][sample_idx] label = self.label_list[batch_idx][sample_idx] if len(self.label_list) != 0 else None else: data, label = self.load_every(self.batches[batch_idx]) data = data[sample_idx] label = label[sample_idx] if label is not None else None if self.transform_data: data = self.transform_data(data) if self.transform_label and label is not None: label = self.transform_label(label) return data, label if label is not None else np.array([])

batch_idx:定位哪一个npy文件

sample_idx:定位在这个npy文件内部取第几个样本

全局样本索引,换算成「npy 文件序号 + 文件内部样本序号」

2.2.3 test.py

地震反演项目测试推理脚本,加载训练好的权重,读取验证数据集,跑模型推理,计算各项评价指标(MAE, MSE, SSIM),支持添加噪声,缺失道模拟真实恶劣地震数据,并输出绘图可视化结果

2.2.3.1 evaluate()核心评估函数
def evaluate(model, criterions, dataloader, device, k, ctx, vis_path, vis_batch, vis_sample, missing, std): model.eval()

参数说明

model:加载好权重的网络模型

criterions:字典,存放numpy版本评估函数(MAE/MSE)

dataloader:验证集

device:cuda/cpu设备

k:对数变换系数,和训练保持一致

ctx:从json读取数据集配置字典

vis_path:可视化图片输出文件夹,None代表不画图

vis_batch:最多可视化多少个batch

vis_sample:每个batch里面可视化几个样本

missing:模拟地震缺失道的数量,0=不模拟缺失

std:高斯噪声标准差,0=不加噪声

model.eval():设置模型为评估模式。作用:关闭 Dropout、冻结 BN 层的滑动均值 / 方差

切换模型训练模式<—>评估模式,如果不调用eval,模型默认处于train()训练模式,两个层会异常:Dropout、BatchNorm2d

  • Dropout:训练模式下 Dropout 会随机把一部分神经元输出置 0,做正则防止过拟合,测试时如果还开着 Dropout:每次跑同一张输入,网络输出会随机不一样,结果波动、不稳定

  • BatchNorm2d:处于 train 模式时,BN 仍然拿当前测试batch的数据去计算均值方差,并且还会尝试更新滑动统计量。测试集 batch 分布和训练集往往不一样,用测试 batch 自己统计出来的均值方差做归一化,就会把特征扰乱,输出结果严重偏离正确值。

with torch.no_grad(): batch_idx = 0 for data, label in dataloader: data = data.type(torch.FloatTensor).to(device, non_blocking=True) label = label.type(torch.FloatTensor).to(device, non_blocking=True)

torch.no_grad:

作用:关闭梯度计算图,如果不写的话,PyTorch会在后台为每一个前向传播操作构建计算图,这会导致显存迅速爆炸。

2.2.3.2 模拟地震数据损坏场景(噪声+缺失道)
if missing or std: # Add gaussian noise data_noise = torch.clip(data + (std ** 0.5) * torch.randn(data.shape).to(device, non_blocking=True), min=-1, max=1) # Mute some traces mute_idx = np.random.choice(data.shape[3], size=missing, replace=False) data_noise[:, :, :, mute_idx] = data[0, 0, 0, 0] data_np = T.tonumpy_denormalize(data, ctx['data_min'], ctx['data_max'], k=k) data_noise_np = T.tonumpy_denormalize(data_noise, ctx['data_min'], ctx['data_max'], k=k) data_list.append(data_np) data_noise_list.append(data_noise_np) pred = model(data_noise) else: pred = model(data)

高斯噪声:std为噪声方差,torch.clip裁剪,保证数值仍然处在归一化范围[-1,1],不能超出网络见过的数据区间

missing:随机挑选若干地震道,全部复制成同一个值,模拟部分地震记录丢失

如果传参设置--missing 5 --std 0.01,模型输入就是被破坏的地震数据,用来测试模型的鲁棒性。

2.2.3.3 指标计算模块
l1 = nn.L1Loss() l2 = nn.MSELoss() print(f'MAE: {l1(label_t, pred_t)}') print(f'MSE: {l2(label_t, pred_t)}') ssim_loss = pytorch_ssim.SSIM(window_size=11) print(f'SSIM: {ssim_loss(label_t / 2 + 0.5, pred_t / 2 + 0.5)}') # (-1, 1) to (0, 1) for name, criterion in criterions.items(): print(f' * Velocity {name}: {criterion(label, label_pred)}')
  • label_t、 pred_t:归一化后的Tensor,值域[-1,1],网络输出,GPU张量

  • label, label_pred:反归一化后的numpy数组,物理速度值,CPU内存

  • criterion字典:存放numpy实现MAE、MSE

2.2.4 train.py

地震反演项目训练主脚本,负责数据集加载、模型构建、损失函数、优化器、学习率调度、分布式训练、训练循环、验证、模型保存断点续训、TensorBoard 日志。

dataset.py数据集 —> train.py训练 —>test.py 测试推理

2.2.4.1 train_one_epoch()

单个epoch训练循环

def train_one_epoch(model, criterion, optimizer, lr_scheduler, dataloader, device, epoch, print_freq, writer): global step model.train() # Logger setup metric_logger = utils.MetricLogger(delimiter=' ') metric_logger.add_meter('lr', utils.SmoothedValue(window_size=1, fmt='{value}')) metric_logger.add_meter('samples/s', utils.SmoothedValue(window_size=10, fmt='{value:.3f}')) header = 'Epoch: [{}]'.format(epoch)
  • model.train():设置模型训练模式,开启Dropout、BN使用batch内统计量

  • MetricLogger / SmoothedValue:工具类,做滑动平均,打印平滑后的loss,避免单batchloss波动大

for data, label in metric_logger.log_every(dataloader, print_freq, header): start_time = time.time() optimizer.zero_grad() data, label = data.to(device), label.to(device) output = model(data) loss, loss_g1v, loss_g2v = criterion(output, label) loss.backward() optimizer.step()

标准pytorch训练五步

  1. optimizer.zero_grad():清空上一轮梯度

  2. 前向传播output = model(data)

  3. 计算loss

  4. loss.backward():反向传播,计算梯度

  5. optimizer.step():根据梯度更新网络权重

loss_val = loss.item() loss_g1v_val = loss_g1v.item() loss_g2v_val = loss_g2v.item() batch_size = data.shape[0] metric_logger.update(loss=loss_val, loss_g1v=loss_g1v_val, loss_g2v=loss_g2v_val, lr=optimizer.param_groups[0]['lr']) metric_logger.meters['samples/s'].update(batch_size / (time.time() - start_time)) if writer: writer.add_scalar('loss', loss_val, step) writer.add_scalar('loss_g1v', loss_g1v_val, step) writer.add_scalar('loss_g2v', loss_g2v_val, step) step += 1 lr_scheduler.step()

writer.add_scalar:向TensorBoard写入loss曲线,按全局step打点,不是epoch

step += 1:全局迭代次数

lr_scheduler.step():每个batch调用一次,不是每个epoch,这个调度器是按迭代次数(step)更新学习率,不是按轮次epoch

2.2.4.2 evaluate()
def evaluate(model, criterion, dataloader, device, writer): model.eval() metric_logger = utils.MetricLogger(delimiter=' ') header = 'Test:' with torch.no_grad(): for data, label in metric_logger.log_every(dataloader, 20, header): data = data.to(device, non_blocking=True) label = label.to(device, non_blocking=True) output = model(data) loss, loss_g1v, loss_g2v = criterion(output, label) metric_logger.update(loss=loss.item(), loss_g1v=loss_g1v.item(), loss_g2v=loss_g2v.item()) # Gather the stats from all processes metric_logger.synchronize_between_processes() print(' * Loss {loss.global_avg:.8f}\n'.format(loss=metric_logger.loss)) if writer: writer.add_scalar('loss', metric_logger.loss.global_avg, step) writer.add_scalar('loss_g1v', metric_logger.loss_g1v.global_avg, step) writer.add_scalar('loss_g2v', metric_logger.loss_g2v.global_avg, step) return metric_logger.loss.global_avg

与test.py的不是同一个

这里的evaluate只算归一化域的loss,不做反归一化,不计算物理MAE/MSE,不绘图

synchronize_between_processes():分布式训练专用,多卡的时候,每张卡只持有一部分验证样本,需要把所有进程 loss 做同步、求全局平均,不然指标只是单卡局部结果

2.2.4.3 main(args)
with open('dataset_config.json') as f: try: ctx = json.load(f)[args.dataset] except KeyError: print('Unsupported dataset.') sys.exit() if args.file_size is not None: ctx['file_size'] = args.file_size # Create dataset and dataloader print('Loading data') print('Loading training data') # Normalize data and label to [-1, 1] transform_data = Compose([ T.LogTransform(k=args.k), T.MinMaxNormalize(T.log_transform(ctx['data_min'], k=args.k), T.log_transform(ctx['data_max'], k=args.k)) ]) transform_label = Compose([ T.MinMaxNormalize(ctx['label_min'], ctx['label_max']) ])

PS:训练集、验证集transform必须完全一模一样,LogTransform、MinMaxNormalize、k系、min/max全部相同,验证集不能用任何数据增强

why验证集不能加数据增强?

1.破坏评估的客观性

训练集加增强:人为制造更多样本,让模型学的更鲁棒,验证集如果开启随机增强,每一次跑验证,样本都会被随机改的不一样

2.验证集要模拟真实推理

真实测试(test.py)输入是原始、未做随机改动的数据。 验证集的预处理,要和离线测试保持对齐:只做确定性变换(对数变换、归一化)不能带任何随机操作

3.指标不可复现

加随机增强后,相同权重跑两遍验证,得到的 loss 不一样,实验不可复现

三、存在问题

1.train()和test()中的evaluate()职责容易混淆

  • train.py:训练中只看loss,GPU张量计算

  • test.py:离线完整评估,反归一化、物理指标、绘图

2.对启动参数的记忆模糊,容易忘记参数对应的是什么作用是什么

3.可以用其他方法来替代CRF后处理吗?

四、下一步计划

1.完成论文中的实验复现,把CRF模块去掉,只跑纯CNN,对比有CRF和没CRF的结果差异,理解CRF在做什么

2.进一步深入学习InversionNet相关论文,准备阅读Song, C., et al. (2022)."OpenFWI: Large-scale Multi-structural Benchmark Datasets for Seismic Full Waveform Inversion."arXiv:2111.02926,对比InversionNet和更新的模型(FCNVMB、VelocityGAN 等)

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

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

立即咨询