request-free-img

大语言模型中BPE分词技术

大语言模型如何“读懂”你的输入?

你是否好奇过,当你在ChatGPT中输入一段文字时,大模型是怎么‘读懂’这句话的呢?

我们经常所说的大语言模型的TOKEN又是什么呢?

今天这个视频,我们一起来了解大语言模型如何理解你输入句子,以及如何将你输入的文本转化为可计算的token序列的。也就是分词技术。

传统分词方法及其问题

说到分词技术,相信大家都知道一种最基本的分词方法, 也就是把每一个单词单独分词,例如,句子“I love to eat apples”会被分词为5个单词:[“I”, “love”, “to”, “eat”, “apples”]。这种分词方式十分符合人类语言习惯,因为人类语言也经常以单词为单位进行交流,然后这这种分词方式存在非常多的问题。

但是以英语为例《牛津英语词典》收录了约60万个单词,新的单词还在不断的增加,另外还有拼写错误词、缩写词,这会导致词汇表过大,并且也无法处理OOV问题。

还有人提出了另一种分词方法,基于字符的分词(Character-based Tokenization),它把单词中的每个字符和符号都提取出来作为独立token。

虽然能解决 OOV 问题,也避免了大词汇量,但缺点也太明显了,粒度太细,训练花费的成本太高。

BPE:折中的优秀方案

因此我们需要一个更折中的方案,这种分词方法应该有着较小的词汇表,它能有效的处理未登录词,同时它还能不依赖特定语言的语法或词典,能适用于多种语言。

Byte-Pair Encoding(字节对编码,简称 BPE)正是这样一种分词技术。

BPE的历史与演变

BPE 最早作为一种数据压缩算法由 Philip Gage 于 1994 年提出《A New Algorithm for Data Compression》。

其核心思想是: 将输入文本视为字节序列(每个字节对应 0-255 的值,基于 ASCII 或扩展 ASCII)。然后统计相邻字节对的频次,合并最常见字节对为新符号,重复此过程以构建压缩表。从而实现文本压缩。

2016 年,Rico Sennrich 等人在神经机器翻译中首次将 BPE 应用于分词,用于解决词汇表过大和未登录词(OOV)问题。 之后BPE 被正式应用于 NLP领域,并成为GPT的标准分词方法。

值得一提的是BPE虽然叫:Byte-Pair Encoding(字节对编码)它在被Philip Gage提出时也的确是使用字节对进行编码的,但在被Rico Sennrich 等人引入NLP中时,则使用的是统计字符对(例如 (lo))出现的频次。

这是因为早期NLP模型只会依赖较小的语料库,并且通常会对文本进行预分词(pre-tokenization),例如英文基于空格,中文基于词典的方式分词,字符级处理更直观。

而字节级 BPE 需要处理更长的字节序列(尤其对于像中文这样的多字节字符),这大大增加计算成本,且当时硬件(GPU/TPU)能力有限。无法处理这种大规模的计算量。

BPE的实现过程详解

接下来,我们就一起来了解Byte-Pair Encoding字节对编码,简称 BPE的实现过程。

假设我们有这样一段原始语料:”low lower newest widest”

首先,原始语料会被进行预处理,对于像英语这种有明确分隔的语言,预处理很简单,它会按空格或标点符号分割并添加词的边界标记,在实际处理中通常会加上</w>。

预处理后会变成这样:

low</w>
lower</w>
newest</w>
widest</w>

为什么要在每个单词后边添加边界符号呢?词边界标记不仅避免跨词合并,还能帮助模型区分完整单词和子词单元,增强分词的语义一致性。

有人可能要问,那不添加行不行,当然行,只是那样导致出现语义无关的子词单元,破坏单词的完整性。

例如我们的单词low的结尾字符w和lower的开头字符l合并,产生wl,如果对它进行频率统计,可能生成大量无意义的子词单元。

初始字符拆分与词汇表构建

我们继续,接下来会对语料库按字符拆分,变成这样:

