/ NLP  

NLP系列

来自Google的Transformer模型

本章概述

  • Google的Transformer模型
    • 编码器,解码器
    • 传统的注意力机制及Multi-head attention
    • 基于位置的单词编码,及词向量,输出层
    • 可视化multi-head attention
    • Transformer与RNN和CNN神经翻译模型的对比
  • Google模型的训练细节
    • 优化器选择
    • 正则化
    • label smoothing
  • 实战演示
    • 介绍encoder,decoder类及model类
    • 介绍如何训练模型
    • 运用OpenNMT开源工具来实战演示

1.来自Google的Transformer模型

  • 序列计算中,传统的RNN在预测下一个符号(token)的时候,会对以往的历史信息有很强的依赖,使得难以充分地并行化,也无法很好地加深网络的层级结构。而对于传统的基于CNN的神经机器翻译模型,两个任意输入与输出位置的信号关联所需要的运算数量与它们的位置距离成正比,Facebook提出的CNN NMT 为线性增长。这两种常见的结构使得学习较远位置的依赖关系(long-term dependency)非常困难。

  • 在 Transformer 中,两个任意输入的信号关联的开销会减少到一个固定的运算数量,使用 Multi-Head Attention 注意力机制可以完全脱离RNN及CNN的结构,只使用自注意机制(self-attention),使得Transformer可以高效地并行化,并堆叠非常多层的深层网络。

  • 自注意力(Self-attention),是一种涉及单序列不同位置的注意力机制,并能计算序列的表征。自注意力在多种任务中都有非常成功的应用,例如阅读理解、摘要概括、文字蕴含和语句表征等。自注意力这种在序列内部执行 Attention 的方法可以视为搜索序列内部的隐藏关系,这种内部关系对于翻译以及序列任务的性能非常重要。

1.1 编码器 encoder

  • 编码器encoder由6层结构一样的网络层组成,每一层有2个子层:
    • 第一个子层是multi-head self-attention Layer
    • 第二个子层是一个基于位置编码的全连接网络层(position-wise fully connected feed-forward network)
    • 我们会使用残差连接的方式,分别对每个子层的输入加到这个子层的输出上,然后我们再接一个Layer normalization的归一化层。
    • 所有的embedding及hidden state的维度都是512
      $$ \text{LayerNorm}(x+\text{Sublayer}(x)) $$

1.2 解码器 decoder

  • 解码器decoder由6层结构一样的网络层组成,每一层除了跟encode人一样有2个子层以外,还有第3个子层
    • 第一个子层是multi-head self-attention Layer
    • 第二个子层是一个基于位置编码的全连接网络层(position-wise fully connected feed-forward network)
    • 第三个子层用于对encoder的输出向量进行multi-head attention
    • 同样的,我们会使用残差连接的方式,分别对每个子层的输入加到该子层的输出上,然后我们再接一个Layer normalization的归一化层。
      $$ \text{LayerNorm}(x+\text{Sublayer}(x))\ $$
    • decoder还需要将还没有生成的后续序列掩盖掉(masking),这样做是为了防止decoder在做self-attention的时候关注到后续还未生成的单词上去。

1.3 注意力机制

  • 传统的注意力机制,也称为scaled Dot-Product Attention,可以看成是有一个询问的词(query),去跟一堆哈希表中的键值对(key-value pair)进行匹配,找到最相关的键(key),之后返回该键所对应的值(value)。通常的,如果我们只返回一个key所对应的value,我们称之为hard attention。如果我们对所有的key都计算一个相关系数,(也称之为attention weight),我们可以将所有key对应的value进行加权求和(weighted sum)这样的操作我们称之为soft attention。
    $$\text{Attention}(Q,K,V) = \text{softmax}\left({QK^T \over \sqrt{d_k}}\right)V$$
  • 其中所有的query和key都是维度为$d_k$的向量,我们将这些向量分别叠在一起形成 $Q\in\mathbb{R}^{|Q|\times d_k}, K\in\mathbb{R}^{|K|\times d_k}$的矩阵。
  • 所有的value都是维度为$d_v$的向量,我们将这些向量叠在一起形成$V\in\mathbb{R}^{|V|\times d_k}$
  • 这里如果维度$d_k$很大的时候,两个向量的乘积会变得很大,使得softmax会得到非常小的数值,所以我们会在这里除以$\sqrt{d_k}$来抵消这个影响

1.4 Multi-Head Attention

  • 这里我们假设$Q,~K,~V\in \mathbb{R}^{d_\text{model}}$都在一个${d_\text{model}}$维度的空间中
  • 我们使用h个不一样权重的线性映射函数$(QW^Q_i, KW^K_i, VW^V_i)$将Q, K, V分别映射到$d_k,~d_k,~d_v$空间中
  • 我们对映射之后的Q, K, V 做h次attention,并将h个attention head连接在一起形成一个新的向量
  • 最后再将这个向量映射到$d_\text{model}$空间,作为下一层的输入

