前面的模型把一个字符当成一个 token。实际文本还包含多语言、标点和特殊符号。分词器负责把文本转换为模型能够处理的整数序列,但字符、字节和 token 是三种不同单位。
模型看到的是 token ID
汉字和 emoji 的 UTF-8 表示可能占用多个字节;一个 token 也可能对应一个字符、多个字符,或某段字节。词表决定每个 token ID 对应什么内容。模型的 embedding 行数和输出词表大小必须与这套分词器匹配。
不要仅凭字数估算上下文占用,也不要用字符切割来模拟实际 token 边界。相同文本经过不同分词器,得到的序列长度可能不同。
手动做一次 BPE 合并
用 banana 的字符序列 [b,a,n,a,n,a] 做一个简化示例。相邻对 (a,n) 出现两次。把每一处不重叠的 (a,n) 合并成新符号 an,序列变为 [b,an,an,a],词表新增这个符号。
继续合并时,要重新统计当前序列上的相邻对,并固定并列频次的处理规则。真实字节级 BPE 还涉及训练语料、基础字节表、预切分和特殊 token。这个小例子说明的是合并过程,不是一个生产分词器的完整实现。
训练和推理必须使用同一套约定
如果训练时 ID 10 表示某个片段,推理时却把 ID 10 解释为别的内容,模型参数即使完整加载,也无法正确使用。保存检查点时,应同时记录分词器文件、词表、特殊 token 和版本。
对未经规范化、不会主动丢弃文本信息的编码器,可以检查 decode(encode(text)) 是否还原输入。对于带有文本规范化的分词器,应按其官方规则判断,而不是把所有差异都叫作错误。
# tokenizer 使用你正在学习的那一套实现。
texts = ["你好,AI", "Hello, world!", "测试🙂换行\n"]
for text in texts:
ids = tokenizer.encode(text)
decoded = tokenizer.decode(ids)
print(repr(text), len(ids), repr(decoded))
把分词器接回学习主线
这一课配 Karpathy 的 Tokenizer 视频,并用 Happy-LLM 的中文原理作伴读。观看时重点看词表和合并规则怎样保存,而不是只观察压缩后的序列有多短。
你现在已有 token 序列和 embedding 的直觉。下一课开始实现因果注意力:每个位置会根据已经看见的 token 选择信息,同时阻止偷看未来答案。