2. Tokenization 基础概念:分词定义、词级 vs 子词 vs 字符级、常见工具

大家好啊,我是这门课的主讲。今天咱们聊 Tokenization,也就是分词。这个词儿你肯定听过,但真正搞明白它的三个层次——词级、子词、字符级——的人,说实话不多。

我刚开始做 NLP 的时候,觉得分词嘛,就是按空格切一切。直到我在一个中文客服系统项目里栽了跟头——用户说“我也要买个iPhone15ProMax”,按空格切出来一堆破烂,模型根本认不出“iPhone15ProMax”是个完整的商品名。从那以后,我才开始认真研究这玩意儿。

好,咱们一步步拆开。

2.1 什么是 Tokenization?

Tokenization 就是把一段文本拆成一个个“最小语言单元”的过程。这个最小单元就叫 (令牌)。

举个例子:

  • 输入:“I love NLP.”
  • 词级分词 → [“I”, “love”, “NLP”, “.”]
  • 子词分词(比如 BPE)→ [“I”, “ love”, “ NL”, “P”, “.”]
  • 字符级分词 → [“I”, “ “, “l”, “o”, “v”, “e”, “ “, “N”, “L”, “P”, “.”]

你看,同一个句子,不同切法得到完全不同的 序列。这直接影响模型的理解能力。说白了,Tokenization 就是文本和模型之间的“翻译官”——翻得好,模型学得快;翻得烂,后面全完蛋。

2.2 三种粒度:词级 vs 子词 vs 字符级

这三种粒度各有各的脾气。我整理了一张表,方便你对比:

维度 词级 (Word-level) 子词 (Subword-level) 字符级 (Character-level)
基本单元 完整单词(含标点) 词根、词缀、常见片段 单个字符(含空格)
词典大小 5万~50万+ 3万~10万左右 很小(几十~几百)
OOV 问题 严重 几乎无 无
序列长度 短 中等 很长(易超出模型限制)
代表模型 传统统计模型、早期 RNN BERT、、T5 CharCNN、Char-LSTM
训练效率 高 中等 低(长序列计算慢)
典型应用 拼写检查、简单分类 几乎所有现代 LLM 拼写纠正、语言模型预训练

我个人习惯:能用子词就别用词级。为什么?因为词级遇到新词就翻车,比如“”在2019年的词典里根本不存在,但你用 BPE 子词能拆成 “Chat” + “G” + “PT”,模型照样能理解。字符级虽然永远不会遇到生词,但序列太长,显存受不了,速度也慢。

2.3 常见工具:NLTK、SpaCy、Hugging Face

市面上分词工具多如牛毛,但真正经得起项目考验的,就这三家。咱们一个个过。

2.3.1 NLTK——老牌经典,适合教学和快速验证

NLTK(Natural Language Toolkit)是 Python 里最老牌的自然语言工具包。优点:上手快、文档全、内置多种 tokenizer。缺点:慢,不处理词向量或模型集成。

import nltk
nltk.download('punkt_tab')  # 第一次需要下载模型

text = "I'm learning tokenization. It's fun!"
tokens = nltk.word_tokenize(text)
print(tokens)
# 输出: ['I', "'m", 'learning', 'tokenization', '.', 'It', "'s", 'fun', '!']

注意 NLTK 会把 “I'm” 拆成 [“I”, “'m”] ——这在某些场景下很坑。我在一个情感分析项目里,因为这种拆分导致 “don't” 被切成 “do” 和 “n't”,模型把否定意义搞丢了,准确率掉了 6 个点。所以 NLTK 适合快速原型,生产环境慎用。

2.3.2 SpaCy——工业级速度,生产首选

SpaCy 专为生产环境设计,用 Cython 写的,速度比 NLTK 快一个数量级。它还自带词性标注、依存句法、命名实体识别,一条龙服务。

import spacy
nlp = spacy.load("en_core_web_sm")

text = "I'm learning tokenization. It's fun!"
doc = nlp(text)
tokens = [.text for  in doc]
print(tokens)
# 输出: ['I', "'m", 'learning', 'tokenization', '.', 'It', "'s", 'fun', '!']

看起来和 NLTK 结果一样?仔细看区别不大,但 SpaCy 的速度快 10 倍以上。我曾在每天处理千万条日志的系统里用 SpaCy,CPU 占用不到 20%。另外 SpaCy 支持自定义 tokenizer 规则,比如你想把 “iPhone15ProMax” 整个视为一个 :

