AI Hero Club

从零训练语言模型 · 第 2 章

已自行标记完成0 / 8

仅保存在当前浏览器;完成标记不代表通过项目验收。

本章提纲 6 个小节
全课目录 8 章
完整课程计划 ↗
从零训练语言模型 / 课程计划 ↗

CHAPTER 02 / 8建议学习 120 分钟

字节、Unicode 与预测单位

一个汉字、它的 UTF-8 字节和模型词元分别是什么,为什么能编码不等于能合法生成?

本章要解决的 3 个问题
  • 逐位还原 UTF-8 字节并区分字符、字节与 token 三种长度
  • 验证固定字节词表对新字符的编码覆盖,同时构造不能解码的生成反例
  • 按项目编号和边界协议计算目标数、均匀基线与字节归一化代价
课程项目 ZIP ↓

任务与先修:让任何新字符都有编码

第一章已经固定文档和划分。本章要把 text 转成明确的整数序列,并保证一个训练中没出现的新汉字或表情仍可编码。读者需要会整数除法、二进制移位与十六进制;不要求熟悉 Unicode 规范。我们只处理 Python 字符串可以按严格 UTF-8 编码的文本,不接受孤立代理码点。本项目的基本字母表是二百五十六个可能字节,编号零到二百五十五;BOS 编号二百五十六,EOS 编号二百五十七。零是实际字节,不能当“空值”跳过。此时词表大小为二百五十八,没有未知符号,也没有独立填充编号。

Unicode 码点是抽象整数;UTF-8 是把码点变成一到四个字节的编码。屏幕上的一个可见字形还可能由多个码点组成,比如字母加组合重音。因此 len(text)、len(text.encode("utf-8")) 与分词后的长度是三个不同量。我们保留原始文本,不自动正规化、裁空格或改换行;这些操作会改变字节身份和评估分母。若将来明确采用正规化,必须同时版本化它,并承认任务已变为预测正规化文本。编码覆盖不是对视觉字形的“理解”,只是可逆表示的性质。

从码点推到字节

以码点 uu 表示一个有效 Unicode 标量值。ASCII 范围 0≤u≤1270\le u\le127 直接用一个字节;更大值分配到若干负载位,首字节的前缀指明总长度,后续字节前缀固定为二进制十。对三字节情形,负载位数量是四、六、六,总共十六位。把高位放首字节,中间六位放第二字节,低六位放第三字节,解码时删除前缀再拼回;不是把码点十进制字符串拆成若干数字。合法范围还要排除代理区、过长编码和超出最高码点的值。

b1=224+(u≫12),b2=128+((u≫6)&63),b3=128+(u&63).b_1=224+(u\gg12),\quad b_2=128+((u\gg6)\mathbin{\&}63),\quad b_3=128+(u\mathbin{\&}63).
仅用于三字节 UTF-8 的有效码点范围,并排除代理码点;通用项目编码交给 Python 严格 UTF-8。

算例一:把“春”完整拆开

“春”的码点为十六进制 6625,也就是十进制二万六千一百四十九。高四位是六,中六位是二十四,低六位是三十七。因此首字节是二百二十四加六等于二百三十;第二字节一百二十八加二十四等于一百五十二;第三字节一百二十八加三十七等于一百六十五,十六进制为 E6、98、A5。反向计算 6×4096+24×64+37=261496\times4096+24\times64+37=26149,恢复原码点。由此“春”的基础 token 序列就是 [230,152,165]。

项目加边界后为 [256,230,152,165,257]。输入是前四项,目标是后四项:先从 BOS 预测二百三十,再逐字节预测一百五十二、一百六十五,最后预测 EOS。正文只有一个汉字,却有四个预测目标,因为结束事件也要计分。独立检查一是 Python 的 UTF-8 编码必须输出同样三个整数,独立检查二是拼接这三个字节后严格解码必须恰好恢复一个“春”。两项检查分别覆盖手算与往返,不能只看长度相等。

算例二:有覆盖不等于每次生成都合法

任意有效 UTF-8 文本都能拆成字节,因此训练中没出现的“🙂”仍能用其四个字节编码,而无需把它映射为未知符号。可是字节模型每一步可以输出二百五十六种字节中任意一种。若从 BOS 生成 [255,257],EOS 前的 FF 字节在 UTF-8 中无合法解释,严格解码会报错。若生成 [230,257],它是未完成的三字节序列,也会报错。这两个失败分别是非法首字节与提前终止,词表覆盖保证不了任何一个不会发生。

项目推理把 generated_ids 原样保存,并返回 valid_utf8。显示时若严格解码失败,可以用替代字符显示,但必须报告发生过替代,不能把替代后的文本当成模型实际生成的字节串。若要强制 UTF-8 合法生成,需要额外有限状态约束,跟踪尚欠几个连续字节及首字节范围,再遮掉不合法候选;本项目没有实现该约束。训练在合法文本上给非法续接低概率是一种经验效果,不是类型系统提供的保证。

