周报(6.24-6.30)
本周工作:
- 探索了DL-FWI经典模型的InversionNet的训练工作
- 了解常见优化器,如SGD、Adam等的区别,形成博客一篇。
- 构建数据集,优化数据加载。
构建数据集
我使用了Open-FWI的FlatVel_A数据集,该数据集大小为43GB,共30k条数据。要想把这些数据全部读入内存可不是个简单的事情。在训练之前,我观察了师兄的代码,师兄在构建数据集时直接将所有数据全部读入内存。我在学习过程中使用的电脑无法支持我将所有数据全部读入内存,因此采用了缓存机制加快运行速度。这种设计下数据集随机读取的代价仍然非常高昂,因此在加载DataLoader
时,需要关闭shuffle
参数。
为了方便模型在PyTorch上的调用,我将数据集包装为OpenFWI类。为了使用DataLoader调用数据集,需要在类里实现__len__
和__getitem__
方法。我的第一版数据集类如下:
class OpenFWI(Dataset):
def __init__(self):
self.DataPath = "/path/to/data/"
# 使用正则表达式提取数据文件的编号
pattern = re.compile(r"seismic(\d*)")
fileList = os.listdir(os.path.join(self.DataPath, "seismic"))
self.DataList = []
for i in fileList:
dataFileName = re.search(pattern,i)[1]
if dataFileName not in self.DataList:
self.DataList.append(dataFileName)
# 保存数据文件路径到DataList中
self.DataList.sort()
self.Offset = 500 # FlatVel_A 每个数据包含500条数据
self.Length = len(self.DataList)*self.Offset
# 缓存
self.Cache = {}
# 缓存队列,当缓存被占满时,释放最久读入数据
self.CacheQueue = Queue()
# 10文件缓存
self.CacheLength = 10
def __len__(self)->int:
return self.Length
def __getitem__(self, index:int)->Tensor:
realIndex = int(index / self.Offset)
realoffset = index % self.Offset
dataset = self.Cache.get(realIndex, None)
# 如果存在缓存
if dataset:
data = dataset["data"]
label = dataset["label"]
else:
# 删除过多缓存
if self.CacheQueue.qsize() >= self.CacheLength:
deleteAim = self.CacheQueue.get()
del self.Cache[deleteAim]
# 加载数据
data = np.load(os.path.join(self.DataPath, "seis%s.npy"%self.DataList[realIndex]))
label = np.load(os.path.join(self.DataPath, "vel%s.npy"%self.DataList[realIndex]))
# 更新缓存
self.Cache.update({
realIndex: {
"data":data,
"label":label
}
})
self.CacheQueue.put(realIndex)
data = Tensor(data[realoffset,:,:,:])
label = Tensor(label[realoffset,:,:])
return data,label
但实际运行后,我发现这样的数据仍有问题,虽然单文件包含的500条数据可以迅速的被读取,但一旦读取新的文件的数据时,读取过程仍然会阻塞训练。因此我设计了第二版数据集类。
class OpenFWI(Dataset):
def __init__(self):
self.DataPath = "/path/to/data/"
pattern = re.compile(r"seismic(\d*)")
fileList = os.listdir(os.path.join(self.DataPath, "seismic"))
self.DataList = []
for i in fileList:
dataFileName = re.search(pattern,i)[1]
if dataFileName not in self.DataList:
self.DataList.append(dataFileName)
self.DataList.sort()
self.Offset = 500 # FlatVel_A 每个数据包含500条数据
self.Length = len(self.DataList)*self.Offset
# 3数据缓存
self.CacheLength = 3
# 缓存队列
self.CacheQueue = Queue(self.CacheLength)
# 当前读取文件缓存
self.ThisCache = None
# 当前读取文件的编号
self.ThisIndex = -2
# 线程开始加载缓存的编号
self.ThreadIndex = 0
# 缓存线程
self.Thread = Thread(target=DataReadThreader, args=(self,))
# 线程运行状态
self.ThreadRunning = True
def __len__(self)->int:
return self.Length
def pushData(self, data):
self.CacheQueue.put(data)
def __getitem__(self,index):
realIndex = int(index / self.Offset)
realoffset = index % self.Offset
if realIndex == self.ThisIndex:
# 当 想要获取的数据在 当前读取文件 里时,直接返回对应的数据
return self.ThisCache[0][realoffset,:,:,:], self.ThisCache[1][realoffset,:,:,:]
elif realIndex == self.ThisIndex+1:
# 如果想要的是下一个文件的数据,就将下一个文件从缓存载入到 当前读取文件
self.ThisIndex += 1
self.ThisCache = self.CacheQueue.get()
return self.ThisCache[0][realoffset,:,:,:], self.ThisCache[1][realoffset,:,:,:]
else:
# 当数据既不在当前读取文件里,也不在下一个缓存文件里,则清空缓存,重启线程,设置线程读取文件编号
self.ThreadIndex = realIndex
# 设置当前文件编号
self.ThisIndex = realIndex
# 重启上个线程
if self.Thread.is_alive():
self.ThreadRunning = False
while not self.CacheQueue.empty():
self.CacheQueue.get()
self.Thread.join()
self.ThreadRunning = True
self.CacheQueue = Queue(self.CacheLength)
self.Thread = Thread(target=DataReadThreader, args=(self,))
self.Thread.start()
# 读入缓存,CacheQueue是队列,当队列为空时会阻塞,直到读入数据
self.ThisCache = self.CacheQueue.get()
return self.ThisCache[0][realoffset,:,:,:], self.ThisCache[1][realoffset,:,:,:]
# 缓存线程
def DataReadThreader(dataset:OpenFWI_Iter):
# 从设置的文件读取编号开始,依次读入所有数据
for i in range(dataset.ThreadIndex, len(dataset.DataList)):
if not dataset.ThreadRunning:
return
data = np.load(os.path.join(dataset.DataPath, "seismic", "seismic%s.npy"%dataset.DataList[i]))
label = np.load(os.path.join(dataset.DataPath, "vmodel", "vmodel%s.npy"%dataset.DataList[i]))
data = Tensor(data)
label = Tensor(label)
# 将数据压入缓存,CacheQueue是队列,当队列满时会阻塞,直到Dataset取出数据导致队列非满
dataset.CacheQueue.put((data, label))
这样一来,在训练过程中,数据集始终加载了未来要训练的数据,当模型训练时就能直接取出数据,减少IO时间。
模型训练
在训练前期,同一epoch下loss上下波动,不同epoch间loss下降缓慢,在参考师兄的代码做调整后loss仍然下降缓慢。但在交流学习后我了解到这是正常现象,参数较多的模型确实需要更多轮次和数据进行训练,检查模型的输出也逐渐有了速度模型的雏形。因此接下来准备进行完整的训练,对比几个优化器、损失函数的效果,同时预学习FCNVMB网络。
出现问题:
- 本地设备内存较小,无法将所有数据读入内存。通过改进数据集函数的方式完成优化。
- 模型Loss下降缓慢,根据结果来看确实在进行学习
下一步工作:
- 继续学习地震反演原理,深度学习原理。
- 对比我自己复现的代码与师兄的成熟代码,找到不同点,探索成熟代码的优势。
- 使用成熟代码改进复现InversionNet,解决已有问题,对比成熟代码的结果。
- 学习优化器中设计的权重衰减。