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

# Day 002：文本规范化与中英文预处理

{% hint style="info" %}
**今天要解决的问题：** 同一句话可能因为组合字符、全角字符、大小写、空白和换行方式不同，在程序中变成不同的字符串。应该怎样减少无意义差异，同时避免把有用信息删掉？
{% endhint %}

## 学习目标

完成今天的学习后，你应该能够：

1. 解释文本规范化和“文本清洗”的区别；
2. 区分 NFC、NFD、NFKC、NFKD；
3. 说明英文小写化、`casefold`、词形还原分别解决什么问题；
4. 说明中文预处理为什么不能照搬英文流程；
5. 根据任务和模型选择预处理操作，而不是套用固定模板；
6. 用 Python 写出可检查、可复现、尽量不破坏原文的预处理流水线。

## 1. 先建立正确目标：预处理不是“把文本洗干净”

文本预处理的目标可以写成一句话：

> 消除与当前任务无关的表面差异，保留完成任务所需要的信息。

这句话包含两个方向：

* **统一差异：** 例如 Windows 与 Linux 换行、组合字符与预组合字符；
* **保护信息：** 例如情感分析中的感叹号、命名实体中的大小写、代码中的空格。

假设有两个输入：

```
ＡＩ学习！
AI学习!
```

在搜索关键词的任务里，我们可能希望它们匹配；但在字符纠错或排版检测任务里，全角与半角差异就是要预测的信息。不存在脱离任务的“最正确清洗结果”。

### 1.1 三类操作

| 类型    | 例子                   | 默认态度          |
| ----- | -------------------- | ------------- |
| 表示层统一 | 正确解码、统一换行、NFC        | 通常较安全，但仍要记录规则 |
| 匹配层折叠 | `casefold`、NFKC、压缩空白 | 按任务决定，可能丢信息   |
| 语言学处理 | 分词、词形还原、去停用词         | 强烈依赖语言、任务和模型  |

“删除标点、删除数字、全部小写、去停用词、词干提取”不是通用必选项。

## 2. 一条工程上更稳妥的数据流

```
原始字节
  ↓ 使用已知编码解码；错误要被记录
原始 Unicode 文本（保留副本）
  ↓ 统一换行等确定性格式
基础规范化文本（通常考虑 NFC）
  ↓ 任务特定变换：大小写、NFKC、空白、HTML 等
模型输入文本
  ↓ 与模型配套的 Tokenizer
Token、Token ID、offset mapping
```

推荐同时保存：

* `raw_text`：未经清洗的原文；
* `model_text`：真正送入模型的文本；
* `rules/version`：使用了哪些规则和版本；
* 必要时保存字符位置映射。

这样做的原因是：清洗通常不可逆。只保存清洗结果，会让错误分析和结果回溯非常困难。

## 3. Unicode 规范化：四种 Normalization Form

Day 1 已经看到：

```python
"é" != "e\u0301"
```

两者视觉上相同，但码点序列不同。Unicode 规范化把等价表示转换到约定形式。

### 3.1 两种等价关系

**规范等价（canonical equivalence）**：不同码点序列表示同一个抽象文本。

```
é             U+00E9
e + ◌́        U+0065 U+0301
```

**兼容等价（compatibility equivalence）**：字符在某些使用场景中可以视为相同，但外观、排版或语义区别可能被抹去。

```
Ａ 与 A       全角和半角
① 与 1       带圈数字和普通数字
ﬁ 与 fi      连字和两个字母
```

### 3.2 分解与组合

* **分解（D）**：尽量拆成基础字符与组合标记；
* **组合（C）**：分解并排序后，在允许时重新合成预组合字符。

四种形式由两个维度组成：

| 形式   | 等价范围 | 输出方式 | 直观理解                |
| ---- | ---- | ---- | ------------------- |
| NFC  | 规范等价 | 组合   | 保持语义和外观，尽量使用组合字符    |
| NFD  | 规范等价 | 分解   | 保持语义和外观，拆成基础字符与组合标记 |
| NFKC | 兼容等价 | 组合   | 额外折叠全角、连字、带圈数字等差异   |
| NFKD | 兼容等价 | 分解   | 兼容折叠后再保留分解形式        |

字母 `K` 表示 compatibility。

### 3.3 实验：NFC 和 NFKC 的边界

