大语言模型是怎么工作的?浅析 Transformer 架构

· · 科技·工程

浅析 Transformer 架构

前言

不知道从什么时候起,我已经用 AI 帮我做了很多事情了 大多数是用 AI 糊弄学校的作业。彼时的我和大多数人一样,完全不理解 AI 到底是怎么工作的,觉得这个东西是一个玄而又玄的技术。

就在这几天,我决定要对 AI Agent 方向进行学习探索,于是第一个接触的内容就是 Transformer 架构。网络上很多深入浅出的教程给了我很大的启发,其实 LLM 的基本原理没有那么高端复杂。

所以本文从一个刚刚接触 AI Agent 方向的小白的角度,阐述 Transformer 的主要基本原理。一方面供自己温习,一方面也可能可以帮助其他想要入门的人。

万字长文警告。

正文

从 LLM 开始

LLM 是大型语言模型(英语:large language model)的缩写,简称大模型。其中,“大型”代表其具有庞大的参数量;“语言模型”代表其专为自然语言处理任务而设计,尤其适用于语言生成。平日我们对话的大部分聊天 AI 都属于 LLM。

LLM 的一大用处就是补全文本。譬如你给一个 LLM 丢一份只写了开头的句子,它可以顺利地补全句子,甚至可以不断继续生成下去,组成一篇文章。你可能会好奇它到底是怎么工作的,其实 LLM 的核心思想非常简单:LLM 通过已有的文字,预测下一个词(准确来说是 token)的概率分布,来实现生成补全。

例如,你输入“今天天气真”,LLM 会计算所有可能的下一个词的概率。可能的概率分布长这样:

概率
30%
不错 20%
10%

LLM 会根据概率和一定的策略选择一个词,补充在句子后面。然后根据补充的句子继续预测下一个词,如此循环,一个词一个词地生成完整的内容。这个机制叫做自回归生成(Autoregressive Generation)

听起来好像很简单,实际上也确实是很容易理解。但是困难的点在于,LLM 如何计算下一个词的概率?如何才能保证 LLM 计算的概率分布符合我们所知的语法、事实、推理甚至世界知识?这就是我们接下来要介绍的东西。

概念:词嵌入

词嵌入(Word Embedding) 是 LLM 的一个核心基础。

想一想,如果是人类来补全“今天天气真”这句话,人类会怎么思考?显然,我们也会补充出例如“好”、“不错”、“糟糕”、“热”、“凉爽”之类的词语。我们之所以会认为这些填法都是合理的,是因为这些词都有一些相似之处:都是形容词,且都能形容天气。

人类能理解这种词语间关系,是不断学习应用的结果。但计算机本身读不懂文字,它只读得懂数字。所以,如果我们要让“模型”也能理解词语间的关系,就需要量化每个词语。

词嵌入的本质就是将文字转化为「数字坐标」,所有的词都放置在一个巨大的语义地图上,每个词都是地图上的一个点。语义相近的词在数学空间上的距离也相近。

例如:

所以,词嵌入本质就是在给每个词一个精确的坐标,让模型看得懂这些关系。

为了更精确的表示这些关系,地图的维度不会只是简单的二维或三维,而是用更多更大的维度,比如 768 维、4096 维,这样一个词语的坐标可以用一个向量来表示。

每个维度代表了不同的含义,有不同的偏向,你可以粗略地类比为:每个维度就相当于提了一个问题,词语在这个维度上的坐标值就是其相关度。

例如:“这个词和水果的相关度?” 可能的结果:

通过上述方法,赋予每个词语在每个维度上的值,也就是构造合适的向量,让语义关系体现在数学关系上,模型就能知道词之间的关联,在什么语境下应当填写哪类词语。

值得一提的是,词嵌入还有一些神奇的特性。最著名的例子就是:其向量可以做「语义算术」。例如:

用数学来解释,就是:

类似地,也有:巴黎 - 法国 + 意大利 ≈ 罗马。

事实上,我们可以用更加“数学”的方式去阐述这个现象。我们可以让一个向量 \vec V 表示「女性」向量减「男性」向量,那么我们会大致得到以下结果:

这说明了,词嵌入不仅记住了词的含义,还很好地编码了词之间的关系。

词嵌入是如何训练的

