探秘 Transformer 之(2):总体架构与文本生成执行流程

梳理 Transformer 的总体架构与执行流程:设计动机、模型结构与注意力模块,并按分词、编码、嵌入、位置编码、注意力计算到采样的顺序完整走一遍文本生成过程。

流程

使用Transformer来进行文本生成其实就是用模型来预测下一个词,完整流程包括多个阶段,如分词、向量化、计算注意力和采样,具体运作流程如下:

  • 分词(tokenize)。把用户的输入文本(此处假设是“Data visualization empowers users to”)拆解为若干独立的词汇单元,即token。
  • 编码。借助词表把token映射为数字,每个token由一个唯一的数字表示。
  • embedding(嵌入)化。embedding模块将token代表的数字转换为embedding向量,即将词映射到一个向量空间,这样LLM才能处理。此时还会加上位置编码信息,因为理解语言不仅关乎单词,还关乎单词的顺序。位置编码可以确保单词的顺序不会丢失。所有嵌入向量组合在一起形成嵌入矩阵。
  • 注意力计算。这是语境化操作,若干堆叠的Transformer Block通过注意力机制将这些Embedding向量转换成若干特征向量,构建词和词之间的关系。在注意力计算过程中,每个token可以了解自己与其它token的相关性。最终每个token流经Transformer最后一层之后得到的是一个代表语义的特征向量。
  • 计算概率。将最后一个token(”to“)对应的特征向量映射为下一个待预测词的概率分布(logits)。具体操作是通过一个线性层把特征向量升维到词表维度(即把解码器的输出转化为与词典大小相同的向量),并且通过softmax进行归一化,最终输出一个概率分布。该分布表示对词表中每个词匹配这个特征向量的概率。
  • 采样。依据这些概率,按照一定的采样规则来采样下一个token,比如选取概率最高的”visualize“作为最有可能出现的下一个单词。
  • 再次使用分词表将”visualize“对应的整数转换回原始的词汇,形成推理结果句子。
  • 不断重复上述过程。直到LLM输出结束流(EOS)标记表示解码结束或者已经生成所需数量的token。
    下图将上述流程的核心部分作了可视化,也是本篇讲解的基础,后续将对模型结构和执行流程进行逐步细化。

说明

本系列主要以下面几项为基础:

  • Transformer论文:Attention Is All You Need https://arxiv.org/abs/1706.03762v7。
  • 其它相关经典论文和精彩博客,参考将在各个篇幅的具体部分中给出。
  • “The Annotated Transformer” 博客以及其源码(后续简称为哈佛源码)。“The Annotated Transformer” 是Transformer论文的读书笔记,而且博客作者用代码实现了论文的模型,并且结合实现的模型对原始论文做了详细解读。与互联网上可以获取的其他Transformer的模型实现相比较,“The Annotated Transformer” 更适合学习和解读。其地址为:
    另外,本篇以文本翻译功能为例来进行说明。

一、总体架构

1.1 设计动机

Transformer的新颖之处在于它是一个完全基于注意力机制实现的序列转换架构,我们对Transformer的主要设计动机分析如下:

  • 解决长距离依赖关系。论文希望解决RNN在序列长距离上的限制,而注意力机制可以将序列中的任意两个位置之间的距离是缩小为一个常量,从而在长文本分析时可以捕获更多的语义关联关系。
  • 提升训练并行度。论文希望克服RNN不能并行的缺点,而注意力机制可以无视序列的先后顺序来捕捉序列间的关系,因此具有更好的并行性,符合现有的GPU框架,能够进行分布式训练,提升模型训练效率。
    因此,Jakob Uszkoreit(Transformer作者之一)提出了用自注意力机制来替换RNN对序列的编解码过程。而Noam Shazeer(Transformer作者之一)在此基础上提出了scaled dot-product attention、多头注意力和位置表示。

1.2 模型结构

首先我们来看看原始论文里面的架构图,接下来就以它为源头进行分析。

主体模块

从网络结构来分析,Transformer 包括了四个主体模块。

  • 输入模块,对应下图的绿色圈。
  • 编码器(Encoder),对应下图的蓝色圈。
  • 解码器(Decoder),对应下图的红色圈。编码器和解码器都有自己的输入和输出,编码器的输出会作为解码器输入的一部分(位于解码器的中间的橙色圈)。
  • 输出模块,对应下图的紫色圈。
    确切的说,蓝色圈是编码器层(Encoder layer),红色圈是解码器层(Decoder layer)。图中的 N×N× 代表把若干具有相同结构的层堆叠起来,这种将同一结构重复多次的分层机制就是栈。为了避免混淆,我们后续把单个层称为编码器层或解码器层,把堆叠的结果称为编码器或解码器。在Transformer论文中,Transformer使用了6层堆叠来进行学习。

多层

在Transformer中,第一层的输入是嵌入矩阵。第一层的输出随后被用作第二层的输入,依此类推。每一层都生成了一组嵌入,但这些嵌入不再直接与单个词元相关,而是与某种更复杂的词元关系的理解相关联。比如下图给出了一个模型中的第6层和第7层之间的关系,该模型每层有12个注意头。

对于多层的作用,目前也有不同的解释。比较常见的解释是分层的本质是由下往上从不同上下文中逐步构建不同层次的特征。比如底层学习单词特征,中间层学习句法特征,高层学习语义特征等,每一层做各自的事情,不会相互影响。