```python
import unicodedata

samples = ["é", "e\u0301", "ＡＩ１２３", "①", "ﬁ"]

for text in samples:
    print(
        repr(text),
        "NFC =", repr(unicodedata.normalize("NFC", text)),
        "NFKC =", repr(unicodedata.normalize("NFKC", text)),
    )
```

运行结果：

```
'é' NFC = 'é' NFKC = 'é'
'é' NFC = 'é' NFKC = 'é'
'ＡＩ１２３' NFC = 'ＡＩ１２３' NFKC = 'AI123'
'①' NFC = '①' NFKC = '1'
'ﬁ' NFC = 'ﬁ' NFKC = 'fi'
```

观察：

* NFC 统一了 `é` 的两种规范等价写法；
* NFC 没有折叠全角字符、带圈数字和连字；
* NFKC 折叠了这些兼容字符，因此更激进，也更可能丢失信息。

### 3.4 应该选择 NFC 还是 NFKC？

**NFC 常适合作为基础层候选**，因为它主要统一规范等价表示，同时尽量保留文本区别。

**NFKC 适合明确需要宽松匹配的派生字段**，例如某些搜索索引、去重键或受限标识符；不要不加判断地覆盖原文。

一个实用设计是双字段：

```python
raw_text = input_text
display_text = unicodedata.normalize("NFC", raw_text)
search_key = unicodedata.normalize("NFKC", raw_text).casefold()
```

`display_text` 用于展示与模型输入候选，`search_key` 只服务于宽松匹配。是否这样设计仍由具体任务决定。

{% hint style="warning" %}
NFKC/NFKD 会改变字符结构并抹去兼容差异。Unicode 与 W3C 都提醒不能把兼容规范化盲目用于任意文本。
{% endhint %}

## 4. 空白、换行与不可见字符

文本里常见的空白不只有普通空格：

* 空格 `U+0020`；
* 制表符 `\t`；
* 换行 `\n`；
* 回车 `\r`；
* 不换行空格 `U+00A0`；
* 零宽字符和语言相关分隔符。

### 4.1 统一换行

常见换行形式：

```
Windows: \r\n
Unix/Linux: \n
旧式 Mac: \r
```

可以显式统一为 `\n`：

```python
def normalize_newlines(text: str) -> str:
    return text.replace("\r\n", "\n").replace("\r", "\n")
```

顺序很重要：先替换 `\r\n`，否则一次 Windows 换行可能变成两个换行。

### 4.2 压缩空白不一定安全

```python
" ".join(text.split())
```

这行代码很方便，但会把换行、制表符和连续空格都折叠成普通空格。它可能破坏：

* Python、YAML 等对缩进敏感的代码；
* Markdown 表格与段落；
* 地址、诗歌和对话排版；
* 文档布局信息；
* 字符级标注的 offset。

更可控的做法是按行处理，明确是否保留换行：

```python
def collapse_inline_whitespace(text: str) -> str:
    return "\n".join(" ".join(line.split()) for line in text.split("\n"))
```

### 4.3 不要批量删除所有不可见字符

“看不见”不代表“无意义”。例如零宽连接符参与 emoji 序列，某些脚本也需要特殊连接或方向控制字符。应针对已确认的问题字符制定规则，而不是删除所有 Unicode 类别为格式字符的码点。

## 5. 英文预处理：大小写只是其中一步

### 5.1 `lower()` 与 `casefold()`

`lower()` 主要用于产生小写文本；`casefold()` 是更激进的、面向不区分大小写匹配的 Unicode 操作。

```python
text = "Straße"

print(text.lower())
print(text.casefold())
```

输出：

```
straße
strasse
```

如果目标是跨语言的不区分大小写比较，`casefold()` 通常比 `lower()` 更完整；如果目标是保留可读文本，或模型区分大小写，就不应该自动使用它。

### 5.2 大小写可能携带信息

```
US     美国
us     我们（宾格）
Apple  公司名或句首单词
apple  水果
May    月份或人名
may    情态动词
```

在命名实体识别、缩写识别、代码处理和某些检索任务中，大小写是特征。预训练模型还有一个硬约束：

> 使用模型配套的 Tokenizer，并遵循其训练时的规范化方式。

`bert-base-uncased` 与 cased 模型不是“同一个模型加一个开关”。它们的预处理、词表和训练过程不同。

