宁德信息港

当前位置: 首页 >军事

想要千行代码搞定Transformer这

来源: 作者: 2019-05-15 00:35:51

(导语)想要做个神经机器翻译模型?想要做个强大的Transformer?搞定这千行PaddlePaddle代码你也可以。

目前,无论是从性能、结构还是业界应用上,Transformer 都有很多无可比拟的优势。本文将介绍Paddle Paddle 的Transformer项目,我们从项目使用到源码解析带你玩一玩NMT。只需千行模型代码,Transformer实现带回家。

其实PyTorch、TensorFlow等主流框架都有Transformer的实现,但如果我们需要将它们应用到产品中,还是需要修改很多。

例如谷歌大脑构建的Tensor2Tensor,它开始是为了实现 Transformer,后来扩大到了各种任务。对基于Tensor2Tensor实现翻译任务的用户,他们需要在10万+行TensorFlow代码找到需要的部分。

PaddlePaddle 提供的Transformer实现,项目代码只有2000+行,简洁优雅。如果我们使用大Batch Size,那么在预测速度上,PaddlePaddle复现的模型比TensorFlow官方使用tensor2tensor实现的模型还要快4倍。

项目地址:

Transformer怎么用

相比此前 Seq2Seq 模型中广泛使用的循环神经络,Transformer 使用深层注意力机制获得了更好的效果,目前大多数神经机器翻译模型都采用了这一络结构。另外,不论是新兴的预训练语言模型,还是问答或句法分析,Transformer都展现出强大的建模能力。

相比传统NMT使用循环层或卷积层抽取文本信息,Transformer使用自注意力络抽取并表征这些信息,下图对比了不同层级的特点:

图注:不同络的主要性质,其中n表示序列长度、d为隐向量维度、k为卷积核大小。例如单层计算复杂度,一般句子长度n都小于隐向量维度d,那末自注意力层级的计算复杂度小。

如上所示,Transformer使用的自注意力模型主要具有以下优点,1)络结构的计算复杂度;2)由于序列操作数复杂度低,模型的并行度很高;3)路径长度小,能够更好地表示长距离依赖关系;4)模型更容易训练。

现在,如果我们需要训练一个Transformer,那末的方法是什么?固然是直接跑已复现的模型了,下面我们将跑一跑PaddlePaddle 实现的Transformer。

处理数据

在Paddle的复现中,百度采用原论文测试的WMT'16 EN-DE 数据集,它是一个中等规模的数据集。这里比较方便的是,百度将数据下载和预处理等过程都放到了gen_脚本中,包括Tokenize 和 BPE 编码。

在这个项目中,我们既可以通过脚本预处理数据,也可以使用百度预处理好的数据集。首先简单的方式是直接运行gen_脚本,运行后可以生成gen_data文件夹,该文件夹主要包括以下文件:

其中 wmt16_ende_data_bpe 文件夹包含终使用的英德翻译数据。

如果我们从头下载并预处理数据,那末大概需要花1到2个小时完成预处理。为此,百度也提供了预处理好的WMT'16 EN-DE数据集,它包括训练、验证和测试所需要的BPE数据和字典。

其中,BPE策略会把稀疏词拆分为高频的子词,这样既能解决低频词无法训练的问题,也能合理降低词表规模。

如果不采取BPE的策略,要么词表的范围变得很大,从而使训练速度变慢或者显存太小而没法训练;要么一些低频词会当作未登录词处理,从而得不到训练。

预处理数据地址:

如果我们有其它数据集,例如中英翻译数据,也可以根据特定的格式进行定义。例如用空格分隔不同的token(对于中文而言需要提前用分词工具进行分词),用\t分隔源语言与目标语句对。

训练模型

如果需要履行模型训练,我们也可以直接运行训练主函数。如下简要配置了数据路径以及各种模型参数:

# 显存使用的比例,显存不足可适当增大,为1

export FLAGS_fraction_of_gpu_memory_to_use=0.8

# 显存清算的阈值,显存不足可适当减小,小为0,为负数时不启用

export FLAGS_eager_delete_tensor_gb=0.7

python -u \

--src_vocab_fpath gen_data/wmt16_ende_data_bpe/vocab_e.32000 \

--trg_vocab_fpath gen_data/wmt16_ende_data_bpe/vocab_e.32000 \

--special_token '' '' '' \

--train_file_pattern gen_data/wmt16_ende_data_bpe/-de \

--token_delimiter ' ' \

--use_token_batch True \

--batch_size 1600 \

--sort_type pool \

--pool_size 200000 \

n_head 8 \

n_layer 4 \

d_model 512 \

d_inner_hid 1024 \

prepostprocess_dropout 0.3

此外,如果显存不够大,那么我们可以将Batch Size减小一点。为了快速测试训练效果,我们将模型调得比Base Transformer还小(下降络的层数、head的数量、以及隐层的大小)。

