跳至内容
8/30第 8 章,共 30 章

Next-token 预测:embedding,以及困惑度到底意味着什么

在 32,033 个名字上训练字符模型,看 gradient descent 将计数表重现到四位小数,再解释为什么困惑度很少能直接对齐。

本页内容

下面是一个从未见过单词的程序生成的十个名字:

TEXT
cexze   momakurailezitynn   konimittain   llayn   ka
da      moliellavo          emia          sade    ftlsp

它们没有一个是真名。但几乎每一个都在努力像真名。它们可发音,结束的位置也像名字会结束的地方,其中一个——emia——只差一个字母就是真名。生成它们的程序只持有 729 个数字,没有“词”“音节”或“人”的概念,并且只是通过一次统计相邻字母对完成拟合。

到本章结束时,一个神经网络会在同一项测量上把这个程序的分数降低三分之一。真正值得留下来看的,是这个网络最先做的事:它在没有任何提示的情况下,在每一个样本充足的行上把计数表重现到小数点后三位,因为这两个对象是在回答同一个问题。之后的一切,才是计数永远做不到的事。

目标是一个恒等式,不是设计选择

链接到此部分:目标是一个恒等式,不是设计选择

第 7 章给你留下了一串整数,却没有解释为什么一个整数应该跟在另一个后面。原因在这里,而且它就是第 2 章里的一行。

语言模型是一个函数:它接收到目前为止的 tokens,并返回下一个 token 的分布:词表中每个条目对应一个非负数,总和为一。仅此而已。要从这里得到整篇文档的概率,只需应用概率的链式法则:

P(x1,x2,,xT)=t=1TP(xtx1,,xt1)P(x_1, x_2, \ldots, x_T) = \prod_{t=1}^{T} P(x_t \mid x_1, \ldots, x_{t-1})

这是一个恒等式,对任何东西组成的任何序列都成立,不附带任何假设。因此,一个完成了“小任务”——给定前面的 tokens 预测下一个 token——的模型,已经完成了“大任务”:精确且免费地给每一篇可能的文档分配概率。把这件事流行地说成廉价小把戏(“它只是预测下一个词”)是把逻辑倒过来了:预测下一个 token 就是在建模联合分布。从来就没有第二件事要做。

损失也同样机械地推出。在每个位置,模型产生一个分布 qq,而真值是一个已知的 token,所以第 4 章的交叉熵原封不动地适用:

L=1Tt=1Tlogqθ(xtx<t)L = -\frac{1}{T}\sum_{t=1}^{T} \log q_\theta(x_t \mid x_{<t})

这就是平均负对数似然——第 2 章的配方,只是把高斯分布所在的位置换成了类别分布。并且由于真实分布是 one-hot,它的熵为零,所以根据第 4 章的恒等式,交叉熵等于 KL 散度:把这个数压低,与把模型的信念拉向数据,是同一个动作。

有一个推论值得单独成句,因为它是整个领域底层的经济事实。**标签就是数据,向后平移一个位置。**没有人需要标注任何东西。一万亿 tokens 的文本就是一万亿个预先标好标签的样本,这就是为什么现代模型的训练语料是“互联网”,而不是“某个人构建的数据集”。

在任何网络之前,先看基线:32,033 个名字,每行一个,任务是一次生成一个字母,产生更多这样的名字。1

词表是 26 个字母加一个边界符号 .,它同时标记名字的开始和结束,所以模型必须学会名字从哪里开始、到哪里停止。一共 27 个符号,而最小的可行模型,就是一张表:每个符号后面跟着另一个符号的频率。

bigram.pyPYTHON
N = torch.zeros((27, 27), dtype=torch.int32)
for w in words:
    cs = ["."] + list(w) + ["."]
    for a, b in zip(cs, cs[1:]):
        N[stoi[a], stoi[b]] += 1

P = N.float()
P = P / P.sum(1, keepdim=True)            # one distribution per row   

两行算术,模型就拟合好了——而且这不是启发式方法:用计数除以行总数,就是类别分布的最大似然估计,也就是第 2 章的配方,只不过微积分已经替你做完了。