$$ \text{MultiHead}(Q,K,V) = \text{Concat}(\text{head}_1,\cdots, \text{head}_h) W^O \ \text{head}_i = \text{Attention}(QW^Q_i, KW^K_i, VW^V_i) $$

  • 其中$W^Q_i\in \mathbb{R}^{d_\text{model}\times d_k}, W^K_i\in \mathbb{R}^{d_\text{model}\times d_k}, W^V_i\in \mathbb{R}^{d_\text{model}\times d_v}, W^O\in \mathbb{R}^{hd_v\times d_\text{model}}$, 常见的我们设置$h=8,d_k=d_v=d_\text{model}/h=64$
  • 模型图

1.4 Multi-Head Attention

  • 应用

    • 将decoder上一个时刻的hidden state 作为query,将encoder的最顶层的所有输出的hidden state作为key和value,这样可以类似传统的attention机制一样去发现源语言单词与目标语言单词之间的联系
    • encoder本身会对源语言单词进行multi-head self attention,其中query,key,value都是一样的,都是上一层中输出的单词的hidden state,每一个时刻计算出来的context vector都会作为该层输出的新的单词的hidden state,并作为下一层的输入。
    • decoder本身也会类似encoder一样去做self attention,不同的是,decoder只对左边已经生成的序列进行attention,对还没有生成的(右边的)序列掩盖掉(masking)
  • 完整的模型图

1.5 基于位置的前向神经网络(Position-wise Feed-Forward Networks)

  • 对于encoder和decoder的每个attention层之后,我们还会在连接一个全连接的前向神经网络。这个网络包含了两个线性转换和中间加一个ReLU的激活函数
    $$FFN(x) =\max(0, xW_1+b_1) W_2+b_2$$
  • 这里每一层,我们都用不同的$W_1,W_2,b_1,b_2$。

1.6 词向量矩阵及Softmax层

  • 这里我们使用常见的词向量矩阵,并encoder会把词向量映射到$d_\text{model}$空间上,作为第一层的输入
  • 在做预测的时候,我们会将输出向量映射到一个词表大小的概率空间中,并使用softmax来归一化到一个$[0,1]$之间的概率值。

1.7 位置编码(position embeddings)

  • 因为模型没有recurrence及convolution的操作,所以为了让模型能够分辨不同位置的单词,我们需要对单词的位置进行编码。

$$PE(pos, 2i)=\sin(pos/10000^{2i/d_\text{model}}) \ PE(pos, 2i+1)=\cos(pos/10000^{2i/d_\text{model}})$$

  • pos是这个单词在句子中的位置,i是这个位置向量的第i个维度的编号。这样的波长形成了一个从$2\pi$到$1000\cdot 2\pi$的几何级数。这样会使得模型更容易学到相对距离,因为$PE_{pos+k}$可以表示为$PE_{pos}$的一个线性变化。

1.8 Transformer 对比RNN及CNN

  • 我们发现RNN需要进行$O(n)$个序列操作,而Transformer和CNN只需要$O(1)$个
  • CNN会形成一个层级结构,类似树状,所以任意两个单词到达的最大路径长度是$O(\log_k(n))$
  • 如果self-attention只对该单词周围r个单词进行attention操作,我们可以得到restricted版本的self-attention, 这样可以减少每一层的计算复杂度,但为增加两个任意词之间到达的最长路径

1.9 可视化attention

  • 一个例子发现attention能找到长距离的依赖关系“making … more difficult”

1.9 可视化attention

  • 一个例子发现attention能找到名词的对应关系”Law” 和 “application” 都对应于 “its”

1.9 可视化attention

  • 一个例子发现attention能找到句子中的结构关系,比如某一些head能发现单词的依赖关系

2. Transformer模型的训练细节

  • 优化方法
  • 正则化 (regularization)
  • label smoothing

2.1 优化方法

  • Adam 优化方法,$\beta_1=0.9, \beta_2=0.98, \epsilon=10^{-9}$
  • learning rate是随着训练的过程中,通过以下一个函数进行变化。一开始在前 warmup_steps个训练迭代中learning rate是线性增长的,往后随着步长的增加而下降。 一般会设置 warmup_steps = 4000
    $$lr = d_\text{model}^{-0.5} \cdot \min(\text{step_num}^{-0.5},\text{step_num}\cdot \text{warmup_steps}^{-1.5}) $$

2.2 正则化 Regularization

  • 对每一个子层的输出,在该子层的输出加上该子层的输入之前进行dropout
  • 对encoder及decoder,词向量和位置向量求和之后都进行dropout