l o w</w>
l o w e r</w>
n e w e s t</w>
w i d e s t</w>

然后分词器会扫描语料,提取所有唯一字符(包括特殊标记), 从而形成初始词汇表:

{‘l’, ‘o’, ‘w’, ‘e’, ‘r’, ‘n’, ‘s’, ‘t’, ‘i’, ‘d’, ‘</w>’}

迭代合并过程

紧接着针对所有相邻字符进行频次统计,(‘lo’)组成相邻字符,(‘ow’)组成一对, (‘w</w>’)组成一对。

第2个单词的(‘lo’)组成一对,(‘ow’)组成一对,以此类推,分别将语料库所有相邻字符组成对并统计它们出现的频次。

lo ow w</w>
lo ow we er r</w>
ne ew we es st t</w>
wi id de es st t</w>

经过第1轮对相邻字符的统计,我们得知出现频次最高的是lo, ow, we, es, st, t</w> 都是2次。 如果出现频次相同时,BPE通常按照字典序选择第一个字节对,以确保合并规则的确定性和一致性。

(‘lo’): 2
(‘ow’): 2
(‘we’): 2
(‘es’): 2
(‘st’): 2
(‘t</w>’): 2

那么我们这里就选择es字符对,我们把它加入词汇表。

{‘l’, ‘o’, ‘w’, ‘e’, ‘r’, ‘n’, ‘s’, ‘t’, ‘i’, ‘d’, ‘</w>’, ‘es’ }

同时会更新词料库的分词状态,将es合并:

l o w</w>: 1
l o w e r</w>: 1
n e w es t</w>: 1
w i d es t</w>: 1

再继续第二轮相邻字符对的频次统计,此时es会被认作是一个字符和前后组成字符对,最终形成这样:

lo ow w</w>
lo ow we er r</w>
ne ew wes est t</w>
wi id des est t</w>

并且得出各字符对的频次分别如下:

(‘lo’): 2
(‘ow’): 2
(‘we’): 2
(‘est’): 2
(‘t</w>’): 2

这些字符对都出现了2次,所以仍按字典顺序的话我们选择est,并把它加入词汇表。

{‘l’, ‘o’, ‘w’, ‘e’, ‘r’, ‘n’, ‘s’, ‘t’, ‘i’, ‘d’, ‘</w>’, ‘es’,’est’ }

然后将est字符合并变成这样:

l o w</w>
l o w e r</w>
n e w est</w>
w i d est</w>

再继续第三轮相邻字符对的频次统计,此时est会被认作是一个字符。

再次统计出各相邻字符对的频次最高的是这些:

(‘lo’): 2
(‘ow’): 2
(‘est</w>’): 2

继续将est</w>加入词汇表。

{‘l’, ‘o’, ‘w’, ‘e’, ‘r’, ‘n’, ‘s’, ‘t’, ‘i’, ‘d’, ‘</w>’, ‘es’,’est’,’est</w>’ }

之后继续迭代…

第4轮可能合并(‘l’, ‘o’) → 变成 ‘lo’
第5轮可能合并(‘lo’, ‘w’) →变成 ‘low’

直到达到预设的词汇表大小(例如GPT3的词汇表大小大约是50257个)。

最终词汇表与Token概念

最终便形成我们的词汇表,也就是我们常说的token集合:

{
    # 原始字符
    'l', 'o', 'w', 'e', 'r', 'n', 's', 't', 'i', 'd', '</w>',
    # 学到的子词
    'es','est','est</w>','lo', 'low'...
}

Token是模型能够理解的最小语言单位, 它可以是:字符、子词、完整单词、标点符号等。

处理新词:lowest的分词示例

现在假设我们想用训练好的词汇表来处理一个我们的分词器从示见过的新文本”lowest”:

分词器分采用贪心匹配原则,从左到右扫描词汇表中的token集合,查找词汇表中最长的匹配 token,将其添加到结果token序列中,重复此过程直到整个序列被分词。

对于lowest单词,先会找到low, 再找到est。