TEXT
names: 32033        train/val/test: 25626 / 3203 / 3204
training bigrams: 182583

the six most likely letters after 'a':
    a -> '.'  0.1944   a -> 'n'  0.1600   a -> 'r'  0.0967
    a -> 'l'  0.0749   a -> 'h'  0.0690   a -> 'y'  0.0606

从它里面采样——从当前字母所在行选一个字母,移到那一行,重复直到边界符号出现——你就得到本章开头的那些名字。它们以一种具体且有启发性的方式失败了:局部合理,整体胡扯。momakurailezitynn 里的每一对相邻字母都是真实名字里出现过的字母对;问题只是这样的字母对连续出现了十七个。模型只有一个字母的记忆,所以它无法知道自己已经生成太久了。

在留出名字上的损失是 2.4546 nats。这个数字本身没有意义,这就是困惑度存在的原因:

PPL=exp ⁣(1Ttlogq(xtx<t))=eL\mathrm{PPL} = \exp\!\left(-\frac{1}{T}\sum_t \log q(x_t \mid x_{<t})\right) = e^{L}

不靠库来做,直接写出来:

perplexity.pyPYTHON
@torch.no_grad()
def perplexity(logits, Y):
    logp = F.log_softmax(logits, dim=1)          # log q for every symbol
    chosen = logp[torch.arange(len(Y)), Y]       # log q of the one that came next   
    return torch.exp(-chosen.mean())             

取指数会抵消对数,把数字带回到计数事物的单位。看清它在数什么的最干净方式,是测一个什么都不知道的模型——无论上下文如何,都给每个符号分配概率 1/271/27

TEXT
uniform over 27 symbols            loss 3.2958 nats   ppl  27.000
bigram counts, add-one smoothed    loss 2.4546 nats   ppl  11.642

正好是 27.000,因为 elog27=27e^{\log 27} = 27。**困惑度就是模型正在等效地在多少个同等可能的选项之间做选择。**困惑度为 27 意味着“完全不知道,什么都可能”。计数模型的 11.642 意味着,一个字母的上下文让它的不确定性相当于从大约十二个选项里盲选,而不是从二十七个里盲选——这就是为什么大家引用困惑度,而不是原始损失。

它有两个问题,第二个还经常出现在已发表论文里。

**零概率是致命的。**这张表的 729 个单元格里,有 113 个在训练中从未出现——15.5 % 是空的。在留出集落到其中某格之前,这没问题;而验证集中确实有七个 bigram 落了进去,其中包括 dqzj,以及出现两次的 qo。概率为零意味着 log -\infty,也就意味着无限损失和无限困惑度:三千个名字里的一个就能摧毁指标。常见补丁是在归一化前给每个计数加 1,在这里几乎没有代价(2.4546 而不是 2.4524)。但这个补丁本身就是一种承认。**计数模型完全无法泛化。**它无法怀疑 qo 是合理的,因为 qu 很常见,而且 o 在别处的行为像 u;它没有任何“两个符号可以相似”的概念。每个单元格都是单独学到的,而修复这一点,就是本章后半部分要做的事。

**困惑度是每个 token 的价格,而 token 是一个自由参数。**这是比较模型时不断出现的错误,而且一旦你认真看就很容易发现。取第 7 章里同一份英语散文语料、同一个插值 bigram 模型,只改变文本被切分的方式:

单位词表测试中的 tokens交叉熵困惑度每字符 bits
字符7614,4692.521712.453.6378
BPE,512 次 merge3296,8713.854747.212.6407
BPE,2,048 次 merge1,8204,2335.7468313.202.4254
2,9916,2843.562735.262.2322

这些行之间的困惑度相差 25 倍。模型本身没有任何变化;变的只是被预测对象的大小。预测整个词比预测一个字母更难,所以每次预测的成本更高——但需要做的预测次数也更少。

