个人写lstm为核心的算法时遇到的一些小问题,主要是变长序列的处理,多GPU训练的使用,以及在使用多GPU训练时同时处理变长序列时遇到数据不能正常传入模型等问题,总结如下。
1、变长序列处理
变长序列构成的数据集如果想以batch size>1的形式读入算法模型,需要统一长度。
pytorch架构下,使用:
“from torch.nn.utils.rnn import pad_sequence, pack_padded_sequence, pack_sequence, pad_packed_sequence”导入对应函数,各函数简要说明如下,详细的还是去看官方的说明文档和示例程序,更便于理解:
(1)、pad_sequence
对变长序列进行填充,填充后得到shape为{batch_size, length, dimension}的tensor。其中batch_size是自己设置的batch大小,length是该batch中各向量的最大长度,dimension是该batch中各向量的维数(需要一致)。
(2)、pack_padded_sequence
输入为经过“pad_sequence”或者已经手动填充补齐长度的向量组成的tensor,输出数据格式为“padedsequence”,内部由已根据时间点及batch排序的向量和对应的输入参数构成,这个不太容易说清,新手还是在python里自己试一下会有更好的理解。
(3)、pad_packed_sequence
输入“padedsequence”,输出为tuple,由填充好的序列组,shape为{batch_size, length, dimension},及该batch中各序列的真实长度数值构成的一维tensor。相当与把封装好的“padedsequence”还原成填充后的tensor。
(4)、pack_sequence
相当于pad_sequence()+pack_padded_sequence()
输入长度不等的多个序列,输出封装好的序列数据。
了解完各个函数,现在说明一下这几个函数具体的用法:
pytorch 的dataloder里有一个可以重写的函数叫做collate_fn, 在dataset.py(就是数据集文件)里重写此函数。(此处写法并非唯一,还有更简洁的写法,该函数的用法也不仅止于此,只要数据形式合理理论上它可以完成所有你设想的数据读取方式)
import torch
from torch.utils.data import dataset
from torch.nn.utils.rnn import pad_sequence, pack_padded_sequence, pack_sequence, pad_packed_sequence
....
class Dataset(dataset.Dataset):
def __init__(self, configs):
...
def __len__(self):
...
def __getitem__(self, item):
...
def collate_fn(data):
# data: 通过__getitem__()函数读到的数据,如果batch size设置为k,则此处读到的“data”即包含k个数据样本
# print(len(data))
datas = [] # 准备一个空的列表
for each in data:
datas.append(each[0])
datas.sort(key=lambda x: len(x), reverse=True) # 从长到短排序
seq_len = [s.size(0) for s in ndi_data] # real length of data list
datas = pad_sequence(datas, batch_first=True) # tensor:{batch-size,length,dim}
datas = pack_padded_sequence(datas, seq_len, batch_first=True)
return datas # packedsepuence:{tensor,length,...,...}
重写完collate_fn()函数后,在DataLoader()函数中加上collate_fn=collate_fn即可实现变长序列封装成mini-batch进行训练。数据以序列的形式经过LSTM等循环神经网络后需要使用pad_packed_sequence函数将序列还原为tensor!!(切记!!)
2、使用多个GPU 进行训练
有时候模型太大或者参数量太大需要使用多个GPU并行训练,语句很简单,就在main函数里加上
(1)os.environ["CUDA_VISIBLE_DEVICES"] = "0,1"
(2)device_id = [0,1]
(3)model = nn.DataParallel(model, device_ids=device_id)等几句话就好,(1)(2)可以放在训练入口处(就是if__name__ == '__main__':那里),(3)可以放在(1)(2)之后,如果使用的是多折交叉验证,(3)也可以写到多折交叉的函数里,要放在“...=train()之前”。
不过同时使用变长序列填充和多GPU训练时,会出一点小小的bug,因为从流程上来说,数据是先由dataloader函数读入训练函数中,在传进模型的,如果在dataloader读出数据之时,数据已经被封装成序列,在传入模型的时候序列数据“padedsequence”会被直接改成tuple类型数据,并且“padedsequence”中记录每个时间点该输入几个数据点的列表不能被分割而是复制后传入两个GPU,导致模型不能正常运行(lstm之类的模型可以接受tensor或者“padedsequence”输入,不能接受tuple,而且每个时间点该输入几个数据点的列表不能合理传入会导致状态矩阵不能正常初始化,其他模型我没试,可能还会有别的类型的bug。)
出现上述问题时,需要在collate_fn()函数中使输出为排好序的样本序列。在train函数中读到数据后将读到的数据列表,排序,统计长度信息列表(此列表必须转换成一维tensor!!),填充成长度一致的序列拼合的tensor(即完成pad_sequence),在模型的forward()函数中将传入的tensor封装成序列(padedsequence)再进行接下来的运算。