现在位置: 首页 > AI 入门教程 > 正文

Transformer 深度解析

2017 年,Google 发表论文《Attention Is All You Need》,提出了 Transformer 架构。谁也没想到,这篇论文会彻底改变 AI 领域。

今天的 GPT、Claude、Gemini、Llama……几乎所有主流大语言模型,本质上都是 Transformer 的变体。

理解了 Transformer,就理解了现代 LLM 的 90%。

这篇文章我们会深入 Transformer 的每一个组件:自注意力、多头注意力、位置编码、前馈网络、层归一化……不仅讲"是什么",更讲"为什么"。

这是技术深度最高的模块之一。我们会用代码演示核心计算,确保你不仅看懂公式,还能亲手实现。


Transformer 前传:RNN 的局限

在 Transformer 出现之前,序列任务(如翻译、文本生成)主要用 RNN(循环神经网络)和它的变体 LSTM、GRU。

RNN 是怎么工作的

RNN 的核心思想是"逐个处理":输入序列一个词一个词地进入网络,每一步的输出包含之前所有词的信息。

实例

# ============================================
# 简化版 RNN 前向传播演示
# ============================================

import math


class SimpleRNN:
    """简化的 RNN 实现,用于演示原理"""

    def __init__(self, input_size: int, hidden_size: int):
        """初始化 RNN 参数"""
        import random
        random.seed(42)  # 设置随机种子保证可复现

        # 输入到隐藏层的权重
        self.Wx = [[random.uniform(-0.1, 0.1) for _ in range(hidden_size)]
                   for _ in range(input_size)]
        # 隐藏层到隐藏层的权重
        self.Wh = [[random.uniform(-0.1, 0.1) for _ in range(hidden_size)]
                   for _ in range(hidden_size)]
        # 偏置
        self.b = [0.0 for _ in range(hidden_size)]

    def step(self, x: list, h_prev: list) -> list:
        """单步 RNN:输入 x 和前一时刻隐藏状态 h_prev,输出新的隐藏状态"""
        hidden_size = len(h_prev)
        h_new = [0.0 for _ in range(hidden_size)]

        # h_new = tanh(Wx·x + Wh·h_prev + b)
        for i in range(hidden_size):
            # 计算 Wx·x
            wx_sum = sum(x[j] * self.Wx[j][i] for j in range(len(x)))
            # 计算 Wh·h_prev
            wh_sum = sum(h_prev[j] * self.Wh[j][i] for j in range(hidden_size))
            # 加偏置,过 tanh
            h_new[i] = math.tanh(wx_sum + wh_sum + self.b[i])

        return h_new

    def forward(self, sequence: list) -> list:
        """处理完整序列"""
        hidden_size = len(self.b)
        h = [0.0 for _ in range(hidden_size)]  # 初始隐藏状态
        hidden_states = []

        for x in sequence:
            h = self.step(x, h)
            hidden_states.append(h)

        return hidden_states


# 测试:用简单的向量序列演示
input_size = 4
hidden_size = 3

rnn = SimpleRNN(input_size, hidden_size)

# 假设输入序列是 4 个词,每个词用 4 维向量表示
sequence = [
    [1.0, 0.0, 0.0, 0.0],  # 词 1
    [0.0, 1.0, 0.0, 0.0],  # 词 2
    [0.0, 0.0, 1.0, 0.0],  # 词 3
    [0.0, 0.0, 0.0, 1.0],  # 词 4
]

hidden_states = rnn.forward(sequence)

print("Runoob 简单 RNN 演示")
print("=" * 40)
for i, h in enumerate(hidden_states):
    print(f"第 {i+1} 步隐藏状态: {[f'{v:.4f}' for v in h]}")
# 输出:
# Runoob 简单 RNN 演示
# ========================================
# 第 1 步隐藏状态: ['0.0204', '-0.0434', '0.0556']
# 第 2 步隐藏状态: ['-0.0061', '-0.0529', '0.0775']
# 第 3 步隐藏状态: ['0.0641', '-0.0409', '0.0655']
# 第 4 步隐藏状态: ['0.0143', '-0.0805', '0.0277']

RNN 看起来合理,但它有三个致命问题:

问题一:梯度消失,难以记忆长距离依赖

RNN 是"链式"结构,每一步的梯度要反向传播回第一步。多次相乘后,梯度会指数级衰减,变成几乎 0。

比如句子"我在 2010 年去了巴黎,……,那是我最喜欢的城市"——"城市"要指代"巴黎",但中间隔了太多词,RNN 很难学到这种长距离依赖。

LSTM 和 GRU 缓解了这个问题,但没有彻底解决。

