To Top
首页 > 自然语言处理 > 正文

nmt

标签:nmt, paddle, lstm, gru


目录

paddle rnn系列文档: https://github.com/PaddlePaddle/Paddle/tree/develop/doc/howto/deep_model/rnn

0. rnn

numpy写的rnn:https://gist.github.com/karpathy/d4dee566867f8291f086

拷了一份过来:https://daiwk.github.io/assets/min-char-rnn.py

"""
Minimal character-level Vanilla RNN model. Written by Andrej Karpathy (@karpathy)
BSD License
"""
import numpy as np

# data I/O
data = open('input.txt', 'r').read() # should be simple plain text file
chars = list(set(data))
data_size, vocab_size = len(data), len(chars)
print 'data has %d characters, %d unique.' % (data_size, vocab_size)
char_to_ix = { ch:i for i,ch in enumerate(chars) }
ix_to_char = { i:ch for i,ch in enumerate(chars) }

# hyperparameters
hidden_size = 100 # size of hidden layer of neurons
seq_length = 25 # number of steps to unroll the RNN for
learning_rate = 1e-1

# model parameters
Wxh = np.random.randn(hidden_size, vocab_size)*0.01 # input to hidden
Whh = np.random.randn(hidden_size, hidden_size)*0.01 # hidden to hidden
Why = np.random.randn(vocab_size, hidden_size)*0.01 # hidden to output
bh = np.zeros((hidden_size, 1)) # hidden bias
by = np.zeros((vocab_size, 1)) # output bias

def lossFun(inputs, targets, hprev):
  """
  inputs,targets are both list of integers.
  hprev is Hx1 array of initial hidden state
  returns the loss, gradients on model parameters, and last hidden state
  """
  xs, hs, ys, ps = {}, {}, {}, {}
  hs[-1] = np.copy(hprev)
  loss = 0
  # forward pass
  for t in xrange(len(inputs)):
    xs[t] = np.zeros((vocab_size,1)) # encode in 1-of-k representation
    xs[t][inputs[t]] = 1
    hs[t] = np.tanh(np.dot(Wxh, xs[t]) + np.dot(Whh, hs[t-1]) + bh) # hidden state
    ys[t] = np.dot(Why, hs[t]) + by # unnormalized log probabilities for next chars
    ps[t] = np.exp(ys[t]) / np.sum(np.exp(ys[t])) # probabilities for next chars
    loss += -np.log(ps[t][targets[t],0]) # softmax (cross-entropy loss)
  # backward pass: compute gradients going backwards
  dWxh, dWhh, dWhy = np.zeros_like(Wxh), np.zeros_like(Whh), np.zeros_like(Why)
  dbh, dby = np.zeros_like(bh), np.zeros_like(by)
  dhnext = np.zeros_like(hs[0])
  for t in reversed(xrange(len(inputs))):
    dy = np.copy(ps[t])
    dy[targets[t]] -= 1 # backprop into y. see http://cs231n.github.io/neural-networks-case-study/#grad if confused here
    dWhy += np.dot(dy, hs[t].T)
    dby += dy
    dh = np.dot(Why.T, dy) + dhnext # backprop into h
    dhraw = (1 - hs[t] * hs[t]) * dh # backprop through tanh nonlinearity
    dbh += dhraw
    dWxh += np.dot(dhraw, xs[t].T)
    dWhh += np.dot(dhraw, hs[t-1].T)
    dhnext = np.dot(Whh.T, dhraw)
  for dparam in [dWxh, dWhh, dWhy, dbh, dby]:
    np.clip(dparam, -5, 5, out=dparam) # clip to mitigate exploding gradients
  return loss, dWxh, dWhh, dWhy, dbh, dby, hs[len(inputs)-1]

def sample(h, seed_ix, n):
  """ 
  sample a sequence of integers from the model 
  h is memory state, seed_ix is seed letter for first time step
  """
  x = np.zeros((vocab_size, 1))
  x[seed_ix] = 1
  ixes = []
  for t in xrange(n):
    h = np.tanh(np.dot(Wxh, x) + np.dot(Whh, h) + bh)
    y = np.dot(Why, h) + by
    p = np.exp(y) / np.sum(np.exp(y))
    ix = np.random.choice(range(vocab_size), p=p.ravel())
    x = np.zeros((vocab_size, 1))
    x[ix] = 1
    ixes.append(ix)
  return ixes

