阅读思路
- 以往的模型存在的问题/瓶颈——结构的哪些缺陷导致了性能的有限
- 论文提出的注意力结构解决了什么问题,为什么可以解决
- 核心:Transformer架构和Self-Attention机制
- 对应:Model ,Why Self-Attention
Abstract
Transformer之前的sequence transduction models(序列传导模型):
- 复杂的循环
- 卷积为基础:RNN/CNN
- encoder-decoder结构
- 表现好的模型大多借助了Attention机制链接或增强
Transformer结构的创新:
- 完全摒弃RNN/CNN的结构
- 仍使用encoder-decoder
- 完全基于注意力
Introduction
Feedforward Neural Network(FNN)

不适合做序列转导的原因
在做Embedding的时候,对词向量有两种处理方式,但对于FNN来说都损失了一定的信息:
- 平均:丢失词语顺序信息
- 拼接:
- FNN需要固定维度的输入,但句子通常长度不同,FNN处理效率低下
- 同样失去顺序信息
Recurrent Neural Network(RNN)


解决的问题:
- 保留顺序信息: RNN按照时间顺序接收Token的输入
- 保留上下文依赖:RNN有记忆机制
- 支持不定长输入
潜在问题:
- 难以处理输入输出序列长度不同的对齐问题
- 梯度消失或爆炸(me)
Encoder-Decoder
简单理解:将RNN的上下两个部分拆来来做,encoder只管输入,decoder只管输出

潜在问题:
- 远距离遗忘:处理长序列,可能遗忘较远的信息
- 无重要性区别:序列中各个元素的贡献度和重要程度不同
注意力机制

解决:
- 长序列遗忘问题:对序列中元素加上不同的权重
- 解决不同时间步对当前时刻输出的“重要性”问题:所有时间步的输入在计算当前时刻输出时被同等对待,忽略不同时间步对当前时刻输出的重要性可能存在差异
Introduction中介绍了RNN系列的研究,并提到注意力机制通常是和RNN一起使用。
Background
关于CNN
为解决顺序计算问题,引入CNN以支持并行,但难以捕捉远距离关系。
介绍注意力机制
- 自注意力机制
- Memory Network的递归注意力机制
本文
- Transformer是首个完全基于自注意力,无需使用RNN/CNN 的序列建模模型
Model Arch
模型维度 d_model = 512
首先,如何解决的串行计算问题。
串行计算主要发生在以往模型的encoder部分,所有的后面的token必须等待前面的输入完成才能参与计算。
Encoder
但对于翻译任务,待翻译的源文本是全局已知的。因此需要设计一种方法,让序列中所有 token 可以同时感知上下文信息,并行参与计算,而非像 RNN 那样按顺序逐个处理。
这种计算涉及到两种信息:
- 该序列中所有token的向量
- 每个token在序列中的位置信息
由于翻译文本同样是已知的答案,且,对于已经翻译完的部分来说,这个部分的答案对模型是可见的——后续的部分理论上对于模型是不可见的,就使用mask遮住,不进入decoder进行计算。
所以在decoder处,模型顺序生成翻译后的文本,且decoder接收每次生成后的答案作为输入——decoder的输入来自两个地方:encoder输出和自己上一步的输出

