第2课:Transformer架构详解

130 阅读15分钟

第2课:Transformer架构详解

引言

欢迎来到《从零构建大型语言模型:Python实现20亿参数LLM的完整指南》的第二课。在上一课中,我们回顾了大型语言模型的发展历程,比较了当前市场上的主流模型,并勾勒出了我们的课程项目:构建一个20亿参数的LLM。

今天,我们将深入探讨现代LLM的核心基础——Transformer架构。自2017年Google研究团队在论文《Attention Is All You Need》中提出以来,Transformer架构已经成为NLP领域的主导范式,彻底改变了机器学习对序列数据的处理方式。无论是OpenAI的GPT系列,还是我们将要构建的模型,都以这一架构为基础。

在本课中,我们将分解Transformer的各个核心组件,解析其工作原理,理解其数学基础,为我们从零构建LLM奠定坚实的理论基础。让我们开始这段探索之旅!

1. Transformer架构概览

传统序列模型的局限性

在Transformer出现之前,处理序列数据(如文本)的主流模型是循环神经网络(RNN)及其变种LSTM和GRU。这些模型通过维持一个随时间步更新的隐藏状态来捕捉序列信息,但它们存在几个关键限制:

  • 长距离依赖问题:随着序列长度增加,早期信息容易"丢失"
  • 并行计算受限:由于顺序处理的本质,难以有效利用现代GPU并行计算能力
  • 梯度消失/爆炸:尽管LSTM/GRU在一定程度上缓解了这个问题,但长序列处理时仍然存在挑战

Transformer的革命性创新

Transformer架构通过几个创新性设计解决了上述问题:

  1. 完全抛弃递归结构:不依赖顺序处理,使得并行计算成为可能
  2. 引入注意力机制:允许模型直接关注输入序列中的任何位置,有效捕捉长距离依赖
  3. 结合位置编码:虽然没有固有的序列顺序感,但通过位置编码保留了位置信息

Transformer整体架构

标准的Transformer架构包含编码器(Encoder)和解码器(Decoder)两部分,形成了一个经典的序列到序列(Seq2Seq)结构:

  • 编码器:将输入序列转换为连续表示(潜在空间表示)
  • 解码器:根据编码器的输出和之前生成的标记预测下一个标记

然而,在现代LLM中,我们通常只使用架构的一部分:

  • 编码器模型(如BERT):专注于理解输入文本,适用于分类、情感分析等任务
  • 解码器模型(如GPT):专注于生成文本,适用于文本生成、对话等任务
  • 编码器-解码器模型(如T5):适用于翻译、摘要等转换任务

在我们的LLM项目中,我们将重点关注解码器架构,这是现代文本生成模型如GPT系列的基础。

2. 注意力机制:Transformer的核心

为什么需要注意力机制?

注意力机制的核心思想是允许模型在处理当前标记时,有选择地"关注"输入序列中的相关部分。这种机制模拟了人类认知中的选择性注意,即我们能够从复杂信息中筛选出最相关的部分。

自注意力机制详解

自注意力(Self-attention)是Transformer中最关键的组件,也称为缩放点积注意力(Scaled Dot-Product Attention)。其计算过程可分为以下步骤:

  1. 线性投影:将输入向量转换为查询(Query)、键(Key)和值(Value)三种表示:

    • Query (Q): 表示"我想要寻找什么信息"
    • Key (K): 表示"我包含什么信息"
    • Value (V): 表示"我的实际内容是什么"
  2. 计算注意力分数:通过Query和Key的点积衡量相似度:

    Attention Scores = (Q × K^T) / √d_k
    

    其中,√d_k是缩放因子,防止点积在高维空间中变得过大。

  3. 应用Softmax:将分数转换为概率分布:

    Attention Weights = Softmax(Attention Scores)
    
  4. 加权聚合:根据注意力权重聚合Value向量:

    Output = Attention Weights × V
    

整个自注意力机制可以用一个公式表示:

Attention(Q, K, V) = Softmax((Q × K^T) / √d_k) × V