上面仅展现了小部分的超参设置,更多的配置可以在GitHub项目文件中找到。默许情况下,模型每迭代一万次保存一次模型,每个epoch结束后也会保存一次cheekpoint。此外,在我们训练的过程中,默认每一百次迭代会打印一次模型信息,其中ppl表示的是困惑度,困惑度越小模型效果越好。

在单机训练中,默许使用所有 GPU,可以通过 CUDA_VISIBLE_DEVICES 环境变量来设置使用的 GPU,例如CUDA_VISIBLE_DEVICES=0,1,表示使用0号和1号卡进行训练。

预测推断

训练完Transformer后就只可以履行推断了,我们需要运行对应的推断文件。我们也可以在推断过程中配置超参数,但注意超参需要和前面训练时保持一致。

python -u \

--src_vocab_fpath gen_data/wmt16_ende_data_bpe/vocab_e.32000 \

--trg_vocab_fpath gen_data/wmt16_ende_data_bpe/vocab_e.32000 \

--special_token '' '' '' \

--test_file_pattern gen_data/wmt16_ende_data_bpe/-de \

--token_delimiter ' ' \

--batch_size 32 \

model_path trained_models/iter_del \

n_head 8 \

n_layer 4 \

d_model 512 \

d_inner_hid 1024 \

prepostprocess_dropout 0.3

beam_size 5 \

max_out_len 255

相比模型的训练,推断过程需要一些额外的超参数,例如配置model_path指定模型所在目录、设置beam_size 和 max_out_len 来指定 Beam Search 每一步候选词的个数和翻译长度。这些超参数也可以在中找到,该文件对这些超参都有注释说明。

履行以上预测命令会将翻译结果直接打出来,每行输出是对应行输入得分的翻译。对使用 BPE 的英德数据,预测出的翻译结果也将是 BPE 表示的数据,所以需要还原成原始数据才能进行正确评估。如下命令可以将t 内的翻译结果(BPE表示)恢复到t文件中(tokenize后的数据):

sed -r 's/(@@ )|(@@ ?$)//g' t t

在未使用集成方法的情况下,百度表示 base model 和 big model 在收敛后,测试集的 BLEU 值参考如下:

这两个预训练模型也提供了下载地址:

Base:

Big:

Transformer 怎样改

如果我们想要训练自己的Transformer,那末又该怎样理解并修改PaddlePaddle代码呢?如果我们需要根据自己的数据集和任务改代码,除前面数据预处理过程,模型结构等模块有时也需要修改。这就需要我们先理解源代码了,PaddlePaddle的源代码基本都是基础的函数或运算,我们很容易理解并使用。

对于PaddlePaddle不熟悉的读者可查阅文档,也可以看看入门教程,了解基本编写模式后就可以看懂整个实现了。

PaddlePaddle官地址:

如 Seq2Seq 一样,原版 Transformer 也采取了编码器-解码器框架,但它们会使用多个 Multi-Head 注意力、前馈络、层级归一化和残差连接等。下图从左到右展示了原论文所提出的 Transformer 架构、Multi-Head 注意力和标量点乘注意力。

上图右边的点乘注意力就是标准 Seq2Seq 模型中的注意力机制,中间的Multi-head 注意力其实就是将一个隐层信息切分为多份,并单独计算注意力信息,使得一个词与其它多个目标词的注意力信息计算更精确。左侧为Transformer的整体架构,编码器与解码器由多个类似的模块组成,后面将简要介绍这些模块与对应的Paddle代码。

点乘注意力

注意力机制目前在机器翻译中已极其流行了,我们可以认为Transformer是一种堆叠多层注意力络的模型,它采用的是一种名为经缩放的点乘注意力机制。这种注意力机制使用经缩放的点乘作为作为评分函数,从而评估各隐藏状态对当前预测的重要性,如下是该注意力的表达式:

其中 Query 向量与 (Key, Value ) 向量在 NMT 中相当于目标语输入序列与源语输入序列,Query 与 Key 向量的点乘,经过 SoftMax 函数后可得出一组归一化的概率。这些几率相当于给源语输入序列做加权平均,即表示在生成新的隐层信息的时候需要关注哪些词。

在Transformer 的PaddlePaddle实现中,经缩放的点乘注意力是在Multi-head 注意力函数下实现的,如下所示为上述表达式的实现代码:

def scaled_dot_product_attention(q, k, v, attn_bias, d_key, dropout_rate):

"""

Scaled Dot-Product Attention

"""

product = tmul(x=q, y=k, transpose_y=True, alpha=d_key**-0.5)

if attn_bias:

product += attn_bias

weights = ftmax(product)

if dropout_rate:

weights = opout(

weights,

dropout_prob=dropout_rate,

seed=opout_seed,

is_test=False)

out = tmul(weights, v)

