Fairseq Notes

1.基于CNN的翻译系统模型结构

​ 在自然语言处理中,大部分流行的seq2seq模型都是基于RNN结构去构建encoder和decoder,但是RNN对于下一个状态的预测需要依赖前面的所有历史状态,使得并行化操作难以充分进行,难以发挥完全发挥GPU并行的效率。相反CNN通过在固定窗口内的计算,使得计算的并行化变得更加简单,而且通过多层CNN网络可以构建层级结构(hierarchical structure),可以达到利用更短的路径去覆盖更长范围内的信息。

​ Facebook提出了基于CNN的机器翻译模型,并开源了CNN的机器翻译工具Fairseq

1.1 Pooling Encoder

​ 最简单的non-recurrent encoder就是把k个连续的单词的词向量求平均值,通过在句子左右两边都添加额外的空单词(paddings),可以使得encoder输出跟原来句子同等长度的hidden embeddings。

  • 假设原来的句子的词向量(word embedding)表示为 \(w=[w_1,\cdots,w_m],~\forall~w_j\in R^f\)
  • absolute position embeddings用于编码位置信息 \(p=[p_1,\cdots,p_m],~\forall~p_j\in R^f\) \[e_j = w_j + p_j,~~ z_j = {1\over k} \sum_{t=-k/2}^{k/2}e_{j+t} \]
  • 传统的attention机制 \[ c_i = \sum_{j=1}^m a_{ij} e_j\]

1.2 卷积编码器Convolutional Encoder NMT

  • ​ 卷积编码器在pooling encoder的基础上进行改进,使用一个CNN-a 卷积层来进一步编码源语言句子中的词。输出原句长度的第一层卷积结果Z。

\[z_j = CNN\_a(e)_j \]

  • ​ 注意attention的时候,使用了另一个CNN-c卷积层来编码源语言句子中的单词,还是输出原句长度的第一层卷积结果,作为计算atttention weight的encoder hidden states。然后计算atttention weight,再进行加权求和。

\[c_i = \sum_{j=1}^m a_{ij} CNN\_c(e)_j\]

​ 该模型的encoder 采用的是CNN,但其decoder还是采用了传统的RNN模型

