> 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-001.md).

# Day 001：Unicode、字符、字节和 Token

{% hint style="info" %}
**一段人类可读的文字，如何一步步变成语言模型能够计算的数字？**
{% endhint %}

## 学习目标

完成这部分学习后，你应该能够：

1. 严格区分字符、字形、码点、字节、字素簇、Token 和 Token ID。
2. 解释为什么 Python 的 `len(text)`、UTF-8 字节数、Token 数量可能完全不同。
3. 说清 Tokenizer 内部至少经历哪些步骤。
4. 看懂 `input_ids`、`attention_mask`、`token_type_ids` 和 `offset_mapping`。
5. 用矩阵索引解释 Token ID 如何变成 Embedding。

***

## 1. 完整数据流

文字经过以下步骤转化为token：

```
人看到的文字
    ↓
Unicode 码点序列
    ↓  encode("utf-8")
UTF-8 字节序列
    ↓  Tokenizer 的规范化、预切分、子词模型与后处理
Token 序列
    ↓  查询词表 Vocabulary
Token ID 序列
    ↓  查询 Embedding 矩阵
向量序列
    ↓
Transformer
```

首先需要建立的认识是：**同一段文本可以同时拥有多种“长度”**。

* 人眼可能认为它有 1 个字符；
* Unicode 层可能由多个码点组成；
* UTF-8 层可能占用更多字节；
* Tokenizer 又可能把它切成不同数量的 Token。

这些数字并不矛盾，因为它们统计的是不同层次的单位。

***

## 2. 第一层：人看到的“字符”并不是一个严格单位

日常语言里的“字符”很模糊，它至少可能指三件不同的东西。

### 2.1 抽象字符（abstract character）

抽象字符是文本信息中的语义单位，例如英文字母 `A`、汉字 `学`。它讨论的是“这个文本元素是什么”，而不是屏幕上具体画成什么样。