n, p = 0, 0
mWxh, mWhh, mWhy = np.zeros_like(Wxh), np.zeros_like(Whh), np.zeros_like(Why)
mbh, mby = np.zeros_like(bh), np.zeros_like(by) # memory variables for Adagrad
smooth_loss = -np.log(1.0/vocab_size)*seq_length # loss at iteration 0
while True:
  # prepare inputs (we're sweeping from left to right in steps seq_length long)
  if p+seq_length+1 >= len(data) or n == 0: 
    hprev = np.zeros((hidden_size,1)) # reset RNN memory
    p = 0 # go from start of data
  inputs = [char_to_ix[ch] for ch in data[p:p+seq_length]]
  targets = [char_to_ix[ch] for ch in data[p+1:p+seq_length+1]]

  # sample from the model now and then
  if n % 100 == 0:
    sample_ix = sample(hprev, inputs[0], 200)
    txt = ''.join(ix_to_char[ix] for ix in sample_ix)
    print '----\n %s \n----' % (txt, )

  # forward seq_length characters through the net and fetch gradient
  loss, dWxh, dWhh, dWhy, dbh, dby, hprev = lossFun(inputs, targets, hprev)
  smooth_loss = smooth_loss * 0.999 + loss * 0.001
  if n % 100 == 0: print 'iter %d, loss: %f' % (n, smooth_loss) # print progress
  
  # perform parameter update with Adagrad
  for param, dparam, mem in zip([Wxh, Whh, Why, bh, by], 
                                [dWxh, dWhh, dWhy, dbh, dby], 
                                [mWxh, mWhh, mWhy, mbh, mby]):
    mem += dparam * dparam
    param += -learning_rate * dparam / np.sqrt(mem + 1e-8) # adagrad update

  p += seq_length # move data pointer
  n += 1 # iteration counter 

1. LSTM & GRU

1.1 LSTM

参考https://en.wikipedia.org/wiki/Long_short-term_memory

最开始的lstm只有inputgate和outputgateLong short-term memory, 1997

后来在Learning to Forget: Continual Prediction with LSTM, 2000引入了forgetgate。

公式如下,其中,\(\circ \)表示element-wise的乘积:

\[ \\ f_t=\sigma _g(W_fx_t+U_fh_{t-1}+b_f) \\ i_t=\sigma _g(W_ix_t+U_ih_{t-1}+b_i) \\ o_t=\sigma _g(W_ox_t+U_oh_{t-1}+b_o) \\ c_t=f_t \circ c_{t-1}+i_t \circ \sigma _c(W_cx_t+U_ch_{t-1}+b_c) \\ h_t=o_t \circ \sigma _h(c_t) \]

其中,初始值\(c_0=0\)\(h_0=0\)\(\circ \)表示element-wise的乘积,也就是图中的\(\otimes \)

变量

  • \(x_t \in R^d\):LSTM block的input vector
  • \(f_t \in R^h\):forgetgate的activation vector
  • \(i_t \in R^h\):inputgate的activation vector
  • \(o_t \in R^h\):outputgate的activation vector
  • \(h_t \in R^h\):LSTM block的output vector
  • \(c_t \in R^h\):cell state vector
  • \(W \in R^{h \times d}\)\(U \in R^{h \times h}\)\(b \in R^{h}\):需要学习的权重和bias

激活函数

  • \(\sigma _g\): sigmoid
  • \(\sigma _c\): tanh
  • \(\sigma _h\): tanh,但在peephole LSTM中,这个是\(\sigma _h(x)=x\)

LSTM Recurrent Networks Learn Simple Context Free and Context Sensitive Languages, 2001以及Learning Precise Timing with LSTM Recurrent Networks引入peephole conneciton:

和lstm基本一样,区别:

  • \(f_t\)\(i_t\)\(o_t\)中的\(h_{t-1}\)都换成了\(c_{t-1}\)
  • 干掉了和\(i_t\)乘的那块的\(h_{t-1}\)
  • \(c_t\)的激活函数\(\sigma _h\)换成了线性激活,即\(\sigma _h(c_t)=c_t\)

可见这些改变,都是为了加强\(c\)的影响

\[ \\ f_t=\sigma _g(W_fx_t+U_fc_{t-1}+b_f) \\ i_t=\sigma _g(W_ix_t+U_ic_{t-1}+b_i) \\ o_t=\sigma _g(W_ox_t+U_oc_{t-1}+b_o) \\ c_t=f_t \circ c_{t-1}+i_t \circ \sigma _c(W_cx_t+b_c) \\ h_t=o_t\circ \sigma _h(c_t) \]



直观对比如下:



1.1.1 lstm为何可以缓解梯度消失

参考知乎

An Empirical Exploration of Recurrent Network Architectures的第2节



Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling的第3.3节



  • 传统的RNN总是用“覆写”的方式计算状态,\(S_t=f(S_{t-1},x_t)\),f是仿射运算后再套sigmoid。所以根据求导的链式法则,梯度会变成连积形式。多个小于1的连乘就会很快接近0,导致梯度消失。
  • 引入门控单元的rnn,使用的是”累加”的方式,\(S_t=\sum_{\tau=1}^{t}\Delta S_{\tau}\),这种累加形式导致导数也是累加形式,因此可以缓解梯度消失。