我们刚刚提到,词嵌入其实就是把每个词用一个向量表示,向量上不同维度的值大致代表了与某个问题的相关性。

那每个词语的向量是怎么构造出来的?世界上的词语浩如烟海,一个词语又有如此多的维度信息需要填写,总不可能人为去设置每个维度的问题,然后去筛选填写吧!所以,我们需要一个合理的训练方式,给每个词赋予合适的向量。

对于词嵌入训练,核心思想是上下文预测,也就是:一个词的意思,由它周围的词决定。

我们以 Word2Vec(词语转向量)为例,训练过程如下:

  1. 准备大量的文本,例如可以从维基百科、新闻中选材,做一遍文本清洗(去噪、规范化、分词)。
  2. 使用滑动窗口:取一个句子,遮住中间的一个词,让模型选词。例如:
    我 喜欢 [?] 因为 它 很 可爱

    模型需要根据上下文「喜欢」「因为」「可爱」来预测中间是「猫」。

  3. 调整向量:如果模型猜错了,就调整这个词的向量坐标,让其更接近正确答案。
  4. 反复迭代:经过海量文本的训练,每个词的坐标就稳定下来了。

上面是大概的流程,具体而言,模型会把「我」「喜欢」「因为」「它」「很」「可爱」的向量取平均,输入一个简单的神经网络,然后输出一个巨大的词语概率分布。调整向量与迭代的目的是为了让「猫」的概率尽量大。

这种训练模式叫做 CBOW(Continuous Bag of Words),也就是用上下文预测中心词

在数据集比较大的情况下,有一种更常用的方法叫做 Skip-gram,也就是用中心词预测上下文。和 CBOW 反过来,通过给模型输入「猫」的向量,经过神经网络,目标是让窗口内的词(我、喜欢、因为...)概率高,窗口外的词概率低。

上面描述的只是很粗略的步骤,实际上词嵌入训练还有很多细节。

另外,训练的技术也是在不断更新迭代的。现如今的 LLM(如 GPT、Claude)已经不再单独训练词嵌入,而是把一个词拆成词元(Token),例如把 "unhappiness" -> "un" + "happi" + "ness"。然后将词嵌入作为模型的第一层,和后续层一起端到端训练。其训练出来的维度更大(4096D、8192D),表达能力也更强。

Transformer 架构的引入

经过词嵌入后,对于一个词而言,其对应且仅对应了一个向量坐标。

但这又引入了另外一个问题,同一个词在不同语境下可能是具有不同含义的,举例来说:

再比如:

那对于同一个词,其词嵌入向量相同的情况下,怎么才能让模型知道这个“他”指代的不是张三而是李四呢?这就引入了 Transformer 架构的一个重要概念:自注意力(Self-Attention) 机制。

只需要注意力就够了!

现代的 Transformer 架构首次在 2017 年谷歌发布的一篇论文《Attention Is All You Need》中被提出,其中自注意力机制是最引人注目的部分。在此之前,注意力机制虽已广泛用于辅助循环神经网络(RNN),但几乎没人相信仅靠注意力本身就能支撑整个架构。

:::info[论文趣事一则]{open} Attention Is All You Need 这个标题致敬了披头士乐队的歌曲《All You Need Is Love》,“Transformer”这一名称由论文作者之一雅各布·乌什科雷特(Jakob Uszkoreit)选定,因其喜欢该词的发音。

早期设计文档曾命名为《Transformers:面向多任务的迭代式自注意力与处理》,并包含《变形金刚》(Transformers)系列六个角色的插图,团队亦命名为“Team Transformer”。 :::

回想一下之前的例子,一个词在不同语境下拥有了不同的含义,本质上是上下文对其产生了影响。

对于「苹果」这个词,如果在前面发现了「吃」这样的词,说明其代表的是水果;而如果在后面发现了「公司」的字样,说明其代表的是品牌名;同样的,要清楚句子中的「他」指代的是谁,则需要综合上下文的语义及逻辑关系来进行分析。

这说明,一个词的含义会被上下文所影响。有时候这种影响可能会来自很远的地方,词的含义也有可能会受到大量而非少量上下文的影响。例如,给模型输入一整本推理小说,最后一句“真正的罪犯是”让模型补全,那么模型就需要具备综合分析大量上下文的能力。

