这一步的核心是构建词汇表(Vocabulary/Vocab),再通过词汇表完成 Token 到索引的映射。整个流程分为 4 步,我们结合具体文本例子讲解。
前置条件:已完成Token 化
假设我们有一批文本数据,已经完成 Token 化(中文按字拆分):
文本1:"我爱吃苹果" → Token序列:["我", "爱", "吃", "苹果"]
文本2:"我爱香蕉" → Token序列:["我", "爱", "香蕉"]
文本3:"苹果好吃" → Token序列:["苹果", "好", "吃"]
一、步骤 1:统计所有 Token,生成 Token 频率字典
遍历所有文本的 Token 序列,统计每个 Token 出现的次数 ------ 这一步是为了筛选高频 Token,构建核心词汇表。
频率统计结果:
| Token | 出现次数 |
|---|---|
| 我 | 2 |
| 爱 | 2 |
| 吃 | 2 |
| 苹果 | 2 |
| 香蕉 | 1 |
| 好 | 1 |
二、步骤 2:定义特殊 Token,预留索引位置
在构建词汇表前,需要先为 <PAD> <UNK> <SOS> <EOS> 等特殊 Token 分配固定索引 ------ 这是工程上的约定,避免特殊 Token 的索引被普通 Token 占用。
通常的索引分配规则(优先级:特殊 Token > 普通 Token):
| 特殊 Token | 索引值 | 作用 |
|---|---|---|
<PAD> |
0 | 填充,统一序列长度 |
<UNK> |
1 | 替换未登录词 |
<SOS> |
2 | 标记序列开始 |
<EOS> |
3 | 标记序列结束 |
三、步骤 3:构建词汇表(Vocab),分配普通 Token 索引
词汇表是一个 Python 字典,键是 Token,值是对应的整数索引。构建规则分两种场景:
场景 A:无词汇量限制(小数据集)
直接将所有普通 Token 加入词汇表,索引从 特殊 Token 的最大索引 + 1 开始分配。
-
示例:特殊 Token 最大索引是 3 → 普通 Token 索引从 4 开始
普通 Token 索引值 我 4 爱 5 吃 6 苹果 7 香蕉 8 好 9 -
最终词汇表:
vocab = { "<PAD>":0, "<UNK>":1, "<SOS>":2, "<EOS>":3, "我":4, "爱":5, "吃":6, "苹果":7, "香蕉":8, "好":9 }
场景 B:有词汇量限制(大数据集)
真实场景中,文本数据的 Token 数量可能达数百万(如中文词汇量超 10 万),无法全部加入词汇表 ------ 此时需要按频率筛选 Top-N 个 Token ,低频 Token 会被替换为 <UNK>。
-
示例:设定词汇量
vocab_size=8(只要8个Token,包含 4 个特殊 Token + 4 个普通 Token)1.按频率排序普通 Token:
我=2爱=2吃=2苹果=2香蕉=1好=12.取 Top-4 普通 Token:
我、爱、吃、苹果3.分配索引:从 4 开始 →
我:4爱:5吃:6苹果:74.低频 Token
香蕉、好被归为<UNK>(索引 1) -
最终词汇表(大小 = 8):
vocab = { "<PAD>":0, "<UNK>":1, "<SOS>":2, "<EOS>":3, "我":4, "爱":5, "吃":6, "苹果":7 }
四、步骤 4:Token→整数索引映射(核心操作)
遍历每个文本的 Token 序列,通过词汇表字典的 get() 方法完成映射,规则如下:
若 Token 在词汇表中 → 取对应的索引;
若 Token 不在词汇表中 → 取
<UNK>的索引(1)。
示例(基于场景 B 的词汇表)
| 原始 Token 序列 | 映射后的整数索引序列 | 说明 |
|---|---|---|
| ["我", "爱", "吃", "苹果"] | [4,5,6,7] | 所有 Token 都在词汇表中 |
| ["我", "爱", "香蕉"] | [4,5,1] | "香蕉" 不在词汇表 → 映射为 <UNK> 的索引 1 |
| ["苹果", "好", "吃"] | [7,1,6] | "好" 不在词汇表 → 映射为 <UNK> 的索引 1 |
关键代码(映射逻辑)
token_seq = ["我", "爱", "香蕉"]
index_seq = [vocab.get(token, vocab["<UNK>"]) for token in token_seq]
print(index_seq) # 输出:[4,5,1]
五、关键细节与工程优
1. 反向映射:索引→Token
训练完成后,若需要将模型输出的索引序列转回文本,需要构建反向词汇表(索引为键,Token 为值):
reverse_vocab = {v: k for k, v in vocab.items()}
index_seq = [4,5,6,7]
token_seq = [reverse_vocab[idx] for idx in index_seq]
print(token_seq) # 输出:["我", "爱", "吃", "苹果"]
2. 未登录词(OOV)的处理原则
训练阶段 :低频 Token 主动替换为
<UNK>,让模型学习<UNK>的语义;推理阶段 :所有不在词汇表中的 Token 都替换为
<UNK>,避免索引错误;优化方案 :使用子词分词(如 BPE、WordPiece) ,将未登录词拆分为子词(子词大概率在词汇表中),减少
<UNK>的出现。(非常妙)
3. 索引值的分配原则
特殊 Token 索引必须固定 :尤其是
<PAD>的索引(通常为 0),后续掩码(Mask)和损失计算需要依赖这个固定值;普通 Token 索引无需连续 :但连续分配可以节省内存(嵌入层的权重矩阵大小为
[vocab_size, embedding_dim],索引不连续会浪费空间)。
4. 词汇表的保存与加载
真实项目中,词汇表需要保存为文件(如 JSON),避免每次训练都重新构建:
import json
# 保存词汇表
with open("vocab.json", "w", encoding="utf-8") as f:
json.dump(vocab, f, ensure_ascii=False, indent=2)
# 加载词汇表
with open("vocab.json", "r", encoding="utf-8") as f:
vocab = json.load(f)
六、完整代码实现(PyTorch 实战)
我们结合真实流程,实现 "Token 化→构建词汇表→Token→索引映射" 的完整代码:
import json
from collections import Counter
# ===================== 1. 准备数据 =====================
corpus = [
"我爱吃苹果",
"我爱香蕉",
"苹果好吃"
]
# ===================== 2. Token 化(中文按字拆分) =====================
def tokenize(text):
return list(text) # 简单按字拆分,实际可用 jieba 分词
token_seqs = [tokenize(text) for text in corpus]
print("Token 序列列表:", token_seqs)
# 输出:[['我', '爱', '吃', '苹果'], ['我', '爱', '香蕉'], ['苹果', '好', '吃']]
# ===================== 3. 统计 Token 频率 =====================
all_tokens = []
for seq in token_seqs:
all_tokens.extend(seq)
token_freq = Counter(all_tokens)
print("Token 频率:", token_freq)
# 输出:Counter({'我':2, '爱':2, '吃':2, '苹果':2, '香蕉':1, '好':1})
# ===================== 4. 构建词汇表 =====================
# 4.1 定义特殊 Token
special_tokens = ["<PAD>", "<UNK>", "<SOS>", "<EOS>"]
# 4.2 设定词汇量
vocab_size = 8 # 4个特殊 Token + 4个普通 Token
# 4.3 筛选 Top-N 普通 Token
top_tokens = [token for token, _ in token_freq.most_common(vocab_size - len(special_tokens))]
# 4.4 分配索引
vocab = {}
# 先分配特殊 Token 索引
for idx, token in enumerate(special_tokens):
vocab[token] = idx
# 再分配普通 Token 索引
start_idx = len(special_tokens)
for idx, token in enumerate(top_tokens):
vocab[token] = start_idx + idx
print("最终词汇表:", vocab)
# 输出:{'<PAD>':0, '<UNK>':1, '<SOS>':2, '<EOS>':3, '我':4, '爱':5, '吃':6, '苹果':7}
# ===================== 5. Token→整数索引映射 =====================
def token_to_index(token_seq, vocab):
return [vocab.get(token, vocab["<UNK>"]) for token in token_seq]
index_seqs = [token_to_index(seq, vocab) for seq in token_seqs]
print("索引序列列表:", index_seqs)
# 输出:[[4,5,6,7], [4,5,1], [7,1,6]]
# ===================== 6. 保存词汇表 =====================
with open("vocab.json", "w", encoding="utf-8") as f:
json.dump(vocab, f, ensure_ascii=False, indent=2)
七、常见误区与注意事项
误区 1:索引值越大,Token 越重要 → 否:索引值只是 Token 的 "编码",和 Token 的语义重要性无关。语义的重要性由后续嵌入层的向量值决定。
误区 2:词汇表越大越好 → 否:词汇表过大会导致嵌入层权重矩阵过大(如vocab_size=10万,embedding_dim=512→ 权重矩阵大小为10万×512=5120万参数),增加训练难度和内存消耗。
注意事项:特殊 Token 的索引必须固定 → 尤其是<PAD>的索引(通常为 0),后续使用CrossEntropyLoss(ignore_index=0)时,必须保证这个索引对应<PAD>,否则会计算填充位置的损失,干扰模型训练。
八、总结
Token→整数索引 的核心是构建词汇表 ,流程可简化为:
Token 化 → 统计频率 → 定义特殊 Token → 筛选普通 Token → 分配索引 → 映射转换这一步的本质是将人类可读的符号系统,转化为机器可读的数值系统,是文本数据进入神经网络的必经之路。
九、词汇表构建的进阶技巧清单
词汇表(Vocab)是文本与神经网络之间的核心桥梁,基础构建方法适用于小数据集,而在大规模、复杂文本任务(如大语言模型预训练、多语言处理)中,需要结合工程优化、语义增强、效率提升的进阶技巧,以下是全面且可落地的方法清单:
一、 分词策略优化:从 "字 / 词级" 到 "子词级",减少未登录词
基础的字 / 词级分词会产生大量未登录词(OOV),而子词分词 能将未登录词拆分为模型见过的子词,大幅降低 <UNK> 比例,是工业界的主流方案。
1. 核心子词算法(按落地难度排序)
| 算法名称 | 核心原理 | 适用场景 | 优势 | 工具实现 |
|---|---|---|---|---|
| BPE(字节对编码) | 1. 统计词汇中字符对的频率;2. 迭代合并频率最高的字符对,生成新子词;3. 直到达到预设子词数量 | 英文 / 中文(拼音)、大规模语料 | 简单高效,适合预训练模型 | Hugging Face Tokenizers、SentencePiece |
| WordPiece | 类似 BPE,但合并规则改为 "使训练集的对数似然最大" | 英文(如 BERT 的分词) | 子词语义更连贯 | TensorFlow Text、Hugging Face Transformers |
| Unigram | 1. 初始化包含所有可能子词的集合;2. 迭代删除对模型损失影响最小的子词;3. 保留最优子词集合 | 多语言处理、对 OOV 要求高的场景 | 子词粒度更灵活,支持多语言 | SentencePiece |
2. 中文子词分词的特殊处理
中文无天然空格分隔,直接用子词算法效果有限,需结合预处理:
-
方案 1:拼音子词 → 将汉字转为拼音(如 "苹果"→
ping guo),再用 BPE 分词; -
方案 2:汉字笔画子词 → 将汉字拆为笔画(如 "苹"→
艹 平),适合生僻字多的场景; -
方案 3:预分词 + 子词 → 先用 jieba 等工具做粗粒度分词,再对分词结果做子词拆分(如 "自动驾驶"→
自动 驾驶→再拆为子词)。
3. 实战优势对比
| 分词方式 | OOV 比例 | 语义保留度 | 计算效率 | 适用模型规模 |
|---|---|---|---|---|
| 字级分词 | 低(但语义粒度细) | 低(单个字无完整语义) | 高 | 小模型(如文本分类) |
| 词级分词 | 高(生僻词多) | 高(完整词语义) | 中 | 中等模型(如情感分析) |
| 子词分词 | 极低(几乎无 OOV) | 中高(子词兼顾粒度和语义) | 中 | 大模型(如预训练 LLM) |
二、 词汇表大小优化:平衡 "覆盖度" 与 "计算成本"
词汇表大小(vocab_size)是核心超参数:过大增加嵌入层参数规模,过小导致 OOV 增多。
1. 最优词汇表大小的选择原则
-
小任务(如文本分类) :
vocab_size=1万~5万→ 足够覆盖核心词汇,参数规模小; -
中等任务(如机器翻译) :
vocab_size=5万~30万→ 平衡 OOV 和计算量; -
大模型预训练(如 LLM) :
vocab_size=30万~100万→ 覆盖更多生僻词和子词,提升模型泛化能力。
2. 动态词汇表:按需扩展词汇
固定词汇表无法适配新领域文本(如医疗、法律),动态词汇表方案可解决:
-
领域词汇注入 → 从领域语料中提取专业词汇(如医疗领域的 "CT 扫描""靶向药"),直接加入核心词汇表;
-
增量子词训练 → 用新领域语料继续训练 BPE 模型,生成新子词并合并到原词汇表;
-
注意事项 → 新增词汇的嵌入向量需初始化(可采用 "平均嵌入法":类似语义词汇的向量平均值),再通过微调更新。
3. 减少词汇表冗余的技巧
-
低频 Token 过滤:删除训练集中出现次数 < N(如 N=5)的 Token,这些 Token 对模型贡献极小;
-
Token 去重:合并同义 Token(如 "新冠" 和 "新型冠状病毒"),减少词汇表冗余;
-
特殊 Token 精简 :仅保留任务必需的特殊 Token(如分类任务无需
<SOS>/<EOS>)。
三、 嵌入层与词汇表的协同优化:提升语义表示能力
词汇表的最终价值是通过嵌入层转化为语义向量,以下技巧可提升向量质量:
1. 特殊 Token 的嵌入向量初始化策略
特殊 Token 无实际语义,需针对性初始化,避免干扰模型:
| 特殊 Token | 初始化方法 | 核心目的 |
|---|---|---|
<PAD> |
全 0 向量(torch.zeros(embedding_dim)) |
确保填充位置无语义信息 |
<UNK> |
随机初始化 + 训练中学习 | 让模型学习未登录词的通用语义 |
<SOS>/<EOS> |
均值初始化(所有普通 Token 向量的平均值) | 赋予 "序列边界" 的基础语义 |
<MASK>(BERT 用) |
随机初始化 + 掩码任务微调 | 适配掩码语言模型的预训练目标 |
2. 预训练词向量融合:冷启动词汇表语义
直接随机初始化嵌入层,模型需要大量数据才能学习到好的语义;融合预训练词向量可实现 "冷启动":
-
步骤 1:用 Word2Vec、GloVe 等工具预训练词向量(基于大规模通用语料);
-
步骤 2:构建词汇表时,若 Token 存在预训练词向量,直接赋值给嵌入层权重;
-
步骤 3:未匹配的 Token(如特殊 Token、生僻词)随机初始化;
-
步骤 4:训练时选择 "冻结预训练向量" 或 "微调预训练向量":
-
小数据集任务 → 冻结(避免过拟合);
-
大数据集任务 → 微调(让向量适配任务语义)。
-
3. 词汇表的分层设计:适配多粒度语义
复杂任务(如阅读理解)需要同时捕捉 "字、词、短语" 的语义,可采用分层词汇表:
-
底层:字级 Token(如 "我""爱""吃")→ 捕捉细粒度语义;
-
中层:词级 Token(如 "苹果""香蕉")→ 捕捉基础语义;
-
高层:短语级 Token(如 "爱吃苹果")→ 捕捉组合语义;
-
实现方式 → 嵌入层输出不同粒度 Token 的向量,再通过注意力机制融合为最终表示。
四、 工程化技巧:词汇表的保存、加载与跨平台兼容
在真实项目中,词汇表的复用性和兼容性至关重要,以下是工程化最佳实践:
1. 标准化存储格式
避免自定义格式,优先选择跨平台的存储格式:
| 存储格式 | 优点 | 适用场景 | 工具 |
|---|---|---|---|
| JSON | 人类可读,支持字符串键值对 | 中小规模词汇表(<50 万) | Python json 库 |
| Protocol Buffers(PB) | 体积小,序列化 / 反序列化速度快 | 大规模词汇表(>50 万) | Google Protobuf |
| SentencePiece Model | 内置分词 + 词汇表,支持跨语言 | 子词分词场景 | SentencePiece |
存储内容必须包含:
-
正向映射:
token → index; -
反向映射:
index → token; -
元信息:词汇表大小、特殊 Token 列表、分词算法版本。
2. 跨框架兼容方案
不同深度学习框架(PyTorch/TensorFlow)的嵌入层输入要求一致,但词汇表加载需适配:
-
方案:定义统一的词汇表加载类,输出框架兼容的索引张量;
-
示例 :
class VocabLoader: def __init__(self, vocab_path): self.vocab = self._load_vocab(vocab_path) self.reverse_vocab = {v:k for k,v in self.vocab.items()} def _load_vocab(self, path): # 支持JSON/PB格式加载 pass def token_to_idx(self, token_seq, framework="pytorch"): idx_seq = [self.vocab.get(t, self.vocab["<UNK>"]) for t in token_seq] if framework == "pytorch": return torch.tensor(idx_seq, dtype=torch.long) elif framework == "tensorflow": return tf.convert_to_tensor(idx_seq, dtype=tf.int32)
3. 词汇表的版本管理
当语料更新或分词算法调整时,词汇表会发生变化,需做好版本管理:
-
命名规范 :
vocab_<任务>_<分词算法>_<vocab_size>_<版本号>.json(如vocab_chatbot_bpe_30w_v1.0.json); -
版本日志:记录每个版本的变更(如 "v1.1:新增医疗领域词汇 1000 个");
-
回滚机制:保存历史版本,避免新版本引入问题无法回退。
五、 特殊场景的词汇表定制技巧
1. 多语言任务词汇表
-
方案 1:共享词汇表 → 用 Unigram 算法训练多语言语料,生成统一子词词汇表(如 XLM 模型),优势是支持跨语言语义对齐;
-
方案 2:独立词汇表 + 共享嵌入层 → 每种语言构建独立词汇表,但共享同一嵌入层权重,适合语言差异大的场景(如中文 + 英文)。
2. 低资源语言任务词汇表
低资源语言(如少数民族语言)语料少,直接训练词汇表效果差:
-
迁移学习 → 用高资源语言(如中文)的词汇表和嵌入层,微调适配低资源语言;
-
跨语言子词 → 将低资源语言与高资源语言混合训练 BPE 模型,利用高资源语言的子词提升覆盖度。
3. 生成式任务词汇表
文本生成任务(如续写、翻译)对词汇表的要求更高:
-
必须包含
<SOS>/<EOS>标记序列边界; -
词汇表需覆盖高频生成词汇(如标点符号、连接词);
-
可加入控制 Token (如
<POSITIVE><NEGATIVE>),引导模型生成特定情感的文本。
六、 避坑指南:词汇表构建的常见错误
-
错误 1:词汇表过大 / 过小→ 解决:通过 "语料覆盖率曲线" 选择最优大小(覆盖率 = 词汇表覆盖的 Token 数 / 总 Token 数,通常选择覆盖率 99% 时的最小词汇表大小);
-
错误 2:特殊 Token 索引不固定 → 解决:特殊 Token 必须放在词汇表最前面,索引固定(如
<PAD>=0<UNK>=1),避免训练和推理时索引不一致; -
错误 3:忽略词汇表的领域适配→ 解决:通用词汇表在特定领域(如医疗)效果差,需注入领域词汇并微调嵌入层;
-
错误 4:子词分词后未做还原 → 解决:生成任务中,子词序列需通过 "反向分词" 还原为完整文本(如子词
ping@@ guo→pingguo→苹果)。