### 5.3 标点、缩写和连字符

英文里的标点可能改变结构或含义：

```
don't       与 dont 不同
U.S.        缩写
state-of-the-art
3.14        小数
user@example.com
```

简单地执行 `re.sub(r"[^a-zA-Z ]", "", text)` 会同时删除撇号、连字符、数字、中文和 emoji，通常过于粗暴。

### 5.4 词干提取与词形还原

**词干提取（stemming）**：使用规则砍掉词缀，结果不一定是真实单词。

```
studies → studi
```

**词形还原（lemmatization）**：结合词汇或词性，将屈折变化映射到词元。

```
studies → study
was → be
```

它们更常用于传统信息检索、词袋模型或需要词汇归并的分析流程。现代预训练 Transformer/LLM 通常直接接收原始或轻度规范化文本；预先做词干提取或词形还原会改变模型训练时见到的输入分布。

## 6. 中文预处理：不要照搬英文清洗模板

### 6.1 中文没有天然的空格分词边界

```
研究生命起源
```

可能涉及不同切分：

```
研究 / 生命 / 起源
研究生 / 命 / 起源
```

“先按空格分词”不适用于中文。中文分词本身就是一个需要上下文的建模问题。

对现代子词 Tokenizer，也不要擅自在前面加入中文分词器。SentencePiece 的设计目标之一就是直接从原始句子训练，不要求预先切成词；BERT 类 Tokenizer 也有自己的中文字符处理和词表。多加一次分词会改变输入形式，未必与模型训练过程一致。

### 6.2 简繁转换不是普通规范化

Unicode 规范化不会把简体和繁体中文统一，因为它们通常是不同码点，也可能涉及词汇和地区用法差异。

```
后 / 後
发 / 發 / 髮
面条 / 麵條
```

繁简转换可能是一对多或依赖上下文，因此应被视为语言转换或任务特定处理，而不是无损清洗。若确实需要转换，应保存原文、说明转换方向与工具版本，并评估实体名称和地区用词。

### 6.3 全角、半角和中文标点

NFKC 可以把许多全角 ASCII 兼容字符转换为半角形式：

```
ＡＩ１２３ → AI123
```

但标点也可能变化，例如全角感叹号 `！` 可变为 `!`。对于搜索匹配这可能有用；对于风格识别、情感分析、排版恢复则可能损失特征。

不要默认删除中文标点：

* `！`、`？` 可能携带情绪；
* `，`、`。` 提供句子边界；
* 引号可以帮助识别直接引语；
* 书名号可能暗示作品名实体。

### 6.4 中英混排

真实中文数据常包含英文、数字、URL、型号和 emoji：

```
我在学习BERT-base，准确率提高了2.5%🙂
```

如果只保留汉字，会丢掉模型名、数值和情绪符号。预处理规则必须覆盖混合文本，而不是假定输入只含一种语言。

## 7. 任务决定预处理策略

这一节的重点不是记住一张“任务—清洗操作”对照表，而是学会自己推出策略。我的判断顺序可以概括成四个问题：

1. **任务最终要预测什么？** 是整段文本的类别、某个字符区间、生成一段新文本，还是判断两段文本是否相同？
2. **完成预测需要哪些原始信息？** 大小写、标点、词序、空格、emoji 和字符位置中，哪些可能成为有效特征？
3. **模型训练时接收了什么形式的输入？** 使用预训练模型时，我的输入应尽量与预训练阶段一致。
4. **变换是否可逆？** 如果变换会删除、合并或拆分字符，我是否保存了原文和位置映射？

因此，预处理策略可以写成下面的因果链：

```
任务输出
  ↓ 决定
必须保留的信息
  ↓ 限制
允许执行的文本变换
  ↓ 再结合
模型与 Tokenizer 的既有规则
  ↓ 得到
最终预处理流水线
```

### 7.1 预训练 Transformer 或 LLM

假设我要用一个已经训练好的 BERT、GPT 或其他大语言模型。这些模型在预训练时已经配套使用了特定 Tokenizer，Tokenizer 内部可能已经进行大小写转换、Unicode 规范化、预切分和特殊 Token 添加。

此时我的目标通常不是重新设计文本表示，而是让实际输入尽量接近模型训练时见过的形式。因此：