输入文本对应的embedding在Transformer内部各层流通时会不断演变,这个过程类似于逐层“精炼”和“抽象”输入的信息。每一层都会对输入进行不同级别的变换和抽象,都会在其输入的基础之上吸收更多上下文信息来丰富自己的表示,逐层提取出更高层次的特征,从而在综合多层之后就会获得更加强大的表达能力。随着深度学习模型进行训练,这些网络层会逐渐学习到各种范畴之间的关系和相似性,从而在推理和回答问题时能够利用这些知识。当embedding到达最后一层时,其不仅仅代表对应token独立的含义,而是具备深刻的语境信息,反应了该token与序列中其它token的综合关系。我们可以把多层加工理解为工厂的流水线,假定要生产一件瓷器,我们要先通过印坯和修坯来确定器物形状,然后通过刻花来在已经干了的坯体上刻画出各种精美的花纹或者图案。接下来进行施釉,在成型的陶瓷坯体表面施以釉浆;最后将瓷坯装入匣钵,高温入窑烧造。最终才能得到一件精美的瓷器。

针对分层中的每一层可能都会起到不同的作用这点,研究人员做了深入的研究。

论文“What Does BERT Learn about the Structure of Language?”剖析了 BERT 所理解的英语结构的复杂性。他们的研究发现,BERT 的短语表示主要在神经网络的较低层捕捉短语级别的信息,并在中间层中编码了语言要素的复杂层次结构。这个层次结构以表层特征作为基础,中间层提取语法特征,最上层呈现语义特征。

论文”Analyzing Memorization in Large Language Models through the Lens of Model Attribution”指出:

  • 较深层的注意力模块(最后25%的层)主要负责记忆。

  • 较浅层的注意力模块对模型的泛化和推理能力至关重要。

  • 在深层注意力模块应用短路(short-circuit)干预可以显著降低记忆所需内存,同时保持模型性能。
    论文”Interpreting Key Mechanisms of Factual Recall in Transformer-Based Language Models”则发现。语言模型存在一种普遍机制:防止过度自信(anti-overconfidence):在模型的最后若干层,语言模型总是在抑制正确答案的输出。这种抑制具体又分为两种:

  • 通过注意力头将输入起始位置的信息复制到了最末位置,我们发现起始位置的信息似乎包含了很多高频的token,模型可以通过这种方法来让高频token稀释残差流中的正确回答,降低回答的自信度。

  • 末层的MLP似乎在将残差流引导向一个“平均”token的方向(平均token是基于训练数据的词频,对token embedding加权平均得到的结果)。
    另外,模型的效果往往和模型的参数量成正比,Transformer就是通过增加模型的层数来加大模型可学习的参数量,让更多的参数来承载文本中深层次的信息。

1.3 注意力模块

注意力机制是Transformer模型的心脏,它赋予模型洞察句子中每个单词与其它单词间错综复杂关系的超能力。

分类

在Transformer中有三种注意力结构:全局自注意力,掩码自注意力和交叉注意力,具体如下图所示。交叉注意力主要用于处理两个不同序列之间的关系;全局自注意力主要用于处理单个序列内元素之间的关系;掩码自注意力(也被称做因果自注意力)通过掩码来控制模型在计算注意力分数时的关注范围,从而确保在解码时不会受到未来信息的影响。

位置

三种注意力模块在Transformer网络对应位置如下图所示。

作用

Transformer实际上是通过三重注意力机制建立起了序列内部以及序列之间的全局联系。论文中对这三种注意力作用的解释如下图所示。

我们具体分析下这三种注意力。

全局自注意力层

全局自注意力层(Global self attention layer)位于编码器中,它负责处理整个输入序列。在全局自注意力机制中,序列中的每个元素都可以直接访问序列中的其它元素,从而与序列中的其他元素建立动态的关联,这样可以使模型更好地捕捉序列中的重要信息。自注意力的意思就是关注于序列内部关系的注意力机制,那么是如何实现让模型关注序列内部之间的关系呢?自注意力将query、key、value设置成相同的东西,都是输入的序列,就是让注意力机制在序列的本身中寻找关系,注意到不同部分之间的相关性。

对于全局自注意力来说,Q、K、V有如下可能:

  • Q、K、V都是输入序列。
  • Q、K、V都来自编码器中前一层的输出。编码器中的每个位置都可以关注编码器前一层输出的所有位置。
    再细化来说,Q是序列中当前位置的词向量,K和V是序列中的所有位置的词向量。

掩码自注意力

掩码自注意力层或者说因果自注意力层(Causal attention layer)可以在解码阶段捕获当前词与已经解码的词之间的关联。它是对解码器的输入序列执行类似全局自注意力层的工作,但是又有不同之处。

Transformer是自回归模型,它逐个生成文本,然后将当前输出文本附加到之前输入上变成新的输入,后续的输出依赖于前面的输出词,具备因果关系。这种串行操作会极大影响训练模型的时间。为了并行提速,人们引入了掩码,这样在计算注意力时,通过掩码可以确保后面的词不会参与前面词的计算。

对于掩码自注意力来说,Q、K、V有如下可能:

  • Q、K、V都是解码器的输入序列。
  • Q、K、V都来自解码器中前一层的输出。解码器中的每个位置都可以关注解码器前一层的所有位置。
    再细化来说,Q是序列中当前位置的词向量,K和V是序列中的所有位置的词向量。

交叉注意力层

交叉注意力层(Cross attention layer)其实就是传统的注意力机制。交叉注意力层位于解码器中,但是其连接了编码器和解码器,这样可以刻画输入序列和输出序列之间的全局依赖关系,完成输入和输出序列之间的对齐。因此它需要将目标序列作为Q,将上下文序列作为K和V。

