/ NLP  

NLP系列

基于seq2seq的机器翻译模型

本章概述

  • 基础seq2seq编解码模型及应用
    • 简介
    • 应用:神经机器翻译
  • 基于注意力机制的seq2seq机器翻译模型
    • 词向量
    • RNN的解码器,编码器
    • 上下文内容向量
    • 注意力机制
    • 可视化
  • 【实战】基于keras完成的基础seq2seq机器翻译模型
  • 【实战】基于tensorflow的google版本seq2seq机器翻译模型

1.seq2seq(序列到序列模型)简介

  • 对于很多自然语言处理任务,比如聊天机器人,机器翻译,自动文摘,智能问答等,传统的解决方案都是检索式(从候选集中选出答案),这对素材的完善程度要求很高。
  • 随着深度学习的发展,研究界将深度学习技术应用与自然语言的生成和自然语言的理解的方面的研究,并取得了一些突破性的成果,比如,Sequence-to-sequence (seq2seq) 模型,它是目前自然语言处理技术中非常重要和流行的一个模型,该技术突破了传统的固定大小输入问题框架,开通了将经典深度神经网络模型运用于翻译与对话问答这一类序列型任务的先河,并且被证实在各主流语言之间的相互翻译以及语音助手中人机短问快答的应用中有着非常好的表现。

参考资料:Visualizing A Neural Machine Translation Model

1.seq2seq(序列到序列模型)

  • 序列到序列的模型是非常有意思的NLP模型,我们的很多NLP任务,是文本到文本的映射(对应),这个过程就像是下面图里展示的过程。
  • seq2seq模型不仅仅是用在NLP中的模型,它的输入也可以是语音信号或者图像表示。

1.seq2seq 应用:神经机器翻译

  • 在NLP的任务中,大部分输入的是文本序列,输出的很多时候也是文本序列。
  • 下图所示的是一个典型的机器翻译任务中,输入的文本序列(源语言句子)到输出的文本序列(目标语言句子)之间的变换。

2.编码解码模型

  • seq2seq 是由一个“编码解码器”(encoder-decoder)结构组成
    • Encoder: 编码器处理输入序列中的每个元素(在这里可能是1个词),将捕获的信息编译成向量(称为上下文内容向量)。
    • Decoder: 在处理整个输入序列之后,编码器将上下文发送到解码器,解码器逐项开始产生输出序列。

2. 编码解码模型

  • 应用:神经机器翻译(Neural Machine Translation)

2. 编码解码模型

  • 输入: $x = (x_1,…,x_{T_x})$
  • 输出: $y = (y_1,…,y_{T_y})$

    1. $h_t = RNN_{enc}(x_t, h_{t-1})$ , Encoder接受每一个word embedding $x_t$和上一个时刻的hidden state $h_{t-1}$。输出这个时刻的hidden state $h_t$。

    2. $s_t = RNN_{dec}(\hat{y}{t-1},s{t-1})$ , Decoder接受上一个生成的单词的word embedding $\hat{y}{t-1}$,和上一个时间点的hidden state $s{t-1}$。

    3. $c_i = \sum_{j=1}^{T_x} \alpha_{ij}h_j$ , attentional context vector是一个对于encoder输出的hidden states的一个加权平均。

    4. $\alpha_{ij} = \frac{exp(e_{ij})}{\sum_{k=1}^{T_x}exp(e_{ik})}$ , 每一个encoder的hidden states对应的权重。

    5. $e_{ij} = score(s_i, h_j)$ , 通过decoder的hidden states加上encoder的hidden states来计算一个分数,用于计算权重 4.

    6. $\hat{s}_t = tanh(W_c[c_t;s_t])$, 将context vector 和 decoder的hidden states 串起来。

    7. $p(y_t|y_{<t},x) = softmax(W_s\hat{s}_t)$ ,计算最后的输出概率。

2.1 词向量(word embedding)

  • 输入的数据(文本序列)中的每个元素(词)通常会被编码成一个稠密的向量 $x = (x_1,…,x_{T_x})$,这些向量叫做word embedding,如下图所示

