上一课预测一个数。这一课改为预测一个离散选择:给定目前看到的字符,下一个字符可能是什么?从相邻字符计数开始,就能得到最小的语言模型。
为什么标签要向后移动一位
把名字 anna 写成 .anna.,其中点号代表边界。我们得到五对输入和目标:.→a、a→n、n→n、n→a、a→.。输入是当前位置,标签是下一位置。边界符让模型知道怎样开始,也知道什么时候结束。
对于更长的模型,输入 [t0,t1,t2] 对应目标 [t1,t2,t3]。它们不是完全相同的序列。若把输入本身作为答案,模型可能学会复制,却没有学到生成下一个 token 的任务。
把计数变成条件概率
用 anna、anne、bob 三个名字构造字符转移表。词表包含边界符、a、n、e、b、o,共六种字符。a 出现的后继里,n 有两次,边界符有一次。未经平滑时,P(n|a)=2/3。
若对六个候选都加 1,a 这一行的总计数变为 3+6=9,n 的计数变为 2+1=3,因此平滑后的 P(n|a)=1/3。平滑给未见过的转移留下非零概率,但也改变了原本的概率分布。
这个模型叫 bigram,因为它只看当前一个 token。它知道“a 后面常接什么”,却不知道更早的字符。后面引入更长上下文,就是为了弥补这个限制。
概率怎样变成训练目标
正确字符的概率越高,负对数似然 −log(p_correct) 越小。若某一步正确答案概率是 0.5,这一步损失约为 0.693;概率是 0.1 时,损失约为 2.303。对许多位置求平均,可以得到一组样本的平均损失。
下面是计算这个目标的最小示例。自然对数对应的单位是 nat,比较实验时应保持同一种定义。
import math
correct_probabilities = [0.5, 0.1, 0.8]
loss = sum(-math.log(p) for p in correct_probabilities)
loss /= len(correct_probabilities)
print(loss)
不要把训练损失当成独立评估结果。即使是这个小实验,也应先按名字或文档划分训练与验证,再只用训练数据建立计数表。
采样和训练是两件事
生成时从边界符开始,在当前行的概率分布中抽取一个字符,再把它作为下一步输入,直到抽到结束符。抽样有随机性:同一个分布可以生成不同名字。一段好看的输出不代表整个模型变好了。
看 makemore 第一讲时,把“构造数据”“计算损失”“采样”三个过程分开记录。下一课会把这张固定计数表换成可训练的 embedding 和 MLP,让模型同时利用多个位置。