【HuggingFace LLM】规范化与预分词(BPE、WordPiece以及Unigram)

规范化

python 复制代码
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
print(type(tokenizer.backend_tokenizer)) # <class 'tokenizers.Tokenizer'>
print(tokenizer.backend_tokenizer.normalizer.normalize_str("Héllò hôw are ü?"))
# 'hello how are u?'

使用normalize_str方法删除了重音等特殊符号。

预分词

快速分词器(FastTokenizer,比如 Hugging Face 的BertFastTokenizer)处理文本分为两步:

  1. 预分词(Pre-tokenization):把原始文本拆成 "基础语义单元"(比如单词、标点),并记录每个单元在原始文本中的位置;
  2. 子词切分(Subword Tokenization) :把基础单元进一步拆成模型能识别的子词(比如HelloHell+o)。
    pre_tokenize_str() 是展示预分词结果的方法,使看到分词器对原始文本的 "初步拆解"。
  • BERT Tokenizer
python 复制代码
tokenizer.backend_tokenizer.pre_tokenizer.pre_tokenize_str("Hello, how are  you?")
[('Hello', (0, 5)), (',', (5, 6)), ('how', (7, 10)), ('are', (11, 14)), ('you', (16, 19)), ('?', (19, 20))]

注意到BERT Tokenizer不会把两个空格作为语义单元,但是会在制作offset时定位准确CharSpan

  • GPT2
python 复制代码
tokenizer = AutoTokenizer.from_pretrained("gpt2")
tokenizer.backend_tokenizer.pre_tokenizer.pre_tokenize_str("Hello, how are  you?")
[('Hello', (0, 5)), (',', (5, 6)), ('Ġhow', (6, 10)), ('Ġare', (10, 14)), ('Ġ', (14, 15)), ('Ġyou', (15, 19)), ('?', (19, 20))]

而在GPT2 Tokenizer中,则是会把空格作为语义单元,用Ġ符号进行表示,这样解码后可以恢复出原始的空格。

  • T5
python 复制代码
tokenizer = AutoTokenizer.from_pretrained("t5-small")
tokenizer.backend_tokenizer.pre_tokenizer.pre_tokenize_str("Hello, how are  you?")
[('▁Hello,', (0, 6)), ('▁how', (7, 10)), ('▁are', (11, 14)), ('▁you?', (16, 20))]

T5默认在句首添加_,并且忽略两个空格仅作为一个。

Byte-Pair Encoding tokenization
训练

首先计算单个字符的出现频率

复制代码
"hug", "pug", "pun", "bun", "hugs"

#重要 这一步是pre-tokenization结果统计

以上述例子为例,统计单个字符的出现频次

复制代码
("h" "u" "g", 10), ("p" "u" "g", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "u" "g" "s", 5)

假设出现的频次如上,那么下一步BPE就会统计连续两个同时出现次数最多的字符合成 token

这里ug相邻一共出现了16次,因此合并为一个token继续训练

复制代码
Vocabulary: ["b", "g", "h", "n", "p", "s", "u", "ug"]
Corpus: ("h" "ug", 10), ("p" "ug", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "ug" "s", 5)

下一步统计其中ug相邻且出现次数最多,因此合并为一个token继续训练

复制代码
Vocabulary: ["b", "g", "h", "n", "p", "s", "u", "ug", "un"]
Corpus: ("h" "ug", 10), ("p" "ug", 5), ("p" "un", 12), ("b" "un", 4), ("h" "ug" "s", 5)

再下一步,发现hug相邻且出现的次数最多,因此合并为一个token继续训练

复制代码
Vocabulary: ["b", "g", "h", "n", "p", "s", "u", "ug", "un", "hug"]
Corpus: ("hug", 10), ("p" "ug", 5), ("p" "un", 12), ("b" "un", 4), ("hug" "s", 5)
分词算法

输入通过以下几个步骤进行分词

复制代码
1. Normalize;
2. Pre-tokenize;
3. 拆分单词为单个字符;
4. 将单个字符按照已经学习到的规则进行组合形成token

以训练得到的规则为例

复制代码
("u", "g") -> "ug"
("u", "n") -> "un"
("h", "ug") -> "hug"

那么

复制代码
"bug" -> 'b' 'u' 'g' -> 'b' 'ug'
"mug" -> '[UNK]' 'u' 'g' -> "[UNK]" 'ug'
"thug" -> '[UNK]' 'h' 'u' 'g' -> '[UNK]' 'h' 'ug' -> '[UNK]' 'hug'
"unhug" -> 'u' 'n' 'h' 'u' 'g' -> "un" 'h' "ug" -> "un" "hug"