现在读最后一列:它改为用字符数量来除总成本,并转换成 bits。**它重新排列了这张表。**按困惑度排名是字符、词、BPE-512、BPE-2048;按每字符 bits 排名则是词、BPE-2048、BPE-512、字符。字符模型从第一名变成最后一名。按困惑度看,2,048-merge 模型比 512-merge 模型差 6.6 倍;但事实上,前者在 2.4254 bits 对 2.6407 bits 上更好。

所以,只有共享同一个 tokenizer 的两个模型,困惑度才可比较;使用不同 tokenizers 的模型,只能用每字符 bits 比较——这也是 Shannon 在 1951 年通过让人类受试者猜英语文本下一个字母所测量的量,并将其约束在每字符约一 bit。2 我们最好的 bigram 是 2.23 bits,这很好地概括了本章还差多远。

现在把同一个模型构造成网络。它会用多几个数量级的算术才到达同一个地方,而“到达同一个地方”正是重点。

用一个形状为 27×2727 \times 27 的权重矩阵 WW 替代表格。把当前字母变成 one-hot 向量,相乘,并把结果称为 logits——第 4 章里的未归一化分数。然后 softmax,然后交叉熵,然后 gradient descent。

neural_bigram.pyPYTHON
W = torch.randn((27, 27), requires_grad=True)

for step in range(3000):
    logits = W[xs]                            
    loss = F.cross_entropy(logits, ys)
    W.grad = None
    loss.backward()
    W.data -= 50.0 * W.grad

高亮行包含一个值得记住的定义。用 one-hot 向量乘以矩阵,就是选出矩阵的一行,所以这个乘法是一次查找——而每个实现都会跳过算术,直接做查找,这就是 W[xs]

**这就是 embedding 表。**一个矩阵,词表中每个条目一行,由 token id 索引。没有几何,没有语义,也没有单独的算法:它只是一个查找表,只不过其中的内容恰好和其他所有东西一起由 gradient descent 学到。所有关于“embedding space”的神秘说法,最终都落在这里。

训练它,看看它会走向哪里:

TEXT
  step     1   train 3.7550   val 3.3882   max gap to the count table 0.757269
  step   100   train 2.4732   val 2.4726   max gap to the count table 0.388354
  step  1000   train 2.4557   val 2.4549   max gap to the count table 0.041862
  step  3000   train 2.4547   val 2.4544   max gap to the count table 0.004048

最后一列是 softmax(W) 的任意单元格与计数表对应单元格之间的最大绝对差,它趋向于零。3,000 步之后,729 个单元格里任何位置的最大分歧都是 0.004048,平均值为 0.000224。最糟的单元格是 qi,在整个训练集中只见过十二次;在出现次数超过一千的 22 行里,最糟分歧是 0.000562。

TEXT
                 count table   network
    a -> '.'        0.1945     0.1945
    a -> 'n'        0.1601     0.1601
    a -> 'r'        0.0967     0.0967

gradient descent 从随机数字出发,只被告知“让下一个字母的对数概率变大”,最终重新发现了计数表。而且它不得不如此:计数是最大似然估计,交叉熵是负对数似然,所以两套过程优化的是同一个目标,而这个目标只有一个最优解。网络学到的不是类似计数的东西。它缓慢地收敛到了计数。

这就引出了一个合理的问题:为什么还要费这个劲?因为计数表从这里再也无路可走,而网络可以。

把模型扩展到查看不止一个前置字符。这是 Bengio 2003 年的架构,是本课程后续每个模型的直系祖先:取最后三个字符,通过 embedding 表把每个字符映射成 10 维的一行,把这些行拼接成 30 个数字,送入第 5 章的隐藏层,最后接一个输出层,为词表中每个条目产生一个 logit。4

mlp.pyPYTHON
C  = torch.randn((27, 10))          # the embedding table
W1 = torch.randn((3 * 10, 200))     # the hidden layer from Chapter 5
W2 = torch.randn((200, 27))         # one output per vocabulary entry

emb = C[X].view(-1, 30)             # three lookups, concatenated   
h = torch.tanh(emb @ W1 + b1)
logits = h @ W2 + b2                
loss = F.cross_entropy(logits, Y)

