NLP最重要的编码方式--BPE

2023-07-16  本文已影响0人  不可能打工

今天想简单聊聊在自然语言处理领域用得比较多,像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单元对出现的频数最高?

显然一眼看过去,es这个相邻的subword单元对频繁出现,总共出现了6 + 7 + 4 = 17次。所以我们用es这个新的subword单元替换语料中出现过的es相邻单元对。像这样:

image.png
这里注意词表的变化哦。s这个subword单元没了,增加了一个es的subword单元。(词表加一减一,数量不变)

step 3. 好,接下来。我们再重复一次上述操作。基于最新的语料,哪个相邻的subword单元对出现频数最高?

又显然一下,est这个相邻的subword单元对频繁出现,总共出现了6 + 7 + 4 = 17次。所以我们用est这个新的subword单元替换语料中出现过的ess相邻单元对。像这样:

image.png

又要注意词表的变化哦。 est这两个subword单元没了,增加了一个est的subword单元。(词表加一减二,数量减一)

step 4. 来来来,别气馁。我们继续,又基于最新的语料,哪个相邻的subword单元对出现频数最高?

est</w>总共出现了 7 + 4 = 11次。所以我们用est</w>这个新的subword单元替换语料中出现过的est</w>相邻单元对。像这样:

image.png

再来关注一下词表的变化。词表增加了est</w>的subword单元,并且词表里面,estest</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单元

上一篇下一篇

猜你喜欢

热点阅读