今天想简单聊聊在自然语言处理领域用得比较多,像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单元,像这样:
这里有个小细节是,在将单词拆分成字母的时候,我在每个单词的最后都加上了</w>。这是为啥呢?主要是为了用它来表示中止,这样在解码的时候,看到这个符号,后续就可以用空格做个replace。
step 2. 我们现在盯着上述这个语料,看下哪个相邻的subword单元对
出现的频数最高?
显然一眼看过去,e
和s
这个相邻的subword单元对频繁出现,总共出现了6 + 7 + 4 = 17次。所以我们用es
这个新的subword单元替换语料中出现过的e
和s
相邻单元对。像这样:
这里注意词表的变化哦。
s
这个subword单元没了,增加了一个es
的subword单元。(词表加一减一,数量不变)
step 3. 好,接下来。我们再重复一次上述操作。基于最新的语料,哪个相邻的subword单元对
出现频数最高?
又显然一下,es
和t
这个相邻的subword单元对频繁出现,总共出现了6 + 7 + 4 = 17次。所以我们用est
这个新的subword单元替换语料中出现过的es
和s
相邻单元对。像这样:
又要注意词表的变化哦。 es
和t
这两个subword单元没了,增加了一个est
的subword单元。(词表加一减二,数量减一)
step 4. 来来来,别气馁。我们继续,又基于最新的语料,哪个相邻的subword单元对
出现频数最高?
est
和</w>
总共出现了 7 + 4 = 11次。所以我们用est</w>
这个新的subword单元替换语料中出现过的est
和</w>
相邻单元对。像这样:
再来关注一下词表的变化。词表增加了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']
所以最终
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单元