BPE、Wordpiece、Unigram、SpanBERT等Tokenizer细节总结

发布于:2024-04-28 ⋅ 阅读:(23) ⋅ 点赞:(0)

BPE(Byte Pair Encoding)

GPT-2和Roberta用的是这种,不会产生[UNK]这个unknown字符

这部分部分摘录自https://martinlwx.github.io/zh-cn/the-bpe-tokenizer/

看以下code例子就足够理解了,核心是维护self.merges(维护一个pair->str的字典)和self.vocab(每次挑最高频的pair加入self.vocab)做训练,每次刷新一遍最新的self.splits,具体格式参考注释:

from collections import defaultdict, Counter
from pprint import pprint
from typing import List

class BPE:
    def __init__(self, corpus: List[str], vocab_size: int, max_iter: int, debug: bool, ):
        self.corpus = corpus
        self.vocab_size = vocab_size
        self.vocab = []
        self.word_freq = Counter()
        self.splits = {}  # 格式:highest: [high, est</w>]
        self.merges = {}  # 格式:[high, est</w>]: highest
        self.max_iter = max_iter
        self.debug = debug

    def train(self):
        """Train a BPE Tokenizer"""
        # count the word frequency
        for document in self.corpus:
            words = document.split() #按照空格等whitespae进行split
            self.word_freq += Counter(words)

        # initialize the self.splits
        for word in self.word_freq:
            self.splits[word] = list(word) + ["</w>"]
        if self.debug:
            print(f"Init splits: {self.splits}")
        alphabet = set()
        for word in self.word_freq:
            alphabet |= set(list(word))
        alphabet.add("</w>")
        self.vocab = list(alphabet)
        self.vocab.sort()

        cnt = 0
        while len(self.vocab) < self.vocab_size:
            if self.max_iter and cnt >= self.max_iter:
                break
            pair_freq = self.get_pairs_freq() #格式为 {('a','b'):3,('c','d'),5}
            if len(pair_freq) == 0:
                print("No pair available")
                break
            pair = max(pair_freq, key=pair_freq.get) #输出值最大的key
            self.update_splits(pair[0], pair[1])
            if self.debug:
                print(f"Updated splits: {self.splits}")
            self.merges[pair] = pair[0] + pair[1]
            self.vocab.append(pair[0] + pair[1])
            if self.debug:
                print(f"Most frequent pair({max(pair_freq.values())} times) "
                    f"is : {pair[0]}, {pair[1]}. Vocab size: {len(self.vocab)}"
                )
            cnt += 1

    def update_splits(self, lhs: str, rhs: str):
        """If we see lhs and rhs appear consecutively, we merge them"""
        for word, word_split in self.splits.items():
            new_split = []
            cursor = 0
            while cursor < len(word_split):
                if (word_split[cursor] == lhs and cursor + 1 < len(word_split) and word_split[cursor + 1] == rhs):
                    new_split.append(lhs + rhs)
                    cursor += 2
                else:
                    new_split.append(word_split[cursor])
                    cursor += 1
            self.splits[word] = new_split

    def get_pairs_freq(self) -> dict:
        """Compute the pair frequency"""
        pairs_freq = defaultdict(int)
        for word, freq in self.word_freq.items():
            split = self.splits[word]
            for i in range(len(split)):
                if i + 1 < len(split):
                    pairs_freq[(split[i], split[i + 1])] += freq
        return pairs_freq

    def tokenize(self, s: str) -> List[str]:
        splits = [list(t) + ["</w>"] for t in s.split()]
        for lhs, rhs in self.merges:
            for idx, split in enumerate(splits):
                new_split = []
                cursor = 0
                while cursor < len(split):
                    if (cursor + 1 < len(split) and split[cursor] == lhs and split[cursor + 1] == rhs):
                        new_split.append(lhs + rhs)
                        cursor += 2
                    else:
                        new_split.append(split[cursor])
                        cursor += 1
                assert "".join(new_split) == "".join(split)
                splits[idx] = new_split
        # splits是二维数组,最终拼成一维
        return sum(splits, [])

corpus = ["highest", "higher", "lower", "lowest", "cooler", "coolest"]