代码实现示例

下面是自注意力机制的PyTorch简化实现:

import torch
import torch.nn as nn
import torch.nn.functional as F
​
class SelfAttention(nn.Module):
    def __init__(self, embed_size, heads):
        super(SelfAttention, self).__init__()
        self.embed_size = embed_size
        self.heads = heads
        self.head_dim = embed_size // heads
        
        assert (self.head_dim * heads == embed_size), "Embedding size needs to be divisible by heads"
        
        # 创建查询、键、值和输出的线性层
        self.q = nn.Linear(embed_size, embed_size)
        self.k = nn.Linear(embed_size, embed_size)
        self.v = nn.Linear(embed_size, embed_size)
        self.fc_out = nn.Linear(embed_size, embed_size)
        
    def forward(self, values, keys, query, mask=None):
        N = query.shape[0]  # 批次大小
        value_len, key_len, query_len = values.shape[1], keys.shape[1], query.shape[1]
        
        # 线性投影
        values = self.v(values)  # (N, value_len, embed_size)
        keys = self.k(keys)      # (N, key_len, embed_size)
        queries = self.q(query)  # (N, query_len, embed_size)
        
        # 分割成多头
        values = values.reshape(N, value_len, self.heads, self.head_dim)
        keys = keys.reshape(N, key_len, self.heads, self.head_dim)
        queries = queries.reshape(N, query_len, self.heads, self.head_dim)
        
        # 计算注意力分数
        energy = torch.einsum("nqhd,nkhd->nhqk", [queries, keys])
        # energy 维度: (N, heads, query_len, key_len)
        
        # 应用掩码(如果提供)
        if mask is not None:
            energy = energy.masked_fill(mask == 0, float("-1e20"))
        
        # 缩放注意力分数
        energy = energy / (self.head_dim ** (1/2))
        
        # 应用softmax
        attention = torch.softmax(energy, dim=3)  # (N, heads, query_len, key_len)
        
        # 加权聚合
        out = torch.einsum("nhql,nlhd->nqhd", [attention, values])
        # out 维度: (N, query_len, heads, head_dim)
        
        # 合并多头结果
        out = out.reshape(N, query_len, self.heads * self.head_dim)
        # out 维度: (N, query_len, embed_size)
        
        # 最后的线性层
        out = self.fc_out(out)  # (N, query_len, embed_size)
        
        return out

多头注意力机制

为了增强模型的表示能力,Transformer使用了多头注意力(Multi-Head Attention)。这一机制允许模型同时关注不同位置的不同表示子空间,捕捉更丰富的信息:

  1. 将输入分割成多个"头"(head)
  2. 每个头独立进行自注意力计算
  3. 合并各头的输出结果并通过线性变换

多头注意力可以表示为:

MultiHeadAttention(Q, K, V) = Concat(head_1, head_2, ..., head_h) × W^O

其中:

head_i = Attention(Q × W_i^Q, K × W_i^K, V × W_i^V)

实践中,这种机制能够显著提升模型性能,GPT-3使用96个注意力头,而我们的20亿参数模型可能会使用20-32个注意力头。

3. 编码器与解码器的工作原理

编码器结构

Transformer编码器由多个相同层堆叠而成,每层包含两个子层:

  1. 多头自注意力层:允许编码器关注输入序列中的不同部分
  2. 前馈神经网络:对每个位置独立应用相同的全连接网络

每个子层都使用残差连接(Residual Connection)和层归一化(Layer Normalization):

LayerNorm(x + Sublayer(x))

编码器的主要特点是每个位置可以看到完整的输入序列(双向注意力),这使其特别适合理解任务。

解码器结构

Transformer解码器同样由多个相同层堆叠而成,但每层包含三个子层:

  1. 掩码多头自注意力层:只允许关注当前位置及其之前的位置(防止信息泄露)
  2. 编码器-解码器注意力层:关注编码器的输出
  3. 前馈神经网络:与编码器中的相同