自注意力机制的作用就是,让模型在处理一个词时,让其能够关注上下文中的其他位置。当模型在处理「他」这个词时,自注意力机制让其与「李四」联系起来。

为了达成这个目的,我们需要让所有输入的初始向量之间可以相互交流更新,让每个词向量停留在更具体更精准的含义上。让「吃」这个词去更新「苹果」,使「苹果」向量的含义更精准地停留在“一种水果”的含义上;让「书」「喜欢」「阅读」等词去更新「他」,使「他」向量的含义更精准的停留在指代「李四」的含义上。

那么,自注意力具体是如何实现的呢?

自注意力机制如何工作?

让我们通过这句话来分析:

这 有 只 毛茸茸 的 棕色 动物

最初,每个词都会初始嵌入一个向量(通过词嵌入),此时,每个词的向量只编码了其初始含义,和上下文是没有关联的。然后,通过位置编码(Positional Encoding) 将每个词的位置编码进向量中。这里不介绍其具体是如何工作的,你只需要知道在编码后,所有向量足以说明这个词是什么,以及在哪个位置。我们记词的初始向量为 \vec E

我们的最终目标是通过一系列计算,更新原来的向量 \vec E,使新的向量停留在更精准的含义上。例如 \vec E_{动物} 就应该包含「毛茸茸」与「棕色」的特征含义。

为此,我们需要引入三个角色:Q、K、V,分别代表查询、键、值。更详细地说:

其中,Q, K, V 都以矩阵的方式进行编码。

这样讲还是太抽象了,我们回到刚刚的例子上来。现在我们要更新「动物」这个词的向量,你可以想象这个词发出了一个提问:“我前面存在形容词吗?”,而「毛茸茸」和「棕色」这两个词想回答:“有的!我就是形容词”。

从数学上的实现来讲,像“查询前面是否有形容词”这样的询问都被编码为了一个“查询矩阵”,记作 W_Q。然后将 W_Q 与每个 \vec E 进行矩阵乘法,得到每个词的查询向量,姑且记作 \vec Q

同样的,像“我是形容词”这样的回答都被编码为了一个“键矩阵”,记作 W_KW_K 也需要与 \vec E 进行相乘,得到的键向量记作 \vec K。从概念上讲,你可以把「键」视作想要回答「查询」。

需要指出的是,W_QW_K 包括后面的 W_V 都是通过从大量数据中学习训练得到的,矩阵里面的数值都是模型的参数。实际上来看,一个矩阵的作用其实很难用语言去解释,但为了便于理解,你可以想象其中有一个矩阵起到了“查询前面是否有形容词”的作用,这样可以更方便快速地理解相关的概念。

对于一个词得到的 \vec Q\vec K 而言,如果它们的方向越对齐,就越能说明它们相匹配。为了衡量每个查询与键的相关度,我们要计算所有的“键-查询”对之间的点积。

从数学意义上来讲,假设各向量长度相近时,两个向量 \vec a, \vec b 的点积 \vec{a} \cdot \vec{b}

所以可以通过计算 \vec Q\vec K 的点积,来判断词与词之间的相关度。从例子上来说,\vec Q_{动物}\vec K_{毛茸茸}, \vec K_{棕色} 的点积就会较大,代表其询问得到了相关度较大的查询结果。用机器学习的术语来说,就是「动物」的嵌入注意到了「毛茸茸」和「棕色」的嵌入。反过来说,\vec Q_{动物}\vec K_{的} 的点积是一个较小的负值,说明这两个词关系不大。

现在我们有了「查询」与「键」之间的相关度,但光有相关度还不够——相关度只回答了"该关注谁",还没有回答"关注到了什么内容"。这就是第三个角色 V 要干的事情。

回到例子上,「动物」发出了询问,「毛茸茸」和「棕色」回答了“我就是形容词”。那么接下来,「动物」真正想从它们那里拿走的,是它们各自的实际内容,也就是“毛茸茸的特征”和“棕色的特征”本身。这些内容,正是通过「值」来传递的。

词的实际内容也被编码为一个"值矩阵",记作 W_V。将 W_V 与每个词的初始向量 \vec E 相乘,记作 \vec V。你可以把 \vec V 理解成这个词"准备贡献给别人"的那份信息。

