背景:为什么需要Transformer
1.1 RNN的局限性
在Transformer出现之前,序列模型(如机器翻译、文本生成)主要使用RNN(循环神经网络)和它的变体LSTM、GRU。这些模型有一个根本问题:难以处理长序列。
RNN的工作方式类似人读文章:逐词阅读,同时记住之前的内容。问题在于,当序列很长时,早期的信息会被后续的信息"稀释",形成所谓的梯度消失问题。举个具体例子:
# RNN处理序列的问题示例
# 假设我们有一个很长的句子:
# "小明来自北京,他在那儿出生,在那儿上学,后来去了上海工作。"
# 对于RNN来说:
# 当模型读到最后的"上海"时,要理解"那儿"指的是什么,
# 需要回顾整个句子。但RNN记住的信息会随着序列增长而衰减
# 问题示意图:
# 输入: 小明 | 来自 | 北京 | , | 他 | 在 | 那儿 | 出生 | ...
# ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
# RNN: [h0]→[h1]→[h2]→[h3]→[h4]→[h5]→[h6]→[h7]→...
# ↑ ↑
# 早期信息被压缩 要理解这个词
# 成单一向量h4 需要回顾h0-h3
# 当序列长度超过100个token时,RNN几乎无法有效关联开头和结尾的内容1.2 注意力机制的革命性突破
2017年,Google的论文《Attention Is All You Need》提出了Transformer架构,彻底改变了这个局面。核心创新是自注意力机制(Self-Attention):不再逐词处理,而是让每个词同时"看到"整个序列,根据相关性动态分配权重。
这就像你读一篇文章时,不是逐字记忆,而是先快速扫一遍找出哪些词和你的问题最相关,然后重点关注那些词。这就是"注意力"的含义。
核心:自注意力机制(Self-Attention)
2.1 注意力机制的核心公式
自注意力的核心是计算"查询-键-值"三者的关系。让我用通俗的方式解释:
- Query(查询):当前位置的内容,比如"那儿"这个词想知道"那儿"和之前哪些词最相关
- Key(键):每个词的特征,用于匹配Query。比如"北京"这个词的Key代表它的语义
- Value(值):每个词的实际内容,当Key匹配成功时,用Value来提供信息
# ============================================
# 自注意力机制(Self-Attention)详解
# ============================================
# 假设我们有3个词:"北京", "是", "首都"
# 每个词被编码为4维向量(实际应用中通常是512或768维)
# 为了简化,我们用2维向量表示词嵌入
import numpy as np
# 词嵌入:每个词被表示为一个向量
word_embeddings = {
"北京": np.array([4.0, 2.0]), # 高相关度概念
"是": np.array([1.0, 0.5]), # 功能词
"首都": np.array([3.8, 1.8]) # 和"北京"语义接近
}
print("词嵌入向量:")
for word, vec in word_embeddings.items():
print(f" {word}: {vec}")# ============================================
# Step 1: 定义Q, K, V权重矩阵
# ============================================
# 我们将词嵌入通过权重矩阵W转换为Q, K, V
# Wq, Wk, Wv 是可学习的参数(随机初始化,通过训练学习)
# 为了计算简单,我们手动设置这些权重矩阵
# 实际训练中,这些权重会通过反向传播自动学习
# 设Wq = Wk = Wv(简化模型,实际中通常不同)
W = np.array([
[1.0, 0.5],
[0.3, 0.7]
])
def compute_qkv(word_embedding, W):
"""将词嵌入转换为Query, Key, Value"""
return word_embedding @ W # 矩阵乘法
# 计算每个词的Q, K, V
qkv = {}
for word, embedding in word_embeddings.items():
qkv[word] = {
'q': compute_qkv(embedding, W),
'k': compute_qkv(embedding, W),
'v': compute_qkv(embedding, W)
}
print("每个词的Q, K, V向量:")
for word, data in qkv.items():
print(f"{word}:")
print(f" Q = {data['q']}")# ============================================
# Step 2: 计算注意力分数(Attention Scores)
# ============================================
# 注意力分数 = Query · Key^T / sqrt(d_k)
# d_k 是Key向量的维度(用于缩放,防止点积过大)
def softmax(x):
"""Softmax函数:将分数转换为概率分布"""
exp_x = np.exp(x - np.max(x)) # 减去最大值防止数值溢出
return exp_x / np.sum(exp_x)
def compute_attention(query_word, key_words, qkv):
"""
计算某个词对所有词的注意力权重
参数:
query_word: 当前要计算的词
key_words: 所有参与竞争的词
qkv: 每个词的QKV向量字典
返回:
注意力权重向量
"""
d_k = len(qkv[key_words[0]]['k']) # Key向量维度
sqrt_dk = np.sqrt(d_k)
# 1. 计算Query与每个Key的点积
query = qkv[query_word]['q']
scores = {}
for word in key_words:
key = qkv[word]['k']
score = np.dot(query, key) / sqrt_dk # 点积 / sqrt(d_k)
scores[word] = score
# 2. Softmax归一化
score_values = list(scores.values())
attention_weights = softmax(np.array(score_values))
return dict(zip(key_words, attention_weights))
# 计算"首都"这个词对所有词的注意力
print("计算:'首都' 对 ['北京', '是', '首都'] 的注意力")
weights = compute_attention("首都", ["北京", "是", "首都"], qkv)
print("\n注意力权重:")
for word, weight in weights.items():
print(f" {word}: {weight:.4f}")
print(f" 解释:当模型处理'首都'时,{weight*100:.1f}%的注意力放在'{word}'上")# ============================================
# Step 3: 计算最终输出
# ============================================
def compute_output(query_word, key_words, qkv):
"""
计算自注意力的最终输出
output = sum(attention_weight_i * value_i)
"""
# 先计算注意力权重
attention_weights = compute_attention(query_word, key_words, qkv)
# 加权求和Value
output = np.zeros_like(qkv[query_word]['q'])
for word, weight in attention_weights.items():
output += weight * qkv[word]['v']
return output, attention_weights
# 计算"首都"的最终输出
output, weights = compute_output("首都", ["北京", "是", "首都"], qkv)
print("=" * 50)
print("自注意力完整计算结果:")
print("=" * 50)
print(f"\n原始词嵌入:'首都' = {word_embeddings['首都']}")
print(f"输出向量: '首都' = {output}")
print(f"\n注意力权重解释:")
print(f" '首都' ← '北京': {weights['北京']:.4f} (相关性:非常高)")
print(f" '首都' ← '是': {weights['是']:.4f} (相关性:低)")
print(f" '首都' ← '首都': {weights['首都']:.4f} (相关性:高,自我关注)")
print("\n" + "=" * 50)
print("物理意义:")
print("=" * 50)
print("""
'首都'通过自注意力机制,从整个序列中获取信息:
- 从'北京'获得了大量信息(因为语义相近,在向量空间中接近)
- 从'是'获得的信息较少(功能词,主要起语法作用)
- 从'首都'本身也获得了信息(自我关联)
这就是注意力机制的强大之处:它不是简单平均,
而是通过学习到的权重,动态决定每个词应该关注什么。
""")2.2 用矩阵运算实现高效注意力
上面的演示为了易懂用的是循环计算,实际工程中用的是矩阵批量运算,速度快几十倍:
# ============================================
# 矩阵形式的自注意力(实际工程用法)
# ============================================
import numpy as np
def softmax(x, axis=-1):
"""Numpy实现的Softmax"""
exp_x = np.exp(x - np.max(x, axis=axis, keepdims=True))
return exp_x / np.sum(exp_x, axis=axis, keepdims=True)
def self_attention(X, Wq, Wk, Wv, dk):
"""
完整的自注意力计算(矩阵形式)
参数:
X: 输入序列的词嵌入矩阵,shape = (seq_len, embed_dim)
Wq, Wk, Wv: QKV权重矩阵
dk: Key向量维度(用于缩放)
返回:
output: 自注意力输出
attention_weights: 注意力权重矩阵
"""
seq_len, embed_dim = X.shape
# 1. 计算Q, K, V矩阵
# Q = X @ Wq: (seq_len, embed_dim) @ (embed_dim, dk) → (seq_len, dk)
Q = X @ Wq
K = X @ Wk
V = X @ Wv
# 2. 计算注意力分数
# scores = Q @ K^T: (seq_len, dk) @ (dk, seq_len) → (seq_len, seq_len)
# 除以sqrt(dk)防止点积过大导致梯度消失
scores = Q @ K.T / np.sqrt(dk)
# 3. Softmax归一化
attention_weights = softmax(scores, axis=-1)
# 4. 加权求和Value
# output = attention_weights @ V: (seq_len, seq_len) @ (seq_len, dk) → (seq_len, dk)
output = attention_weights @ V
return output, attention_weights
# ============================================
# 示例:处理一个3个词的序列
# ============================================
# 输入序列的词嵌入矩阵:3个词,每个词4维向量
X = np.array([
[4.0, 2.0, 1.0, 0.5], # 词1:"北京"
[1.0, 0.5, 2.0, 1.0], # 词2:"是"
[3.8, 1.8, 1.2, 0.4], # 词3:"首都"
])
# 假设词嵌入维度是4,我们想让QKV维度为2
embed_dim = 4
dk = 2
# 随机初始化权重矩阵(实际训练中通过反向传播学习)
np.random.seed(42)
Wq = np.random.randn(embed_dim, dk) * 0.1
Wk = np.random.randn(embed_dim, dk) * 0.1
Wv = np.random.randn(embed_dim, dk) * 0.1
# 计算自注意力
output, attention_weights = self_attention(X, Wq, Wk, Wv, dk)
print("输入矩阵 X (3词 × 4维):")
print(X)
print("\n注意力权重矩阵 (3×3):")
print(f"行=查询词,列=被关注词")
print(attention_weights)
print("\n每行的Softmax概率(横着相加=1):")
print(attention_weights.sum(axis=1, keepdims=True))
print("\n输出矩阵 O (3词 × 2维):")
print(output)多头注意力(Multi-Head Attention)
3.1 为什么需要多头?
单个注意力头只能捕捉一种类型的关系。实际上,词与词之间可能有多种相关性:
- "猫"和"猫粮"语义相关
- "猫"和"捉"语法相关(主语-动词)
- "猫"和"老鼠"情感相关(天敌关系)
多头注意力就是让模型同时学习多种不同的注意力模式。每个"头"学会关注不同的关系类型。
# ============================================
# 多头注意力机制(Multi-Head Attention)
# ============================================
class MultiHeadAttention:
"""
多头注意力实现
参数:
embed_dim: 词嵌入维度(如512)
num_heads: 注意力头数量(如8)
"""
def __init__(self, embed_dim, num_heads):
assert embed_dim % num_heads == 0, "embed_dim必须能被num_heads整除"
self.embed_dim = embed_dim
self.num_heads = num_heads
self.head_dim = embed_dim // num_heads # 每个头的维度
# 可学习参数:每个头有自己的Wq, Wk, Wv
# 为了效率,我们用一个大矩阵代替多个小矩阵
self.Wq = np.random.randn(embed_dim, embed_dim) * 0.02
self.Wk = np.random.randn(embed_dim, embed_dim) * 0.02
self.Wv = np.random.randn(embed_dim, embed_dim) * 0.02
# 输出权重矩阵
self.Wo = np.random.randn(embed_dim, embed_dim) * 0.02
def split_heads(self, X, batch_size):
"""
将嵌入维度分成多个头
输入: (batch_size, seq_len, embed_dim)
输出: (batch_size, num_heads, seq_len, head_dim)
"""
X = X.reshape(batch_size, -1, self.num_heads, self.head_dim)
return X.transpose(0, 2, 1, 3) # 交换轴:把头放到第2维
def forward(self, X):
"""
前向传播
X: (batch_size, seq_len, embed_dim)
"""
batch_size, seq_len, _ = X.shape
# 1. 计算Q, K, V
Q = X @ self.Wq # (batch, seq_len, embed_dim)
K = X @ self.Wk
V = X @ self.Wv
# 2. 分成多个头
Q = self.split_heads(Q, batch_size) # (batch, heads, seq_len, head_dim)
K = self.split_heads(K, batch_size)
V = self.split_heads(V, batch_size)
# 3. 计算注意力(每个头独立计算)
# 为了简化,省略详细的注意力计算
# 实际中,这里是 Q @ K^T / sqrt(head_dim) @ softmax @ V
# 合并多头输出
# 先把头维度和seq_len交换回来
attn_output = Q.transpose(0, 2, 1, 3).reshape(batch_size, seq_len, self.embed_dim)
# 4. 最终线性变换
output = attn_output @ self.Wo
return output
# ============================================
# 多头注意力的物理意义
# ============================================
print("=" * 60)
print("多头注意力的直观理解(以8头为例)")
print("=" * 60)
print("""
假设处理句子:"猫坐在垫子上睡觉"
Head 1 (语义关系):
猫 ←垫子: 0.82 (猫和垫子的物理关系)
猫 ←睡觉: 0.75 (猫的行为)
Head 2 (语法关系):
猫 ←坐: 0.91 (主语-动词)
垫子←在: 0.88 (介词短语)
Head 3 (共指关系):
猫 ←猫: 1.0 (自我指代)
它 ←猫: 0.79 (代词消解)
... (其他头省略)
最终输出:将所有头的输出拼接后通过线性变换融合
每个头学习到不同的"视角",就像多个专家从不同角度分析文本,
最后汇总成一个更全面、更准确的表示。
""")
print("=" * 60)
print("多头 vs 单头 的效果对比")
print("=" * 60)
print("""
单头注意力只能学到一种固定的关系模式。
比如只能学到"语义相似",但无法同时捕捉语法结构。
多头注意力的优势:
1. 每个头可以专注学习不同类型的关系
2. 头的数量是可调的超参数
3. 头的维度通常64-128,在计算量和效果间平衡
4. 可以可视化每个头学到了什么(可解释性强)
典型配置:
- BERT Base: 12头,768维隐藏层
- GPT-3: 96头,12288维隐藏层(稀疏注意力变体)
- LLaMA: 32头,4096维隐藏层
""")完整Transformer架构
4.1 Transformer编码器-解码器结构
原始Transformer是用于机器翻译的,由编码器(Encoder)和解码器(Decoder)组成:
- 编码器(左侧):处理源语言句子,提取特征
- 解码器(右侧):根据编码器输出和已生成的内容,生成目标语言
# ============================================
# 简化版Transformer架构(仅编码器部分)
# ============================================
class TransformerBlock:
"""
Transformer的一个Block,包含:
1. Multi-Head Self-Attention(多头自注意力)
2. Add & Layer Norm(残差连接+层归一化)
3. Feed Forward Network(前馈神经网络)
4. Add & Layer Norm
"""
def __init__(self, embed_dim=512, num_heads=8, ff_dim=2048, dropout=0.1):
"""
参数:
embed_dim: 词嵌入维度
num_heads: 注意力头数
ff_dim: 前馈网络隐藏层维度(通常是embed_dim的4倍)
dropout: Dropout比例
"""
self.attention = MultiHeadAttention(embed_dim, num_heads)
# Layer Normalization参数(可学习)
self.norm1 = LayerNorm(embed_dim)
self.norm2 = LayerNorm(embed_dim)
# Feed Forward Network
self.ff = FeedForwardNetwork(embed_dim, ff_dim)
self.dropout = dropout
def forward(self, x):
"""
前向传播
x: (batch_size, seq_len, embed_dim)
"""
# 1. Multi-Head Self-Attention + 残差连接
# 自注意力层
attn_output = self.attention.forward(x)
# 残差连接:x + attention(x)
x = self.norm1(x + attn_output)
# 2. Feed Forward + 残差连接
ff_output = self.ff.forward(x)
x = self.norm2(x + ff_output)
return x
class FeedForwardNetwork:
"""
前馈神经网络(Position-wise FFN)
每个位置独立应用相同的双层全连接网络
结构:Linear → ReLU → Dropout → Linear
"""
def __init__(self, embed_dim, ff_dim):
self.layer1 = np.random.randn(embed_dim, ff_dim) * 0.02
self.layer2 = np.random.randn(ff_dim, embed_dim) * 0.02
self.ff_dim = ff_dim
def forward(self, x):
"""
x: (batch_size, seq_len, embed_dim)
"""
# 第一层:升维
x = x @ self.layer1
x = np.maximum(0, x) # ReLU激活
# 第二层:降维回原始维度
x = x @ self.layer2
return x
class LayerNorm:
"""
Layer Normalization(层归一化)
公式:y = (x - mean) / sqrt(var + epsilon) * gamma + beta
"""
def __init__(self, embed_dim, epsilon=1e-6):
self.gamma = np.ones(embed_dim) # 可学习缩放参数
self.beta = np.zeros(embed_dim) # 可学习偏移参数
self.epsilon = epsilon
def forward(self, x):
"""
x: (batch_size, seq_len, embed_dim)
"""
mean = np.mean(x, axis=-1, keepdims=True)
var = np.var(x, axis=-1, keepdims=True)
x_norm = (x - mean) / np.sqrt(var + self.epsilon)
return self.gamma * x_norm + self.beta4.2 完整的Transformer Encoder堆叠
# ============================================
# 完整Transformer编码器(堆叠多个Block)
# ============================================
class TransformerEncoder:
"""
完整的Transformer编码器
包含:
- 词嵌入层(Word Embedding)
- 位置编码(Positional Encoding)
- N个Transformer Block
"""
def __init__(self,
vocab_size=30000, # 词表大小
embed_dim=512, # 嵌入维度
num_heads=8, # 注意力头数
num_layers=6, # Transformer Block数量
ff_dim=2048, # 前馈网络维度
max_seq_len=512): # 最大序列长度
self.embed_dim = embed_dim
self.num_layers = num_layers
# 词嵌入层
self.word_embedding = np.random.randn(vocab_size, embed_dim) * 0.02
# 位置编码(后面会详细讲)
self.pos_encoding = self._create_positional_encoding(max_seq_len, embed_dim)
# 堆叠Transformer Blocks
self.blocks = [
TransformerBlock(embed_dim, num_heads, ff_dim)
for _ in range(num_layers)
]
# 最终层归一化
self.final_norm = LayerNorm(embed_dim)
def _create_positional_encoding(self, max_len, d_model):
"""创建位置编码矩阵"""
PE = np.zeros((max_len, d_model))
# 位置编码公式:
# PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
# PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
positions = np.arange(max_len).reshape(-1, 1)
div_term = np.exp(np.arange(0, d_model, 2) * (-np.log(10000.0) / d_model))
PE[:, 0::2] = np.sin(positions * div_term)
PE[:, 1::2] = np.cos(positions * div_term)
return PE
def forward(self, x):
"""
x: 输入token IDs,shape = (batch_size, seq_len)
"""
batch_size, seq_len = x.shape
# 1. 词嵌入
# 将token IDs转为词向量
word_emb = self.word_embedding[x] # (batch, seq_len, embed_dim)
# 2. 加上位置编码
# 位置编码让模型知道每个词在序列中的位置
pos_emb = self.pos_encoding[:seq_len] # (seq_len, embed_dim)
x = word_emb + pos_emb
# 3. 通过N个Transformer Block
for block in self.blocks:
x = block.forward(x)
# 4. 最终归一化
x = self.final_norm.forward(x)
return x
# ============================================
# 使用示例
# ============================================
print("=" * 60)
print("Transformer编码器工作流程")
print("=" * 60)
# 模拟输入:一个batch,序列长度10
batch_size = 2
seq_len = 10
vocab_size = 30000
# 随机生成token IDs(实际应该是词表索引)
token_ids = np.random.randint(0, vocab_size, size=(batch_size, seq_len))
# 创建编码器
encoder = TransformerEncoder(
vocab_size=vocab_size,
embed_dim=512,
num_heads=8,
num_layers=6,
ff_dim=2048
)
# 前向传播
output = encoder.forward(token_ids)
print(f"\n输入: token_ids shape = {token_ids.shape}")
print(f"输出: encoding shape = {output.shape}")
print(f"\n解释:")
print(f" - 输入2个句子,每个句子10个token")
print(f" - 输出2个编码结果,每个是10×512维的向量序列")
print(f" - 每个位置的向量包含了:该词的语义信息 + 在序列中的位置信息")
print(f" - 通过6层Transformer Block,模型学到了深层的上下文关系")位置编码(Positional Encoding)
5.1 为什么需要位置编码?
Attention机制本身不包含位置信息——它把序列当作一个集合,词与词之间的顺序对注意力计算没有影响。"我爱你"和"你爱我"在Attention看来是一样的。
为了让模型知道词的顺序,需要额外加上位置信息,这就是位置编码(Positional Encoding)。
# ============================================
# 位置编码详解
# ============================================
import numpy as np
import matplotlib.pyplot as plt
def create_positional_encoding(max_len, d_model):
"""
创建Transformer的位置编码
使用正弦和余弦函数,让模型能够学习相对位置关系
公式:
PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
其中:
pos = 词在序列中的位置(0, 1, 2, ...)
i = 维度索引(0, 1, 2, ..., d_model/2)
"""
PE = np.zeros((max_len, d_model))
positions = np.arange(max_len).reshape(-1, 1) # (max_len, 1)
# div_term:控制不同频率的衰减
div_term = np.exp(np.arange(0, d_model, 2) * (-np.log(10000.0) / d_model))
# 偶数维度用sin,奇数维度用cos
PE[:, 0::2] = np.sin(positions * div_term)
PE[:, 1::2] = np.cos(positions * div_term)
return PE
# 可视化位置编码
max_len = 100
d_model = 64
PE = create_positional_encoding(max_len, d_model)
print("=" * 60)
print("位置编码可视化")
print("=" * 60)
print(f"\n位置编码矩阵 shape: {PE.shape}")
print(f" - 100个位置 × 64维向量")
print("\n前4个位置的编码(只显示前16维):")
for pos in range(4):
print(f"位置{pos}: {PE[pos, :16].round(3)}")
print("\n" + "=" * 60)
print("为什么用sin/cos函数?")
print("=" * 60)
print("""
1. 【周期性】不同频率的sin/cos可以让模型学习不同范围的相对位置
- 高频(i大):感知短距离位置差异
- 低频(i小):感知长距离位置差异
2. 【线性关系】sin(a+b)和cos(a+b)可以写成sin(a)和cos(a)的线性组合
这让模型能够轻松学习相对位置(比如"后面3个词")
3. 【外推能力】用数学函数生成位置编码,理论上可以处理任意长度的序列
只需要计算更多的sin/cos值
4. 【对称性】对于固定偏移,PE(pos+k)可以表示为PE(pos)的线性变换
""")
print("\n" + "=" * 60)
print("位置编码 vs 位置嵌入( Learned Positional Embedding)")
print("=" * 60)
print("""
位置编码(Transformer原版):
- 使用固定的sin/cos函数生成
- 不需要学习参数
- 可以处理任意长度
位置嵌入(BERT等用这个):
- 每个位置是一个可学习的向量
- 需要预先设定最大长度
- 通常效果略好,但长度受限
结论:短序列(<512)用 Learned PE,长序列用 位置编码
""")GPT的工作原理
6.1 GPT是一个单向的解码器
理解了Transformer之后,理解GPT就简单了。GPT(Generative Pre-trained Transformer)是只有解码器(Decoder)部分的Transformer,且用的是掩码注意力(Masked Attention)——让你只能看到当前词之前的内容,不能偷看未来。
# ============================================
# GPT的核心:掩码注意力(Masked Self-Attention)
# ============================================
def masked_self_attention(Q, K, V, mask=None):
"""
带掩码的自注意力
关键:通过对"不应该看到"的位置加上极大的负数,
让softmax把这些位置的注意力变成0
参数:
Q, K, V: 查询、键、值矩阵
mask: 掩码矩阵,shape = (seq_len, seq_len)
mask[i,j] = 1 表示位置i可以看到位置j
mask[i,j] = 0 表示位置i不能看到位置j
"""
dk = K.shape[-1]
# 计算注意力分数
scores = Q @ K.transpose(-2, -1) / np.sqrt(dk)
# 应用掩码:将不允许看到的位置设为负无穷
if mask is not None:
# mask=0的位置变成负无穷
scores = np.where(mask == 0, -1e9, scores)
# Softmax归一化
attention_weights = softmax(scores, axis=-1)
# 加权求和
output = attention_weights @ V
return output, attention_weights
def create_causal_mask(seq_len):
"""
创建因果掩码(Causal Mask)
这就是GPT"不能看到未来"的关键
生成一个下三角矩阵:
[[1, 0, 0, 0],
[1, 1, 0, 0],
[1, 1, 1, 0],
[1, 1, 1, 1]]
含义:第i行(当前词)可以看到第0到i列(之前的词),
但看不到第i+1列到最后一列(未来的词)
"""
mask = np.tril(np.ones((seq_len, seq_len)))
return mask
# 示例
seq_len = 5
mask = create_causal_mask(seq_len)
print("=" * 60)
print("GPT的掩码注意力(Causal Mask)")
print("=" * 60)
print(f"\n序列长度={seq_len}的因果掩码:")
print(mask.astype(int))
print("""
掩码解读:
- 第1行(位置0):只能看到位置0(自己)
- 第2行(位置1):能看到位置0、1
- 第3行(位置2):能看到位置0、1、2
- 第4行(位置3):能看到位置0、1、2、3
- 第5行(位置4):能看到位置0、1、2、3、4(所有之前的词)
这就是为什么GPT只能"向前生成"——
训练时让模型预测下一个词,模型只能看到之前的词;
生成时也是逐词生成,每个新词都只能基于之前的所有词。
""")
print("=" * 60)
print("GPT训练 vs 生成的对比")
print("=" * 60)
print("""
【训练阶段】( teacher forcing,并行计算):
句子:"今天 是 好 日子"
输入: 今天 是 好 日子
输出: 是 好 日子 (目标:预测下一个词)
注意力掩码:
- 计算"是"的输出时,只能看"今天"
- 计算"好"的输出时,只能看"今天 是"
- 计算"日子"的输出时,只能看"今天 是 好"
- 可以一次性计算所有位置(并行)
【生成阶段】(自回归,串行):
要预测"日子",需要:
1. 先预测"是" → 条件:只看"今天"
2. 再预测"好" → 条件:看"今天 是"
3. 最后预测"日子" → 条件:看"今天 是 好"
生成"日子"时必须先生成前面的词,这就是为什么GPT生成是串行的。
""") 6.2 GPT的语言建模目标
GPT的训练目标非常简单:根据前面的词预测下一个词。这叫做"因果语言建模(Causal Language Modeling)"。
# ============================================
# GPT训练目标:下一个词预测
# ============================================
print("=" * 60)
print("GPT的语言建模目标")
print("=" * 60)
print("""
【训练数据示例】
句子:"小明 去 北京旅游"
训练时构建的输入-输出对:
位置0: 输入="", 输出="小明" (预测第一个词)
位置1: 输入=" 小明", 输出="去" (预测第二个词)
位置2: 输入=" 小明 去", 输出="北京" (预测第三个词)
位置3: 输入=" 小明 去 北京", 输出="旅游"(预测第四个词)
位置4: 输入=" 小明 去 北京 旅游", 输出="" (预测结束)
【损失函数】
对每个位置,计算预测词的概率分布与真实词的交叉熵:
Loss = -Σ log(P(真实词_i | 之前所有词))
模型通过反向传播学习:调整参数,使得预测下一个词的概率越来越高。
【为什么这个目标能学到通用能力】
"预测下一个词"这个任务需要:
1. 理解语法:知道什么样的词序列是合理的
2. 理解语义:知道词与词之间的含义关系
3. 理解常识:知道世界如何运作
4. 理解上下文:知道当前话题是什么
为了准确预测,模型必须学会语言的一切知识。
这就是为什么GPT能"无监督地"学习到通用智能。
""")
print("=" * 60)
print("GPT vs BERT的区别")
print("=" * 60)
print("""
【GPT】(Decoder-only,生成式)
- 单向注意力(只看前面的词)
- 训练目标:预测下一个词
- 擅长:文本生成、对话、创意写作
- 模型:GPT-2, GPT-3, GPT-4, LLaMA, ChatGPT
【BERT】(Encoder-only,判别式)
- 双向注意力(能看到整个序列)
- 训练目标:完形填空(masked language modeling)
- 擅长:文本分类、实体识别、问答
- 模型:BERT, RoBERTa, ELECTRA
【T5】(Encoder-Decoder)
- 编码器看全部,解码器看前面
- 训练目标:输入转成输出(如翻译、摘要)
- 通用:可以做各种任务
- 模型:T5, FLAN-T5, BART
当前趋势:大语言模型(LLM)基本都用GPT-style(Decoder-only),
因为 scaling law(规模法则)效果最好。
""") 代码实现:从零理解Transformer
7.1 完整可运行的最小Transformer(PyTorch风格伪代码)
# ============================================
# 完整Transformer实现(PyTorch风格伪代码)
# ============================================
import numpy as np
class LayerNorm:
"""Layer Normalization"""
def __init__(self, d_model, eps=1e-6):
self.gamma = np.ones(d_model)
self.beta = np.zeros(d_model)
self.eps = eps
def forward(self, x):
mean = x.mean(axis=-1, keepdims=True)
std = x.std(axis=-1, keepdims=True)
return self.gamma * (x - mean) / (std + self.eps) + self.beta
class MultiHeadAttention:
"""多头注意力"""
def __init__(self, d_model, num_heads):
self.num_heads = num_heads
self.head_dim = d_model // num_heads
self.Wq = np.random.randn(d_model, d_model) * 0.02
self.Wk = np.random.randn(d_model, d_model) * 0.02
self.Wv = np.random.randn(d_model, d_model) * 0.02
self.Wo = np.random.randn(d_model, d_model) * 0.02
def forward(self, Q, K, V, mask=None):
batch_size = Q.shape[0]
seq_len = Q.shape[1]
# 线性变换
Q = Q @ self.Wq
K = K @ self.Wk
V = V @ self.Wv
# 分成多个头 (batch, heads, seq, head_dim)
Q = Q.reshape(batch_size, seq_len, self.num_heads, self.head_dim).transpose(0, 2, 1, 3)
K = K.reshape(batch_size, seq_len, self.num_heads, self.head_dim).transpose(0, 2, 1, 3)
V = V.reshape(batch_size, seq_len, self.num_heads, self.head_dim).transpose(0, 2, 1, 3)
# 计算注意力分数
scores = Q @ K.transpose(0, 1, 3, 2) / np.sqrt(self.head_dim)
# 应用掩码
if mask is not None:
scores = np.where(mask == 0, -1e9, scores)
# Softmax
attention_weights = np.exp(scores) / np.exp(scores).sum(axis=-1, keepdims=True)
# 加权求和
output = attention_weights @ V
# 合并多头
output = output.transpose(0, 2, 1, 3).reshape(batch_size, seq_len, -1)
return output @ self.Wo
class FeedForward:
"""前馈网络"""
def __init__(self, d_model, d_ff):
self.linear1 = np.random.randn(d_model, d_ff) * 0.02
self.linear2 = np.random.randn(d_ff, d_model) * 0.02
def forward(self, x):
return (np.maximum(0, x @ self.linear1) @ self.linear2)
class TransformerBlock:
"""单个Transformer Block"""
def __init__(self, d_model, num_heads, d_ff):
self.attention = MultiHeadAttention(d_model, num_heads)
self.norm1 = LayerNorm(d_model)
self.ff = FeedForward(d_model, d_ff)
self.norm2 = LayerNorm(d_model)
def forward(self, x, mask=None):
# Self-Attention + 残差
attn_out = self.attention.forward(x, x, x, mask)
x = self.norm1.forward(x + attn_out)
# FFN + 残差
ff_out = self.ff.forward(x)
x = self.norm2.forward(x + ff_out)
return x
class GPTModel:
"""完整的GPT模型(简化版)"""
def __init__(self, vocab_size, d_model=512, num_heads=8, num_layers=6, d_ff=2048):
self.vocab_size = vocab_size
self.d_model = d_model
# 词嵌入
self.word_embedding = np.random.randn(vocab_size, d_model) * 0.02
# 位置编码
self.pos_embedding = np.random.randn(512, d_model) * 0.02
# Transformer Blocks
self.blocks = [TransformerBlock(d_model, num_heads, d_ff) for _ in range(num_layers)]
# 输出层
self.lm_head = np.random.randn(d_model, vocab_size) * 0.02
def forward(self, input_ids, targets=None):
"""
前向传播
input_ids: (batch_size, seq_len)
"""
seq_len = input_ids.shape[1]
# 词嵌入 + 位置嵌入
x = self.word_embedding[input_ids] + self.pos_embedding[:seq_len]
# 因果掩码
mask = np.tril(np.ones((seq_len, seq_len)))
# 通过Transformer Blocks
for block in self.blocks:
x = block.forward(x, mask)
# 投影到词表大小
logits = x @ self.lm_head # (batch, seq_len, vocab_size)
# 如果有目标,计算损失
loss = None
if targets is not None:
# 交叉熵损失
loss = self.compute_cross_entropy(logits, targets)
return {"logits": logits, "loss": loss}
def compute_cross_entropy(self, logits, targets):
"""计算交叉熵损失"""
batch_size, seq_len, vocab_size = logits.shape
# Reshape for cross entropy
logits_flat = logits.reshape(-1, vocab_size)
targets_flat = targets.reshape(-1)
# Log softmax
log_probs = logits_flat - np.max(logits_flat, axis=1, keepdims=True)
log_probs = log_probs - np.log(np.exp(log_probs).sum(axis=1, keepdims=True))
# Negative log likelihood
nll = -log_probs[np.arange(len(targets_flat)), targets_flat]
return nll.mean()
# ============================================
# 使用示例
# ============================================
print("=" * 60)
print("GPT模型完整前向传播示例")
print("=" * 60)
# 创建模型
model = GPTModel(
vocab_size=30000,
d_model=512,
num_heads=8,
num_layers=6
)
# 模拟输入
batch_size = 2
seq_len = 10
input_ids = np.random.randint(0, 30000, size=(batch_size, seq_len))
# 前向传播(训练模式)
output = model.forward(input_ids, targets=input_ids) # 语言模型预测自己
print(f"\n输入: input_ids shape = {input_ids.shape}")
print(f"输出: logits shape = {output['logits'].shape}")
print(f"损失: {output['loss']:.4f}")
print("""
训练循环:
1. 输入一个句子,预测下一个词
2. 计算预测和真实下一个词的交叉熵损失
3. 反向传播,更新所有参数
4. 重复,直到损失足够低
这就是GPT能够"学会说话"的核心机制!
""")总结
本文从数学公式到代码实现,详细讲解了Transformer架构的核心组件:
- 自注意力机制(Self-Attention):通过Query-Key-Value计算词与词之间的相关性
- 多头注意力(Multi-Head):同时学习多种关系模式
- 位置编码(Positional Encoding):用sin/cos函数注入位置信息
- 残差连接+层归一化:训练深层网络的关键技术
- 前馈神经网络(FFN):每个位置独立做非线性变换
- GPT的单向掩码:让模型只能看前面的词,实现生成式训练
理解这些核心概念,是深入学习大语言模型(LLM)、掌握提示工程、优化模型使用的基础。