bpe = BPE(corpus, vocab_size=17, debug=True, max_iter=100)
bpe.train()
print('---------------output of tokenize---------------')
print(bpe.tokenize(" ". join(corpus)))
'''
Init splits: {'highest': ['h', 'i', 'g', 'h', 'e', 's', 't', '</w>'], 'higher': ['h', 'i', 'g', 'h', 'e', 'r', '</w>'], 'lower': ['l', 'o', 'w', 'e', 'r', '</w>'], 'lowest': ['l', 'o', 'w', 'e', 's', 't', '</w>'], 'cooler': ['c', 'o', 'o', 'l', 'e', 'r', '</w>'], 'coolest': ['c', 'o', 'o', 'l', 'e', 's', 't', '</w>']}
Updated splits: {'highest': ['h', 'i', 'g', 'h', 'es', 't', '</w>'], 'higher': ['h', 'i', 'g', 'h', 'e', 'r', '</w>'], 'lower': ['l', 'o', 'w', 'e', 'r', '</w>'], 'lowest': ['l', 'o', 'w', 'es', 't', '</w>'], 'cooler': ['c', 'o', 'o', 'l', 'e', 'r', '</w>'], 'coolest': ['c', 'o', 'o', 'l', 'es', 't', '</w>']}
Most frequent pair(3 times) is : e, s. Vocab size: 13
Updated splits: {'highest': ['h', 'i', 'g', 'h', 'est', '</w>'], 'higher': ['h', 'i', 'g', 'h', 'e', 'r', '</w>'], 'lower': ['l', 'o', 'w', 'e', 'r', '</w>'], 'lowest': ['l', 'o', 'w', 'est', '</w>'], 'cooler': ['c', 'o', 'o', 'l', 'e', 'r', '</w>'], 'coolest': ['c', 'o', 'o', 'l', 'est', '</w>']}
Most frequent pair(3 times) is : es, t. Vocab size: 14
Updated splits: {'highest': ['h', 'i', 'g', 'h', 'est</w>'], 'higher': ['h', 'i', 'g', 'h', 'e', 'r', '</w>'], 'lower': ['l', 'o', 'w', 'e', 'r', '</w>'], 'lowest': ['l', 'o', 'w', 'est</w>'], 'cooler': ['c', 'o', 'o', 'l', 'e', 'r', '</w>'], 'coolest': ['c', 'o', 'o', 'l', 'est</w>']}
Most frequent pair(3 times) is : est, </w>. Vocab size: 15
Updated splits: {'highest': ['h', 'i', 'g', 'h', 'est</w>'], 'higher': ['h', 'i', 'g', 'h', 'er', '</w>'], 'lower': ['l', 'o', 'w', 'er', '</w>'], 'lowest': ['l', 'o', 'w', 'est</w>'], 'cooler': ['c', 'o', 'o', 'l', 'er', '</w>'], 'coolest': ['c', 'o', 'o', 'l', 'est</w>']}
Most frequent pair(3 times) is : e, r. Vocab size: 16
Updated splits: {'highest': ['h', 'i', 'g', 'h', 'est</w>'], 'higher': ['h', 'i', 'g', 'h', 'er</w>'], 'lower': ['l', 'o', 'w', 'er</w>'], 'lowest': ['l', 'o', 'w', 'est</w>'], 'cooler': ['c', 'o', 'o', 'l', 'er</w>'], 'coolest': ['c', 'o', 'o', 'l', 'est</w>']}
Most frequent pair(3 times) is : er, </w>. Vocab size: 17
['h', 'i', 'g', 'h', 'est</w>', 'h', 'i', 'g', 'h', 'er</w>', 'l', 'o', 'w', 'er</w>', 'l', 'o', 'w', 'est</w>', 'c', 'o', 'o', 'l', 'er</w>', 'c', 'o', 'o', 'l', 'est</w>']
'''

Wordpiece

这部分摘录自huggingface的教程 https://huggingface.co/learn/nlp-course/chapter6/6?fw=pt,Bert,DistilBERT, MobileBERT用的是这种

训练过程

Wordpiece和BPE的训练过程很像,区别在于两点:

  1. Wordpiece不再使用出现最高频的pair,而是用下面的score来筛选每一个pair
score=(freq_of_pair)/(freq_of_first_element×freq_of_second_element)
  1. Wordpiece不是在结尾填充</w>,而是把中间字符前填充##,例如“word”这个词会被分割成w,##o,##r,##d

