Files
happy-llm/Extra-Chapter/transformer-architecture
dapeng 887ffc1c11 feat: add extra-chapter transformer-architecture
feat: update picture to center
2025-07-14 10:30:38 +08:00
..

transformer-architecture

当本节内容作为第二章 Transformer架构中2.2章节Encoder-Decoder的补充结合Pytorch的API源码从Transformer的整体设计上来解释Transformer的各个模块。

从经典架构开始Encoder-Decoder结构

让我们先从经典架构图理解Transformer的整体设计思路。Transformer分为两个主要部分左侧的编码器Encoder和右侧的解码器Decoder。那么这两块结构的输入和输出分别是什么

alt text

1.jpg

Encoder的职责是接受完整的源序列输入将其转换为一个富含语义信息的表示序列。想象一下如果我们要做机器翻译Encoder就像是一个深度理解原文的专家它需要充分理解整个句子的含义、语法结构和上下文关系。

Decoder则承担着更复杂的任务它需要接受目标序列和编码器输出的表示序列然后输出词汇/字符的概率分布。这就像是一个翻译专家既要理解原文的含义通过Encoder的输出又要根据已经翻译的部分来决定下一个词应该是什么。

Positional Encoding位置编码设计

但这里有一个关键问题需要解决Transformer本身对位置信息不敏感。比如"我爱你"和"你爱我"这两个句子,在没有位置信息的情况下,模型无法感知到这是语义完全不同的句子。这就像是一个人失去了对词语顺序的感知能力,显然无法正确理解语言。

因此我们需要一个带有位置信息的向量将其添加到每个input embedding上来对不同位置得到不同的表征。这个模块就是图中的Positional Encoding

位置编码的设计原则

在设计编码模块时,有三个重要的前提假设,这些假设直接影响了最终的实现方案:

1. 确定性原则:每个位置的编码应该是确定的数字,不同序列中相同位置的编码应该相同。

为什么这个原则如此重要让我们考虑一个反例如果用等分的设计方法将一个序列从0~1之间做均匀划分那么序列长度不一样时每个位置上的编码也就不一样。当序列长度为5时位置编码可能是0、0.2、0.4、0.6、0.8但如果序列长度为10就变成了0、0.1、0.2...。同样对于第二个位置上的字符在第一个序列中是0.2在第二个序列中又是0.1,这样的编码就失去了确定性。

2. 相对关系一致性:不同句子中,对于任意两个位置之间的相对距离,相对关系应该保持一致。

这个目的是为了学习通用的语言关系,比如:"修饰词在被修饰词前1个位置"是通用模式。以下面的长短句举例:

- 长句子10个词
  位置: 0  1  2  3  4  5  6  7  8  9
  词汇: I  am learning about transformers today in class now

- 短句子6个词
  位置: 0  1  2  3  4  5
  词汇: I  like deep learning models

在长句子中位置1和位置4之间的编码关系应该与短句子中位置2和位置5之间的编码关系完全相同因为模型需要学会的是通用的相对位置关系。

3. 泛化能力:位置序列应该能推广到没见过的更长序列。

第三个假设希望位置编码可以推广到更长的测试句子。假如训练集中序列长度都是10以内的但测试集中可能会有长度为15的句子我们希望即使测试集中句子长度更长在训练中没有见过我们也能通过这样的position encoding推广过去。

三角函数编码

基于这些假设Transformer采用了sin和cos的组合来表征绝对位置信息

- 向量维度为偶数PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
- 向量维度为奇数PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))

