文本怎样变成编号:语料、Token 和词表

从一句中文出发,理解字符、词与子词的切分,运行 Python 构建词表,再用互动实验观察 Token、编号与未知输入。配套 1080p 中文讲解视频。

学习路线:NLP 与序列模型(69~78) · 第 69 课

带着问题开始

本课目标

  • 区分语料、文档、Token 和词表
  • 比较字符、词、子词三种切分粒度
  • 运行 Python 建立稳定词表,并解释未知输入
前置知识与概念回顾

前置知识

  • 能运行简单的 Python 列表、字典与函数。
  • 已了解模型输入需要转换成数字;本课不要求先掌握 RNN 或 Transformer。

概念回顾

  • 图像通过像素进入模型,文本需要先切分、再查词表得到编号。
  • 同一条输入规则与词表必须贯穿训练和推理,编号本身不表示语义。

图片预览

上一阶段完成了深度学习项目,这一阶段开始处理文本序列。先把文字变成可计算的输入,再逐步学习 Embedding、RNN 和 Transformer。建议先看视频,再运行代码和切分实验。

配套视频与章节

第 69 课完整讲解 · 8 分 12 秒 · 1080p · 中文配音与字幕

视频已内嵌短句字幕,也提供可切换的中文字幕轨。章节按钮可直接跳转播放。

一句话进入神经网络前,要先经历一次翻译:从人类文字,翻译成模型可以计算的整数序列。

图片可以变成像素,表格可以变成数值,但文字不是天然的数字。模型看到“机器学习很有趣”时,并不知道“机器”和“学习”有什么含义。

我们要先回答三个问题:

  1. 一段文字应该切成多大的单位?
  2. 每个单位怎样获得稳定编号?
  3. 训练时没见过的新词怎么办?

本课目标

学完后,你应该能:

  • 区分语料、句子、Token 和词表。
  • 理解字符、词和子词三种切分粒度。
  • 用 Python 构建一个最小词表。
  • 把句子转换成 Token 列表,并发现中文分词的难点。

第 69 课知识图解:文本怎样变成编号:语料、Token 和词表

看图抓主线:文本切分 → 频次统计 → 建立词表 → 稳定编号。

看图自测:点击检查自己能否复述这条主线

先遮住上面的正文,只看图回答:

  1. 这一课的输入是什么?
  2. 中间最关键的变化发生在哪里?
  3. 最终输出或判断标准是什么?
  4. 哪一步最容易出错,为什么?

能用自己的话串起四张卡片,就说明你已经抓住了本课骨架。再回到正文补充公式、代码和边界条件。


先认识四个基本概念

概念 含义 例子
语料 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:先划分数据以后,仍用全部语料建词表

严格实验中,词表只能用训练集构建。若用验证集和测试集统计词表,就提前看到了未来数据。


课后练习

  1. 分别按字符和按空格切分 "deep learning is fun",比较 Token 数。
  2. 修改 tokenize,把中文标点过滤掉,但保留英文和数字。
  3. 把 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。

完整代码可下载 完整示例代码。

读完以后

动手练习,检查理解。

常见误区

  • Token 可以是字、词或子词;词级示例不代表所有分词器的真实输出。
  • 词表只能由训练语料构建,验证集与测试集不能参与统计。
  • 更换切分规则时必须配套对应词表,词表越大并不一定越好。

课后练习

  • 比较 deep learning is fun 的字符级与空白级切分。
  • 把频次阈值改成 2,观察词表大小与未知输入。
  • 运行空语料统计,确认不会发生最小值或除零错误。

自检与参考答案

1. 本课语料有多少个 Token、多少种普通 Token?

三份文档长度分别为 7、12、11,总共 30 个字符 Token,去重后有 23 种普通 Token。

2. 为什么必须保存分词规则与词表?

更换词表后相同编号可能代表其他字符。训练与推理需要同一套映射和特殊编号。

3. 词表阈值为 2 时,词表大小是多少?

保留“习、器、学、我、机”五种普通 Token,加上四种特殊 Token,共 9 项;低频与新输入走 UNK。

下一课:从词到 ID,建立双向映射与未知词处理;完成后继续学习 Padding、截断与 Mask。

沿着路线继续

NLP 与序列模型(69~78)

查看学习路线
Discussion

留言讨论