* 我通常保留词序、标点、数字、大小写和换行等原文信息；
* 我先修复确定的编码错误、异常换行或数据采集噪声；
* 我使用 checkpoint 配套的 Tokenizer；
* 我不会在 Tokenizer 前重复执行小写化、分词、去重音或词形还原，除非模型文档明确要求。

例如，假设原文是：

```
I LOVE this model!!! 🙂
```

如果我先变成：

```
love model
```

虽然文本“更干净”，但我删除了主语、大小写、程度表达、感叹号和 emoji。模型原本可以利用的信息反而减少了。

主要风险是**输入分布偏移**：模型训练时看到的是自然文本，使用时却收到经过另一套规则改写的文本。

### 7.2 情感分析

情感分析预测的是文本表达的态度或情绪。标点、否定词、重复字符和 emoji 往往就是情感信号：

```
好
好！
好？？？
好耶！！！🙂
```

这些文本包含相同的核心汉字，但语气和情绪强度不同。因此我通常应保留：

* `！`、`？` 等标点及其重复次数；
* `不`、`没`、`从未` 等否定表达；
* emoji 和网络语言；
* 大写、重复字母或重复汉字等强调方式。

我可以考虑 NFC、明确的乱码修复和受控空白处理，但不能机械去标点或去“停用词”。例如把“不喜欢”中的“不”删除后，情感方向会完全反转。

这里的推理是：

```
任务要预测情感
→ 语气和强调是有效特征
→ 标点、emoji、否定词不能被当作噪声删除
```

### 7.3 命名实体识别（NER）

命名实体识别不仅要判断“有什么实体”，还要找出实体在原文中的位置。例如：

```
原文：我在 OpenAI 学习
实体：OpenAI
区间：[3, 9)
```

这个任务同时依赖两类信息：

1. **文本特征**：英文大小写可能帮助识别公司名、产品名和缩写；
2. **字符位置**：模型输出最终必须映射回原文 span。

如果我删除前面的空格、统一大小写或把一个字符拆成多个字符，原标签区间可能失效。即使变换后的文字仍能阅读，训练数据也可能已经错位。

因此 NER 的预处理通常较保守：

* 保存原文；
* 只做必要且经过测试的规范化；
* 使用 `offset_mapping` 或自行维护字符对齐；
* 文本发生长度变化时，同步变换实体标签。

### 7.4 抽取式问答

抽取式问答要求模型从给定文章中选出一段连续文本。训练标签常用答案的起止位置表示：

```
文章：BERT 由 Google 在 2018 年提出。
答案：Google
起止区间：[8, 14)
```

因此它比普通文本分类更依赖原始字符位置。删除空格、HTML 标记或不可见字符后，答案的 offset 都可能移动。

更稳妥的流程是：

1. 保存原始文章和原始答案区间；
2. 尽量少改文本；
3. 必须清理 HTML 等内容时，同时建立原文到处理后文本的映射；
4. Tokenizer 切分后，再把字符区间转换为 Token 区间。

需要注意，**生成式问答**与抽取式问答不同。生成式问答只需生成答案文本，不一定要求答案对应原文的精确 span，所以位置约束通常较弱。

### 7.5 传统词袋分类

词袋模型把文本表示成词频或 TF-IDF 特征。它通常不直接利用完整词序，也没有预训练 Tokenizer 的输入分布需要遵守。

例如：

```
Dogs are running
dog runs
```

如果任务只关心主题，两句话中的 `dogs`、`dog`、`running`、`runs` 可能需要适当归并。因此可以把以下操作作为验证集上的实验变量：

* 英文小写化；
* 词形还原；
* 任务相关的停用词处理；
* 词或字符 n-gram；
* 最低词频阈值。

但这些操作仍不是必然有益。例如在作者识别中，大小写和停用词可能反映写作风格；在情感分类中，否定词不能删除。

这里与预训练模型的主要区别是：**词袋模型的特征是我自己定义的，所以我可以通过验证实验选择归并规则；预训练模型的输入表示已经在预训练阶段固定下来。**

### 7.6 搜索与去重

搜索和去重往往希望“形式略有差异的文本仍能匹配”：

```
ＡＩ学习
AI学习
ai学习
```

这时可以建立一个宽松匹配键：