也就是说,传统的rnn是对\(\sigma _c(xxxh_{t-1})\)【其中\(\sigma _c\)是tanh】乘以w,然后再算sigmoid。而lstm对这项只是乘了一个(0,1)的系数(\(i_t\)),然后加上\(f_t \dot c_{t-1}\),再求tanh再乘一个(0,1)的系数(\(o_t\)),也就是说,并没有乘w,只是和一堆(0,1)的系数进行了线性变换,然后求和。。。

参考https://weberna.github.io/blog/2017/11/15/LSTM-Vanishing-Gradients.html

1.2 GRU

GRU是Cho等人在LSTM上提出的简化版本,也是RNN的一种扩展,如下图所示。GRU单元只有两个门:

  • 重置门(reset gate):如果重置门关闭,会忽略掉历史信息,即历史不相干的信息不会影响未来的输出。
  • 更新门(update gate):将LSTM的输入门和遗忘门合并,用于控制历史信息对当前时刻隐层输出的影响。如果更新门接近1,会把历史信息传递下去。

\(z_t\)【更新门】和\(r_t\)【重置门】都作用(sigmoid)于\([h_{t-1},x_t]\)【即上一时刻的隐层状态\(h_{t-1}\)和这一时刻的输入\(x_t\)】。

而重置门作用于\(h_{t-1}\)得到\(r_t*h_{t-1}\)【注意,是element-wise乘积】再与\(x_t\)一起经过tanh得到节点状态。

最终的输出是更新门与节点状态相乘,再加上\(1-z_t\)与上一时刻的隐层状态\(h_{t-1}\)的乘积。

总结一下:

  • 更新门和重置门都作用于上一时刻的隐层状态和这一时刻的输入。
  • 节点状态是本时刻的输入与经过重置门作用了的上一时刻的输出一起决定的。
  • 最终输出是更新门作用于节点状态,1-更新门作用于上一时刻隐层状态。


wikipedia的定义:

\[ \\ z_t=\sigma _g(W_zx_t+U_zh_{t-1}+b_z) \\ r_t=\sigma _g(W_rx_t+U_rh_{t-1}+b_r) \\ h_t=z_t \circ h_{t-1}+(1-z_t) \circ \sigma _h(W_hx_t+ U_h(r_t \circ h_{t-1}) + b_h) \]

激活函数

  • \(\sigma _g\): sigmoid
  • \(\sigma _h\): tanh

小结:

  • 没有输出门
  • 没有\(c\),这里的\(h\)就有一部分\(c\)的作用
  • \(z_t\)类似\(f_t\)的作用
  • \(1-z_t\)类似\(i_t\)的作用,但它乘的那个tanh里的元素,\(h_{t-1}\)\(r_t\)这个门来控制

2. 双向GRU

我们已经在语义角色标注一章中介绍了一种双向循环神经网络,这里介绍Bengio团队在论文【Bahdanau等人Neural Machine Translation by Jointly Learning to Align and Translate, ICLR, 2015】中提出的另一种结构。该结构的目的是输入一个序列,得到其在每个时刻的特征表示,即输出的每个时刻都用定长向量表示到该时刻的上下文语义信息。

具体来说,该双向循环神经网络分别在时间维以顺序和逆序——即前向(forward)和后向(backward)——依次处理输入序列,并将每个时间步RNN的输出拼接成为最终的输出层。这样每个时间步的输出节点,都包含了输入序列中当前时刻完整的过去和未来的上下文信息。下图展示的是一个按时间步展开的双向循环神经网络。该网络包含一个前向和一个后向RNN,其中有六个权重矩阵:输入到前向隐层和后向隐层的权重矩阵(W1,W3),隐层到隐层自己的权重矩阵(W2,W5),前向隐层和后向隐层到输出层的权重矩阵(W4,W6)。注意,该网络的前向隐层和后向隐层之间没有连接。



3. encoder-decoder框架

编码器-解码器(Encoder-Decoder)(Learning phrase representations using RNN encoder-decoder for statistical machine translation)框架用于解决由一个任意长度的源序列到另一个任意长度的目标序列的变换问题。即编码阶段将整个源序列编码成一个向量,解码阶段通过最大化预测序列概率,从中解码出整个目标序列。编码和解码的过程通常都使用RNN实现。



3.1 encoder

总体流程如下:



重点在于: 最后对于词xi,通过拼接两个GRU的结果得到它的隐层状态。



3.2 decoder



关键在于,预测的\(z_{i+1}\)由以下三部分经过非线性激活产生:

  • 源语言句子的编码信息\(c\)(\(c=qh\),如果不用注意力,可以直接取最后一个时间步的编码\(c=h_T\),也可以用时间维上的pooling的结果)。
  • 真实目标语言序列的第i个词\(u_i\)\(u_0\)是开始标志<s>
  • i时刻的RNN隐层状态\(z_i\), \(z_0\)是全零向量。

4. 注意力机制