有了 \vec V 之后,更新向量就变得非常自然了:用上一步算出的相关度作为权重,对所有词的值向量进行加权求和。以「动物」为例:

\vec E_{动物}' = w_{毛茸茸} \cdot \vec V_{毛茸茸} + w_{棕色} \cdot \vec V_{棕色} + w_{的} \cdot \vec V_{的} + \cdots

其中权重 w 来自 \vec Q_{动物} 与各个 \vec K 的点积。由于「毛茸茸」和「棕色」与「动物」的点积较大,它们的 \vec V 就在求和中占据了主导地位;而「的」这类相关度低的词,其贡献几乎为零。

当然,实际运算上,我们不会把每个词的向量分开进行分别计算,所有上下文的词嵌入向量都被打包成了一个矩阵,然后整体进行矩阵乘法运算。上面所有的描述,在《Attention Is All You Need》中被描述为一个优雅的公式:

\text{Attention}(Q,K,V) = \text{softmax}(\frac{QK^T}{\sqrt{d_k}})V

你可能对其中的 \sqrt{d_k}\text{softmax} 有所疑惑。简单来说,矩阵乘法中,当计算的维度越大,得到的值可能就越大,为了数值的稳定性,避免出现值与值差距过大导致梯度消失的问题,一般都会将所有点积除以“键-查询”空间维度的平方根。而 \text{softmax} 是一个归一化函数,它把所有权重压缩到 0 到 1 之间,并且总和为 1。这样既保证了更新后的向量不会因为权重过大而数值爆炸,也让“关注多少”变成了一个较为清晰的分配比例值。

如果你对 \text{softmax} 的原理感兴趣,它其实是这样实现的:

p_i = \frac{e^{z_i}}{\sum_{j} e^{z_j}}

至此,我们最初的目标就达成了:更新后的 \vec E_{动物}' 不再只包含“动物”这个孤立的含义,而是融合了上下文的信息,隐含着“这是一只毛茸茸的、棕色的动物”的特征。同样的,序列中的每个词都经历了一样的过程:发出查询、匹配键、按权重收集值。一句话经过一层注意力计算后,每个词的向量都从“孤立含义”变成了“上下文含义”。

如果你能理解上面的内容,你就已经理解了自注意力机制的核心。

很多很多头的注意力

回顾一下我们刚才做的事情,「动物」发出了一个查询:“我前面存在形容词吗?”,然后根据回答更新了自己。整个过程看起来很完美,但仔细想想会发现一个问题:一个词需要关心的关系,往往不止一种

还是以「动物」为例。它可能想同时发出好几个不同的查询:

如果只有一套 W_Q,W_K,W_V,就意味着所有词只能用同一种方式去提问和回答。显然,一种视角不足以捕捉一句话里丰富多样的关系。

于是,论文中引入了一种名为 “多头注意力”(Multi-Head Attention) 机制的改进,进一步提升了自注意力层的性能。它的核心思路也很简单粗暴:既然一套注意力不够用,那就同时用很多套。

从数学的含义上讲,我们准备 h 组彼此独立的矩阵。这里取 h=8 组,于是我们有了:

(W_Q^0,W_K^0,W_V^0),(W_Q^1,W_K^1,W_V^1),\dots,(W_Q^7,W_K^7,W_V^7)

其中,每一组称为一个「头」,每个头都完整地执行一遍我们之前讲过的自注意力全流程。

你可以想象有 8 个人同时读这句话,每个人只从自己关心的角度去理解:第一个人专门盯形容词,第二个人专门盯量词,第三个人专门盯句子的主干结构...第一个人理解出的「动物」是“毛茸茸的、棕色的”,第二个人理解出的是“数量为一只的”,第三个人理解出的是“被拥有着的”。

当然,这里再提一嘴,我们其实很难用语言去解释一个矩阵它到底干了什么,在运算起了什么作用。上面只是一种形式化的比喻,让其过程更容易理解。实际上,没有人告诉第一个头“你要去关注形容词”。每个头的矩阵是从大量数据中训练出来的,模型在学习过程中自发地让不同的头分化出了不同的关注点。研究人员在分析训练好的模型时,确实发现了有的头偏好相邻的词、有的头偏好指代关系、有的头偏好标点结构,这种现象再次印证了多头设计的合理性。