2.2 循环神经网络(RNN)

  • 我们的encoder和decoder都会借助于循环神经网络(RNN)这类特殊的神经网络完成,循环神经网络会接受每个位置(时间点)上的输入,同时经过处理进行信息融合,并可能会在某些位置(时间点)上输出。如下图所示。
    1. Encoder: $h_t = RNN_{enc}(x_t, h_{t-1})$
    2. Decoder: $s_t = RNN_{dec}(\hat{y}{t-1},s{t-1})$

2.3 上下文向量(context vector)

  • 编码器会将一整句话的信息编译到一个向量中,这个向量总结了这一句话的主要信息,称之为上下文向量
  • 一般我们会采取RNN 编译完最后一个单词时的输出向量$h_{T_x}$ 作为上下文向量

2.4 举例

  • 动态地展示整个编码器和解码器,分拆的步骤过程大概是下面这个样子。

2.4 举例

  • 更详细的演示

2.5 注意力机制 (Attention)

  • 如果把所有句子信息都压缩到一个定长的上下文向量中,当遇到长句子的时候,编码器很难保存句子中的所有信息。
  • 我们考虑到提升效果,不会寄希望于把所有的内容都放到一个上下文向量(context vector)中,而是会采用一个叫做注意力模型的模型来动态处理和解码,动态的图如下所示。

2.5 注意力机制

  • 在解码阶段,解码器根据已生成的序列 $y_{<i}$,将当前时刻hidden state $s_i$, 对编码器中的hidden states $h_j, j\in[1,T_x]$ 计算权重。

    $e_{ij} = score(s_i, h_j), ~~\alpha_{ij} = \frac{exp(e_{ij})}{\sum_{k=1}^{T_x}exp(e_{ik})}$

  • 根据权重,对编码器中的hidden states求加权和,得到attentional context vector

    $c_i = \sum_{j=1}^{T_x} \alpha_{ij}h_j$

2.6 解码

  • 带注意力的解码器RNN接收的上一个单词的词向量(embedding)和一个初始的解码器隐藏状态(hidden state)
  • RNN处理输入,产生输出和新的隐藏状态向量
  • attention的步骤:使用编码器隐藏状态(hidden state)和$h_4$来计算该时刻的attentional context vector $C_4$
  • 把h4和C4拼接成一个向量$\hat{s}_t=[h_t,C_t]$,再通过一个全连接层(fully-connected layer)和softmax完成解码,$p(y_t|y_{<t},x) = softmax(W_s\hat{s}_t)$
  • 每个时间点上重复这个操作

2.6 解码

  • 这个动态解码的过程展示成下述图所示的过程

2.7 可视化(Visualization)

  • 注意力机制是一个很神奇地可以学习源语言和目标语言之间词和词对齐关系的方式

3 [实战] 基于OpenNMT完成的基础seq2seq机器翻译模型

  1. 处理数据
  2. 训练模型
  3. 翻译

3.1 处理数据

  • 下载代码及数据
  • 预处理
    1
    2
    3
    4
    5
    6
    7
    8
    9
    cd $HOME/MT/
    git clone https://github.com/OpenNMT/OpenNMT-py.git
    opennmt=$HOME/MT/OpenNMT-py
    python $opennmt/preprocess.py \
    -train_src $opennmt/data/src-train.txt \
    -train_tgt $opennmt/data/tgt-train.txt \
    -valid_src $opennmt/data/src-val.txt \
    -valid_tgt $opennmt/data/tgt-val.txt \
    -save_data $opennmt/data/demo

