重写dataset调用OpenFWI数据集,采用模仿FCNVMB的代码进行重写dataset。
运行OpenFWI中的InversionNet网络部分,并进行训练。抄代码,提取InversionNet相关代码重新写。
——数据加载:数据类型转换、归一化、数据维度······
OpenFWI数据集的FlatVel-A类共有60个npy文件,每个npy文件包含500条数据。PyTorch提供了两种datasets:map-style和iterable-style。如果使用map-style datasets会存在问题:由于Dataset支持随机读取,则实现__getitem__()时需要反复进行np.load()操作,这会导致读盘时间过长,拖慢训练速度。因此,使用IterableDataset类。
构造一个dataset数据类,需要继承Dataset,并重写Dataset中的方法:__init__、__getitem__
和__len__。
为了消除指标之间的量纲影响,需要进行数据标准化处理,以解决数据指标之间的可比性。Min-Max Normalization离差标准化,是对原始数据的线性变换,使结果值映射到[0-1]之间。转换函数如下:
x* = ( x − min ) / ( max − min )
其中max为样本数据的最大值,min为样本数据的最小值。该方法缺陷:当有新数据加入时,可能导致max和min的变化,需要重新定义。
# 定义自己的数据集
class FWIDataset(Dataset):
# anno:注释文件的路径
# preload:是否将整个数据集加载到内存中
# sample_ratio:地震数据的下采样率
# file_size:每个npy文件中的样本数
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
# 读文件,r:以只读方式打开文件。文件的指针将会放在文件的开头。
with open(anno, 'r') as f:
# readlines():返回列表,包含所有的行。即可以一次读取所有内容并按行返回list
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)
# Load from one line
def load_every(self, batch):
# split():拆分字符串。通过指定分隔符对字符串进行切片,并返回分割后的字符串列表list
batch = batch.split('\t')
# 切片操作
# [:-1]:除了最后一个取全部
data_path = batch[0] if len(batch) > 1 else batch[0][:-1]
# 取数组所有元素中每隔1步长的所有元素
data = np.load(data_path)[:, :, ::self.sample_ratio, :]
# 通过astype()方法强制转换数据的类型
data = data.astype('float32')
# 取label
if len(batch) > 1:
label_path = batch[1][:-1]
label = np.load(label_path)
label = label.astype('float32')
else:
label = None
return data, label
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([])
def __len__(self):
return len(self.batches) * self.file_size
可参考培训视频InversionNet部分。网络结构中展示的每个卷积操作实质上都是由卷积层/反卷积层、批归一化(BN)和LeaklyReLU共同构成的。
图1
图2
注:没有使用原论文中提到的1000*32的数据(图1),而是使用的1000*70的OpenFWI的数据(图2),这里采用的代码参照OpenFWI论文(2022年)中公开的InversionNet的修改版代码。
补充LeakyReLU:当输入为负值时,它会提供一个小的负斜率,从而允许一些负值通过,避免了梯度消失的问题。因此,LeakyReLU在处理一些具有稀疏数据的任务时表现更好。
# 对于一次卷积操作进行封装:conv2d -> nn.BatchNorm2d -> nn.LeakyReLU
NORM_LAYERS = { 'bn': nn.BatchNorm2d, 'in': nn.InstanceNorm2d, 'ln': nn.LayerNorm }
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))
# 激活函数层,LeaklyReLU通过将x的非常小的线性分量 给予 负输入αx来调整负值的零梯度问题,此外也可以扩大函数y的范围。
layers.append(nn.LeakyReLU(relu_slop, inplace=True))
if dropout:
# 赋值对象是彩色的图像数据(N,C,H,W)的一个通道里的每一个数据,
# 即输入为 Input: (N,C,H,W) 时,对每一个通道维度C按概率赋值为0。输出和输入的形状一致
# 0.8:元素置零的概率
layers.append(nn.Dropout2d(0.8))
# 输入也可以是list,然后输入的时候用*来引用,否则会报错 TypeError: list is not a Module subclass
# *作用在形参上,代表这个位置接收任意多个非关键字参数,转化成元组方式;
# *作用在实参上,代表的是将输入迭代器拆成一个个元素。
self.layers = nn.Sequential(*layers)
def forward(self, x):
# x:输入图像
return self.layers(x)
class InversionNet(nn.Module):
# dim:通道数,5是起始通道数
# input:(,5,1000,70)
def __init__(self, dim1=32, dim2=64, dim3=128, dim4=256, dim5=512, sample_spatial=1.0, **kwargs):
super(InversionNet, self).__init__()
# 一共8次
# 时间域的第一次降维,通过一批(32个)卷积来进行降维,(,32,500,70),H:1000->H:500
self.convblock1 = ConvBlock(5, dim1, kernel_size=(7, 1), stride=(2, 1), padding=(3, 0))
# 时间域的第二次降维,通过两批(64个)卷积来进行降维,(,64,250,70),H:500->H:250
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))
# 时间域的第三次降维,通过两批(64个)卷积来进行降维,(,64,125,70),H:250->H:125
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))
# 时间域的第四次降维,通过两批(64个)卷积来进行降维,(,128,63,70),H:125->H:63
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))
# 单方向降维结束,进入两个方向同时的降维。(,128,32,35),W:70->35 W:63->32
self.convblock5_1 = ConvBlock(dim3, dim3, stride=2)
self.convblock5_2 = ConvBlock(dim3, dim3)
# 两个方向同时的降维。(,256,16,18),W:35->18 W:32->16
self.convblock6_1 = ConvBlock(dim3, dim4, stride=2)
self.convblock6_2 = ConvBlock(dim4, dim4)
# 两个方向同时的降维。(,256,8,9),W:16->8 W:16->8
self.convblock7_1 = ConvBlock(dim4, dim4, stride=2)
self.convblock7_2 = ConvBlock(dim4, dim4)
# 512通道数,(512,1,1),失去空间信息
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) 125, 100
x = self.deconv6(x) # (None, 1, 70, 70)
return x
与训练和测试相关的文件:
- dataset_config.json:包含每个数据集的生成参数,包括最小/最大速度、大小、频率等。
- dataset.py:定义FWIDataset,它是 torch.utils.data 的子类,用于将每个数据样本从文本文件加载到数据加载器。
- network.py:InversionNet模型架构。
- train.py:执行 InversionNet的训练。
- transforms.py:提供归一化、度量记录、损失函数等所需的外设功能。
- utils.py:定义loss、评价指标等
- vis.py支持速度图像和地震数据的可视化。
定义损失函数,L1和MSE损失函数,每一次训练都希望损失函数下降,通过反向传播,模型被更新,不断优化。
TensorBoard:可视化工具,用于训练时需要保存中间结果,如模型参数(方便中断后继续训练)、损失函数(观察训练过程)等等。还可看网络结构
参数对象:采用argparse模块,导入对象argparse,定义所有我们要用到的参数。路径、模型、训练(学习率、轮次、batchsize、多线程等等)、损失函数、分布式训练、tensorboard······
argparse模块是命令行选项、参数和子命令解析器。适用于代码需要频繁地修改参数的情况。用法如下:
命令行参数分为必选参数和可选参数:
各参数解释如下:
1. 参数nargs:
nargs='*' :表示参数可设置零个或多个
nargs='+':表示参数可设置一个或多个
nargs='?':表示参数可设置零个或一个
2. 位置参数:
在命令行中传入参数时候,传入的参数的先后顺序不同,运行结果往往会不同,这是因为采用了位置参数。
3. 默认值:
add_argument中有一个default参数。有时需要对某个参数设置默认值,即如果命令行中没有传入该参数的值,程序使用默认值。如果命令行传入该参数,则程序使用传入的值。
4. '-' 和 '--':
不带-的是位置参数,带-的是可选参数,位置参数在调动时必须传值。
'--'参数:add_argument("-shortname","--name", help="params means")。
缩写命名时,缩写的名字(一个短线)和全名代表同一个名字,如-d 和 --device 是同一个名字。
但是,打印的时候要写全名。可以一个名字对应多个缩写,但不要一个缩写对应多个名字。如-d 对应 --device 和 --depth,这样会出错。
argparse --- 命令行选项、参数和子命令解析器 — Python 3.12.0 文档
1. Tensorboard的使用 ---- SummaryWriter类(pytorch版)
【精选】Tensorboard的使用 ---- SummaryWriter类(pytorch版)_chuanauc的博客-CSDN博客
2. torch.backends.cudnn.benchmark=True
将会让程序在开始时花费一点额外时间,为整个网络的每个卷积层搜索最适合它的卷积实现算法,进而实现网络的加速。用场景是网络结构固定(不是动态变化的),网络的输入形状(包括 batch size,图片大小,输入的通道)是不变的,其实也就是一般情况下都比较适用。反之,如果卷积层的设置一直变化,将会导致程序不停地做优化,反而会耗费更多的时间。
torch.backends.cudnn.benchmark ?! - 知乎 (zhihu.com)
3. python 使用 with open() as 读写文件
Python中 with open(file_abs,'r') as f: 的用法以及意义-CSDN博客
4. 何为文件句柄?
何为文件句柄??_文件句柄什么意思-CSDN博客
5. PyTorch之torchvision.transforms详解
PyTorch之torchvision.transforms详解[原理+代码实现]-CSDN博客
6. python的print字符串前面加f表示格式化字符串,加f后可以在字符串里面使用用花括号括起来的变量和表达式,如果字符串里面没有表达式,那么前面加不加f输出应该都一样.
7. readlines() 方法用于读取所有行(直到结束符 EOF)并返回列表,该列表可以由 Python 的 for... in ... 结构进行处理。如果碰到结束符 EOF 则返回空字符串。
8. nn.Dropout2d
看pytorch文档学深度学习——Dropout Layer - 知乎 (zhihu.com)
9. nn.Sequential(*layers)
Pytorch 容器-CSDN博客
PyTorch 中的 ModuleList 和 Sequential: 区别和使用场景 - 知乎 (zhihu.com)
10. 激活函数区别
激活函数sigmoid 、tanh、Relu、Leaky Relu 优缺点对比(最全)_leakyrelu激活函数优缺点-CSDN博客
11. 反卷积
反卷积通俗详细解析与nn.ConvTranspose2d重要参数解释_iioSnail的博客-CSDN博客
ConvTranspose2d原理,深度网络如何进行上采样?_dwconvtranspose2d-CSDN博客
问题1:(已解决)
——解决办法:在txt文件最后一行进行换行处理。debug时发现问题所在,原本的txt文件到最后一行内容就截止了,而每行最后是进行了换行处理,截图如下:
问题2:
——解决办法1: 在train.py中的DataLoader中的参数num_workers设置为0,不采用num_workers=args.workers。
——解决办法2(该代码是写在main函数中,仍报错):注意windows用户如果要使用多核多线程必须把训练放在if __name__ == '__main__':下才不会报错
问题3:loss仍然没有规律,并未一直下降。(已解决,正常现象,不断迭代更新的过程)
问题4:train.py中main函数里面,标签也归一化[-1,1]?
继续OpenFWI中InversionNet的代码学习。