好了,现在 8 个头各自给出了一个更新后的向量,每个向量都只说对了一部分,怎么把它们合并成一个最终结果呢?

做法很简单:我们先将每个向量拼接在一起,然后再将它们与一个额外的权重矩阵 W_O 相乘。

这里的 W_O 同样是一个通过训练学习得到的矩阵,我们把它称作输出矩阵W_O 与拼接后的大向量相乘,把维度压缩回原来的大小,得到最终的输出。你可以想象 W_O 的作用就是一名“主编”,它通过阅读 8 个人的笔记,权衡哪些信息重要、哪些可以忽略,然后汇总成一份统一的报告。同样的,W_O 中哪些信息该保留、如何混合,也是在训练中自动学到的。

用公式总结整个过程就是:

\text{MultiHead}(\vec E) = \text{Concat}(\text{head}_0,\dots,\text{head}_{h-1}) \cdot W_O

至此,每个词的新向量就真正融合了来自多个视角的上下文信息。

残差连接

到目前为止,我们已经讲完了一个注意力模块的内部结构。但一个完整的模型并不会只做一次注意力计算,注意力计算后还会经过一个前馈网络(FFN) 模块,这两个模块最后会打包成一组,进行反复的堆叠,堆上几十层甚至上百层,每一层都在上一层的基础上继续加工向量。

这意味着,某个词在吸收一些上下文信息后,产生的含义更丰富细致的向量,还有更多的机会被周围含义更细致的向量所影响。其经过越多的模块,其向量本身的含义就会越来越丰富,理想的话,对于输入的内容,能提炼出更高级、更抽象的概念,而不仅仅是修饰和语法结构。也许还包括情感、语气、诗意以及科学原理等等。

但事不遂人愿,实际上,向量在不断的加工后,会出现一个问题:信息传得越久,就越容易走样

你可以想象一个“传话游戏”:一句话经过几十个人依次转述,传到最后一句话时往往面目全非——每转述一次都会丢失一点细节、混入一点偏差。向量在层间传递也是同样的道理:每一层的注意力计算都会重新混合所有词的信息,「动物」原始的词义在一层层加工中被不断稀释。传到几十层之后,它可能早就“忘记”自己最初是什么词了。

更要命的是训练层面的困难。如果你了解基本的机器学习就会知道,训练模型靠的是从输出端反向传播误差信号,来逐层调整参数。但是如果层数太深,这个信号在层层回传的过程中会不断衰减(即梯度消失),导致靠前的几十层几乎接收不到任何有效的调整指令,那这些层就“学不动”了。

如何解决这个问题呢?出乎意料的是,解决方法异常地简单:每一层算出新向量后,不急着把旧向量扔掉,而是把它原封不动地加回来。这个方法就叫做残差连接(Residual Connection)

从数学上的实现来讲,就是:

输出 = 输入 + \text{Attention}(输入)

也就是说,每一层的工作不再是“重写”向量,而是“修改”向量,只负责在原有内容的基础上补充增量

回到我们的例子:「动物」向量经过注意力层后,新向量 = 原来的「动物」+ 刚学到的“毛茸茸、棕色”等增量信息。这样一来,无论堆多少层,「动物」的原始词义始终完好地保存在向量里,每一层只是在它之上做加法,而不是把它覆盖掉。这类似于我们修订一篇文稿:原稿始终都是不变可见的,而每个人只是在旁边添加批注和修改,不会把原稿翻新重写。

对于训练来说,残差连接还带来了一个额外的好处:它为误差信号修了一条“高速公路”。由于每一层都有一条绕过注意力计算的直达通路,误差信号可以顺着这些通路几乎无损地传回最靠前的层,让深层网络也能被有效地训练。可以这么说:没有残差连接,现在动辄上百层的大模型根本无从训练。

:::info[残差连接的发展]{open} 残差连接是继承了 ResNet(残差网络) 这一思想得来的。其核心论文《Deep Residual Learning for Image Recognition》发表于 2015 年(CVPR 2016,获最佳论文奖),该论文作者均是华人研究者,第一作者是何恺明,当时任职于微软亚洲研究院(MSRA),论文的共同作者张祥雨、任少庆、孙剑也都是华人研究者。

