pyTorch版OpenNMT的学习笔记
前言
2017年1月18日Touch7的开发团队发布了pyTorch,pyTorch是一个python优先的深度学习框架,能够在GPU加速的基础上实现Tensor计算和动态神经网络。
是的,相较于G家以静态图为基础的tensorFlow,pyTorch的动态神经网络结构更加灵活,其通过一种称之为「Reverse-mode auto-differentiation(反向模式自动微分)」的技术,使你可以零延迟或零成本地任意改变你的网络的行为。(然而我暂时并没有领略到这项技术的精髓... -.-!)
关于pyTorch细节的问题另做讨论,这里说一说正题--基于pyTorch实现的OpenNMT。
prepocess.py
preprocess.py相对来说比较好理解,但对于OpenNMT-py环环相扣的编程方法感到很新奇,函数封装的很细致,便于后续的debug或修改,对自己以后的编程是一个很好的启发。此外其代码很优雅(beam search部分除外,稍后会有介绍)。
关于这部分代码中makedata函数中:
if opt.shuffle == 1:
print('... shuffling sentences')
perm = torch.randperm(len(src))
src = [src[idx] for idx in perm]
tgt = [tgt[idx] for idx in perm]
sizes = [sizes[idx] for idx in perm]
print('... sorting sentences by size')
_, perm = torch.sort(torch.Tensor(sizes))
src = [src[idx] for idx in perm]
tgt = [tgt[idx] for idx in perm]
预先shuffle一下,再根据句子长度排序,这样在每一种长度的句子的内部,句子是顺序是随机的,按照句长排序,使每一个batch中的句长基本相等,以加快训练速度。
而以下这部分代码:
src += [srcDicts.convertToIdx(srcWords,
onmt.Constants.UNK_WORD)]
tgt += [tgtDicts.convertToIdx(tgtWords,
onmt.Constants.UNK_WORD,
onmt.Constants.BOS_WORD,
onmt.Constants.EOS_WORD)]
在tgt语句中,在句前加了BOS符号,在句末加了EOS符号。
prepocess.py最后保存了一个.pt文件,其中:
- dict:字典格式,保存有'src'和'tgt'的两个字典
- train:字典格式,保存有'src'和'tgt'两个Dict类
- valid:字典格式,保存有'src'和'tgt'两个Dict类
此外,还对dict字典进行了存储。
train.py
直接从main()函数的'Building model'开始说起吧,中间串联对各个函数的理解。
这里的encoder直接调用了pyTorch封装好的nn.LSTM()类,其初始化参数包括:
- input_size : input的Embedding_size
- hidden_size : 隐状态的数量
- num_layers : 层数
- bias : 默认为True,如果设置为False,网络将不使用 b_ih,b_hh。(详见链接中LSTM中的计算公式)
- batch_fisrt : 如果设置为True,输入和输出的形状将变为(batch x seq_length x embedding_size)
- dropout : 如果非0,除了最后一层,纵向层之间,丢弃(1-dropout)比例的隐藏神经元
- bidirectional : 默认为False,如果为True,成为双向的RNN
LTSM的输入为:input,(h_0,c_0) - input : seq_len x batch x enbedding_size
- h_0 : num_layers * num_directions x batch x hidden_size
-
c_0 : num_layers * num_directions x batch x hidden_size
输出为: - output :seq_len x batch x hidden_size * num_directions
- h_n : num_layers * num_directions x batch x hidden_size
-
c_n : num_layers * num_directions x batch x hidden_size
而decoder中self.rnn却是用LSTMCell()堆叠出来的,然而为什么要这么做呢?-.-!
LSTMCell()的输入输出维度为:
输入: - input : batch x embedding_size
- h_0 : batch x hidden_size
-
c_0 : batch x hidden_size
输出: - h_1 : batch x hidden_size
-
c_1 : batch x hidden_size
在decoder中引入了attention机制,类似于于pytorch tutorials中seq2seq模型中的attention机制,
图片截自pytorch tutorials
但又略有不同,如图在bmm的到attn_applied之后,OpenNMT-py代码没有选择将attn_applied与embedd相结合,而是经过一次softmax后变形为batch x 1 x src_sent_length(attn3) ,再和context 矩阵相乘(weightedContext)后与input连接(contextCombined),最后经过线性变化再取tanh后返回。
(其实对attention机制这样的处理方式并没有一个直观理解,求大神讲解)
模型部分说明完毕接下来看看trainModel函数,这里首先需要注意的一点是,在Dataset.py中重写了getitem方法,每次给trainData一个一个batchIdx去的是一个batch的数据,也重写了len方法,用len(trainData)返回的是numBatchs。
然后将batch输入进model,batch输入进model之后将tgt切掉最后一维EOS符号的,然后默认是以Teacher forcing的方式进行训练。Teacher forcing 就是将tgt的值作为decoder每次的输入,而不是使用其产生的预测值,这样做的好处就是可以使模型更快的收敛,但是对没有见到过的句子效果可能欠佳。
translata.py
这里面的重点是:Translator.py文件中的translateBatch()函数。
# (2) if a target is specified, compute the 'goldScore'
# (i.e. log likelihood) of the target under the model
goldScores = context.data.new(batchSize).zero_()
if tgtBatch is not None:
decStates = encStates
decOut = self.model.make_init_decoder_output(context)
self.model.decoder.apply(applyContextMask)
initOutput = self.model.make_init_decoder_output(context)
decOut, decStates, attn = self.model.decoder(
tgtBatch[:-1], decStates, context, initOutput)
for dec_t, tgt_t in zip(decOut, tgtBatch[1:].data):
gen_t = self.model.generator.forward(dec_t)
tgt_t = tgt_t.unsqueeze(1)
scores = gen_t.data.gather(1, tgt_t)
scores.masked_fill_(tgt_t.eq(onmt.Constants.PAD), 0)
goldScores += scores
其中这部分代码,是计算model翻译的结果与标准答案对比后获得分数,分数由翻译正确的词的概率取和得到。
接下来重点说明一下,OpenNMT-py优雅的代码中的一个槽点,beam-search部分,实在写的略难理解。
首先:
context = Variable(context.data.repeat(1, beamSize, 1))
decStates = (Variable(encStates[0].data.repeat(1, beamSize, 1)),
Variable(encStates[1].data.repeat(1, beamSize, 1)))
beam = [onmt.Beam(beamSize, self.opt.cuda) for k in range(batchSize)]
将encoder 输出的context,decStates各沿第二维方向重复beamsize遍,其中context维度由seq_len x batch x hidden_size * num_directions变为seq_len x batch*beamsize x hidden_size * num_directions,并将beam初始化为一个含有batch个Beam类的列表。
input = torch.stack([b.getCurrentState() for b in beam
if not b.done]).t().contiguous().view(1, -1)
这行代码将每个beam中上一时间步的预测值取出来,再将得到的batch x beam_size 转置成beam_size x batch 后在view成一行,没隔batch个数据属于同一个beam,形成beam_size个batch恰好与context和decStates的seq_len x batch*beam_size x rnn_size相对应。而model计算之后的out与input相对应,故
wordLk = out.view(beamSize, remainingSents, -1).transpose(0, 1).contiguous()
此处对view的计算方法论存疑,理解上out应该是batch * beam x num_words ,
wordLk = out.view(remainingSents, beamsize, -1).contiguous()
就可以直接得到batch x beam x num_words 。
然后关注Beam.advance()方法,
其中的prevKs是后指针,即记录的是这一步结果对应来自上一步nextYs的第几个值,nextYs记录的是每一时间步产生的beam_size个最佳结果的idx。
因为每次传进beam_size x num_words个值,展成一个列表之后选取的最佳beam_size个值在整除num_words后得到的是这个最佳值来自那个beam,而bestScoresId - prevK * numWords得到的是最佳结果的idx。
(另有细节问题,会不定时更新。)