一句话总结
分词(Tokenization)是 NLP 的第一步--把一段连续文本切成机器能处理的最小单元.
中文没有天然空格分隔,因此中文分词本身就是一个需要解决的问题.
为什么需要分词
回忆上一篇的 pipeline 调用流程:
|
模型只认数字,不认文字. 所以必须有一个环节把文字变成数字序列. 这个环节就是 Tokenizer 做的事,核心包含两步:
- 分词(Tokenize):把连续文本切成 token 列表
- 编码(Encode):把每个 token 映射成词表中的数字 ID
分词器 = Go 中 http.Request 的解析器. 原始请求是一长串字节流,解析器把它拆成 Method, URL, Headers, Body 等结构化字段--分词器的角色等同于这个解析器,把原始文本拆成结构化的 token 序列.
英文分词 vs 中文分词
英文天然有空格分隔单词,分词几乎是"免费的":
|
但中文没有空格,所有字连在一起:
|
中文分词的歧义问题是经典 NLP 难题. 好消息是:现代预训练模型(BERT 系列)使用子词切分,基本绕过了传统分词问题--后面会详细解释.
三种分词粒度
根据切分粒度从粗到细,分词方法分三类:
| 粒度 | 示例 | 优点 | 缺点 | 代表工具 |
|---|---|---|---|---|
| 词级(Word) | "机器学习" → ["机器学习"] | 语义完整 | 词表巨大,OOV(未登录词)多 | jieba,pkuseg |
| 子词级(Subword) | "机器学习" → ["机器", "学习"] | 词表可控,能处理新词 | 需要预训练词表 | BPE,WordPiece |
| 字级(Character) | "机器学习" → ["机", "器", "学", "习"] | 无 OOV,词表最小 | 序列过长,丢失词义 | 逐字切分 |
OOV 是什么
OOV(Out of Vocabulary,未登录词)指模型词表中没有的词. 比如 "yyds""绝绝子" 这类新词,词级分词遇到它们就无法处理. 子词切分通过把未知词拆成已知的子片段来解决这个问题.
词级分词:jieba 实战
jieba 是中文最常用的词级分词工具,基于 HMM + 词典的混合方案. 虽然 BERT 不直接依赖它,但数据预处理和特征工程阶段经常用到.
|
jieba 的局限在于它是通用词典,群聊中的缩写("yyds""xswl""nmsl")和新词无法识别. 可以通过添加自定义词典缓解:
|
分词器 vs 编码器:jieba 为什么没有 encode
jieba 只输出字符串列表,而 BERT Tokenizer 还能输出数字 ID 序列. 这不是 jieba 少了什么功能--是它们的职责定位不同:
| jieba | BERT Tokenizer | |
|---|---|---|
| 定位 | 纯分词工具(只管切) | 模型前端(切词 + 编码 + 加特殊标记) |
| 有 ID 词表吗 | 有词频词典,但不维护 token → ID 映射 | 有固定词表(21128 个 token),每个 token 对应唯一 ID |
| 输出 | ["你", "脑子", "有", "问题"] |
[101, 872, 5554, 2094, 3300, 7309, 7579, 102] |
| 下游对接 | 交给 FastText/sklearn 等各自编码 | 直接喂给 BERT 模型 |
jieba = Go 的 json.Decoder, 只负责把 []byte 拆成 token 流,下游爱怎么用怎么用.
BERT Tokenizer = proto.Marshal, 直接输出模型能消费的二进制格式,和模型绑定.
jieba 路线下,"编码为 ID" 这一步由下游各自处理:
- FastText:内部自带词表,喂字符串进去它自己编码
- TF-IDF + LogisticRegression:sklearn 的 vectorizer 把词转为稀疏向量,不需要 ID
- 自训练神经网络:自己建 word2id 字典做映射
关键原则:ID 只是查找键,不同词表产生不同 ID,但这不影响模型效果. 真正重要的是词表覆盖率(能不能编码所有输入词)和粒度匹配(用什么粒度训练的模型,推理时就得用同样的粒度).
子词分词:BERT 的 WordPiece
BERT 是什么的缩写
Bidirectional Encoder Representations from Transformers--来自 Transformer 的双向编码表示. 名字透露了两个核心特征:
- 双向:同时看一个词的左边和右边上下文来理解它,而不是像 GPT 那样只看左边
- 编码器:只做"理解"(编码输入文本的语义),不做"生成"(写出新文本). 对分类任务来说,只需要理解就够了,所以 BERT 正好对口.
BERT 使用的是 WordPiece 算法--一种子词切分方法. 核心思路:
- 从字符粒度开始
- 统计语料中相邻字符对的共现频率
- 把高频字符对合并为一个子词
- 重复直到词表达到预设大小(中文 BERT 通常 21128 个)
WordPiece = HTTP/2 的 HPACK 头部压缩. 高频出现的 header 组合(如 "content-type: application/json")被分配一个短编码. WordPiece 做的事一样--高频字符组合变成一个 token,节省序列长度.
实际效果:
|
中文 BERT 的 WordPiece 约等于字级切分
对于中文,BERT 的 WordPiece 结果基本是逐字切分. 这不是缺陷--BERT 通过多层 Transformer 的自注意力机制,在后续计算中自动学习哪些字组成词,哪些字组成短语. 分词的"智能"从 Tokenizer 转移到了 Model 内部.
特殊 Token:CLS 和 SEP
BERT Tokenizer 编码后,序列头尾会自动加上两个特殊标记:
- [CLS] = Classification,分类标记
- [SEP] = Separator,分隔标记
|
CLS 的作用:整句语义的汇聚点
BERT 模型内部有多层 Transformer,每一层中每个 token 都会"看"其他所有 token(这就是 Self-Attention). 经过 12 层计算后,[CLS] 这个位置的输出向量已经融合了整个句子所有字的信息.
[CLS] = Go 里的 context.Context. 它本身不是业务数据,但在整个调用链中被层层传递,最终承载了请求的全部上下文信息(超时,取消信号,trace ID 等). [CLS] 就是 BERT 的 "context 对象"--本身只是一个占位符,经过多层注意力计算后汇聚了整句话的语义.
做文本分类时,工作流是这样的:
|
为什么不取其他位置? 因为 BERT 预训练时就是让 [CLS] 承担句子级任务(Next Sentence Prediction),所以这个位置天然适合做整句分类. 其他位置的输出更适合做 token 级任务(如命名实体识别--判断每个字属于什么实体).
SEP 的作用:句子边界标记
SEP 标记句子的结束. 当需要同时输入两段文本时(比如判断两句话是否构成对骂对话),SEP 用来分隔它们:
|
如果后续要做对话级对骂检测(把上下文两条消息一起输入判断是否在互骂),就会用到双句输入格式.
简单记忆
CLS = 汇总器,经过模型计算后它"读过"了整句话,输出向量代表全句语义,直接用来做分类.
SEP = 分隔符,告诉模型"这里是一句话的结束/两句话的边界".
这两个 token 不是文本内容,是给模型的结构信号.
做对骂检测(文本分类)时,模型取 [CLS] 位置的输出向量,接一个分类头(线性层)得到预测结果. 这是标准范式,后续阶段三会详细展开.
文本预处理流水线
在真实群聊场景中,原始消息很脏. 分词之前通常需要一系列清洗步骤:
|
每个步骤的作用和群聊场景示例:
| 步骤 | 做什么 | 群聊示例 |
|---|---|---|
| 去特殊字符 | 删除零宽字符,控制符 | 肉眼看不到但会干扰分词的字符 |
| 处理表情 | emoji 转文本描述或移除 | 😡 → [愤怒] 或直接删除 |
| 统一编码 | 全角 → 半角,大写 → 小写 | "NMSL nmsl" 统一为 "nmsl" |
| 去 URL/at | 移除链接和 @提及 | "@张三 你看这个 http://..." → "你看这个" |
| 繁简转换 | 繁体统一为简体 | "這個人真煩" → "这个人真烦" |
| 去停用词 | 移除 "的""了""啊" 等无意义词 | 对分类类任务通常不去,因为语气词携带情绪信息 |
什么是停用词
停用词(Stop Words)是指在文本中高频出现但通常不携带实质语义的词. 类比 Go 代码中的 if, for, return--它们是语法骨架,但不是业务逻辑本身.
中文常见停用词举例:
| 类别 | 示例 | 特点 |
|---|---|---|
| 助词 | 的,地,得,了,过 | 几乎每句话都有,不影响核心语义 |
| 介词/连词 | 在,和,与,或,但是 | 连接作用,本身无意义 |
| 代词 | 这,那,它,其 | 指代功能,独立时无意义 |
| 语气词 | 啊,呢,吧,嘛,呗 | 表达语气--在情感分析中有用 |
去停用词的典型做法是维护一个停用词列表(几百到几千个词),分词后把命中的词删掉:
|
去停用词在搜索引擎和文本摘要中很有用--减少噪声,让模型聚焦关键内容. 但对情感分析来说是把双刃剑.
对骂检测不要轻易去停用词
"你他妈的滚" 中的 "的" 是停用词,但 "他妈" 是关键特征,去掉 "的" 后 "他妈" 可能也被拆散.
情感分析类任务中语气助词("啊""呢""吧""呗")承载攻击性强度信息--"滚吧" 比 "滚" 多一分轻蔑,"滚啊" 比 "滚" 多一分激烈. 建议保留. 去停用词更适合搜索,摘要等纯语义理解任务.
预处理代码示例
一个适用于群聊对骂检测的精简预处理函数:
|
实际工作中谁来分词
理解了分词的原理后,实际工作流是这样的:
| 场景 | 谁负责分词 | 需要做什么 |
|---|---|---|
| 使用 BERT/Transformer 模型 | 模型自带的 Tokenizer(WordPiece) | 只做文本清洗,分词交给 Tokenizer |
| 使用 FastText/传统方法 | 需要自己先分词(jieba 等) | 清洗 + jieba 分词 + 拼接为空格分隔文本 |
| 特征工程/数据分析 | jieba 或自定义分词 | 分词后统计词频,提取关键词等 |
简单记忆
用 BERT → 只管清洗,分词它自己做.
用 FastText → 需要先分词再喂进去.
对骂检测最终会用 BERT 微调,所以重心放在数据清洗而非分词算法本身.
词表大小的工程意义
作为后端开发,可能会关心词表大小对系统的影响:
| 模型 | 词表大小 | Embedding 层参数 | 含义 |
|---|---|---|---|
| bert-base-chinese | 21,128 | 21128 × 768 ≈ 16M 参数 | 每个 token 对应一个 768 维向量 |
| GPT-2 | 50,257 | 50257 × 768 ≈ 38M 参数 | 子词粒度更细 |
| jieba 词典 | ~58 万词 | - | 词级粒度,词表巨大 |
词表越大 → Embedding 层越大 → 模型文件越大 → 推理时内存占用越大. 这就是为什么 BERT 选择 2 万级别的子词词表,而不是用 58 万的完整词典--是效果和资源的平衡.
本篇要点串联
把这篇的知识点放回上一篇的 pipeline 调用流程中:
|
蓝色部分就是本篇覆盖的内容. 预处理和分词是能直接控制的环节--清洗质量直接影响模型效果.
快速回顾
- 分词三种粒度:词级(jieba) → 子词级(WordPiece/BPE) → 字级,BERT 中文约等于字级
- BERT Tokenizer:自动完成分词+编码,只需要做好前置清洗
- CLS/SEP:BERT 的特殊标记,CLS 位置承载整句语义用于分类
- 预处理优先级:对骂检测中,去 URL/@提及 > 统一编码 > 不要去停用词/emoji
- 工作流分工:BERT 路线只需清洗,FastText 路线需要 jieba 分词
动手练习
- 安装 jieba 并分词:执行
uv add jieba,对种子数据集的 20 条对骂文本做分词,观察分词结果是否合理 - 对比 WordPiece:使用
BertTokenizer.from_pretrained("bert-base-chinese")对同样的文本做编码,对比 jieba 和 WordPiece 的切分差异 - 编写预处理函数:对种子数据集做清洗,保存为
data/seed_dataset_cleaned.csv