return out

在这个函数中,q、k、v和公式中的一样,attn_bias用于Mask掉选定的特定位置(encode 的self attention 和decoder端的encode attention都是屏蔽掉padding的词;decoder的self attention屏蔽掉当前词后面的词,目的是为了和解码的进程保持一致),因此在给不同输入加权时忽略该位置的输入。

如上product计算的是q和k之间的点乘,且经过根号下d_key(key的维度)的缩放。这里我们可以发现参数alpha可以直接对矩阵乘法的结果进行缩放,默认情况下它为1.0,即不进行缩放。在Transformer原论文中,作者表示如果d_key比较小,那么直接点乘和带缩放的点乘差别不大,所以他们认为高维情况下可能不带缩放的乘积太大而令Softmax函数饱和。

weights表示对输入的不同元素加权,即不同输入对当前预测的重要性,训练中也可以对该权重进行Dropout。out表示按照weights对输入V进行加权和,得出来就是当前注意力的运算结果。

Muti-head 注意力

Multi-head 注意力其实就是多个点乘注意力并行地处理并将结果拼接在一起。一般而言,我们可以对三个输入矩阵 Q、V、K 分别进行线性变换,然后分别将它们投入 h 个点乘注意力函数并拼接所有的输出结果。

这种注意力允许模型联合关注不同位置的不同表征子空间信息,我们可以理解为在参数不同享的情况下,多次执行点乘注意力。如下所示为Muti-head 注意力的表达式:

其中每一个head都为一个点乘注意力,不同head 的输入是相同Q、K、V的不同线性变换。

总体而言,Paddle的Multi-head 注意力实现分为几个步骤:先为Q、K、V执行线性变换;再变换维度以计算点乘注意力;计算各head的注意力输出并合并在一起。

1.线性变换

如前公式所示,Muti-head首先要执行线性变换,从而令不同的head关注不同表征空间的信息。这种线性变换即乘上不同的权重矩阵,且模型在训练过程中可以学习和更新这些权重矩阵。在如下的Paddle代码中,我们可以直接调用全连接层() 完成线性变换。

def __compute_qkv(queries, keys, values, n_head, d_key, d_value):

"""

Add linear projection to queries, keys, and values.

"""

q = (input=queries,

size=d_key * n_head,

bias_attr=False,

num_flatten_dims=2)

# For encoder-decoder attention in inference, insert the ops and vars

# into global block to use as cache among beam search.

fc_layer = wrap_layer_with_block(

, fault_main_program().current_block()

.parent_idx) if cache is not None and static_kv else

k = fc_layer(

input=keys,

size=d_key * n_head,

bias_attr=False,

num_flatten_dims=2)

v = fc_layer(

input=values,

size=d_value * n_head,

bias_attr=False,

num_flatten_dims=2)

return q, k, v

直接调用全连接层会自动为输入创建权重,且我们要求不使用偏置项和激活函数。这里比较方便的是,Paddle 的() 函数可以接受高维输入,省略了手动展平输入向量的操作。因此这里有num_flatten_dims=2,行将前两个维度展平为一个维度,第三个维度保持不变。

例如对于输入张量q而言,线性变换的输出维度应该是[batch_size,max_sequence_length,d_key * n_head],一个维度即n_head个d_key维的Query向量。每一个d_key维的向量都会馈送到不同的head,并拼接起来。

2.维度变换

为了进行Multi-Head的运算,我们需要将线性变换的结果进行reshape和转置操作。现在我们将这几个张量的一个维度分割成不同的head,并做转置以便于后续运算。

具体而言,输入张量q、k和v的维度信息为[bs, max_sequence_length, n_head * hidden_dim],我们希望把它们转换为[bs, n_head, max_sequence_length, hidden_dim]。

def __split_heads_qkv(queries, keys, values, n_head, d_key, d_value):

reshaped_q = shape(

x=queries, shape=[0, 0, n_head, d_key], inplace=True)

q = anspose(x=reshaped_q, perm=[0, 2, 1, 3])

reshape_layer = wrap_layer_with_block(

shape,

fault_main_program().current_block()

.parent_idx) if cache is not None and static_kv else shape

transpose_layer = wrap_layer_with_block(

anspose,

fault_main_program().current_block().

parent_idx) if cache is not None and static_kv else anspose

reshaped_k = reshape_layer(

x=keys, shape=[0, 0, n_head, d_key], inplace=True)

k = transpose_layer(x=reshaped_k, perm=[0, 2, 1, 3])

reshaped_v = reshape_layer(

x=values, shape=[0, 0, n_head, d_value], inplace=True)

v = transpose_layer(x=reshaped_v, perm=[0, 2, 1, 3])

return q, k, v