```python
display_text = unicodedata.normalize("NFC", raw_text)
match_key = unicodedata.normalize("NFKC", raw_text).casefold()
```

但我不应该用 `match_key` 覆盖原文。更合理的是保存两个字段：

* `display_text`：用于展示和回溯，尽量保留作者写法；
* `match_key`：用于召回候选或初步去重，允许更激进的折叠。

NFKC 与 `casefold()` 会产生碰撞，即原本不同的字符串可能得到相同匹配键。例如 `①` 与 `1`、全角 `Ａ` 与半角 `A` 会被合并。因此匹配键适合帮助找到候选，不一定适合独自作出“内容完全相同”的最终判断。

### 7.7 代码模型

代码中的空格、换行、大小写和标点可能属于语法：

```python
if ready:
    run()
```

删除换行和缩进后，Python 程序可能直接失去合法语法。以下差异也不能随意折叠：

```
name != Name
+  != ++
"a b" != "ab"
```

因此代码数据通常只修复已知的编码、文件截断或采集错误，不能套用普通自然语言的去标点、压缩空白和小写化流程。

### 7.8 重新阅读原表格

理解上述推理后，表格可以看成每个场景的结论摘要：

| 场景                  | 任务依赖的关键信息               | 通常可做              | 不能盲目做         | 原因           |
| ------------------- | ----------------------- | ----------------- | ------------- | ------------ |
| 预训练 Transformer/LLM | 自然文本及配套 Tokenizer 的输入形式 | 正确解码、修复明确格式问题     | 自行分词、词干化、去标点  | 会偏离预训练分布     |
| 情感分析                | 语气、否定、强调、emoji          | NFC、受控空白处理        | 去标点、去否定词      | 会丢失情绪方向或强度   |
| 命名实体识别              | 大小写、原始字符、实体位置           | 保守规范化、维护 offset   | 任意增删字符        | 会丢特征或导致标签错位  |
| 抽取式问答               | 答案原文和起止位置               | 最小变换、保存对齐         | 无映射地清洗文章      | 答案 span 会失效  |
| 传统词袋分类              | 词项及其频率                  | 小写、词形还原、n-gram 实验 | 未验证地删除词项      | 归并可能同时丢失区分信息 |
| 搜索与去重               | 内容相似性及可回显原文             | 额外生成宽松匹配键         | 用匹配键覆盖原文或直接判等 | 折叠规则会制造碰撞    |
| 代码模型                | 精确字符、缩进、符号和大小写          | 修复已知编码问题          | 普通文本清洗        | 会改变程序语法和行为   |

### 7.9 与预训练模型保持一致

如果使用已有模型，最重要的规则不是“采用最先进清洗方法”，而是：

1. 查看模型说明与 Tokenizer 配置；
2. 使用与 checkpoint 配套的 Tokenizer；
3. 不在 Tokenizer 前重复小写、分词或去重音，除非模型文档明确要求；
4. 训练、验证、测试和线上推理使用同一套预处理版本。

否则会产生训练—推理偏移：模型训练时看到一种输入，实际使用时却看到另一种输入。

## 8. 从简单函数到可审计流水线

### 8.1 一个保守的基础版本

```python
from dataclasses import dataclass
import unicodedata

@dataclass
class TextRecord:
    raw_text: str
    model_text: str

def normalize_newlines(text: str) -> str: # 处理换行
    return text.replace("\r\n", "\n").replace("\r", "\n")

def collapse_inline_whitespace(text: str) -> str: # 处理空格
    return "\n".join(" ".join(line.split()) for line in text.split("\n"))

def preprocess_for_general_nlp(text: str) -> TextRecord:
    raw_text = text
    text = normalize_newlines(text)
    text = unicodedata.normalize("NFC", text)
    text = collapse_inline_whitespace(text).strip()
    return TextRecord(raw_text=raw_text, model_text=text)
```

测试：

```python
sample = "ＡＩ学习\r\n  很  有趣！  Model①  "
record = preprocess_for_general_nlp(sample)

print(repr(record.raw_text))
print(repr(record.model_text))
```

输出：

```
'ＡＩ学习\r\n  很  有趣！  Model①  '
'ＡＩ学习\n很 有趣！ Model①'
```

这个保守版本没有执行 NFKC、大小写折叠、去标点或中文分词。它不是对所有任务都正确，但每一步的意图清楚，也保留了原文。

