关键词:Transformer、Self-Attention、多头注意力、位置编码、BERT、GPT
目录
- [为什么需要 Transformer?](#为什么需要 Transformer?)
- 整体架构一览
- [Self-Attention 自注意力机制](#Self-Attention 自注意力机制)
- [多头注意力(Multi-Head Attention)](#多头注意力(Multi-Head Attention))
- [位置编码(Positional Encoding)](#位置编码(Positional Encoding))
- 前馈网络与残差连接
- [Encoder 与 Decoder 的区别](#Encoder 与 Decoder 的区别)
- [从 Transformer 到 BERT](#从 Transformer 到 BERT)
- [从 Transformer 到 GPT](#从 Transformer 到 GPT)
- [代码实现:手写 Self-Attention](#代码实现:手写 Self-Attention)
- 总结与面试要点
1. 为什么需要 Transformer?
在 Transformer 出现之前,NLP 领域主要依赖 RNN(循环神经网络)和 LSTM。它们存在两个致命缺陷:
- 无法并行计算:RNN 必须一个时间步一个时间步地顺序处理,训练效率极低。
- 长距离依赖问题:句子很长时,梯度消失导致模型"记不住"前面的信息。
2017 年,Google 的论文《Attention is All You Need》横空出世,提出了完全基于注意力机制的 Transformer,彻底解决了上述问题,并成为后续 BERT、GPT、T5 等一切大模型的基石。
2. 整体架构一览
Transformer 由 Encoder(编码器) 和 Decoder(解码器) 两部分组成,原始论文中各堆叠 6 层。
输入序列
↓
[Embedding + Positional Encoding]
↓
┌─────────────────────┐
│ Encoder × N │
│ ┌───────────────┐ │
│ │ Multi-Head │ │
│ │ Self-Attention│ │
│ └───────┬───────┘ │
│ Add & Norm │
│ ┌───────────────┐ │
│ │ Feed Forward │ │
│ └───────┬───────┘ │
│ Add & Norm │
└─────────────────────┘
↓
┌─────────────────────┐
│ Decoder × N │
│ Masked Self-Attn │
│ Cross-Attention │
│ Feed Forward │
└─────────────────────┘
↓
Linear + Softmax
↓
输出序列
3. Self-Attention 自注意力机制
Self-Attention 是 Transformer 的核心,它让序列中每个位置都能"关注"到其他所有位置。
3.1 Q、K、V 是什么?
对于输入序列的每个词向量 x i x_i xi,通过三个可学习的权重矩阵分别映射出:
- Q(Query,查询):当前词"想要查找什么"
- K(Key,键):每个词"能提供什么信息"
- V(Value,值):每个词"实际携带的内容"
Q = X W Q , K = X W K , V = X W V Q = XW^Q, \quad K = XW^K, \quad V = XW^V Q=XWQ,K=XWK,V=XWV
3.2 注意力分数计算
Attention ( Q , K , V ) = softmax ( Q K T d k ) V \text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V Attention(Q,K,V)=softmax(dk QKT)V
- Q K T QK^T QKT:计算每对词之间的相关性分数(点积)
- d k \sqrt{d_k} dk :缩放因子,防止点积值过大导致 softmax 梯度消失
- softmax:将分数归一化为概率分布
- 最终乘以 V V V:按概率加权求和,得到每个词的上下文表示
直觉理解:处理句子"我爱北京天安门"时,"天安门"这个词在计算自己的表示时,会给"北京"分配更高的注意力权重,因为它们语义相关。
4. 多头注意力(Multi-Head Attention)
单头注意力只能从一个"角度"看词与词之间的关系。多头注意力把 Q、K、V 分别投影到 h h h 个不同的子空间,并行运行 h h h 次注意力计算:
MultiHead ( Q , K , V ) = Concat ( head 1 , ... , head h ) W O \text{MultiHead}(Q,K,V) = \text{Concat}(\text{head}_1, \ldots, \text{head}_h)W^O MultiHead(Q,K,V)=Concat(head1,...,headh)WO
head i = Attention ( Q W i Q , K W i K , V W i V ) \text{head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V) headi=Attention(QWiQ,KWiK,VWiV)
好处:不同的头可以捕捉不同类型的语言关系,例如:
- 第 1 个头关注句法结构(主谓关系)
- 第 2 个头关注语义关联(近义词)
- 第 3 个头关注指代关系("它"指代"猫")
原始论文使用 h = 8 h=8 h=8 个头, d m o d e l = 512 d_{model}=512 dmodel=512,每个头维度为 64。
5. 位置编码(Positional Encoding)
Attention 本身没有位置信息(对词序无感知)。Transformer 通过在词嵌入中加入位置编码来引入位置信息:
P E ( p o s , 2 i ) = sin ( p o s 10000 2 i / d m o d e l ) PE_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{2i/d_{model}}}\right) PE(pos,2i)=sin(100002i/dmodelpos)
P E ( p o s , 2 i + 1 ) = cos ( p o s 10000 2 i / d m o d e l ) PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{2i/d_{model}}}\right) PE(pos,2i+1)=cos(100002i/dmodelpos)
- 使用正余弦函数是因为:任意位置的编码可以表示为另一位置编码的线性组合,便于模型学习相对位置。
- 现代模型(如 LLaMA)改用 RoPE(旋转位置编码),性能更好且可外推到更长序列。
6. 前馈网络与残差连接
每个 Transformer 层还包含:
前馈网络(FFN):对每个位置独立进行两层线性变换(中间激活函数通常用 ReLU 或 GeLU):
FFN ( x ) = max ( 0 , x W 1 + b 1 ) W 2 + b 2 \text{FFN}(x) = \max(0, xW_1 + b_1)W_2 + b_2 FFN(x)=max(0,xW1+b1)W2+b2
FFN 的维度通常是 d m o d e l d_{model} dmodel 的 4 倍(如 512 → 2048 → 512),负责存储模型的"知识"。
残差连接 + Layer Norm:每个子层的输出加上输入(残差),再做归一化:
output = LayerNorm ( x + Sublayer ( x ) ) \text{output} = \text{LayerNorm}(x + \text{Sublayer}(x)) output=LayerNorm(x+Sublayer(x))
这样做的好处是防止梯度消失,使深层网络更容易训练。
7. Encoder 与 Decoder 的区别
| 组件 | Encoder | Decoder |
|---|---|---|
| Self-Attention | 双向(可看全文) | Masked(只能看已生成部分) |
| Cross-Attention | 无 | 有(关注 Encoder 输出) |
| 典型应用 | BERT(理解任务) | GPT(生成任务) |
Masked Self-Attention 是 Decoder 的关键设计:在训练时,通过遮住未来位置防止"作弊",保证自回归生成的合理性。
8. 从 Transformer 到 BERT
BERT(Bidirectional Encoder Representations from Transformers)只使用 Transformer 的 Encoder 部分。
核心思想:双向预训练
-
MLM(掩码语言模型):随机遮住 15% 的词,让模型预测被遮住的词
-
NSP(下一句预测):判断两个句子是否相邻
输入:我 [MASK] 北京 → 预测:爱
输入:[CLS] 句子A [SEP] 句子B [SEP]
BERT 的双向性使它在理解任务(分类、NER、问答)上表现优异,但不擅长文本生成。
9. 从 Transformer 到 GPT
GPT 系列只使用 Transformer 的 Decoder 部分(去掉 Cross-Attention)。
核心思想:自回归语言模型,预测下一个词
输入:今天天气 → 输出:真
输入:今天天气真 → 输出:好
输入:今天天气真好 → 输出:啊
GPT 的单向性(只能看左边)使其天然适合文本生成任务,通过 Instruction Fine-tuning 和 RLHF 变成了 ChatGPT。
10. 代码实现:手写 Self-Attention
python
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
class SelfAttention(nn.Module):
"""单头自注意力实现"""
def __init__(self, d_model: int):
super().__init__()
self.d_model = d_model
# Q、K、V 的线性投影
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.W_o = nn.Linear(d_model, d_model)
def forward(self, x, mask=None):
"""
x: (batch_size, seq_len, d_model)
mask: (batch_size, seq_len, seq_len) 可选,用于 Decoder 的因果掩码
"""
batch_size, seq_len, _ = x.shape
# 计算 Q、K、V
Q = self.W_q(x) # (B, L, d_model)
K = self.W_k(x)
V = self.W_v(x)
# 计算注意力分数并缩放
scale = math.sqrt(self.d_model)
scores = torch.bmm(Q, K.transpose(1, 2)) / scale # (B, L, L)
# 应用掩码(Decoder 中防止看到未来位置)
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
# Softmax 归一化
attn_weights = F.softmax(scores, dim=-1) # (B, L, L)
# 加权求和
output = torch.bmm(attn_weights, V) # (B, L, d_model)
output = self.W_o(output)
return output, attn_weights
class MultiHeadAttention(nn.Module):
"""多头注意力实现"""
def __init__(self, d_model: int, num_heads: int):
super().__init__()
assert d_model % num_heads == 0
self.d_model = d_model
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.W_o = nn.Linear(d_model, d_model)
def split_heads(self, x):
"""将最后一维拆分成 (num_heads, d_k)"""
B, L, _ = x.shape
x = x.view(B, L, self.num_heads, self.d_k)
return x.transpose(1, 2) # (B, num_heads, L, d_k)
def forward(self, x, mask=None):
B, L, _ = x.shape
# 线性投影
Q = self.split_heads(self.W_q(x)) # (B, h, L, d_k)
K = self.split_heads(self.W_k(x))
V = self.split_heads(self.W_v(x))
# 缩放点积注意力
scale = math.sqrt(self.d_k)
scores = torch.matmul(Q, K.transpose(-2, -1)) / scale # (B, h, L, L)
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
attn_weights = F.softmax(scores, dim=-1)
# 加权聚合
output = torch.matmul(attn_weights, V) # (B, h, L, d_k)
# 拼接所有头
output = output.transpose(1, 2).contiguous().view(B, L, self.d_model)
output = self.W_o(output)
return output
# ========== 测试 ==========
if __name__ == "__main__":
batch_size, seq_len, d_model = 2, 10, 512
num_heads = 8
x = torch.randn(batch_size, seq_len, d_model)
# 单头测试
single_attn = SelfAttention(d_model)
out, weights = single_attn(x)
print(f"单头注意力输出: {out.shape}") # (2, 10, 512)
print(f"注意力权重矩阵: {weights.shape}") # (2, 10, 10)
# 多头测试
multi_attn = MultiHeadAttention(d_model, num_heads)
out = multi_attn(x)
print(f"多头注意力输出: {out.shape}") # (2, 10, 512)
# 创建因果掩码(用于 Decoder)
causal_mask = torch.tril(torch.ones(seq_len, seq_len)).unsqueeze(0).unsqueeze(0)
out_masked, _ = single_attn(x, mask=causal_mask)
print(f"带掩码的输出: {out_masked.shape}") # (2, 10, 512)
11. 总结与面试要点
核心知识点速查
| 问题 | 答案 |
|---|---|
| 为什么要除以 d k \sqrt{d_k} dk ? | 防止点积过大,导致 softmax 进入饱和区,梯度近似为 0 |
| Transformer 复杂度是多少? | 时间和空间复杂度均为 O ( n 2 ⋅ d ) O(n^2 \cdot d) O(n2⋅d), n n n 为序列长度 |
| BERT 和 GPT 的本质区别? | BERT 用 Encoder + 双向注意力,适合理解;GPT 用 Decoder + 单向注意力,适合生成 |
| LayerNorm 的作用? | 稳定训练过程,加速收敛,防止梯度爆炸/消失 |
| 位置编码为什么用 sin/cos? | 可以用线性变换表示相对位置,且对任意长度序列有定义 |
学习路线建议
- 精读原论文《Attention is All You Need》
- 手写实现 Self-Attention(本文代码)
- 用 HuggingFace 跑一个 BERT/GPT-2 的 Fine-tuning
- 阅读 LLaMA 代码,理解 RoPE、RMSNorm 等现代改进