上一阶段完成了深度学习项目,这一阶段开始处理文本序列。先把文字变成可计算的输入,再逐步学习 Embedding、RNN 和 Transformer。建议先看视频,再运行代码和切分实验。
配套视频与章节
视频已内嵌短句字幕,也提供可切换的中文字幕轨。章节按钮可直接跳转播放。
一句话进入神经网络前,要先经历一次翻译:从人类文字,翻译成模型可以计算的整数序列。
图片可以变成像素,表格可以变成数值,但文字不是天然的数字。模型看到“机器学习很有趣”时,并不知道“机器”和“学习”有什么含义。
我们要先回答三个问题:
- 一段文字应该切成多大的单位?
- 每个单位怎样获得稳定编号?
- 训练时没见过的新词怎么办?
本课目标
学完后,你应该能:
- 区分语料、句子、Token 和词表。
- 理解字符、词和子词三种切分粒度。
- 用 Python 构建一个最小词表。
- 把句子转换成 Token 列表,并发现中文分词的难点。
看图抓主线:文本切分 → 频次统计 → 建立词表 → 稳定编号。
看图自测:点击检查自己能否复述这条主线
先遮住上面的正文,只看图回答:
- 这一课的输入是什么?
- 中间最关键的变化发生在哪里?
- 最终输出或判断标准是什么?
- 哪一步最容易出错,为什么?
能用自己的话串起四张卡片,就说明你已经抓住了本课骨架。再回到正文补充公式、代码和边界条件。
先认识四个基本概念
| 概念 | 含义 | 例子 |
|---|---|---|
| 语料 Corpus | 用于训练或分析的全部文本 | 一万条影评 |
| 文档 Document | 语料中的一个独立文本单元 | 一篇影评 |
| Token | 切分后的最小处理单位 | “机器学习”或“机” |
| 词表 Vocabulary | Token 与编号的映射表 | 学习 -> 12 |
一份小语料可以是:
corpus = [
"我喜欢机器学习",
"机器学习可以解决分类问题",
"我正在学习自然语言处理",
]
这里有 3 个文档。切分后产生 Token,所有保留下来的 Token 组成词表。
Token 不一定等于“词”
方案一:按字符切分
text = "我喜欢机器学习"
tokens = list(text)
print(tokens)
# ['我', '喜', '欢', '机', '器', '学', '习']
优点是实现简单,相比词级切分通常未知风险较低;训练词表外的字符仍需映射到 <UNK>;缺点是序列较长,而且“机器学习”这个整体概念被拆散。
方案二:按词切分
我 / 喜欢 / 机器学习
序列更短、语义更完整,但中文没有天然空格,需要分词工具或规则。新词、专有名词和网络用语也会带来问题。
方案三:按子词切分
机器 / 学习
上面的“机器 / 学习”只是教学示意,不保证任何真实 tokenizer 都这样切。现代 Transformer 常用 BPE、WordPiece 等方法,SentencePiece 是常用工具库。实际切分取决于算法、训练语料和词表;常见片段可以保留,生僻词可拆成较小单位。
下表描述常见趋势,具体数值依任务而定。
| 粒度 | 词表大小 | 序列长度 | 未知词风险 |
|---|---|---|---|
| 字符 | 小 | 长 | 低 |
| 词 | 大 | 短 | 高 |
| 子词 | 中等 | 中等 | 较低 |
先猜再看:同一句话能切成几种样子
句子:
南京市长江大桥
你会怎样切分?
展开查看可能答案
至少有两种容易想到的切法:
南京市 / 长江大桥
南京 / 市长 / 江大桥
第二种虽然语法奇怪,却说明中文分词不能只靠逐字扫描。切分结果会直接影响后续模型看到的语义单位。
小实验:改变切分规则
词级模式是小词典的教学示例,不是成熟中文分词器。本实验故意沿用固定字符词表,观察更换规则后的未知输入;实际训练必须为对应切分规则建立配套词表。
试试“你好”或“deep learning is fun”,观察未知编号和 Token 数量。
写一个最小中文切分器
为了理解流程,我们先不依赖第三方分词库,只做字符级切分,并过滤空白:
import re
def tokenize(text):
text = text.strip().lower()
text = re.sub(r"\s+", "", text)
return list(text)
print(tokenize("我 喜欢 AI!"))
# ['我', '喜', '欢', 'a', 'i', '!']
这个切分器不够聪明,但它有一个重要优点:规则完全透明。学习 NLP 时,先让数据流可见,再更换成熟 tokenizer,会更容易理解。
从语料统计 Token
from collections import Counter
corpus = [
"我喜欢机器学习",
"机器学习可以解决分类问题",
"我正在学习自然语言处理",
]
counter = Counter()
for text in corpus:
counter.update(tokenize(text))
print(counter.most_common(8))
输出可能类似:
[('学', 3), ('习', 3), ('我', 2), ('机', 2), ('器', 2), ('喜', 1), ('欢', 1), ('可', 1)]
频次统计决定哪些 Token 值得进入词表。出现一次的噪声、错别字或随机编号,未必需要各占一个位置。
构建第一个词表
先预留特殊 Token:
special_tokens = ["<PAD>", "<UNK>", "<BOS>", "<EOS>"]
然后按频次筛选,再按字符排序加入普通 Token:频率决定保留与否,排序决定稳定的编号顺序。
def build_vocab(corpus, min_frequency=1):
counter = Counter()
for text in corpus:
counter.update(tokenize(text))
vocabulary = list(special_tokens)
normal_tokens = [
token
for token, frequency in sorted(counter.items())
if frequency >= min_frequency
]
vocabulary.extend(normal_tokens)
return vocabulary
vocabulary = build_vocab(corpus)
word_to_index = {
token: index for index, token in enumerate(vocabulary)
}
print(word_to_index)
为什么先放特殊 Token?因为它们的编号需要固定。例如 Padding 通常约定为 0,这会让 Mask 和 Embedding 更容易处理。
从文本到模型输入的完整链路
原始文本
↓ 清洗
标准化文本
↓ 分词
Token 列表
↓ 查词表
Token ID 列表
↓ 补齐
批次内补齐的 Tensor
↓ Embedding
连续向量
这一课完成了前半段:文本、Token 和词表。后面三篇会继续完成编号、补齐和向量化。
实战:查看语料统计
def corpus_report(corpus):
tokenized = [tokenize(text) for text in corpus]
lengths = [len(tokens) for tokens in tokenized]
counter = Counter(token for tokens in tokenized for token in tokens)
return {
"documents": len(corpus),
"total_tokens": sum(lengths),
"unique_tokens": len(counter),
"min_length": min(lengths, default=0),
"max_length": max(lengths, default=0),
"average_length": sum(lengths) / len(lengths) if lengths else 0,
"empty_documents": sum(length == 0 for length in lengths),
"top_tokens": counter.most_common(10),
}
for key, value in corpus_report(corpus).items():
print(f"{key}: {value}")
在训练任何文本模型前,都应该先看:
- 文档数量。
- 序列长度分布。
- 高频 Token。
- 词表规模。
- 空文本和异常长文本。
数据统计不是附加工作,它决定最大长度、词表阈值和模型规模。
常见误区
误区 1:Token 就是汉语词语
Token 是模型的处理单位,可以是字、词、子词甚至字节。不要把语言学中的“词”和模型中的 Token 完全等同。
误区 2:词表越大越好
词表越大,Embedding 参数越多,低频 Token 也越难学好。需要在覆盖率和模型规模之间取平衡。
误区 3:先划分数据以后,仍用全部语料建词表
严格实验中,词表只能用训练集构建。若用验证集和测试集统计词表,就提前看到了未来数据。
课后练习
- 分别按字符和按空格切分
"deep learning is fun",比较 Token 数。 - 修改
tokenize,把中文标点过滤掉,但保留英文和数字。 - 把
min_frequency设为 2,观察词表怎样变化。
参考答案 / 自检
使用本课删除空白的字符切分器会得到 17 个字母;按空格切分得到 4 个 Token。过滤标点可使用:
def tokenize(text):
text = text.lower()
return re.findall(r"[\u4e00-\u9fff]|[a-z0-9]+", text)
这段参考正则也改变了粒度:连续英文和数字会组成一个 Token;若希望仍按字符拆分,需要额外展开这些片段。
min_frequency=2 会从词表中移除只出现一次的 Token。它们之后应映射到 <UNK>,而不是直接丢弃整条文本。
本课小结
- 语料由文档组成,文档切成 Token,Token 集合构成词表。
- 字符、词和子词是不同切分粒度,没有一种方案适合所有任务。
- 中文没有天然空格,分词本身就是模型输入设计的一部分。
- 词表只应由训练集构建,并为特殊 Token 预留稳定编号。
- 训练前先统计长度、频次和词表规模。
下一课,我们会让词表真正工作起来:建立 word_to_index 与反向映射,并处理训练时从未出现过的新词。
延伸阅读
本课实际运行结果
3 份文档,30 个字符 Token,23 种普通 Token。长度为 7、12、11,平均 10。加上特殊 Token 后词表大小为 27;阈值设为 2 时词表大小为 9。
完整代码可下载 完整示例代码。
留言讨论