注意哪些是新的,哪些不是。隐藏层还是第 5 章的隐藏层,没有改变;损失还是第 4 章的损失,没有改变。新东西是前面的 embedding 表,以及一个与第 7 章词表一样宽的输出层——而第二个正是有史以来每个语言模型最昂贵的部分,因为真实词表有 100,000 个条目,而这个矩阵乘法在每个位置都会运行。

同样的代码,以同样方式训练,只改变 context window 的大小:

context参数验证损失验证困惑度
计数,1 个字符7292.454611.642
神经网络,1 个字符7,8972.457711.678
神经网络,3 个字符11,8972.11458.285
神经网络,8 个字符21,8972.05067.773

第二行才是有意思的。一张拥有 200 单元隐藏层、参数量是计数表 11 倍的网络,表现和计数表完全一样,并不更好。容量从来不是限制。一个字符的 context 只允许某个损失水平,不管你往上加什么都低不过它,因为信息并不在那里。

给它三个字符,困惑度从 11.68 降到 8.29——下降 29 %,代价是额外 4,000 个参数。它在这里打败计数模型,原因正是前面诊断出的那一个:基于三字符 context 的计数模型需要 273=19,68327^3 = 19{,}683 行,其中大多数为空或只含一次观察,并且每一行都单独学习。网络会共享。如果 aei 最终有相似的 embedding 行,那么它在 bra 之后学到的东西,就会迁移到 bre,即使它从未见过 bre。这种迁移就是 embedding 表的全部价值,也正是第二行和第三行之间的差距。

样本也相应改善:

TEXT
deliah   nellara   joce     kael      quintis
salayson  reety    khyrmin  mahnen    madiaryxia

仍然不是真名列表。但 deliahnellarakael 放在真名列表里并不会显得突兀,而且那些一路跑下去的怪物消失了:计数模型二十个样本中最长的是十九个字母,而这个模型二十个样本中最长的是十三个。

这张表是 27×1027 \times 10:每个字符一行十个数字,全部随机初始化,并且只被下一个字符损失的 gradient 移动。没有人往里面放任何东西。那么最后里面出现了什么?

提问的工具是余弦相似度,也就是第 1 章中的点积,但把长度除掉:

cos(a,b)=abab\cos(\mathbf{a}, \mathbf{b}) = \frac{\mathbf{a} \cdot \mathbf{b}}{\lVert \mathbf{a} \rVert \, \lVert \mathbf{b} \rVert}

它测量两个向量之间的角度,并忽略它们的长度;这正是你想要的,因为某一行的长度反映的是它的 token 出现频率,而不是它的含义。先把每个向量归一化到长度 1——真实系统在索引时也会这样做一次——余弦相似度就只是点积。

下面是训练后表中几个字符的最近邻:

TEXT
  'c' -> 'k':+0.598      'j' -> 'z':+0.650      'i' -> 'y':+0.541
  'u' -> 'e':+0.482      'a' -> 'h':+0.367      '.' -> 'q':+0.077

其中一部分正是民间说法承诺的那样。ck 在名字中可以互换,iy 也一样;jz 都是罕见、主要出现在开头的辅音,行为相似。边界符号 . 几乎不靠近任何东西——离最近字母也只有 0.077——因为它是唯一一个标记位置而不是声音的符号。

但也有一些并非如此。a 的最近邻是 h,不是另一个元音。对所有配对求平均:

TEXT
mean cosine, vowel to vowel         : +0.1889
mean cosine, consonant to consonant : +0.0765
mean cosine, vowel to consonant     : -0.0042

元音彼此之间确实比它们和辅音之间更相似,而且这个效应真实存在,但很小。与 2,000 个随机选出的五字母组相比,其中有 58 组至少分离得同样干净——差距的显著性大约是 p=0.03p = 0.03。所以它是真的,但完全不像大众叙述中 embeddings 暗示的那种清晰几何岛屿。

