BPE 全称 Byte Pair Encoding,字节对编码,是一种数据压缩方法。最早是论文 [1] 将其引入到 NLP 技术中。BPE 迭代地合并最频繁出现的字符或字符序列,具体步骤:举一个例子,有一个段文本““FloydHub is the fastest way to build, train and deploy deep learning models. Build deep learning models in the cloud. Train deep learning models.””
清洗去重,直到达到定义的令牌限制或设定的迭代次数(如我们的示例所示)在一次迭代之后,我们最频繁的字符对是“ d ”和“ e ”。因此,我们将这些结合起来创建了我们的第一个子词标记(不是单个字符)“ de ”。我们是如何计算的?如果你还记得我们之前计算的词频,你会发现“ de ”是最常见的配对。如果你把“ de ”出现的单词的频率加起来,你会得到 3 + 2 + 1 + 1 = 7,这就是我们新的“ de ”标记的频率。由于“ de ”是一个新token,我们需要重新计算所有标记的计数。我们通过从合并操作之前的单个字符的频率中减去新的“ de ”标记的频率 7 来实现这一点。如果我们考虑一下,这是有道理的。我们刚刚创建了一个新的token“ de ”。这在我们的数据集中出现了 7 次。现在我们只想计算“ d ”和“ e ”未配对时出现的次数。为此,我们从“ e”的原始出现频率中减去 7”,16,得到 9。我们从“ d ”的原始频率,12 中减去 7,得到 5,可以在“迭代 1”表中看到这一点。
def initialize_vocab(self, text): text = re.sub('\s+', ' ', text) all_words = text.split() vocab = {} for word in all_words: word = self.format_word(word) vocab[word] = vocab.get(word, 0) + 1 tokens = collections.Counter(text) return vocab, tokens
def get_bigram_counts(self, vocab): pairs = {} for word, count in vocab.items(): symbols = word.split() for i in range(len(symbols)-1): pair = (symbols[i], symbols[i+1]) pairs[pair] = pairs.get(pair, 0) + count return pairs
def merge_vocab(self, pair, vocab_in): vocab_out = {} bigram = re.escape(' '.join(pair)) p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)') bytepair = ''.join(pair) for word in vocab_in: w_out = p.sub(bytepair, word) vocab_out[w_out] = vocab_in[word] return vocab_out, (bigram, bytepair)
模型在训练中主要使用统计指标,比如出现的频率,左右连接度等,还有困惑度来训练最终的结果,论文题目为:《SentencePiece: A simple and language independent subword tokenizer
and detokenizer for Neural Text Processing》,地址为:https:///pdf/1808.06226.pdf
SentencePiece 的训练目标如下。我们希望最大化对数似然
其中x是 unigram 序列,S( x ) 表示所有可能序列的集合。同样,这些是隐藏变量,我们只看到未标记的语料库!为了解决这个问题,我们采用了 EM 类型的算法。如果熟悉 EM,你会注意到这些步骤实际上是倒退的,我们采用 ME 方法。尽管名字很花哨,但它实际上非常直观和直接。步骤是:
初始化一元概率。记住 P( x ) = P(x_1)…P(x_n) 所以一旦我们有了
unigrams,我们就有了任何序列的概率。在我们的代码中,我们只是使用 BPE 频率计数来更接近目标。
# train sentencepiece model from our blog corpus spm.SentencePieceTrainer.train('--model_type=bpe --input=blog_test.txt --model_prefix=bpe --vocab_size=500 --normalization_rule_tsv=normalization_rule.tsv')
训练完模型后,加载它就可以开始使用了!
# makes segmenter instance and loads the BPE model file (bpe.model) sp_bpe = spm.SentencePieceProcessor() sp_bpe.load('bpe.model')
3.2 训练 Unigram 模型
可以采用与 BPE 模型大致相同的方式训练 Unigram 模型。
# train sentencepiece model from our blog corpus spm.SentencePieceTrainer.train('--model_type=unigram --input=blog_test.txt --model_prefix=uni --vocab_size=500 --normalization_rule_tsv=normalization_rule.tsv')
# makes segmenter instance and loads the BPE model file (bpe.model) sp_uni = spm.SentencePieceProcessor() sp_uni.load('uni.model')
3.3 对比两种模型
可以通过调用“encode_as_pieces”函数使用训练好的子词模型对句子进行编码。我们对句子进行编码:“This is a test”。
print("BPE: {}".format(sp_bpe.encode_as_pieces('This is a test'))) print("UNI: {}".format(sp_uni.encode_as_pieces('This is a test')))
输出:
BPE: ['▁This', '▁is', '▁a', '▁t', 'est']
UNI: ['▁Thi', 's', '▁is', '▁a', '▁t', 'est']
3.4 查看所有的token
可以运行以下代码以查看完整词汇列表
vocabs = [sp_bpe.id_to_piece(id) for id in range(sp_bpe.get_piece_size())] bpe_tokens = sorted(vocabs, key=lambda x: len(x), reverse=True) bpe_tokens
#!/usr/bin/env python # -*- coding:utf-8 _*- """ @author:quincy qiang @license: Apache Licence @file: step4_merge_tokenizers.py @time: 2023/05/19 @contact: yanqiangmiffy@gamil.com @software: PyCharm @description: coding.. """ from sentencepiece import sentencepiece_model_pb2 as model
''' Merge tokenizer ''' orig_model_path = '/path/to/llama/tokenizer.model' belle_model_path = '/path/to/belle/belle.model' orig_m = model.ModelProto() belle_m = model.ModelProto() orig_m.ParseFromString(open(orig_model_path, "rb").read()) belle_m.ParseFromString(open(belle_model_path, "rb").read()) print(len(orig_m.pieces), len(belle_m.pieces)) orig_pieces = [] for piece in orig_m.pieces: orig_pieces.append(piece.piece) for piece in belle_m.pieces: if piece.piece not in orig_pieces: orig_m.pieces.append(piece) orig_pieces.append(piece.piece)
print(len(orig_m.pieces)) save_vocab_path = '/path/to/merge_tokenizer/tokenizer.model' with open(save_vocab_path, 'wb') as f: f.write(orig_m.SerializeToString())