概率单位改变,数字的含义也改变

设相同测试文本共有 BB 个正文原始字节,编码后正文有 nn 个 token,边界 EOS 加入后有 N=n+1N=n+1 个目标。总负对数为 SS,单位是自然对数的信息量,称 nats。每目标平均 S/NS/N 与 perplexity exp⁡(S/N)\exp(S/N) 依赖分词方式;每原始字节位数 S/(Blog⁡2)S/(B\log2) 则把不同编码的分子放到相同的字节分母。正文单位一致只是比较的必要条件,后面还必须统一窗口、边界和评估样本。

ℓtok=S/N,PPL=eℓtok,rbyte=SBlog⁡2.\ell_{tok}=S/N,\qquad \mathrm{PPL}=e^{\ell_{tok}},\qquad r_{byte}=\frac{S}{B\log2}.
本课 r_byte 计入 EOS 的负对数,但分母只计正文原始字节;明确包含结束开销。

如果一个完全均匀的基础模型在全部二百五十八类中分配同样概率,每个目标 NLL 为 log⁡258\log258,约五点五五三。对只含“春”的文档,总 NLL 是四倍这个数,除以三个正文原始字节与 log⁡2\log2 后得到约十点六八一位每字节。它可以大于八,因为模型还给两个特殊类分配概率并支付结束事件成本。不能看到高于八就断言代码错误;应该先查看分子是否包含边界。

反过来,压缩一个高重复短语可以让正文 token 少很多,均匀模型的每 token 损失可能更大,但每原始字节总损失更小。这是词表大小与序列长度的取舍。实际模型还会改变条件分布、参数量与计算量,所以不能由均匀基线直接预测训练好坏。第三章采用 BPE 合并来测这个取舍,并保留基础字节模型作为可比较的表示起点,而不是一开始就把分词器当成无需解释的黑箱。

独立运行标准库独立验证:三字节手算、不同长度、非法生成与均匀基线。
import math
u=ord('春')
parts=[224+(u>>12),128+((u>>6)&63),128+(u&63)]
assert parts==[230,152,165]==list('春'.encode('utf-8'))
assert ((parts[0]&15)<<12)+((parts[1]&63)<<6)+(parts[2]&63)==u
for text in ['春','A🙂','é','é']:
    raw=text.encode('utf-8')
    assert raw.decode('utf-8')==text
    print(repr(text),'codepoints',len(text),'bytes',len(raw))
for bad in [bytes([255]),bytes([230])]:
    try: bad.decode('utf-8')
    except UnicodeDecodeError: print('invalid bytes rejected',list(bad))
    else: raise AssertionError('should fail')
s=4*math.log(258)
print('uniform bits/raw-byte with EOS',round(s/(3*math.log(2)),6))

边界、填充与可逆性的严格条件

BOS 和 EOS 不属于字节值,因此不会与文本中的某个控制字符碰撞。项目 decode 跳过这两个特殊编号,把正文 token 对应的 bytes 按顺序连接,再严格解码。这个函数是展示与往返工具:即使序列中间错误插入 EOS,它仍会跳过,不能据此声称序列边界语法有效。生成循环另行在首次 EOS 停止,语料编码则只在开头和结尾各放一次。把表示函数和边界规则区分开,才能为错误定位到具体责任。

本项目右填充输入使用 EOS 这个编号,但它只承担填充角色,没有另一个 PAD 编号。目标填充是负一百,损失忽略这些位置。有效性不能通过“输入是不是 EOS”判断,因为真正的 EOS 事件必须作为目标计分,后续窗口也可能出现实际边界。实际窗口不会跨文档,填充仅在右侧,因果性使右侧填充不影响左侧有效查询。第四章将从精确索引推导此协议,这里只冻结符号含义,不宣称支持任意左填充。

字节层的可逆性是一个全称性质:对所有在适用域内的输入文本,decode(encode(text)) 等于原文本。单测无法穷举全部文本,却可以结合构造证明和有辨别力的例子。证明的关键是 UTF-8 编码本身可逆,基本 token 完全覆盖字节,并且下一章的合并只连接相邻字节片段而不改变顺序。反例测试应包含空串、换行、零字节、组合字符、新汉字和表情,而不只是一段训练原文。

本章项目增量是一个没有训练状态的基础字节编码器及长度台账。它解决未知字符编码,不解决语言泛化、合法生成和高效上下文。接下来 BPE 从训练文本估计有序合并规则;因此需要在序列缩短的同时保存每个新编号的原始字节片段,并用摘要防止编码规则在训练与恢复间漂移。读者现在应能手算任意示例的原始字节数与边界目标数,避免在后面把“训练了多少 token”误读成“看过多少汉字”。

