目录
一、本周计划
二、完成情况
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对比核心差异
归一化不再简单是布尔bn=True/False,用norm字符串参数,可以选bn/in/ln三种归一化
内部序列的变量名改为self.layers,对应权重key就是layers
其他逻辑点一样,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—->32 | 1000 x 70—>500 x 70(高度减半,宽度不变) |
convblock2_1 convblock2_2 | 第2组下采样+细化 | 32—>64 | 500 x 70—>250 x 70 |
convblock3_1 convblock3_2 | 第3组下采样+细化 | 64—>128 | 250 x 70—>125 × 70 |
convblock4_1 convblock4_2 | 第4组下采样+细化 | 128—>256 | 125 x 70—>63 × 70 |
convblock5_1 convblock5_2 | 第5组宽高同步下采样 | 258—>128 | 63 x 70—>32 × 35(宽度开始减半) |
convblock6_1 convblock6_2 | 第6组同步下采样 | 128—>256 | 32 × 35—>16 × 18 |
convblock7_1 convblock7_2 | 第7组同步下采样 | 256—>256 | 16 × 18—>8 × 9 |
| convblock8 | 压缩 | 256—>512 | 8 × 9—>1 × 1(用同尺寸卷积核一步压成单点) |
解码器部分:扩写还原
输入:1 x 1的512维特征向量
经过5组反卷积+卷积,一步步上采样放大
中间穿插了一个裁剪操作,把80 x 80裁成70 x 70
输出:用Tanh激活的卷积层输出1通道的结果,尺寸70 x 70,最后得到重建的地下速度模型图
| 层级 | 作用 | 输入—>输出通道 | 尺寸变化 |
deconv1_1 deconv1_2 | 初始铺展特征 | 512—->512 | 1 x 1—>5 x 5(单点特征铺开成小图) |
deconv2_1 deconv2_2 | 第2次放大+细化 | 512—>256 | 5 x 5—>10 x 10(尺寸翻倍,通道减半) |
deconv3_1 deconv3_2 | 第3次放大+细化 | 256—>128 | 10 x 10—>20 × 20 |
deconv4_1 deconv4_2 | 第4次放大+细化 | 128—>64 | 20 x 20—>40 × 40 |
deconv5_1 deconv5_2 | 第5次放大+细化 | 64—>32 | 40 x 40—>80 × 80 |
F.pad(负参数) | 裁剪边缘 | 通道不变 | 80 × 80—>70 × 70(裁掉四周各5像素,匹配目标尺寸) |
| deconv6(ConvBlock_Tanh) | 最终输出 | 32—>1 | 70 × 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训练五步
optimizer.zero_grad():清空上一轮梯度
前向传播output = model(data)
计算loss
loss.backward():反向传播,计算梯度
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 等)