Tokenize过程

  1. wordpiece没有像BPE一样存self.merge,而是只存了self.vocab,每次都是最长匹配
  2. wordpiece会给填充[UNK]的token,同时还有"[PAD]", “[UNK]”, “[CLS]”, “[SEP]”, "[MASK]"这些特殊token

Unigram

这部分摘录自https://huggingface.co/learn/nlp-course/chapter6/7?fw=pt,Unigram的基本思路用下面例子比较明显,其实就是把句子理解成了unigram的language model
在这里插入图片描述

HuggingFace的tokenizer梳理

这部分摘录自https://huggingface.co/docs/tokenizers/components#models,HF的Tokenizer分为以下几个components:
在这里插入图片描述

  • Normalization: 比如unicode转换、大小写转换
  • Pre-tokenizers:作用是splitting the input into words,比如ByteLevel, this technique as been introduced by OpenAI with GPT-2, a tokenizer using this only requires 256 characters as initial alphabet (the number of values a byte can have), as opposed to the 130,000+ Unicode characters.
  • Models:WordLevel、BPE、WordPiece和Unigram
  • post-processing:adding the special tokens of the tokenizer, generating the attention mask and token type IDs

Tokenizer的mask策略

部分转载自https://zhuanlan.zhihu.com/p/360982134

静态mask

输入时,随机遮盖或替换一句话里面任意字或词, 然后让模型通过上下文的理解预测那一个被遮盖或替换的部分, 之后做 的时候只计算被遮盖部分的 。

随机把一句话中 15% 的 替换成以下内容:

  1. 这些 有 80% 的几率被替换成 [ ];
  2. 有 10% 的几率被替换成任意一个其他的 ;
  3. 有 10% 的几率原封不动。

动态mask

RoBERTa中引入了动态mask的策略,原论文中将原始数据复制n份,每份都进行随机的静态mask,从而每份数据的mask结果都不太一样。huggingface中data collator使用的是动态mask,但不是复制数据,而是每一个epoch的mask策略都不同,这样就可以达到动态mask的效果了,从而使得每一个epoch的mask的情况都不同,更方便更胜内存。

whole word mask (wwm)和ernie

对于原始的 BERT,训练时,会随机选取整句中的最小输入单元 token 来进行遮盖。因为用到 Byte Pair Encoding (BPE)技术,所以也可以把这些最小单元当作是子词(subword),比如说superman,分成 super+man 两个子词。

但这样会让本来应该有强相关的一些连在一起的字词,在训练时是割裂开来的。

因此我们就会想到,那能不能遮盖掉这样连在一起的片段训练呢?当然可以。

首先想到的做法,既然现在遮盖子词,那能不能直接遮盖整个词,比如说对于 super + man,只要遮盖就两个同时遮盖掉,这便是 Google 放出的 BERT WWM 模型所做的。

ERNIE类似的思路做了一些改进:
– Basic-Level Masking: 跟bert一样对单字进行mask,很难学习到高层次的语义信息;
– Phrase-Level Masking: 输入仍然是单字级别的,mask连续短语;
– Entity-Level Masking:首先进行实体识别,然后将识别出的实体进行mask。

n-gram mask

使用n-gram(uni-gram,bi-gram, tri-gram)来做MLM任务,,即以不同的概率使用n-gram,其中 uni-gram的概率最大,bi-gram其次,tri-gram概率最小。和下面的span有一些类似。

random span mask

在这里插入图片描述

来自于论文SpanBERT: Improving Pre-training by Representing and Predicting Spans
大体过程是,对一个句子X = (x1, x2, . . . , xn), 我们选取它的一个子序列(span)进行mask。通过不断地选取span,直到选够了X中15%的token。选取的方法是,首先通过几何分布选取span的长度L,(均匀分布采样应该大家都比较熟悉,对于不均匀的分布进行采样,简单的方式是将概率进行展平然后转化为均匀分布采样的问题,例如 0.6,0.3,0.2,0.1这样的分布,可以统一切割为6,3,2,1个0.1,然后均匀采样即可,当然按照0.01之类的来进行切割也是可以的)然后,均匀随机地选取span的起始位置。选取长度时,官方的设置是 L ∼Geo(0.2),同时裁剪L使Lmax=10,于是span长度的分布如下,平均值为3.8。span masking,指的是对span中的每一个token都替换成[MASK]。


网站公告

今日签到

点亮在社区的每一天
去签到