3.2 编码器(Encoder)

  • 将词转换成词向量,再通过RNN encoder 生成下一个hidden state
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    class RNNEncoder(EncoderBase):
    """rnn_type (:obj:`str`): one of [RNN, LSTM, GRU, SRU]
    bidirectional (bool) : use a bidirectional RNN
    num_layers (int) : number of stacked layers
    hidden_size (int) : hidden size of each layer
    dropout (float) : dropout value for :obj:`nn.Dropout`
    embeddings (:obj:`onmt.modules.Embeddings`): embedding module to use
    """
    def __init__(self, rnn_type, bidirectional, num_layers,
    hidden_size, dropout=0.0, embeddings=None, use_bridge=False):
    super(RNNEncoder, self).__init__()
    num_directions = 2 if bidirectional else 1
    hidden_size = hidden_size // num_directions
    self.embeddings = embeddings

    self.rnn, self.no_pack_padded_seq = \
    rnn_factory(rnn_type,
    input_size=embeddings.embedding_size,
    hidden_size=hidden_size,
    num_layers=num_layers,
    dropout=dropout,
    bidirectional=bidirectional)
    def forward(self, src, lengths=None):
    emb = self.embeddings(src)
    packed_emb = emb
    memory_bank, encoder_final = self.rnn(packed_emb)

3.3 解码器

  • init_state 初始化RNN的hidden state
  • _run_forward_pass 通过对memory_bank 计算attention,计算当前单词预测的概率
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    class RNNDecoderBase(nn.Module):
    """rnn_type (:obj:`str`): one of [RNN, LSTM, GRU, SRU]
    num_layers (int) : number of stacked layers
    hidden_size (int) : hidden size of each layer
    attn_type (str) : see :obj:`onmt.modules.GlobalAttention`
    dropout (float) : dropout value for :obj:`nn.Dropout`
    embeddings (:obj:`onmt.modules.Embeddings`): embedding module to use
    """
    def __init__(self, rnn_type, num_layers, hidden_size, attn_type="general",
    attn_func="softmax", dropout=0.0, embeddings=None):
    super(RNNDecoderBase, self).__init__()
    # Basic attributes.
    self.decoder_type = 'rnn'
    self.bidirectional_encoder = bidirectional_encoder
    self.num_layers = num_layers
    self.hidden_size = hidden_size
    self.embeddings = embeddings
    self.dropout = nn.Dropout(dropout)
    # Decoder state
    self.state = {}
    # Build the RNN.
    self.rnn = self._build_rnn(rnn_type,
    input_size=self._input_size,
    hidden_size=hidden_size,
    num_layers=num_layers,
    dropout=dropout)
    def init_state(self, src, memory_bank, encoder_final):
    pass
    def _run_forward_pass(self, tgt, memory_bank, memory_lengths=None):
    pass

3.4 损失函数

  • 对计算预测的单词和参考单词的negative log-likelihood (NLL)
    1
    criterion = nn.NLLLoss(ignore_index=padding_idx, reduction='sum')

3.5 训练

1
2
opennmt=$HOME/MT/OpenNMT-py
python $opennmt/train.py -data $opennmt/data/demo -save_model $opennmt/demo-model
1
2
3
4
5
6
7
8
[2019-01-21 23:15:10,522 INFO] encoder: 16506500
[2019-01-21 23:15:10,522 INFO] decoder: 41613820
[2019-01-21 23:15:10,522 INFO] * number of parameters: 58120320
[2019-01-21 23:15:10,523 INFO] Starting training on CPU, could be very slow
[2019-01-21 23:15:10,523 INFO] Start training...
[2019-01-21 23:15:10,707 INFO] Loading dataset from data/demo.train.0.pt, number of examples: 10000
[2019-01-21 23:17:32,401 INFO] Step 50/100000; acc:4.21; ppl:9741.36; xent:9.18; lr:1.0; 0/500 tok/s; 142 sec
[2019-01-21 23:19:49,994 INFO] Step 100/100000; acc:5.13; ppl:3308.13; xent:8.10; lr:1.0; 0/525 tok/s; 279 sec

3.6 翻译

1
2
3
4
5
opennmt=$HOME/MT/OpenNMT-py
python $opennmt/translate.py \
-model $opennmt/demo-model_XYZ.pt \
-src $opennmt/data/src-test.txt \
-output $opennmt/pred.txt -replace_unk -verbose

4 基于TensorFlow的google版seq2seq机器翻译模型