### 8.2 为搜索额外生成匹配键

```python
def make_search_key(text: str) -> str:
    text = normalize_newlines(text)
    text = unicodedata.normalize("NFKC", text)
    text = text.casefold()
    text = collapse_inline_whitespace(text).strip()
    return text


print(repr(make_search_key("ＡＩ学习\r\n  很  有趣！  Model①  ")))
```

输出：

```
'ai学习\n很 有趣! model1'
```

现在可以清楚看到发生了什么：

* `ＡＩ` → `AI` → `ai`；
* `！` → `!`；
* `①` → `1`；
* 连续行内空白被折叠。

这适合作为某些搜索键，却不适合覆盖展示原文。

### 8.3 记录每一步变化

排查数据问题时，只看最终文本很难知道是哪条规则造成了变化。可以给流水线加追踪：

```python
from collections.abc import Callable

def apply_with_trace(
    text: str,
    steps: list[tuple[str, Callable[[str], str]]],
) -> tuple[str, list[dict[str, str]]]:
    trace = []

    for name, function in steps:
        before = text
        text = function(text)
        if text != before:
            trace.append({
                "step": name,
                "before": before,
                "after": text,
            })

    return text, trace

steps = [
    ("newlines", normalize_newlines),
    ("nfc", lambda value: unicodedata.normalize("NFC", value)),
    ("inline_whitespace", collapse_inline_whitespace),
    ("strip", str.strip),
]

model_text, trace = apply_with_trace(
    "AI\r\n  学习  ",
    steps,
)

for item in trace:
    print(item)
```

输出：

```
{'step': 'newlines', 'before': 'AI\r\n  学习  ', 'after': 'AI\n  学习  '}
{'step': 'inline_whitespace', 'before': 'AI\n  学习  ', 'after': 'AI\n学习'}
```

这种设计让规则可测试、可解释，也便于定位某一批数据为什么突然发生变化。

## 9. 位置对齐：预处理最容易被忽略的技术细节

序列标注和抽取式问答常把标签绑定在字符位置上：

```
原文：我爱ＡＩ
实体：ＡＩ，区间 [2, 4)
```

若 NFKC 后变为：

```
我爱AI
```

这个例子的长度碰巧没变，但 `ﬁ → fi` 会让一个码点变成两个，删除空格或标点也会整体移动后续位置。于是原始标签不再能直接套到处理后文本。

### 9.1 三种策略

1. **最小化变换**：需要精确位置时优先选择；
2. **变换标签**：文本变换时同步更新 span；
3. **维护对齐映射**：记录处理后每个位置来自原文哪里。

Hugging Face Tokenizers 的一个重要工程特性是规范化时保留对齐关系，从而能够返回 Token 对应原文的 offset。自己写清洗代码时，这种能力不会自动出现。

{% hint style="danger" %}
如果任务依赖字符区间，任何删除、插入、合并或拆分字符的操作都必须重新检查标签对齐。
{% endhint %}

## 10. 常见错误与改进方式

### 错误 1：所有文本都执行同一个 `clean_text`

问题：不同任务依赖的信息不同。

改进：按“数据来源 + 任务 + 模型”配置流水线，并对规则做版本管理。

### 错误 2：先去掉所有非字母字符

问题：数字、标点、中文、emoji、URL 和代码符号会全部损失。

改进：只针对已定义的噪声模式操作，并建立包含边界情况的测试集。

### 错误 3：认为 NFKC 是更彻底、更高级的 NFC

问题：NFKC 解决的是兼容等价，会抹除更多区别。

改进：基础文本优先评估 NFC；把 NFKC 作为明确用途的派生变换。

### 错误 4：先分词，再交给预训练模型的 Tokenizer

问题：重复分词可能改变空格和词边界，与模型预训练输入不一致。

改进：先理解配套 Tokenizer 的完整 pipeline，再决定是否加入外部分词。

### 错误 5：只保存最终清洗结果

问题：无法回显、复查或更换规则。

改进：保存原文、模型文本、规则版本；需要 span 时保存对齐信息。

### 错误 6：训练与推理使用不同规则

问题：产生数据分布偏移，并且很难从模型指标中直接定位。

改进：把预处理封装为同一个可复用组件，用单元测试锁定行为。