解码器中的掩码机制至关重要,它确保模型在预测第i个标记时,只能看到第i-1个及之前的标记,这与自回归生成的本质一致。

解码器模型(如GPT)

在现代LLM中,尤其是像GPT这样的自回归生成模型,我们主要使用Transformer解码器(或其变体)。这些模型去除了编码器-解码器注意力层,只保留了掩码自注意力和前馈网络。

GPT架构的关键特点:

  • 只使用解码器组件(掩码自注意力)
  • 使用因果掩码(causal mask)确保自回归属性
  • 通常堆叠更多层以增加模型容量

我们的20亿参数模型将遵循类似架构,使用24-36个Transformer解码器层。

4. 前馈神经网络

在每个注意力层之后,Transformer使用一个简单但强大的前馈神经网络(Feed-Forward Network,FFN)处理每个位置的表示。

结构与功能

FFN在每个位置上独立应用,由两个线性变换和一个非线性激活函数组成:

FFN(x) = max(0, x × W_1 + b_1) × W_2 + b_2

这里使用ReLU作为激活函数,但在现代实现中,我们通常使用GELU或SiLU(Swish)等更平滑的激活函数。

FFN的作用是引入非线性并增强模型的表示能力。虽然结构简单,但其参数量通常是模型总参数的大部分,因为内部维度通常设置为嵌入维度的4倍。

代码实现示例

class FeedForward(nn.Module):
    def __init__(self, embed_size, ff_hidden_size, dropout=0.1):
        super(FeedForward, self).__init__()
        self.fc1 = nn.Linear(embed_size, ff_hidden_size)
        self.fc2 = nn.Linear(ff_hidden_size, embed_size)
        self.gelu = nn.GELU()
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, x):
        # x shape: (batch_size, seq_len, embed_size)
        x = self.fc1(x)  # (batch_size, seq_len, ff_hidden_size)
        x = self.gelu(x)
        x = self.dropout(x)
        x = self.fc2(x)  # (batch_size, seq_len, embed_size)
        return x

5. 位置编码与上下文理解

位置信息的重要性

Transformer架构本身不包含序列顺序的内置感知,因为自注意力对所有位置一视同仁。然而,序列中标记的位置对于理解语言至关重要(如"狗咬人"和"人咬狗"的意思完全不同)。

正弦位置编码

原始Transformer论文使用了正弦和余弦函数构造的位置编码:

PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))

其中:

  • pos是标记在序列中的位置
  • i是维度索引
  • d_model是模型的嵌入维度

这种编码有几个有趣的属性:

  • 每个位置有唯一的编码
  • 编码之间的相对关系可以通过线性投影学习
  • 理论上可以外推到未见过的序列长度

代码实现示例

import numpy as np

def get_positional_encoding(max_seq_len, embed_size):
    positional_encoding = np.zeros((max_seq_len, embed_size))
    
    for pos in range(max_seq_len):
        for i in range(0, embed_size, 2):
            # 为偶数索引使用正弦
            positional_encoding[pos, i] = np.sin(pos / (10000 ** (i / embed_size)))
            
            # 为奇数索引使用余弦(如果在维度范围内)
            if i + 1 < embed_size:
                positional_encoding[pos, i + 1] = np.cos(pos / (10000 ** (i / embed_size)))
    
    # 转换为PyTorch张量
    return torch.FloatTensor(positional_encoding)

可学习位置编码

虽然固定的正弦位置编码在原始Transformer中表现良好,但许多现代实现(包括GPT系列)使用可学习的位置嵌入,允许模型自己学习最优的位置表示:

class LearnablePositionalEncoding(nn.Module):
    def __init__(self, max_seq_len, embed_size):
        super(LearnablePositionalEncoding, self).__init__()
        self.positional_embedding = nn.Parameter(torch.zeros(max_seq_len, embed_size))
        nn.init.normal_(self.positional_embedding, std=0.02)
        
    def forward(self, x):
        # x shape: (batch_size, seq_len, embed_size)
        seq_len = x.size(1)
        return x + self.positional_embedding[:seq_len, :]

