> For the complete documentation index, see [llms.txt](https://inwt233.gitbook.io/ai-learning/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://inwt233.gitbook.io/ai-learning/di-yi-bu-fen-nlp-ji-chu/week-01/day-004.md).

# Day 004：字符、单词、子词与字节级分词

{% hint style="info" %}
**同一段文本为什么可以被切成完全不同的 Token？字符、单词、子词和字节这四种粒度，分别把什么当作模型的基本输入单位？**
{% endhint %}

## 学习目标

完成本节后，你应该能够：

1. 用统一框架比较词、子词、字符与字节四种输入粒度；
2. 区分纯字节模型、byte-level BPE 与 byte fallback；
3. 解释粒度如何影响词表、序列长度、覆盖率与语义组合起点；
4. 根据 checkpoint、语言、任务与部署指标选择粒度；
5. 用标准 Python 比较同一文本在不同粒度下的长度。

{% hint style="info" %}
本节默认已经掌握 [Day 001](/ai-learning/di-yi-bu-fen-nlp-ji-chu/week-01/day-001.md) 的 Unicode、UTF-8、Token 与 Token ID，[Day 002](/ai-learning/di-yi-bu-fen-nlp-ji-chu/week-01/day-002.md) 的规范化与预切分，以及 [Day 003](/ai-learning/di-yi-bu-fen-nlp-ji-chu/week-01/day-003.md) 的词表、OOV 和 Tokenizer–Embedding 契约。这里不再重复定义，而聚焦一个新问题：**模型应该把多大的文本单位当作输入原子？**
{% endhint %}

## 1. 分词粒度其实是在决定模型的“原子”

神经网络不能直接接收人类看到的文字。Tokenizer 必须先把字符串映射为有限 ID 序列：

```
原始字符串
  ↓ 规范化
规范化字符串
  ↓ 选择切分边界和基本单位
Token 字符串/符号序列
  ↓ 词表查找
Token ID 序列
  ↓ Embedding 查表
向量序列 X ∈ R^(n×d)
```

其中：

* `n` 是切分后的序列长度；
* `d` 是模型隐藏维度；
* 每一种粒度都会同时影响词表大小 `|V|` 和序列长度 `n`；
* 模型从哪一层开始组合语义，也由基本单位决定。

例如，同一句“ChatGPT正在学习!”可以有多种表示：

```
单词式：ChatGPT / 正在 / 学习 / !
子词式：Chat / GPT / 正在 / 学习 / !
字符式：C / h / a / t / G / P / T / 正 / 在 / 学 / 习 / !
字节式：67 / 104 / 97 / ... / 228 / 185 / 160 / 33
```

这些切分没有一个脱离任务和模型的“唯一正确答案”。它们是在不同成本之间做选择。

{% hint style="warning" %}
粒度描述的是 Tokenizer 允许模型直接查表的单位，不代表模型只能理解这一层。字符模型可以在高层组合出词，子词模型也可以在高层组合出短语。
{% endhint %}

## 2. 四种粒度放在同一坐标系里

“粒度”问的是：**Embedding 层一次查表得到的单位是什么？**

| 粒度   | 典型输入单位         | 直接优势                | 主要代价             |
| ---- | -------------- | ------------------- | ---------------- |
| 词级   | 规则、词典或模型识别的整词  | 序列通常较短              | 词表大，长尾和 OOV 明显   |
| 子词级  | 从语料学得的常见字符串片段  | 覆盖、词表与长度较平衡         | 切分依赖训练分布，不保证符合词素 |
| 字符级  | 通常是 Unicode 码点 | 拼写信息细，词表较小          | 序列较长；码点不等于字素簇    |
| 纯字节级 | UTF-8 的单个字节    | 256 个单位覆盖任意合法 UTF-8 | 序列通常最长，需重建字符和词   |

同一句话可以有不同输入原子：

```
单词式：ChatGPT / 正在 / 学习 / !
子词式：Chat / GPT / 正在 / 学习 / !
字符式：C / h / a / t / G / P / T / 正 / 在 / 学 / 习 / !
字节式：67 / 104 / 97 / ... / 228 / 185 / 160 / 33
```

需要特别保留两个边界：

* **词级不等于按空格切分。** 英文仍有缩写、连字符和标点；中文等语言通常还需要规则、词典或统计模型。
* **子词不等于词素。** BPE、WordPiece、Unigram 优化统计或概率目标，结果有时像词根，有时只是高频字符串。

严格的码点、字素簇与 UTF-8 定义见 [Day 001](/ai-learning/di-yi-bu-fen-nlp-ji-chu/week-01/day-001.md)；不同粒度对 OOV 的影响见 [Day 003](/ai-learning/di-yi-bu-fen-nlp-ji-chu/week-01/day-003.md)。

## 3. “字节级 Tokenizer”可能指三种系统

| 系统                  | 常规处理方式                  | 是否学习合并 |
| ------------------- | ----------------------- | ------ |
| 纯字节模型               | 每个 UTF-8 字节直接成为模型 Token | 否      |
| Byte-level BPE      | 从完整字节集合起步，合并高频字节序列      | 是      |
| 带 byte fallback 的子词 | 常规用子词，无法覆盖时退回字节 Token   | 取决于主模型 |

### 3.1 纯字节模型

```
文本 → UTF-8 → 单字节序列 → Embedding
```

ByT5 属于这类模型。它覆盖稳定、基础集合固定，但中文字符通常需要 3 个 UTF-8 字节，因此序列明显变长。

### 3.2 Byte-level BPE

```
文本 → UTF-8 字节 → 可逆字节代理符号 → BPE 合并 → Token ID
```

GPT-2 风格 Tokenizer 属于这一类。最终 Token 可以包含多个字节；`Ġ` 等显示符号来自具体映射或边界约定，不等于原文中的普通字符。

### 3.3 Byte fallback

```
常见输入 → 普通子词
无法覆盖的字符 → 对应 UTF-8 字节 Token
```

它是一条兜底路径，不表示全部文本始终逐字节处理。Day 005 会继续解释 byte-level BPE 的合并机制。

## 4. 实验：同一段文本在四种粒度下有多长

下面的代码故意不依赖第三方分词库，以便逐行理解。它不是生产级 Tokenizer，而是用于观察粒度差异。

```python
import re

text = "ChatGPT正在学习!"

def word_like_tokenize(s: str) -> list[str]:
    """连续英文、连续中文、标点分别成组。"""
    return re.findall(r"[A-Za-z]+|[\u4e00-\u9fff]+|[^\w\s]", s)

def longest_match(s: str, vocab: set[str],) -> list[str]:
    """从左到右选择词表中最长的可匹配片段。"""
    result = []
    i = 0
    max_len = max(map(len, vocab))

    while i < len(s):
        for width in range(min(max_len, len(s) - i), 0, -1):
            piece = s[i : i + width]
            if piece in vocab:
                result.append(piece)
                i += width
                break
        else:
            result.append("<UNK>")
            i += 1

    return result

char_tokens = list(text) # 字符级
word_tokens = word_like_tokenize(text) # 词级
subword_vocab = {"Chat","GPT","正在","学习","!"} 
subword_tokens = longest_match(text, subword_vocab) # 字词级
byte_tokens = list(text.encode("utf-8")) # 字节级

print("text:", text)
print(
    "character:", char_tokens,
    "count =", len(char_tokens),
)
print(
    "word-like:", word_tokens,
    "count =", len(word_tokens),
)
print(
    "subword:", subword_tokens,
    "count =", len(subword_tokens),
)
print(
    "UTF-8 bytes:", byte_tokens,
    "count =", len(byte_tokens),
)
print("byte round-trip:", bytes(byte_tokens).decode("utf-8"),)
```

本次实测输出：

```
text: ChatGPT正在学习!
character: ['C', 'h', 'a', 't', 'G', 'P', 'T', '正', '在', '学', '习', '!'] count = 12
word-like: ['ChatGPT', '正在学习', '!'] count = 3
subword: ['Chat', 'GPT', '正在', '学习', '!'] count = 5
UTF-8 bytes: [67, 104, 97, 116, 71, 80, 84, 230, 173, 163, 229, 156, 168, 229, 173, 166, 228, 185, 160, 33] count = 20
byte round-trip: ChatGPT正在学习!
```

### 4.1 为什么不能据此断言“词级最好”

这个例子中，词式规则只产生 3 个单位，看起来最短，但它把连续中文 `正在学习` 整体当成一个“词”。这并不是真实中文词边界，也意味着词表必须直接收录这个长字符串，否则可能变成 `<UNK>`。

子词示例产生 5 个单位，因为我们事先给了一个刚好覆盖文本的玩具词表。如果移除 `正在`：

```python
subword_vocab.remove("正在")
print(longest_match(text, subword_vocab))
```

最长匹配器会在“正”和“在”处输出 `<UNK>`。真实子词 Tokenizer 通常会保留字符或字节级基础符号来提高覆盖率。

因此，比较 Token 数时必须同时报告：

* 使用的具体 Tokenizer/checkpoint；
* 词表大小；
* 是否产生 unknown；
* 训练语料与测试语料；
* 规范化和预切分规则。

### 4.2 最长匹配不等于 BPE 训练

`longest_match()` 只负责在一个已经给定的词表中编码。它没有解释这个词表如何学出来。

BPE 的训练阶段会从小单位开始，反复选择高频相邻符号对并合并，形成新的词表项；编码阶段再按学到的合并规则切分。Day 5 将手工追踪完整 BPE 合并过程。

## 5. 模型怎样从更小单位恢复词义

假设“自然语言处理”被表示为：

```
自然 / 语言 / 处理
```

Embedding 层最初只查到三个静态向量：

$$
x\_1=E\[\text{自然}],\quad
x\_2=E\[\text{语言}],\quad
x\_3=E\[\text{处理}]
$$

加上位置等信息后，它们进入 Transformer。自注意力与前馈网络让每个位置逐层结合上下文：

$$
H^{(\ell+1)}
\=\operatorname{Block}\_{\ell}
\left(H^{(\ell)}\right)
$$

高层隐藏状态可以表示整个术语在当前句子中的意义。模型不要求每个概念一开始就对应一个词表行。

四种粒度的区别在于组合从哪里开始：

| 粒度  | Embedding 层直接提供 | 模型还需要组合            |
| --- | --------------- | ------------------ |
| 词级  | 整词的初始向量         | 词 → 短语/句子          |
| 子词级 | 常见片段的初始向量       | 子词 → 词 → 更高结构      |
| 字符级 | 字符的初始向量         | 字符 → 词 → 更高结构      |
| 字节级 | 字节的初始向量         | 字节 → 字符 → 词 → 更高结构 |

“更细粒度”并不必然让模型无法理解，只是把更多组合工作交给网络，同时拉长序列。

## 6. 粒度是一组联动的工程权衡

Day 003 已推导过 Embedding 参数量：

$$
P\_{\text{emb}}=|V|d
$$

把它与序列长度 `n` 一起看：

* 粒度更粗，`n` 往往较小，但 `|V|`、长尾稀疏和 OOV 风险上升；
* 粒度更细，基础覆盖通常更稳，但 `n` 较大，模型还要完成更多局部组合；
* 标准全注意力的关系矩阵随 `n` 近似按 `O(n²)` 增长。

若同一文本的子词长度为 128、字节长度为 384，只看注意力分数元素数，比例是：

$$
\left(\frac{384}{128}\right)^2=9
$$

这不表示端到端延迟一定变为 9 倍，因为还受线性层、内存带宽、批处理、下采样和优化内核影响；它只说明细粒度覆盖并非免费。

| 粒度   | 常见优势              | 典型失败方式                 |
| ---- | ----------------- | ---------------------- |
| 词级   | 序列短、容易解释          | 新词变 `<UNK>`，形态丰富语言词表膨胀 |
| 子词级  | 覆盖与长度较均衡，可共享片段    | 低资源语言被过度切碎，边界符号被误读     |
| 字符级  | 拼写与噪声变化直接         | 序列长，有限字符表仍可能 OOV       |
| 纯字节级 | 完整覆盖 UTF-8，基础词表固定 | 多字节字符被拆开，计算与解释成本上升     |

ByT5 等研究表明字节级模型可能在噪声、拼写或发音敏感任务上占优；这不能推出它在吞吐、延迟或所有任务上都更好。

## 7. 怎样为任务选择粒度

没有一种粒度对所有任务都最优。可以按下面顺序思考。

### 7.1 先问是否必须兼容预训练模型

如果使用现成 BERT、T5 或 LLM，通常必须使用随 checkpoint 发布的 Tokenizer。不能因为自己的任务更适合字符级，就直接把字符 ID 送入原模型；Embedding 行与 ID 语义完全不匹配。

此时更现实的选择是：

* 保持原 Tokenizer；
* 调整预处理和最大长度；
* 必要时少量扩词表并继续训练；
* 或选择一个原本就采用更合适 Tokenizer 的 checkpoint。

### 7.2 再看任务依赖什么信息

| 任务/场景        | 重要信息       | 常见考虑                              |
| ------------ | ---------- | --------------------------------- |
| 通用 LLM       | 语义、覆盖、生成效率 | 子词或 byte-level BPE 常见             |
| 拼写纠错、发音、噪声鲁棒 | 精细字符/字节变化  | 字符或字节可能有优势                        |
| 中文分词、词性标注    | 词边界本身      | 字符/子词输入 + 序列标注较自然                 |
| NER、抽取式问答    | 原文位置对齐     | 需要 offset mapping，粒度越细对齐越直接但序列更长  |
| 搜索与关键词系统     | 词和短语匹配     | 可能同时保留词级索引与字符 n-gram              |
| 低资源或多语言      | 词表公平覆盖     | 检查不同语言的 fertility 和 byte fallback |
| 代码模型         | 空白、符号、标识符  | 必须保留格式，避免普通自然语言规则破坏代码             |

### 7.3 用数据而不是直觉评估

至少测量：

1. **fertility**：每个原始词平均被切成多少 Token；
2. 每字符或每字节 Token 数；
3. unknown/byte fallback 比例；
4. 截断率和可容纳的原文长度；
5. 不同语言、领域和群体之间的差异；
6. 下游任务指标；
7. 训练/推理吞吐量、显存与延迟。

如果一个多语言 Tokenizer 对英文平均每词 1.2 个 Token，对目标语言平均每词 5.8 个 Token，那么同样的上下文窗口和计费方式会对目标语言更不利。只看总体平均值可能掩盖这个问题。

## 8. 必须带着具体 Tokenizer 理解边界

1. **Token 不等于显示字符串。** 它可能携带前导空格、字节代理符号、规范化结果或控制语义；恢复文本应调用同一 Tokenizer 的 `decode()`。
2. **字符级不保证无 OOV。** 有限 Unicode 字符表仍可能遗漏罕见码点。
3. **一个汉字不固定对应一个 Token。** 它可能是字符 Token、子词的一部分、三个纯字节 Token，或 `[UNK]`。
4. **粒度不是强弱排名。** 更细通常提高覆盖却拉长序列；更粗可能缩短序列却扩大词表和长尾。
5. **不能随意替换预训练模型的 Tokenizer。** Token ID 与 Embedding 行构成契约。
6. **Token 数不等于模型能力。** 它衡量表示效率与部分成本，质量还受数据、架构和训练目标影响。

## 9. 面试准备与答案

### 9.1 30 秒回答

> 词级序列通常较短，但词表大、依赖语言分词且 OOV 明显；字符级和纯字节级覆盖更稳，却拉长序列，并把更多组合工作交给模型；子词级从语料学习常见片段，在词表大小、序列长度和覆盖率之间折中。Byte-level BPE 不等于纯字节模型。实际选择时先保证 checkpoint 兼容，再分语言和领域测 fertility、覆盖、截断、质量、吞吐与延迟。

### 9.2 递进面试题

#### 问题 1：四种粒度的关键区别是什么？

<details>

<summary>查看答案</summary>

区别是 Embedding 层直接查表的单位：词、学得的子词片段、通常意义上的码点，或 0–255 的 UTF-8 字节。单位越细，基础覆盖通常越稳，但序列更长，模型需要从更低层组合语义。

</details>

#### 问题 2：为什么词级不等于按空格切分，子词也不等于词素？

<details>

<summary>查看答案</summary>

空格只为部分语言提供线索；英文仍有缩写、连字符和标点，中文等语言还需词典、规则或统计模型。子词由训练语料和算法目标决定，可能对应词素，也可能只是高频相邻字符串。

</details>

#### 问题 3：纯字节、byte-level BPE 与 byte fallback 如何区分？

<details>

<summary>查看答案</summary>

纯字节模型始终逐字节输入；byte-level BPE 从字节起步，再合并高频序列；byte fallback 常规使用普通子词，只在无法覆盖时退回字节 Token。

</details>

#### 问题 4：序列长度扩大 3 倍，标准注意力关系规模如何变化？

<details>

<summary>查看答案</summary>

从 `n×n` 变为 `3n×3n`，元素数约扩大 9 倍。这不是端到端延迟必然扩大 9 倍，但揭示了细粒度表示的主要成本。

</details>

#### 问题 5：怎样评估目标语言的切分质量？

<details>

<summary>查看答案</summary>

分语言和领域报告 fertility、每字符/字节 Token 数、UNK/fallback、截断率、有效原文长度、下游质量、延迟、吞吐和显存，不能只看总体平均 Token 数。

</details>

### 9.3 手写题：带 offset 的最长匹配

实现 greedy longest-match：每个位置选词表中最长片段，无匹配时退回单字符，返回 Token 与 `[start, end)`；用 `{"自然", "自然语言", "语言", "处理"}` 编码“自然语言处理”。

<details>

<summary>查看参考答案</summary>

```python
def longest_match_with_offsets(text: str, vocab: set[str]):
    result = []
    start = 0
    max_len = max(map(len, vocab))
    while start < len(text):
        for width in range(min(max_len, len(text) - start), 0, -1):
            token = text[start : start + width]
            if token in vocab:
                end = start + width
                result.append((token, (start, end)))
                start = end
                break
        else:
            end = start + 1
            result.append((text[start:end], (start, end)))
            start = end
    return result
```

输出是 `[('自然语言', (0, 4)), ('处理', (4, 6))]`。它是给定词表上的贪心编码，不是 BPE 训练或 merge-rank 编码。

</details>

### 9.4 项目答辩题：中文医疗问答 Tokenizer

英文中心 LLM 在中文医疗文本上平均每字符 2.3 个 Token，大量术语被切成单字符或字节。是否应直接扩充词表？

<details>

<summary>查看答题要点</summary>

不能只凭 2.3 直接扩词表。先检查 normalizer、pre-tokenizer、fallback 和统计口径；再按通用中文、医疗术语、罕见字符等切片测 fertility、每字符 Token 数、fallback/UNK、截断率与上下文有效长度。若确由领域覆盖导致，可从清洗去重的医疗语料产生候选片段；用旧切分向量聚合初始化新 Embedding，并继续训练。最终对照医疗问答质量、原通用能力、延迟、吞吐、显存和上下文长度，并定义回滚标准。不能只替换 Tokenizer 文件而不调整权重。

</details>

## 10. 权威资料与延伸阅读

1. [Unicode Standard Annex #29：Unicode Text Segmentation](https://unicode.org/reports/tr29/)\
   字素簇、词和句子默认边界的 Unicode 官方规范，以及中文等语言需要定制边界机制的说明。
2. [Speech and Language Processing, Chapter 2: Words and Tokens](https://web.stanford.edu/~jurafsky/slp3/2.pdf)\
   词、词素、Unicode、规则分词和子词分词的系统教材章节。
3. [Neural Machine Translation of Rare Words with Subword Units](https://aclanthology.org/P16-1162/)\
   将 BPE 用于开放词汇神经机器翻译、缓解稀有词问题的经典论文。
4. [SentencePiece](https://aclanthology.org/D18-2012/)\
   直接从原始句子训练语言无关子词模型，以及把空格纳入表示的方法。
5. [ByT5: Towards a Token-Free Future with Pre-trained Byte-to-Byte Models](https://aclanthology.org/2022.tacl-1.17/)\
   纯字节 Transformer 的覆盖、鲁棒性、训练计算和推理速度权衡。
6. [CANINE: Pre-training an Efficient Tokenization-Free Encoder](https://aclanthology.org/2022.tacl-1.5/)\
   直接处理字符并通过下采样控制序列长度的预训练编码器。
7. [Hugging Face Tokenizers：Models](https://huggingface.co/docs/tokenizers/main/api/models)\
   BPE、WordPiece、Unigram 和 WordLevel 模型的官方接口与编码行为。
8. [Hugging Face Tokenizers：Pre-tokenizers](https://huggingface.co/docs/tokenizers/main/api/pre-tokenizers)\
   空白、字节级和其他预切分组件如何先产生粗粒度片段。

## 一页回顾

```
词级：
整词 → ID
序列短、可解释，但词表大、依赖分词、OOV 明显

子词级：
常见字符串片段 → ID
在覆盖、词表大小和序列长度之间折中
子词不保证等于词素

字符级：
通常是 Unicode 码点 → ID
保留拼写细节，但“字符”需明确定义，序列更长

纯字节级：
UTF-8 的 0–255 字节 → ID
覆盖任意合法 UTF-8，基础集合固定，但序列通常最长

不要混淆：
纯字节 ≠ byte-level BPE ≠ byte fallback

两类主要成本：
Embedding 参数量 = |V| × d
标准注意力关系规模 ≈ n²

选择顺序：
先保证 checkpoint 与 Tokenizer 兼容
→ 再看语言与任务需要
→ 测 fertility、UNK/回退、截断、效果、延迟和吞吐

下一步：
Day 5 手工追踪 BPE 怎样从字符/字节基础单位
一步步学出子词合并规则
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://inwt233.gitbook.io/ai-learning/di-yi-bu-fen-nlp-ji-chu/week-01/day-004.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