如果在编码阶段的输出是一个固定维度的向量,会有以下两个问题:

  • 不论源语言序列长度是5个词还是50个词,如果都用固定长度的向量去编码其中的语义和句法信息,对模型来讲,是一个非常高的要求,特别对长句子而言。
  • 直觉上,当人类翻译一句话时,会对与【当前】译文更相关的源语言片段给予更多关注,且关注点会随翻译的进行而改变。

而固定维度的向量相当于,任何时刻都对源语言所有信息给予同等程度的关注,Bahdanau等人Neural Machine Translation by Jointly Learning to Align and Translate, ICLR, 2015引入注意力机制,对编码后的上下文片段进行解码,以此解决长句子的特征学习问题。

和简单的编码器不同之处在于,前面生成\(z_{i+1}\)用的是\(c\),而这里用的是\(c_i\),也就是,对每个真实目标语言序列中的词\(u_i\)【注意!!是目标语言的i,不是源语言的!!】,都有一个特定的\(c_i\)与之对应。

\[ z_{i+1}=\phi _{\theta }(c_i, u_i, z_i) \]

\[ c_i=\sum ^T_{j=1}a_{ij}h_j, a_i=[a_{i1}, a_{i2}, ..., a_{iT}] \]

可见,注意力机制是通过对编码器中各时刻的RNN状态\(h_j\)进行加权平均实现的。权重\(a_{ij}\)计算方法如下:

\[ c_i=\sum ^T_{j=1}a_{ij}h_j, a_i=[a_{i1}, a_{i2}, ..., a_{iT}] \]

\[ a_{ij}=\frac {exp(e_{ij})}{\sum _{k=1}^Texp(e_{ik})} \]

\[\ e_{ij}=align(z_i,h_j) \]

其中,align可以看作一个对齐模型,用于衡量目标语言第i个词(第i个隐层状态\(z_i\))和源语言第j个词(第j个词的上下文片段\(h_j\))的匹配程度。

传统的对齐模型中,目标语言的每个词明确对应源语言的一个词或多个词(hard alignment);而这里用的是soft alignment,即任何两个目标语言和源语言词间均存在一定的关联(模型计算出的实数值)。



柱搜索(beam search)算法是一种启发式图搜索算法,用于在图或树中搜索有限集合中的最优扩展节点。通常用在解空间非常大的系统(如机器翻译、语音识别)中,因为内存无法存下图或权中所有展开的解。

beam search使用广度优先策略建立搜索树,在树的每一层,按照启发代价(heuristic cost, 例如本例中的生成词的log概率之和)对节点进行排序,然后仅留下预先确定的个数(即beam width/beam size/柱宽度)的节点。只有这些节点会在下一层继续进行扩展,其他节点被裁剪掉了。可以减少搜索所占用的时间和空间,但无法保证一定获得最优解。

使用beam search的解码阶段,目标是最大化生成序列的概率,具体思路如下:

  • 每个时刻,根据源语言句子的编码信息c、生成的第i个真实目标语言序列单词\(u_i\),和i时刻RNN的隐层状态\(z_i\),计算出下一个隐层状态\(z_{i+1}\)
  • \(z_{i+1}\)通过softmax【只针对beamsearch得到的可能的解做处理】归一化,得到的目标语言序列的第i+1个单词的概率分布\(p_{i+1}\) \[ p(u_{i+1}|u_{\lt i+1}, x)=softmax(W_sz_{i+1}+b_z) \] 其中,\(W_sz_{i+1}+b_z\)对每个可能的输出单词进行打分
  • 根据\(p_{i+1}\)采样出单词\(u_{i+1}\)
  • 重复前三个步骤,直到获得句子的结束标记<e>或者超过句子的最大生成长度为止。

6. paddle demo

6.1 模型结构

一些全局变量

dict_size = 30000 # 字典维度
source_dict_dim = dict_size # 源语言字典维度
target_dict_dim = dict_size # 目标语言字典维度
word_vector_dim = 512 # 词向量维度
encoder_size = 512 # 编码器中的GRU隐层大小
decoder_size = 512 # 解码器中的GRU隐层大小
beam_size = 3 # 柱宽度
max_length = 250 # 生成句子的最大长度

6.1.1 编码器

输入是一个文字序列,被表示成整型的序列。序列中每个元素是文字在字典中的索引。

src_word_id = paddle.layer.data(
        name='source_language_word',
        type=paddle.data_type.integer_value_sequence(source_dict_dim))

然后映射成词向量src_embedding

src_embedding = paddle.layer.embedding(
     input=src_word_id, size=word_vector_dim)

然后使用双向gru进行编码,然后拼接两个gru的输出得到\(h\)【解码器中的\(h_j\)】即encoded_vector

src_forward = paddle.networks.simple_gru(
     input=src_embedding, size=encoder_size)
src_backward = paddle.networks.simple_gru(
     input=src_embedding, size=encoder_size, reverse=True)
encoded_vector = paddle.layer.concat(input=[src_forward, src_backward])

6.1.2 解码器

