BERT 分词过程详解:第 1 步 从文字到数字

一句话理解

BERT 分词 = 把文字切成 token(中文切成单字,英文切成子词) → 查词表转成数字 → 填充到固定长度 → 送入模型。模型只认识数字,不认识文字。

⚠️ 术语说明:中文其实是"分字"不是"分词"

虽然这篇笔记标题写的是"分词",但对中文来说,BERT 实际上是逐字切分——每个汉字就是一个 token,不存在"词"的概念。

``` 英文 BERT:分词(Tokenize)→ 切成子词(play + ##ing) 中文 BERT:分字(Character Split)→ 每个字独立(今 / 天 / 天 / 气)

"分词"是 NLP 领域的通用叫法(Tokenization), 但中文 BERT 的本质是"分字"——WordPiece 对中文完全退化为逐字切分。 ```

⚠️ 核心认知:BERT 分类模型只能输出"类别编号"

BERT 分类模型的全部输出 = 一个类别编号(如 12)。 它不会说话、不会生成文字、不会调用工具。 所谓的"回答问题",是代码拿到类别编号后查 categories.json 返回预设答案——这一步是 Python 代码做的,不是 AI 做的。

BERT = 一个只会指手指的人 你问他问题 → 他指向 17 个按钮中的第 12 个 就这么多。 按钮后面写着什么答案 → 是你提前贴上去的(categories.json) 按了按钮之后发生什么 → 是你写的代码控制的


分词的完整流程

用户输入:"今天天气如何"
    ↓
第 1 步:Normalize(标准化)
    英文转小写、去除多余空格等
    ↓
第 2 步:Pre-tokenize(预分词)
    按空格和标点切分成"词"
    "今天天气如何" → ["今", "天", "天", "气", "如", "何"]
    ↓
第 3 步:Tokenize(查词表)
    每个字/词 → 查 tokenizer.json 词表 → 得到 token ID
    "今"→791, "天"→1921, "天"→1921, "气"→3698, "如"→1963, "何"→862
    ↓
第 4 步:Post-process(后处理)
    ├── 加特殊标记:[CLS] 开头 + [SEP] 结尾
    ├── 截断:超过 max_length 就砍掉
    └── 填充:不够固定长度就补 0
    ↓
最终输出:input_ids + attention_mask + token_type_ids
    → 送入模型推理

实际例子:"今天天气如何"

第 1 步:预分词(中文逐字切分)

中文 BERT 对中文采用"逐字切分"策略:

"今天天气如何" → ["今", "天", "天", "气", "如", "何"]

每个字都是一个独立的 token
(不像英文会按 WordPiece 切成子词)

第 2 步:查词表转 ID

在 tokenizer.json 词表中查找每个字对应的编号:

  "今"  → 791     (tokenizer.json 第 953 行)
  "天"  → 1921    (tokenizer.json 第 2083 行)
  "天"  → 1921    (同一个字,同一个 ID)
  "气"  → 3698    (tokenizer.json 第 3860 行)
  "如"  → 1963    (tokenizer.json 第 2125 行)
  "何"  → 862     (tokenizer.json 第 1024 行)

第 3 步:加特殊标记

前后各加一个特殊 token:

  [CLS]  今   天   天   气   如   何  [SEP]
   101   791  1921 1921 3698 1963 862  102

[CLS](ID=101)= Classification 标记,放在句子开头
[SEP](ID=102)= Separator 标记,放在句子结尾

第 4 步:填充到固定长度

tokenizer.json 设置:
  padding = Fixed 64     ← 固定填充到 64 位
  truncation max_length = 64  ← 超过 64 就截断

当前只有 8 个 token → 需要补 56 个 0:

  [101, 791, 1921, 1921, 3698, 1963, 862, 102, 0, 0, ... 0]
   ←──────── 8 个真实 token ────────→  ←── 56 个填充 ──→
   └─────────────────── 总共 64 位 ───────────────────────┘

最终输出:三个数组

input_ids(token 编号):
  [101, 791, 1921, 1921, 3698, 1963, 862, 102, 0, 0, ... 0]
  → 告诉模型"输入了什么"

attention_mask(注意力掩码):
  [1, 1, 1, 1, 1, 1, 1, 1, 0, 0, ... 0]
  → 告诉模型"哪些是真实内容(1),哪些是填充(0)"

token_type_ids(句子编号):
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... 0]
  → 告诉模型"都属于第几句"(单句任务全是 0)

对照图:文字 → 数字 → 模型

文字:    "今"   "天"   "天"   "气"   "如"   "何"
          ↓      ↓      ↓      ↓      ↓      ↓
分词:   [CLS]   今     天     天     气     如     何   [SEP]
          ↓      ↓      ↓      ↓      ↓      ↓      ↓     ↓
ID:     101    791    1921   1921   3698   1963   862   102
          ↓      ↓      ↓      ↓      ↓      ↓      ↓     ↓