对于交叉注意力来说,Q、K、V来自如下:

  • Q来自前一个解码器层,是因果注意力层的输出向量。
  • K和V来自编码器输出的注意力向量。
    这使得解码器中的每个位置都能关注输入序列中的所有位置。另外,编码器并非只传递最后一步的隐状态,而是把所有时刻(对应每个位置)产生的所有隐状态都传给解码器,这就解决了中间语义编码上下文的长度是固定的问题。

或者从另一个角度来理解,交叉注意力是序列到序列模式;双向自注意力是自编码模式;单向自注意力是自回归模式。

1.4 执行流程

我们再来结合模型结构图来简述推理阶段的计算流程,具体如下图所示。

假设我们进行机器翻译工作,把中文”我吃了一个苹果“翻译成英文”I ate an apple“,在假设模型只有一层,执行步骤如下:

  1. 处理输入。用户输入自然语言句子”我吃了一个苹果“;tokenizer先把序列转换成token序列;然后Input Embedding层对每个token进行embedding编码,再加入Positional Encoding(位置编码),最终形成带有位置信息的embedding编码矩阵。编码矩阵用 Xn∗d 表示, n 是句子中单词个数,d 是表示向量的维度(论文中 d=512)。注:原论文图上的输入是token,本篇为了更好的说明,把输入设置为自然语言句子。
  2. 编码器进行编码。编码矩阵首先进入MHA(Multi-Head Attention,多头注意力)模块,在这里每个token会依据一定权重把自己的信息和其它token的信息进行交换融合;融合结果会进入FFN(Feed Forward Network)模块做进一步处理,最终得到整个句子的数学表示,句子中每个字都会带上其它字的信息。整个句子的数学表示就是Encoder的输出。
  3. 通过输入翻译开始符来启动解码器。
  4. 解码器进行解码。解码器首先进入Masked Multi-Head Attention模块,在这里解码器的输入序列会进行内部信息交换;然后在Multi-Head Attention模块中,解码器把自己的输入序列和编码器的输出进行融合转换,最终输出一个概率分布,表示词表中每个单词作为下一个输出单词的概率;最终依据某种策略输出一个最可能的单词。这里会预测出第一个单词”I“。
  5. 把预测出的第一个单词”I“和一起作为解码器的输入,进行再次解码。
  6. 解码器预测出第二个单词”ate“。
    针对本例,解码器的每一步输入和输出具体如下表所示。

1.6 小结

Transformer总体架构是一个有机整体,难以分割。组合的意义不在于构成它的基本单元,而在于这些单元之间形成的复杂关系和涌现的行为。比如集体智能来自个体的组合,却产生了所有个体都不具备的高阶能力。

有些工作就将焦点转移到 transformer 模块的高级架构上,并认为其完整结构,而不仅仅是标记混合注意力操作,对Transformer实现具有竞争力的性能至关重要。

论文”Attention is not all you need”指出如果没有skip connection(residual connection-残差链接)和MLP,自注意力网络的输出会朝着一个rank-1的矩阵收缩。即,skip connection和MLP可以很好地阻止自注意力网络的这种”秩坍塌(秩坍塌)退化“。这揭示了skip connection,MLP对self-attention的不可或缺的作用;

论文”MetaFormer is Actually What You Need for Vision“则描述了一种通用架构,在该结构中,输入首先经过embedding,得到 𝑋。然后embedding送入重复的blocks中,第一个block主要包含了token mixer,使得不同的token能够相互信息通信(Y = TokenMixer(Norm(X)) + X,);第二个block包含两层MLP。该架构通过指定token mixer的具体设计,可以获得不同的模型。如果将token mixer指定为注意力或spatial MLP,则MetaFormer将分别成为一个transformer或类似MLP的模型。

二、构建

我们接下来结合哈佛源码进行分析和学习。哈佛代码中的make_model()函数是Transformer模型的构建函数。

2.1 参数

make_model()函数的参数有如下7个:

  • src_vocab:源语言词表中单词数目,即源词典的大小。
  • tgt_vocab:目标语言词表中单词数目,即目标词典的大小。
  • N=6:编码器和解码器堆叠数,即编码器层数和解码器层数。
  • d_model=512:模型所处理数据的维度,即词向量(word embedding)的大小。
  • d_ff=2048:FFN(前馈全连接层)中变换矩阵的维度,即隐层神经元的数量。
  • head=8:多头注意力层中的注意力头数。
  • dropout=0.1:防止过拟合。

    2.2 构建逻辑

make_model()函数的主要思路就是用从小到大搭建积木的方式来构建Transformer。我们先脱离代码来构思下,看看架构图上的哪些模块可以作为积木。

  • 输入模块:Input Embedding和Positional Encoding分别可以作为单独的积木块,它们结合在一起又可以作为一个新的积木块。
  • 编码器层和解码器层可以作为两个单独的大积木块。其内部的Masked Multi-Head Attention、Multi-Head Attention、Feed Forward和Add & Norm也都可以作为单独的小积木块。
  • 输出模块:Linear和Softmax分别可以作为单独的积木块,它们结合在一起又可以作为一个新的积木块。
    有了这些积木块,我们就可以很容易的构建起Tranformer了。当然,在make_model()函数中做了一定的抽象,有些小积木块被用某些类进行了封装(比如Linear和Softmax被封装在Generator类中,细节没有在make_model()函数中展示出来)。我们把这些模块和代码中一一对应起来看,下图中的数字代表代码中某模块出现的顺序,这些数字在下面代码的注释中也有标明。

