速不通 LLM
句子与字词
我们将一句话看作一串字词按照特定顺序出现的概率——计算这句话的概率时,可以把它拆成多个步骤:先看第一个字词出现的概率,再看第一个字词出现的情况下,第二个字词出现的概率……一直到最后一个字词。
比方说句子「我喜欢吃苹果」。
将其拆分为字词「我」「喜欢」「吃」「苹果」。
那么这个句子整体出现的概率:
可以拆成:
「我」出现的概率:
已经有「我」后,「喜欢」出现的概率:
已经有「我喜欢」后,「吃」出现的概率:
已经有「我喜欢吃」后,「苹果」出现的概率:
最后把它们乘起来:
就得到:
将之推广到任意一个句子(序列),我们就得到:
分词、token
前面将「我喜欢吃苹果」拆分为字词「我」「喜欢」「吃」「苹果」,如果用不同的方式(分词器),也可以将其拆分为「我」「喜」「欢」「吃」「苹」「果」。具体采用哪种拆分,取决于模型训练时预先确定好的分词规则。
用分词器将一个句子拆分出来的最小单元,我们称其为 token。它可以是一个完整的词(如「喜欢」),也可以是一个词根(如「un- 」),还可以是一个单字(如「苹」),甚至是一个字节或一个表情符号。
因此前面概率公式里的 严谨说来指的并非人类理解的「字词」,而是分词器拆分的 token。
token 构成的集合被称为词汇表(Vocabulary),通常用符号 表示。词汇表的大小记作 。
假如词汇表有 50000 个 token,那么光是由 10 个 token 组成的上下文(句子)就有 种——比可观测宇宙中的原子数量还要多。
我们无法为每个可能的上下文单独存储一个概率。最好有个函数能够接受任意的上下文,并生成关于下一个 token(字词)的概率分布。
即:
这个函数,就是语言模型。
模型与函数
怎么得到这个函数呢——我们不能「设计」它,语法规则、语义关系、修辞手法……没有人能手工写出一套规则,让计算机根据上下文精确地算出下一个词的概率。
我们可以设计一套框架,让模型从大量的文本中自己把规律「调」出来。
具体来说:
1. 定义一个带参数的结构
选一个灵活的架构(如 Transformer),里面包含数十亿个可调节的数值(参数),初始时全部是随机的。
2. 让它反复试错
把海量真实文本喂给它。每看一个上下文,它就预测下一个 token,然后与真实值对比:
给真实 token 的概率越高 → 误差越小
给真实 token 的概率越低 → 误差越大
这个误差可以直观地理解为模型的「惊讶程度」。
3. 自动调整参数
把误差对每一个参数的影响计算出来——这个计算依赖于链式法则——它把最终误差沿着计算路径逐层拆解,分配给每一个参数——然后所有参数自动朝「让误差更小」的方向微调一点点。
4. 重复
在数万亿个 token 上反复执行步骤 2-3,参数从「随机乱猜」逐步变成「能给出合理预测」。
这个过程,就叫训练(Training)。训练语言模型,就是让它在「真实文本」上的平均惊讶程度不断降低。
一个被训练好的语言模型,本质上就是一个从「任意 token 序列」到「下一个 token 概率分布」的可计算函数。
数学与简单例子
函数(模型)怎么处理 token 呢?
实际上,我们会将 token 映射成一个固定长度的数值列表(向量),这个过程叫作嵌入(Embedding)。
我们来个简单的例子。
假设词汇表有 5 个 token:[猫, 狗, 车, 人, 树]
嵌入维度设为 3(即每个 token 用一个 3 维向量表示)。
这 5 个 token 的嵌入向量分别是(数值为示意):
| token | 维度 1 | 维度 2 | 维度 3 |
|---|---|---|---|
| 猫 | 1.2 | 0.8 | 0.1 |
| 狗 | 1.1 | 0.9 | 0.0 |
| 车 | -1.2 | 0.3 | 1.5 |
| 人 | 0.9 | -0.5 | 0.3 |
| 树 | -0.8 | 1.2 | -1.1 |
每个维度可以粗略理解为一种「特征」,但不要把它理解成现实中的某个具体属性。模型并不会规定「第 1 维代表动物性、第 2 维代表大小」,这些数值及其组合关系,是模型在训练过程中从大量文本中自动学习出来的。
这些向量被组织成一张嵌入矩阵 :
反过来,当模型看到 token「猫」时,它去矩阵里取第 1 行:
然后分别去找猫和狗的距离:
猫和车的距离:
显然,在这个例子中,「猫」和「狗」的向量距离较近,说明它们在这个向量空间中的表示比较相似。
这里用欧几里得距离可以直观地理解两个向量之间的距离。实际上,在神经网络中,更常使用点积来衡量两个向量之间的关联程度。(Attention 中,也会使用点积来衡量 Query 和 Key 的匹配程度。)
「猫」和「狗」的点积:
「猫」和「车」的点积:
因此,在这个简化的例子中,「猫」和「狗」的向量方向更加一致,而「猫」和「车」的方向差异更大。
在真实的语言模型中,这并不意味着模型看到「猫」就会预测「狗」。Embedding 只是把 token 转换成模型可以进行数学运算的向量。
真正预测下一个 token 时,模型还会结合当前 token 以及前面的上下文,通过 Transformer 进行计算,得到一个能够表示当前语境的向量,然后根据这个向量对词汇表中的 token 进行打分,最终得到下一个 token 的概率。
上下文
在静态嵌入里,「苹果」永远只有一个向量。但在「我吃了苹果」和「我买了苹果手机」两句话里,「苹果」的含义显然不同。
模型需要在看到具体上下文之后,动态地为「苹果」生成一个新的、融合了上下文信息的表示。
怎么做呢?
对上下文里的每一个词,先计算它与上下文中每个位置的关联得分,再将其转换为「注意力权重」,然后把所有词的内容按权重混合起来,从上下文中提取信息。
比如「我吃了苹果」:
「吃」和「苹果」的关联度很高 → 权重很大
「我」和「苹果」的关联度中等 → 权重中等
「了」和「苹果」的关联度很低 → 权重很小
混合之后,「苹果」的新表示会向「吃」的方向偏移——于是它变成了「食物语境下的苹果」。
权重从哪来——Attention
我们需要一个方法,计算「当前位置 」和「上下文位置 」之间的关联度。
最直接的方案:用点积。
点积越大,说明两个向量方向越一致,关联度越高。
点积有一个性质:
这意味着,用这种方法计算出来的关联度天然是对称的: 对 的关联度,必然等于 对 的关联度。
语言不是这样。
看「猫坐在垫子上」这句话:
「猫」对「坐」的影响很大(猫是动作的发出者)
「坐」对「猫」的影响相对较小(坐只是描述了猫的状态)
因此,我们需要一种能够表达有方向的关联关系的方法。
想象两个人在对话:
A说:「我想找一个能陪我跑步的伙伴。」
B说:「我是一个喜欢晨跑的人。」
A 和 B 的匹配度很高——因为 A 的「需求」和 B 的「特征」对上了。
但反过来呢?
B说:「我想找一个能教我做饭的人。」
A说:「我是一个爱跑步的人。」
B 的需求和 A 的特征就对不上。
同一个 A,在面对不同需求时,被打出的「匹配分」是不同的。
这里的核心是:「我想找什么」和「我能提供什么」是两种不同的信息,它们不共享同一个评价标准。
在语言模型里,当我们在处理「坐」这个词时:
「坐」作为当前位置,它想知道:「谁能告诉我,是谁在坐?」(这是 Query)
「猫」作为上下文词,它可以提供:「我是主语,我是动物。」(这是 Key)
当 Query 和 Key 匹配上时,关联度得分就高。
反过来,如果让「猫」作为 Query 去问「坐」:「你能告诉我什么?」它得到的回答只是「一个动作」,作用显然小得多。
**所以,我们需要把同一个词映射成两种不同的表述——一种用来「提出需求」(Query),一种用来「提供信息」(Key)。**同一个词在这两种角色下,应该有不同的数值形态。
数学上我们可以**对同一个向量 ** ,用两个不同的矩阵 和 分别去乘它,得到两个不同的新向量。一个用于「提出需求」,一个用于「提供匹配」。
和 都是可学习的参数矩阵——它们的内容不是设计出来的,而是在训练过程中逐渐被「挤」出来的,使得「需求」和「匹配」能够更好地对齐。
然后:
表示当前位置 :「我在寻找什么」。
表示位置 :「我能提供什么信息」。
于是,位置 对位置 的注意力得分可以计算为:
表示:**位置 ** 关注位置 。
反过来:
表示:**位置 ** 关注位置 。
由于 和 是两个不同的投影矩阵,一般情况下:
因此,我们虽然仍然使用对称的点积操作,但通过让同一词以不同角色参与计算,使最终得到的注意力得分具有方向性。
Query 和 Key 可以理解为两种不同的角色:Query 表示「我正在寻找什么」,Key 表示「我能提供什么」。二者匹配得越好,注意力得分越高。
有了注意力得分之后,一个很自然的问题就出现了:
我们算出「坐」对「猫」的得分很高(「坐」这个位置的 Query 和「猫」的 Key 匹配)——然后呢?这个得分用来做什么?
如果只是算出一个分数放在那里,没有任何意义。这个分数最终是为了做一件事:从「猫」那里提取一些信息,加到「坐」的表示里去。
那么,「猫」应该贡献什么信息?
一个最直接的想法是:把「猫」的原始向量 直接拿过来,按权重加进「坐」的表示里。
但这里有一个问题:
决定「猫」是否值得被关注的特征,和「猫」实际贡献出去的内容,可以是不同的特征。
举个例子:
假设「猫」被「坐」关注,是因为它在句子中承担主语等语法角色。
但「坐」真正要从「猫」那里获取的,是「毛茸茸的小动物」这个语义内容。
前者决定「值不值得关注」,后者决定「关注了之后拿到什么」。它们是两回事。
如果 Key 和 Value 使用同一个向量,模型就会面临一个两难:
为了让「坐」关注到自己,「猫」需要把 Key 调成「名词、主语」——但这组特征未必是它想传递的语义。
为了传递「毛茸茸的小动物」这个语义,它可能需要把 Value 调成另一种形态——但这可能让它不再容易被「坐」匹配到。
一个向量要同时服务于两个不同的目标,通常只能两头妥协,无法同时做到最优。
这就引入第三个投影矩阵:
它产生 Value(值),负责提供实际传递的信息。
Key 和 Value 承担不同职责:
Key: 用于匹配,决定「一个位置在当前 Query 下有多容易被匹配到」。
Value: 用于传递,决定「这个位置实际提供什么信息」。
两个投影矩阵各自独立:
模型可以单独调整 Key,让某些词更容易被匹配到,而不改变它们实际传递的内容。
模型也可以单独调整 Value,让某些位置传递不同的信息,而不影响它们被匹配的方式。
**Key 决定一个位置在当前 Query 下有多容易被匹配到,Value 决定匹配成功后向当前位置传递什么信息。**两者解耦,模型就有了更大的自由度去优化预测。
例如,「坐」这个位置在处理当前上下文时,可能通过 Query 与「猫」的 Key 产生较高的匹配分数,于是更加关注「猫」;但最终从「猫」那里获取什么信息,则由「猫」的 Value 决定。
可以粗略把这三个步骤理解为 Attention:
也就是:
Q 决定「我想找什么」,K 决定「我有什么可以匹配」,V 决定「真正传递什么」。
实际上, 、 、 这三个投影矩阵也是被训练出来的。它的灵感来源于信息检索(Information Retrieval)。
在数据库里,你要查一本书:
你输入的关键词是 Query(你在找什么)。
书上的标签/索引是 Key(它能提供什么匹配)。
书的内容本身是 Value(实际拿到的信息)。
模型把这个逻辑借了过来:它自己造数据库,自己造查询。 、 就是造查询和造标签的工具。没人告诉它标签怎么造,它通过训练自己学会了怎样造标签能更好地匹配查询,怎样造查询能更精准地找到有用的信息。
缩放点积
我们已经通过 Query 和 Key 的点积得到了匹配分数:
但在实际的 Attention 中,这个分数还要先进行缩放:
其中 是 Key 向量的维度,即 Key 向量中数值的个数。
为什么要除以 ,为什么除以的是 ?
因为点积是 个乘积项的总和。随着向量维度 增大,点积的数值波动也会变大,而且其典型尺度大致与 成正比。
如果直接把这些分数交给 Softmax,容易使输出过于集中:最高分的位置接近 1,其他位置接近 0。这样会让 Softmax 的梯度变得很小,不利于模型训练。
除以 ,可以把点积的数值缩放到更合适的范围。
因此,实际的 Attention 使用的是缩放点积:
接下来,再把这些缩放后的分数交给 Softmax,将它们转换成注意力权重。
Softmax: 把得分转成权重
我们把注意力得分 算出来了——它告诉我们「当前位置 」对「上下文位置 」的关注程度有多高。
但 只是一个原始分数,可以是正数、负数、很大或很小。我们真正需要的是一组能够表示「注意力分配」的权重:分数越高的位置,应该获得越大的权重。
Softmax 函数恰好可以把任意一组实数转换成满足以下条件的权重:
非负(不能有负的贡献)
总和为 1(使它们可以被理解为一组分配给不同位置的权重)
它的作用很直接:
对得分取指数 ,保证结果为正数(因为 )
除以所有位置的总和,保证所有权重加起来等于 1(因为分母是所有分子的和)
Softmax 还有一个附带效果:通过指数函数放大不同分数之间的差异**。**
举个例子,假设某一步的注意力得分是:
「猫」:4
「垫子」:1
「的」:0
经过 Softmax 后,权重可能变成:
「猫」:0.936
「垫子」:0.047
「的」:0.017
这就是模型「聚焦」于重要词的机制——高分词几乎占据了全部权重,低分词几乎被忽略。
加权平均
有了权重之后,将所有 Value 按照注意力权重进行加权求和。由于 Softmax 后的权重总和为 1,因此这个结果也可以直观地理解成一种加权平均。
其中 是 Value 向量(「被关注后实际传递的内容」)。
回到「苹果」的例子:
在「我吃了苹果」里,Attention 会根据当前上下文调整不同 token 的权重,使「苹果」的位置融合「吃」等上下文提供的信息。因此,「苹果」的表示会包含更多与食物有关的上下文信息。
在「我买了苹果手机」里,「苹果」的位置又会融合「手机」等上下文提供的信息,因此它的表示会包含更多与品牌、电子产品有关的上下文信息。
所以,同一个 token「苹果」,在不同上下文中经过 Attention 后,可以得到不同的表示。
同一个 token,因为所处的上下文不同,会从上下文中获取不同的信息,于是得到不同的上下文表示。
堆叠多层
我们已经有了一个最核心的机制:Attention。
一次 Attention 可以让每个 token 根据当前上下文,从其他位置获取信息,得到一个融合了上下文的新表示(输出)。
但一次 Attention 能做的事情仍然有限。我们可以把得到的新表示继续交给下一层,让模型在已经融合了一部分上下文信息的基础上,再进行一次信息交互。
于是,把这样的处理重复多次:
每一层都有自己独立的 ,因此可以学习不同的关注方式。
实际中,一层并不只有 Attention,还会包含前馈网络、残差连接、归一化等组件。我们这里先把这些细节省略,只关注 Attention 如何逐层传递和加工信息。
把这些组件组织起来,并重复堆叠多层,就构成了 Transformer。
经过 层之后,我们得到了每个位置的新表示。对于最后一个位置 ,取出:
在因果语言模型中,当前位置只能关注自己以及它之前的位置,不能看到后面的 token。对于最后一个位置 ,这个向量已经融合了当前位置及其之前上下文的信息。
现在,模型要利用这个向量预测下一个 token。
具体来说,模型需要回答:
词汇表中的每一个 token,哪个最适合作为下一个 token?
因此,我们需要把当前的上下文表示 转换成对词汇表中每个 token 的一个分数。
对于词汇表中的某个 token ,用点积计算它与当前上下文表示的匹配分数:
其中, 是 token 对应的输出向量。词汇表中的每个 token 都有一个这样的向量。
这样,我们就得到了一组分数:
它们分别表示词汇表中不同 token 作为下一个 token 的匹配程度。
但这些分数还不是概率。于是,再对所有分数统一进行 Softmax:
这样,就得到了词汇表中每一个 token 作为下一个 token 的概率。
位置编码
回顾一下注意力得分的计算:
如果 和 中没有位置信息,那么这个点积本身并不知道两个 token 在句子中的位置。
但在语言中,「狗咬人」和「人咬狗」虽然包含完全相同的 3 个 token,却表示不同的含义。
如果模型不知道每个 token 位于什么位置,就无法利用「谁在前、谁在后」这一信息,也就无法区分这两句话。
因此,我们需要把位置信息加入模型,让 Attention 不仅知道「有哪些 token」,还知道「它们分别位于什么位置」。
很容易想到一个直接的方案:给每个位置加一个「位置标记」。
其中:
是 token 的嵌入向量(「猫」的语义)
是位置向量(「第 i 个位置」的标记)
两者相加,得到一个既包含语义、又包含位置的新向量。
但语言中的很多关系并不只是「你在第几个位置」,而是「你和我相隔多远」。例如,一个词更关心前一个词、后两个词,还是相隔很远的词。
因此,现代模型常用 RoPE 等方法,把位置信息融入 Query 和 Key,使 Attention 能够利用 token 之间的位置关系。