2. Tokenization在NLP中的位置:从文本到模型输入的流程
做 NLP 项目久了,我有一个很深的体会:Tokenizer 才是真正的主角。不管你后面用 Transformer 还是 LSTM,第一步走错了,后面全白搭。
今天咱们就聊聊,一段原始文本是怎么一步步变成模型能吃的数字的。说白了,就是让你看清 Tokenization 在整个流水线里到底站在哪个坑位上。
2.1 从字符串到张量,中间隔了几道坎?
你先想想看:模型只认识数字,对吧?那“我爱北京天安门”这句话,怎么变成一堆数字?
嗯,大致得经过这么几步:
- 原始文本 —— 用户输入的那一串字符。
- 文本清洗 —— 去掉乱七八糟的符号、统一大小写、处理空格。
- Tokenization —— 把字符串切成一个个小片段(词、子词、或者字符)。
- 词表映射 —— 把每个小片段替换成词表里的编号(ID)。
- Padding / 截断 —— 对齐长度,搞成固定长度的序列。
- 输入向量 —— 查 Embedding 表,变成模型能往下传的高维向量。
我当年刚入门的时候,以为只要把单词塞给模型就行。结果第一个项目就翻车了——中文句子没分词,直接按字输入,模型效果奇差。后来才明白,Tokenizer 的质量直接决定了模型的上限。
下面这个表格帮你快速对齐每个步骤的作用:
| 步骤 | 输入 | 输出 | 关键动作 |
|---|---|---|---|
| 文本清洗 | 原始字符串 | 干净字符串 | 去除HTML标签、统一Unicode、转小写 |
| Tokenization | 干净字符串 | 列表 | 按规则切分(词/子词/字符) |
| 词表映射 | 列表 | ID列表 | 查词汇表,OOV处理 |
| Padding/截断 | ID列表 | 定长ID序列 | 补0或裁剪到max_len |
| 嵌入层 | 定长ID序列 | Embedding张量 | 查表得到向量 |
2.2 Tokenization 到底站在流水线的哪个位置?
如果画一条完整的 NLP 处理流水线,Tokenization 差不多处于 中间偏前 的位置。它的前面是文本预处理,后面是特征映射。但你别小看它——它其实是个分水岭。
我个人的习惯是,先确定用哪种 Tokenization 策略,再反过来决定预处理该做到什么程度。举个例子:如果你用 BPE 子词分词,那清洗时保留大小写可能就没必要;如果你用整词分词,那大小写归一化就得好好做。
这里要注意一个常见的坑:Tokenization 不是单纯的切分,它还在帮你定义“词汇”的边界。你想想看,模型看到的“I”、“love”、“you”和我看到的“Iloveyou”是完全不同的信息结构。切得好,模型才会学得顺。
2.3 可视化流程:从文本到模型输入
下面这张图把整个过程串起来了。你看一眼就能明白 Tokenization 在哪个位置,前后分别是啥。
2.4 三个绕不开的概念
你一旦开始接触 Tokenization,下面这三个词就会天天见。我先帮你理清楚:
- 词表(Vocabulary):模型能认识的所有 的集合。常见大小是 3万~10万。太小了 OOV 多,太大了算力受不了。
- OOV(Out of Vocabulary):词表里没有的 。处理方式一般有两个——要么标为
[UNK]特殊 ,要么拆成更细的子词(能认出部分信息)。我推荐后者。 - Padding:把短的序列补齐到固定长度。通常补 0 或者
[PAD]的 ID。注意——Padding 的位置会影响 Attention,所以 Transformer 里都要加 attention mask 告诉模型别关注 padding 位。
我曾经在一个文本分类项目里,忘了处理 OOV,结果模型看到生僻词就直接输出 [UNK],测试集准确率暴跌 15%。后来改用 BPE 子词分词,OOV 几乎消失,效果立马上来了。所以啊,选 Tokenizer 时一定得关注它对 OOV 的处理策略。
2.5 代码看一眼就懂
说那么多抽象的东西,不如直接看代码。下面这段 Python 演示了最简单的手写单词 ID 映射流程:
# 手工做一个迷你词表
vocab = {"我": 1, "爱": 2, "NLP": 3, "[PAD]": 0, "[UNK]": 4}
def tokenize_and_encode(text, vocab, max_len=5):
# 简单按空格切分(仅演示)
tokens = text.split()
ids = []
for in tokens:
if in vocab:
ids.append(vocab[])
else:
ids.append(vocab["[UNK]"])
# Padding / 截断
if len(ids) < max_len:
ids += [vocab["[PAD]"]] * (max_len - len(ids))
else:
ids = ids[:max_len]
return ids
# 试试
print(tokenize_and_encode("我 爱 NLP", vocab))
# 输出: [1, 2, 3, 0, 0]
print(tokenize_and_encode("我 爱 深度学习", vocab))
# 输出: [1, 2, 4, 0, 0] (因为“深度学习”不在词表里,变成[UNK])
看到没?Tokenization 就是把非结构化的文字,变成了结构化的、固定长度的整数序列。后面模型就能拿这些整数去查 Embedding,再扔进 Transformer 里做计算了。
总结一下本章的核心:
- Tokenization 处于文本预处理和数值映射之间的关键衔接点。
- 它决定了模型看到的“词汇”粒度(词/子词/字符),直接影响 OOV 和语义保留。
- 从原始文本到模型输入,必须经过:清洗 → Tokenization → 词表映射 → Padding/截断。
- 实操中,选对 Tokenizer 比调模型超参数更值——我这两年项目经验证明了这一点。