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的训练过程很像,区别在于两点:
- Wordpiece不再使用出现最高频的pair,而是用下面的score来筛选每一个pair
python
score=(freq_of_pair)/(freq_of_first_element×freq_of_second_element)
- Wordpiece不是在结尾填充</w>,而是把中间字符前填充##,例如"word"这个词会被分割成w,##o,##r,##d
Tokenize过程
- wordpiece没有像BPE一样存self.merge,而是只存了self.vocab,每次都是最长匹配
- 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% 的 替换成以下内容:
- 这些 有 80% 的几率被替换成 [ ];
- 有 10% 的几率被替换成任意一个其他的 ;
- 有 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]。