这就是对 embedding 表的诚实描述,而且在本课程后续值得一直记住。它不是意义地图。它是一种坐标变换,是学出来而不是设计出来的,唯一工作就是让下一层的工作变容易——这和第 5 章描述隐藏层折叠平面以解决 XOR 时用的是同一句话。你在其中发现的任何结构,都是因为它降低了损失;不能降低损失的结构,就根本不会在那里。

word2vec、GloVe,以及人人引用的算术

链接到此部分:word2vec、GloVe,以及人人引用的算术

如果有用的部分是表,你就可以直接追它。这就是 word2vec:保留 embedding 查找,扔掉语言模型。5

带负采样的 skip-gram 目标只有一行。对一个从语料中抽到的真实(中心词,context)对,把它们的点积推高;对 kk 个从噪声分布中抽到的假配对,把点积压低:6

logσ(vcvo)+i=1klogσ(vcvni)\log \sigma(\mathbf{v}_c \cdot \mathbf{v}_o) + \sum_{i=1}^{k} \log \sigma(-\mathbf{v}_c \cdot \mathbf{v}_{n_i})

这是一个二分类——“这两个词真的一起出现过吗?”——而它便宜,正是因为它永远不接触完整词表,这使得 2013 年在数十亿词上训练成为现实。GloVe 从另一个方向得到相似的向量:它分解全局共现计数矩阵,而不是流式遍历样本。7 两者拟合的正是计数表所构建的统计量。它们是被压缩的计数。

text8 上训练——17,005,207 个英文 Wikipedia 词,其中 71,290 个至少出现五次,100 维,三遍——向量展现出让它们出名的性质:

TEXT
king     -> charles 0.700, son 0.693, queen 0.686, henry 0.669, throne 0.667
physics  -> chemistry 0.672, electromagnetism 0.661, quantum 0.654, theoretical 0.624
guitar   -> bass 0.733, vocals 0.732, acoustic 0.728, guitars 0.703, drums 0.685
three    -> seven 0.892, two 0.877, one 0.875, five 0.871, four 0.870

没有人为乐器或数字提供类别。现在到了著名部分:取 king,减去 man,加上 woman,然后找离结果最近的向量。

TEXT
king - man + woman
   nothing excluded : king 0.693, elizabeth 0.657, wife 0.629, woman 0.607
   a, b, c excluded : elizabeth 0.657, wife 0.629, mary 0.607   (queen is 4th, 0.604)

king - man + woman 最近的向量是 king。这不是某个例子的怪癖。Mikolov 的评测集提出形如 a : b :: c : ? 的问题——8,869 个语义问题(paris : france :: rome : italy)和 10,675 个句法问题(walking : walked :: swimming : swam)——而在这个词表能回答的 4,103 个语义问题中,胜者有 99.8 % 的时间是三个输入词之一。已发表的演示没有提到这一点,因为标准评分规则会先删除 abc 再寻找答案。这是一个合法规则,而且它比算术本身做了更多工作:

答案如何选择语义句法
offset,排除输入词(标准)17.0 %11.9 %
offset,不排除任何词0.1 %0.4 %
只取 c 的最近邻,排除输入词13.1 %9.3 %
只取 b 的最近邻,排除输入词2.3 %0.4 %

第三行值得多停留一下。扔掉 ab,完全不做算术,直接返回离 c 最近的东西——你仍然保住了 77 % 的语义得分。大多数看起来像类比推理的东西,其实是近邻关系加上一条禁止显而易见答案的规则;这正是 Linzen 在恰当训练的向量上测到的现象,也是上面的基线复现出的现象。8 这些特定向量很小——1,700 万词,相比已发表模型背后的数十亿词——所以请把百分比读成一种形状,而不是 state of the art。这个形状在任何规模上都保留下来:算术是真的,也远弱于人人引用的那一个演示。

静态与上下文:每个词一个向量,还是每次出现一个向量

链接到此部分:静态与上下文:每个词一个向量,还是每次出现一个向量

到目前为止的一切,都在数据结构中内置了一个硬限制。**一张表对每个 token 只有一行。**单词 bank 得到一个向量,在关于河岸的句子里和关于抵押贷款的句子里是同一个向量——这是必然的,因为按 id 查找不可能依赖其他任何东西。