6.1.2.1 得到对齐模型

首先,对源语言序列编码后的结果【\(h_j\)】过一个fc,得到其映射encoded_proj【即,\(h_j\)\(z_i\)的映射,也就是对齐模型\(e_{ij}\)】。

encoded_proj = paddle.layer.fc(
      act=paddle.activation.Linear(),
      size=decoder_size,
      bias_attr=False,
      input=encoded_vector)
6.1.2.2 构造解码器RNN的初始状态

由于解码器需要预测时序目标序列,但在0时刻并没有初始值,所以我们希望对其进行初始化。

这里采用的是将源语言序列逆序编码后(src_backward)的最后一个状态(backward_first)进行非线性映射fc,作为源语言上下文向量\(c_i\)的初始值(decoder_boot),即\(c_0=h_T\)

backward_first = paddle.layer.first_seq(input=src_backward)
decoder_boot = paddle.layer.fc(
      size=decoder_size,
      act=paddle.activation.Tanh(),
      bias_attr=False,
      input=backward_first)
6.1.2.3 定义解码阶段每一个时间步的RNN行为

根据三个输入【当前时刻的源语言上下文向量\(c_i\)、解码器隐层状态\(z_i\)、真实目标语言中的第i个词\(u_i\)】,来预测第i+1个词的概率\(p_{i+1}\)

主要过程(输入\(h_j\)【即enc_vec】、\(e_{ij}\)【即enc_proj】和\(u_i\)【即current_word】:

  • 通过\(c_0\)的初始化,得到解码器隐层状态\(z_i\)【即decoder_mem
  • 然后通过\(e_{ij}\)生成attention\(a_{ij}\)
  • 再将\(a_{ij}\)\(h_j\)一起得到\(c_i\)【和上一步一起,封装在simple_attention函数中,得到context
  • 接下来,通过\(c_i\)\(u_i\)\(z_i\)得到\(z_{i+1}\)【先contextcurrent_word一起经过一个fc,得到decoder_inputs,然后和decoder_mem一起给gru_step做输入,得到gru_step
  • 最后,对使用softmax归一化计算单词的概率,即通过full_matrix_projection+softmax,得到out【即公式:\(p(u_{i+1}|u_{\lt i+1})=softmax(W_sz_{i+1}+b_z)\)
def gru_decoder_with_attention(enc_vec, enc_proj, current_word):
    """
    enc_vec: h_j
    enc_proj: e_{ij}
    current_word: u_i
    """
     decoder_mem = paddle.layer.memory(
         name='gru_decoder', size=decoder_size, boot_layer=decoder_boot)

     context = paddle.networks.simple_attention(
         encoded_sequence=enc_vec,
         encoded_proj=enc_proj,
         decoder_state=decoder_mem)

     decoder_inputs = paddle.layer.fc(
         act=paddle.activation.Linear(),
         size=decoder_size * 3,
         bias_attr=False,
         input=[context, current_word],
         layer_attr=paddle.attr.ExtraLayerAttribute(
             error_clipping_threshold=100.0))

     gru_step = paddle.layer.gru_step(
         name='gru_decoder',
         input=decoder_inputs,
         output_mem=decoder_mem,
         size=decoder_size)

     out = paddle.layer.mixed(
         size=target_dict_dim,
         bias_attr=True,
         act=paddle.activation.Softmax(),
         input=paddle.layer.full_matrix_projection(input=gru_step))
     return out
6.1.2.4 定义解码器框架名字及gru_decoder_with_attention的前两个输入

也就是说,\(h_j\)\(e_{ij}\)需要分别通过StaticInput进行包装一下。

参考recurrent_group教程

recurrent_group处理的输入序列主要分为以下三种类型:

  • 数据输入:一个双层序列进入recurrent_group会被拆解为一个单层序列,一个单层序列进入recurrent_group会被拆解为非序列,然后交给step函数,这一过程对用户是完全透明的。可以有以下两种:1)通过data_layer拿到的用户输入;2)其它layer的输出。
  • 只读Memory输入:StaticInput 定义了一个只读的Memory,由StaticInput指定的输入不会被recurrent_group拆解,recurrent_group 循环展开的每个时间步总是能够引用所有输入,可以是一个非序列,或者一个单层序列。
  • 序列生成任务的输入:GeneratedInput只用于在序列生成任务中指定输入数据。
decoder_group_name = "decoder_group"
group_input1 = paddle.layer.StaticInput(input=encoded_vector)
group_input2 = paddle.layer.StaticInput(input=encoded_proj)
group_inputs = [group_input1, group_input2]

6.2 训练模式的decoder

注意,wmt14的reader(paddle/v2/dataset/wmt14.py中)如下:

def reader_creator(tar_file, file_name, dict_size):
    def reader():
        src_dict, trg_dict = __read_to_dict__(tar_file, dict_size)
        with tarfile.open(tar_file, mode='r') as f:
            names = [
                each_item.name for each_item in f
                if each_item.name.endswith(file_name)
            ]
            for name in names:
                for line in f.extractfile(name):
                    line_split = line.strip().split('\t')
                    if len(line_split) != 2:
                        continue
                    src_seq = line_split[0]  # one source sequence
                    src_words = src_seq.split()
                    src_ids = [
                        src_dict.get(w, UNK_IDX)
                        for w in [START] + src_words + [END]
                    ]

                    trg_seq = line_split[1]  # one target sequence
                    trg_words = trg_seq.split()
                    trg_ids = [trg_dict.get(w, UNK_IDX) for w in trg_words]

                    # remove sequence whose length > 80 in training mode
                    if len(src_ids) > 80 or len(trg_ids) > 80:
                        continue
                    trg_ids_next = trg_ids + [trg_dict[END]]
                    trg_ids = [trg_dict[START]] + trg_ids

                    yield src_ids, trg_ids, trg_ids_next

    return reader
  • 首先,对目标语言计算embedding,得到trg_embedding,作为current_word,即\(u_i\),放到group_inputs
  • 然后使用recurrent_group,step指定为gru_decoder_with_attention,input为group_inputs
  • 使用目标语言的下一个词序列target_language_next_word作为label
  • 对decoder的输出与label做多分类的交叉熵算cost
if not is_generating:
    trg_embedding = paddle.layer.embedding(
        input=paddle.layer.data(
            name='target_language_word',
            type=paddle.data_type.integer_value_sequence(target_dict_dim)),
        size=word_vector_dim,
        param_attr=paddle.attr.ParamAttr(name='_target_language_embedding'))
    group_inputs.append(trg_embedding)

    # For decoder equipped with attention mechanism, in training,
    # target embeding (the groudtruth) is the data input,
    # while encoded source sequence is accessed to as an unbounded memory.
    # Here, the StaticInput defines a read-only memory
    # for the recurrent_group.
    decoder = paddle.layer.recurrent_group(
        name=decoder_group_name,
        step=gru_decoder_with_attention,
        input=group_inputs)

    lbl = paddle.layer.data(
        name='target_language_next_word',
        type=paddle.data_type.integer_value_sequence(target_dict_dim))
    cost = paddle.layer.classification_cost(input=decoder, label=lbl)

6.3 生成模型的解码器

  • 首先,在序列生成任务中,由于解码阶段的RNN总是引用上一时刻生成出的词的词向量,作为当前时刻的输入,因此,使用GeneratedInput来自动完成这一过程。
  • 然后,使用beam_search函数循环调用gru_decoder_with_attention函数,生成出序列id。

和训练的区别:

  • group_inputs的前两个输入一样,都是\(h_j\)\(e_{ij}\)的StaticInput,第三个输入有区别:训练是目标语言序列的embedding,而生成使用的是GeneratedInput
  • 使用beam_search,而训练使用的是recurrent_group
if is_generating:
    # In generation, the decoder predicts a next target word based on
    # the encoded source sequence and the previous generated target word.

    # The encoded source sequence (encoder's output) must be specified by
    # StaticInput, which is a read-only memory.
    # Embedding of the previous generated word is automatically retrieved
    # by GeneratedInputs initialized by a start mark <s>.

    trg_embedding = paddle.layer.GeneratedInput(
        size=target_dict_dim,
        embedding_name='_target_language_embedding',
        embedding_size=word_vector_dim)
    group_inputs.append(trg_embedding)

    beam_gen = paddle.layer.beam_search(
        name=decoder_group_name,
        step=gru_decoder_with_attention,
        input=group_inputs,
        bos_id=0,
        eos_id=1,
        beam_size=beam_size,
        max_length=max_length)

6.4 attention的可视化

https://github.com/PaddlePaddle/Paddle/issues/1269

7. 应用

AI Challenger比赛的机器翻译赛题中,冠军的做法如下:

用pytorch(轻量,且实现Deliberation networks相对容易),8卡titan xp,一次训练将近一周。

另外一篇类似的AI Challenger 2018 机器翻译参赛总结

7.1 Backbone模型

基于encoder-decoder+attention,其中,rnn使用3层lstm,dim(h)=1024。

对于处理未登录词:

参考Neural Machine Translation of Rare Words with Subword Units【NMT中的OOV(集外词)和罕见词(Rare Words)问题通常用back-off 词典的方式来解决,本文尝试用一种更简单有效的方式(Subword Units)来表示开放词表。 本文从命名实体、同根词、外来语、组合词(罕见词有相当大比例是上述几种)的翻译策略中得到启发,认为把这些罕见词拆分为“子词单元”(subword units)的组合,可以有效的缓解NMT的OOV和罕见词翻译的问题。子词单元的拆分策略,则是借鉴了一种数据压缩算法Byte Pair Encoding(BPE),这里的压缩算法不是针对于词做变长编码,而是对于子词来操作。这样,即使是训练语料里未见过的新词,也可以通过子词的拼接来生成翻译。本文还探讨了BPE的两种编码方式:一种是源语言词汇和目标语言词汇分别编码,另一种是双语词汇联合编码。前者的优势是让词表和文本的表示更紧凑,后者则可以尽可能保证原文和译文的子词切分方式统一。从实验结果来看,在音译或简单复制较多的情形下(比如英德)翻译,联合编码的效果更佳。github:https://github.com/rsennrich/subword-nmt】 解决低频词问题,相比Char/Word混合编码更为高效。源和目标的vocabulary size大约都是3.5w。