google的这个教程使用高版本tensorflow(TensorFlow 1.2+)的 seq2seq API完成,该API使seq2seq模型的构建过程干净、简单、易读,主要包括以下内容:

  • 使用 tf.data 中最新输入的管道对动态调整的输入序列进行预处理。
  • 使用批量填充和序列长度 bucketing,提高训练速度和推理速度。
  • 使用通用结构和训练时间表训练 seq2seq 模型,包括多种注意力机制和固定抽样。
  • 使用 in-graph 集束搜索在 seq2seq 模型中进行推理。
  • 优化 seq2seq 模型,以实现在多 GPU 设置中的模型训练。

4.1 安装TensorFlow 及nmt

  • 安装 TensorFlow,请按照以下安装指导:https://www.tensorflow.org/install/。

    1
    git clone https://github.com/tensorflow/nmt/
  • 主要代码在 model.py 文件中。在网络的底层,编码器和解码器 RNN 接收到以下输入:首先是原句子,然后是从编码到解码模式的过渡边界符号「<s>」,最后是目标语句。对于训练来说,我们将为系统提供以下张量,它们是以时间为主(time-major)的格式,并包括了单词索引:

    • encoder_inputs [max_encoder_time, batch_size]:源输入词。
    • decoder_inputs [max_decoder_time, batch_size]:目标输入词。
    • decoder_outputs [max_decoder_time, batch_size]:目标输出词,这些是 decoder_inputs 按一个时间步向左移动,并且在右边有句子结束符。

4.2 词向量

给定单词的分类属性,模型首先必须查找词来源和目标嵌入以检索相应的词表征。为了令该嵌入层能够运行,我们首先需要为每一种语言选定一个词汇表。通常,选定词汇表大小 V,那么频率最高的 V 个词将视为唯一的。而所有其他的词将转换并打上「unknown」标志,因此所有的词将有相同的嵌入。我们通常在训练期间嵌入权重,并且每种语言都有一套。

1
2
3
4
5
# Embedding
embedding_encoder = variable_scope.get_variable(
"embedding_encoder", [src_vocab_size, embedding_size], ...)# Look up embedding:# encoder_inputs: [max_time, batch_size]# encoder_emp_inp: [max_time, batch_size, embedding_size]
encoder_emb_inp = embedding_ops.embedding_lookup(
embedding_encoder, encoder_inputs)

4.3 编码器(encoder)

  • 词向量就能作为输入馈送到主神经网络中。该网络有两个多层循环神经网络组成,一个是原语言的编码器,另一个是目标语言的解码器。
  • 这两个 RNN 原则上可以共享相同的权重,然而在实践中,我们通常使用两组不同的循环神经网络参数(这些模型在拟合大型训练数据集上做得更好)。
  • 解码器 RNN 使用零向量作为它的初始状态
1
2
3
4
5
6
# Build RNN cell
encoder_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units)
# Run Dynamic RNN# encoder_outpus: [max_time, batch_size, num_units]# encoder_state: [batch_size, num_units]
encoder_outputs, encoder_state = tf.nn.dynamic_rnn(
encoder_cell, encoder_emb_inp,
sequence_length=source_seqence_length, time_major=True)
  • 注意语句有不同的长度以避免浪费计算力,因此我们会通过 source_seqence_length 告诉 dynamic_rnn 精确的句子长度。因为我们的输入是以时间为主(time major)的,我们需要设定 time_major=True。

4.4 解码器(decoder)

  • decoder 也需要访问源信息,一种简单的方式是用编码器最后的隐藏态 encoder_state 对其进行初始化。
1
2
# Build RNN cell
decoder_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units)
1
2
3
4
5
6
7
8
# Helper
helper = tf.contrib.seq2seq.TrainingHelper(
decoder_emb_inp, decoder_lengths, time_major=True)# Decoder
decoder = tf.contrib.seq2seq.BasicDecoder(
decoder_cell, helper, encoder_state,
output_layer=projection_layer)# Dynamic decoding
outputs, _ = tf.contrib.seq2seq.dynamic_decode(decoder, ...)
logits = outputs.rnn_output
  • 此处代码的核心是 BasicDecoder、获取 decoder_cell(类似于 encoder_cell) 的 decoder、helper 以及之前作为输入的 encoder_state。
  • 通过分离 decoders 和 helpers,我们能重复使用不同的代码库,例如 TrainingHelper 可由 GreedyEmbeddingHelper 进行替换