具体的代码逻辑如下。

  • 把copy.deepcopy()这个深度拷贝函数重新命名为c,这样后续代码会比较简洁。deepcopy()函数会开辟一个新内存并将源实例完全复制过来,复制过来的对象和源对象没有任何关联。后续各种类的构造函数中会调用copy.deepcopy()来重新生成一个对应实例,比如上面图中的标号1,2,3在解码器端就分别被做了深度拷贝。这样,两个标号1对应的实例彼此之间相互独立,不受干扰。
  • 构建 MultiHeadedAttention,PositionwiseFeedForward 和 PositionalEncoding 对象。
  • 构造 EncoderDecoder 对象,这是Transformer主体类,其参数是Encoder、Decoder、src-embed、tgt-embed和 Generator。我们先分析后面三个参数。
  • Encoder和Decoder两个类很像,我们以Encoder为例来说明。Encoder由N个EncoderLayer构成,EncoderLayer的参数是d_model, c(attn), c(ff), dropout,即word embedding维度、多头注意力、FFN层和Dropout。可以看到,Encoder和Decoder类中的注意力都是MultiHeadedAttention的实例,只是因为传递参数的不同,才决定某个注意力是交叉注意力还是掩码多头注意力。
  • 初始化模型参数。Xavier初始化可以参考论文”Understanding the difficulty of training deep feedforward neural networks”。
    具体代码如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def make_model(src_vocab, tgt_vocab, N=6, d_model=512, d_ff=2048, h=8, dropout=0.1):
"Helper: Construct a model from hyperparameters."
# copy.deepcopy是深度拷贝函数,即重新生成一个新实例。重新命名可以让后续代码比较简洁
c = copy.deepcopy
# 构建多头注意力层的实例,对应上图的数字标号1
attn = MultiHeadedAttention(h, d_model)
# 构建前馈神经网络层的实例,对应上图的数字标号2
ff = PositionwiseFeedForward(d_model, d_ff, dropout)
# 构建位置编码模块的实例,对应上图的数字标号3
position = PositionalEncoding(d_model, dropout)
# 总的Transformer模型
model = EncoderDecoder(
# EncoderLayer只包含一个Attention层,对应上图的数字标号4。Encoder则包括外面的N
Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N),
# DecoderLayer包含两个Attention层,对应上图的数字标号5,Decoder则包括外面的N
Decoder(DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout), N),
# 输入的Embedding和位置编码,Embeddings对应上图的数字标号6,Sequential就是两个编码合并的结果,对应上图的数字标号7
nn.Sequential(Embeddings(d_model, src_vocab), c(position)),
# 输出的Embedding和位置编码,Embeddings对应上图的数字标号8,Sequential就是两个编码合并的结果,对应上图的数字标号9
nn.Sequential(Embeddings(d_model, tgt_vocab), c(position)),
# Generator类包括Linear层和Softmax层,对应上图的数字标号10,负责依据Decoder的输出来预测下一个token
Generator(d_model, tgt_vocab),
)

# This was important from their code.
# Initialize parameters with Glorot / fan_avg.
# 初始化模型参数,这里采用xavier初始化,即如果参数的维度大于1,则将其初始化成一个服从均匀分布的矩阵
for p in model.parameters():
if p.dim() > 1:
nn.init.xavier_uniform_(p)
return model

2.3 主体类

EncoderDecoder类就是基于Transformer架构的编码器-解码器实现,其成员变量如下:

  • encoder:Encoder类实例,这是编码器的实现。

  • decoder:Decoder类实例,这是解码器的实现。

  • src_embed:源语言的word embedding生成模块,是一个nn.Sequential对象,包括Embebddings和PositionalEncoding。src_embed将对输入进行Embedding和位置编码

  • tgt_embed:目标语言的word embedding生成模块,是一个nn.Sequential对象,包括Embebddings和PositionalEncoding。tgt_embed将对再传入的输出进行Embedding和位置编码

  • generator:Generator类的对象,包括Linear层和Softmax层,负责对Decoder的输出做预测,即依据Decoder的隐状态输出来预测当前时刻的词。隐状态会输入到全连接层(全连接层的输出大小是词典的大小),全连接层会接上一个softmax得到预测词的概率。
    EncoderDecoder类的forward()函数完成了编码和解码的工作,它接受四个函数:

  • src:源序列,其内容是token在词表对应的编号。src的形状是[batch_size, seq_len],举例是[[ 0, 2, 4, 8, 1, 2, 2 ]] ,即批量大小为1,句子长度是7,其中0为bos,1为eos,2为pad。

  • tgt:目标序列,具体含义类似src。

  • src_mask:源序列掩码,具体作用是对填充符号进行掩码。以[[ 0, 2, 4, 8, 1, 2, 2 ]]为例,其掩码是[[True,True,True,True,True,False,False]],即对两个填充的pad进行掩码。

  • tgt_mask:目标序列掩码,其作用有两种:不让注意力计算看到未来的单词;对填充符号进行掩码。其形状是[batch_size, seq_len, seq_len],上面对应的掩码如下:

    1
    2
    3
    4
    5
    6
    7
    [True,False,False,False,False,False,False],
    [True,True,False,False,False,False,False],
    [True,True,True,False,False,False,False],
    [True,True,True,True,False,False,False],
    [True,True,True,True,True,False,False], # 例句是5个正式token,后面两个pad被置为False
    [True,True,True,True,True,False,False],
    [True,True,True,True,True,False,False],

EncoderDecoder代码具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 继承nn.Module
class EncoderDecoder(nn.Module):
"""
A standard Encoder-Decoder architecture. Base for this and many other models.
标准的编码器-解码器架构,这是很多模型的基础。
"""