问题二:无法并行计算,训练慢

RNN 必须等第 t-1 步算完,才能算第 t 步。这意味着:

  • 即使有 100 个 GPU,也只能一个词一个词地算
  • 序列越长,训练时间越长
  • 很难扩展到超大规模数据

问题三:位置靠前的信息容易被"覆盖"

RNN 的隐藏状态是一步步更新的,后面的信息会不断覆盖前面的。句子开头的重要信息,到最后可能已经"稀释"得差不多了。

Transformer 的出现,一次性解决了这三个问题。

特性RNN/LSTMTransformer
计算方式串行,一步步来并行,一次性算完
长距离依赖弱,梯度消失强,任意距离直接连接
位置信息天然有顺序需要位置编码
训练速度快(可并行)

自注意力机制(Self-Attention)

自注意力是 Transformer 的核心。它的思想很简单:每个词都要和句子里的所有词"交流"一下,看看谁对自己重要,然后按重要程度加权求和。

先看整体架构图:

Transformer 整体架构图

Q、K、V 的直觉理解

自注意力用三个向量来描述每个词:

  • Q(Query,查询)——"我在找谁?"——这个词想关注谁
  • K(Key,键)——"我是谁?"——这个词的身份标识
  • V(Value,值)——"我有什么?"——这个词的实际内容

计算过程是:

  1. 每个词用 Q 去和所有词的 K 做"匹配",得到注意力分数
  2. 用 Softmax 把分数归一化,总和为 1
  3. 用归一化后的分数加权求和所有词的 V

看图更清楚:

自注意力 QKV 计算流程图

注意力分数计算公式

完整的公式是:

Attention(Q, K, V) = softmax( Q·Kᵀ / √dₖ ) · V

让我们用纯 Python 一步一步实现它:

实例

# ============================================
# 纯 Python 实现自注意力(Self-Attention)
# ============================================

import math


def softmax(x: list) -> list:
    """计算 Softmax:把一组数变成概率分布,总和为 1"""
    # 减去最大值防止数值溢出
    max_val = max(x)
    exp_x = [math.exp(v - max_val) for v in x]
    sum_exp = sum(exp_x)
    return [v / sum_exp for v in exp_x]


def matrix_multiply(A: list, B: list) -> list:
    """矩阵乘法:A 是 m×n,B 是 n×p,输出 m×p"""
    m = len(A)
    n = len(B)
    p = len(B[0])

    result = [[0.0 for _ in range(p)] for _ in range(m)]
    for i in range(m):
        for j in range(p):
            for k in range(n):
                result[i][j] += A[i][k] * B[k][j]
    return result


def transpose(matrix: list) -> list:
    """矩阵转置"""
    return list(map(list, zip(*matrix)))


class SelfAttention:
    """纯 Python 实现的自注意力层"""

    def __init__(self, d_model: int, d_k: int):
        """
        初始化自注意力层
        d_model: 输入/输出维度
        d_k: Q/K 的维度(通常 d_k = d_model / num_heads)
        """

        import random
        random.seed(42)  # runoob:固定随机种子

        self.d_model = d_model
        self.d_k = d_k

        # 初始化 Q, K, V 的投影矩阵
        # 实际中这些是训练出来的
        self.Wq = [[random.normalvariate(0, 0.1) for _ in range(d_k)]
                   for _ in range(d_model)]
        self.Wk = [[random.normalvariate(0, 0.1) for _ in range(d_k)]
                   for _ in range(d_model)]
        self.Wv = [[random.normalvariate(0, 0.1) for _ in range(d_k)]
                   for _ in range(d_model)]

    def forward(self, X: list) -> list:
        """
        自注意力前向传播
        X: 输入序列,形状 [seq_len, d_model]
        返回: 输出序列,形状 [seq_len, d_k]
        """

        seq_len = len(X)

        # 步骤 1:计算 Q, K, V
        # Q = X·Wq, K = X·Wk, V = X·Wv
        Q = matrix_multiply(X, self.Wq)
        K = matrix_multiply(X, self.Wk)
        V = matrix_multiply(X, self.Wv)

        print("  Q 的形状:", len(Q), "×", len(Q[0]))
        print("  K 的形状:", len(K), "×", len(K[0]))
        print("  V 的形状:", len(V), "×", len(V[0]))

        # 步骤 2:计算注意力分数 Q·Kᵀ
        K_transposed = transpose(K)
        scores = matrix_multiply(Q, K_transposed)

        print("\n  原始注意力分数:")
        for i, row in enumerate(scores):
            print(f"    第 {i} 行: {[f'{v:.4f}' for v in row]}")

        # 步骤 3:缩放:除以 √d_k
        # 这是为了防止 Softmax 进入梯度饱和区
        scale_factor = math.sqrt(self.d_k)
        scores_scaled = [[v / scale_factor for v in row] for row in scores]

        print("\n  缩放后的注意力分数:")
        for i, row in enumerate(scores_scaled):
            print(f"    第 {i} 行: {[f'{v:.4f}' for v in row]}")

        # 步骤 4:Softmax 归一化
        attention_weights = []
        for row in scores_scaled:
            attention_weights.append(softmax(row))

        print("\n  注意力权重(Softmax 后):")
        for i, row in enumerate(attention_weights):
            print(f"    第 {i} 行: {[f'{v:.4f}' for v in row]}")

        # 步骤 5:加权求和 V
        output = matrix_multiply(attention_weights, V)

        return output, attention_weights


