一、先接上第 7 讲的半成品
第 7 讲把一段文字切成了一串离散的编号——“苹果"可能是词表里第 4821 号,“公司"可能是第 917 号。但这串编号本身,对模型来说还是不能直接用的半成品。
回忆第 3 讲开头的警告:把词编号成 1、2、3……,编号之间的大小关系没有任何意义——4821 不比 917 更"大"或更"重要”,这只是一个身份证号,不是一个可以参与计算的量。第 3 讲要的是向量:一串数字,使得意思相近的词,向量也相近。
这一讲要解决两件事:怎么把第 7 讲切出来的编号,变成第 3 讲要的向量;以及一个更容易被忽略、但同样重要的问题——顺序信息去哪了。
二、打个比方:一排编号储物柜
想象一整排储物柜,编号从 1 一直排到词表的大小(几万到十几万)。每个 token 编号,对应一个专属的储物柜——4821 号词的向量,就放在 4821 号柜子里。
柜子一开始装的是什么?随机的杂物(第 5 讲结尾说过,参数一开始是随机初始化的)。柜子里的东西不是凭空正确的,而是训练过程中被反复"整理"出来的:每次模型猜错一个词,第 6 讲的反向传播就会顺着链条,回来微调一下相关柜子里的内容,让它更有助于猜对。训练跑得足够久,柜子里的内容就会自然沉淀出意思——“苹果"和"香蕉"的柜子会因为在相似的语境里反复被同时用到而变得越来越像,“苹果"和"民主"的柜子则始终八竿子打不着。
三、把直觉钉成数学
词嵌入:一张会被训练的查表矩阵
给词表里的每一个 token,配一个 d 维向量(d 是设计时选定的超参数,比如几百到几千),把所有向量按行摞起来,就是一张 嵌入矩阵(embedding matrix) E,大小是"词表大小 × d":
[ 0.12 -0.5 ... ] ← 编号 0 的向量
E = [ 0.88 1.2 ... ] ← 编号 1 的向量
[-0.31 -0.7 ... ] ← 编号 2 的向量
[ ... ... ]
[ 0.44 0.9 ... ] ← 编号 4821 号("苹果")的向量
要取 4821 号 token 的向量,直接取出 E 的第 4821 行——这个操作叫查表(lookup)。
⭐ 这个查表操作,其实是第 3 讲讲的矩阵乘法的一个特例,值得点破:如果把 token 编号 4821 写成一个 one-hot 向量(长度等于词表大小,除了第 4821 位是 1,其余全是 0),那么这个 one-hot 向量乘上 E,算出来的结果,恰好就是 E 的第 4821 行——因为乘法里几乎所有项都被 0 乘掉了,只留下那一行:
one-hot(4821) = [0, 0, ..., 0, 1, 0, ..., 0] ← 只有第 4821 位是 1
one-hot(4821) · E = E 的第 4821 行
查表和矩阵乘法在数学上是同一件事,实现时直接查表更快(不需要真的做一次几万维的乘法),但这条等价关系说明:词嵌入没有引入任何新的数学工具,它就是第 3 讲讲过的"变换”,只是变换的输入被限制成了 one-hot 向量这种极端情况。
词嵌入(word embedding):一张大小为"词表大小
× d“的矩阵,每一行是一个 token 对应的d维向量。这张矩阵本身是模型的参数,和第 5、6 讲讲的W、b属于同一类东西,由训练自动调整,不是人工指定的。
⚠️ 这里要提醒一句和第 3 讲相呼应的局限:这张表给出的是静态向量——“苹果"不管出现在"苹果公司"还是"咬一口苹果"里,查表查出来的都是同一个 4821 号向量。这个"一词一个固定向量"的空隙,还是没有被填上,第 9 讲的自注意力才是真正去填它的地方。这一讲给出的,只是模型看到的起点。
位置编码:顺序信息从哪来
现在看一个容易被忽略的问题。“狗咬人"和"人咬狗"用的是完全相同的三个 token(“狗"“咬"“人”),唯一的区别是顺序,但意思截然相反。
第 9 讲会正式证明这件事,这里先把结论摆出来:自注意力这个机制,从数学结构上看,是在一个"词的集合"上计算的——如果不做任何额外处理,把"狗咬人"三个字的顺序打乱喂进去,自注意力算出来的结果在数学上不会有任何变化,它天生分不清谁在前谁在后。
⚠️ 这不是一个可以忽略的小毛病——语言的意思大量地依赖顺序,一个不区分顺序的模型,连基本的语法都无法把握。必须有一种机制,把"这是第几个词"这件事,也编码进向量里,模型才能用上顺序信息。这就是**位置编码(positional encoding)**要解决的问题。
最直接的方案:像词嵌入一样,再建一张查表矩阵,这次索引不是"这是哪个词”,而是"这是第几个位置”——位置 1 一个向量,位置 2 一个向量……把它和词嵌入按位置相加,作为模型真正看到的输入:
模型的输入向量 = 词嵌入(这个词是什么) + 位置嵌入(这是第几个位置)
这种"直接学一张位置的查表矩阵"的方案叫可学习位置嵌入(learned positional embedding),和词嵌入训练方式完全一样,也会遇到第四节要讲的一个具体代价。
另一种经典方案不用学习,而是用数学公式直接构造出每个位置的向量,叫正弦位置编码(sinusoidal positional encoding):向量的每一维,用不同频率的正弦或余弦波,按位置编号取值:
第 2i 维: sin(位置 / 10000^(2i/d))
第 2i+1 维: cos(位置 / 10000^(2i/d))
不需要记住这个公式本身,只需要抓住直觉:向量里低编号的维度,振荡频率很慢(走很多个位置,数值才变化一点点);高编号的维度,振荡频率很快(挪一个位置,数值就变化很多)——这有点像一块有很多根指针的表盘,秒针转得飞快、时针转得很慢,把所有指针的位置合在一起看,任意两个不同的时刻,指针组合都是独一无二的。每个位置因此拿到一个独一无二的"指纹”,而且因为正弦、余弦函数本身满足漂亮的加法公式,两个位置之间"差了多少"这件事,可以直接从它们的指纹里用线性运算读出来,不需要重新训练。
四、这一讲的代价:位置编码的选择不是免费的
词嵌入表本身的参数量不小:词表大小乘以向量维度,如果词表 10 万、维度 1000,光这一张表就是 1 亿个参数——在模型总参数量里往往占相当可观的一块,这是"词表切得越大越省 token”(第 7 讲)和"词表越大参数越多"之间的一个直接权衡。
⭐ 位置编码方案的选择,会直接决定模型能不能处理训练时没见过的长度——这是这一讲留给第 23 讲最重要的一个伏笔:
| 方案 | 直觉 | 能否外推到训练时没见过的更长序列 |
|---|---|---|
| 可学习位置嵌入 | 每个位置一个查表向量,和词嵌入训练方式相同 | 差:如果训练时最长只见过 2000 个位置,第 2001 号位置的向量从来没被训练过,模型对它一无所知 |
| 正弦位置编码 | 用固定的数学公式直接算出每个位置的向量,不需要训练 | 理论上可以计算任意位置的向量,但实践中模型没见过那么长的序列时,效果依然会明显下降 |
⚠️ 表格第二行"理论上可以但实际效果有限"这句话很重要:位置编码方案解决的是"给不给得出一个位置向量”,不完全解决"模型有没有学会怎么用一个从没见过的位置向量”——这正是第 23 讲要展开讲的"上下文长度是工程难题,不只是调个参数"的一个具体来源。
⚠️ 关于时效性:正弦位置编码是最早的 Transformer 论文里用的方案,今天主流模型用的位置编码方案已经演化了好几代(比如把位置信息直接构造进注意力计算本身,而不是加在输入向量上的方案)。具体用哪一种会持续演化,但这一讲要记住的设计约束不会过时:自注意力天生不带顺序信息,某种方式的位置编码是必需品,不是可选项;而"能不能处理比训练时更长的序列”,始终是评价任何一种位置编码方案的核心标准。
五、和后面课程的关系
- 第 9 讲直接从这一讲的产出(词嵌入 + 位置编码相加后的向量)开始:自注意力要做的事,正是让这些目前还是"静态"的向量,根据上下文互相调整、动态更新——这一讲给的是起点,第 9 讲给出"怎么根据上下文修正"。
- 第 23 讲会把这一讲第四节埋的伏笔正式展开:位置编码方案的选择,是"上下文长度是工程难题"这个结论最直接的技术来源之一。
- 第 26 讲讲向量检索时,“用向量表示语义"这个思想会从"一个 token"延伸到"一整段文本”——句子、段落、文档也可以有自己的 embedding,原理和这一讲讲的 token 嵌入完全一样,只是被嵌入的对象变大了。
六、本讲小结
- 词嵌入是一张"词表大小
× d“的查表矩阵,每个 token 编号对应一行向量;这张表本身是模型参数,随训练自动调整,和第 5、6 讲的W、b是同一类东西。 - ⭐ 查表和矩阵乘法在数学上等价:one-hot 向量乘嵌入矩阵,等于直接取出对应那一行——词嵌入没有引入新工具,只是第 3 讲讲过的"变换"的一个特例。
- ⚠️ 词嵌入给出的向量是静态的:同一个 token 永远对应同一行,一词多义的问题在这一讲依然没有解决,要等第 9 讲的自注意力。
- ⭐ 自注意力天生分不清顺序,“狗咬人"和"人咬狗"如果不额外处理会被当成同一回事——必须用位置编码把"这是第几个位置"注入进向量里。
- 两种代表性方案:可学习位置嵌入(像词嵌入一样训练)和正弦位置编码(用固定公式构造,理论上可以覆盖任意长度,但实际效果依然受训练时见过的长度限制)。
- 位置编码的选择,是第 23 讲"上下文长度是工程难题"的直接技术来源之一——不管用哪种方案,“能不能处理比训练时更长的序列"始终是绕不开的短板。
思考题
- one-hot 向量乘嵌入矩阵等价于查表——如果
d=3,词表大小是 5,手写出编号 2 的 one-hot 向量,并验证它和一个5×3的嵌入矩阵相乘,结果确实等于矩阵的第 2 行。 - 为什么说"查表"在实现时比"真的做一次矩阵乘法"更快?(提示:one-hot 向量里有多少个数字是 0,这些位置的乘法在算什么无意义的事)
- “苹果"作为一个 token,查表查出来的向量是固定的,不随上下文变化——请举一个具体的句子对,说明这种"一词一个固定向量"会在什么场景下造成明显的语义损失(可以直接借用第 1 讲的例子,也可以自己想一个新的)。
- 用第三节"表盘"的比方解释:为什么正弦位置编码里,同时使用"转得快"和"转得慢"的维度,比只用一种频率更能区分大量不同的位置?
- 假设一个模型训练时序列最长见过 2000 个 token,用的是可学习位置嵌入。如果推理时输入了一段 3000 个 token 的文本,第 2001 到 3000 号位置会发生什么?这和"正弦位置编码理论上能算出任意位置的向量,但实际效果依然有限"这句话是不是同一个问题的两种不同表现?