2.3 Label Smoothing

  • 对于正确的标注label,在其one-hot表达上,加上一个均匀分布的向量,这个smoothing的数值是$\epsilon_{ls}=0.1$

3 Tranformer源码解析

我们来看一下基于OpenNMT中Transformer的实现

3.1 编码器encoder

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
31
32
33
34
35
36
37
38
39
class TransformerEncoderLayer(nn.Module):
"""编码器中的一个层 """
def __init__(self, d_model, heads, d_ff, dropout, max_relative_positions=0):
self.self_attn = MultiHeadedAttention(
heads, d_model, dropout=dropout,
max_relative_positions=max_relative_positions)
self.feed_forward = PositionwiseFeedForward(d_model, d_ff, dropout)
self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
self.dropout = nn.Dropout(dropout)

def forward(self, inputs, mask):
# 1. LayerNorm 2. self-attention 3. dropout 4. Feed-Forward
input_norm = self.layer_norm(inputs)
context, _ = self.self_attn(input_norm, input_norm, input_norm, mask=mask, type="self")
out = self.dropout(context) + inputs
return self.feed_forward(out)

class TransformerEncoder(EncoderBase):
def __init__(self, num_layers, d_model, heads, d_ff, dropout, embeddings, max_relative_positions):
...
self.embeddings = embeddings
self.transformer = nn.ModuleList([TransformerEncoderLayer(...) for i in range(num_layers)])
self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)

def forward(self, src, lengths=None):
...
# 单词的编码
emb = self.embeddings(src)

out = emb.transpose(0, 1).contiguous()
words = src[:, :, 0].transpose(0, 1)
w_batch, w_len = words.size()
padding_idx = self.embeddings.word_padding_idx
mask = words.data.eq(padding_idx).unsqueeze(1) # [B, 1, T]
# 遍历多层网络
for layer in self.transformer:
out = layer(out, mask)
out = self.layer_norm(out)
return emb, out.transpose(0, 1).contiguous(), lengths

3.2 解码 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class TransformerDecoderLayer(nn.Module):
'''解码器中一个单层'''
def __init__(self, d_model, heads, d_ff, dropout,
self_attn_type="scaled-dot", max_relative_positions=0):

# 定义self-attention类型
if self_attn_type == "scaled-dot":
self.self_attn = MultiHeadedAttention(heads, d_model, dropout, max_relative_positions)
elif self_attn_type == "average":
self.self_attn = AverageAttention(d_model, dropout=dropout)
# 定义对encoder所有输出单词的context attention
self.context_attn = MultiHeadedAttention(heads, d_model, dropout)
self.feed_forward = PositionwiseFeedForward(d_model, d_ff, dropout)
self.layer_norm_1 = nn.LayerNorm(d_model, eps=1e-6)
self.layer_norm_2 = nn.LayerNorm(d_model, eps=1e-6)
self.drop = nn.Dropout(dropout)

def forward(self, inputs, memory_bank, src_pad_mask, tgt_pad_mask, layer_cache=None, step=None):
# 对还未生成的单词做masking
dec_mask = None
if step is None:
tgt_len = tgt_pad_mask.size(-1)
future_mask = torch.ones(...)
future_mask = future_mask.triu_(1).view(1, tgt_len, tgt_len)
dec_mask = torch.gt(tgt_pad_mask + future_mask, 0)

input_norm = self.layer_norm_1(inputs)
if isinstance(self.self_attn, MultiHeadedAttention):
query, attn = self.self_attn(input_norm, input_norm, input_norm, ..., type="self")
elif isinstance(self.self_attn, AverageAttention):
query, attn = self.self_attn(input_norm, mask=dec_mask, layer_cache=layer_cache, step=step)
# 对encoder输出做context attention
query = self.drop(query) + inputs
query_norm = self.layer_norm_2(query)
mid, attn = self.context_attn(memory_bank, memory_bank, query_norm, ..., type="context")
output = self.feed_forward(self.drop(mid) + query)
return output, attn

class TransformerDecoder(DecoderBase):
def __init__(self, num_layers, d_model, heads, d_ff, attn_type,
copy_attn, self_attn_type, dropout, embeddings,
max_relative_positions):
self.embeddings = embeddings

# Decoder State
self.state = {}
self.transformer_layers = nn.ModuleList([TransformerDecoderLayer(...) for i in range(num_layers)])

self._copy = copy_attn
self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)

def forward(self, tgt, memory_bank, step=None, **kwargs):
...
src = self.state["src"]
src_words = src[:, :, 0].transpose(0, 1)
tgt_words = tgt[:, :, 0].transpose(0, 1)
src_batch, src_len = src_words.size()
tgt_batch, tgt_len = tgt_words.size()

emb = self.embeddings(tgt, step=step) # len x batch x embedding_dim

