赞
踩
文章介绍的是使用SeqSeq完成一个机器翻译任务,数据集使用的是torchrext中的multi30k从德语到英语,我们先介绍Seq2Seq的原理,再进行pytorch实现。
Seq2Seq的主要原理就是通过两个rnn一个充当编码器一个充当解码器,编码器的任务就是把机器翻译中源语言的句子给编码成向量,而解码器的作用就是把编码器编码好的向量再通过rnn一点点生成语言的句子,其中解码器的作用类似于文本生成。这里要注意的一些地方是,源语言(德语)和目标语言(英语)需要分开建立词库,就是不能把两种语言混合在一起。
这个图能很好的解释seqseq的机制,图中A代表一个循环神经网络例如普通rnn或者是lstm或者是gru都可以,图左边的部分代表编码器部分,编码器把输入的源语言(德语)的句子通过rnn编码得到输出h(m),这里的h(m)就是编码器rnn的输出的隐藏状态,把这个输出的h(m)做为解码器rnn的输入s(0)送入解码器网络(需要注意的是SeqSeq在数据处理时会把每个输入的源语言句子的最后加上一个"eos"字符,表示句子输入完毕,而在目标语言句子的开头会加上一个“sos”,结尾加上一个“eos”,这个sos代表的就是此处解码器网络的初始输入,然后一旦编码器输出eos,则代表翻译结束。还有数据处理的时候会对愿语言和目标语言分别做句子对齐处理,不够长的句子会用pad补齐。)得到s(0)后就要使用attention机制计算相关性a,a代表s与每个时刻h(t)的相关性。计算得到 m 个相关性 a 之后,将这些值与编码器的h(t)进行加权平均,即
计算a的方法如下图所示。
后面代码实现步骤会详细讲到怎么计算a这里,只要有个基本认识就好,a代表此时刻的s与编码器的各个时刻的隐藏状态h(t)的相关性,因为a最后通过softmax了,所以可以有个直观的理解,就是如果对应某一个编码器h(t)的a值特别大,则说明这一个时刻的h对c的贡献特别大,所以这一时刻解码器就会更关注这个h,这也就是注意力机制的直观理解。
解码器rnn的第一个输入为s(0),看作是rnn的初始隐藏状态,然后是目标句子的第一个词(sos)和计算得到的c(0)做拼接看作是编码器0时刻的输入,经过网络后,得到第一个时刻的输出隐藏状态s(1),再然后就是一样的把s1跟编码器所有时刻的h做attention计算c(1),再把s(1)做为隐藏状态,输入为c(1)和上一时刻输出的词做拼接(根据训练方式的不同这里可以是上一时刻编码器输出的词,也可以是目标句子实际的正确值),然后就是一直循环直到结束,这里的结束一般可以认为是某一时刻解码器输出了"eos",或者解码器输出的长度达到了目标句子的长度,就翻译结束。
这里我把一些容易疑惑的点说一下
1.seq2seq训练阶段和推理阶段不同,训练阶段有target监督知道输出的长度也知道输出的具体句子是什么,所以上面介绍的都是指训练阶段的过程,在推理阶段我们没有target,所以解码器的输入只有第一个时刻是已知的因为第一词都是“sos”,然后后面时刻解码器的输入xt就等于上一时刻解码器的输出
2…等待补充
Encoder部分就是一个循环神经网络,这里我使用的是gru。
encoder部分主要是以下三个部分
1. Embedding层,用于把trg目标句子的维度从[batch,trg_len]做词嵌入变到[batch, emb_dim]
nn.Embedding(input_dim, emb_dim)
2.gru层,关于gru的网络细节博客一搜一大把,这里不再赘述,简单来说就是简化版本的lstm比lstm更容易训练,这里我们使用的是单层双向gru
nn.GRU(emb_dim, enc_hid_dim, bidirctiona=TRUE)
3.fc全连接层,用于把encoder输出的enc_hidden变成decoder的输入。
enc_hid_dim*2是因为我们使用了双向gru,需要把最后一个时刻正向的隐藏层输出和反向的隐藏层输出拼接起来
nn.Linear(enc_hid_dim*2, dec_hid_dim)
接下来看到forward过程:
输入的src源语言数据维度为
[batch, src_len]
经过embedding层得到输出(调换维度,把batch放到第二个位置)
embedded[src_len, batch, emb_dim]
把embdedd送入gru得到enc_output和enc_hidden
enc_output [src_len, batch, enc_hid_dim*2]
enc_hidden [2, batch, enc_hid_dim]
把双向gru最后一个时刻正向隐藏状态和反向隐藏状态拼接起来送进fc全连接网络得到S0
S0 [batch, dec_hid_dim]
下面贴出具体代码
class Encoder(nn.Module): def __init__(self, input_dim, emb_dim, enc_hid_dim, dec_hid_dim, dropout): super().__init__() self.embedding = nn.Embedding(input_dim, emb_dim) self.rnn = nn.GRU(emb_dim, enc_hid_dim, bidirectional=True) self.fc = nn.Linear(enc_hid_dim * 2, dec_hid_dim) self.dropout = nn.Dropout(dropout) def forward(self, src): ''' src = [src_len, batch_size] ''' src = src.transpose(0, 1) # src = [batch_size, src_len] embedded = self.dropout(self.embedding(src)).transpose(0, 1) # embedded = [src_len, batch_size, emb_dim] # enc_output = [src_len, batch_size, hid_dim * num_directions] # enc_hidden = [n_layers * num_directions, batch_size, hid_dim] enc_output, enc_hidden = self.rnn(embedded) # if h_0 is not give, it will be set 0 acquiescently # enc_hidden is stacked [forward_1, backward_1, forward_2, backward_2, ...] # enc_output are always from the last layer # enc_hidden [-2, :, : ] is the last of the forwards RNN # enc_hidden [-1, :, : ] is the last of the backwards RNN # initial decoder hidden is final hidden state of the forwards and backwards # encoder RNNs fed through a linear layer # s = [batch_size, dec_hid_dim] s = torch.tanh(self.fc(torch.cat((enc_hidden[-2, :, :], enc_hidden[-1, :, :]), dim=1))) return enc_output, s
attention部分主要就是计算某一个时刻decoder的S与encoder隐藏状态的相关性,
就是图中的过程
先看第一个时刻S0
S0 [batch, dec_hid_dim]
enc_output [src_len, batch, enc_hid_dim*2]
计算相关性的步骤如下
1. 把s0变为[batch, src_len, dec_hid_dim]
2.把enc_output变为[batch, src_len, enc_hid_dim2]
3.拼接s0和enc_output并送进全连接网络
nn.Linear(dec_hid_dim+enc_hid_dim2, ?)
4.再送入一个全连接网络
nn.Linear(?, 1)
5.再送入softmax层计算得到相关性得到attention
attention [batch, src_len]
具体代码是
class Attention(nn.Module): def __init__(self, enc_hid_dim, dec_hid_dim): super().__init__() self.attn = nn.Linear((enc_hid_dim * 2) + dec_hid_dim, dec_hid_dim, bias=False) self.v = nn.Linear(dec_hid_dim, 1, bias=False) def forward(self, s, enc_output): # s = [batch_size, dec_hid_dim] # enc_output = [src_len, batch_size, enc_hid_dim * 2] batch_size = enc_output.shape[1] src_len = enc_output.shape[0] # repeat decoder hidden state src_len times # s = [batch_size, src_len, dec_hid_dim] # enc_output = [batch_size, src_len, enc_hid_dim * 2] s = s.unsqueeze(1).repeat(1, src_len, 1) enc_output = enc_output.transpose(0, 1) # energy = [batch_size, src_len, dec_hid_dim] energy = torch.tanh(self.attn(torch.cat((s, enc_output), dim=2))) # attention = [batch_size, src_len] attention = self.v(energy).squeeze(2) return F.softmax(attention, dim=1)
decoder也是一个gru,但是不同于encoder的是这是一个单层单向的gru
1.attention层,decoder需要调用attention类计算相关性
2.embedding层,这里的embedding是对目标语言做词嵌入
nn.embedding(output_dim, emb_dim)
3.gru层,把attention层结果a与enc_output相乘得到结果c,再把c与此时刻的dec_input拼接送入gru
nn.GRU(enc_hid_dim*2 + emb_dim, dec_hid_dim)
4. fc_out层,其实就是做一个output_dim类的分类器,输出此时刻decoder的输出。
forward过程如下
调用attention计算某时刻s与encoder隐藏状态的相关性得到a,s为decoder的隐藏状态初始为s0
a [batch, src_len]
enc_output [batch, src_len, enc_hid_dim*2]
把a与enc_output相乘(需要调整维度),使用batch内相乘torch.bmm得到结果c
c [batch, 1, enc_hid_dim*2]
调整维度得到c [1, batch, enc_hid_dim*2]
而此时刻decoder的输入embedded为[batch, emb_dim]
调整维度得到embedded [1, batch, emb_dim]
拼接embedded和c得到
[1, batch, enc_hid_dim*2 + emb_dim]
把结果和s送进gru得到结果dec_output和dec_hidden
dec_output [1, batch, dec_hid_dim]
dec_hidden [1, batch, dec_hid_dim]
把emdedded,dec_output和c拼接(需要调整维度)起来送进fc_out层做一个分类得到pred
下面是具体代码
class Decoder(nn.Module): def __init__(self, output_dim, emb_dim, enc_hid_dim, dec_hid_dim, dropout, attention): super().__init__() self.output_dim = output_dim self.attention = attention self.embedding = nn.Embedding(output_dim, emb_dim) self.rnn = nn.GRU((enc_hid_dim * 2) + emb_dim, dec_hid_dim) self.fc_out = nn.Linear((enc_hid_dim * 2) + dec_hid_dim + emb_dim, output_dim) self.dropout = nn.Dropout(dropout) def forward(self, dec_input, s, enc_output): # dec_input = [batch_size] # s = [batch_size, dec_hid_dim] # enc_output = [src_len, batch_size, enc_hid_dim * 2] dec_input = dec_input.unsqueeze(1) # dec_input = [batch_size, 1] embedded = self.dropout(self.embedding(dec_input)).transpose(0, 1) # embedded = [1, batch_size, emb_dim] # a = [batch_size, 1, src_len] a = self.attention(s, enc_output).unsqueeze(1) # enc_output = [batch_size, src_len, enc_hid_dim * 2] enc_output = enc_output.transpose(0, 1) # c = [1, batch_size, enc_hid_dim * 2] c = torch.bmm(a, enc_output).transpose(0, 1) # rnn_input = [1, batch_size, (enc_hid_dim * 2) + emb_dim] rnn_input = torch.cat((embedded, c), dim=2) # dec_output = [src_len(=1), batch_size, dec_hid_dim] # dec_hidden = [n_layers * num_directions, batch_size, dec_hid_dim] dec_output, dec_hidden = self.rnn(rnn_input, s.unsqueeze(0)) # embedded = [batch_size, emb_dim] # dec_output = [batch_size, dec_hid_dim] # c = [batch_size, enc_hid_dim * 2] embedded = embedded.squeeze(0) dec_output = dec_output.squeeze(0) c = c.squeeze(0) # pred = [batch_size, output_dim] pred = self.fc_out(torch.cat((dec_output, c, embedded), dim=1)) return pred, dec_hidden.squeeze(0)
1.encoder
2. decoder
记录trg(翻译目标语言)的长度和trg的词汇表大小
定义一个outputs初始tensor用来存储decoder的输出
先调用encoder得到encoder_output和s
然后是一个循环,在1到trg_len之间(decoder的第一个输出固定是"sos",每次输入一个词,直到输出"eos"结束。)
而且训练阶段使用了一个teacher_forcing机制,就是定义一个teacher_forcing_ratio比例,然后每次decoder产生一个0-1随机数,如果这个随机数小雨这个比例,下一个时刻decoder就使用该时刻预测的输出做为输入,否则就使用trg目标句子真实的此时刻的值做为下一时刻decoder的输入。
最后就是seqseq返回预测的output,并与trg对比预测损失。
class Seq2Seq(nn.Module): def __init__(self, encoder, decoder, device): super().__init__() self.encoder = encoder self.decoder = decoder self.device = device def forward(self, src, trg, teacher_forcing_ratio=0.5): # src = [src_len, batch_size] # trg = [trg_len, batch_size] # teacher_forcing_ratio is probability to use teacher forcing batch_size = src.shape[1] trg_len = trg.shape[0] trg_vocab_size = self.decoder.output_dim # tensor to store decoder outputs outputs = torch.zeros(trg_len, batch_size, trg_vocab_size).to(self.device) # enc_output is all hidden states of the input sequence, back and forwards # s is the final forward and backward hidden states, passed through a linear layer enc_output, s = self.encoder(src) # first input to the decoder is the <sos> tokens dec_input = trg[0, :] for t in range(1, trg_len): # insert dec_input token embedding, previous hidden state and all encoder hidden states # receive output tensor (predictions) and new hidden state dec_output, s = self.decoder(dec_input, s, enc_output) # place predictions in a tensor holding predictions for each token outputs[t] = dec_output # decide if we are going to use teacher forcing or not teacher_force = random.random() < teacher_forcing_ratio # get the highest predicted token from our predictions top1 = dec_output.argmax(1) # if teacher forcing, use actual next token as next input # if not, use predicted token dec_input = trg[t] if teacher_force else top1 return outputs
写到这里Seq2eq(attention)的pytorch实现就结束了,简单写了一下原理和代码实现,做个学习记录。
这里贴一下参考的博客和github地址
https://wmathor.com/index.php/archives/1451/
https://github.com/wmathor/nlp-tutorial
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。