7.2 其他一些尝试

  • 结合语法信息的特征: part-of-speech信息的tagging的信息做为sub-word的一个额外的embedding信息,相当于拿词语的词性来做额外的信息辅助。在200w-400w的小数据集上,使用这个方法,可以提升bleu 0.3左右。part-of-speech可以用nltk,而stanford的nlp的库在英文上的效果会更好。
  • 解码的方向: 左->右 or 右->左其实是两种条件概率,同时做两个方向的解码并且取最优(平均/max之类的都可以试试),bleu也可以提升0.4左右。最终的模型融合中暂未使用,因为就需要16个模型,而不是8个了。
  • 基于非rnn的encoder-decoder: facebook的conv-seq2seq,单模型比lstm略有提升,但性能相对差一点。t2t效果也略好于baseline。但不太方便扩展(如,加上deliberation network,加入词性信息)。
  • 迭代编码: 传统encoder-decoder模型中,一旦decoder output了一个字符,就固定不可修改,希望可以继续迭代,进一步调整。所以,可以先训一个encoder-decoder,然后把output再作为输入,再搞一次encoder-decoder。【msra的nips2017有尝试:Deliberation Networks: Sequence Generation Beyond One-Pass Decoding】【编码器-解码器框架在许多序列生成任务中都实现了非常好的性能,包括机器翻译、自动文本摘要、对话系统和图像描述等。这样的框架在解码和生成序列的过程中只采用一次(one-pass)正向传播过程,因此缺乏推敲(deliberation)的过程:即生成的序列直接作为最终的输出而没有进一步打磨的过程。然而推敲是人们在日常生活中的一种常见行为,正如我们在阅读新闻和写论文/文章/书籍一样。在该研究中,我们将推敲过程加入到了编码器-解码器框架中,并提出了用于序列生成的推敲网络(Deliberation networks)。推敲网络具有两阶段解码器,其中第一阶段解码器用于解码生成原始序列,第二阶段解码器通过推敲的过程打磨和润色原始语句。由于第二阶段推敲解码器具有应该生成什么样的语句这一全局信息,因此它能通过从第一阶段的原始语句中观察未来的单词而产生更好的序列。神经机器翻译和自动文本摘要的实验证明了我们所提出推敲网络的有效性。在 WMT 2014 英语到法语间的翻译任务中,我们的模型实现了 41.5 的 BLEU 分值,即当前最优的 BLEU 分值。】

7.3 上述模型存在的问题

  • 曝光偏差(Exposure Bias) 训练采用teacher forcing原则,永远假设以正确的答案作为结果进行训练,即output的前缀都是在正确的基础上输出下一个字符,和真实的decoding不太一样。即中间如果有个错了,会不会越错越远。
  • 训练与测试失配(loss-evaluation mismatch) 训练使用cross-entropy,但测试的目标却是bleu

7.4 采用增强学习进行优化

目标: \[ max_\theta E_{\tau \sim \pi _\theta }[R(\tau )] \]

先用cross-entropy训出一个基本可用的,再用增强学习,以bleu为目标,使用梯度上升的方法去改进\(\theta \)

\[ g=E_{\tau \sim \pi _\theta }[\nabla _\theta log\pi _\theta (\tau)R(\tau ))] \]

其中,

  • \(\pi _\theta (\cdot )\)为现有的翻译模型
  • \(\tau\)为根据现有模型所生成的一个翻译结果
  • \(R(\tau)\)采用句子级别BLEU进行计算

7.5 reranking

假设模型基本正确,生成k个可能正确的翻译结果,即,采用beamsearch,得到源语言x的多个翻译结果\(\{t_i\}^K_{i=1}\),然后根据bleu score计算平均值:

\[ \bar{b}=\frac{1}{K}\sum_iR(t_i) \]

然后估算梯度:

\[ \hat{g}=\frac{1}{K}\sum_i\nabla _\theta log\pi _\theta (t_i)[R(t_i)-\bar{b}) \]

有0.3bleu的提升

7.6 rl进一步改进的方向

  • MIXER:第一步用cross-entropy,后面直接用bleu,有gap, [sequence level traing with Recurrent neural networks]
  • learning rate的设置:Trust Region Policy Optimization,Proximal Polocy Optimization Algorithms解决学习率难以调整的问题。