output = emb.transpose(0, 1).contiguous()
src_memory_bank = memory_bank.transpose(0, 1).contiguous()

# Masking
pad_idx = self.embeddings.word_padding_idx
src_pad_mask = src_words.data.eq(pad_idx).unsqueeze(1) # [B, 1, T_src]
tgt_pad_mask = tgt_words.data.eq(pad_idx).unsqueeze(1) # [B, 1, T_tgt]

for i, layer in enumerate(self.transformer_layers):
...
# 多层网络
output, attn = layer(output, src_memory_bank, src_pad_mask, tgt_pad_mask, layer_cache, step)
output = self.layer_norm(output)
dec_outs = output.transpose(0, 1).contiguous()
attn = attn.transpose(0, 1).contiguous()
attns = {"std": attn}
return dec_outs, attns

3.3 Transformer模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class NMTModel(nn.Module):
def __init__(self, encoder, decoder):
super(NMTModel, self).__init__()
self.encoder = encoder
self.decoder = decoder

def forward(self, src, tgt, lengths, bptt=False):
tgt = tgt[:-1] # 从输入中去掉最后一个单词
# 编码
enc_state, memory_bank, lengths = self.encoder(src, lengths)
if bptt is False:
self.decoder.init_state(src, memory_bank, enc_state)
# 解码
dec_out, attns = self.decoder(tgt, memory_bank, memory_lengths=lengths)
return dec_out, attns

3.4 训练

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
31
32
33
34
35
36
37
38
39
40
class Trainer(object):
# 定义训练的类
def __init__(self, model, train_loss, valid_loss, optim,
trunc_size=0, shard_size=32,
norm_method="sents", grad_accum_count=1, n_gpu=1, gpu_rank=1,
gpu_verbose_level=0, report_manager=None, model_saver=None,
average_decay=0, average_every=1):
...
# 把模型设置成训练状态
self.model.train()

def train(self, train_iter, train_steps, save_checkpoint_steps=5000, valid_iter=None, valid_steps=10000):
...
# 开始遍历每个mini-batch
for i, (batches, normalization) in enumerate(
self._accum_batches(train_iter)):
step = self.optim.training_step
...
# 累积多个mini-batch算出来的gradient
self._gradient_accumulation(
batches, normalization, total_stats,
report_stats)
# 把多个gradient求平均值并更新模型参数
if self.average_decay > 0 and i % self.average_every == 0:
self._update_average(step)
...
# 做validation,可用于判断是否需要终止训练
if valid_iter is not None and step % valid_steps == 0:
valid_stats = self.validate(valid_iter, moving_average=self.moving_average)
...

# 保存model
if (self.model_saver is not None
and (save_checkpoint_steps != 0
and step % save_checkpoint_steps == 0)):
self.model_saver.save(step, moving_average=self.moving_average)

if train_steps > 0 and step >= train_steps:
break
return total_stats

3.5 实战例子

  • 运行预处理句子,
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    python preprocess.py -train_src data/src-train.txt -train_tgt data/tgt-train.txt -valid_src data/src-val.txt -valid_tgt data/tgt-val.txt -save_data data/demo

    # 会得到以下三个文件
    demo.train.pt: serialized PyTorch file containing training data
    demo.valid.pt: serialized PyTorch file containing validation data
    demo.vocab.pt

    (2) 训练
    python train.py -data data/demo -save_model demo-model \
    -layers 6 -rnn_size 512 -word_vec_size 512 -transformer_ff 2048 -heads 8 \
    -encoder_type transformer -decoder_type transformer -position_encoding \
    -train_steps 200000 -max_generator_batches 2 -dropout 0.1 \
    -batch_size 4096 -batch_type tokens -normalization tokens -accum_count 2 \
    -optim adam -adam_beta2 0.998 -decay_method noam -warmup_steps 8000 -learning_rate 2 \
    -max_grad_norm 0 -param_init 0 -param_init_glorot \
    -label_smoothing 0.1 -valid_steps 10000 -save_checkpoint_steps 10000 \
    -world_size 4 -gpu_ranks 0 1 2 3

    (3) 翻译
    python translate.py -model demo-model_acc_XX.XX_ppl_XXX.XX_eX.pt -src data/src-test.txt -output pred.txt -replace_unk -verbose

本章小结

  • Google的Transformer模型
    • 编码器,解码器
    • 传统的注意力机制及Multi-head attention
    • 基于位置的单词编码,及词向量,输出层
    • 可视化multi-head attention
    • Transformer与RNN和CNN神经翻译模型的对比
  • Google模型的训练细节
    • 优化器选择
    • 正则化
    • label smoothing
  • 实战演示
    • 介绍encoder,decoder类及model类
    • 介绍如何训练模型
    • 运用OpenNMT开源工具来实战演示