残差的思想在 ResNet 之前已有雏形,但运用的工作上产生的影响极其有限。是 ResNet 首次把“输入直接加回输出”这种简洁形式做到极深的网络上,以压倒性优势拿下 ImageNet 冠军,让全世界认识到残差连接是训练深层网络的关键。何恺明也因此成为深度学习领域被引用最多的研究者之一。Transformer 论文(2017)沿用的正是 ResNet 验证过的这套做法。

何恺明后来加入 Facebook AI Research(FAIR),又主导了 Mask R-CNN 等重要工作,2024 年起在 MIT 任教。 :::

需要指出的是,实际的模型中通常还会搭配一个层归一化(LayerNorm) 操作,在每一层计算前把向量的数值调整到稳定的范围内,防止数值在层层累加中越滚越大。残差连接配合层归一化共同保证了深层网络的稳定。

概念:前馈网络

在前面,我们提到了前馈网络(Feed-Forward Network,FFN) 这一概念,这同样是 Transformer 架构的重要组成之一。实际上,相较注意力模块,FFN 才是模型参数量的“大头”。在一个典型的 Transformer 层里,大约三分之二的参数都集中在 FFN 中。

那为什么我们需要 FFN,它起到了什么作用呢?

简单来说,自注意力层让词与词之间相互交换信息,使一个词向量嵌入了更丰富更精确的含义。但要注意,注意力本质上只做了一件事:搬运。它只是将别的词的信息按权重混合进指定词的向量,但信息的总量并没有被“消化”。

类比一下,就像是一个团队开完了一场会议,每个人都收到了同事的各种资料。那么接下来,每个人都需要回到自己工位上,把这些资料好好读一遍、提炼消化,形成自己的结论。这个“回到工位独立思考”的环节,就是前馈网络

我们再来举一个更具体的例子。例如我们给模型输入“法国的首都”,那么经过自注意力模块后,「首都」这个词理应嵌入了「法国」这个词的相关含义,但是没有进行总结提炼。那么在经过 FFN 后,「首都」这个词就应该自行总结出「巴黎」这样的含义,再通过残差连接把这个含义包含进原来的词。

听起来很厉害吧!那要实现这样的效果该怎么做呢?实际上,其本质还是在通过一些训练过的矩阵进行数学计算。这部分概念可能比较抽象,好在对于每个词,其在 FFN 中的过程是并行的,它不像自注意力模块中,词与词之间会相互交流。所以我们只需要弄清楚一个词向量在 FFN 中是怎么进行计算的,就能推广到全体向量了。

前馈网络如何工作?

从结构上看,FFN 简单得有些出人意料——它只有三步:升维、筛选以及降维。我们来分开解析每一步的作用。

第一步:升维

首先,经过自注意力模块的词语向量 \vec E 会与一个升维矩阵 W_{\uparrow} 相乘,这会得到一个相较原词向量维度更高的新向量。在原始论文中,词向量是 512 维的,经过 W_{\uparrow} 后会被放大到 2048 维,足足膨胀到原来的 4 倍。

升维矩阵做了什么?我们可以把矩阵的每一列拆开来,视作很多不同的向量,回想一下自注意力机制中 W_Q 的比喻,这里的每一个列向量相当于对现在的词向量提了一个问题。整个矩阵相当于模型对这个词一口气抛出几千个问题来审视它:

维度越高,能同时问的问题就越多,向量的特征就被剖析得越细致。这同时回答了“为什么要升维”这一问题:低维空间里挤在一起、难以区分的信息,到了高维空间里就有了足够的“地方”被分门别类地展开,方便模型进行更细致的处理。

整个升维矩阵和原词语向量相乘,得到的新高维向量在高维空间中的每个坐标,就是它对每个问题的回答得分。

然后,我们还需要引入一个偏置项 \vec b_{\uparrow},这是和升维后的向量同维的一个向量,我们需要把这两个向量加起来。

偏置项起的作用类似于划定了每个问题的“及格线”,当两向量相加后,值为正的坐标说明词语与问题相关,值越大则相关度越大。反之,值为负则一般说明不相关。例如偏置项中的某一坐标是 -5,那么词向量回答该坐标的问题值必须超过 5 才被视作“合格”。