如上使用shape() 和 anspose() 函数完成分割与转置。其中 shape() 在接收输入张量后会按照形状[0, 0, n_head, d_key]进行转换,其中0表示从输入张量对应维数复制出来。此外,因为inplace设置为True,那么reshape操作就不会进行数据的复制,从而提升运算效率。

后面的转置就比较简单了,只需要依照维度索引将第1个维度和第2个维度交换就行了。此外为了更快地执行推断,Paddle实现代码还做了非常多的优化,例如这部份后续会对推断进程的缓存和处理流程进行优化。

3.合并

前面已经介绍过点乘注意力了,那末上面对q、k、v履行维度变换后就可直接传入点乘注意力函数,并计算出head_1、head_2等注意力结果。现在一步只需要将这些head拼接起来就完成了整个过程,也就完成了上面Multi-head 注意力的计算式。

由于每个批量、head和时间步都会计算得出一个注意力向量,因此总体上注意力计算结果的维度信息为[bs, n_head, max_sequence_length, hidden_dim]。如果要将不同的head拼接在一起,行将head这个维度合并到hidden_dim中去,因此合并的过程和前面维度变换的进程正好相反。

def __combine_heads(x):

"""

Transpose and then reshape the last two dimensions of inpunt tensor x

so that it becomes one dimension, which is reverse to __split_heads.

"""

if len(ape) != 4:

raise ValueError("Input(x) should be a 4-D Tensor.")

trans_x = anspose(x, perm=[0, 2, 1, 3])

# The value 0 in shape attr means copying the corresponding dimension

# size of the input as the output dimension size.

return shape(

x=trans_x,

shape=[0, 0, trans_ape[2] * trans_ape[3]],

inplace=True)

如上合并过程会先检验维度信息,然后先转置再reshape合并不同的head。注意在原论文中,合并不同的head后,还需要再做一个线性变换,这个线性变换的结果就是Muti-head 注意力的输出了。

,我们再将上面的4部份串起来就是Transformer核心的Multi-head 注意力。理解了各个模块后,下面串起来就能愉快地看懂整个过程了:

q, k, v = __compute_qkv(queries, keys, values, n_head, d_key, d_value)

q, k, v = __split_heads_qkv(q, k, v, n_head, d_key, d_value)

ctx_multiheads = scaled_dot_product_attention(q, k, v, attn_bias, d_model,

dropout_rate)

out = __combine_heads(ctx_multiheads)

# Project back to the model size.

proj_out = (input=out,

size=d_model,

bias_attr=False,

num_flatten_dims=2)

当然,如果编码器和解码器输入到Multi-head 注意力的q与(k、v)是相同的,那末它又可称为自注意力络。

前馈络

对于每一个编码器和解码器模块,除了残差连接与层级归1化外,重要的就是堆叠Muti-head 注意力和前馈络(FFN)。前面我们已解决了Multi-head 注意力,现在需要理解主位置的前馈络了。直观而言,FFN的作用是整合Multi-head 注意力生成的上下文向量,因此能更好地利用从源语句子和目标语句子抽取的深度信息。

如下所示在原论文中,前馈络的计算过程可以表达为以下方程:

前馈络的结构很简单,一个ReLU激活函数加两次线性变换就完成了。如下基本上只需要调用Paddle的() 就可以了:

def positionwise_feed_forward(x, d_inner_hid, d_hid, dropout_rate):

hidden = (input=x,

size=d_inner_hid,

num_flatten_dims=2,

act="relu")

if dropout_rate:

hidden = opout(

hidden,

dropout_prob=dropout_rate,

seed=opout_seed,

is_test=False)

out = (input=hidden, size=d_hid, num_flatten_dims=2)

return out

现在基本上核心操作就定义完了,后面还有更多模块与架构,例如怎样利用核心操作搭建编码器模块与解码器模块、如何搭建整体Transformer模型等,读者可继续阅读原项目中的简洁代码。整体而言,包括上面代码在内,千行代码就可以完全弄懂Transformer,Paddle的Transformer复现值得我们仔细读一读。

此外,在这千行模型代码中,为了给训练和推断加速,还有很多特殊技巧。例如在Decoder中加入对Encoder计算结果的缓存等。加上这些技巧,PaddlePaddle的实现才能在大Batch Size下实现4倍推断加速。

因为本身PaddlePaddle代码就已经非常精炼,通过它们也很容易理解这些技巧。基本上看函数名称就能知道大致的作用,再结合文档使用就能完全读懂了。

,除了模型架构,全部项目还会有其它组成部分,例如训练、推断、数据预处理等等。这些代码同样非常简洁,我们可以根据实际需求阅读并修改它们。总体而言, PaddlePaddle 的Transformer 实现确切非常合适理解与修改。想要跑一跑神经机器翻译的同学,PaddlePaddle的Transformer实现确实值得推荐。

月经经期延长怎么调理
月经后期吃什么排污
月经后期病吃什么好

相关推荐