# ============================================
# 演示:用一个简单例子运行自注意力
# ============================================

# 假设我们有一个 4 词的句子,每个词用 8 维向量表示
# 为了演示,我们用 one-hot + 小随机噪声
d_model = 8
d_k = 6

# 输入序列:4 个词,每个 8 维
X = [
    [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],  # 词 0
    [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],  # 词 1
    [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],  # 词 2
    [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0],  # 词 3
]

print("=" * 50)
print("Runoob 自注意力演示")
print("=" * 50)
print(f"输入序列长度: {len(X)}")
print(f"输入维度: {len(X[0])}")
print()

attn = SelfAttention(d_model=d_model, d_k=d_k)
output, attention_weights = attn.forward(X)

print("\n" + "=" * 50)
print("自注意力输出:")
print("=" * 50)
for i, row in enumerate(output):
    print(f"词 {i} 的输出: {[f'{v:.4f}' for v in row]}")

运行这段代码,你会看到完整的计算过程:Q/K/V 生成、注意力分数计算、缩放、Softmax、最后加权输出。

为什么要除以 √dₖ?

这是一个重要的细节。假设 Q 和 K 的每个元素都是均值 0、方差 1 的随机变量,那么它们的点积的方差是 dₖ。

如果 dₖ 很大(比如 512),点积的值会很大,导致 Softmax 进入"饱和区"——梯度几乎为 0,训练不动。

除以 √dₖ 后,方差变回 1,Softmax 的梯度就健康了。

缩放点积是 Transformer 训练稳定的关键技巧之一。


多头注意力(Multi-Head Attention)

单头自注意力虽然强大,但有个问题:每个词只能"关注"一次。如果我们想让词同时关注多个不同方面呢?

比如在句子"动物园里,老虎追着兔子跑"中:

  • "老虎"可能需要一个头关注"动物园"(地点)
  • 另一个头关注"兔子"(动作对象)
  • 第三个头关注"追"(动作)

多头注意力就是把 Q/K/V 拆分成多个"头",每个头学不同的注意力模式,最后拼起来。

多头注意力的计算步骤

  1. 把 Q/K/V 按最后一维拆分成 h 份(h 是头数)
  2. 每个头独立做自注意力
  3. 把所有头的输出拼起来
  4. 过一个线性投影层得到最终输出

公式是:

MultiHead(Q, K, V) = Concat(head₁, head₂, ..., headₕ) · Wₒ
  其中 headᵢ = Attention(Q·Wqᵢ, K·Wkᵢ, V·Wvᵢ)

实例

# ============================================
# 纯 Python 实现多头注意力(Multi-Head Attention)
# ============================================

import math
import random
random.seed(42)  # runoob:固定随机种子


def softmax(x: list) -> list:
    """计算 Softmax"""
    max_val = max(x)
    exp_x = [math.exp(v - max_val) for v in x]
    sum_exp = sum(exp_x)
    return [v / sum_exp for v in exp_x]


def matrix_multiply(A: list, B: list) -> list:
    """矩阵乘法"""
    m = len(A)
    n = len(B)
    p = len(B[0])
    result = [[0.0 for _ in range(p)] for _ in range(m)]
    for i in range(m):
        for j in range(p):
            for k in range(n):
                result[i][j] += A[i][k] * B[k][j]
    return result


def transpose(matrix: list) -> list:
    """矩阵转置"""
    return list(map(list, zip(*matrix)))


