
从零搭建一个Transformer
前置知识
- 深度学习基础
- 线性代数
- 概率论与数理统计
I. 从零运行一个Transformer
Encoder部分
假设我们输入transformer的是“我爱学习”
这一句话。
这句话首先会经过一个tokenizer(分词器),变成“我,爱,学习”
这三个词语,也就是我们所说的token。
我们会构建一个词表,将每个token映射到对应的id,我们这里假设词表仅有上文三个词:
1 | {“我”: 1, “爱”:2, “学习”: 3} |
这样,我们的输入就可以表征为一个序列长度seq_l
为3一维张量
[1, 2, 3]
。
虽然我们能够把自然语言通过这种方式数学化地表示出来,但是却损失了本来语句中包含的信息(1, 2, 3这三个数字之间并没有包含任何语义和语法上的信息和关联),所以我们需要用更复杂的方式来表征我们的token,而这种方式就是更长的向量。
我们如果能够让语义相近的词在向量空间中距离更近(“番茄”和“西红柿”),以及多个语义相叠加或排除能够得到一个新的语义(女人+警察=女警 / 警察-男人=女警),就能够通过向量很好地表征自然语言的信息,而从token到语义向量的这个过程我们可以通过机器学习来完成。
我们先将token转化为独热编码的形式,以便于进行矩阵运算,这个时候我们的输入就变成了这样一个[3, 3]
的矩阵:
1 | [[1, 0, 0], |
我们用X来表示当前的输入,X会与一个名为嵌入矩阵(embedding)的矩阵Wembed相乘,这个矩阵通过反向传播来调整参数,从而使得X能够变成包含语义信息的矩阵。
嵌入矩阵的大小为[vocab_size, d_m]
,
其中vocab_size
是指词表大小(在我们的例子中为3),而d_m
则是后续的模型参数大小,我们假设模型参数大小为9,则此时的嵌入矩阵大小为[3, 9]
经过下列运算我们得到:
XWembed = H
此时的H正是我们真正输入transformer的矩阵,其大小为[seq_l, d_m]
:
1 | [[0.1, 0.5, 0.2], |
由于transformer的注意力机制无法像RNN那样自然地关注到序列的位置信息,所以这里需要对每个token向量加上一个positional code,具体公式如下:
$PE_{(pos, 2i)} = \sin(\frac{pos}{10000^{2i/d_m}})$
$PE_{(pos, 2i+1)} = \cos(\frac{pos}{10000^{2i/d_m}})$
所以此时的输入H变成了:
H + PE = H′
其中PE是位置编码矩阵,其大小与H相同,也为[seq_l, d_m]
。这样,每个token的向量表示中就包含了它在序列中的位置信息。接下来,我们需要将这个输入H′送入Transformer的核心组件——多头自注意力机制(Multi-head
Self-Attention)。
Transformer的多头自注意力机制通过Q, K, V
三个核心矩阵来完成(Query,
Key和Value)
在多头自注意力机制中,我们首先计算Q, K, V矩阵:
Q = H′WQ
K = H′WK
V = H′WV
其中,WQ, WK, WV是参数矩阵,它们的大小都是[d_m, d_k]
,其中d_k = d_m / h
,h
是注意力头的数量。
我们假设有3个注意力头,那么就会存在三组参数矩阵,生成三组Q, K, V
。接下来,对于每个注意力头,我们计算注意力得分矩阵:
$Attention(Q, K, V) = softmax(\frac{QK^T}{\sqrt{d_k}})V$
在我们的例子中:d_k=3
,而$\frac{1}{\sqrt{d_k}}$是一个缩放因子,用于防止softmax数值爆炸的问题。
在注意力公式中,QKT可以视作Q和K两个矩阵中的向量进行点积;对于第一个注意力头,此时的结果可能为:
1 | [[0.7, 0.1, 0.2], |
这个矩阵表示每个token对其他token的关注程度。
例如,第一行表示”我”这个token对”我”、“爱”和”学习”这三个token的关注度分别为0.7、0.1和0.2。
经过softmax归一化后,这些注意力权重将用于对V矩阵进行加权求和,从而生成新的上下文表示。
计算出每个注意力头的注意力得分矩阵后,我们将这些结果拼接起来,并通过一个线性变换,得到多头注意力的输出:
MultiHead(Q, K, V) = Concat(head1, head2, ..., headh)WO
其中,WO是一个参数矩阵,大小为[d_m, d_m]
。这个多头注意力的输出将进入Transformer的下一个组件——前馈神经网络(Feed-Forward
Network)。
对于FNN,可以理解为一个MLP,而我们最终会得到一个形状为[seq_l, d_m]
的输出。
✅ 至此,transformer的encoder部分已经运行结束
Decoder部分
Decoder部分的运行过程类似于Encoder,但有几个关键区别:
- 首先,Decoder是自回归(Autoregressive)的,意味着它一次只能生成一个token。
- 其次,为了防止模型在训练时”作弊”看到未来的token,我们使用掩码机制(Masked Attention)。在实践中,这意味着我们在计算注意力分数时,将未来位置的分数设为负无穷(-inf),这样softmax后的权重就为0,模型就无法使用未来的信息。假设我们的目标输出序列是”我喜欢学习”,在生成”喜欢”这个token时,模型只能看到”我”这个已经生成的token。
- 除了掩码自注意力外,Decoder还包含一个额外的
编码器-解码器注意力层
,用于关注Encoder的输出。这使得Decoder能够在生成每个token时,考虑输入序列的全部信息。这种结构特别适合机器翻译等任务,使模型能够准确捕捉源语言和目标语言之间的对应关系。
假设我们运行的transformer执行的是汉译英的翻译任务,并且已经翻译出"I like"
(目标为"I like study"
):
我们首先将已经生成的结果通过嵌入层和位置编码
处理,得到初始的向量表示;然后这个向量会通过带掩码的自注意力层
,确保模型只能看到已经生成的token。
如果此时是推理阶段,那么掩码与否无关紧要;如果此时是训练阶段,我们要模型在生成"I like"
的前提下生成"study"
,那么在进行并行的训练时候,需要将"I like study"
进行掩码处理,结果形如:
<BOS> -inf -inf -inf -inf
<BOS> I -inf -inf -inf
<BOS> I like -inf -inf
<BOS> I like study -inf
<BOS> I like study <EOS>
接着通过编码器-解码器注意力层
,模型能够关注编码器输出的信息(我们的"我爱学习"
),从而生成相应的英文翻译"study"
。
✅ 至此,transformer已完整运行一次
Q&A
Q1: 归一化(Normalization)的核心思想是什么?为什么它能解决梯度问题?
- 核心思想是让神经网络每一层的输入分布保持一致,从而解决因参数更新导致的“内部协变量偏移”(Internal Covariate Shift)。这使得网络学习更稳定,加速收敛,并能缓解梯度消失/爆炸问题。这里的“分布”通常指神经元激活值的均值和方差。
Q2: 为什么批量归一化(BN)不适用于循环神经网络(RNN)?
- 分布差异:BN对一个批次中同一时间步的激活值进行归一化。但在变长序列中,这些激活值可能对应完全不同的词语(如“你”和“机器学习”),它们来自不同分布,强制归一化会破坏特征。
- 变长序列问题:BN需要为每个时间步保存统计量。但测试集可能出现比训练集更长的序列,导致无法获取后续时间步的统计量。
- 计算开销:BN需要为每个时间步单独计算和保存统计量,这在时间维度上增加了巨大的计算和内存开销。
Q3: 批量归一化与大数定理和中心极限定理有什么关系?
- 大数定理:BN利用大数定理的思想,用mini-batch的样本统计量来近似整个数据集的总体统计量,从而使归一化操作在每次迭代中都具有代表性。
- 中心极限定理:BN利用该定理所揭示的“样本均值的分布趋近于正态分布”的规律,来保证用于标准化的统计量是稳定可靠的。这使得BN能够放心地将数据强制归一化到均值为0、方差为1的稳定分布,这才是其真正的目的,而非让数据本身变为正态分布。
Q4: 层归一化(LN)和批量归一化(BN)在参数更新和效率上有何不同?
- 参数更新频率:无论是BN还是LN,它们的可学习参数(γ和β)以及模型的其他参数,都是每个训练批次(batch)更新一次。
- 效率:LN的效率通常更高。因为LN对每个样本独立计算统计量,其计算模式非常适合现代GPU的并行计算。而BN需要先汇总整个批次的统计量,会产生额外的同步开销。
- 推理:LN的归一化只依赖于单个样本,因此训练和测试时的行为完全一致,无需保存任何全局统计量。而BN必须在训练时保存一个全局统计量的运行平均值,用于测试时使用。
Q5: Transformer模型如何处理长序列?
- Transformer不使用循环,而是通过自注意力机制进行并行计算。它使用位置编码来提供序列中的位置信息,并用掩码(Masking)来处理变长序列中的填充部分,确保模型不会将注意力放在无意义的填充词上。
Q6: 在大模型中,嵌入层(Embedding Layer)的参数量是如何计算的?
- 嵌入层的参数量非常大,它等于词汇量大小(vocab_size)乘以嵌入维度(embedding_dim或d_model)。例如,一个有5万词汇,嵌入维度为1024的模型,其嵌入层参数量就是50,000 * 1024。