3. 词汇表构建:从语料到令牌映射的第一步
词汇表是令牌化系统的心脏。没有它,你手里的文本就是一盘散沙。我最早做工业级 NLP 项目时,以为随便拿个词频表就能用——结果模型训练到一半报错说 " not found in vocabulary",整个管道直接炸了。从那以后我明白:词汇表构建,不能偷懒。
说白了,这一步就是把语料库里的原始文本,变成有序的、带特殊标记的令牌集合。我们不光要记录「哪些词出现了」,还得决定「哪些词值得保留」「哪些词需要归并」「哪些词必须拥有专用 ID」。让我们从头捋一遍流程。
3.1 语料收集与预处理
先说说语料。你得准备好至少一个大型文本集合,比如维基百科 dump、新闻语料、业务日志等。我习惯先做几件事:
- 统一编码:全部转为 UTF-8,避免混合编码导致乱码
- 去除噪音:比如 HTML 标签、URL(除非你专门建模 URL)
- 大小写规整:大多数任务会转小写(英文)
- 分词:中文用 jieba/BERT 自带的分词器,英文用空格 + .split()
预处理后的结果是一系列 列表。举个例,假设我们有 3 条样本:
corpus = [
"我 爱 自然 语言 处理",
"深度学习 与 神经网络 是 热门",
"Python 是 编程 语言"
]
3.2 频率统计与阈值截断
统计每个 出现的次数,这是最朴素也最必要的步骤。你可能会想:「全都保留不就得了?」——不行。原因有两个:
- 稀有词是噪声源:只出现一两次的词,模型根本学不到可靠表征,反而拉低 embedding 质量。
- 词汇表过大:softmax 层的参数量 = vocab_size × hidden_dim,太多 会让模型臃肿得不行。
在我的项目里,通常设一个 最小频率阈值,比如 10。低于这个数就归入 [UNK]。如果语料规模很大(上亿 tokens),阈值可以提到 50 甚至 100。怎么定?我习惯先跑一次全量统计,画一个词频累积曲线——看看低于阈值会丢弃多少 、影响多少语料覆盖率。
下面是一个简化的统计与截断实现:
from collections import Counter
def build_vocab(corpus, min_freq=10):
counter = Counter()
for sentence in corpus:
tokens = sentence.split()
counter.update(tokens)
# 过滤低频
vocab = {: idx for idx, (, freq) in enumerate(counter.items()) if freq >= min_freq}
return vocab
# 示例
raw_vocab = build_vocab(corpus, min_freq=1) # 不截断
print(len(raw_vocab)) # 输出 13(所有词都保留)
filtered_vocab = build_vocab(corpus, min_freq=2) # 只保留出现≥2次的词
print(filtered_vocab) # 只保留 "语言"(出现2次)
3.3 特殊令牌 — 为什么我们需要它们?
原始词汇表只有你语料里的词,但模型还需要一些特殊标记来辅助任务。这些令牌每个都有一个固定的 ID,不会从统计中产生。
| 令牌 | 解释 | 谁在用 |
|---|---|---|
[UNK] |
未知词。遇到不在词汇表中的词时,用它替换 | 几乎所有 NLP 模型 |
[CLS] |
分类令牌。BERT 系列用它作为序列表示的聚合输出 | BERT、RoBERTa、ALBERT |
[SEP] |
分隔令牌。区分两个句子(比如问答、NLI) | BERT 及变体 |
[PAD] |
填充令牌。让 batch 内序列长度统一 | 几乎所有 Transformer |
[MASK] |
掩码令牌。MLM 预训练时遮盖部分 | BERT、RoBERTa |
这些特殊令牌必须放在词汇表的开头位置(ID 0, 1, 2…),这样 embedding 矩阵的对应行就固定了。有些框架要求 [PAD] 必须是 ID 0,因为注意力掩码默认忽略 0。我个人的习惯是:
SPECIAL_TOKENS = {
"[PAD]": 0,
"[UNK]": 1,
"[CLS]": 2,
"[SEP]": 3,
"[MASK]": 4,
}
# 后续的常规 从 ID 5 开始
3.4 最终词汇表结构
组合在一起,完整的词汇表就是一个 令牌 → ID 的映射字典。除此之外,我们通常还要保留反向映射(ID → 令牌),方便解码。
下面这张图展示了从原始语料到最终词汇表的完整流程:
3.5 实战避坑与最佳实践
最后总结几条我亲手趟过的坑:
- 不要忘记 [PAD] 放 ID 0:有些实现把 [UNK] 放 0,[PAD] 放 1。结果注意力掩码默认把 0 当作填充,导致 [UNK] 被完全忽略,模型学不到未知词的处理。我吃过这个亏,训练了两个 epoch 才发现 loss 不对劲。
- 特殊令牌的数量要一致:如果你在其他任务里用了
[SOS]、[EOS],记得在词汇表里也加上。千万不要预训练一套特殊令牌,微调时又加新的——之前预训练的 embedding 就白练了。 - 对中文进行子词分词后再构建词汇表:直接对切好词的中文建 vocab,词汇量会很大(常用汉字 3500+,但分词后 数量会少很多)。如果你用 BPE 或 WordPiece,词汇表控制在 30K-50K 比较合适。
嗯,关于词汇表构建就聊这么多。你只要记住一个核心:词汇表是沟通文本和数字的唯一桥梁。桥没搭好,后面的模型再花哨也没用。