李宏毅机器学习课程——Seq2Seq/Attention学习笔记

这一部分比较有难度,希望结合作业hw8代码,认真整理一下Seq2Seq与Attention机制的内容。本文主要是我对于这部分知识的理解和再加工,如有理解不当之处欢迎大家指出。文中的图文均来自于李老师的PPT。

多层LSTM

以下是LSTM的原理\(^{[3]}\)。

李宏毅机器学习课程——Seq2Seq/Attention学习笔记

一直以来对多层LSTM存在误解,把time_stepsn_layers搞混,其实两者是不同的\(^{[1]}\)​​​​​​。两者是正交的。时间序列是在每一个层内存在的,层与层之间是并行的。

李宏毅机器学习课程——Seq2Seq/Attention学习笔记

Seq2Seq

Seq2Seq表示模型输入一个序列,输出一个新序列。主要由Encoder于Decoder组成。

Encoder是一个多层的RNN结构,将输入序列投入Encoder中,最终产生一个向量,可以理解为将整个序列进行了学习得到了一个包含上下文信息的向量。

Decoder是RNN结构,从<START>开始逐个成分地生成序列。每个时间步t,Decoder接收Encoder产生的上下文向量与Decoder在(t-1)时刻的输出,产生t时刻的输出。通俗理解,Decoder需要知道整句话在说什么,并且自己刚刚说到哪里了,才能知道现在自己要说什么。

由于Encoder产生的中间向量——上下文向量在原始的Seq2Seq中是固定的,一个固定维度的向量很难一次性将整个输入序列的信息全部学到,为此引入了Attention机制来解决这个问题。

下图展示了一下student forcing下的Decoder\(^{[2]}\)​。

李宏毅机器学习课程——Seq2Seq/Attention学习笔记

Attention

引入了Attention机制后,Encoder不会只产生一个中间向量,而是会根据Decoder的在每一时间步t的输出再产生一个中间向量,同样这也是一个上下文向量,但是他更加注意了Decoder正在翻译的部分的上下文的信息,这样这个中间的上下文向量对于Decoder来说包含了更多的有效信息。通俗理解,Decoder翻译到哪里,Encoder就会给出那个地方附近的上下文。

在Seq2Seq的student forcing时,如何选择生成的下一个元素是非常重要的策略问题,其中一种重要的方法是beam-search,在Seq2Seq模型进行test时几乎都要使用这个方法,所以在这里也顺便整理一下。

如果每一步都选择生成概率最大的元素作为输出,往往会陷入局部最优,难以保证全局最优,如果要确保全局最优,使用穷举的方法,会导致搜索空间指数级爆炸,难以实现。所以两者折中就产生了beam-search。

在beam-search中,要先指定超参数k,表示beam-search的宽度,在每一个时间步t,Decoder会选择当前累计的生成概率最高的前k个,用于下一个时间步的探索,最终在得到的全序列生成概率最高的k个中选择最高的那个进行输出。

\(P(t) = P(w_1)P(w_2|w_1)P(w_3|w_1,w_2)...P(w_{t}|w_{1},w_{2},...,w_{t-1})\)​

如果输出是不定长的,会用平均每个元素的生成概率\(p(t)\)代替上述\(P(t)\)。

\(p(t)=\sqrt[t]{P(t)}\)

Coding

如果不需要了解细节,这一部分可以跳过。

以上对Seq2Seq和Attention的机制进行了一个宏观的概述,不同的实现中会遇到不同的细节,这里以李宏毅老师的机器学习中的版本为例,对细节进行一下梳理,以便coding的顺利进行n。

李宏毅机器学习课程——Seq2Seq/Attention学习笔记

在李老师的PPT中可以找到上图的每一步的详细步骤,这里不再赘述。下面结合核心代码对过程进行详细的解读,一下是未使用attention与beam-search的原始版本:

