【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 
相关推荐
灰灰勇闯IT13 小时前
领域制胜——CANN 领域加速库(ascend-transformer-boost)的场景化优化
人工智能·深度学习·transformer
灰灰勇闯IT13 小时前
从零到一——CANN 社区与 cann-recipes-infer 实践样例的启示
人工智能
小白狮ww13 小时前
要给 OCR 装个脑子吗?DeepSeek-OCR 2 让文档不再只是扫描
人工智能·深度学习·机器学习·ocr·cpu·gpu·deepseek
lili-felicity13 小时前
CANN优化LLaMA大语言模型推理:KV-Cache与FlashAttention深度实践
人工智能·语言模型·llama
程序猿追13 小时前
深度解码昇腾 AI 算力引擎:CANN Runtime 核心架构与技术演进
人工智能·架构
金融RPA机器人丨实在智能13 小时前
Android Studio开发App项目进入AI深水区:实在智能Agent引领无代码交互革命
android·人工智能·ai·android studio
lili-felicity13 小时前
CANN异步推理实战:从Stream管理到流水线优化
大数据·人工智能
做人不要太理性13 小时前
CANN Runtime 运行时组件深度解析:任务下沉执行、异构内存规划与全栈维测诊断机制
人工智能·神经网络·魔珐星云
不爱学英文的码字机器13 小时前
破壁者:CANN ops-nn 仓库与昇腾 AI 算子优化的工程哲学
人工智能
晚霞的不甘13 小时前
CANN 编译器深度解析:TBE 自定义算子开发实战
人工智能·架构·开源·音视频