def __init__(self, encoder, decoder, src_embed, tgt_embed, generator):
"""
初始化函数有5个参数,从外部传入参数的目的是更加灵活,可以更换组件
"""
super(EncoderDecoder, self).__init__()
self.encoder = encoder # 编码器对象
self.decoder = decoder # 解码器对象
# 源语言input embedding和position embedding的组合
self.src_embed = src_embed
# 目标语言output embedding和position embedding的组合
self.tgt_embed = tgt_embed
self.generator = generator # 类别生成器对象

def forward(self, src, tgt, src_mask, tgt_mask):
# 前向传播函数有四个参数:源序列,目标序列,源序列掩码,目标序列掩码
"Take in and process masked src and target sequences."
# 1. 将source, source_mask传入编码函数encode(),让编码器对源序列进行编码,得到编码结果memory
# 2. 将memory,source_mask,target,target_mask一同传给解码函数decode()进行解码
return self.decode(self.encode(src, src_mask), src_mask, tgt, tgt_mask)

# 编码函数,接受参数是源序列和掩码
def encode(self, src, src_mask):
# 1. 对src编码,得到input embedding
# 2. 计算位置编码,将input embedding和位置编码相加,得到word embedding
# 3. 使用编码器encoder进行编码,编码结果记作memory
return self.encoder(self.src_embed(src), src_mask)

# 解码函数,参数为:编码器输出(memory)、源序列掩码、目标序列和目标序列掩码
def decode(self, memory, src_mask, tgt, tgt_mask):
# 1. 对tgt编码,得到得到input embedding
# 2. 计算位置编码,将input embedding和位置编码相加,得到word embedding
# 3. 使用编码器decoder进行解码,解码器输出可以使用self.generator进行最后的预测
return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)

2.4 如何调用

模型调用的方式如下面代码所示,forward()函数负责把输入编码成隐状态,然后把隐状态解码称输出logits(对数几率)。其参数都从batch类的实例中获取,具体如下:

  • src:源句子列表。形状是[batch size, max sequence length]。每个句子是从数据集提取出来,经过词典处理过的,举例为:[ 0, 5, 12,…, 1, 2, 2]。其中0为bos,1为eos,2为pad。
  • tgt:目标句子列表。具体意义同上。
  • src_mask:注意力层要用的掩码(后面章节会详细分析)。
  • tgt_mask:解码器的掩码自注意力层要用的掩码(后面章节会详细分析)。
    1
    out = model.forward(batch.src, batch.tgt, batch.src_mask, batch.tgt_mask

三、输入

3.1 输入分类

我们还是用把中文”我吃了一个苹果“翻译成英文”I ate an apple“为例。模型的总体输入是若干个单词组成的句子,不同模型支持的句子最大长度不同,如果句子较短,则会用某些特殊词填充多余的位置。而源语言和目标语言(或者说编码器和解码器)分别对应了两种不同的独立的输入。

  • 编码器的输入对应图上的Inputs,这是从原始的源序列文本得到的token列表。在机器翻译业务中,Encoder是一次性接受一个完整的句子,然后进行处理。比如”我吃了一个苹果“这几个字经过tokenizer之后,得到了每个字对应的唯一序号,假设序号为[5, 4, 6,7, 8, 9, 10],然后会把[5, 4, 6,7, 8, 9, 10]作为inputs传给Encoder层。
  • 解码器的输入其实有两种:
    具体如下图所示。

3.2 输入模块

输入模块具体包括如下:

  • Tokenizer(词元分析器)。注:原论文图上的输入是token,本篇为了更好的说明,把输入设置为自然语言句子,也加入了tokenizer。
  • 源语言文本嵌入层(对应图上的 Input Embedding)和位置编码器(对应图上的Positional Encoding)。
  • 目标语言文本嵌入层(对应图上的Output Embedding)和位置编码器(对应图上的Positional Encoding)。

3.3 文字转换

回到本篇最开始图1的例子。“Data visualization empowers users to”这个句子并不能被模型理解,因此我们需要把自然语言进行编码,也就是对文字进行向量化。具体分为以下几个步骤:

  • 对输入文字进行tokenize(分词),得到token。
  • 在词表(假设词表大小是10000)中找到每个token对应的token id。
  • 把每个token id转化成一个token embedding(假设维度是512)。
  • 给序列中的每个位置添加一个位置编码。
  • 把input embedding和位置编码相加,得到最终的word embedding。
    具体流程如下。

让我们看看每个步骤是如何完成的。

分词

分词是将输入文本分解为更小、更易于管理的语义片段的过程,这些片段被成为token。token是模型词汇表的一部分,词汇表是LLM在训练时使用的词元列表。tokenizer(分词器)会做两件事:

  • 首先,tokenizer会将输入文本切分为更小、更易于管理的token。token可以是单词或子词(sub-word),比如单词"Data" 就被映射成token,而单词 "empowers" 则被分为两个token:“em”和“powers”。
  • 接下来,tokenizer会把token映射成不同的整数,这些整数就是词表的索引。可以认为这是一种one-hot编码形式。

embedding化

我们接下来看看在NLP领域中,词嵌入向量生成过程,即如何把单词在词表里面的索引转换为一个Transformer可以使用的向量,也就是embedding(嵌入)。embedding是每个词元的固定向量表示,它比纯整数更适合深度学习,因为它捕捉到了单词的语义意义。embedding向量的大小取决于模型维度。当输入中每个token的embedding堆叠在一起时,它们构成了输入的嵌入矩阵。

在Transformer架构图中,Inputs和Outputs的上面分别有一个Embedding模块,每个模块都是由两个子模块组合而成。

  • Inputs相关的embedding模块包括Input Embedding和Positional Encoding。
  • Outputs相关的embedding模块包括Output Embedding + Positional Encoding:与上面类似,不再赘述。

Token Embedding

Token Embedding会将文字转换成模型可以理解和处理的数学表示,即将每个token id(one-hot)和一个高维向量相关联,向量的每个维度对应语义的某个方面。embedding通常是查表操作,即根据token_id的值,去embedding矩阵中查找第token_id行的数据作为embedding。向量的维度取决于模型,Transformer论文将每个token表示为512维向量。Token Embedding也叫word embedding。

Positional Encoding

一个句子中的文字先后顺序很重要,比如以下两句话的文字完全相同,但因为文字顺序不同,其语义完全不同。

  • 买一张从上海到北京的车票。
  • 买一张从北京到上海的车票。
    因此,模型还需要对输入句子中每个token的位置信息进行编码。

Word Embedding

最后,模型将token embedding和Positional Encoding相加,得到最终的嵌入表示。这种组合表示捕获了标记的语义及其在输入序列中的位置。在 LLM(大型语言模型)中,将 word embedding(词嵌入)加上位置编码后的结果通常仍可以称为 “输入嵌入(Input Embeddings)” 或 “带位置信息的词嵌入”。为了讲述方便,后续我们依然称之为Word Emebdding 。

本阶段输出张量的形状是[batch size, sequence length,embedding dimension]。以“Data visualization empowers users to”为例,5个单词被切分成6个token,最终得到的word embedding是一个[batch size, sequence length, embedding dimension]的矩阵。因为只有一句话,所以batch size是1,sequence length是6,假设embedding dimension是512,则矩阵维度是[1,6,512]。

实际上,我们可以认为embedding是LLM自己的语言系统(包括文本信息特征空间与位置信息特征空间)。输入层的作用就是把自然语言、程序语言、视觉听觉语言等信息都映射(或者叫编码)到这个高维的语言空间中。接下来会通过注意力机制从高维语言空间中提取各种丰富的知识和结构,加权积累与关联生成自己的语言,最后再“编码”回人类的语言。

四、Transformer Layer

Transformer处理的核心在于Transformer块,它包括多头注意力机制和多层感知器层。大多数模型由多个这样的块组成,这些块一个接一个地顺序堆叠。

从Transformer的构造代码可以看出来,Encoder类实例是由N个EncoderLayer类实例构建而成,Decoder类实例由N个DecoderLayer类实例构建而成,这和论文相符合。

1
2
3
4
# EncoderLayer只包含一个注意力层,Encoder则包括外面的N参数
Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N),
# DecoderLayer包含两个注意力层,Decoder则包括外面的N参数
Decoder(DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout), N),