重点记录：哪些变化只发生在 NFKC/NFKD？哪些变换改变字符串长度？

## 11. 自测题

<details>

<summary>1. NFC 与 NFKC 的根本区别是什么？</summary>

NFC 处理规范等价并采用组合形式，主要统一同一文本的不同 Unicode 表示；NFKC 还处理兼容等价，会折叠全角字符、连字、带圈数字等区别，因此更可能丢失信息。

</details>

<details>

<summary>2. 为什么不能把 NFKC 后的结果直接当作唯一原文？</summary>

因为兼容规范化可能改变外观、结构或语义区别，而且通常不可逆。更稳妥的方式是保留原文，并只为明确用途生成 NFKC 派生字段。

</details>

<details>

<summary>3. `lower()` 与 `casefold()` 有什么区别？</summary>

`lower()` 主要生成小写形式；`casefold()` 面向 Unicode 不区分大小写匹配，处理范围更激进，例如德语 `ß` 可以折叠为 `ss`。

</details>

<details>

<summary>4. 为什么中文不能照搬英文的“按空格分词 + 词形还原”？</summary>

中文通常不使用空格标出词边界，也没有与英语相同的屈折词形系统。中文分词本身需要上下文，而且现代模型往往已经有配套的字符或子词 Tokenizer。

</details>

<details>

<summary>5. 为什么预处理会让 NER 或抽取式问答标签失效？</summary>

删除、插入、合并或拆分字符会改变字符位置，原始实体或答案的起止 offset 就不再对应处理后的文本。需要最小变换、同步更新标签或维护对齐映射。

</details>

<details>

<summary>6. 使用预训练模型时，为什么不应自行增加一整套传统清洗？</summary>

模型是在特定 Tokenizer 和预处理分布上训练的。额外小写、分词、词干化或去标点会让输入偏离训练分布，也可能与 Tokenizer 自己的操作重复。

</details>

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

讲义已经整合了今天需要掌握的内容，以下原始资料用于核对和深入，不要求明天全部阅读。

### 核心规范与官方文档

1. [Unicode Standard Annex #15: Unicode Normalization Forms](https://unicode.org/reports/tr15/)\
   四种规范化形式、规范等价、兼容等价及稳定性的正式说明。
2. [Unicode FAQ: Normalization](https://www.unicode.org/faq/normalization.html)\
   Unicode 官方的常见问题解释。
3. [W3C Character Model: String Matching](https://www.w3.org/TR/charmod-norm/)\
   规范化、大小写折叠和字符串匹配的工程边界，尤其强调兼容规范化的风险。
4. [Python `unicodedata`](https://docs.python.org/3/library/unicodedata.html)\
   `normalize()`、字符名称、类别和组合类等标准库接口。
5. [Python `str.casefold`](https://docs.python.org/3/library/stdtypes.html#str.casefold)\
   Unicode 大小写折叠的官方说明。

### Tokenizer 与模型

6. [Hugging Face Tokenizers: Components](https://huggingface.co/docs/tokenizers/python/latest/components.html)\
   Normalizer、Pre-tokenizer、Model、Post-processor 等组件及规范化时的对齐维护。
7. [Hugging Face Tokenizers: Pipeline](https://huggingface.co/docs/tokenizers/python/latest/pipeline.html)\
   从原始文本到 Token ID 的完整流程。
8. [BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding](https://aclanthology.org/N19-1423/)\
   理解预训练模型、WordPiece 与输入表示的原始论文。
9. [SentencePiece: A Simple and Language Independent Subword Tokenizer and Detokenizer](https://aclanthology.org/D18-2012/)\
   直接从原始句子训练子词模型、不依赖预先按词切分的代表性方法。

## 一页回顾

```
预处理目标：消除任务无关差异，保留任务相关信息

NFC  = 规范等价 + 组合
NFD  = 规范等价 + 分解
NFKC = 兼容等价 + 组合
NFKD = 兼容等价 + 分解

保守默认：
正确解码 → 保存原文 → 统一明确格式问题 → 评估 NFC → 配套 Tokenizer

按任务决定：
NFKC、casefold、压缩空白、去标点、去停用词、分词、词形还原

工程底线：
训练与推理规则一致；需要 span 时维护字符对齐；不可逆操作前保留原文
```


---

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