一、引言
Transformer 是 Google 的团队在 2017 年提出的一种 NLP 经典模型,现在比较火热的 Bert 也是基于 Transformer。Transformer 模型使用了 Self-Attention 机制,不采用 RNN 的顺序结构,使得模型可以并行化训练,而且能够拥有全局信息。
2020年Transformer火爆了整个CV界,像DETR直接使用CNN+Transformer的混合结构,还有Vision Transformer直接舍弃了CNN结构,单纯使用Transformer来做图像分类,这些模型都取得了比较好的效果。
二、Transformer核心思想
Transformer中抛弃了传统的CNN和RNN,整个网络结构完全是由Attention机制组成。作者采用Attention机制的原因是考虑到RNN(或者LSTM,GRU等)的计算是顺序的,RNN相关算法只能从左向右依次计算或者从右向左依次计算,这种机制带来了两个问题:
- 时间片 t 的计算依赖 t−1t−1 时刻的计算结果,这样限制了模型的并行能力;
- 顺序计算的过程中信息会丢失,尽管LSTM等门机制的结构一定程度上缓解了长期依赖的问题,但是对于特别长期的依赖现象,LSTM依旧无能为力。
Transformer的提出就是为了解决RNN中存在的问题:
- 首先它使用了Attention机制,将序列中的任意两个位置之间的距离是缩小为一个常量;
- 其次它不是类似RNN的顺序结构,因此具有更好的并行性,符合现有的GPU框架。
三、Transformer结构
Transformer的模型本质上就是一个Encoder-Decoder结构,输入序列先进行Embedding,经过Encoder之后结合上一次output再输入Decoder,最后用softmax计算序列下一个单词的概率。
3.1 Input Embedding
Transformer的输入为Input Embedding + Positional Embedding,其中Input Embedding在pytorch中用torch.nn.Embedding包实现,将每个词转化为词向量Input Embedding,一般dim=256或512,代码中用d_model变量来表示dim,即词向量的维度。
torch.nn.Embedding(num_embeddings, embedding_dim, padding_idx=None, max_norm=None, norm_type=2.0, scale_grad_by_freq=False, sparse=False, _weight=None)
torch.nn.Embedding 模块可以看做一个字典,字典中每个索引对应一个词和词的embedding形式。利用这个模块,可以给词做embedding的初始化操作。
主要参数介绍:
- num_embeddings :字典中词的个数
- embedding_dim:embedding词向量的维度
代码如下:
class Embeddings(nn.Module):
def __init__(self, d_model, vocab):
super(Embeddings, self).__init__()
# d_model表示embedding的维度,即词向量的维度;vocab表示词汇表的数量。
self.lut = nn.Embedding(vocab, d_model)
self.d_model = d_model #表示embedding的维度
def forward(self, x):
return self.lut(x) * math.sqrt(self.d_model)
3.2 Positional Embedding
在RNN中,对句子的处理是一个个word按顺序输入的。但在 Transformer 中,输入句子的所有word是同时处理的,没有考虑词的排序和位置信息。因此,Transformer 的作者提出了加入 “positional encoding” 的方法来解决这个问题。“positional encoding“”使得 Transformer 可以衡量 word 位置有关的信息。
如何实现具有位置信息的encoding呢?作者提供了两种思路:
- 通过训练学习 positional encoding 向量;
- 使用公式来计算 positional encoding向量。
试验后发现两种选择的结果是相似的,所以采用了第2种方法,优点是不需要训练参数,而且即使在训练集中没有出现过的句子长度上也能用(意思是说假设在训练集的句子最长有100个单词,而测试集来了个200个单词的句子,公式法Positional Encodding照样能解决)。
Positional Encoding的公式如下:
公式参数解析:
指这个word在这个句子中的位置,如“I am Chinese”这句话中,
指代的是I,
指代的是am;
指的是embedding词向量的偶数维度(d_model = 512);
指的是embedding词向量的奇数维度(d_model = 512);
的取值范围为[0,
];
# Positional Encoding
class PositionalEncoding(nn.Module):
"实现PE功能"
def __init__(self, d_model, dropout, max_len=5000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
pe = torch.zeros(max_len, d_model)
position = torch.arange(0., max_len).unsqueeze(1)
div_term = torch.exp(torch.arange(0., d_model, 2) *
-(math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term) # 偶数列
pe[:, 1::2] = torch.cos(position * div_term) # 奇数列
pe = pe.unsqueeze(0) # [1, max_len, d_model]
self.register_buffer('pe', pe)
def forward(self, x):
x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)
return self.dropout(x)
注意:"x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)" 这行代码表示;输入模型的整个Embedding是Word Embedding与Positional Embedding直接相加之后的结果。
四、Encoder
Encoder部分是由N个相同的Encoder Layer串联而成。Encoder Layer可以简化为两个部分:(1)Multi-Head Self Attention (2) Feed-Forward network。示意图如下:
4.1 Self Attention
Multi-Head Self Attention 实际上是由h个Self Attention 层并行组成,原文中h=8。我们先来介绍Self Attention。
Self Attention的输入是词向量,记为,
经过一个线性变换得到query(Q),经过第二个线性变换得到
key(K)
, 经过第三个线性变换得到value(V)
。也就是:
- query = linear_q(x)
- key = linear_k(x)
- value = linear_v(x)
用矩阵表示如下:
假设我们的输入是下图中的
,每个
是一个512维的词向量,将每个词向量先乘上一个矩阵W得到embedding,即向量
。接着这个embedding进入self-attention层,每一个向量
分别乘以3个不同的矩阵
,以向量
为例,分别得到3个不同的向量
。
接下来使用每个query 去对每个key
做attention,attention就是匹配这2个向量有多接近,比如我现在要对
和
做attention,我就可以把这2个向量做scaled Dot-product,得到
。接下来再拿
和
做attention,得到
....。计算公式见下图:
上式中,d是q和k的维度,因为的数值会随着dimension的增大而增大,所以要除以根号dimension。
接下来,把计算得到的所有值取softmax操作。
取完 softmax 操作以后,就得到了,我们用它和所有对应的
相乘,再求和,得到
,如下图:
用上述同样的方法,也可以计算出,如下图,
就是用
去对其它的k做attention,得到
,再与相对应的value值
相乘再求和得到的。
以上过程是计算的详细过程,但一般我们是用代码实现的,那么我们就得把上述过程简化为矩阵(张量)运算。
注:self-attention就是一堆矩阵乘法,可以实现GPU加速
4.2 Multi-Head Self Attention
Multi-Head-Attention 就是将embedding之后的X按维度d_model=512 切割成h=8个,分别做self-attention之后再合并在一起。
以两个head得情况为例,由乘以
生成的
,而
再分别进一步乘以2个转移矩阵变成
、
和
。接下来
再与其它所有的
做attention,然后再与
相乘,求和得到
。(
,本例中n_head为2)
同理可以得到。
将和
执行concat操作,再通过一个transformation matrix调整维度,使之与刚才的
维度一致。
从下图14可以看到 Multi-Head Attention 包含多个 Self-Attention 层,首先将输入 X 分别传递到 2个不同的 Self-Attention 中,计算得到 2 个输出结果。得到2个输出矩阵之后,Multi-Head Attention 将它们拼接在一起 (Concat),然后传入一个Linear层,得到 Multi-Head Attention 最终的输出Z 。可以看到 Multi-Head Attention 输出的矩阵 Z 与其输入的矩阵 X 的维度是一样的。
Multi-Head Self Attention代码如下:
class MultiHeadedAttention(nn.Module):
def __init__(self, h, d_model, dropout=0.1):
"Take in model size and number of heads."
super(MultiHeadedAttention, self).__init__()
assert d_model % h == 0
self.d_k = d_model // h
self.h = h
self.linears = clones(nn.Linear(d_model, d_model), 4)
self.attn = None
self.dropout = nn.Dropout(p=dropout)
def forward(self, query, key, value, mask=None):
"""
实现MultiHeadedAttention。
输入的q,k,v是形状 [batch, L, d_model]。
输出的x 的形状同上。
"""
if mask is not None:
# Same mask applied to all h heads.
mask = mask.unsqueeze(1)
nbatches = query.size(0)
# 1) 这一步qkv变化:[batch, L, d_model] ->[batch, h, L, d_model/h]
query, key, value = \
[l(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2)
for l, x in zip(self.linears, (query, key, value))]
# 2) 计算注意力attn 得到attn*v 与attn
# qkv :[batch, h, L, d_model/h] -->x:[b, h, L, d_model/h], attn[b, h, L, L]
x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)
# 3) 上一步的结果合并在一起还原成原始输入序列的形状
x = x.transpose(1, 2).contiguous().view(nbatches, -1, self.h * self.d_k)
# 最后再过一个线性层
return self.linears[-1](x)
4.3 Add & Norm
x 序列经过multi-head-self-attention 之后实际经过一个“add+norm”层,再进入feed-forward network(后面简称FFN),在FFN之后又经过一个norm再输入下一个encoder layer。
其中BatchNorm和LayerNorm的区别如下:
Batch Normalization强行让一个batch的数据的某个channel的 ,而Layer Normalization让一个数据的所有channel的
。
还是云里雾里,那再给张图,如下所示,一列代表一条数据x,行代表channel的索引:
几乎每个sub layer之后都会经过一个归一化,然后再加在原来的输入上,进行残差连接。(残差连接,防止网络退化,梯度消失)
4.4 Feed-Forward Network
Feed-Forward Network可以细分为有两层,第一层激活函数为ReLU,第二层为线性激活函数。可以表示为:
代码如下:
# Position-wise Feed-Forward Networks
class PositionwiseFeedForward(nn.Module):
"实现FFN函数"
def __init__(self, d_model, d_ff, dropout=0.1):
super(PositionwiseFeedForward, self).__init__()
self.w_1 = nn.Linear(d_model, d_ff)
self.w_2 = nn.Linear(d_ff, d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
return self.w_2(self.dropout(F.relu(self.w_1(x))))
五、Decoder
Decoder输入包括2部分,下方是前一个time step的输出的embedding,再加上一个表示位置的Positional Encoding,得到一个张量。
首先是Masked Multi-Head Self-attention,masked的意思是使attention只会attend on已经产生的sequence。因为还没有产生出来的东西不存在,就无法做attention。
输入:Encoder的输出和 对应 i-1 位置decoder的输出。所以中间的attention(上图中上面一个self attention)不是self-attention,它的Key和Value来自encoder,Query来自上一位置 Decoder 的输出。
输出:对应 i 位置的输出词的概率分布。
解码:注意,Encoder可以并行计算,一次性全部Encoding出来,但Decoder不是一次把所有序列解出来的,而是像 RNN 一样一个一个解出来的,因为要用上一个位置的输入当作attention的query。
Decoder结构:
- 包含两个 Multi-Head Attention 层,第一个 Multi-Head Attention 层采用了 Masked 操作,确保预测第 i 个位置时不会接触到未来的信息,即防止模型看到要预测的数据,防止泄露;第二个 Multi-Head Attention 层的Key,Value矩阵使用 Encoder 的编码信息矩阵 进行计算,而Query使用上一个 Decoder block 的输出计算。
- 最后有一个 Softmax 层计算下一个翻译单词的概率。
5.1 Masked操作
Masked在Scale操作之后,softmax操作之前。
因为在翻译的过程中是顺序翻译的,即翻译完第i个单词,才可以翻译第i+1个单词。通过 Masked 操作可以防止第i个单词知道第 i+1 个单词之后的信息。
Mask的目的是防止 Decoder “seeing the future”,就像防止考生偷看考试答案一样。这里mask是一个下三角矩阵,对角线以及对角线左下都是1,其余都是0。下面是个10维度的下三角矩阵:
tensor([[[1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]], dtype=torch.uint8)
当mask不为空的时候,attention计算需要将x做一个操作:scores = scores.masked_fill(mask == 0, -1e9),即把mask==0的替换为-1e9,其余不变。
5.2 Decoder Multi-head Attention
这部分和Multi-head Attention的区别是该层的输入来自encoder和上一次decoder的结果。其中k和v来自Encoder的输出,q来自上一个上一个位置的Decoder输出。
5.3 Linear and Softmax to Produce Output Probabilities
Decoder的最后一个部分是过一个linear layer将decoder的输出扩展到与vocabulary size一样的维度上。经过softmax 后,选择概率最高的一个word作为预测结果。假设我们有一个已经训练好的网络,在做预测时,步骤如下:(假设输入的句子为“我是一个学生”)
(1)给 decoder 输入 encoder 对整个句子 embedding 的结果 和一个特殊的开始符号 </s>。decoder 将产生预测,在我们的例子中应该是 ”I”。
(2)给 decoder 输入 encoder 的 embedding 结果和 “</s>I”,在这一步 decoder 应该产生预测 “am”。
(3)给 decoder 输入 encoder 的 embedding 结果和 “</s>I am”,在这一步 decoder 应该产生预测 “a”。
(4)给 decoder 输入 encoder 的 embedding 结果和 “</s>I am a”,在这一步 decoder 应该产生预测 “student”。
(5)给 decoder 输入 encoder 的 embedding 结果和 “</s>I am a student”, decoder应该生成句子结尾的标记,decoder 应该输出 ”</eos>”。
(6)然后 decoder 生成了 </eos>,翻译完成。
六、Transformer总结
6.1 优点:
- 每层计算复杂度比RNN要低,并且在self attention中可以并行计算;
- 从计算一个序列长度为n的信息要经过的路径长度来看, CNN需要增加卷积层数来扩大视野,RNN需要从1到n逐个进行计算,而Self-attention只需要一步矩阵计算就可以。Self-Attention可以比RNN更好地解决长时依赖问题。当然如果计算量太大,比如序列长度N大于序列维度D这种情况,也可以用窗口限制Self-Attention的计算数量。
6.2 缺点:
- 实践上:有些RNN轻易可以解决的问题transformer没做到,比如复制string,或者推理时碰到的sequence长度比训练时更长(因为碰到了没见过的position embedding);
- 理论上:transformers不是computationally universal(图灵完备),这种非RNN式的模型是非图灵完备的的,无法单独完成NLP中推理、决策等计算问题(包括使用transformer的bert模型等等)
七、References