EncoderLayer和DecoderLayer是基础构建块,每个块主要包括如下:

  • 多头注意力机制。它允许token与其他token进行通信,彼此交流信息,从而捕获上下文信息和单词之间的关系。
  • FFN层。一个对每个token 独立运行的前馈网络。注意力层的目标是在 token 之间路由信息,而 MLP 的目标是细化每个 token 的表示。
    本小节先概要介绍多头自注意力机制和FFN层,后续篇幅会对EncoderLayer和DecoderLayer进行详细介绍。

4.1 多头自注意力机制

自注意力机制使模型能够专注于输入序列的相关部分,从而使其能够捕获数据内的复杂关系和依赖关系。多头注意力机制借鉴了CNN中multi-kernel 的思想,对不同头使用不同的线性变换。多头自注意力机制本质上是构造多个子空间,在这些子空间上再构建多个注意力来替代单个注意力,这样可以获取更多维度的信息和相互关系。自注意力机制是LLM架构中唯一计算序列中词元间关系的地方,因此它构成了语言理解的核心,涵盖了对词汇关系的理解。

让我们看看多头自注意力机制如何计算。为了简化说明,这里假设编码器也用到了掩码,对于某些复合操作也分解说明。

第 1 步:根据原始嵌入计算查询、键和值矩阵

自注意力机制的输入是n_tokens x n_embd的嵌入矩阵,其中每一行或向量表示一个独立的词元。LLM计算的第一部分从词元嵌入矩阵中提取每个词元的相关行。每个 token 的word embedding被转换为三个不同的向量,分别称为query、 key和 value。这些向量是通过将输入嵌入矩阵与学习到的$W^Q$、 $W^K$和 $W^V$权重矩阵相乘(这些矩阵是模型参数的一部分)得出的。query和key的点积被用来来判断两个向量之间的相似性,这也是论文中提到的点积注意力。

我们用淘宝搜索来类比,可以帮助我们对这些矩阵有更好的理解。假如我们在淘宝上进搜索”李宁鞋“。

  • query是你在搜索栏输入的查询内容。
  • key是在页面上返回的商品描述、标题,其实就是淘宝商品数据库中与候选商品相关的关键字。
  • value是李宁鞋商品本身。因为一旦依据搜索词(query)搜到了匹配的商品描述、标题(key),我们就希望具体看看商品内容。
    通过使用这些 QKV 值,模型可以计算注意力分数,从而确定每个token在生成预测时应从其它token那里获得多少关注。

第 2 步:掩码自我注意力

掩码自注意力允许模型通过关注输入的相关部分来生成序列,同时防止访问未来的token。下图展示了如何使用查询、键和值矩阵来计算掩码自注意力。