class Encoder(nn.Module):
  def __init__(self, en_vocab_size, emb_dim, hid_dim, n_layers, dropout):
    super().__init__()
    self.embedding = nn.Embedding(en_vocab_size, emb_dim)
    self.hid_dim = hid_dim
    self.n_layers = n_layers
    # 使用双向GRU作为Encoder的模型
    self.rnn = nn.GRU(emb_dim, hid_dim, n_layers, dropout=dropout, batch_first=True, bidirectional=True)
    self.dropout = nn.Dropout(dropout)

  def forward(self, input):
    '''
    	@param: input (batch_size, sequence_len, vocab_size)
    	@return: outputs (batch_size, sequence_len, hid_dim * directions)
    	@return: hidden (num_layers * directions, batch_size, hid_dim)
    	outputs是最后一层RNN的全部输出(sequence_len),可以交给attention处理。双向的RNN需要把最优的outputs在hid_dim维度上进行拼接。
    	hidden是每一层(num_layers)最后时间步的隐藏状态,传递到Decoder进行译码。
    	作为decoder每一层的初始的h0,所以要求Encoder与decoder的layers相同,与seq_len无关
    '''
    embedding = self.embedding(input)
    outputs, hidden = self.rnn(self.dropout(embedding))
    # hidden才是最终投喂给decoder作为初始隐层 即一次性将上下文信息告诉decoder
    # outputs用来计算上下文attention的
    return outputs, hidden

Decoder 是另一个 RNN,在最简单的 seq2seq decoder 中,仅使用 Encoder 每一层最后的隐藏状态来进行译码,而这最后的隐藏状态有时被称为 “content vector”,因为可以想象它对整个前文序列进行编码, 此 “content vector” 用作 Decoder初始隐藏状态, 而 Encoder 的输出通常用于 Attention Mechanism。

如下的维度细节需要考虑明白。

Encoder中,在多层rnn中,outputs是最后一层所有时间步(seq_len)的“隐层”输出,hidden是所有层最后一个时间步的“隐层”输出。两者在维度上都有一个hid_dim信息。directions信息体现在不同的维度位置上。

Decoder的输出和隐层并不是RNN的输出与隐层,RNN中(c,h)h隐层也是rnn的输出,c使用用来传递的,Decoder的输出还需要对RNN的隐层进行进一步的加工(如加一个线性层等)得到符合我们要求的输出。

class Decoder(nn.Module):
  def __init__(self, cn_vocab_size, emb_dim, hid_dim, n_layers, dropout, isatt=False):
    '''
    	@param isatt: 是否使用注意力机制
    '''
    super().__init__()
    self.cn_vocab_size = cn_vocab_size
    self.hid_dim = hid_dim * 2
    self.n_layers = n_layers
    self.embedding = nn.Embedding(cn_vocab_size, config.emb_dim)
    self.isatt = isatt
    self.attention = Attention(hid_dim)
    # 注意力机制下input拼接了上一个单词与注意力部分
    self.input_dim = emb_dim + hid_dim * 2 if isatt else emb_dim
    self.rnn = nn.GRU(self.input_dim, self.hid_dim, self.n_layers, dropout = dropout, batch_first=True)
    # hid_dim --> cn_vocab_size
    self.embedding2vocab1 = nn.Linear(self.hid_dim, self.hid_dim * 2)
    self.embedding2vocab2 = nn.Linear(self.hid_dim * 2, self.hid_dim * 4)
    self.embedding2vocab3 = nn.Linear(self.hid_dim * 4, self.cn_vocab_size)
    self.dropout = nn.Dropout(dropout)

  def forward(self, input, hidden, encoder_outputs):
    '''
    	batch_first == True
    	@param input(batch_size, vocab_size)
    	@param hidden(batch_size, n_layers * 1(directions), hid_dim)
    	t-1时刻decoder的隐层
    '''
    # Decoder是单向 directions为1 input要添加一个维度
    input = input.unsqueeze(1) # input(batch_size,1,vocab_size)
    embedded = self.dropout(self.embedding(input)) # embedded(batch_size,1,embed_dim)
    if self.isatt:
      attn = self.attention(encoder_outputs, hidden)
      # 需要将attention部分与单词的embed进行拼接作为带有注意力的input
      embedded = torch.cat((embedded,attn),dim=2) #embedded(batch_size,1,embed_dim + hid_dim*2)
    
    output, hidden = self.rnn(embedded, hidden) # output(batch_size,1,hid_dim)
    											# hidden(num_layers*1,batch_size,hid_dim)
    output = self.embedding2vocab1(output.squeeze(1))
    output = self.embedding2vocab2(output)
    prediction = self.embedding2vocab3(output) # 转为在不同的词的概率分布 prediction(batch_size,cn_vocab_size)
    return prediction, hidden
