自2017年Google提出以来,Transformer已经成为几乎所有大模型的底层骨架。它完全抛弃了RNN的循环结构,仅靠注意力机制就能捕捉序列中任意两个位置之间的依赖关系,训练时还可以完全并行化。要真正理解GPT、BERT这类模型的能力来源,就必须弄懂两个核心组件:Self-Attention(自注意力)机制和位置编码(Positional Encoding)。本文将结合公式和代码,把这两部分的设计原理讲透。

一、Transformer整体架构拆解
Transformer由编码器和解码器两大部分组成。原始论文中编码器有6层,每层包含两个子模块:多头自注意力(Multi-Head Self-Attention)和前馈神经网络(Feed-Forward Network)。解码器同样有6层,但比编码器多了一个编码器-解码器注意力模块,用于让解码器在生成每个词时能够关注输入序列的信息。
每个子模块外面都套了一个残差连接和层归一化,也就是所谓的Add & Norm结构。残差连接保证了梯度能够顺畅回传,层归一化则稳定了训练过程。前馈网络本身是两层全连接,中间夹一个激活函数,对每个位置的表示独立地进行非线性变换,可以理解为对注意力聚合后的信息做一次深加工。
整个模型的计算流程是:输入的词先经过embedding层映射成向量,然后叠加位置编码,再依次流过编码器的各层。解码器采用自回归方式生成输出,训练时通过掩码机制保证每个位置只能看到它之前的内容,防止信息泄露。
二、Self-Attention机制的原理与计算过程
Self-Attention的核心思想是:序列中的每个位置都直接和所有位置计算关联度,然后按关联度加权聚合信息。具体做法是把输入向量分别乘以三个可学习的投影矩阵W_Q、W_K、W_V,得到Query(查询)、Key(键)、Value(值)三个矩阵。可以把这个过程类比成一次检索:Query是你要查的问题,Key是每条记录的索引,Value是记录的实际内容。
注意力分数的计算公式为Attention(Q, K, V) = softmax(QK^T / √d_k)V。先用Q和K做点积得到相似度矩阵,接着除以√d_k进行缩放,再通过softmax归一化成权重,最后对V加权求和。为什么要除以√d_k?因为当向量维度较高时,点积的结果方差会随着维度线性增大,数值过大会导致softmax进入饱和区,梯度几乎为零。除以√d_k可以把方差拉回到1附近,让训练更稳定,这是一个非常实用的小技巧。
import torch
import torch.nn.functional as F
def self_attention(x, Wq, Wk, Wv):
# x: (seq_len, d_model)
Q = x @ Wq # 查询矩阵
K = x @ Wk # 键矩阵
V = x @ Wv # 值矩阵
d_k = Q.size(-1)
# 缩放点积注意力,除以 sqrt(d_k) 防止softmax饱和
scores = Q @ K.transpose(-2, -1) / (d_k ** 0.5)
attn_weights = F.softmax(scores, dim=-1)
return attn_weights @ V, attn_weights多头注意力则把d_model维的空间切分成h个子空间,每个头在自己的低维空间里独立计算注意力,最后拼接起来再做一次线性投影。这样做的好处是不同的头可以关注不同类型的关系:有的头捕捉语法依赖,有的头关注远距离的语义关联,相当于从多个视角同时理解序列。
三、位置编码:为什么需要以及有哪些方案
注意力机制本身是置换不变的,也就是说把输入序列打乱顺序,输出的每个位置结果也只是跟着换位置,模型感知不到先后关系。但语言显然是有序的,"猫追狗"和"狗追猫"意思完全不同,所以必须把位置信息注入到输入表示中,这就是位置编码的由来。
原始Transformer采用的是正弦位置编码,公式为PE(pos, 2i) = sin(pos / 10000^(2i/d_model)),PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))。不同维度对应不同频率的正弦波,低维变化快,高维变化慢,有点像二进制计数器。这种编码有个优雅的性质:任意固定偏移量k,PE(pos+k)都可以表示为PE(pos)的线性变换,这意味着模型能轻松学到相对位置关系,而且不受训练序列长度的限制,可以外推到更长的序列。
后来出现的改进方案各有利弊。BERT使用可学习的位置编码,直接把位置embedding当作可训练参数,简单灵活但无法外推到训练时没见过的长度。目前大模型里最主流的是旋转位置编码RoPE,它通过复数旋转的方式把相对位置信息融入Q和K向量,在长文本场景下表现优异,Llama、Qwen等模型都在使用。此外还有ALiBi这种直接在注意力分数上按距离加线性惩罚的方案,实现极其简单,外推能力也不错。
import numpy as np
def sinusoidal_position_encoding(seq_len, d_model):
pe = np.zeros((seq_len, d_model))
position = np.arange(seq_len)[:, np.newaxis]
div_term = np.exp(np.arange(0, d_model, 2) * -(np.log(10000.0) / d_model))
pe[:, 0::2] = np.sin(position * div_term) # 偶数维用sin
pe[:, 1::2] = np.cos(position * div_term) # 奇数维用cos
return pe四、动手实现一个简化版Transformer
理解原理之后,动手写一遍代码是最好的巩固方式。PyTorch已经内置了nn.MultiheadAttention模块,但自己实现一遍缩放点积注意力能加深对细节的理解,比如掩码的处理、缩放因子的位置等。
import torch
import torch.nn as nn
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, num_heads):
super().__init__()
assert d_model % num_heads == 0
self.d_k = d_model // num_heads
self.num_heads = num_heads
self.Wq = nn.Linear(d_model, d_model)
self.Wk = nn.Linear(d_model, d_model)
self.Wv = nn.Linear(d_model, d_model)
self.out = nn.Linear(d_model, d_model)
def forward(self, x, mask=None):
batch, seq_len, d_model = x.shape
# 拆分成多头: (batch, num_heads, seq_len, d_k)
Q = self.Wq(x).view(batch, seq_len, self.num_heads, self.d_k).transpose(1, 2)
K = self.Wk(x).view(batch, seq_len, self.num_heads, self.d_k).transpose(1, 2)
V = self.Wv(x).view(batch, seq_len, self.num_heads, self.d_k).transpose(1, 2)
scores = Q @ K.transpose(-2, -1) / (self.d_k ** 0.5)
if mask is not None:
scores = scores.masked_fill(mask == 0, float("-inf"))
attn = torch.softmax(scores, dim=-1)
output = attn @ V
output = output.transpose(1, 2).reshape(batch, seq_len, d_model)
return self.out(output)上面的代码中,掩码部分值得注意:解码器的因果掩码是一个下三角矩阵,把上三角(未来位置)填充为负无穷,softmax之后这些位置的权重就变成0,从而保证自回归生成的正确性。这也是GPT类模型训练时最关键的细节之一。
五、总结
Transformer的成功并非某个单点突破,而是一系列精心设计的组合:Self-Attention提供全局建模能力,缩放点积保证数值稳定,多头机制带来多视角表达,位置编码补上了顺序信息,残差连接和层归一化让深层堆叠成为可能。理解了这些组件,再去阅读GPT的 decoder-only结构、BERT的双向编码设计,以及各种注意力优化方案(如Flash Attention、稀疏注意力)就会顺畅很多。建议读者对照公式亲手推导一遍注意力计算,并跑通自己实现的代码,这是从知道到掌握最短的路径。
Transformer架构Self-Attention位置编码修改时间:2026-09-13 14:54:47