字节对编码(Byte-Pair Encoding,简称BPE)最初是作为一种压缩文本的算法开发的,然后由OpenAI用于训练GPT模型时的标记化。它被许多Transformer模型广泛使用,包括GPT、GPT-2、RoBERTa、BART和DeBERTa。
Tokenizer
Tokenizer 是自然语言处理中的一个重要步骤,用于将连续的文本序列切分成一个个有含义的标记(tokens)。标记可以是单词、数字、符号或其他语言单位,它们作为构成文本的基本单元,可以被用于后续的文本处理任务。
BPE的优势
BPE 分词相对于传统的分词方法具有一些优势,这些优势使得它成为自然语言处理中常用的分词形式之一:
1. 子词处理:BPE 分词可以将单词拆分成更小的子词(subwords)。这种子词处理方法能够更好地处理未知词问题,对于罕见的或专有名词等在训练数据中出现较少的词汇有很好的覆盖。英文中会产生很多新词,如果用传统的词分段方法会产生很多未知Token,而BPE不会有这个问题。
2. 可变长度编码:与传统的固定长度编码(如单词级别的编码)相比,BPE 分词可以灵活地处理不同长度的词汇。这使得它适用于多种任务和语言,减少了在处理各种数据时需要重新训练分词器的开销。
3. 上下文相关性:由于 BPE 分词将单词切分为子词,它在保留了含义的同时保持了一定的上下文相关性。这有助于提高模型对于复杂语境和歧义性的处理能力。
4. 数据压缩技术:BPE 最初是作为一种文本压缩算法开发的,因此它可以通过合并高频子词来构建一个更小的词汇表,减少模型的参数量和存储空间。这在处理大规模数据时尤为重要。
训练算法
BPE 训练首先计算语料库中使用的唯一单词集(在标准化和预标记化步骤完成之后),然后通过使用用于书写这些单词的所有符号来构建词汇表。作为一个非常简单的例子,假设我们的语料库使用这五个单词:
"hug", "pug", "pun", "bun", "hugs"
基本词汇将是["b", "g", "h", "n", "p", "s", "u"]
。对于现实世界的情况,该基本词汇表将至少包含所有 ASCII 字符,也可能包含一些 Unicode 字符。如果您要标记的示例使用的字符不在训练语料库中,则该字符将转换为<unknown>未知标记。这就是为什么许多 NLP 模型在分析带有表情符号的内容方面表现不佳的原因之一。
有一个聪明的方法来处理这个问题:它们不将单词视为用 Unicode 字符编写的,而是用字节编写的。这样,基本词汇表的大小就很小(256),但您能想到的每个字符仍然会被包含在内,并且最终不会被转换为未知<unknown>标记。这个技巧称为字节级 BPE。
获得这个基本词汇后,我们通过学习合并添加新的 token,直到达到所需的词汇大小,合并是将现有词汇的两个元素合并为一个新词汇的规则。因此,一开始这些合并将创建具有两个字符的标记,然后随着训练的进行,创建更长的子词。
在分词器训练期间的任何步骤,BPE 算法都会搜索最常见的现有Token对(这里所说的“对”是指一个单词中的两个连续Token)。频率出现最高的Token对将会被合并,然后重复这个过程,知道词表到达一个预设的阈值。
回到我们之前的例子,我们假设这些单词有以下频率:
("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5)
该含义"hug"
在语料库中出现了 10 次、"pug"
5 次、"pun"
12 次、"bun"
4 次和"hugs"
5 次。我们通过将每个单词分割成字符(构成我们初始词汇的字符)来开始训练,这样我们就可以将每个单词视为Token列表:
("h" "u" "g", 10), ("p" "u" "g", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "u" "g" "s", 5)
然后我们看成对的。对("h", "u")
出现在单词"hug"和"hugs"
,因此在语料库中总共出现了 15 次。不过,这并不是出现频率最高的一对:("u", "g")
这一对Token,它出现在"hug"
、"pug"
、 和"hugs"
中,在词汇表中总共出现了 20 次。
因此,分词器学习到的第一个合并规则是("u", "g") -> "ug"
,这意味着"ug"
将被添加到词汇表中,并且该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)
Vocabulary是我们的词表,它记录了我们支持的所有Token,Corpus是语料以及出现的频率列表,语料会根据Vocabulary词表里面的Token进行切分。
现在,我们有一些对会导致标记长度超过两个字符:例如,对("h", "ug")
(在语料库中出现 15 次)。然而,此阶段最频繁的对("u", "n")
在语料库中出现了 16 次,因此学习的第二个合并规则是("u", "n") -> "un"
。将其添加到词汇表中并合并所有现有的事件会导致我们:
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)
现在最常见的对是("h", "ug")
,因此我们学习合并规则("h", "ug") -> "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)
我们可以持续这个合并过程,直到达到所需的词汇量。
这就是BPE的整个训练过程。
=========================================================
原文:NLP最重要的编码方式--BPE - 简书 (jianshu.com)
今天想简单聊聊在自然语言处理领域用得比较多,像BERT,GPT等自然语言模型都会用到的技术,BPE,全称是Byte Pair Encoding。
这个技术呢,在面试实习生过程中,发现其实很多学生不太能解释清楚,所以我打算自己也沉淀一下。
为啥要BPE编码?
现在的语言模型BERT,GPT,LLaMa等等,在预训练的时候都得tokenization。最简单的一种tokenization,就是把每个单词看成一个token,然后对训练语料进行编码,也就是用一个整数id代表这个token。
但是呢,就英语来说,通常都有几万,甚至几十万的单词。如果用上述的编码方式的话,自然语言模型在算softmax的时候,就得在一个几十万个单词列表上计算一个概率分布,那显然是相当费时。费时也就算了,有些token可能就不怎么常见,硬是这样算分布,模型也不会准。纯纯的事倍功半。
又譬如,现在的GPT,不仅仅能看懂英文,中文,日文这些都能懂。那随着集成的不同国家的语言越来越多,词汇表肯定会大到惊人。所以肯定得想一个能高效的减少token数量的方法。
没错,这个方法就是BPE。
BPE是怎么编码的?
一句话总结BPE。
它无非就是在反复迭代直到你想停为止,而每次迭代都在选取频数最高
的相邻subword单元对
合并成新的subword单元
。
实例是最好的老师。来,我们一起搞个例子。
假设有一份语料,经过统计之后呢,我们可以把这份语料表示成:
{
'estern': 6,
'widest': 7,
'longest': 4
}
step 1. 我们把单词拆分成字母,也就是说,一开始,每个字母就是一个subword单元,像这样:
image.png
这里有个小细节是,在将单词拆分成字母的时候,我在每个单词的最后都加上了</w>。这是为啥呢?主要是为了用它来表示中止,这样在解码的时候,看到这个符号,后续就可以用空格做个replace。
step 2. 我们现在盯着上述这个语料,看下哪个相邻的subword单元对
出现的频数最高?
显然一眼看过去,e
和s
这个相邻的subword单元对频繁出现,总共出现了6 + 7 + 4 = 17次。所以我们用es
这个新的subword单元替换语料中出现过的e
和s
相邻单元对。像这样:
image.png
这里注意词表的变化哦。s
这个subword单元没了,增加了一个es
的subword单元。(词表加一减一,数量不变)
step 3. 好,接下来。我们再重复一次上述操作。基于最新的语料,哪个相邻的subword单元对
出现频数最高?
又显然一下,es
和t
这个相邻的subword单元对频繁出现,总共出现了6 + 7 + 4 = 17次。所以我们用est
这个新的subword单元替换语料中出现过的es
和s
相邻单元对。像这样:
image.png
又要注意词表的变化哦。 es
和t
这两个subword单元没了,增加了一个est
的subword单元。(词表加一减二,数量减一)
step 4. 来来来,别气馁。我们继续,又基于最新的语料,哪个相邻的subword单元对
出现频数最高?
est
和</w>
总共出现了 7 + 4 = 11次。所以我们用est</w>
这个新的subword单元替换语料中出现过的est
和</w>
相邻单元对。像这样:
image.png
再来关注一下词表的变化。词表增加了est</w>
的subword单元,并且词表里面,est
和est</w>
同时存在,直观感受就是est</w>
就只会出现在后缀上,而est
可以出现在开头,也可以出现词中。(词表加一,数量加一)
step 5. 继续像上面不停的迭代,直到达到预设的subword词表大小(或者下一个最高频出现的单元对的频数是1)
我们回顾一下,我们刚才在干啥?
我们每一步都在问自己,当前的语料中哪个相邻的subword单元对
出现得最频繁?找到这样的单元对后,我们将这个单元对合并作为一个新的subword单元,并且替换语料中相应的相邻单元对。
那是不是一句话就能概括?反复迭代直到你想停为止,而每次迭代都在选取出现频数最高
的相邻subword单元对
合并成新的subword单元
另外,细心观察上述整个过程,会发现词表的大小在每次迭代的时候可能不变,可能增加,也可能减少。
实际上,随着合并的次数增加,词表大小通常是先增加后减少。
为啥呢?可能是人类语言发展的特点吧,有些字母之间本身就是会固定搭配。中文也是一样,像魑魅魍魉
,饕餮
这些词,语料中基本不会单独出现饕
,也不会单独出现餮
。正因为有这种语言现象,BPE才能起到缩减词表的作用。
用BPE编码得到了词表后,怎么用呢?
使用BPE编码得到的词表,无非就是弄懂怎么编码,怎么解码。
编码过程,有种最长字符串匹配的意味。具体来说:
编码
step 1. 将词表中的subword单元,按照长度从长到短进行排序;
step 2. 对于一个待编码的单词,遍历step 1中排好序的词表的每个subword单元,
看看这个subword单元是不是待编码单词的子字符串。
- 如果不是,那continue。
- 如果是,这个subword单元是最终编码的一部分;
- 然后待编码单词去掉subword部分,对剩余的单词字符串继续再重新遍历一次词表。
step 3. 如果遍历完整个词表,还有子字符串没有匹配,那就把剩余字符串替换成<unk>。
step 4. 最终待编码的单词,就表示成上述过程中找到的subword的组合。
好的,别说你们。我描述完上述过程,我都觉得很绕。
还是那句,实例是最好的老师。我们来搞个例子:
# 待编码单词:
'highest</w>'
# 按长度排好序的subword词表
['est</w>', 'hi', 'g', 'h']
image.png
所以最终highest
这个单词就表示成[est</w>
, hi
, g
, h
]
编码的复杂度还是挺高的,实际实现中会增加cache。
解码过程,就相对来说很好理解。具体来说:
# 解码
如果相邻subword中间没有</w>中止符,就将两个subword直接拼接。
如果有</w>,就用空格seperate。
# 编码序列
['wedd', 'ing</w>', 'party']
# 最终解码序列
"wedding party"
中文怎么处理呢?
好了。上面说了一通,都在说英文怎么用BPE。那中文呢?毕竟现在的大语言模型基本还是外国友人搞得牛逼一些。我们如果想用中文语料搞个中文的大语言模型的话,怎么用BPE呢?
其实BPE是个通用方法,本质上就是定义好初始的subword单元,然后按照频数,不停合并成常见的subword单元。
对于中文来说,我们完全可以把每个汉字看成初始的subword单元,直接套用BPE就行。
而我这里想说的是,当前大多数GPT模型,都不是以汉字作为subword初始单元来进行BPE。他们定义的初始单元是byte,这样做的好处是可以避免OOV,也能兼顾各种语言符号,这也就是大家听到的Byte-Level BPE
。
好了。我也是简单聊聊BPE,可能有些细节也是没聊到的。Anyway,遇到细节问题的时候再研究吧。
那BPE是啥?
一句话概括:反复迭代直到你想停为止,而每次迭代都在选取频数最高
的相邻subword单元对
合并成新的subword单元
作者:不可能打工
链接:https://www.jianshu.com/p/283ea050faa8
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。