class Seq2Seq(nn.Module):
  def __init__(self, encoder, decoder, device):
    super().__init__()
    self.encoder = encoder
    self.decoder = decoder
    self.device = device
    # 直接将Encoder的隐层喂给decoder的隐层
    # encoder的所有层的最后step成为decoder的第一个step
    assert encoder.n_layers == decoder.n_layers, "Encoder and decoder must have equal number of layers!"
            
  def forward(self, input, target, teacher_forcing_ratio):
    '''
    	@param input (batch_size, input_len, vocab_size)
    	@param target (batch_size, target_len, vocab_size)
    	@param teacher_forcing_ratio 使用teacher forcing的几率
    '''
    batch_size = target.shape[0]
    target_len = target.shape[1]
    vocab_size = self.decoder.cn_vocab_size
    
    outputs = torch.zeros(batch_size, target_len, vocab_size).to(self.device)
    
    encoder_outputs, hidden = self.encoder(input)
    # (num_layers * directions, batch_size, hid_dim)-->(num_layers, directions, batch_size, hid_dim)
    hidden = hidden.view(self.encoder.n_layers, 2, batch_size, -1)
    # 将directions拼接到最后一个维度
    hidden = torch.cat((hidden[:, -2, :, :], hidden[:, -1, :, :]), dim=2)
    input = target[:, 0]
    preds = []
    for t in range(1, target_len):
      # target_len范围内
      output, hidden = self.decoder(input, hidden, encoder_outputs)
      outputs[:, t] = output
      teacher_force = random.random() <= teacher_forcing_ratio
      top1 = output.argmax(1)
      input = target[:, t] if teacher_force and t < target_len else top1
      preds.append(top1.unsqueeze(1))
    preds = torch.cat(preds, 1)
    return outputs, preds

  def inference(self, input, target):
    batch_size = input.shape[0]
    input_len = input.shape[1]        
    vocab_size = self.decoder.cn_vocab_size
    outputs = torch.zeros(batch_size, input_len, vocab_size).to(self.device)
    encoder_outputs, hidden = self.encoder(input)
    hidden = hidden.view(self.encoder.n_layers, 2, batch_size, -1)
    hidden = torch.cat((hidden[:, -2, :, :], hidden[:, -1, :, :]), dim=2)
    input = target[:, 0]
    preds = []
    # input_len范围内
    for t in range(1, input_len):
      output, hidden = self.decoder(input, hidden, encoder_outputs)
      outputs[:, t] = output
      top1 = output.argmax(1)
      input = top1
      preds.append(top1.unsqueeze(1))
    preds = torch.cat(preds, 1)
    return outputs, preds

beam-search与Attention的具体实现接下来会更新……

参考文献

[1] https://blog.csdn.net/weixin_41041772/article/details/88032093

[2] http://karpathy.github.io/2015/05/21/rnn-effectiveness/

[3] https://zhuanlan.zhihu.com/p/32085405

上一篇:数据学习系列1-VBA数组和循环语句(3)


下一篇:小记:EXCEL + VBA 用户记录:保留一个月的以前的登陆用户