一句话理解
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
→ 送入模型推理
中文 BERT 对中文采用"逐字切分"策略:
"今天天气如何" → ["今", "天", "天", "气", "如", "何"]
每个字都是一个独立的 token
(不像英文会按 WordPiece 切成子词)
在 tokenizer.json 词表中查找每个字对应的编号:
"今" → 791 (tokenizer.json 第 953 行)
"天" → 1921 (tokenizer.json 第 2083 行)
"天" → 1921 (同一个字,同一个 ID)
"气" → 3698 (tokenizer.json 第 3860 行)
"如" → 1963 (tokenizer.json 第 2125 行)
"何" → 862 (tokenizer.json 第 1024 行)
前后各加一个特殊 token:
[CLS] 今 天 天 气 如 何 [SEP]
101 791 1921 1921 3698 1963 862 102
[CLS](ID=101)= Classification 标记,放在句子开头
[SEP](ID=102)= Separator 标记,放在句子结尾
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
英文不是逐字切分,而是按"子词"切分:
输入:"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] ...
中文:"今天天气如何"
→ ["今", "天", "天", "气", "如", "何"]
→ 逐字切分(分字,不是分词!)
→ 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] = 标准输入格式
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=第一句)
→ 这三个数组就是分词器的全部输出,接下来送入模型推理
分词完成后,三个数组如何变成最终答案?请看下一篇笔记:
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