1.3 全卷积神经翻译模型 Convolutional NMT

  • 该模型的encoder和decoder都采用的是卷积核CNN,动图演示

  • 卷积核结构

    • 假设有1D的卷积核的窗口大小是k(比如k=5),每个卷积核都可以用一个权重矩阵\(W\in \mathbb{R}^{2d\times kd}\)和 bias \(b_w\in \mathbb{R}^{2d}\)。对于窗口内的词向量 \(X\in \mathbb{R}^{k\times d}\)把所有单词拼接成一个长向量 \(X'\in \mathbb{R}^{kd}\). \[Y=WX'+b_w = [A B] \in \mathbb{R}^{2d} \\ A,B\in \mathbb{R}^{d} \]

    • 接下来采用Gated Linear Unites(GLU)的方式来进行编码, \(\sigma()\)是一个非线性的激活函数, \(\otimes\)是element-wise mulitiplication \[v([A B] = A \otimes \sigma(B) \in \mathbb{R}^d\]

    • 残差连接 Residual Connection: 把这一层的输入也累加到下一层的输出 \[h_i^l = v(W^l [h_{(i-k)/2}^{l-1},\cdots,h_{(i+k)/2}^{l-1}]+b_w^l)+h_i^{l-1} \in \mathbb{R}^d\]

  • 编码器 Encoder:

    • 假设原来的句子的词向量(word embedding)表示为 \(w=[w_1,\cdots,w_m],~\forall~w_j\in \mathbb{R}^f\)

    • absolute position embeddings用于编码位置信息 \(p=[p_1,\cdots,p_m],~\forall~p_j\in \mathbb{R}^f\) \[e_j = w_j + p_j \\ \]

    • encoder 先用一个线性函数\(f:\mathbb{R}^f\rightarrow \mathbb{R}^d\),把词向量映射到d维空间中

    • 接下来encoder会将词向量通过一层层卷积核,得到每一层的单词的隐式表达(hidden state), 其中 \(z_j^u\) 代表的是第u层CNN中第j个单词的表达

  • Multi-step Attention机制

    • 假设已经翻译的单词的词表达是 \(g=[g_1,\cdots, g_n]\),跟源语言的词表达一样,这里也是word embeddings加上positional embeddings

    • 假设decoder的卷积核的hidden state \(h_i^l\), 可以进一步计算decoder已经生成的单词的每一层的单词表达 \[d_i^l = W_d^l h_i^l + b_d^l + g_i \]

    • 假设encoder 最顶层(假设是第u层)中,每个单词的表达是\(z_j^u\)。可以计算decoder第l层中第i个已经生成的单词\(h_i^l\)与源语言句子中最顶层(也即是第u层)的第j个单词 \(z_j^u\)的权重:
      \[a_{ij}^l = {\exp(d_i^l \cdot z_j^u) \over \sum_{t=1}^m \exp(d_i^l \cdot z_t^u) } \]

    • 可以进一步计算在decoder第l层,在第i个时刻的上下文向量(也即是context vector)如以下公式,其中将encoder最顶层(第u层)的词向量\(z_j^u\)与最底层的词向量\(e_j\)相加。

    \[c_i^l = \sum_{j=1}^m a_{ij}^l (z_j^u + e_j) \]

    • \(c_i^l\)加到\(h_i^l\)中,作为decoder 的下一层的输入
  • 解码器 decoder

    • 把decoder最顶层的hidden state \(h_i^L\) 通过一个线性的函数映射到词表空间上\(d\rightarrow |V|\),之后在通过一个softmax函数 归一化成一个条件概率向量: \[p(y_{i+1}|y_1,\cdots, y_i, x)= softmax(W_o h_i^L + b_0) \in \mathbb{R}^{|V|} \]
  • 模型的结构图

Drawing

1.4 全卷积神经翻译模型对比RNN神经翻译模型

  1. 全卷积神经网络使用层级结构,可以充分地并行化
  2. 对于一个窗口大小为\(k\)的CNN,编码一个特征向量可以总结一个窗口为n个单词的信息,只需要做\(O(n/k)\)个卷积核操作。对比RNN,RNN编码一个窗口为n个单词的信息,需要做\(O(n)\)个操作,跟句子的长度成正比
  3. 对于一个CNN的输入,都进行了相同数量的卷积操作及非线性操作。对比RNN,第一个输入的单词进行了n词非线性操作,而最后一个输入的单词只进行了一次非线性操作。对于每个输入都进行相同数量的操作会有利于训练
  4. 训练CNN NMT需要非常小心地设置参数及调整网络中某些层的缩放

2 使用CNN完成神经机器翻译系统的tricks

​ 训练过程中,需要将网络中某些部分进行缩放(scaling),需要对权重初始化,需要对超参数进行设置

2.1 缩放操作(scaling)

  • 将残差层的输出乘以 \(\sqrt{0.5}\), 这样会减小一半的偏差variance
  • 对于attention机制产生的上下文向量 \(c_{ij}^l\) 乘以一个系数 \(m\sqrt{1/m}\), 其中m为源语言句子中单词个个数,这样做的好处也是能减小偏差。
  • 对于CNN decoder有multiple atttention的情况,将encoder 每一层的gradient乘以一个系数,该系数是使用的attention的数量。注意,只对encoder中除了源语言单词的词向量矩阵以外的参数,放大gradient,源语言的词向量矩阵的gradient不进行放大。在实验中,这样的操作会使得训练能更加稳定。

2.2 参数初始化

  • 所有的词向量矩阵从一个以0为中心,标准差为0.1的高斯分布中随机初始化 \(\mathcal{N}(0, \sqrt{n_l})\), 其中\(n_l\)为输入到这个神经元的输入个数,一般可以设置为0.1。这样能有助于保持一个正态分布的偏差。
  • 还需要对每一层的激活函数输出进行正规化(normalization), 比如残差连接中,每一层层的输出向量需要先做正则化,再把这一层的输入加到输出的向量上。
  • 对于GLU,需要对其权重 \(W\)从一个正态分布\(\mathcal{N}(0, \sqrt{4p\over n_l})\)中随机抽样,而其bias设置成0
  • 对每一层网络的输入向量都进行dropout处理

2.3 超参数设置

  • encoder 和decoder都是用512维的hidden units,512维的word embeddings
  • 训练的时候使用Nesterov's accelerated gradient 的方法进行优化模型,momentum 设置成0.99
  • 如果gradient的norm超过0.1就把gradient 重新归一化到0.1以内。
  • 初始的learning rate设置成0.25,如果在每次进行valudation的时候dev数据集中的perplexity没有下降,就将learning rate乘以0.1, 一直持续到learning rate 降到\(10^{-4}\)以下停止训练
  • mini-batch的大小设置成每次处理64句双语句子

3. Facebook CNN 机器翻译系统代码解析

  • 相应的代码可以在github上找到 fairseq
  • 安装
1
2
3
4
git clone https://github.com/pytorch/fairseq.git
cd fairseq
pip install -r requirements.txt
python setup.py build develop

https://fairseq.readthedocs.io/en/latest/command_line_tools.html

3.1 Code

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
# 预处理数据
$ bash prepare-wmt14en2de.sh --icml17

$ cd examples/translation/
$ bash prepare-wmt14en2de.sh
$ cd ../..

# 将数据处理成二进制形式,加速读写
$ TEXT=examples/translation/wmt14_en_de
$ python preprocess.py --source-lang en --target-lang de \
--trainpref $TEXT/train --validpref $TEXT/valid --testpref $TEXT/test \
--destdir data-bin/wmt14_en_de --thresholdtgt 0 --thresholdsrc 0

# 训练模型
# 如果显存不足,可以将--max-tokens设置成1500
$ mkdir -p checkpoints/fconv_wmt_en_de
$ python train.py data-bin/wmt14_en_de \
--lr 0.5 --clip-norm 0.1 --dropout 0.2 --max-tokens 4000 \
--criterion label_smoothed_cross_entropy --label-smoothing 0.1 \
--lr-scheduler fixed --force-anneal 50 \
--arch fconv_wmt_en_de --save-dir checkpoints/fconv_wmt_en_de

# 测试,生成
$ python generate.py data-bin/wmt14_en_de \
--path checkpoints/fconv_wmt_en_de/checkpoint_best.pt --beam 5 --remove-bpe

3.2 使用预训练好的模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 下载模型及测试数据
$ mkdir -p data-bin
$ curl https://dl.fbaipublicfiles.com/fairseq/models/wmt14.v2.en-fr.fconv-py.tar.bz2 | tar xvjf - -C data-bin
$ curl https://dl.fbaipublicfiles.com/fairseq/data/wmt14.v2.en-fr.newstest2014.tar.bz2 | tar xvjf - -C data-bin

# 进行翻译生成
$ python generate.py data-bin/wmt14.en-fr.newstest2014 \
--path data-bin/wmt14.en-fr.fconv-py/model.pt \
--beam 5 --batch-size 128 --remove-bpe | tee /tmp/gen.out


# 对翻译结果打分
$ grep ^H /tmp/gen.out | cut -f3- > /tmp/gen.out.sys
$ grep ^T /tmp/gen.out | cut -f2- > /tmp/gen.out.ref
$ python score.py --sys /tmp/gen.out.sys --ref /tmp/gen.out.ref

3.3 Notes

  • CNN NMT类 FConvModel
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
@register_model('fconv')
class FConvModel(FairseqModel):
"""
Args:
encoder (FConvEncoder): the encoder
decoder (FConvDecoder): the decoder
"""

def __init__(self, encoder, decoder):
...
@staticmethod
def add_args(parser):
parser.add_argument('--dropout', type=float, metavar='D',
help='dropout probability')
parser.add_argument('--encoder-embed-dim', type=int, metavar='N',
help='encoder embedding dimension')
parser.add_argument('--encoder-embed-path', type=str, metavar='STR',
help='path to pre-trained encoder embedding')
parser.add_argument('--encoder-layers', type=str, metavar='EXPR',
help='encoder layers [(dim, kernel_size), ...]')
parser.add_argument('--decoder-embed-dim', type=int, metavar='N',
help='decoder embedding dimension')
parser.add_argument('--decoder-embed-path', type=str, metavar='STR',
help='path to pre-trained decoder embedding')
parser.add_argument('--decoder-layers', type=str, metavar='EXPR',
help='decoder layers [(dim, kernel_size), ...]')
parser.add_argument('--decoder-out-embed-dim', type=int, metavar='N',
help='decoder output embedding dimension')
@classmethod
def build_model(cls, args, task):
base_architecture(args)
...
encoder = FConvEncoder(
dictionary=task.source_dictionary,
embed_dim=args.encoder_embed_dim,
embed_dict=encoder_embed_dict,
convolutions=eval(args.encoder_layers),
dropout=args.dropout,
max_positions=args.max_source_positions,
)
decoder = FConvDecoder(
dictionary=task.target_dictionary,
embed_dim=args.decoder_embed_dim,
embed_dict=decoder_embed_dict,
convolutions=eval(args.decoder_layers),
out_embed_dim=args.decoder_out_embed_dim,
attention=eval(args.decoder_attention),
dropout=args.dropout,
max_positions=args.max_target_positions,
share_embed=args.share_input_output_embed,
)
return FConvModel(encoder, decoder)
  • CNN 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
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
class FConvEncoder(FairseqEncoder):
def __init__(
self, dictionary, embed_dim=512, embed_dict=None, max_positions=1024,
convolutions=((512, 3),) * 20, dropout=0.1, left_pad=True,
):
...
# 定义词向量矩阵及位置矩阵
self.embed_tokens = Embedding(num_embeddings, embed_dim, self.padding_idx)
self.embed_positions = PositionalEmbedding(
max_positions,
embed_dim,
self.padding_idx,
left_pad=self.left_pad,
)

convolutions = extend_conv_spec(convolutions)
in_channels = convolutions[0][0]
self.fc1 = Linear(embed_dim, in_channels, dropout=dropout)
self.projections = nn.ModuleList()
self.convolutions = nn.ModuleList()
self.residuals = []

# 定义CNN层及残差层
layer_in_channels = [in_channels]
for _, (out_channels, kernel_size, residual) in enumerate(convolutions):
if residual == 0:
residual_dim = out_channels
else:
residual_dim = layer_in_channels[-residual]
self.projections.append(Linear(residual_dim, out_channels)
if residual_dim != out_channels else None)
if kernel_size % 2 == 1:
padding = kernel_size // 2
else:
padding = 0
self.convolutions.append(
ConvTBC(in_channels, out_channels * 2, kernel_size,
dropout=dropout, padding=padding)
)
self.residuals.append(residual)
in_channels = out_channels
layer_in_channels.append(out_channels)
self.fc2 = Linear(in_channels, embed_dim)

def forward(self, src_tokens, src_lengths):
# 查找词向量及位置向量
x = self.embed_tokens(src_tokens) + self.embed_positions(src_tokens)
x = F.dropout(x, p=self.dropout, training=self.training)
input_embedding = x

# 将词的表达映射到CNN的输入空间 fc1: R^f ->R^d
x = self.fc1(x)

# 在句子左右两边添加padding
encoder_padding_mask = src_tokens.eq(self.padding_idx).t() # -> T x B
if not encoder_padding_mask.any():
encoder_padding_mask = None

# 转置:B x T x C -> T x B x C
x = x.transpose(0, 1)

residuals = [x]
# 多层的CNN 层叠起来
for proj, conv, res_layer in zip(self.projections, self.convolutions, self.residuals):
if res_layer > 0:
residual = residuals[-res_layer]
residual = residual if proj is None else proj(residual)
else:
residual = None

if encoder_padding_mask is not None:
x = x.masked_fill(encoder_padding_mask.unsqueeze(-1), 0)

x = F.dropout(x, p=self.dropout, training=self.training)
if conv.kernel_size[0] % 2 == 1:
# padding is implicit in the conv
x = conv(x)
else:
padding_l = (conv.kernel_size[0] - 1) // 2
padding_r = conv.kernel_size[0] // 2
x = F.pad(x, (0, 0, 0, 0, padding_l, padding_r))
x = conv(x)
# GLU 层
x = F.glu(x, dim=2)

# 残差层
if residual is not None:
x = (x + residual) * math.sqrt(0.5)
residuals.append(x)

# T x B x C -> B x T x C
x = x.transpose(1, 0)

# 将x映射回词向量空间 R^d -> R^f
x = self.fc2(x)

if encoder_padding_mask is not None:
encoder_padding_mask = encoder_padding_mask.t() # -> B x T
x = x.masked_fill(encoder_padding_mask.unsqueeze(-1), 0)

# 将gradient放大
x = GradMultiply.apply(x, 1.0 / (2.0 * self.num_attention_layers))

# 把input embedding加到output中
y = (x + input_embedding) * math.sqrt(0.5)

return {
'encoder_out': (x, y),
'encoder_padding_mask': encoder_padding_mask, # B x T
}
  • 解码器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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
class FConvDecoder(FairseqIncrementalDecoder):
def __init__(self,...):

# 定义词向量矩阵及位置向量矩阵
self.embed_tokens = Embedding(num_embeddings, embed_dim, padding_idx)
self.embed_positions = PositionalEmbedding(
max_positions,
embed_dim,
padding_idx,
left_pad=self.left_pad,
) if positional_embeddings else None

convolutions = extend_conv_spec(convolutions)
in_channels = convolutions[0][0]

self.fc1 = Linear(embed_dim, in_channels, dropout=dropout)
self.projections = nn.ModuleList()
self.convolutions = nn.ModuleList()
self.attention = nn.ModuleList()
self.residuals = []

# 定义多层CNN
layer_in_channels = [in_channels]
for i, (out_channels, kernel_size, residual) in enumerate(convolutions):
if residual == 0:
residual_dim = out_channels
else:
residual_dim = layer_in_channels[-residual]
self.projections.append(Linear(residual_dim, out_channels)
if residual_dim != out_channels else None)
self.convolutions.append(
LinearizedConv1d(in_channels, out_channels * 2, kernel_size,
padding=(kernel_size - 1), dropout=dropout)
)
self.attention.append(AttentionLayer(out_channels, embed_dim)
if attention[i] else None)
self.residuals.append(residual)
in_channels = out_channels
layer_in_channels.append(out_channels)

self.adaptive_softmax = None
self.fc2 = self.fc3 = None

def forward(self, prev_output_tokens, encoder_out_dict=None, incremental_state=None):
...
# 获得位置向量
if self.embed_positions is not None:
pos_embed = self.embed_positions(prev_output_tokens, incremental_state)
else:
pos_embed = 0

# 获得上一个生成的单词的词向量
x = self._embed_tokens(prev_output_tokens, incremental_state)

# 将词向量加上位置向量作为当前时刻的输入
x += pos_embed
x = F.dropout(x, p=self.dropout, training=self.training)
target_embedding = x

# 将输入从词向量空间映射到CNN输入空间
x = self.fc1(x)

# 转置:B x T x C -> T x B x C
x = self._transpose_if_training(x, incremental_state)

# 多层的CNN 堆叠
avg_attn_scores = None
num_attn_layers = len(self.attention)
residuals = [x]
for proj, conv, attention, res_layer in zip(self.projections, self.convolutions, self.attention,
self.residuals):
if res_layer > 0:
residual = residuals[-res_layer]
residual = residual if proj is None else proj(residual)
else:
residual = None

x = F.dropout(x, p=self.dropout, training=self.training)
x = conv(x, incremental_state)
x = F.glu(x, dim=2)

# 注意力机制
if attention is not None:
x = self._transpose_if_training(x, incremental_state)

x, attn_scores = attention(x, target_embedding, (encoder_a, encoder_b), encoder_padding_mask)

if not self.training and self.need_attn:
attn_scores = attn_scores / num_attn_layers
if avg_attn_scores is None:
avg_attn_scores = attn_scores
else:
avg_attn_scores.add_(attn_scores)

x = self._transpose_if_training(x, incremental_state)

# 残差连接
if residual is not None:
x = (x + residual) * math.sqrt(0.5)
residuals.append(x)

# 转置:T x B x C -> B x T x C
x = self._transpose_if_training(x, incremental_state)

# fc2:将输入映射到词表大小空间,可进行预测
if self.fc2 is not None and self.fc3 is not None:
x = self.fc2(x)
x = F.dropout(x, p=self.dropout, training=self.training)
x = self.fc3(x)

return x, avg_attn_scores
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
def main(args, init_distributed=False):
...

# 载入数据
load_dataset_splits(task, ['train', 'valid'])

# 构建模型及优化函数
model = task.build_model(args)
criterion = task.build_criterion(args)

# 构建训练器 trainer
trainer = Trainer(args, task, model, criterion, dummy_batch, oom_batch)

# 初始化dataloader
epoch_itr = task.get_batch_iterator(...)

# 训练一直到learning rate太小就停止
max_epoch = args.max_epoch or math.inf
max_update = args.max_update or math.inf
lr = trainer.get_lr()
train_meter = StopwatchMeter()
train_meter.start()
while lr > args.min_lr and epoch_itr.epoch < max_epoch and trainer.get_num_updates() < max_update:
# 训练一个epoch
train(args, trainer, task, epoch_itr)

if epoch_itr.epoch % args.validate_interval == 0:
valid_losses = validate(args, trainer, task, epoch_itr, valid_subsets)

# 只用第一个validation loss去更新learning rate
lr = trainer.lr_step(epoch_itr.epoch, valid_losses[0])

# 保存模型
if epoch_itr.epoch % args.save_interval == 0:
save_checkpoint(args, trainer, epoch_itr, valid_losses[0])
train_meter.stop()

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!