在这里,值的正负很重要,0 被视作一个分界线,这也是为什么需要引入偏置项,你会在后面的步骤理解其为什么重要。

我们用“法国的首都”来举一个例子。也许存在这样一个“提问”,其偏置项中对应的坐标值是 -5。当其检测到「法国」会产生 3 的贡献,而检测到「首都」又会产生 3 的贡献。所以当完整地检测到一个词包含「法国」和「首都」的信息,就会得到 3+3-5=1,算作合格了。但如果只包含「法国」或「首都」其一,比如进入模型的是“法国的名菜”或“英国的首都”,这个问题就不会被视作合格,从而被后面的过程筛选掉。

第二步:筛选

这一步非常易于理解:把第一步得出的向量取出,然后将每个坐标值送进一个激活函数(Activation Function)。论文中使用的是 ReLU,它的规则十分简单粗暴:负数一律归零,正数原样保留。也就是:

\text{ReLU}(x) = \max(0, x)

经过这一步,绝大多数“问题”都被筛掉了,只有少数真正匹配的特征被保留下来。

这下,你应该可以稍微理解为什么前面要引入“偏置项”,是因为后续的计算环节,我们天生就把 0 当作分水岭来用。这个分界线的源头可以追溯到最早的感知机(Perceptron,1958),它直接模仿生物神经元“要么放电、要么静默”的特性,用的激活函数是阶跃函数

f(z) = \begin{cases} 1, \quad z > 0 \\ 0, \quad z \le 0 \\ \end{cases}

在这个模型里,“大于 0 激活、小于 0 不激活”是字面意义上完全成立的。而现代网络大量使用的 ReLU 是阶跃函数的“软化版”,体现为:当值大于 0,则神经元激活,且激活强度线性增长;否则神经元完全不激活,特征被丢弃。

当下的大模型大多不用 ReLU,而是用 GELU 或 SwiGLU 这类函数,但起到的作用是类似的。

第三步:降维

接下来,经过 ReLU 筛选后的向量会与一个降维矩阵 W_{\downarrow} 相乘,同时也会增加一个偏置项 \vec b_{\downarrow}。高维向量会被压缩回原来的 512 维。

如果说 W_{\uparrow} 是“提问”,类似于注意力机制中的 W_Q,那么 W_{\downarrow} 就是“根据提问结果需要写入的内容”,类似于 W_V。它把每一个被激活的特征翻译成“该往向量里补充什么信息”,汇总成最终的思考结果。

回到之前的例子:在“法国的首都”这句话里,「首都」的向量经过注意力后已经带上了「法国」的信息。进入 FFN 后,某个神经元恰好负责检测“前面有国家名 + 当前词是首都”这个模式,经过升维和筛选后,它被成功激活。

而把 W_{\downarrow} 矩阵按行拆开后,其每个行向量都可能包含了某个词的特征,也许就有那么一行代表了「巴黎」。在对应的神经元被激活后,「巴黎」的信息就被写进了向量里。这一系列过程都是通过矩阵乘法计算的。

至此,经过 FFN 的三步骤,向量完成了一次从“收集资料”到“得出结论并记录”的完整过程。上面的过程同样可以总结为一个优雅的公式:

\text{FFN}(\vec E) = \text{ReLU}(\vec E \cdot W_{\uparrow} + \vec b_{\uparrow}) \cdot W_{\downarrow} + \vec b_{\downarrow}

毫不意外的是,W_{\uparrow}, \vec b_{\uparrow}, W_{\downarrow}, \vec b_{\downarrow} 都是通过大量训练得到的。当然,和注意力头一样,真实的神经元很少像上面描述的那样干净地对应某个具体概念,一个概念往往分散在成百上千个神经元中。上面的说法也只是为了便于理解的比喻。

:::info[FFN 是模型的“记忆库”吗?]{open} 研究人员发现,FFN 的行为很像一个“键-值”数据库(参见 2021 年的论文《Transformer Feed-Forward Layers Are Key-Value Memories》):W_{\uparrow} 的每一列像一把“钥匙”,负责识别输入中的特定模式(例如上下文中出现了“迈克尔·乔丹”),W_{\downarrow} 对应的行则储存着要写入的信息(例如“篮球”这个联想)。