修复办法是停止从表中读取向量,转而从句子中计算它。这就是 contextual embedding,由 ELMo 在 2018 年引入,并在同年由 BERT 变成标准。910 在真实模型上测量时,数字比解释更尖锐:

TEXT
sentence A: "He sat on the bank of the river and watched the water go by."
sentence B: "She deposited the cheque at the bank on the corner of the street."

static vector for 'bank' (a row of the input embedding table)
    cosine A vs B ........................ 1.000000

contextual vector for 'bank', layer by layer
    layer  |  A vs B  |  A vs another river sentence  |  B vs another money sentence
        0  |  0.9512  |            0.9512             |            0.9359
        4  |  0.5647  |            0.8987             |            0.7716
        9  |  0.4284  |            0.8699             |            0.7568
       12  |  0.5278  |            0.8702             |            0.7335

第一行是精确的,不是近似:bank 的静态向量在两个句子中都是同样的 768 个数字,所以余弦按构造就是 1。九层之后,两次出现的相似度降到 0.43,而两个不同河流句子中的 bank 仍然保持在 0.87。这个过程中没有人在任何地方标注词义;词义分开了,是因为把它们分开能让训练目标——根据邻居猜一个被隐藏的 token——更容易满足。

有两个细节值得注意。第 0 层已经是 0.9512 而不是 1.0,因为位置 embeddings 已经被加上,而且这个词在两个句子中的位置不同。并且相似度在第 11 和第 12 层又升了回来:预训练模型的最后几层专门服务于它的训练目标,往往不是取表示的最佳位置。

查看详情

可选:权重绑定。

bert-base-uncased 中,embedding 表是 30,522×76830{,}522 \times 768——23,440,896 个数字,占模型 109,482,240 个参数的 21.4 %。在小语言模型里,这个比例还更大,所以有一个技巧几乎无处不在:输入表和产生 logits 的输出层是同一个矩阵,一次按行查找使用,一次转置后使用。输出层本来就会给词表中的每个条目分配一个向量——它会和每个向量做点积——而绑定的意思是,用来一个 token 的向量,和用来它的向量,应该是同一个对象。它既减少参数又改善困惑度,这种事罕见到值得注意。

要按意义搜索语料,你需要每个句子一个向量。有了这些向量,搜索就很简单——这就是语义检索的全部,而第 19 章讨论的是它周围的一切:

search.pyPYTHON
E = normalise(embed(sentences))       # (200, d), every row of length 1
q = normalise(embed([query]))         # (1, d)
scores = q @ E.T                      # one matrix multiply   
top5 = scores[0].argsort()[::-1][:5]

所以唯一真正的问题是 embed 从哪里来。显然的做法是拿一个预训练语言模型,把每个句子跑一遍,然后平均 token 向量。下面把这种方法与四种替代方法比较,用两种方式打分:在 STS benchmark 的 1,379 对句子上,余弦与人类相似度判断之间的秩相关;以及在由其中 200 对最强改写句构建的索引上的 top-1 检索——每对的一边放入索引,另一边作为查询。

句子如何 embedding秩相关200 句索引上的 top-1
二元词重叠(完全没有模型)0.550089.0 %
上面训练的静态向量平均0.526385.5 %
BERT,[CLS] token0.203067.0 %
BERT,token 向量平均0.472984.0 %
MiniLM,对比式训练0.820392.0 %

把中间三行和前两行对照来看。一个 1.09 亿参数的预训练 transformer,以最显然的方式使用时,在判断句子相似度上比数两个句子共享多少词还差——也比刚才训练的 100 维 text8 向量平均更差。[CLS] token 更是还不到它的一半;教程仍然推荐它,是因为 BERT 预训练时在它上面接过一个句子级目标。

