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

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,具体格式参考注释:

python 复制代码
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
python 复制代码
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]。

相关推荐
AngelPP3 小时前
OpenClaw 架构深度解析:如何把 AI 助手搬到你的个人设备上
人工智能
宅小年3 小时前
Claude Code 换成了Kimi K2.5后,我再也回不去了
人工智能·ai编程·claude
九狼3 小时前
Flutter URL Scheme 跨平台跳转
人工智能·flutter·github
ZFSS3 小时前
Kimi Chat Completion API 申请及使用
前端·人工智能
天翼云开发者社区4 小时前
春节复工福利就位!天翼云息壤2500万Tokens免费送,全品类大模型一键畅玩!
人工智能·算力服务·息壤
知识浅谈5 小时前
教你如何用 Gemini 将课本图片一键转为精美 PPT
人工智能
Ray Liang5 小时前
被低估的量化版模型,小身材也能干大事
人工智能·ai·ai助手·mindx
shengjk16 小时前
NanoClaw 深度剖析:一个"AI 原生"架构的个人助手是如何运转的?
人工智能
西门老铁8 小时前
🦞OpenClaw 让 MacMini 脱销了,而我拿出了6年陈的安卓机
人工智能