← 返回投肯智能知识库首页

Transformer架构详解:从注意力机制到GPT原理

作者:重庆投肯小刚更新日期:2026年5月

目录

  1. 背景:为什么需要Transformer
  2. 核心:自注意力机制(Self-Attention)
  3. 多头注意力(Multi-Head Attention)
  4. 完整Transformer架构
  5. 位置编码(Positional Encoding)
  6. GPT的工作原理
  7. 代码实现:从零理解Transformer

背景:为什么需要Transformer

1.1 RNN的局限性

在Transformer出现之前,序列模型(如机器翻译、文本生成)主要使用RNN(循环神经网络)和它的变体LSTM、GRU。这些模型有一个根本问题:难以处理长序列

RNN的工作方式类似人读文章:逐词阅读,同时记住之前的内容。问题在于,当序列很长时,早期的信息会被后续的信息"稀释",形成所谓的梯度消失问题。举个具体例子:

python
# 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):不再逐词处理,而是让每个词同时"看到"整个序列,根据相关性动态分配权重。

这就像你读一篇文章时,不是逐字记忆,而是先快速扫一遍找出哪些词和你的问题最相关,然后重点关注那些词。这就是"注意力"的含义。

核心思想:Transformer不再受序列长度的限制,因为注意力机制允许任意两个位置之间直接交互,不再需要依次传递。

核心:自注意力机制(Self-Attention)

2.1 注意力机制的核心公式

自注意力的核心是计算"查询-键-值"三者的关系。让我用通俗的方式解释:

python
# ============================================
# 自注意力机制(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}")
python
# ============================================
# 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']}")
python
# ============================================
# 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}'上")
python
# ============================================
# 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 用矩阵运算实现高效注意力

上面的演示为了易懂用的是循环计算,实际工程中用的是矩阵批量运算,速度快几十倍:

python
# ============================================
# 矩阵形式的自注意力(实际工程用法)
# ============================================
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 为什么需要多头?

单个注意力头只能捕捉一种类型的关系。实际上,词与词之间可能有多种相关性:

多头注意力就是让模型同时学习多种不同的注意力模式。每个"头"学会关注不同的关系类型。

python
# ============================================
# 多头注意力机制(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)组成:

python
# ============================================
# 简化版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.beta

4.2 完整的Transformer Encoder堆叠

python
# ============================================
# 完整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)

python
# ============================================
# 位置编码详解
# ============================================
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)——让你只能看到当前词之前的内容,不能偷看未来。

python
# ============================================
# 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)"。

python
# ============================================
# 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风格伪代码)

python
# ============================================
# 完整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架构的核心组件:

  1. 自注意力机制(Self-Attention):通过Query-Key-Value计算词与词之间的相关性
  2. 多头注意力(Multi-Head):同时学习多种关系模式
  3. 位置编码(Positional Encoding):用sin/cos函数注入位置信息
  4. 残差连接+层归一化:训练深层网络的关键技术
  5. 前馈神经网络(FFN):每个位置独立做非线性变换
  6. GPT的单向掩码:让模型只能看前面的词,实现生成式训练

理解这些核心概念,是深入学习大语言模型(LLM)、掌握提示工程、优化模型使用的基础。

相关推荐