\[ L^{clip}(\theta)=E_\tau [min(r(\theta )R(\tau),clip(r(\theta),1-\epsilon,1+\epsilon)R(\tau))] \]

其中,\(r(\theta)=\frac{\pi_\theta(\tau)}{\pi_{\theta_{old}}(\tau)}\)

7.7 模型融合

计算算数平均值

\[ \bar{\pi_\theta}(a;s)=\frac{1}{M}\sum_{j=1}^M\pi^j_\theta(a;s) \]

然后采用上述结果\(\bar{\pi_\theta}(a;s)\)进行beam search,得到最终结果 M=8个模型融合,相比单模型,+1.1 bleu

beamsearch时,除了max-likelihood外,还加入了对coverage的penalty和对句子长度的normalization计算。

8. 其他

8.1 sequence_conv_pool相关

注意: cnn的卷积操作参考https://daiwk.github.io/posts/image-classical-cnns.html

所以,推荐系统里面cnn的例子里,原矩阵是nxk,卷积核是hxk,所以得到的结果是(n-h+1)x(k-k+1)=(n-h+1)x1.所谓的时间维度上的maxpooling,就是对着这(n-h+1)个数字取max.

如果有多个(例如,xx个)卷积核,那就有多个(n-h+1)个序列,h和每个卷积核相关,那取pooling就是每个(n-h+1)里取max,最后得到xx个max值。

在sequence_conv_pool中,本质是Text input => Context Projection => FC Layer => Pooling => Output.

    with mixed_layer(
            name=context_proj_layer_name,
            size=input.size * context_len,
            act=LinearActivation(),
            layer_attr=context_attr) as m:
        m += context_projection(
            input,
            context_len=context_len,
            context_start=context_start,
            padding_attr=context_proj_param_attr)
    ### 打平成一个input.size*context_len的了?假设输入是一个序列,那么输出不是序列的序列了?而是一个单层序列?另外注意,这里的输入实际上是embedding,也就是序列的序列.
    fc_layer_name = "%s_conv_fc" % name \
        if fc_layer_name is None else fc_layer_name
    fl = fc_layer(
        name=fc_layer_name,
        input=m,
        size=hidden_size,
        act=fc_act,
        layer_attr=fc_attr,
        param_attr=fc_param_attr,
        bias_attr=fc_bias_attr)

    return pooling_layer(
        name=name,
        input=fl,
        pooling_type=pool_type,
        bias_attr=pool_bias_attr,
        layer_attr=pool_attr)

而其中的context_projection如下:

    For example, origin sequence is [A B C D E F G], context len is 3, then
    after context projection and not set padding_attr, sequence will
    be [ 0AB ABC BCD CDE DEF EFG FG0 ].

8.2 paddle的lstm相关

在stacked_lstm的例子中:

    fc_last = paddle.layer.pooling(
        input=inputs[0], pooling_type=paddle.pooling.Max())
    lstm_last = paddle.layer.pooling(
        input=inputs[1], pooling_type=paddle.pooling.Max())
    output = paddle.layer.fc(
        input=[fc_last, lstm_last],
        size=class_dim,
        act=paddle.activation.Softmax(),
        bias_attr=bias_attr,
        param_attr=para_attr)

另外,

lstmemory的输出大小固定是输入大小的1/4

size=input.size / 4,

agg_level=AggregateLevel.TO_NO_SEQUENCE pooling 默认把一个单层序列变成一个0层序列,默认是maxpooling,也就是整个序列取一个最大值当做输出。

8.3 评价指标

在计算BLEU得分的时候,连续匹配的词越多,得分越高。

BLEU值也有比较明显的缺点。用一个词来举例,比如『你好』,人给出的一个参考译文是『hello』。机器给出的译文是『how are you』,跟这个reference没有一个词匹配上,从BLEU值的角度来看,它得分是零。所以BLEU值的得分,受reference影响。Reference越多样化,匹配上的可能性就会越大。一般来说,用于评价机器翻译质量的测试集有4个reference,也有的有一个reference,也有的有十几个reference。

不止bleu

Beyond BLEU: Training Neural Machine Translation with Semantic Similarity

在本文中,研究者提出以一种可替代的奖励函数(reward function)来优化神经机器翻译(NMT)系统,这种奖励函数基于语义相似性的研究。研究者对四种翻译成英文的语言展开评估,结果发现:利用他们提出的度量进行训练,可以产生较 BLEU、语义相似性和人工测评等评估标准更好的翻译效果,并且优化过程的收敛速度更快。分析认为,取得更好翻译效果的原因在于研究者提出的度量更有利于优化,同时分配部分分数(partial credit),提供较 BLEU.1 更强的分数多样性。

nmt的挑战

Six Challenges for Neural Machine Translation


原创文章,转载请注明出处!
本文链接:http://daiwk.github.io/posts/nlp-nmt.html
上篇: alphago-zero
下篇: 文本摘要

comment here..