关键模块:
- (Masked)Multi-Head Attention
- Feed Forward
参数:
- N×:可以任意堆叠,论文N=6
- Transformer的encoder和decoder输入输出维度不会发生变化
完整Encoder代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class TransformerEncoderLayer(nn.Module): def __init__(self, d_model=512, num_heads=8, d_ff=2048, dropout=0.1): super().__init__() self.self_attn = MultiHeadAttention(d_model, num_heads) self.ffn = nn.Sequential( nn.Linear(d_model, d_ff), nn.ReLU(), nn.Linear(d_ff, d_model), ) self.norm1 = nn.LayerNorm(d_model) self.norm2 = nn.LayerNorm(d_model) self.dropout = nn.Dropout(dropout) def forward(self, x): attn_out = self.self_attn(x, x, x) x = self.norm1(x + self.dropout(attn_out)) ffn_out = self.ffn(x) x = self.norm2(x + self.dropout(ffn_out)) return x
|
计算流程
- Inputs DO: Embedding GET tensor
- Inputs 的各个token DO: position encoding GET pe
- 使用正弦和余弦函数编码:
| PE(pos,2i)=sin(pos/100002i/dmodel) |
| PE(pos,2i+1)=cos(pos/100002i/dmodel) |
- tensor = pe + tensor
进入encoder
- 每个 token 通过权重矩阵计算出对应的 Q、K、V 向量,送入多头注意力
- 多头的输出,通过残差和原数据融合,再过Norm,让数值趋于稳定 GET a
- a经过 Feed Forward,它由两个线性变换组成,中间夹有 ReLU 激活函数,学习更复杂的特征
FFN(x)=max(0,xW1+b1)W2+b2
- 再次经过残差链接和归一化(矩阵维度不发生变化)
位置编码的简单实现
1 2 3 4 5 6 7 8 9 10 11 12 13
| import torch import math
def positional_encoding(seq_len, d_model): pe = torch.zeros(seq_len, d_model) position = torch.arange(0, seq_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) return pe
|
进入解码器:模型预测下一个输出
8. 带有掩码的多头注意力(Masked Multi-Headed)对过去的输出进行自注意力的计算 GET b
9. b经过残差和Norm
10. 编码器的输出作为K,V,刚刚的b作为Q输入到多头注意力(Q对KV问道:我的下一个输出应该是什么)
这部分称为交叉注意力(Cross-Attention),decoder 的查询(Q)去关注 encoder 的输出(K、V)
- Feedforward+Add+Norm
找到词语:经过Linear和Softmax
- Embedding:将词语映射到向量空间
- Transformer 中的 Embedding 层与整个模型联合训练(端到端学习),而非使用预训练好的固定词向量。
- 位置信息的加入:让token在语义空间的含义随着上下文进行偏移,一些词语在不同语境下的含义略有差异,位置信息就可以利用上下文得到的情报对原词语的语义进行属于该文章的微调
BERT 使用的是可学习位置编码(Learned Position Embedding),每个位置对应一个可训练向量,而非 Transformer 的固定正余弦编码,缺点是这种标量所含的信息量太少,模型难以理解更抽象的关系的远近;注意力依靠向量计算。
将位置信息抽象到高维空间(高维向量),使其包含更丰富的信息,让模型感受到某种相对关系……
Q:提出问题 (N,512)
K:是否存在这个问题的对应答案(值) (N,512)
V:这个值 (N,512)
每个QKV都有一个自己的权重矩阵(512,512)。经过了和权重的计算后,Q、K、V的尺寸仍为(N,512),这个部分送入attention进行计算。
Attention(Q,K,V)=softmax(dkQKT)V
- QKT :计算Q和K的点积,也就是相似度的计算
- dk:缩放
- softmax:QK相似度的概率权重,K对应的token对Q对应的token的影响的重要程度,这个权重也有自己对自己的。
- 和V进行乘法:V获得加权平均。
简化版代码实现:
1 2 3 4 5 6 7 8 9 10 11 12
| import torch import torch.nn.functional as F
def scaled_dot_product_attention(Q, K, V): """ Q, K, V: (batch_size, seq_len, d_k) """ scores = torch.matmul(Q, K.transpose(-2, -1)) scores = scores / (K.size(-1) ** 0.5) weights = F.softmax(scores, dim=-1) output = torch.matmul(weights, V) return output, weights
|
修改1:权重矩阵改为使用尺寸(64,512)
修改2:单组QKV的权重矩阵→八组(多组,多头)
所以经过了权重的QKV尺寸变为了(N,64),这样的结果也是八组。
追加1:来自八个头的结果进行拼接(8×64=512),又得到尺寸为(N,512)的输出(看似回到了单头的尺寸)
追加2:将拼接后的结果经过一个线性层融合,得到最后的(N,512)的输出
为什么降维到64?又做融合?为什么多头?
答:每个头的Q询问了不同的信息,让模型学到更丰富而详细的表示。做融合的原因是通过额外的线性变换,让不同头学到的信息充分交互。为什么是8个64维呢?在保持计算量不变的情况下,有多头优势。
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
| class MultiHeadAttention(nn.Module): def __init__(self, d_model=512, num_heads=8): super().__init__() self.num_heads = num_heads self.d_k = d_model // num_heads self.W_Q = nn.Linear(d_model, d_model) self.W_K = nn.Linear(d_model, d_model) self.W_V = nn.Linear(d_model, d_model) self.out_proj = nn.Linear(d_model, d_model) def forward(self, Q, K, V): batch_size = Q.size(0) Q = self.W_Q(Q).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2) K = self.W_K(K).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2) V = self.W_V(V).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2) attn_output, _ = scaled_dot_product_attention(Q, K, V) attn_output = attn_output.transpose(1, 2).contiguous().view( batch_size, -1, self.num_heads * self.d_k) return self.out_proj(attn_output)
|
为什么Self-Attention
一:在序列长度不超过模型维度时(n ≤ d),自注意力的计算复杂度不高于 RNN 和 CNN,同时具备更好的并行性和长距离依赖捕捉能力
二:有并行计算的能力,解决了串行等待的问题
三:模型内部学习长距离依赖能力更强