from spacy.tokenizer import Tokenizer
from spacy.util import compile_prefix_regex, compile_infix_regex, compile_suffix_regex

nlp = spacy.load("en_core_web_sm")
prefix_re = compile_prefix_regex(nlp.Defaults.prefixes)
infix_re = compile_infix_regex(nlp.Defaults.infixes)
suffix_re = compile_suffix_regex(nlp.Defaults.suffixes)

def custom_tokenizer(nlp):
    return Tokenizer(nlp.vocab, prefix_search=prefix_re.search,
                     suffix_search=suffix_re.search,
                     infix_finditer=infix_re.finditer)

nlp.tokenizer = custom_tokenizer(nlp)
doc = nlp("I bought iPhone15ProMax yesterday")
tokens = [.text for  in doc]
print(tokens)
# 输出: ['I', 'bought', 'iPhone15ProMax', 'yesterday']

你看,跟 NLTK 比,SpaCy 的扩展性好得多。我建议你的每个生产项目都上 SpaCy,至少用它过一遍文本预处理。

2.3.3 Hugging Face Tokenizers——现代 LLM 标配

到了大模型时代,Hugging Face 的 transformers 和 tokenizers 库几乎成了事实标准。它实现了 BPE、WordPiece、Unigram 等主流子词算法,并且用 Rust 重写底层,速度极快。

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
text = "I'm learning tokenization. It's fun!"
tokens = tokenizer.tokenize(text)
print(tokens)
# 输出: ['i', "'", 'm', 'learning', '', '##ization', '.', 'it', "'", 's', 'fun', '!']

# 获取对应的 ID
ids = tokenizer.encode(text)
print(ids)
# 输出: [101, 1045, 1005, 1049, 4083, 19204, 4104, 1012, 2009, 1005, 1055, 3606, 999, 102]

这里有个关键区别:Hugging Face 的 tokenizer 输出的是 subword ,比如 “tokenization” 被拆成了 “” 和 “##ization”。这个 “##” 表示它是前一个 的后续部分。很多新手第一次看到这个符号会懵,其实它就是 BPE 算法的产物。

我的小经验: 无论你用哪个工具,一定要先看 tokenizer 对“i'm”“don't”“cannot”这类缩写的切法。不同模型处理方式不同,直接决定下游任务的效果。我在微调 BERT 做命名实体识别时,就因为没确认 tokenizer 细节,导致 “O'Brien” 被拆成三个 ,实体标签对齐全乱套了。

2.4 三种粒度选型决策流程

为了帮你快速决策,我画了一张流程图。

Tokenization 粒度选型流程图 输入文本 OOV(未登录词)风险高? 是 否 子词 / 字符级 词级 (Word-level) 序列长度≤512? (显存充足?) 否 是 字符级 子词 (推荐) 词典可控且空间小? 词级可用 💡 大部分 NLP 项目推荐子词

流程图帮你理清了决策路径。简单总结:如果项目涉及大量专业术语、地名、产品名,或者你要处理多种语言混合文本(比如中英文混杂),直接上子词 tokenizer,省事又省心。

2.5 避坑指南(我踩过的坑)

我曾经在金融文本处理时犯过的一个错误:

我把一整批财报里的“$1.2B”用 NLTK 的 word_tokenize 切,结果切成了 [“$”, “1.2”, “B”] — 丢了货币金额的整体语义。后来换成 SpaCy 的自定义 tokenizer,匹配“$+数字+[B|M|K]”模式才搞定。所以:用通用 tokenizer 之前,先看看你的领域数据里有没有特殊模式,比如日期、金额、产品名。该加规则就加,别偷懒。

另一个经验: 如果你用 Hugging Face 的 tokenizer 做微调,记得检查 tokenizer 的 add_prefix_space 参数。默认 False 时,BERT 等模型在句子开头不加空格,但有些模型(如 )需要前缀空格。我在一次复现 训练时,因为没改这个参数,结果 loss 一直不降,折腾了一天才发现是 tokenizer 配置错了。

好了,Tokenization 的基础概念就聊到这儿。你可以动手试试:拿一段你自己的文本,分别用 NLTK、SpaCy、Hugging Face 的 tokenizer 跑一下,看看输出差异。相信我,亲手跑一遍比看十遍文章都管用。