4.5.梯度计算和优化器优化

  • 定义我们的 NMT 模型的前向传播,及计算反向传播
1
2
3
4
5
# Calculate and clip gradients
parameters = tf.trainable_variables()
gradients = tf.gradients(train_loss, params)
clipped_gradients, _ = tf.clip_by_global_norm(
gradients, max_gradient_norm)
  • 训练 RNN 的一个重要步骤是梯度截断(gradient clipping)。这里,我们使用全局范数进行截断操作。最大值 max_gradient_norm 通常设置为 5 或 1。
  • 选择优化器。Adam 优化器是最常见的选择。选择一个学习率,learning_rate 的值通常在 0.0001 和 0.001 之间,且可设置为随着训练进程逐渐减小。
1
2
3
4
# Optimization
optimizer = tf.train.AdamOptimizer(learning_rate)
update_step = optimizer.apply_gradients(
zip(clipped_gradients, params))

4.6 训练 NMT 模型

  • 开始训练第一个 NMT 模型,将越南语翻译为英语。代码的入口是 nmt.py。

  • 我们将使用小规模的 Ted 演讲双语语料库(133k 的训练样本)进行训练。数据可从以下链接找到:https://nlp.stanford.edu/projects/nmt/。

  • 我们将使用 tst2012 作为dev数据集,tst 2013 作为test数据集。

1
nmt/scripts/download_iwslt15.sh /tmp/nmt_data
  • 运行以下命令行开始训练一个2层LSTM Seq2seq模型,128维隐单元,0.2的dropout:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
mkdir /tmp/nmt_model
python -m nmt.nmt \
--src=vi --tgt=en \
--vocab_prefix=/tmp/nmt_data/vocab \
--train_prefix=/tmp/nmt_data/train \
--dev_prefix=/tmp/nmt_data/tst2012 \
--test_prefix=/tmp/nmt_data/tst2013 \
--out_dir=/tmp/nmt_model \
--num_train_steps=12000 \
--steps_per_stats=100 \
--num_layers=2 \
--num_units=128 \
--dropout=0.2 \
--metrics=bleu
```
```python
# First evaluation, global step 0
eval dev: perplexity 17193.66
eval test: perplexity 17193.27
# Start epoch 0, step 0, lr 1, Tue Apr 25 23:17:41 2017
sample train data:
src_reverse: </s> </s> Điều đó , dĩ nhiên , là câu chuyện trích ra từ học thuyết của Karl Marx .
ref: That , of course , was the <unk> distilled from the theories of Karl Marx . </s> </s> </s>
epoch 0 step 100 lr 1 step-time 0.89s wps 5.78K ppl 1568.62 bleu 0.00
epoch 0 step 200 lr 1 step-time 0.94s wps 5.91K ppl 524.11 bleu 0.00
epoch 0 step 300 lr 1 step-time 0.96s wps 5.80K ppl 340.05 bleu 0.00
epoch 0 step 400 lr 1 step-time 1.02s wps 6.06K ppl 277.61 bleu 0.00
epoch 0 step 500 lr 1 step-time 0.95s wps 5.89K ppl 205.85 bleu 0.00

4.7 翻译

  • 创建一个推理文件,用已经训练好的模型去翻译一些语句,详见 inference.py
1
2
3
4
5
6
7
8
cat > /tmp/my_infer_file.vi# (copy and paste some sentences from /tmp/nmt_data/tst2013.vi)

python -m nmt.nmt \
--model_dir=/tmp/nmt_model \
--inference_input_file=/tmp/my_infer_file.vi \
--inference_output_file=/tmp/nmt_model/output_infer

cat /tmp/nmt_model/output_infer # To view the inference as output

本章小结

  • 基础seq2seq编解码模型及应用
    • 简介
    • 应用:神经机器翻译
  • 基于注意力机制的seq2seq机器翻译模型
    • 词向量
    • RNN的解码器,编码器
    • 上下文内容向量
    • 注意力机制
    • 可视化
  • 【实战】基于keras完成的基础seq2seq机器翻译模型
  • 【实战】基于tensorflow的google版本seq2seq机器翻译模型