相对位置编码

最近的研究表明,相对位置编码(RPE)可能比绝对位置编码更有效。RPE直接在注意力计算中编码位置关系,而不是在输入嵌入中添加位置信息。

T5、DeBERTa和PaLM等模型采用了各种相对位置编码方案,它们通常能够捕捉更长距离的依赖关系,并在长文本处理上表现更佳。

位置编码与上下文窗口

位置编码方案直接影响模型的上下文窗口大小(即模型能"看到"多远的上下文)。GPT-2有1024标记的上下文窗口,GPT-3扩展到2048,而最新的模型如Claude和GPT-4甚至支持超过10万标记的上下文窗口。

在我们的20亿参数模型中,我们将使用2048的上下文窗口,并实现可学习的位置编码,以平衡性能和计算需求。

6. 组合完整的Transformer层

现在,我们已经理解了Transformer的核心组件,让我们看看如何将它们组合成一个完整的Transformer层,特别是针对解码器模型。

解码器层结构

一个完整的GPT风格的解码器层包括:

  1. 掩码多头自注意力
  2. 残差连接和层归一化
  3. 前馈神经网络
  4. 另一个残差连接和层归一化

代码实现

class DecoderLayer(nn.Module):
    def __init__(self, embed_size, num_heads, ff_hidden_size, dropout=0.1):
        super(DecoderLayer, self).__init__()
        
        self.attention = SelfAttention(embed_size, num_heads)
        self.norm1 = nn.LayerNorm(embed_size)
        self.ff = FeedForward(embed_size, ff_hidden_size, dropout)
        self.norm2 = nn.LayerNorm(embed_size)
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, x, mask=None):
        # 自注意力块
        attention_output = self.attention(x, x, x, mask)
        x = self.norm1(x + self.dropout(attention_output))
        
        # 前馈网络块
        ff_output = self.ff(x)
        x = self.norm2(x + self.dropout(ff_output))
        
        return x

完整的GPT模型框架

最后,我们可以组合多个解码器层构建一个完整的GPT风格模型:

class GPTModel(nn.Module):
    def __init__(
        self, 
        vocab_size, 
        embed_size, 
        num_layers, 
        num_heads, 
        ff_hidden_size, 
        max_seq_len, 
        dropout=0.1
    ):
        super(GPTModel, self).__init__()
        
        # 标记嵌入
        self.token_embedding = nn.Embedding(vocab_size, embed_size)
        
        # 位置编码
        self.position_embedding = nn.Parameter(torch.zeros(max_seq_len, embed_size))
        
        # 解码器层
        self.layers = nn.ModuleList(
            [DecoderLayer(embed_size, num_heads, ff_hidden_size, dropout) 
             for _ in range(num_layers)]
        )
        
        # 输出层
        self.norm = nn.LayerNorm(embed_size)
        self.fc_out = nn.Linear(embed_size, vocab_size, bias=False)
        
        # 权重绑定(可选,但GPT模型通常使用)
        self.fc_out.weight = self.token_embedding.weight
        
        # 初始化
        self.apply(self._init_weights)
        
    def _init_weights(self, module):
        if isinstance(module, (nn.Linear, nn.Embedding)):
            module.weight.data.normal_(mean=0.0, std=0.02)
            if isinstance(module, nn.Linear) and module.bias is not None:
                module.bias.data.zero_()
        elif isinstance(module, nn.LayerNorm):
            module.bias.data.zero_()
            module.weight.data.fill_(1.0)
        
    def get_causal_mask(self, seq_len):
        # 创建因果掩码(下三角矩阵)
        mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1).bool()
        return ~mask  # 反转使得1表示可见,0表示掩码
    
    def forward(self, x):
        batch_size, seq_len = x.shape
        
        # 获取嵌入
        token_embeds = self.token_embedding(x)  # (batch_size, seq_len, embed_size)
        
        # 添加位置嵌入
        position_embeds = self.position_embedding[:seq_len, :]
        x = token_embeds + position_embeds
        
        # 创建因果掩码
        mask = self.get_causal_mask(seq_len).to(x.device)
        
        # 应用解码器层
        for layer in self.layers:
            x = layer(x, mask)
        
        # 应用最终层归一化
        x = self.norm(x)
        
        # 预测下一个标记
        logits = self.fc_out(x)
        
        return logits

