做Stanford CS224N Deep Learning for NLP课程的作业的一些笔记,第三部分是Assignment4,这个作业实现了一个基于Attention机制的Seq2Seq模型用于实现机器翻译。
模型概览
这次的作业是利用PyTorch框架实现一个有注意力机制的Seq2Seq模型,并用于翻译任务。要实现的模型的总体架构如下图所示:
很明显模型被分成了Encoder和Decoder两个部分,作业首先比较详细的介绍了模型的基本架构。
Encoder部分
Encoder部分主要是一个双向的LSTM模型,我们假设输入的句子是一个长度为m的单词序列,每个单词的嵌入表示都是e维的,我们首先需要将这个输入序列送到双向LSTM中得到hidden和cell的状态:
同时Encoder输出的结果不仅有hidden和cell的状态,还包括了Decoder的初始状态,在Seq2Seq模型中,Decoder的初始输入要用Encoder的最终输出状态进行投影来得到,又因为是双向LSTM,所以需要将两个方向的最终输出合并之后进行投影:
Decoder部分
当Decoder有了初始状态之后,我们需要给Decoder输入一个目标句子,在第t步的时候,我们会查阅嵌入向量表来找到第t个位置对应的单词
老实说看完之后我都惊呆了,我不知道是谁想出来的这么复杂的策略,但事实证明这一套好像确实有效。
代码实现
虽然说总体的思路就和上面所说的内容差不太多了,但是具体的实现过程中还包含了一些trick
Padding操作
Seq2Seq模型为了更好的进行批处理,需要统一每个批次中输入数据的长度,方法是将所有输入的句子扩展到和最长的句子一样长,空的位置用一个padding token填充,我们需要实现这一操作,代码比较简单:
1 | def pad_sents(sents, pad_token): |
词汇表和Embedding
我们需要建立两个词汇表,分别对应source和targrt两种语言的不同词汇,并需要加入padding token,这一步比较简单,直接使用nn.Embedding()
就可以了
模型的初始化
这一部分需要我们在__init__
方法中定义若干关键的神经网络层,这一部分其实没啥好说的,就是对好输入输出的维度就可以了,特别要注意的一点是,torch.nn.Linear()
是一个实现
实现Encoder
虽然在模型中看起来实现一个Encoder只需要将输入数据丢进一个双向LSTM里就可以,但是实际上这里还有各种各样的操作要做:
- 首先输入的是一系列经过填充的token,我们需要先得到这些token的embedding
- 然后将其打包构成双向LSTM的输入X
- 在输入encoder之后得到所有的隐层状态和最后一个位置的hidden和cell
- 然后调用
pa d_packed_sequence
,并计算decoder中的初始状态作为结果。
- 这里的
pack_padded_sequence
和pad_packed_sequence
两个方法的作用分别是将填充后的数据进行压缩,将压缩后的数据恢复成原本的状态,这是PyTorch提供给RNN以及LSTM等结构使用的,用来阻止填充用的token进入RNN或者LSTM,因为这些token是无效的,还会浪费算力,因此使用这样一对函数可以起到现将padding后的数据压缩投入网络中,然后再将结果重新填充得到输出结果
1 | def encode(self, source_padded: torch.Tensor, source_lengths: List[int]) -> Tuple[ |
实现Decoder的总体逻辑
接下来我们需要实现decoder的基本逻辑,这里主要是实现decoder的循环操作,即得到通过decoder中的LSTM一步步得到联合输出向量并更新,输出下一步结果,最后将所有的联合输出向量返回,而每一步的隐状态和联合输出向量的计算则交给step这个函数完成。
1 | def decode(self, enc_hiddens: torch.Tensor, |
实现Decoder的逐步操作
这一步我们主要来实现step函数,这个函数的作用是实现decoder中需要进行循环的一些核心操作,包括将上一个hidden和cell状态输入decoder,然后对得到的结果做注意力计算,然后通过一系列莫名其妙的复杂运算得到当前步骤的联合输出向量并返回 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25def step(self, Ybar_t: torch.Tensor,
dec_state: Tuple[torch.Tensor, torch.Tensor],
enc_hiddens: torch.Tensor,
enc_hiddens_proj: torch.Tensor,
enc_masks: torch.Tensor) -> Tuple[Tuple, torch.Tensor, torch.Tensor]:
""" Compute one forward step of the LSTM decoder, including the attention computation.
"""
combined_output = None
dec_hidden, dec_cell = self.decoder(Ybar_t, dec_state)
dec_state = (dec_hidden, dec_cell)
e_t = torch.squeeze(torch.bmm(enc_hiddens_proj, torch.unsqueeze(dec_hidden, dim=2)), dim=-1)
# Set e_t to -inf where enc_masks has 1
if enc_masks is not None:
e_t.data.masked_fill_(enc_masks.bool(), -float('inf'))
# (b, src_len)
alpha_t = F.softmax(e_t, dim=-1)
a_t = torch.squeeze(torch.bmm(enc_hiddens.permute(0, 2, 1), torch.unsqueeze(alpha_t, dim=2)), dim=-1)
U_t = torch.cat((a_t, dec_hidden), dim=-1)
V_t = self.combined_output_projection(U_t)
O_t = self.dropout(torch.tanh(V_t))
combined_output = O_t
return dec_state, combined_output, e_t
- Decoder中多处使用了squeeze和unsqueeze函数,主要作用就是对张量进行维度压缩或者增加(squeeze可以去掉张量中大小为1的维度,而unsqueeze则可以给张量增加一个大小为1的新维度)
模型训练
因为做这个作业的时候手里还没有GPU,因此就没有在GPU上进行训练和测试(如果是Stanford的学生按照guidance可以使用免费的Azure VM),简单在本地试运行了一下发现可以跑起来,loss也在缓慢下降,就当作这次实验成功了吧。