换言之,模型在训练中记住的大量事实知识,很可能主要就存储在 FFN 的矩阵里。这也解释了为什么 FFN 占据了模型约三分之二的参数,因为它不仅是模型的“消化器官”,还是一个整合了世界知识的“图书馆”。 :::

从向量回到文本

于是,经过几十层加工后,我们最后还是会得到每个词的最终向量。但还记得我们最初的内容吗?模型的使命是预测下一个词,向量毕竟只是数学对象,我们最终想要的,还是文章开头那样的概率分布表。这一步该怎么办呢?

首先我们要明确,虽然我们得到了全部词的最终向量,但我们其实只需要最后一个位置的向量。它站在句子的末尾,综合了全部上下文信息的位置,是最适合用于预测下一个词的向量。我们记它为 \vec E_{\text{last}}

回想一下词嵌入:我们训练的模型把每个词都对应上了一个向量,这相当于模型手里有了一本“词典”,负责把词变成向量。而实际上,模型还有“第二本词典”,叫做输出嵌入矩阵(Output Embedding,也称 Unembedding),记作 W_U。词表里的每个词在这里同样有一个专属向量,负责把向量变回词。

然后,我们来进行一个打分操作。操作的方式你应该很熟悉了:把 W_U\vec E_{\text{last}} 进行矩阵乘法。最后会得到一个新向量,每个坐标位置都对应了一个词,而坐标上的值被称作这个词的分数(logit)

直觉上,这相当于回到了语义地图:加工后的 \vec E_{\text{last}} 落在了地图上“下一个词应该在的区域”。比如“今天天气真”之后,这个向量大致落在“形容天气的形容词”那一片,「好」「不错」「热」这样的词的分数自然就名列前茅。

但现在,我们有大量的词对应大量的分数,每个分数的值有大有小、可正可负,还不是概率。这个时候就要请出我们的老朋友 \text{softmax} 了,让它在注意力机制中干的活在这里再干一遍:把所有分数压缩到 0 到 1 之间,并且总和为 1。

至此,文章开头那张概率分布表就算是真正地算出来了:「好」30%、「不错」20%、「热」10%……每一个概率数字的背后,都是几十层注意力与前馈网络联手加工的结果。

后记

到这里,我们已经讲解了一遍 Transformer 架构的主要结构原理。

让我们回到开头提出的那个问题:LLM 究竟是如何计算下一个词的概率的?现在我们可以给出一个虽然粗略、但不再神秘的回答了:每个词先通过词嵌入变成一个向量坐标,然后在一层又一层的模块里,先靠自注意力机制与上下文交换信息,再靠前馈网络独立思考、消化提炼,全程通过残差连接层归一化保证稳定性。经过几十上百层这样的加工,最初那个只代表“孤立词义”的向量,已经被塑造成蕴含上下文、语法、逻辑乃至世界知识的丰富表示。最后,模型再根据这个向量换算出词表中每个词的得分,得到一个词语的概率分布表。

当然,正如文中反复强调的,本文只是一次入门级的“浅析”。还有太多重要的东西没有展开:模型具体是怎么训练的、生成时为什么不能“偷看”后面的词、编码器与解码器又有什么区别……这些就留给各位继续探索吧!文末的推荐阅读都是我学习时深受启发的资料,有不同视角下对 Transformer 架构的解读,强烈推荐。

另外,坦白说,我自己也只是一个刚入门的小白,写作本文的过程其实是我“以教促学”的尝试。文中难免存在疏漏甚至错误,如果你发现了,请务必指正,非常感谢。

最后,Transformer 的原理固然精妙,但我希望各位在读完这篇文章后能知道:它并不是什么高深的魔法,它只是一群聪明人把线性代数与矩阵乘法玩到极致的产物。希望大家以后再用 AI 的时候,心里能多一份“我知道你大概在干什么”的踏实感。

推荐阅读:

  1. 论文 - Attention Is All You Need
  2. 中文维基百科 - Transformer 架构
  3. Jay Alammar 的图解 Transformer - The Illustrated Transformer
  4. Bilibili: 3Blue1Brown - 直观解释 Transformer、直观解释注意力机制 与 直观解释大语言模型如何储存事实