bigram 只看一个 token。如果需要根据前三个字符预测下一字符,我们需要把多个位置的信息交给一个可训练的函数。embedding 与 MLP 是一个足够小、也足够有说明力的起点。
token ID 只是地址
假设 a 的 ID 是 1、b 的 ID 是 2,并不表示 b 比 a“大一倍”。ID 的作用是从 embedding 表里取出一行。若词表大小为 V,每个向量有 E 个维度,embedding 表就是 [V,E]。
查表后,离散 ID 变成了可以连续调整的向量。训练更新的是这张表里的数值,而不是字符的编号。向量开始可以是随机数,它们是否包含有用关系,要由训练任务和数据决定。
用形状追踪一次前向过程
设一次有 B=4 个样本,每个样本看 T=3 个历史 token,每个 embedding 有 E=8 个数。输入 ID 的形状是 [4,3],查表后是 [4,3,8]。把每个样本的三个位置展平,得到 [4,24]。
接着用线性层和非线性函数形成 H 维隐藏状态,再输出 V 个分数。完整形状链是 [B,T] → [B,T,E] → [B,T×E] → [B,H] → [B,V]。最后这 V 个数是 logits,还没有归一化成概率。
import torch
from torch import nn
vocab_size, context, embedding_size = 6, 3, 8
embedding = nn.Embedding(vocab_size, embedding_size)
hidden = nn.Linear(context * embedding_size, 16)
output = nn.Linear(16, vocab_size)
ids = torch.randint(0, vocab_size, (4, context))
x = embedding(ids)
logits = output(torch.tanh(hidden(x.flatten(1))))
print(x.shape, logits.shape)
为什么需要非线性和留出集
多层线性变换如果中间没有非线性,整体仍可合并为一个线性变换。激活函数让模型可以表达更复杂的关系。不过,表达能力增加也意味着更容易记住训练数据,因此验证集从这一刻开始更重要。
比较 bigram 和 MLP 时,保持同一份数据划分和同一种损失定义。不要只比较参数量,也不要在看到验证结果后不断手工挑选验证样本。需要反复调参时,再保留一份最终测试集。
上下文窗口也有边界
这个 MLP 把固定长度的历史向量拼在一起。超出窗口的内容不会参与预测,改变窗口长度还会改变第一层输入尺寸。它帮助我们建立上下文直觉,但还不是能够按内容动态关联位置的注意力模型。
跟视频做完小 MLP 后,保存形状表和训练/验证曲线。接下来的两课先解决“它为什么不好训练”,再把输入从简单字符扩展到更实用的分词。