这个模型实现了GPT风格架构的核心组件,为我们的20亿参数模型提供了基础框架。在实际实现中,我们还需要添加更多优化和功能,如混合精度训练、梯度累积、检查点保存等。

7. 扩展到20亿参数规模

在理解了基础架构后,让我们简要讨论如何将这个模型扩展到20亿参数规模。

参数数量估计

Transformer模型的参数主要集中在以下几个部分:

  1. 标记嵌入:vocab_size × embed_size
  2. 位置嵌入:max_seq_len × embed_size
  3. 每个注意力层:4 × embed_size × embed_size(Q,K,V投影矩阵和输出投影矩阵)
  4. 每个前馈网络:embed_size × ff_hidden_size + ff_hidden_size × embed_size

对于我们的20亿参数模型,一个可能的配置是:

  • 词汇表大小(vocab_size): 50,257(与GPT-2相同)
  • 嵌入维度(embed_size): 2,048
  • 层数(num_layers): 24
  • 注意力头数(num_heads): 16
  • 前馈网络隐藏维度(ff_hidden_size): 8,192
  • 最大序列长度(max_seq_len): 2,048

这样的配置会产生约20.5亿参数,其中大部分来自多头注意力层和前馈网络。

内存与计算考虑

训练如此大规模的模型需要考虑内存需求和计算效率:

  1. 梯度检查点:只保存部分中间激活以节省内存
  2. 混合精度训练:使用FP16/BF16减少内存占用和加速计算
  3. 模型并行:跨多个GPU分割模型
  4. 梯度累积:多步累积梯度以模拟更大的批量

在实现我们的20亿参数模型时,这些优化技术将是必不可少的。

总结与展望

在本课中,我们深入探讨了Transformer架构的核心组件,包括自注意力机制、前馈神经网络、位置编码以及编码器和解码器的工作原理。我们还提供了关键组件的代码实现,并讨论了如何将这些组件组合成完整的GPT风格模型。

通过理解这些基础组件,我们现在已经为构建我们的20亿参数LLM奠定了理论基础。Transformer的设计精巧而强大,通过自注意力机制有效捕捉长距离依赖,通过并行计算提高训练效率,为现代大型语言模型的成功提供了关键支撑。

在下一课中,我们将探讨语言模型的数学基础,包括概率语言模型、交叉熵损失函数、梯度下降和优化器选择,为我们的实际模型训练做进一步准备。

延伸阅读

  1. Vaswani, A., et al. (2017). Attention Is All You Need. NeurIPS.
  2. Radford, A., et al. (2019). Language Models are Unsupervised Multitask Learners. OpenAI.
  3. Brown, T. B., et al. (2020). Language Models are Few-Shot Learners. NeurIPS.
  4. Child, R., et al. (2019). Generating Long Sequences with Sparse Transformers. arXiv.
  5. Dai, Z., et al. (2019). Transformer-XL: Attentive Language Models Beyond a Fixed-Length Context. ACL.

思考问题:

  1. 自注意力机制与传统RNN相比,在处理长序列时有哪些优势和潜在劣势?
  2. 在扩展模型规模时,应该优先增加哪些架构参数(层数、宽度、注意力头数等)?为什么?
  3. 固定的正弦位置编码和可学习的位置嵌入各有什么优缺点?在什么情况下一种可能优于另一种?
  4. 如果你有有限的计算资源,如何在不牺牲太多性能的情况下简化Transformer架构?

在下一课中,我们将深入探讨语言模型的数学基础,为我们构建的20亿参数LLM的训练和优化奠定坚实的理论基础!