Unicode 官方术语表把字符描述为书写语言中具有语义价值的最小成分。这里的字符是抽象概念，不等同于某种字体绘制出来的图形。[Unicode Glossary](https://unicode.org/glossary/)

### 2.2 字形（glyph）

字形是字体真正画到屏幕上的形状。同一个抽象字符在宋体、黑体和手写体中可能有不同字形。

因此：

```
字符 ≠ 字形
```

字符属于文本数据层；字形属于渲染层。

### 2.3 字素簇（grapheme cluster）

字素簇近似表示“用户认为的一个字符”。一个字素簇可能由多个 Unicode 码点共同组成。

例如：

```
é
```

它可以表示为一个码点 `U+00E9`，也可以表示为两个码点：

```
e           U+0065
◌́          U+0301  COMBINING ACUTE ACCENT
```

人眼通常把第二种表示也看成一个字符，但程序看到的是两个码点。Unicode Annex #29 专门定义了字素簇边界，因为光标移动、退格、选中文本等操作需要尽量符合用户感知。[Unicode Text Segmentation](https://unicode.org/reports/tr29/)

家庭 emoji 是更明显的例子：

```
👨‍👩‍👧‍👦
```

它通常显示为一个图形，但底层包含 7 个码点：4 个 emoji 加 3 个零宽连接符 `U+200D`。

{% hint style="warning" %}
不能笼统地说“一个字符就是一个 Unicode 编码”。用户感知字符、抽象字符、码点和字形不是同一个概念。
{% endhint %}

***

## 3. 第二层：Unicode 码点是编号，不是字节

### 3.1 Unicode 解决什么问题

计算机最终只能处理数字。早期不同语言和系统使用不同字符集，同一个数字在不同编码中可能表示不同字符。Unicode 的核心目标是给全球文本建立统一的编码空间。

Unicode 码空间从 `U+0000` 到 `U+10FFFF`，一共有 1,114,112 个可能位置；并不是每个位置都已经分配给字符。[Unicode 17 Core Specification](https://www.unicode.org/versions/Unicode17.0.0/core-spec/)

### 3.2 什么是码点（code point）

码点是 Unicode 码空间中的一个整数位置，通常写成：

```
U+十六进制数字
```

例如：

| 文本   | 码点        |   十进制值 |
| ---- | --------- | -----: |
| `A`  | `U+0041`  |     65 |
| `学`  | `U+5B66`  |  23398 |
| `🙂` | `U+1F642` | 128578 |

Python 中可以用 `ord()` 得到码点整数，用 `chr()` 反向得到字符：

```python
print(ord("学"))       # 23398
print(f"U+{ord('学'):04X}")  # U+5B66
print(chr(0x5B66))      # 学
```

### 3.3 码点不等于 Token ID

`U+5B66` 是 Unicode 标准为 `学` 指定的码点；而 Token ID 是某个具体模型词表中的行号。

二者来源不同：

* Unicode 码点由 Unicode 标准定义；
* Token ID 由具体 Tokenizer 的词表定义；
* 更换模型时，Token ID 可能改变；
* Unicode 码点不会因为更换模型而改变。

***

## 4. 第三层：UTF-8 把码点变成字节

### 4.1 为什么有码点还需要编码

码点是抽象整数。文件、网络和磁盘真正保存的是字节，因此需要一种规则把码点转换为字节序列。

UTF-8、UTF-16 和 UTF-32 都是 Unicode 编码形式。这里只关注最常见的 UTF-8。

### 4.2 UTF-8 的基本规则

UTF-8 使用 1–4 个字节表示一个 Unicode 标量值，并与 ASCII 兼容。Unicode 官方 FAQ 和 IETF RFC 3629 都给出了规范定义。[Unicode UTF FAQ](https://unicode.org/faq/utf_bom.html) · [RFC 3629](https://www.rfc-editor.org/rfc/rfc3629)

| 码点范围                 | 字节数 | 位模式                                   |
| -------------------- | --: | ------------------------------------- |
| `U+0000`–`U+007F`    |   1 | `0xxxxxxx`                            |
| `U+0080`–`U+07FF`    |   2 | `110xxxxx 10xxxxxx`                   |
| `U+0800`–`U+FFFF`    |   3 | `1110xxxx 10xxxxxx 10xxxxxx`          |
| `U+10000`–`U+10FFFF` |   4 | `11110xxx 10xxxxxx 10xxxxxx 10xxxxxx` |

ASCII 字符只需要 1 字节；常见汉字通常需要 3 字节；许多 emoji 需要 4 字节。

### 4.3 手工编码一次“学”

`学` 的码点是 `U+5B66`，落在三字节范围。

把 `0x5B66` 写成二进制，并按 `4 + 6 + 6` 位分组：

```
0101 | 101101 | 100110
```

填入三字节模板：

```
1110xxxx  10xxxxxx  10xxxxxx
11100101  10101101  10100110
   E5        AD        A6
```

所以：

```
学 → UTF-8: E5 AD A6
```

### 4.4 编码与解码

```python
text = "学习"

data = text.encode("utf-8")
print(data)                 # b'\xe5\xad\xa6\xe4\xb9\xa0'
print(data.hex(" "))       # e5 ad a6 e4 b9 a0

restored = data.decode("utf-8")
print(restored)             # 学习
```

需要记住方向：

```
str --encode--> bytes
bytes --decode--> str
```

在 Python 3 中，`str` 表示 Unicode 文本，`bytes` 表示原始字节。Python 官方 Unicode HOWTO 也用这种方式区分文本与编码后的数据。[Python Unicode HOWTO](https://docs.python.org/3/howto/unicode.html)

***

## 5. 实验一：同一段文本到底有多“长”

```python
import unicodedata as ud

samples = [
    "AI学习🙂",
    "é",
    "e\u0301",
    "👨‍👩‍👧‍👦",
]

for text in samples:
    print("文本：", repr(text))
    print("Python len：", len(text))
    print("码点：", [f"U+{ord(ch):04X}" for ch in text])
    print("UTF-8 字节数：", len(text.encode("utf-8")))
    print("UTF-8：", text.encode("utf-8").hex(" "))
    print()

composed = "é"
decomposed = "e\u0301"

print("原始字符串相等：", composed == decomposed)
print(
    "NFC 后相等：",
    ud.normalize("NFC", composed) == ud.normalize("NFC", decomposed),
)
```

### 输出

```
文本： 'AI学习🙂'
Python len： 5
码点： ['U+0041', 'U+0049', 'U+5B66', 'U+4E60', 'U+1F642']
UTF-8 字节数： 12
UTF-8： 41 49 e5 ad a6 e4 b9 a0 f0 9f 99 82

文本： 'é'
Python len： 1
码点： ['U+00E9']
UTF-8 字节数： 2
UTF-8： c3 a9

文本： 'é'
Python len： 2
码点： ['U+0065', 'U+0301']
UTF-8 字节数： 3
UTF-8： 65 cc 81

文本： '👨\u200d👩\u200d👧\u200d👦'
Python len： 7
码点： ['U+1F468', 'U+200D', 'U+1F469', 'U+200D', 'U+1F467', 'U+200D', 'U+1F466']
UTF-8 字节数： 25
UTF-8： f0 9f 91 a8 e2 80 8d f0 9f 91 a9 e2 80 8d f0 9f 91 a7 e2 80 8d f0 9f 91 a6

原始字符串相等： False
NFC 后相等： True
```

从结果中可以看到：Python 的 `len(str)` 统计码点数量，不是 UTF-8 字节数，也不保证等于用户感知字符数。

两个 `é` 看起来相同，但原始字符串并不相等；转换为 NFC 后相等。Unicode Annex #15 定义了 NFC、NFD、NFKC 和 NFKD 四种规范化形式。[Unicode Normalization Forms](https://unicode.org/reports/tr15/)

规范化会在 Day 002 深入学习。现在只需要记住：**视觉相同不保证底层码点序列相同。**

***

## 6. 第四层：Tokenizer 决定模型看到什么单位

### 6.1 为什么不能直接把码点送进模型

理论上可以按字符甚至字节建模，但工程上需要权衡：

* 词级 Token 语义完整，但词表巨大，遇到新词容易 OOV；
* 字符级 Token 词表较小，但序列更长；
* 子词 Token 在词表大小、序列长度和未知词处理之间取得折中；
* 字节级方法覆盖任意输入，但可能进一步拉长序列。

因此现代 NLP 模型通常使用子词或字节相关的 Tokenizer。BERT 使用 WordPiece；SentencePiece 则提供可直接从原始句子训练的语言无关子词方案。[BERT](https://aclanthology.org/N19-1423/) · [SentencePiece](https://aclanthology.org/D18-2012/)

### 6.2 Tokenizer 不是简单的 `split()`

一个完整 Tokenizer 通常是流水线。Hugging Face 官方 Tokenizers 文档把它拆成以下主要部分：[Tokenizer Pipeline](https://huggingface.co/docs/tokenizers/main/api/tokenizer)

1. **Normalizer**：大小写转换、Unicode 规范化、去重音符号等。
2. **PreTokenizer**：先按空白、标点或语言规则做初步切分。
3. **Model**：使用 WordPiece、BPE、Unigram 等算法继续切成子词，并映射到 ID。
4. **PostProcessor**：添加 `[CLS]`、`[SEP]` 等特殊 Token。
5. **Decoder**：把 Token ID 尽可能还原为文本。

所以：

```
Tokenizer ≠ 分词算法
```

分词算法只是 Tokenizer 流水线中的一个部分。

### 6.3 Token、词表和 Token ID

假设词表是：

```
ID 0  → [PAD]
ID 1  → [UNK]
ID 2  → 学
ID 3  → 习
ID 4  → learning
```

那么 Token `学` 的 ID 是 2。这个数字只表示“词表第 2 行”，它本身不表示相似度、频率或语义大小。

{% hint style="warning" %}
不能因为 Token ID 是 5000，就认为它比 ID 为 100 的 Token“更重要”或“语义更大”。ID 只是离散索引。
{% endhint %}

***

## 7. 实验二：追踪真实 Tokenizer 的输出

安装依赖：

```bash
pip install transformers
```

运行：

```python
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("google-bert/bert-base-chinese")
text = "AI学习🙂"

encoded = tokenizer(text, return_offsets_mapping=True)
tokens = tokenizer.convert_ids_to_tokens(encoded["input_ids"])

print("原始文本：", text)
print("Tokens：", tokens)
print("input_ids：", encoded["input_ids"])
print("attention_mask：", encoded["attention_mask"])
print("token_type_ids：", encoded["token_type_ids"])
print("offset_mapping：", encoded["offset_mapping"])
```

### 输出

```
原始文本： AI学习🙂
Tokens： ['[CLS]', '[UNK]', '学', '习', '[UNK]', '[SEP]']
input_ids： [101, 100, 2110, 739, 100, 102]
attention_mask： [1, 1, 1, 1, 1, 1]
token_type_ids： [0, 0, 0, 0, 0, 0]
offset_mapping： [(0, 0), (0, 2), (2, 3), (3, 4), (4, 5), (0, 0)]
```

这里使用的是 `bert-base-chinese` 当前公开词表与 Tokenizer 配置。模型文件由 Google BERT 团队发布在 Hugging Face Hub。[bert-base-chinese files](https://huggingface.co/google-bert/bert-base-chinese/tree/main)

### 7.1 `[CLS]` 和 `[SEP]`

* `[CLS]` 被放在序列开头；BERT 常使用它的最终表示完成句级任务。
* `[SEP]` 用于标记序列结束或分隔两个序列。
* 它们不是原始文本的一部分，所以 offset 是 `(0, 0)`。

### 7.2 `[UNK]`

`[UNK]` 表示 Tokenizer 无法用当前词表表示这一片段。

在这个例子中：

* `AI` 对应原文区间 `(0, 2)`，得到一个 `[UNK]`；
* `🙂` 对应 `(4, 5)`，得到另一个 `[UNK]`。

需要注意：这不是说 Unicode 无法表示它们，而是说**这个具体 Tokenizer 的词表和规则没有找到合适 Token**。

### 7.3 `input_ids`

`input_ids` 是模型真正接收的离散编号：

```
[CLS] → 101
[UNK] → 100
学    → 2110
习    → 739
[SEP] → 102
```

这些 ID 只对当前词表有意义。换一个 Tokenizer，同样的数字可能对应完全不同的 Token。

### 7.4 `attention_mask`

`attention_mask` 告诉模型哪些位置是真实输入，哪些位置是 padding。

* `1`：正常参与计算；
* `0`：通常表示补齐位置，应被屏蔽。

当前序列没有 padding，所以全是 1。

### 7.5 `token_type_ids`

BERT 可以同时接收句子 A 和句子 B。`token_type_ids` 用 0/1 区分 Token 属于哪个句子。这里只输入了一个句子，所以全部是 0。

### 7.6 `offset_mapping`

offset 把 Token 映射回原始字符串区间，使用左闭右开形式 `[start, end)`。

例如 `(2, 3)` 对应：

```python
text[2:3] == "学"
```

它对命名实体识别、问答系统定位答案、给文本打标签等任务很重要。

***

## 8. Token ID 如何变成向量

神经网络不能直接对离散整数 ID 做语义计算。模型维护一个可训练的 Embedding 矩阵：

```
E ∈ R^(|V| × d)
```

* `|V|` 是词表大小；
* `d` 是向量维度；
* `E[i]` 是 Token ID `i` 对应的向量。

如果输入是：

```
input_ids = [101, 100, 2110, 739, 100, 102]
```

查表后就是：

```
X = E[input_ids]
```

如果取 `d = 768`，形状变化为：

```
[6] → [6, 768]
```

批量输入时：

```
[batch_size, sequence_length]
    ↓ Embedding lookup
[batch_size, sequence_length, hidden_size]
```

BERT 的初始输入表示不只包含 Token Embedding，还会加入位置与句段信息：

```
x_j = token_embedding[id_j]
    + position_embedding[j]
    + segment_embedding[type_j]
```

BERT 论文将输入表示定义为 Token、Segment 与 Position Embedding 之和；原始 Transformer 论文则明确使用可学习 Embedding 把输入 Token 转成 `d_model` 维向量。[BERT paper](https://aclanthology.org/N19-1423/) · [Attention Is All You Need](https://proceedings.neurips.cc/paper_files/paper/2017/hash/3f5ee243547dee91fbd053c1c4a845aa-Abstract.html)

### 一个关键区别

Embedding 查表得到的只是进入 Transformer 之前的初始向量。同一个 Token ID 在这一层查到相同 Token Embedding，但经过 Transformer 后，它在不同上下文中的隐藏状态可以不同。

因此不能把下面两者混为一谈：

* **Token Embedding**：按 ID 查表得到的初始向量；
* **Contextual Representation**：经过多层 Transformer 后、包含上下文信息的向量。

***

## 9. 把整个过程追踪一遍

以 `AI学习🙂` 为例：

### 文本与 Unicode 层

```
A   → U+0041
I   → U+0049
学  → U+5B66
习  → U+4E60
🙂  → U+1F642
```

Python `len` 为 5。

### UTF-8 层

```
41 49 e5 ad a6 e4 b9 a0 f0 9f 99 82
```

共 12 字节。

### Tokenizer 层

```
['[CLS]', '[UNK]', '学', '习', '[UNK]', '[SEP]']
```

共 6 个 Token，包括两个特殊 Token。

### 词表层

```
[101, 100, 2110, 739, 100, 102]
```

共 6 个 Token ID。

### Embedding 层

假设隐藏维度为 768：

```
[6] → [6, 768]
```

这就是对“文本如何变成模型能够计算的数字”这个问题的完整回答。

***

## 10. 需要避免的常见误区

### 误区 1：Unicode 就是 UTF-8

错误。Unicode 定义字符与码点；UTF-8 定义码点如何表示为字节。

### 误区 2：一个字符固定占一个字节

错误。UTF-8 使用 1–4 字节；常见汉字通常是 3 字节。

### 误区 3：Python `len()` 得到用户看见的字符数

错误。Python `str` 的 `len()` 统计码点数量；一个字素簇可能包含多个码点。

### 误区 4：Token 就是一个单词

错误。Token 可能是词、子词、汉字、标点、字节片段或特殊符号。

### 误区 5：Token ID 是 Unicode 编码

错误。Token ID 是具体词表中的索引。

### 误区 6：Tokenizer 只是切分文本

错误。它通常还包括规范化、预切分、ID 映射、特殊 Token、截断和 padding 等步骤。

### 误区 7：同一文本在所有模型中 Token 数相同

错误。Tokenizer 的算法、训练语料、规范化规则和词表都可能不同。

### 误区 8：Token ID 本身带有语义

错误。ID 只是索引，语义来自训练得到的 Embedding 和后续网络参数。

***

## 11. 自测题

### 问题 1

`学` 的 Unicode 码点是 `U+5B66`，它在 UTF-8 中占几个字节？

### 问题 2

视觉上相同的两个字符串是否一定拥有相同码点序列？

### 问题 3

为什么 `AI学习🙂` 的 Python 长度是 5，UTF-8 字节数是 12，BERT 输入 Token 数却是 6？

### 问题 4

如果两个模型都输出 Token ID `100`，这个 ID 是否一定表示同一个 Token？

### 问题 5

`attention_mask` 中的 0 通常表示什么？

<details>

<summary>查看答案</summary>

1. 3 字节，对应 `E5 AD A6`。
2. 不一定。预组字符和“基础字符 + 组合符号”可能视觉相同但码点不同；规范化可以处理某些等价情况。
3. 三个数字分别统计码点、UTF-8 字节和经过 Tokenizer 后包含特殊 Token 的模型单位。
4. 不一定。Token ID 只在具体词表内部有意义。
5. 通常表示 padding 位置，不应作为真实输入参与注意力计算。

</details>

***

## 12. 参考资料

### Unicode 与文本编码标准

1. [The Unicode Standard, Version 17.0](https://www.unicode.org/versions/Unicode17.0.0/)：Unicode 标准组成、码空间与权威核心规范。
2. [Unicode Glossary](https://unicode.org/glossary/)：character、code point、code unit、character encoding form 等严格术语。
3. [Unicode Standard Annex #29: Text Segmentation](https://unicode.org/reports/tr29/)：字素簇、单词与句子边界。
4. [Unicode Standard Annex #15: Normalization Forms](https://unicode.org/reports/tr15/)：NFC、NFD、NFKC、NFKD 与规范等价。
5. [Unicode UTF-8, UTF-16, UTF-32 & BOM FAQ](https://unicode.org/faq/utf_bom.html)：Unicode 编码形式与 UTF-8 官方解释。
6. [RFC 3629: UTF-8](https://www.rfc-editor.org/rfc/rfc3629)：互联网标准中的 UTF-8 定义与合法字节序列。
7. [Python Unicode HOWTO](https://docs.python.org/3/howto/unicode.html)：Python `str`、码点、编码与解码。

### Tokenizer 与模型输入

8. [Hugging Face Tokenizer API](https://huggingface.co/docs/tokenizers/main/api/tokenizer)：Normalizer、PreTokenizer、Model 与 PostProcessor 流水线。
9. [Hugging Face Transformers Tokenizer](https://huggingface.co/docs/transformers/main_classes/tokenizer)：`input_ids`、特殊 Token、padding、truncation 与 attention mask。
10. [BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding](https://aclanthology.org/N19-1423/)：WordPiece 与 BERT 的输入表示。
11. [SentencePiece](https://aclanthology.org/D18-2012/)：直接从原始句子训练语言无关子词模型。
12. [Attention Is All You Need](https://proceedings.neurips.cc/paper_files/paper/2017/hash/3f5ee243547dee91fbd053c1c4a845aa-Abstract.html)：Token Embedding 与 Transformer 输入表示的基础来源。
13. [google-bert/bert-base-chinese](https://huggingface.co/google-bert/bert-base-chinese/tree/main)：实验所用模型的公开 Tokenizer、词表和配置文件。

***


---

# 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-001.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.
