第二章 大模型基础理论:Transformer架构精讲
说实话,Transformer 这个架构刚出来的时候,我第一反应是「这玩意儿能行?」。2017年那篇《Attention Is All You Need》我反复读了三遍,才真正理解它的精妙之处。今天咱们就把它拆开揉碎了讲清楚。
2.1 从RNN到Transformer:为什么非它不可?
在Transformer出现之前,做序列建模基本靠RNN、LSTM这些循环神经网络。它们的问题很明显——串行计算,没法并行。你想想看,处理一句话必须一个字一个字往后推,GPU的并行能力完全浪费了。
我2018年做过一个法律文书生成的项目,用LSTM跑一个3000字的判决书,训练一次要十几个小时。后来换成Transformer,同样的数据量,两个小时就搞定了。这就是差距。
核心痛点:RNN的隐状态必须按时间步传递,导致长距离依赖难以捕捉。Transformer用自注意力机制一步到位,任意两个位置都能直接交互。
2.2 自注意力机制:让模型学会「看重点」
自注意力,说白了就是让每个词去「关注」句子里的其他词。比如「张三起诉了李四,因为他违约了」——这个「他」指的是谁?自注意力机制会计算「他」和「张三」「李四」的关联度,然后加权融合。
具体怎么算的?我习惯用三步来理解:
- 生成Q、K、V:每个词通过三个不同的线性变换,得到查询向量Q、键向量K、值向量V
- 计算注意力分数:Q和K做点积,再除以√d_k(防止梯度爆炸),然后过softmax归一化
- 加权求和:用归一化后的分数对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负责生成输出。中间通过交叉注意力连接。
我画了一张图帮你理解整体流程:
嗯,这里要注意一点:Decoder里的自注意力是带掩码的。为什么?因为生成的时候不能看到未来的词。比如生成「我」的时候,不能提前看到「爱」「你」。这个掩码是个上三角矩阵,把未来位置遮住。
2.6 实际应用中的经验总结
| 组件 | 常见问题 | 我的建议 |
|---|---|---|
| 自注意力 | 计算复杂度O(n²),长文本显存爆炸 | 用稀疏注意力或Flash Attention优化 |
| 多头注意力 | 头数太多导致过拟合 | 小模型用8头,大模型用16头,别贪多 |
| 位置编码 | 长度外推能力差 | 法律场景建议用RoPE或ALiBi |
| 残差连接 | 深层网络梯度消失 | Pre-LN比Post-LN更稳定,我亲测有效 |
核心总结:Transformer的成功不是偶然的。自注意力解决了长距离依赖,多头注意力提供了多视角,位置编码弥补了序列信息的缺失。这三者缺一不可。我在法律大模型项目中,光是调整注意力头数和位置编码类型,就花了整整两周——但效果提升非常明显。
最后说一句:别被论文里的公式吓到。Transformer的核心思想其实很简单——让每个词都能看到所有词,然后自己决定关注谁。剩下的都是工程细节。你动手写一遍代码,比看十遍论文都管用。