class MultiHeadAttention:
    """纯 Python 实现的多头注意力层"""

    def __init__(self, d_model: int, num_heads: int):
        """
        初始化多头注意力
        d_model: 模型维度(必须能被 num_heads 整除)
        num_heads: 注意力头数
        """

        assert d_model % num_heads == 0, "d_model 必须能被 num_heads 整除"

        self.d_model = d_model
        self.num_heads = num_heads
        self.d_k = d_model // num_heads  # 每个头的维度

        # 初始化投影矩阵
        self.Wq = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_model)]
        self.Wk = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_model)]
        self.Wv = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_model)]
        self.Wo = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_model)]

    def split_heads(self, X: list) -> list:
        """
        把最后一维拆分成 num_heads 个头
        输入形状: [seq_len, d_model]
        输出形状: [num_heads, seq_len, d_k]
        """

        seq_len = len(X)
        # 重塑: [seq_len, num_heads, d_k]
        reshaped = []
        for row in X:
            head_rows = []
            for h in range(self.num_heads):
                start = h * self.d_k
                end = start + self.d_k
                head_rows.append(row[start:end])
            reshaped.append(head_rows)
        # 转置: [num_heads, seq_len, d_k]
        result = []
        for h in range(self.num_heads):
            head_data = [reshaped[i][h] for i in range(seq_len)]
            result.append(head_data)
        return result

    def combine_heads(self, heads: list) -> list:
        """
        把多个头拼回去
        输入形状: [num_heads, seq_len, d_k]
        输出形状: [seq_len, d_model]
        """

        seq_len = len(heads[0])
        result = []
        for i in range(seq_len):
            combined = []
            for h in range(self.num_heads):
                combined.extend(heads[h][i])
            result.append(combined)
        return result

    def attention_single_head(self, Q: list, K: list, V: list) -> list:
        """单头自注意力"""
        # Q·K^T
        scores = matrix_multiply(Q, transpose(K))
        # 缩放
        scale = math.sqrt(self.d_k)
        scores_scaled = [[v / scale for v in row] for row in scores]
        # Softmax
        weights = [softmax(row) for row in scores_scaled]
        # 加权求和 V
        output = matrix_multiply(weights, V)
        return output, weights

    def forward(self, X: list) -> list:
        """多头注意力前向传播"""
        seq_len = len(X)

        # 步骤 1:计算 Q, K, V
        Q = matrix_multiply(X, self.Wq)
        K = matrix_multiply(X, self.Wk)
        V = matrix_multiply(X, self.Wv)

        # 步骤 2:拆分成多个头
        Q_heads = self.split_heads(Q)
        K_heads = self.split_heads(K)
        V_heads = self.split_heads(V)

        # 步骤 3:每个头独立做自注意力
        output_heads = []
        all_weights = []
        for h in range(self.num_heads):
            out, weights = self.attention_single_head(
                Q_heads[h], K_heads[h], V_heads[h]
            )
            output_heads.append(out)
            all_weights.append(weights)

        # 步骤 4:把所有头拼起来
        combined = self.combine_heads(output_heads)

        # 步骤 5:最终线性投影
        output = matrix_multiply(combined, self.Wo)

        return output, all_weights


# ============================================
# 演示多头注意力
# ============================================

d_model = 12
num_heads = 3  # 12 / 3 = 4 维每个头