填充后:  101    791    1921   1921   3698   1963   862   102   0  0  ... 0
          ↓      ↓      ↓      ↓      ↓      ↓      ↓     ↓    ↓  ↓      ↓
mask:     1      1      1      1      1      1      1     1    0  0  ... 0

英文例子:WordPiece 子词切分

英文不是逐字切分,而是按"子词"切分:

输入:"I love playing basketball"

分词结果:
  ["I", "love", "play", "##ing", "basket", "##ball"]
   ↑      ↑      ↑      ↑        ↑        ↑
  整词   整词   子词   后缀     子词     后缀

查词表:
  "I"        → 某个 ID
  "love"     → 某个 ID
  "play"     → 某个 ID(没有 ##,可以独立开头)
  "##ing"    → 某个 ID(有 ##,只能接在前面)
  "basket"   → 某个 ID
  "##ball"   → 某个 ID

加标记 + 填充:
  [CLS] I love play ##ing basket ##ball [SEP] [PAD] [PAD] ...

中文 vs 英文:分字 vs 分词

中文:"今天天气如何"
  → ["今", "天", "天", "气", "如", "何"]
  → 逐字切分(分字,不是分词!)
  → 6 个字 = 6 个 token
  → WordPiece 对中文完全退化,没有发挥子词优势

英文:"playing basketball"
  → ["play", "##ing", "basket", "##ball"]
  → 子词切分(真正的"分词")
  → 2 个词 = 4 个 token
  → WordPiece 充分发挥作用

为什么中文是"分字"?
  ├── 中文没有空格分隔单词
  ├── 中文字本身就是最小语义单元
  ├── normalizer 的 handle_chinese_chars 在每个中文字前后加空格
  └── 预分词阶段就把中文字切开了,WordPiece 没有合并机会

长度限制

两层限制:

第 1 层:tokenizer.json 设置(项目自定义)
  ├── 截断:max_length = 64   → 超过 64 token 就砍掉
  └── 填充:Fixed = 64        → 不够 64 就补 0

第 2 层:BERT 模型硬限制
  └── max_position_embeddings = 512
      → 模型最多处理 512 个位置
      → 超过 512 → 位置编码没了,模型"看不到"

为什么项目设为 64?
  ├── 分类任务输入通常是短文本(一句话)
  ├── 64 token ≈ 60 个中文字,够用
  └── 固定长度 → 批量处理方便,计算更快

词表中的特殊标记

BERT 词表有 5 个特殊标记:

  [PAD](ID=0)    → 填充标记(补位用的,没有实际含义)
  [UNK](ID=100)  → 未知标记(遇到完全没见过的字符)
  [CLS](ID=101)  → 分类标记(放在句子开头)
  [SEP](ID=102)  → 分隔标记(放在句子结尾,或分隔两个句子)
  [MASK](ID=103) → 掩码标记(预训练时用来遮住文字)

推理时最常见的是 [CLS] 和 [SEP]:
  [CLS] + 内容 + [SEP] = 标准输入格式

代码实现(test_onnx.py 中的关键步骤)

from tokenizers import Tokenizer

# 加载分词器
tokenizer = Tokenizer.from_file("tokenizer.json")

# 分词(一步完成:标准化 + 预分词 + 查词表 + 后处理)
encoding = tokenizer.encode("今天天气如何")

# 取出三个数组
input_ids = np.array([encoding.ids], dtype=np.int64)
attention_mask = np.array([encoding.attention_mask], dtype=np.int64)
token_type_ids = np.array([encoding.type_ids], dtype=np.int64)

# 送入模型
outputs = session.run(None, {
    "input_ids": input_ids,
    "attention_mask": attention_mask,
    "token_type_ids": token_type_ids,
})

tokenizer.encode() 一行代码就完成了全部 4 步: 标准化 → 预分词 → 查词表 → 加标记/截断/填充


一句话总结

BERT 分词 = 文字 → 数字 的翻译过程

中文:分字(不是分词!)→ 每个字查词表 → 得到 ID
英文:分词(WordPiece)→ 子词切分 → 每个子词查词表 → 得到 ID

后处理:
  ├── 加 [CLS] 和 [SEP]
  ├── 截断(超过 max_length 就砍)
  └── 填充(不够就补 0)

输出三个数组:
  input_ids       → 内容(字的编号)
  attention_mask  → 哪些是真的(1=真实,0=填充)
  token_type_ids  → 属于哪句(0=第一句)

→ 这三个数组就是分词器的全部输出,接下来送入模型推理

→ 下一篇:BERT 推理过程详解:第 2 步 从数字到答案

分词完成后,三个数组如何变成最终答案?请看下一篇笔记:

input_ids + attention_mask + token_type_ids
    ↓ BERT 模型
logits(每个类别的原始得分)
    ↓ softmax
概率(0~1,总和=1)
    ↓ argmax
类别编号
    ↓ 查 categories.json
预设答案

📝 创建:2026-09-12
🔄 标签:BERT、分词、Tokenization、tokenizer.json、input_ids、attention_mask

相关笔记