通过sin和cos的组合来表征绝对位置的好处是pe(pos+k)可以写成pe(k)的线性组合(利用三角函数公式sin(A + B) = sin(A)cos(B) + cos(A)sin(B)。这样做的意义是即使测试集中出现了pos+k这种未见过的位置我们也可以把它写成训练集中见过的位置的线性组合而不用担心测试集中遇到更长的句子无法推广。

位置信息的传递机制

但位置编码是在最底层添加,会不会在深层网络中丢失?这个担心是多余的。通过残差连接,位置信息能够充分传递到上层网络。

假设有一个N层的神经网络输入为x₀包含位置编码那么

- 第1层: x₁ = x₀ + F₁(x₀)
- 第2层: x₂ = x₁ + F₂(x₁) = x₀ + F₁(x₀) + F₂(x₁)
- 第3层: x₃ = x₂ + F₃(x₂) = x₀ + F₁(x₀) + F₂(x₁) + F₃(x₂)
- ...
- 第N层: xₙ = x₀ + Σᵢ₌₁ⁿ Fᵢ(xᵢ₋₁)

可以看到初始的位置信息x₀始终存在于每一层的输出中这确保了位置信息不会随着网络层数的加深而消失。

从PyTorch源码API来理解transformer的架构设计

通过查看PyTorch的源码来了解Encoder和Decoder中的架构实现。源码位于/pytorch/torch/nn/modules/transformer.py此次借助的版本是v2.5.1。

顶层Transformer类的设计

首先PyTorch定义了一个顶层的Transformer类,我们可以通过torch.nn.Transformer来调用它:

# 使用示例
transformer_model = nn.Transformer(d_model=512, nhead=8, num_encoder_layers=6)
alt text

2.jpg

Transformer__init__函数中主要有5个核心参数

  1. d_model整个Transformer的特征维度在原论文中设置的是512。这个维度需要足够大以承载丰富的语义信息但太大会导致计算复杂度过高。

  2. nheadMulti-head attention的头数目。多头设计的目的是让模型可以捕捉到更多位置与位置之间的关系

  3. num_encoder_layers编码器encoder的block数目encoder的每个block包含多头自注意力机制和前馈神经网络这里默认block是6个

  4. num_decoder_layersdecoder解码器中block数目decoder的每个block包含多头自注意力机制、交叉注意力机制以及前馈神经网络

  5. dim_feedforward前馈神经网络层中间的特征维度默认是2048。Multihead attention输出时首先映射到2048这个大的特征空间然后再把它映射回来到512这样的空间。必须要保证输出的维度仍然是512这样就可以进行残差连接

alt text

3.jpg

模块化设计

init函数的作用是实例化模块第一个要实例化的模块就是encoder。

alt text

4.jpg

encoder通过TransformerEncoder的class去实现实例在这个class中需要传入encoder_layer在TransformerEncoderLayer的class中实现了Multihead self attention的调用、残差连接、层归一化、全连接层网络主要是这些来构成一个encoder_layer。

对于decode部分也是一样传入decodeLayer参数这个layer包含了自注意力机制、交叉注意力机制以及前馈神经网络。

alt text

5.jpg

总体上Transformer源码就是由四个class所构成

  • TransformerEncoderLayer:每一个编码层的实现
  • TransformerEncoder:负责把这些编码层串起来
  • TransformerDecoderLayer:每一个解码层的实现
  • TransformerDecoder:把这些解码层串起来

这种模块化的设计体现了软件工程的最佳实践:单一职责原则和组合优于继承的思想。

Forward函数编排计算流程

forward函数中Transformer的计算流程非常清晰

alt text

6.jpg

首先encoder输入是source句子以及padding_maskencoder中的注意力机制不需要掩码因此mask及is_causal参数不需要传入掩码。但需要对样本长度做掩盖padding_mask这个mask表示每一个样本的长度。当我们做训练时序列长度是不一样的有些短的样本在后面的一些位置上就是无效的通过在softmax中把无效位置上的值转成负无穷这样经过归一化后概率就变成0使得在这些位置上这些没有值的位置变得无效。

decode输入第一个是target也就是目标句子。第二个是memory表示encoder输出因为memory会输送到交叉注意力中。第三个是target mask这个target mask是一个考虑因果的mask在数学上是一个上三角矩阵。

因果掩码mask

每次预测时decoder都会有一个输入句子output embedding。但这个output embedding不能全部给它如果全部给它的话那它就变成了identity映射相当于从x到x的关系我给你了那预测出来的答案肯定是从x到x。

我们需要保证output每次只根据当前要预测的单词的左边的所有单词去预测这个单词。这个单词的本身和其他的右边的单词都不要输送到Output Embedding。这样的操作需要通过mask来实现随着预测的字符往右进行我们给到这个decoder中的output会越来越多所以它就是一个上三角的矩阵。

第四个memory mask和source sentence的长度有关在batch训练中source sentence每一个样本都不太一样memory mask就是每一个输入源序列这个样本的一个长度。

Transformer框架

这就是Transformer class总体上的构成框架init函数去实例化encoder和decoder。在forward函数中基于source、target、source mask、target mask分别入参到encoder和decoder中最终得到output即要预测字符的概率。

alt text

7.jpg

Transformer本质上是一个自回归的解码过程不是并行的预测输出而是每次只会预测一个输出一个然后不断的进行解码去预测出整体的target sentence。

Encoder编码器层

接下来我们分别来看下init函数中的4个class首先是单个编码器的实现:在TransformerEncoderLayer__init__函数中,需要实例化四个关键组件:

alt text

8.jpg

先是init函数中的参数和transformer中传入的一致d_model是整个Transformer的特征维度512。nhead是Multi-head attention中多头自注意力机制中头的数目。

多头注意力的设计

为什么是多头?目的是让模型可以捕捉到更多的位置与位置之间的关系。多头会分为多组的query、key和value每一组会单独地去计算attention的上下文的向量最后把这个向量拼起来再通过FFN得到最终的一个向量。

这样做的话对embedding的特征向量的维度会降低比如说原来的特征向量维度是512如果我们分为8个头这时向量每一个头它向量的维度就会变成64这里不是通过压缩而是线性变换重组。每个头独立计算注意力后会得到8个64维的输出向量。然后通过拼接操作concatenation将这8个64维向量首尾相接重新组合成一个512维的向量。最后再通过一个输出线性变换层得到最终的512维输出。

alt text

9.jpg

前馈神经网络

dimension feed forward是前馈神经网络FFN的维度因为需要先从512到2048再从2048到512所以设定了两个全连接层。前馈神经网络它是对每个单独位置进行一个建模并且不同位置的参数是共享的。类比1×1的pointwise卷积对图像中每个像素位置的特征向量独立进行变换。参数共享就是为序列中的每个位置都设计相同的参数目的是希望模型学会"如何处理特征"的通用能力,而不是"如何处理第x个位置的特征"的特定能力。

FFN实际上做的是embedding相同位置不同维度间的融合在每个位置内部对该位置的不同特征维度进行融合注意力机制负责位置间的信息交流。

Encoder编码器层的组件实例化

init函数中需要去实例化一些实例

  1. 首先是Multi-head attention本节着重整个框架
  2. 实例FFN前馈神经网络中的两个Linear层第一个Linear比较大第二个Linear是重新这个投射到d_model的尺度
  3. 实例layer norm在self attention之后会经过层归一化以及在前馈神经网络之后也会经过一个层归一
  4. 实例两个dropoutdropout是为了使得这个网络具备集成学习的特点即使我们在训练多个模提高泛化能力

Encoder编码器前向传播forward: 编排计算流程

forward函数中encoder层的调用很简单。Transformer encoder layer的第一部分通过self attention block得到一个表征(self._sa_block),然后再加一个这个残差连接(就是和x加起来)最后再经过一个层归一化。self attention的输入是序列x和pendding-mask这里的序列x既充当了query又充当了key和value。

alt text

10.jpg

第二部分是feed forward block把第一部分输出经过每个位置独立的一个全连接层再进行一个残差连接输送到层归一化中就得到x。这个就是Transformer encoder中的每个layer的输出。

原始论文的设计是层归一化在后即else的设计。

Encoder编码器的串联

TransformerEncoder class的作用是将多个编码器层串联起来将上一层的输出作为下一层的输入经过多层处理得到最终的编码器输出。

alt text

11.jpg

init主要是传入两个参数一个是encoder_layer表示TransformerEncoderLayer class的一个实例。第二个参数是num_layers表示transformer encoder有多少层层的含义就是block。encoder中自注意力机制+前馈神经网络这两块是一个block也就是一层。

Decoder解码器

解码器的实现比编码器更复杂,因为它包含三个子模块,需要处理更多的交互。

TransformerDecoderLayer中,我们需要实例化三套组件(自注意力+交叉注意力+前馈神经网络

alt text

12.jpg

在init参数中第一个是d_model表示transformer模型特征大小默认512。第二个参数是nhead是Transformer decoder的多头自注意力机制的头数。第三个参数是dimension feed forward表示decoder中FFN前馈神经网络的维度。

alt text

13.jpg

Decoder解码器两种注意力机制的区别

init参数中decoder和encoder不同的地方就是需要实例化两个Multihead attention。

第一个Multi-head attention自注意力机制它是对decoder这个输入序列的target sentence embedding作为输入序列的自身表征。

第二个Multi-head attention是交叉注意力机制我们想知道decoder multihead attention的输出和encoder输出状态的一个关联性用该注意力机制跨越了encoder和decoder两个不同序列不是decoder内部的自我关注而是让decoder去关注encoder的信息。于是我们通过用decoder MHA多头注意力的一个输出作为query然后用encoder的输出作为key和value来去算出一个上下文表征。

同样Decoder要实现两个Linear层第一个Linear层是比较大的把我们交叉自注意力机制的输出投射到一个更高维的空间就是2048。然后再把它投射到低维的空间就是从2048降成512。由于Decoder有三个模块自注意力+交叉注意力+前馈神经网络所以这里要实现3个norm和3个dropout。

Decoder解码器forward编排流程

解码器的forward函数体现了三个模块的协同工作:

  1. 第一个模块会把target sentence也就是序列x和target mask输入到self._sa_block中对target句子做自注意力机制的计算结果放入到残差网络中并且经过层归一化得到输出。

  2. 第二个模块依赖于第一个模块输出的x再和encoder输出的memory做交叉注意力的计算得到新的表征后经过残差网络和归一化的norm输出

  3. 把第二个模块的输出输送到FFN前馈神经网络再次进行残差网络和归一化的norm得到decoder的输出

alt text

14.jpg

可以看下_sa_block和_mha_block各自的调用它们都是调用的是Multihead attention只不过它们的query、key、value是不一样的。

alt text

15.jpg

alt text

16.jpg

self attention中query、key、value都是目标序列自身对自身的求相关性的计算。但在交叉注意力机制中query是decoder的一个输出key和value是encoder的输出始终是memory

通过多个TransformerDecoderLayer构成了TransformerDecoder和TransformerEncoder实现类似这里不重复赘述。

注意力机制的核心计算

最后看下注意力机制的核心计算PyTorch的实际实现更加复杂和优化但核心思想可以用论文版本来理解

alt text

17.jpg

注意力机制的直观理解

attention函数就是将一个query和一个由key和value形成的一对元素建立一个连接最终得到一个输出。比如我们去百度搜索一个词条这个词条就是query然后百度的数据库里有很多词条信息每个信息自身都有个keyvalue就是该词条的具体内容。我们通过这个query百度就会给我们返回一个搜索结果。这个结果就可以理解为一个注意力机制--基于query和key+value计算出来的一个上下文。

注意力机制的计算结果是Value的一个加权求和的结果权重是基于Query和Key的相似度计算出来的。先算Query和每个Key的相似度基于这个相似度进行Softmax归一化得到权重再把这个权重与每个Key所对应的Value进行加权求和。

Scaled Dot-Product Attention

在Transformer模型中用的是"Scaled Dot-Product Attention"这里有个scaled可以看到公式中就是QK会除以一个根号d_k这个目的就是为了使得Softmax的输入分布会更加稳定一点也就是使得它的方差会更小一点。

这个Attention由三部分构成分别是Q、K、V它们都是三个向量。首先我们会把query和key进行一个矩阵相乘如果我们单个样本来看就是向量内积批量来看就是矩阵相乘。

内积过后再除以一个根号d_K把每个位置上的这个内积放到一起去做一个归一化。这样就可以得到每个位置上的一个概率的表示因为Softmax它出来的结果就是它总和为1然后每一个值都是在0到1之间得到这样一个概率然后我们把概率和每个位置上value进行一个加权求和最终能得到attention的一个输出这就是scaled dot product attention的计算逻辑。

论文中讲的Multi-head self attention其实就是有很多个这样的自注意力机制同时计算算完之后我们把每一个得到的结果给拼起来得到了Multi-head self attention最终的输出。

注意力计算的代码实现

attention代码如下这里我们用的是论文实现的简单版本。输入由query、key和value构成。

alt text

18.jpg

首先会把这个q跟k的转置进行一个矩阵相乘那这样的话就能得到一个一✖️t的向量把这个向量做一个mask。这里的mask就是把等于0的位置填充一个非常非常小的一个数负无穷的数因为负无穷的数经过Softmax这个归一化之后它就会变成0的概率目的是希望那些不重要的位置上的概率赋为0。这里的mask这里只有一个mask所以你可以理解为那它这里是一个自注意力机制的一个实现如果有两个mask那就是交叉注意力机制的实现。

得到这个p attention就概率分布之后再把这个p attention跟value进行一个加权求和得到最终自注意力机制的输出这是单个自注意力机制的一个计算逻辑如果是多头的话最终把单个的输出拼起来就好。

在Transformer模型中不同的注意力机制有着不同的QKV来源和映射方式

  1. encoder层的query key和value在编码器中都是由word embedding加上position encoding后通过三个独立的线性映射得到QKV

  2. 在decoder中self attention层同样也是通过target sentence embedding+position encoding通过三个独立的线性映射得到QKV

  3. 在交叉attention中query是由decoder的输出经过一个线性映射得到的key和value是编码器的输出memory分别经过两个映射得到

参考资料:

  1. DataWhale HapplyLLM
  2. deep_thought
  3. HandleNLP