# 输入序列:4 个词,每个 12 维
X = [
    [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
    [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
    [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
    [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
]

print("=" * 50)
print("Runoob 多头注意力演示")
print("=" * 50)
print(f"d_model = {d_model}, num_heads = {num_heads}")
print(f"每个头的维度: d_k = {d_model // num_heads}")
print()

mha = MultiHeadAttention(d_model=d_model, num_heads=num_heads)
output, all_weights = mha.forward(X)

print("\n各个头的注意力权重:")
for h in range(num_heads):
    print(f"\n头 {h}:")
    for i, row in enumerate(all_weights[h]):
        print(f"  位置 {i}: {[f'{v:.4f}' for v in row]}")

print("\n" + "=" * 50)
print("多头注意力最终输出形状:", len(output), "×", len(output[0]))
print("=" * 50)
for i, row in enumerate(output):
    print(f"位置 {i}: {[f'{v:.4f}' for v in row[:6]]}...")

多头注意力的强大之处在于:不同的头会自动学到不同类型的注意力模式。

例如在翻译任务中:

  • 有的头关注语法关系(主谓宾)
  • 有的头关注指代关系(代词指的是谁)
  • 有的头关注局部相邻词

这些模式不是人为设计的,是模型在训练中自动学到的。


位置编码(Positional Encoding)

自注意力有个问题:它是"词袋"模型——不关心词的顺序。"我爱你"和"你爱我"在自注意力看来可能是一样的。

但顺序对语言至关重要。我们需要给模型"注入"位置信息。

正弦位置编码

原始 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 是维度索引
  • d_model 是模型维度

这个设计很巧妙:

  • 每个位置有唯一的编码
  • 相对位置可以用线性组合表示(sin(a+b) = sin a cos b + cos a sin b)
  • 可以外推到比训练时更长的序列

实例

# ============================================
# 实现正弦位置编码
# ============================================

import math


def sinusoidal_positional_encoding(seq_len: int, d_model: int) -> list:
    """
    计算正弦位置编码
    返回: [seq_len, d_model] 的位置编码矩阵
    """

    pe = [[0.0 for _ in range(d_model)] for _ in range(seq_len)]

    for pos in range(seq_len):
        for i in range(0, d_model, 2):
            # 偶数维度用 sin
            div_term = math.pow(10000, 2 * i / d_model)
            pe[pos][i] = math.sin(pos / div_term)

            # 奇数维度用 cos(如果还有下一个维度)
            if i + 1 < d_model:
                pe[pos][i + 1] = math.cos(pos / div_term)

    return pe


# 演示:生成位置编码
seq_len = 10
d_model = 8

pe = sinusoidal_positional_encoding(seq_len, d_model)

print("=" * 50)
print("Runoob 正弦位置编码演示")
print("=" * 50)
print(f"序列长度: {seq_len}, 维度: {d_model}")
print()
for pos in range(seq_len):
    print(f"位置 {pos:2d}: {[f'{v:.4f}' for v in pe[pos]]}")

# 看看相对位置的关系
print("\n" + "=" * 50)
print("验证相对位置的线性关系")
print("=" * 50)
print("位置 0 和位置 1 的编码差异:")
diff = [pe[1][i] - pe[0][i] for i in range(d_model)]
print([f'{v:.4f}' for v in diff])

RoPE:旋转位置编码

正弦位置编码虽然经典,但后来有了更好的方案。现在的主流大模型(如 LLaMA)用的是 RoPE(Rotary Positional Embedding)

RoPE 的思想是:把位置信息"旋转"进 Query 和 Key 里。

具体做法是对 Q 和 K 的每一对维度做旋转:

[ q₁, q₂, q₃, q₄, ... ]
  ↓ 旋转
[ q₁·cos mθ - q₂·sin mθ,
  q₁·sin mθ + q₂·cos mθ,
  q₃·cos mθ' - q₄·sin mθ',
  q₃·sin mθ' + q₄·cos mθ',
  ... ]

RoPE 的优势:

  • 自然包含相对位置信息
  • 外推性好(长度超过训练时也能用)
  • 效果通常比正弦位置编码好

ALiBi:按距离衰减注意力

另一个简单有效的方案是 ALiBi(Attention with Linear Biases)

它不加位置编码,而是直接给注意力分数加一个偏置:离得越远,分数越低。

ALiBi 的优势是简单,而且对长度外推特别友好。

位置编码方法原理优点代表模型
正弦位置编码sin/cos 函数可外推,计算简单原始 Transformer, BERT
可学习位置编码直接训练 embedding表达能力强GPT-1/2
RoPE旋转 Q/K相对位置,效果好LLaMA, PaLM
ALiBi注意力分数加偏置简单,长序列好MPT, BLOOM

前馈网络(FFN)层

每个 Transformer 层里,注意力之后还有一个前馈网络(Feed-Forward Network)。

FFN 的结构

原始 Transformer 的 FFN 是:

FFN(x) = max(0, x·W₁ + b₁) · W₂ + b₂

也就是:线性层 → ReLU 激活 → 线性层。

通常中间维度比输入维度大(比如 d_model=512,中间维度=2048)。

ReLU vs SwiGLU

原始用的是 ReLU,但现在的主流是 SwiGLU

SwiGLU(x) = (x·W₁ ⊗ sigmoid(x·V₁)) · W₂

其中 ⊗ 是逐元素相乘。

SwiGLU 在很多任务上表现比 ReLU 好,LLaMA、PaLM 等都用它。

为什么 FFN 重要?

  • 注意力是"聚合信息",FFN 是"加工信息"
  • FFN 有大量参数,是模型"记忆"知识的主要地方
  • 没有 FFN,Transformer 的表达能力会大大降低

实例

# ============================================
# 实现 FFN:ReLU 版和 SwiGLU 版
# ============================================

import math
import random
random.seed(42)  # runoob


def relu(x: float) -> float:
    """ReLU 激活函数:max(0, x)"""
    return max(0.0, x)


def sigmoid(x: float) -> float:
    """Sigmoid 激活函数"""
    if x < -50:  # 防止数值溢出
        return 0.0
    return 1.0 / (1.0 + math.exp(-x))


def linear(x: list, W: list, b: list) -> list:
    """线性层:y = x·W + b"""
    output_size = len(b)
    output = [0.0 for _ in range(output_size)]

    for i in range(output_size):
        sum_wx = sum(x[j] * W[j][i] for j in range(len(x)))
        output[i] = sum_wx + b[i]

    return output


class FFNReLU:
    """ReLU 版 FFN"""

    def __init__(self, d_model: int, d_ff: int):
        self.d_model = d_model
        self.d_ff = d_ff

        # 随机初始化参数(实际中训练得到)
        self.W1 = [[random.normalvariate(0, 0.1) for _ in range(d_ff)]
                   for _ in range(d_model)]
        self.b1 = [0.0 for _ in range(d_ff)]

        self.W2 = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_ff)]
        self.b2 = [0.0 for _ in range(d_model)]

    def forward(self, x: list) -> list:
        """前向传播"""
        # 第一层:线性 + ReLU
        hidden = linear(x, self.W1, self.b1)
        hidden = [relu(v) for v in hidden]

        # 第二层:线性
        output = linear(hidden, self.W2, self.b2)
        return output


class FFNSwiGLU:
    """SwiGLU 版 FFN(更现代的选择)"""

    def __init__(self, d_model: int, d_ff: int):
        self.d_model = d_model
        self.d_ff = d_ff

        # SwiGLU 需要两个线性层做门控,再加一个输出层
        self.W1 = [[random.normalvariate(0, 0.1) for _ in range(d_ff)]
                   for _ in range(d_model)]
        self.V1 = [[random.normalvariate(0, 0.1) for _ in range(d_ff)]
                   for _ in range(d_model)]
        self.W2 = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_ff)]

    def forward(self, x: list) -> list:
        """前向传播:SwiGLU"""
        # 计算两个线性变换
        x1 = linear(x, self.W1, [0.0 for _ in range(self.d_ff)])
        x2 = linear(x, self.V1, [0.0 for _ in range(self.d_ff)])

        # SwiGLU:x1 * sigmoid(x2)
        hidden = [a * sigmoid(b) for a, b in zip(x1, x2)]

        # 输出层
        output = linear(hidden, self.W2, [0.0 for _ in range(self.d_model)])
        return output