图上具体分为三步:

  • 使用点积来计算注意力分数。使用Q和K矩阵的点积来决定了每个Q与每个K的对齐程度,点积结果是一个反映所有输入token之间关系的矩阵。
  • 对注意力分数施加scaling(缩放)和掩码。首先对注意力分数进行缩放,其次将掩码应用于注意力矩阵的上三角,以防止模型访问未来的标记,因为模型需要学习如何在不“窥视”未来的情况下来预测下一个token。
  • 施加Softmax和dropout操作。注意力分数会通过 softmax 运算转换为概率,然后会施加dropout操作来随机丢弃一些元素。
    此时得到的结果是注意力权重。

第3步:拼接

使用第二步产生的权重和V矩阵进行相乘以获得自注意力机制的最终输出,因为是多头注意力的结果,所以需要把这些头的输出进行拼接并且通过线性层来融合。

4.2 FFN层

在多个自注意力头捕获输入token之间的不同关系后,拼接的输出将通过FFN(feed-forward network)层进行处理,以增强模型的表示能力。下图展示了如何使用 FFN层将自注意力表示投影到更高的维度,以增强模型的表示能力。

FFN层由两个线性变换组成,线性变换 中间有一个激活函数。第一个线性变换将输入的维度从512 增加到四倍2048。第二个线性变换将维度降回到 的原始大小512,以确保后续层接收到维度一致的输入。

与自注意力机制在序列中彼此交流不同,FFN对序列中每个元素都独立计算,因此不会进行元素间的信息交换(元素间的互动完全靠自注意力)。这样有助于在注意力层进行元素间的信息交换之后,让每个元素消化整合自己的信息,为下一层再次通过自注意力交换信息做好准备。

4.3 辅助架构

除了上述主要模块之外,Transformer模型中还应用了LayerNorm(层归一化)和ResNet(残差连接)等设计方法。虽然在上面源码中的构造函数中没有提及,但这些模块对提高模型的整体表示能力非常重要。

LayerNorm

LayerNorm有助于稳定训练过程并提高收敛性。它的工作原理是对输入的各个特征进行归一化,确保激活的均值和方差一致。普遍认为这种归一化有助于缓解与内部协变量偏移相关的问题,使模型能够更有效地学习并降低对初始权重的敏感性。从架构图上看,LayerNorm在每个Transformer 块中应用两次,一次在自注意力机制之后,一次在FFN层之后,但是在实际工作中不一定如此。

Dropout

Dropout 是一种正则化技术,用于通过在训练期间随机将一部分模型权重设置为零来防止过拟合。这鼓励模型学习更强大的特征并减少对特定神经元的依赖,帮助网络更好地泛化新的、未见过的数据。

残差连接

残差连接(Residual Connection)于 2015 年首次由何凯明大神在ResNet论文中引入。残差连接就是把网络的输入和输出相加,得到网络的新输出为F(x)+x。其本质思想是允许网络中的信息和梯度直接跨过一个或多个层进行传播,这样能够保留原始的一些信息。这种架构创新可以有助于缓解梯度消失问题,从而训练更深的神经网络,彻底改变了深度学习Transformer论文中,残差连接在每个 Transformer 块内使用两次:一次在FFN之前,一次在 FFN之后。

五、概率输出(Output Probabilities)

经过一系列Transformer模块的精心处理,输入数据最终来到了它的归宿——最后的输出层。输出模块包括两部分:线性层和softmax层。哈佛代码用Generator类对这两部分进行了封装,将模型输出的 embeding 转换为对下一个词的预测(在整体词表上的概率分布)。

5.1 解码器结果

输入经过所有解码块处理后,最终输出依然是若干token对应的向量,其代表在Transformer视角下的,用高维概率向量编织起来事物之间的各种复杂关系。这些关系在本质上就是范畴论概念下事物的米田嵌入(米田嵌入采用对象的所有关系来表征该对象)。Transformer 学习的过程,是核函数选择与参数化的过程,也是寻找米田嵌入的过程:提取对象的所有关系,形成其关系图 – 即概率化的内部世界模型。

我们首先看看哈佛代码。在推理时,generator使用的并不是编码器的所有输出,而是最后一个token对应的向量out[:, -1],即只使用输出序列中最后一个单词的猜测结果。训练时,generator则使用编码器的全部输出。下面是推理代码示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def inference_test():
test_model = make_model(11, 11, 2)
test_model.eval()
src = torch.LongTensor([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]])
src_mask = torch.ones(1, 1, 10)

memory = test_model.encode(src, src_mask)
ys = torch.zeros(1, 1).type_as(src)

for i in range(9):
out = test_model.decode(
memory, src_mask, ys, subsequent_mask(ys.size(1)).type_as(src.data)
)
prob = test_model.generator(out[:, -1])
_, next_word = torch.max(prob, dim=1)
next_word = next_word.data[0]
ys = torch.cat(
[ys, torch.empty(1, 1).type_as(src.data).fill_(next_word)], dim=1
)

下面是训练代码示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class SimpleLossCompute:
"A simple loss compute and train function."

def __init__(self, generator, criterion):
self.generator = generator
self.criterion = criterion

def __call__(self, x, y, norm):
x = self.generator(x)
sloss = (
self.criterion(
x.contiguous().view(-1, x.size(-1)), y.contiguous().view(-1)
)
/ norm
)
return sloss.data * norm, sloss

5.2 转换