这不是 BERT 的缺陷。这是目标的问题。语言模型被训练成让它的隐藏状态预测 token;其中没有任何东西要求两个改写句彼此靠近,也没有任何奖励一种让余弦意味着“同义”的几何。最后一行是一个只有五分之一大小的模型(22,713,216 个参数),训练在完全不同的损失上:对比学习,其中样本是配对——一个问题和它的答案、一个句子和它的改写——目标是把真配对拉近,同时把采样负例推远。这就是 Sentence-BERT 的贡献,也是整个 embedding-model 行业的起点。11 Dense Passage Retrieval 把同样的配方直接用于搜索,一个编码器处理查询,另一个处理段落。12

所以,实践规则是:

**embedding model 不是去掉最后一层的语言模型。**它是一个在不同目标上训练的不同模型,通常小得多;它的余弦之所以意味着你想要的东西,是因为它在以此为目标的配对数据上训练过。上表就是把两者互相替代的代价。

而这一类模型会在词序上失败。“The dog bit the man”和“the man bit the dog”有完全相同的词袋,所以词重叠和静态向量平均会给出正好 1.000000 的余弦;mean-pooled BERT 虽然看得到位置,却仍然几乎落在那里——而对比式训练的 MiniLM 也仍然给出 0.979。如果你的检索任务取决于谁对谁做了什么,任何余弦阈值都救不了你。

第 19 章会在这个基础上构建一个生产级检索系统,并得到一个具体的余弦截断值。本章最后的测量,正是让这样的数字站得住脚,而不是显得像魔法。

真实 embeddings 有数百或数千个分量,而距离在那样的高维空间里行为很奇怪。取 dd 维单位立方体中的 1,000 个随机点,看看任意两点之间最大距离与最小距离的比值:

维度最近点对最远点对比值
20.00071.36121921.66
100.23612.33979.91
1003.00475.17521.72
1,00011.780914.03061.19
10,00039.615242.01251.06

在一万维里,最远点对只比最近点对远 6 %。一切与其他一切都大致等距,“最近邻”不再携带太多信息,这就是维度灾难——也是大型向量数据库不做精确最近邻搜索的原因之一。同一枚硬币的另一面,则让余弦阈值变得可用:在一千对随机单位向量上测量,平均余弦在 100 维时位于 0.0052-0.0052,在 768 维时位于 +0.0003+0.0003,标准差分别为 0.0968 和 0.0357——而在 768 维中,只有 0.2 % 的随机配对绝对值超过 0.1。因此,测得的 0.4 相似度并不是“40 % 相像”;它远在随机可能产生的范围之外,这就是为什么 0.3 到 0.7 之间的阈值能把信号和噪声分开,而不是坐在中间。

本章的模型读取固定数量的前置字符,查找每一个字符,并按顺序把结果粘在一起。这个设计有两个问题,而它们其实是同一个问题。

再看一次 context 表:从三个字符增加到八个字符,参数几乎翻倍,只换来 0.06 nats。成本随 context 线性增长——每多一个位置,都需要第一层权重矩阵里属于自己的那一块——而收益并不如此。把它推到一千个 tokens,光第一层就会比模型其余部分更重,其中大多数都花在对某次预测并不重要的位置上。

第二个问题也正是如此:模型没有办法决定前面的哪些 tokens 重要。位置二有自己的权重,位置七也有自己的权重,而且永久如此,不管里面是什么。当模型在拼写 nell 时,决定性的字符是紧前面的那个。当一个句子包含代词时,确定其指代对象的词可能在四十个 tokens 之前——而没有任何固定槽位可以被指定为“四十个之前”,因为下一次它可能是六个之前。

我们想要的是这样一个模型:对每次预测,它都会计算每个更早的 token 应该算多大权重——这些 context 上的权重由内容产生,而不是由布局固定。把这件事仔细写下来,它一开始是完全普通的东西:对前面 tokens 的平均。然后让这个平均的权重被学习,并让它们依赖于正在发问的是哪个 token。

这就是 attention,也就是第 9 章