还有一个容易被混淆的边界:正文中看起来像“EOS”的三个拉丁字母,只会编码成它们的普通ASCII字节;它与特殊编号二百五十七没有关系。反过来,二百五十七不可能由单个字节自然出现,因为字节最大二百五十五。只要特殊符号在整数层注入,文本层无需扫描或替换某个字符串拼写。这样既避免用户原文误触发停止,也让空串编码的语义清楚:它仍然有BOS与EOS,只有一个结束预测。

基础字节序列的支持是所有字节串,而训练文本的支持只是合法UTF-8子集。神经网络softmax通常给所有类别正概率,所以非法字节路径并不会因训练集中没有而概率严格为零。合法文本的训练可以提高质量,但有限训练不会把支持集合自动变成形式语言识别器。若把非法生成强行删除后再展示,并未改变模型本身概率,只改变了观察样本选择;因此报告非法率需要固定采样协议和足够样本,当前单个greedy样本仅给出一次合法性事实。

严格解码还会拒绝过长形式。例如ASCII斜线本来用单字节十六进制2F表示;若试图用更长的C0 AF表示相同整数,UTF-8规范不允许这种形式。限制最短编码使一个有效码点有确定的字节表示,避免同一标量值被许多原始串表示。但这仍不同于Unicode正规化:预组合字母与组合序列本来就是不同码点串,二者都可合法。项目依赖标准库严格codec实现这些字节合法性条件,不自行编写一个只检查“前缀像十开头”的不完整解码器。

终端命令在下载项目的根目录、已安装固定依赖的Python环境中运行;这是项目命令,不是Python源码。
python -m unittest -v tests.test_pipeline.PipelineTests.test_utf8_roundtrip_and_invalid_generation

TRY IT YOURSELF

练习一:三种长度

求字符串“A春”的码点数、UTF-8 字节数、基础字节编码后的含边界序列长度与预测目标数。

提示 1
码点、UTF-8字节、含边界token与预测目标是四种计数。
提示 2
先逐字符编码再加BOS/EOS,目标来自整序列去掉首项,BOS不作为待预测项。
对照参考解答
码点是 A 与春,共二。A 占一个字节,春占三个,共四。编码为 [256,65,230,152,165,257],含边界长度六。输入和目标各长五,预测四个正文 bytes 加一个 EOS,目标数五。BOS 已给定,不再被预测。

TRY IT YOURSELF

练习二:等价显示与不同身份

“é”与“e”加组合重音看起来相似。为什么不能先假设它们字节相同?若要把二者合并为同一表示,需要改动哪些实验记录?

提示 1
先分别打印两种写法的码点与UTF-8字节,不从外观判断。
提示 2
若加正规化,沿预处理→分母→摘要→BPE→检查点追踪哪些对象的身份会变化。
对照参考解答
第一种是一个码点,UTF-8 两字节;第二种是两个码点,ASCII e 一字节加组合重音两字节,总三字节。应分别编码并检查,不能从外观推断身份。若采用规范化,必须记录正规化规则和版本,对全体文档一致处理,重新计算正文分母、重复摘要、数据摘要和 BPE 规则,并重新训练或拒绝旧 checkpoint。

TRY IT YOURSELF

练习三:覆盖性质的边界

有人说“固定字节词表从不出现未知符号,所以输出必定能读”。用两个最短类别的反例反驳,并给出项目应保留的证据。

提示 1
词表内字节不等于合法的完整UTF-8编码。
提示 2
分别考虑非法首字节与合法首字节缺少后续字节,检查严格decode与替代显示的不同角色。
对照参考解答
单字节 FF 不是合法 UTF-8 首字节;单字节 E6 是合法首字节但缺两个后续字节。两者都由词表内编号构成,严格解码仍失败。应保存生成编号、原始 bytes、是否严格解码成功与停止原因;替代显示只服务界面,不能删除失败证据。若增加状态约束,须另测所有允许转移及 EOS 是否只在完整码点边界出现。

CHECK YOUR UNDERSTANDING

检验理解

先作答,再决定是否展开解析。选择题用于发现理解缺口;独立实现与迁移任务仍需另行检验。

1. 零号 token 在基础编码中代表什么?

查看解析全部256个字节均有真实编号,不能忽略零。
2. “春”的基础编码正文有多少目标字节?

查看解析UTF-8 为 E6 98 A5,共3字节;另加EOS才是4个总目标。
3. 跨 tokenizer 比较时哪个量至少统一了正文长度单位?

查看解析统一字节分母是必要条件,仍需统一边界和上下文协议。
4. 为什么 [230,EOS] 可能无法解码?

查看解析合法首字节并不代表完整码点,严格解码应报告截断。

自行标记用于记录学习进度,不代表通过项目验收。