# 演示 FFN
d_model = 8
d_ff = 16

# 测试输入
x = [0.1, -0.2, 0.3, -0.4, 0.5, -0.6, 0.7, -0.8]

print("=" * 50)
print("Runoob FFN 演示")
print("=" * 50)
print(f"输入: {[f'{v:.4f}' for v in x]}")

ffn_relu = FFNReLU(d_model, d_ff)
output_relu = ffn_relu.forward(x)
print(f"\nReLU FFN 输出: {[f'{v:.4f}' for v in output_relu]}")

ffn_swiglu = FFNSwiGLU(d_model, d_ff)
output_swiglu = ffn_swiglu.forward(x)
print(f"SwiGLU FFN 输出: {[f'{v:.4f}' for v in output_swiglu]}")

层归一化(Layer Norm)

Transformer 里还有一个重要组件:层归一化(Layer Normalization)。

层归一化做什么

层归一化把每个样本的特征归一化到均值 0、方差 1:

LN(x) = γ ⊗ ((x - μ) / √σ²) + β

其中:

  • μ 是 x 的均值,σ² 是 x 的方差
  • γ(scale)和 β(shift)是可学习参数
  • ⊗ 是逐元素相乘

实例

# ============================================
# 实现层归一化(Layer Normalization)
# ============================================

import math


def layer_norm(x: list, gamma: list, beta: list, eps: float = 1e-6) -> list:
    """
    层归一化
    x: 输入向量
    gamma: 缩放参数
    beta: 偏移参数
    eps: 小常数防止除零
    """

    # 计算均值
    mean = sum(x) / len(x)

    # 计算方差
    var = sum((v - mean) ** 2 for v in x) / len(x)

    # 归一化 + 缩放偏移
    normalized = []
    for xi, gi, bi in zip(x, gamma, beta):
        normalized.append(gi * (xi - mean) / math.sqrt(var + eps) + bi)

    return normalized


# 演示层归一化
d_model = 6

# 输入向量
x = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]

# gamma 初始化为 1,beta 初始化为 0
gamma = [1.0 for _ in range(d_model)]
beta = [0.0 for _ in range(d_model)]

print("=" * 50)
print("Runoob 层归一化演示")
print("=" * 50)
print(f"输入: {[f'{v:.4f}' for v in x]}")

output = layer_norm(x, gamma, beta)
print(f"输出: {[f'{v:.4f}' for v in output]}")

