第二章 大模型基础理论:Transformer架构精讲

说实话,Transformer 这个架构刚出来的时候,我第一反应是「这玩意儿能行?」。2017年那篇《Attention Is All You Need》我反复读了三遍,才真正理解它的精妙之处。今天咱们就把它拆开揉碎了讲清楚。

2.1 从RNN到Transformer:为什么非它不可?

在Transformer出现之前,做序列建模基本靠RNN、LSTM这些循环神经网络。它们的问题很明显——串行计算,没法并行。你想想看,处理一句话必须一个字一个字往后推,GPU的并行能力完全浪费了。

我2018年做过一个法律文书生成的项目,用LSTM跑一个3000字的判决书,训练一次要十几个小时。后来换成Transformer,同样的数据量,两个小时就搞定了。这就是差距。

核心痛点:RNN的隐状态必须按时间步传递,导致长距离依赖难以捕捉。Transformer用自注意力机制一步到位,任意两个位置都能直接交互。

2.2 自注意力机制:让模型学会「看重点」

自注意力,说白了就是让每个词去「关注」句子里的其他词。比如「张三起诉了李四,因为他违约了」——这个「他」指的是谁?自注意力机制会计算「他」和「张三」「李四」的关联度,然后加权融合。

具体怎么算的?我习惯用三步来理解:

  1. 生成Q、K、V:每个词通过三个不同的线性变换,得到查询向量Q、键向量K、值向量V
  2. 计算注意力分数:Q和K做点积,再除以√d_k(防止梯度爆炸),然后过softmax归一化
  3. 加权求和:用归一化后的分数对V做加权平均
# 简化版自注意力计算
def self_attention(Q, K, V):
    # Q, K, V: [batch_size, seq_len, d_k]
    scores = torch.matmul(Q, K.transpose(-2, -1))  # 点积
    scores = scores / math.sqrt(d_k)               # 缩放
    attn_weights = torch.softmax(scores, dim=-1)   # 归一化
    output = torch.matmul(attn_weights, V)         # 加权求和
    return output

我的经验:实际项目中,d_k一般取64或128。太小了表达能力不够,太大了计算量飙升。我踩过坑——有次把d_k设成512,结果显存直接爆了。

2.3 多头注意力:从不同角度理解文本

单头注意力就像一个人看问题,视角有限。多头注意力相当于找了8个专家,每个人从不同角度分析,最后把意见综合起来。

为什么会有效?你想想看,在法律文本里,「应当」这个词可能同时关联着「义务主体」「行为内容」「法律后果」等多个方面。单头注意力只能捕捉一种关联,多头就能同时捕捉多种。

具体实现上,就是把Q、K、V分别切分成h个头,每个头独立计算注意力,最后拼起来再过一次线性层。

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, num_heads):
        super().__init__()
        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 forward(self, x):
        batch_size = x.size(0)
        
        # 线性变换 + 分头
        Q = self.W_q(x).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
        K = self.W_k(x).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
        V = self.W_v(x).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
        
        # 每个头独立计算注意力
        attn_output = self_attention(Q, K, V)
        
        # 合并头
        attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, -1, self.num_heads * self.d_k)
        
        # 最终线性变换
        return self.W_o(attn_output)

注意:d_model必须能被num_heads整除。我曾经因为手误把d_model设成512,num_heads设成7,结果程序跑起来全是NaN。排查了半天才发现是整除问题。

2.4 位置编码:给词加上「位置感」

自注意力有个致命缺陷——它不关心词的顺序。「张三打了李四」和「李四打了张三」,在自注意力看来是一样的。因为它是集合操作,不是序列操作。

解决办法就是位置编码。Transformer原文用的是正弦余弦函数,频率从低到高变化。我刚开始觉得这设计很玄学,后来才明白它的巧妙之处:

  • 不同位置的编码向量不同,模型能区分位置
  • 相邻位置的编码相似,模型能感知相对距离
  • 可以外推到任意长度,不需要训练
def positional_encoding(max_len, d_model):
    pe = torch.zeros(max_len, d_model)
    position = torch.arange(0, max_len).unsqueeze(1).float()
    
    # 计算频率
    div_term = torch.exp(torch.arange(0, d_model, 2).float() * 
                        -(math.log(10000.0) / d_model))
    
    # 偶数位置用sin,奇数位置用cos
    pe[:, 0::2] = torch.sin(position * div_term)
    pe[:, 1::2] = torch.cos(position * div_term)
    
    return pe

避坑指南:我曾经在微调法律大模型时,发现模型对长文本处理效果很差。排查后发现是位置编码只支持512长度,而法律文书动不动就2000字。后来改用RoPE(旋转位置编码)才解决——它天然支持长度外推。

2.5 整体架构:Encoder-Decoder的协作

Transformer的完整架构包含Encoder和Decoder两部分。Encoder负责理解输入,Decoder负责生成输出。中间通过交叉注意力连接。

我画了一张图帮你理解整体流程:

Transformer 架构流程图 输入序列 位置编码 Encoder(编码器) 多头自注意力 前馈神经网络 残差连接 + 层归一化 Decoder(解码器) 掩码多头自注意力 交叉注意力 前馈 + 残差 + 层归一

嗯,这里要注意一点:Decoder里的自注意力是带掩码的。为什么?因为生成的时候不能看到未来的词。比如生成「我」的时候,不能提前看到「爱」「你」。这个掩码是个上三角矩阵,把未来位置遮住。

2.6 实际应用中的经验总结

组件 常见问题 我的建议
自注意力 计算复杂度O(n²),长文本显存爆炸 用稀疏注意力或Flash Attention优化
多头注意力 头数太多导致过拟合 小模型用8头,大模型用16头,别贪多
位置编码 长度外推能力差 法律场景建议用RoPE或ALiBi
残差连接 深层网络梯度消失 Pre-LN比Post-LN更稳定,我亲测有效

核心总结:Transformer的成功不是偶然的。自注意力解决了长距离依赖,多头注意力提供了多视角,位置编码弥补了序列信息的缺失。这三者缺一不可。我在法律大模型项目中,光是调整注意力头数和位置编码类型,就花了整整两周——但效果提升非常明显。

最后说一句:别被论文里的公式吓到。Transformer的核心思想其实很简单——让每个词都能看到所有词,然后自己决定关注谁。剩下的都是工程细节。你动手写一遍代码,比看十遍论文都管用。