最终这个单词被分成这样的token序列:[‘low’, ‘est</w>’]

这样就成功将未见过的单词”lowest”分解为已知的子词!

中文场景下的BPE应用

对于中文字符,假设我们有语料 “我爱吃苹果,他爱吃香蕉” ,并且我们没有采用预分词技术,而是直接按字符分割成这样:

[我 爱 吃 苹 果 ,他 爱 吃 香 蕉]

并提取所有唯一字符, 从而形成初始词汇表:

{我 爱 吃 苹 果 ,他 香 蕉}

然后分别统计相邻字符的出现频次。

第一轮统计后,得到‘爱吃’出现的频次最高,于是将‘爱吃’添加进词汇表。

之后就像我们前边所介绍的方法继续迭代,最终形成我们的词汇表。

中文分词面临的挑战

但是细心的你应该发现了,对于中文,《汉语大字典》收录了约56,000个汉字,加上其他语言的字符以及标点符号,数量会变得更庞大,而在大语言模型中,训练的语料会非常大,那么以上方法构建的初始词汇表可能就会包含数万到十万以上的字符。

再加上之后训练结果所添加的词汇,这将会是一个非常庞大的词汇表。

词汇表过大不仅会导致训练模型训练的成本变得非常高,也会带来很多性能、效率问题。

这还只是中文字符遇到的问题,再加上韩文,日文等其它国家语言文字,这使得大模型的分词技术变得更复杂。

因此早期实现中,通常使用如Jieba这样的分词工具对中文进行预分词,以减少初始词汇表的规模。

字节级BPE:GPT-2的重要创新

于是,2019 年 OpenAI 在 GPT-2 论文《Language Models are Unsupervised Multitask Learners》重 新引入byte level BPE,字节级BPE。

字节级BPE会将整个训练语料视为 UTF-8 编码的字节序列。 UTF-8 是 Unicode 的标准编码方式,每个字符可能对应 1-4 个字节(例如,英文字符如 ‘a’ 的UTF8编码是0x61,中文字符’中’ 的UTF8编码是: e4 b8 ad )。

字节级 BPE 选择字节作为最小单位,也就是说基本词汇表只会包含 256 个字节值。 就能确保任何输入数据(包括文本、符号甚至非文本数据)都能被表示为字节序列,而无需额外的假设或限制。

字节级BPE工作示例

现在假设我们的语料是英文短语 “hello world”。首先转为UTF-8 字节序列为:

[104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]

并构建初始词汇表。

然后和我们前边的规则一样,遍历整个语料,统计所有相邻字节对的出现次数。

在这个小语料中,所有字节对的频次均为 1 次。

这里就涉及到一个字节级 BPE 的词汇表构建逻辑:

首先初始词汇表包含所有可能的 256 个字节值(也就是0 到 255),分别映射到 token ID 0 到 255,然后每次合并字节对时,会创建一个新 token,并为其分配一个新的 ID。

ID 的分配通常自动加 1,依次递增: 也就是256,257….

最终形成词汇表vocab.json文件。

同时它还会维护一个合并规则列表(merge list)。这个列表记录这些合并细节。

最终形成 合并规则列表文件 merges.txt。

GPT系列Tokenizer核心文件

在GPT2 的字节级 BPE(Byte-Level Byte Pair Encoding)实现中 “vocab.json” 和 “merges.txt”是 tokenizer 的核心文件,分别存储了词汇表和合并规则,用于将文本转为 token ID 序列或者将 token ID 序列转回文本。

字节级BPE的优缺点

字节级 BPE(Byte-Level Byte Pair Encoding)是一种在现代自然语言处理(NLP)中广泛使用的折中分词方法,特别是像 GPT-2、GPT-3 和 LLaMA)这样的大型语言模型。

然而它并非“最理想”的分词方法,它存在语义碎片化,依赖大规模语料,在小语料或低资源语言中,合并规则可能不充分,导致 tokenization 不够优化,以及语言偏见问题。

核心参考论文


更多问题探讨,请关注公众号:程序员角