# 验证均值和方差
mean_out = sum(output) / len(output)
var_out = sum((v - mean_out) ** 2 for v in output) / len(output)
print(f"\n输出均值: {mean_out:.4f}")
print(f"输出方差: {var_out:.4f}")

Pre-Norm vs Post-Norm

层归一化有两种放法:

  • Post-Norm:子层 → 残差 → 归一化(原始 Transformer)
  • Pre-Norm:归一化 → 子层 → 残差(现在主流)

Pre-Norm 训练更稳定,现在的大模型几乎都用它。

RMS Norm

RMS Norm 是层归一化的简化版,只算均方根,不减均值:

RMS(x) = sqrt(mean(x²))
RMSNorm(x) = γ ⊗ (x / RMS(x))

RMS Norm 计算更快,效果差不多,LLaMA 等模型用它。


Encoder-Decoder vs Decoder-Only 架构

原始 Transformer 是 Encoder-Decoder 架构,但现在的大语言模型大多是 Decoder-Only。

Encoder-Decoder:用于翻译等任务

Encoder-Decoder 架构:

  • Encoder:读入输入序列(如源语言句子),生成上下文表示
  • Decoder:基于 Encoder 的输出生成目标序列(如翻译结果)

Decoder 里有两种注意力:

  1. 掩码自注意力:只能看之前的词(不能看未来)
  2. 交叉注意力:去关注 Encoder 的输出

适合的任务:机器翻译、语音识别、摘要生成(需要把输入"重写成"输出)。

Decoder-Only:GPT 系列,用于生成

Decoder-Only 架构只有 Decoder,没有 Encoder。它的特点:

  • 只有掩码自注意力(因果注意力)
  • 自回归生成:一个词一个词地输出
  • 适合预训练 + 微调的范式

现在的 GPT、LLaMA、Claude、Gemini……几乎都是 Decoder-Only。

Encoder-Only:BERT 系列,用于理解

Encoder-Only 架构只有 Encoder。它的特点:

  • 双向注意力:每个词能看到所有词
  • 适合理解任务:分类、命名实体识别、提取式问答
  • 用"掩码语言模型"(MLM)预训练
架构组件注意力类型代表模型适用任务
Encoder-DecoderEncoder + Decoder双向 + 掩码 + 交叉T5, BART翻译、摘要
Decoder-OnlyDecoder掩码(因果)GPT, LLaMA, Claude文本生成
Encoder-OnlyEncoder双向BERT, RoBERTa分类、NER

为什么 Decoder-Only 成为主流?因为它简单,而且在大规模预训练下,生成能力极强。"一刀切"反而比"专门设计"效果更好。


Flash Attention:让注意力计算又快又省内存

标准自注意力的时间和内存复杂度都是 O(n²),序列长了之后(比如 n=8192),既慢又显存不够。

Flash Attention 是一个关键优化,它在 2022 年的出现,让长序列训练变得可行。

标准注意力的内存瓶颈

标准自注意力需要保存中间结果(注意力权重矩阵),用来做反向传播。对于 n=8192,这是 8192×8192 ≈ 6700 万个元素,光这个就占几十 GB 显存。

Flash Attention 的优化原理

Flash Attention 的核心思想:

  1. 分块计算:把 Q/K/V 分成小块,一块一块算
  2. 在线 Softmax:不保存完整的注意力矩阵,用数学技巧在反向时重新计算
  3. 利用 SRAM:把分块放进 GPU 的高速缓存(SRAM),减少 HBM 访问

结果是:

  • 内存复杂度从 O(n²) 降到 O(n)
  • 速度快 2-4 倍
  • 精度完全一样(数学上等价)

现在的大模型训练几乎都用 Flash Attention 或它的变种(Flash Attention v2、Memory-Efficient Attention 等)。


KV Cache:推理加速原理

训练优化完了,推理(生成)也需要优化。KV Cache 是推理加速的关键。

问题:自回归生成的重复计算

自回归生成时,每生成一个新词,都要重新算一遍之前所有词的 K 和 V——这很浪费。

比如生成第 100 个词时,前 99 个词的 K/V 之前已经算过了,不需要重算。

KV Cache 怎么做

KV Cache 的思想很简单:

  • 缓存之前所有位置的 K 和 V
  • 生成新词时,只算新词的 K/V,拼到缓存里
  • 用完整的 K/V 做注意力

这样每步的计算量从 O(n²) 降到 O(n),生成速度大幅提升。

实例

# ============================================
# 演示 KV Cache 的原理
# ============================================

import math
import random
random.seed(42)