还值得一起阅读:Jurafsky 和 Martin 的 Speech and Language Processing 第 3 章,它比这里篇幅允许的空间更仔细地处理 n-gram 模型、平滑和困惑度,也解释了为什么插值和 back-off 胜过加一;Stanford CS229 笔记 §17.1–17.2,从概率角度讲语言建模;以及上面的 Linzen 论文,它很短,值得全文阅读。

  1. 名字生成示例、数据集,以及从计数表推进到 Bengio 风格网络的路径,都遵循 Andrej Karpathy 的 building makemore 系列;其前两部分是本章最好的配套材料。

  2. Shannon, C. E. Prediction and Entropy of Printed English. Bell System Technical Journal 30(1), pp. 50–64 (1951). 让人类受试者猜英语下一个字母,以及最初的每字符 bits 测量。

  3. Shannon, C. E. A Mathematical Theory of Communication. Bell System Technical Journal 27 (1948). 信源编码定理,以及把预测与压缩等同起来的思想。

  4. Bengio, Y., Ducharme, R., Vincent, P. and Jauvin, C. A Neural Probabilistic Language Model. Journal of Machine Learning Research 3, pp. 1137–1155 (2003). 上文使用的架构:每个词一个 embedding,在固定窗口上拼接,经过隐藏层,再到词表上的 softmax。

  5. Mikolov, T., Chen, K., Corrado, G. and Dean, J. Efficient Estimation of Word Representations in Vector Space. arXiv:1301.3781 (2013). CBOW 和 skip-gram,以及上文使用的类比集合。

  6. Mikolov, T., Sutskever, I., Chen, K., Corrado, G. and Dean, J. Distributed Representations of Words and Phrases and their Compositionality. arXiv:1310.4546 (2013). 负采样、高频词下采样,以及上文使用的 3/4 次幂噪声分布。

  7. Pennington, J., Socher, R. and Manning, C. GloVe: Global Vectors for Word Representation. EMNLP 2014. 从全局共现矩阵的分解得到词向量,而不是使用流式局部窗口。

  8. Linzen, T. Issues in evaluating semantic spaces using word analogies. RepEval 2016, arXiv:1606.07736. 上文复现的无 offset 基线的来源。

  9. Peters, M. et al. Deep contextualized word representations. arXiv:1802.05365 (2018). ELMo:每次出现一个向量,由双向语言模型计算得到。

  10. Devlin, J., Chang, M.-W., Lee, K. and Toutanova, K. BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding. arXiv:1810.04805 (2018). 在 bank 实验中测量的模型。

  11. Reimers, N. and Gurevych, I. Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks. arXiv:1908.10084 (2019). 它开篇的测量——mean-pooled BERT 在句子相似度上弱于静态向量平均——正是上表复现的内容。

  12. Karpukhin, V. et al. Dense Passage Retrieval for Open-Domain Question Answering. arXiv:2004.04906 (2020). 双编码器检索器的对比式训练;第 19 章检索栈的直接祖先。


作者

David Vicente Campos

NeuraLIA Labs 创始人、MyRealFood 联合创始人

我是莱昂大学毕业的计算机工程师。我共同创立了 MyRealFood,并在那里作为 CTO 打造了一款数百万人用来吃得更健康的应用;我还创立了 NeuraLIA Labs,在这里我打造 AI 产品。我在本站写下一路走来所必须理解的内容,就像我希望当初有人向我讲解的那样。

了解作者更多信息

由 NeuraLIA Labs 发布。

新文章直达你的收件箱

AI 新闻、指南和产品更新——有值得你花时间阅读的内容时,我们会发一封简短邮件。

更喜欢用消息接收?同样的内容,也在这里:WhatsApp 社群 (在新标签页打开)Telegram 频道 (在新标签页打开)

课程目录

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jev10 分钟阅读

Jev AI 模型为决策而生,而非写作

TypeSafe AI 的 Jev 正受到关注,因为它把软件智能视为一个概率问题:选择正确分支,附上置信度,并避免在代码只需要决策时还花钱让 LLM 写文本。

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineering12 分钟阅读

Context engineering for long-horizon AI agents

Long-running agents do not fail only because the window is small. They fail when files, tool outputs and stale history crowd out the task the agent was supposed to finish.

准备好让 LIA 替你选模型了吗?

所有 AI 模型都在一处——今天就免费开始。