如上步骤完成分词,其中未在vocabulary中出现的则被是做[UNK]

WordPiece tokenization

wordPiece tokenization的合并规则为
score=(freq_of_pair)(freq_of_first_element×freq_of_second_element) score=\frac{(freq\_of\_pair)}{(freq\_of\_first\_element \times freq\_of\_second\_element)} score=(freq_of_first_element×freq_of_second_element)(freq_of_pair)

这个得分用于量化 两个元素的共现是偶然的,还是必然的

得分越高 :说明 "实际共现次数" 相对于 "随机组合的预期次数" 越高,即这两个元素几乎只在一起出现 (强绑定),合并的价值越大; 得分越低 :说明 "实际共现次数" 相对于 "随机组合的预期次数" 越低,即这两个元素各自独立出现的场景更多(弱绑定),合并的价值越小。

旨在优先合并词汇表中各部分频率较低的词对

训练
复制代码
("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5)

wordPiece tokenization作为预训练BERT开发的词法分析,BERT会把非首字母字符拆分成##的形式,进行统计得:

复制代码
("h" "##u" "##g", 10), ("p" "##u" "##g", 5), ("p" "##u" "##n", 12), ("b" "##u" "##n", 4), ("h" "##u" "##g" "##s", 5)

其中##g##s组合的分数为
score=520×5=120 score = \frac{5}{20\times5}=\frac{1}{20} score=20×55=201
##u##n组合的分数为
score=1636×16=136 score=\frac{16}{36\times16}=\frac{1}{36} score=36×1616=361

因此优先合并组合分数较高的##gs,注意合并后仅保留一个##

复制代码
Vocabulary: ["b", "h", "p", "##g", "##n", "##s", "##u", "##gs"]
Corpus: ("h" "##u" "##g", 10), ("p" "##u" "##g", 5), ("p" "##u" "##n", 12), ("b" "##u" "##n", 4), ("h" "##u" "##gs", 5)

此时, "##u" 存在于所有可能的配对中,因此它们的得分都相同。假设在这种情况下,第一个配对被合并,即 ("h", "##u") -> "hu" 。这将得到

复制代码
Vocabulary: ["b", "h", "p", "##g", "##n", "##s", "##u", "##gs", "hu"]
Corpus: ("hu" "##g", 10), ("p" "##u" "##g", 5), ("p" "##u" "##n", 12), ("b" "##u" "##n", 4), ("hu" "##gs", 5)

然后,得分第二高的是 ("hu", "##g")("hu", "##gs") (得分均为 1/15,而其他所有组合的得分均为 1/21),因此得分最高的第一对将被合并

复制代码
Vocabulary: ["b", "h", "p", "##g", "##n", "##s", "##u", "##gs", "hu", "hug"]
Corpus: ("hug", 10), ("p" "##u" "##g", 5), ("p" "##u" "##n", 12), ("b" "##u" "##n", 4), ("hu" "##gs", 5)
分词算法
复制代码
Vocabulary: ["b", "h", "p", "##g", "##n", "##s", "##u", "##gs", "hu", "hug"]

WordPiece 只保存最终的词汇表,找到词汇表中最长的子词并以此为分割点进行分割

复制代码
hugs -> hug ##s;
bugs -> b ##u ##gs;
mug -> [UNK]
bum -> [UNK]
pugs -> p ##u ##gs

由于m不在词汇表中,当分词过程中无法在词汇表中找到子词 时,整个词会被分词为未知词

Unigram tokenization

Unigram算法与上两种算法的原理不同,是从一个较大的词汇表中删除 token,通过筛选删去后Loss降低较小的token去整合最终的高可能性tokens列表。

训练
复制代码
Corpus: ("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5)
Vocabulary: ["h", "u", "g", "hu", "ug", "p", "pu", "n", "un", "b", "bu", "s", "hug", "gs", "ugs"]

初始的大词汇表这里暂定选这个,一般可以通过较大的初始文本中通过BPE得到。

复制代码
("h", 15) ("u", 36) ("g", 20) ("hu", 15) ("ug", 20) ("p", 17) ("pu", 17) ("n", 16)
("un", 16) ("b", 4) ("bu", 4) ("s", 5) ("hug", 15) ("gs", 5) ("ugs", 5)

Unigram的一个核心定义就是每个token都是独立的,因此单词的组合就是每个token概率乘积
P([p,u,g])=P(p)×P(u)×P(g)=5210×36210×20210=0.000389P([pu,g])=P(pu)×P(g)=5210×20210=0.0022676 \begin{aligned} P([p,u,g])=P(p)×P(u)×P(g)=\frac{5}{210}×\frac{36}{210}×\frac{20}{210}=0.000389 \\ P([pu,g])=P(pu)×P(g)=\frac{5}{210}×\frac{20}{210}=0.0022676 \end{aligned} P([p,u,g])=P(p)×P(u)×P(g)=2105×21036×21020=0.000389P([pu,g])=P(pu)×P(g)=2105×21020=0.0022676

上述是pug被分词为p、u、gpu、g的概率。一般来说,分词数量最少的方案概率最高(因为每个分词都要除以 210),这符合我们的直觉:将一个词拆分成尽可能少的分词

viterbi算法分词

目标单词huggun,其字符与位置对应关系:

字符位置 0 1 2 3 4 5
字符 h u g g u n

为了实现动态规划,我们需要两个数组:

  1. dp数组dp[i] 表示以第i个字符结尾的最优分词方案的概率得分
    • 初始化:dp[-1] = 1(虚拟起始位置,方便计算第一个字符得分)其余dp[i]初始为 0。
  2. backtrace数组backtrace[i] 记录以第i个字符结尾的最优方案中,最后一个子词的起始位置j和子词内容,用于最后回溯路径。

我们逐个遍历字符位置i(从 0 到 5),计算每个位置的dp[i]backtrace[i]

步骤1处理位置i=0(字符h

目标:找到以h结尾的最优分词方案。

  • 遍历可能的起始位置jj ≤ i,即j=0):
    • 子词:word[0:0+1] = "h"(在词汇表中)。
    • 得分计算:dp[j-1] × P("h") = dp[-1] × 15/210 = 1 × 0.0714 = 0.0714
  • 结果:
    • dp[0] = 0.0714(唯一可能的得分)。
    • backtrace[0] = (j=0, 子词="h")
步骤2处理位置i=1(字符u

目标:找到以u结尾的最优分词方案。

  • 遍历可能的起始位置jj=0、j=1):
    • j=0 :子词word[0:2] = "hu"(在词汇表中),得分 =dp[-1] × 15/210 = 0.0714
    • j=1 :子词word[1:2] = "u"(在词汇表中),得分 =dp[0] × 36/210 = 0.0714 × 0.1714 ≈ 0.0122
  • 比较得分:0.0714(j=0)> 0.0122(j=1)
  • 结果:
    • dp[1] = 0.0714
    • backtrace[1] = (j=0, 子词="hu")
步骤3处理位置i=2(字符g

目标:找到以g结尾的最优分词方案。

  • 遍历可能的起始位置jj=0、j=1、j=2):
    • j=0 :子词word[0:3] = "hug"(在词汇表中),得分 =dp[-1] × 15/210 = 0.0714
    • j=1 :子词word[1:3] = "ug"(在词汇表中),得分 =dp[0] × 20/210 ≈ 0.0714 × 0.0952 ≈ 0.0068
    • j=2 :子词word[2:3] = "g"(在词汇表中),得分 =dp[1] × 20/210 ≈ 0.0714 × 0.0952 ≈ 0.0068
  • 比较得分:0.0714(j=0)最高。
  • 结果:
    • dp[2] = 0.0714
    • backtrace[2] = (j=0, 子词="hug")
步骤4处理位置i=3(字符g

目标:找到以g结尾的最优分词方案。

  • 遍历可能的起始位置jj=0、1、2、3),只保留词汇表中存在的子词
    • j=0:hugg(不在词汇表,跳过)。
    • j=1:ugg(不在词汇表,跳过)。
    • j=2:gg(不在词汇表,跳过)。
    • j=3 :子词word[3:4] = "g"(在词汇表中),得分 =dp[2] × 20/210 ≈ 0.0714 × 0.0952 ≈ 0.0068
  • 结果:
    • dp[3] = 0.0068
    • backtrace[3] = (j=3, 子词="g")
步骤5处理位置i=4(字符u

目标:找到以u结尾的最优分词方案。

  • 遍历可能的起始位置jj=0到4),只保留词汇表中存在的子词
    • j=0-3:huggu/uggu/ggu/gu(均不在词汇表,跳过)。
    • j=4 :子词word[4:5] = "u"(在词汇表中),得分 =dp[3] × 36/210 ≈ 0.0068 × 0.1714 ≈ 0.00117
  • 结果:
    • dp[4] = 0.00117
    • backtrace[4] = (j=4, 子词="u")
步骤6处理位置i=5(字符n

目标:找到以n结尾的最优分词方案。

  • 遍历可能的起始位置jj=0到5),只保留词汇表中存在的子词
    • j=0-3:huggun/uggun/ggun/gun(均不在词汇表,跳过)。
    • j=4 :子词word[4:6] = "un"(在词汇表中),得分 =dp[3] × 16/210 ≈ 0.0068 × 0.0762 ≈ 0.000518
    • j=5 :子词word[5:6] = "n"(在词汇表中),得分 =dp[4] × 16/210 ≈ 0.00117 × 0.0762 ≈ 0.000088
  • 比较得分:0.000518(j=4)> 0.000088(j=5)
  • 结果:
    • dp[5] = 0.000518(这是最终的总得分)。
    • backtrace[5] = (j=4, 子词="un")

从最后一个位置i=5开始,通过backtrace数组倒推,拼接子词:

  1. i=5backtrace[5]显示子词是un,起始位置j=4 → 前驱位置是j-1=3
  2. i=3backtrace[3]显示子词是g,起始位置j=3 → 前驱位置是j-1=2
  3. i=2backtrace[2]显示子词是hug,起始位置j=0 → 前驱位置是j-1=-1(结束)。

最终的分词结果:["hug", "g", "un"],总得分≈0.000518(用分数计算更准确:15/210 × 20/210 × 16/210 = 4800/9261000 ≈ 0.000518)。

词汇表训练

定义

Loss=∑i=1Nfreqi×−log⁡(P(word)) Loss=\sum_{i=1}^N freq_i \times-\log(P(word)) Loss=i=1∑Nfreqi×−log(P(word))

其中,N是语料中的预分词总数,word是预分词经过分词后得到的组成这个预分词的概率freq_i是该单词在语料中的出现频次。

复制代码
("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5)

"hug": ["hug"] (score 0.071428)
"pug": ["pu", "g"] (score 0.007710)
"pun": ["pu", "n"] (score 0.006168)
"bun": ["bu", "n"] (score 0.001451)
"hugs": ["hug", "s"] (score 0.001701)

Loss = 10 * (-log(0.071428)) + 5 * (-log(0.007710)) + 12 * (-log(0.006168)) + 4 * (-log(0.001451)) + 5 * (-log(0.001701)) = 169.8

由于pu、gp、ug的拆分概率是相同的,因此若删除puug这两个token后,整体Loss不变。

但是如果删除hug这个token

复制代码
"hug": ["hu", "g"] (score 0.006802)
"hugs": ["hu", "gs"] (score 0.001701)

Loss += - 10 * (-log(0.071428)) + 10 * (-log(0.006802)) = 23.5 
相关推荐
Data_agent10 小时前
Pantherbuy模式淘宝 / 1688 代购系统(欧美市场)搭建指南
大数据·python·产品经理
大厂技术总监下海10 小时前
从Hadoop MapReduce到Apache Spark:一场由“磁盘”到“内存”的速度与范式革命
大数据·hadoop·spark·开源
元智启10 小时前
企业 AI 应用进入 “能力解耦时代”:模块化重构 AI 落地新范式
大数据·人工智能·重构
RockHopper202510 小时前
驾驶认知的本质:人类模式 vs 端到端自动驾驶
人工智能·神经网络·机器学习·自动驾驶·具身认知
小真zzz10 小时前
【2026新体验】ChatPPT的AI智能路演评测:PPT总结和问答都变的易如反掌
大数据·人工智能·ai·powerpoint·ppt·chatppt
wenzhangli710 小时前
Ooder SkillFlow:破解 AI 编程冲击,重构企业级开发全流程
大数据·人工智能
H79987424210 小时前
ERP管理系统软件推荐:聚焦中小制造,三款高适配MES系统深度对比与选择策略
大数据·人工智能·制造
China_Yanhy10 小时前
后端开发者的 AWS 大数据指南:从 RDS 到 Data Lake
大数据·云计算·aws
●VON10 小时前
智能暗战:AI 安全攻防实战全景解析
人工智能·学习·安全·von