1.注意力机制
为了解决序列到序列模型记忆长序列能力不足的问题,一个直观的想法是,当要生成一个目标语言单词时,不光考虑前一个时刻的状态和已经生成的单词,还考虑当前要生成的单词和源语言句子中哪些单词更相关,即更关注源语言的哪些词,这种做法就叫作注意力机制(Attention mechanism)
图4-14是一个示例,假设模型已经生成单词“我”后,要生成下一个单词,显然和源语言“love”关系最大,因此将源语言中“love”对应的状态乘以一个较大的权重,如0.6,而其余词的权重则较小,最终将源语言句子中每个单词对应的状态加权求和,并用作新状态更新的一个额外输入。
注意力权重的计算公式为:
式中,表示源序列中时刻的状态;表示目标序列中前一个时刻的状态;attn是注意力计算公式,即通过两个输入状态的向量,计算一个源序列时刻的注意力分数;,其中L为源序列的长度;最后对整个源序列每个时刻的注意力分数使用Softmax函数进行归一化,获得最终的注意力权重。
注意力公式attn的计算方式有多种,如:
公式(4-26)
(1)多层感知器
(2)双线性
(3)点积
(4)避免因为向量维度d过大导致点积结果过大
通过引入注意力机制,使得基于循环神经网络的序列到序列模型的准确率有了大幅度的提高。
2.自注意力模型
受注意力机制的启发,当要表示序列中某一时刻的状态时,可以通过该状态与其他时刻状态之间的相关性(注意力)计算,即所谓的“观其伴、知其义”这又被称作自注意力机制(Self-attention)。
具体地,假设输入为个向量组成的序列,输出为每个向量对应的新的向量表示,其中所有向量的大小均为。那么的计算公式为:
式中,是整个序列的索引值;是与之间的注意力(权重),其通过公式(4-26)中的attn函数计算,然后再经过Softmax函数进行归一化后获得。直观上的含义是如果与越相关,则它们计算的注意力值就越大,那么对对应的新的表示的贡献就越大。
通过自注意力机制,可以直接计算两个距离较远的时刻之间的关系,而在循环神经网络中,由于信息时沿着时刻逐层传递的,因此当两个相关性较大的时刻距离较远时,会产生较大的信息损失。虽然引入了门控机制模型,如LSTM等,可以部分解决这种长距离依赖问题,但是治标不治本。因此,基于自注意力机制的自注意力模型已经逐步取代循环神经网络,成为自然语言处理的标准模型。
3.Transfomer
然而,要想真正取代循环神经网络,自注意力模型还需要解决如下问题:
- 在计算自注意力时,没有考虑输入的位置信息,因此无法对序列进行建模
- 输入向量同时承担了三种角色,即计算注意力权重时的两个向量以及被加权的向量,导致其不容易学习
- 只考虑了两个输入序列单元之间的关系,无法建模多个输入序列单元之间更复杂的关系
- 自注意力计算结果互斥,无法同时关注多个输入
下面分别就这些问题给出相应的解决方案,融合了以下方案的自注意力模型拥有一个非常炫酷的名字---Transformer。这个单词并不容易翻译,从本意上讲,其是将一个向量序列变换成另一个向量序列,所以可以翻译成“变换器”或“转换器”。其还有另一个含义是“变压器”,也就是对电压进行变换,所以翻译成变压器比较形象,还寓意着该模型如同变形金刚一样强大。目前Transformer还没有一个翻译的共识,绝大部分人更愿意使用其英文名。
1.融入位置信息
位置信息对于序列的表示至关重要,原始的自注意力模型没有考虑输入向量的位置信息,导致其与词袋模型类似,两个句子只要包含相同的词相同,即使顺序不同,它们的表示也完全相同。
为了解决这一问题,需要为序列中每个输入的向量引入不同的位置信息以示区分,有两种引入位置信息的方式-----位置嵌入(Position Embeddings)和位置编码(Position Encodings)。
其中,位置嵌入与词嵌入类似,即为序列中每个绝对位置赋予一个连续、低维、稠密的向量表示。而位置编码则是使用函数,直接将一个整数(位置索引值)映射到一个维向量上。映射公式为:
式中,为序列中的位置索引值;是位置编码向量中的索引值。
无论是使用位置嵌入还是位置编码,在获得一个位置对应的向量后,再与该位置对应的词向量进行相加,即可表示该位置的输入向量。这样即使词向量相同,但是如果它们所处的位置不同,其最终的向量表示也不相同,从而解决了原始自注意力模型无法对序列进行建模的问题。
2.输入向量角色信息
原始的自注意力模型在计算时直接使用两个输入向量,然后使用得到的注意力对同一个输入向量加权,这样导致一个输入向量同时承担了三种角色:查询(Query)、键(Key)和值(Value)。
更好的做法是,对不同的角色使用不同的向量,为了做到这一点,可以使用不同的参数矩阵对原始的输入向量做线性变换,从而让不同的变换结果承担不同的角色。
具体地,分别使用三个不同的参数矩阵对原始的输入向量做线性变换,从而让不同的变换结果承担不同的角色。具体地,分别使用三个不同的参数矩阵,、和将输入向量映射为三个新的向量、和,分别表示查询、键和值对应的向量。新的输出向量计算公式为:
式中,,其中为序列的长度。
3.多层注意力
原始的自注意力模型仅考虑了序列中任意两个输入序列单元之间的关系,而在实际应用中,往往需要同时考虑更多输入序列单元之间的关系,即更高阶的关系。如果直接建模高阶关系,会导致模型的复杂度过高。
一方面,类似于图模型中的消息传播机制(Message Propogation),这种高阶关系可以通过堆叠多层自注意力模型实现。
另一方面,类似于多层感知器,如果直接堆叠多层注意力模型,由于每层的变换都是线性的(注意力计算一般使用线性函数),最终模型依然是线性的。
因此,为了增强模型的表示能力,往往在每层自注意力计算之后,增加一个非线性的多层感知器(MLP)模型。
另外,如果将自注意力模型看作特征抽取器,那么多层感知器就是最终的分类器。同时,为了使模型更容易学习,还可以使用层归一化(Layer Normalization)、残差连接(Residual Connections)等深度学习的训练技巧。
自注意力层、非线性层以及以上的这些训练技巧,构成了一个更大的Transformer层,也叫作Transformer块(Block)
4.自注意力计算结果互斥
由于自注意力结果需要归一化,导致即使一个输入和多个其他的输入相关,也无法同时为这些输入赋予较大的注意力值,即自注意力结果之间是互斥的,无法同时关注多个输入。
因此,如果能使用多组自注意力模型产生多组不同的注意力结果,则不同组注意力模型可能关注到不同的输入上,从而增强模型的表达能力。
那么如何产生多组自注意力模型呢?方法非常简单,只需要设置多组映射矩阵即可,然后将产生的多个输出向量拼接。为了将输出结果作为下一组的输入,还需要将拼接后的输出向量再经过一个线性映射,映射回d维向量。该模型又叫作多头自注意力(Multi-head Self-attention)模型。从另一个方面理解,多头自注意力机制相当于多个不同的自注意力模型的集成(Ensemble),也会增强模型的效果。类似卷积神经网络中的多个卷积核,也可以将不同的注意力头理解为抽取不同类型的特征。
4.基于Transformer的序列到序列模型
以上介绍的Transfomer模型可以很好地对一个序列编码。此外,与循环神经网络类似,Transformer也可以很容易地实现解码功能,将两者结合起来,就实现了一个序列到序列的模型,于是可以完成机器翻译等多种自然语言处理任务。
解码模块的实现与编码模块基本相同,不过要接收解码模块的最后一层输出作为输入,这也叫作记忆(Memory),另外还要将已经编码的输出结果作为输入。
5.Transformer模型的优缺点
与循环神经网络相比,Transformer能够直接建模输入序列单元之间更长距离的依赖关系,从而使得Transformer对于长序列建模的能力更强。另外,在Transformer的编码阶段,由于可以利用GPU等多核计算设备并行地计算Transformer块内部的自注意力模型,而循环神经网络需要逐个计算,因此Transformer具有更高的训练速度。
不过,与循环神经网络相比,Transformer的一个明显的缺点是参数量过于庞大。每一层的Transformer块大部分参数集中在4--15中的绿色方框中,即自注意力模型中输入向量的三个角色映射矩阵、多头机制导致相应参数的倍增和引入非线性的多层感知器等。更主要的是,还需要堆叠多层的Transformer块,从而参数量又扩大多倍。最终导致一个实用的Transformer模型含有巨大的参数量。
以本书后续章节将要介绍的BERT模型为例,BERT-base含有12层Transformer块,参数量超过1.1亿个,而24层的BERT-large,参数量达到了3.4亿个之多。巨大的参数量导致Transformer模型非常不容易训练,尤其是当训练数据较小时。因此,为了降低模型的训练难度,基于大规模数据的预训练模型应运而生,这也是本书将要介绍的重点内容。唯此,才能发挥Transformer模型强大的表示能力。
6.模型实现
新版本的Pytorch(1.2版以上)实现了Transformer模型。其中,nn.TransformerEncoder实现了编码模块,它是由多层Transformer块构成的,每个块使用TransformerEncoderLayer实现。下面演示具体的示例。
import torch
from torch import nn, optim
encoder_layer = nn.TransformerEncoderLayer(d_model=4,nhead=2)
# 创建一个Transformer块,每个输入向量、输出的向量维度为4,头数为2
src = torch.rand(2,3,4)
# 随机生成输入,三个参数分别为序列的长度,批次的大小和每个输入向量的维度
out = encoder_layer(src)
print(out)
# 然后可以将多个Transformer块堆叠起来,构成一个完整的nn.TransformerEncoder
transformer_encoder=nn.TransformerEncoder(encoder_layer,num_layers=6)
out = transformer_encoder(src)
print(out)
# 解码模块也类似,TransformerDecoderLayer定义了一个解码模块的Transformer块,通过多层块堆叠构成
# nn.TransformerDecoder,下面演示具体的调用方式
memory = transformer_encoder(src)
decoder_layer = nn.TransformerDecoderLayer(d_model=4,nhead=2)
transformer_decoder = nn.TransformerDecoder(decoder_layer,num_layers=6)
out_part = torch.rand(2,3,4)
out = transformer_decoder(out_part,memory)
print(out)