从零构建 GPT 字符级词汇表:stoi/itos 双向映射与 encode/decode 实现(LeetCode 课程实战)
【免费下载链接】leetcodeLeetcode solutions项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode
构建语言模型的第一步不是搭建网络,而是为模型准备好它唯一能"看懂"的整数输入。本篇文章围绕 LeetCode 课程中 articles/build-vocabulary.md 所讲解的**字符级词汇表(Character-Level Vocabulary)**展开:通过 Python 字典构建字符串到整数的双向映射stoi/itos,并实现encode/decode这对互逆函数,将原始训练文本无损地转换为 GPT 模型处理的整数序列。读完本文,你将掌握字符级分词的核心原理、可复制的 Python 实现、复杂度分析与常见陷阱,并理解它在本课程 GPT 项目数据管线中的位置,以及它与 BPE、词级分词之间的取舍。
前置知识
在动手实现之前,需要具备两项基础能力:
- Python 字典(Dictionaries):词汇表本质上是两本字典——字符串到整数的
stoi(string-to-integer)与整数到字符串的itos(integer-to-string)。需要熟练创建键值对、用推导式批量构建映射,并理解dict.items()在反转映射中的作用。 - 字符级分词(Character-Level Tokenization):把每个字符当作一个独立 token,是最简单的分词方式,也是本课程 GPT 模型采用的方式。相关概念可参考 articles/nlp-intro.md 与本系列的 articles/tokenizer-bpe.md 对照学习。
核心概念:为什么语言模型需要词汇表
在语言模型处理文本之前,必须先建立一份词汇表:字符(或 token)与整数之间的双向映射。模型内部只与整数打交道,因此需要:
encode:把文本转换为整数序列;decode:把整数序列还原为文本。
构建过程分为四步:
- 提取唯一字符:从训练文本中取出所有不重复的字符;
- 排序:按字母序排序,保证确定性(deterministic)的顺序;
- 构建
stoi(string-to-integer):为每个字符分配一个从 0 开始、唯一的索引; - 构建
itos(integer-to-string):即反向映射。
这就是字符级分词:词汇表大小等于训练数据中唯一字符的个数,英文文本通常在50~100之间。对比其他方案:
| 分词方案 | 词汇表规模 | 特点 |
|---|---|---|
| 字符级(本课程) | 50~100 | 序列更长,但训练中出现过的字符永远不会 OOV(out-of-vocabulary) |
| BPE(GPT-2 等生产模型) | 50,000+ | 子词粒度,压缩常见模式、拆分罕见词 |
| 词级 | 100,000+ | 序列短,但遇到未登录词即失效 |
encode与decode必须是互逆的:decode(encode(text)) == text。这一往返(round-trip)性质是硬性要求——如果无法完美还原原始文本,模型就无法学到正确的输入输出映射。这一点与 articles/string-encode-and-decode.md 中"编码/解码互为逆操作"的设计思想一脉相承。
解决方案
直觉(Intuition)
用set()提取唯一字符,sorted()排序,再用enumerate一次性构建两本字典。编码是字典查询的列表推导式,解码是把查到的字符拼接成字符串。
实现(Implementation)
from typing import Dict, List, Tuple class Solution: def build_vocab(self, text: str) -> Tuple[Dict[str, int], Dict[int, str]]: chars = sorted(set(text)) stoi = {ch: i for i, ch in enumerate(chars)} itos = {i: ch for ch, i in stoi.items()} return stoi, itos def encode(self, text: str, stoi: Dict[str, int]) -> List[int]: return [stoi[ch] for ch in text] def decode(self, ids: List[int], itos: Dict[int, str]) -> str: return ''.join(itos[i] for i in ids)要点说明:
chars = sorted(set(text))一行同时完成"去重 + 排序",set保证唯一性,sorted保证索引分配的确定性;enumerate(chars)从 0 开始顺序编号,天然满足"每个字符一个唯一索引";itos = {i: ch for ch, i in stoi.items()}通过反转stoi的键值对构建,从构造上保证两本字典互为精确逆映射,而不是另起炉灶独立编号(独立编号极易引入错位)。
逐步走查(Walkthrough)
以text = "hello"为例:
| 步骤 | 输入 | 输出 |
|---|---|---|
| 提取唯一字符 | "hello" | {'h', 'e', 'l', 'o'} |
| 排序 | 集合 | ['e', 'h', 'l', 'o'] |
| 构建 stoi | 排序后的字符 | {'e': 0, 'h': 1, 'l': 2, 'o': 3} |
| 构建 itos | 反转 stoi | {0: 'e', 1: 'h', 2: 'l', 3: 'o'} |
| 编码 "hello" | 逐字符查表 | [1, 0, 2, 2, 3] |
解码[1, 0, 2, 2, 3] | 逐整数查表 | "hello" |
往返验证:decode(encode("hello")) == "hello"。
时间与空间复杂度
- 时间:构建词汇表为 $O(N \log N)$(对唯一字符排序);编码/解码均为 $O(N)$,其中 $N$ 是文本长度。
- 空间:$O(V)$,$V$ 为唯一字符个数,用于存储两本词汇字典。
常见陷阱(Common Pitfalls)
1. 不对唯一字符排序
Python 的set不保证迭代顺序。如果不排序,同一段文本在不同运行环境下可能产生不同的词汇表,导致索引分配不可复现,破坏实验的确定性。
# 错误:顺序不确定 chars = list(set(text)) # 正确:排序保证可复现 chars = sorted(set(text))2. 独立构建 itos 导致映射错位
itos必须是stoi的精确逆映射。如果独立构建(例如对同一个chars列表重新 enumerate),一旦stoi的键顺序与chars顺序不一致,两本字典就会出现错位,decode(encode(text))便不再等于text。
# 错误:独立构建,可能不是精确逆映射 itos = {i: ch for i, ch in enumerate(chars)} # 正确:从 stoi 派生,保证互逆关系 itos = {i: ch for ch, i in stoi.items()}排序陷阱与"互逆性"陷阱的本质都指向同一原则:词汇表的构建必须确定且自洽。这也与 BPE 学习合并表时"词频相同则按字典序打破平局"的确定性要求(见 articles/tokenizer-bpe.md)是一致的。
在 GPT 项目中:词汇表在整个数据管线中的位置
在课程 GPT 项目中,本节内容对应data/vocab.py。本课程 GPT 模型采用字符级分词,因此这份词汇表负责把原始训练文本转换成模型实际处理的整数序列。
要理解它的位置,需要把它放入完整的数据管线中看待:
- 数据集加载(对应
data/dataset.py,见 articles/gpt-dataset.md):从原始文本生成"输入 + 目标(右移一位)"的训练对; - 词汇表编码(对应
data/vocab.py,即本文):把文本字符映射为整数 ID; - 模型前向(对应
model/gpt.py,见 articles/code-gpt.md):模型接收整数 ID,输出词汇表上的 logits——注意输出维度正是vocab_size,即本文词汇表的规模,两者必须严格一致; - 训练(对应
train.py,见 articles/train-your-gpt.md):交叉熵损失在每个位置把输出视为"从 $V$ 个词汇中选下一个 token"的分类问题,$V$ 即词汇表大小;未训练模型的初始损失应接近 $\ln(V)$; - 生成(对应
generate.py,见 articles/make-gpt-talk-back.md):自回归循环每步采样一个 token ID,再通过itos解码成字符输出——生成函数的签名int_to_char正是本文构建的itos映射。
从以上调用链可以推断:词汇表是数据管线的枢纽——它既决定了数据加载器输出什么整数,也决定了模型输出层的维度,还决定了生成阶段能否把采样结果还原成可读文本。任何一个环节的 ID 约定不一致,整条管线都会断裂。
生产级模型(如 GPT-2)并不使用字符级词汇表,而是采用 BPE,其编码器/解码器也是同样的双向映射思想,只是 token 从单个字符变成了子词(详见 articles/tokenizer-bpe.md)。理解字符级版本,是理解真实 tokenizer 的最佳起点。
关键要点(Key Takeaways)
- 字符级词汇表是最简单的分词方式:词汇表大小等于训练数据中唯一字符的个数,英文文本通常为 50~100。
stoi/itos组合保证无损往返转换:文本与整数序列之间可以完美互转,这是任何 tokenizer 的硬性要求;decode(encode(text)) == text必须成立。- 排序保证确定性的 ID 分配:不排序时,同一段文本在不同运行中可能产生不同词汇表,破坏实验可复现性。
- 词汇表贯穿 GPT 全流程:从数据加载到模型输出维度、再到生成解码,
vocab_size与itos是整个数据管线的公共约定,值得在动手搭建模型前优先夯实。
【免费下载链接】leetcodeLeetcode solutions项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考