Transformer的输出是最有可能放在输入序列末尾的单词。但输出的最后一个token对应的向量是512维的向量,无法直接用来推理。另外,词表中每个单词都有可能成为下一个单词, 所以模型需要对所有它知道的单词均按可能性打分,最终选出其中最合适的单词推荐给用户。因此我们最终需要得到词表中所有单词作为下一个单词的概率。这需要经历几个阶段才能完成从512维向量得到10000个单词概率的转换。

  • 在 “Embedding”部分,我们已经学习了一种映射,它可以将给定的(one-hot)单词转换为 512 向量,然后才能被Transformer处理。现在编码器-解码器已经处理结束,所以需要反转这个映射(求逆),将输出的 512 向量转换回词表对应的10000 维度空间中。可以理解为,对于最后一个token向量来说有一个分类任务,需要把该向量分类到词表中的正确token,即下一个token的分类。Transformer通过一个线性层达到了这个目的,从向量维度投影到词表长度([1,embed_size]->[1,vocab_size])。其中词汇表中的每个token都有一个对应的值,称为 logit。该过程类似CNN中,卷积层之后再接一个线性层做分类。
  • 因为是要预测,所以需要根据模型的输出 logits 为词汇表中的每个token分配一个概率。这些概率决定了每个token成为序列中下一个单词的可能性。具体操作是应用 softmax 函数将 logits 转换为总和为 1 的概率分布。
    下图中的Output Probabilities就是线性层后经过Softmax的概率分布,红圈对应就是Generator类。后续会根据这些标记成为下一个单词的可能性对其进行采样。比如我们可以简单地根据对它们进行排名。从中得到最大的值对应的index,然后再去字典中查询,就知道预测的下一个词是什么了,即得到了下一个词在词典中的编号。

简而言之,这最后的线性层和softmax函数共同作用,为模型预测下一个词提供了数学基础和概率指导。

在后续的文章中,我们会逐步深入Transformer的各个组成模块。

参考

interpreting GPT: the logit lens nostalgebraist

A Mathematical Framework for Transformer Circuits (Anthropic blog 2021)

A Mathematical Perspective On Transformers

A PRIMER ON THE INNER WORKINGS OF TRANSFORMER-BASED LANGUAGE MODELS

Analyzing Transformers in Embedding Space

ASK, AND IT SHALL BE GIVEN: TURING COMPLETENESS OF PROMPTING

Backward Lens: Projecting Language Model Gradients into the Vocabulary Space

Chain of Thought Empowers Transformers to Solve Inherently Serial Problems

Differential Transformer

](https://arxiv.org/pdf/2410.05258)

DIFFERENTIAL TRANSFORMER

EMNLP 2024最佳论文:从反向传播矩阵来理解Transformer的运作机制 PaperWeekly

Finding Neurons in a Haystack: Case Studies with Sparse

Four types of emergence: a typology of complexity and its implications for a science of management

Full Stack Transformer Inference Optimization Season 2: Deploying Long-Context Models)Yao Fu | Website | Blog | Twitter / X

GPT4技术原理五:大模型的幻觉,解铃还须系铃人 王庆法 清熙

Grothendieck Graph Neural Networks Framework: An Algebraic Platform for Crafting Topology-Aware GNNs

Jawahar, Ganesh, et al. “What Does BERT Learn about the Structure of Language?” ACL 2019

Let’s Think Dot by Dot: Hidden Computation in Transformer Language Models

LLM CoT的工作原理 王庆法 清熙

LLM的Prompt竟然是图灵完备的?LLM提示范式的第一个研究 | 重磅 AI修猫Prompt

MetaFormer is Actually What You Need for Vision

More About Attention 李新春

Neural Ordinary Differential Equations

nGPT: Normalized Transformer with Representation Learning on the Hypersphere

Reformer 模型 - 突破语言建模的极限 Hugging Face 博客

softmax is not enough

State-Free Inference of State-Space Models:The Transfer Function Approach

THE ASYMPTOTIC BEHAVIOR OF ATTENTION IN TRANSFORMERS

The Platonic Representation Hypothesis

TRANSFORMER EXPLAINER: Interactive Learning of Text-Generative Models

transformer 模型结构详解及实现 zhang

Transformers Are Secretly Collectives of Spin Systems mcbal

Transformer在生物学上是否合理?MIT团队用神经元和星形胶质细胞来构建 ScienceAI 药物分子设计

Transformer模型‌(上篇) OnlyInfo

Transformer模型‌(下篇) OnlyInfo

Transformer的物理原理 Matthias Bal 清熙

Understanding and Improving Transformer From a Multi-Particle Dynamic System Point of View

Understanding how LLM inference works with llama.cpp omrimallis

Wavelets based physics informed neural networks to solve non-linear differential equations

【官方双语】直观解释注意力机制,Transformer的核心 | 【深度学习第6章】 3Blue1Brown

万字长文介绍为大语言模型建立的“语言、统计和范畴”数学框架 Tai-Danae Bradley 编译:王庆法

大脑里也有个Transformer!和「海马体」机制相同 人工智能与算法学习

大语言模型背后的神经科学机制 雅牧

打开黑匣子的神器来了!Transformer Explainer让Transformer模型透明化 小智 智驻未来

探索AGI系列 | 番外01. (全新视角理解)Transformer和大脑新皮质的一致性 MetaUniTech

智源论坛 | 王立威:从经验性的积累到理论空白的弥补 北京智源人工智能研究院

浅谈LLM mechanistic interpretability的几个流派(一) 时间旅客

理解llama.cpp怎么完成大模型推理的 hugulas

神经网络理论研究的物理学思想 黄海平 [现代物理知识杂志]

范畴的相变与知识的形成

解读小模型——SLM 半吊子全栈工匠 喔家ArchiSelf

论文阅读:Differentical Transformer 差分Transformer Eddie

降低大模型幻觉的必由之路 清熙

https://poloclub.github.io/transformer-explainer

本文结束 感谢您的阅读