def softmax(x):
    max_val = max(x)
    exp_x = [math.exp(v - max_val) for v in x]
    sum_exp = sum(exp_x)
    return [v / sum_exp for v in exp_x]


def matmul(A, B):
    m = len(A)
    n = len(B)
    p = len(B[0])
    result = [[0.0 for _ in range(p)] for _ in range(m)]
    for i in range(m):
        for j in range(p):
            for k in range(n):
                result[i][j] += A[i][k] * B[k][j]
    return result


def transpose(matrix):
    return list(map(list, zip(*matrix)))


class SimpleAttentionWithKVCache:
    """带 KV Cache 的简单自注意力"""

    def __init__(self, d_model: int):
        self.d_model = d_model
        self.Wq = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_model)]
        self.Wk = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_model)]
        self.Wv = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_model)]

    def forward(self, x_new: list, kv_cache: tuple = None) -> tuple:
        """
        一步生成,带 KV Cache
        x_new: 当前新输入(单个位置)
        kv_cache: 之前的 K 和 V,(K_cache, V_cache)
        返回: (输出, 新的 kv_cache)
        """

        # 计算当前位置的 Q, K, V
        q = matmul([x_new], self.Wq)
        k = matmul([x_new], self.Wk)
        v = matmul([x_new], self.Wv)

        # 更新 KV Cache
        if kv_cache is None:
            K_full = k
            V_full = v
        else:
            K_cache, V_cache = kv_cache
            K_full = K_cache + k
            V_full = V_cache + v

        # 注意力:只拿当前位置的 Q
        scores = matmul(q, transpose(K_full))
        scale = math.sqrt(self.d_model)
        scores_scaled = [[v / scale for v in row] for row in scores]
        weights = [softmax(row) for row in scores_scaled]
        output = matmul(weights, V_full)

        return output[0], (K_full, V_full)


# 演示 KV Cache 生成过程
d_model = 4

# 模拟 4 步生成
inputs = [
    [1.0, 0.0, 0.0, 0.0],
    [0.0, 1.0, 0.0, 0.0],
    [0.0, 0.0, 1.0, 0.0],
    [0.0, 0.0, 0.0, 1.0],
]

attn = SimpleAttentionWithKVCache(d_model)

print("=" * 50)
print("Runoob KV Cache 演示")
print("=" * 50)

kv_cache = None
for step, x in enumerate(inputs):
    output, kv_cache = attn.forward(x, kv_cache)
    K_cache, V_cache = kv_cache
    print(f"\n步骤 {step}:")
    print(f"  KV Cache 大小: {len(K_cache)} 个位置")
    print(f"  输出: {[f'{v:.4f}' for v in output]}")

print("\n" + "=" * 50)
print("注意:每一步只计算当前位置的 K/V,")
print("然后拼接到 Cache 里,不用重算之前的!")
print("=" * 50)

KV Cache 有个小代价:需要额外显存存 K 和 V。对于 7B 模型、4k 上下文,KV Cache 大约占 1-2GB 显存——这是值得的。

还有更高级的优化:

  • Multi-Query Attention:多个头共享 K/V
  • Grouped Query Attention:分组共享 K/V

这些能进一步减少显存占用,提升推理速度。


小结:Transformer 完整流程

让我们把所有组件串起来,总结一下 Transformer 的完整流程。

一个 Decoder-Only Transformer 层

x → LayerNorm → Masked Multi-Head Attention → 残差连接 →
    → LayerNorm → FFN → 残差连接 → 输出

完整的 Decoder-Only 模型

  1. 输入 token IDs → 词嵌入
  2. 加位置编码
  3. 通过 N 个 Transformer 层
  4. 最终 LayerNorm
  5. 线性层映射到 vocab 大小
  6. Softmax 得到下一个词的概率

关键超参数

超参数含义GPT-2 (124M)LLaMA-7BGPT-3 (175B)
d_model模型维度768409612288
num_heads注意力头数123296
num_layers层数123296
d_ffFFN 中间维度30721100849152
context_len上下文长度10242048/40962048

代码实现指引

如果你想从零实现一个简化的 Transformer,步骤是:

  1. 实现层归一化
  2. 实现自注意力(带掩码)
  3. 实现多头注意力
  4. 实现 FFN
  5. 实现 Transformer 层
  6. 实现位置编码
  7. 组装完整模型
  8. 实现训练循环

实际中,我们会用 PyTorch 等框架,而不是纯 Python。这里的纯 Python 实现是为了演示原理,让你理解每一步在做什么。

理解 Transformer 的最好方式是亲手实现一次。哪怕是简化版,也会让你对很多细节有全新认识。