如果你有一些NLP的经验,你可能知道标记化是任何NLP管道的舵手。
标记化通常被认为是NLP的一个子领域,但它有自己的演变故事。现在,它支撑着许多最先进的NLP模型。
这篇文章是关于通过利用Hugging Face的标记化包
从头开始训练标记化**。**
在我们进入训练和比较不同标记器的有趣部分之前,我想给你一个关于算法之间关键差异的简要总结。
主要区别在于选择
要合并的字符对
,以及每个算法用来生成最终标记集的合并策略
。
BPE算法–基于频率的模型
字节对编码使用子字模式的频率来筛选它们进行合并。
使用频率作为驱动因素的缺点是,你最终可能会有模糊的最终编码,可能对新的输入文本没有用处。
但它在生成无歧义的标记方面仍有改进的余地。
Unigram算法–一个基于概率的模型
接下来我们有一个Unigram模型,它通过计算每个子词组合的可能性来解决合并问题,而不是挑选最频繁的模式。
它计算每个子词标记的概率,然后根据本研究论文中解释的损失函数将其删除。
基于损失值的某个阈值,你就可以触发该模型来丢弃底部20-30%的子词标记。
Unigram是一种完全的概率算法,它在每次迭代中都会根据概率选择字符对和最终决定合并(或不合并)。
WordPiece算法
随着2018年BERT的发布,出现了一种新的子词标记化算法,称为WordPiece,可以认为是BPE和Unigram算法的中介。
WordPiece也是一种贪婪的算法,它利用可能性而不是计数频率来合并每个迭代中的最佳配对,但选择配对的字符是基于计数频率的。
因此,在选择字符配对方面,它与BPE相似,在选择最佳配对合并方面,与Unigram相似。
在涵盖了算法差异的情况下,我试图实现这些算法中的每一种(不是从头开始),以比较它们各自产生的输出。
如何训练BPE、Unigram和WordPiece算法
现在,为了对输出进行无偏见的比较,我不想使用预先训练好的算法,因为这将把数据集的大小、质量和内容带入画面。
一种方法可以是使用研究论文从头开始编码这些算法,然后对其进行测试。这是一个很好的方法,以便真正理解每个算法的工作原理,但你可能最终会花几周时间来做这件事。
相反,我使用了Hugging Face的标记器包,它提供了当今所有最常用的标记器的实现。它还允许我在我选择的数据集上从头开始训练这些模型,然后将我自己选择的输入字符串标记化。
如何训练数据集
我选择了两个不同的数据集来训练这些模型。一个是来自古腾堡的免费书籍,作为一个小型数据集,另一个是wikitext-103,这是一个516M的文本。
在Colab中,你可以先下载数据集并解压(如果需要)。
!wget http://www.gutenberg.org/cache/epub/16457/pg16457.txt
!wget https://s3.amazonaws.com/research.metamind.io/wikitext/wikitext-103-raw-v1.zip
!unzip wikitext-103-raw-v1.zip
导入所需的模型和培训师
翻阅文档,你会发现该包的主要API是类Tokenizer.
然后你可以用你选择的模型(BPE/ Unigram/ WordPiece)来实例化任何标记器。
在这里,我导入了主类,所有我想测试的模型,以及它们的训练器,因为我想从头开始训练这些模型。
## importing the tokenizer and subword BPE trainer from tokenizers import Tokenizer from tokenizers.models import BPE, Unigram, WordLevel, WordPiece from tokenizers.trainers import BpeTrainer, WordLevelTrainer, \ WordPieceTrainer, UnigramTrainer ## a pretokenizer to segment the text into words from tokenizers.pre_tokenizers import Whitespace
如何实现训练和标记化的自动化
由于我需要对三个不同的模型执行有点类似的过程,我把这些过程分成3个函数。我只需要为每个模型调用这些函数,我在这里的工作就完成了。
那幺,这些函数是什幺呢?
第1步 – 准备标记器
准备标记器需要我们用一个我们选择的模型来实例化标记器类。但是,由于我们有四个模型(我也添加了一个简单的词级算法)要测试,我们将写if/else案例来用正确的模型实例化标记器。
为了在小数据集和大数据集上训练实例化的标记器,我们还需要实例化一个训练器,在我们的例子中,这些将是BpeTrainer
, WordLevelTrainer, WordPieceTrainer, and UnigramTrainer.
实例化和训练将需要我们指定一些特殊的标记。这些是未知词的标记和其他特殊的标记,我们以后需要使用这些标记来增加我们的词汇量。
你也可以在这里指定其他训练参数的词汇量大小或最小频率。
unk_token = "<UNK>" # token for unknown words spl_tokens = ["<UNK>", "<SEP>", "<MASK>", "<CLS>"] # special tokens def prepare_tokenizer_trainer(alg): """ Prepares the tokenizer and trainer with unknown & special tokens. """ if alg == 'BPE': tokenizer = Tokenizer(BPE(unk_token = unk_token)) trainer = BpeTrainer(special_tokens = spl_tokens) elif alg == 'UNI': tokenizer = Tokenizer(Unigram()) trainer = UnigramTrainer(unk_token= unk_token, special_tokens = spl_tokens) elif alg == 'WPC': tokenizer = Tokenizer(WordPiece(unk_token = unk_token)) trainer = WordPieceTrainer(special_tokens = spl_tokens) else: tokenizer = Tokenizer(WordLevel(unk_token = unk_token)) trainer = WordLevelTrainer(special_tokens = spl_tokens) tokenizer.pre_tokenizer = Whitespace() return tokenizer, trainer
我们还需要添加一个预标记器,将我们的输入分成单词,因为如果没有预标记器,我们可能会得到与几个单词重叠的标记:例如,我们可能得到一个 **"there is"
**令牌,因为这两个词经常挨着出现。
使用预标记器将确保没有标记比预标记器返回的单词大。
这个函数将返回标记器及其训练器对象,我们可以用它来在数据集上训练模型。
在这里,我们对所有的模型都使用相同的预标记器(Whitespace
)。你可以选择用其他人来测试它。
第2步 – 训练标记器
在准备好标记化器和训练器之后,我们可以开始训练过程。
这里有一个函数,它将接收我们打算训练标记器的文件以及算法标识符。
‘WLV’ ‘WPC’ ‘BPE’ ‘UNI’
def train_tokenizer(files, alg='WLV'): """ Takes the files and trains the tokenizer. """ tokenizer, trainer = prepare_tokenizer_trainer(alg) tokenizer.train(files, trainer) # training the tokenzier tokenizer.save("./tokenizer-trained.json") tokenizer = Tokenizer.from_file("./tokenizer-trained.json") return tokenizer
这是我们在训练标记器时需要调用的主要函数。它将首先准备标记器和训练器,然后开始用提供的文件训练标记器。
训练结束后,它将模型保存在一个JSON文件中,从文件中加载,并返回训练好的标记器,开始对新输入进行编码。
第3步 – 对输入字符串进行标记
最后一步是开始对新的输入字符串进行编码,并比较每种算法所产生的标记。
在这里,我们将写一个嵌套的for循环,首先在较小的数据集上训练每个模型,然后在较大的数据集上训练,并将输入字符串标记化。
输入字符串 -“这是一个深度学习标记化的教程。标记化是深度学习NLP管道的第一步。我们将比较每个标记化模型所产生的标记。很兴奋吧!:heart_eyes:”
small_file = ['pg16457.txt'] large_files = [f"./wikitext-103-raw/wiki.{split}.raw" for split in ["test", "train", "valid"]] for files in [small_file, large_files]: print(f"========Using vocabulary from {files}=======") for alg in ['WLV', 'BPE', 'UNI', 'WPC']: trained_tokenizer = train_tokenizer(files, alg) input_string = "This is a deep learning tokenization tutorial. Tokenization is the first step in a deep learning NLP pipeline. We will be comparing the tokens generated by each tokenization model. Excited much?!:heart_eyes:" output = tokenize(input_string, trained_tokenizer) tokens_dict[alg] = output.tokens print("----", alg, "----") print(output.tokens, "->", len(output.tokens))
下面是输出结果。
对输出的分析。
看一下输出,你会发现标记生成方式的不同,这反过来又导致了标记生成数量的不同。
一个简单的词级算法
无论在哪个数据集上训练都会产生35个标记。
BPE
算法在较小的数据集上训练时创造了55个标记,在较大的数据集上训练时创造了47个标记。这表明,在较大的数据集上训练时,它能够合并更多的字符对。
Unigram模型
在两个数据集上创造了类似的(68和67)个标记。但你可以看到生成的标记的差异。
有了更大的数据集,合并更接近于生成更适合于编码我们经常使用的真实世界的英语语言单词的标记。
WordPiece在较小的数据集上训练时创造了52个标记,在较大的数据集上训练时创造了48个标记。生成的代币有双#,表示代币作为前缀/后缀的使用。
当在较大的数据集上训练时,这三种算法都产生了较差和较好的子词令牌。
如何比较令牌
为了比较标记,我把每个算法的输出存储在一个字典中,我将把它变成一个数据框架,以便更好地查看标记的差异。
由于每个模型产生的标记数量不同,我添加了一个标记,以使数据成为矩形并适合数据框架。
在数据框架中基本上是nan。
import pandas as pd max_len = max(len(tokens_dict['UNI']), len(tokens_dict['WPC']), len(tokens_dict['BPE'])) diff_bpe = max_len - len(tokens_dict['BPE']) diff_wpc = max_len - len(tokens_dict['WPC']) tokens_dict['BPE'] = tokens_dict['BPE'] + ['<PAD>']*diff_bpe tokens_dict['WPC'] = tokens_dict['WPC'] + ['<PAD>']*diff_wpc del tokens_dict['WLV'] df = pd.DataFrame(tokens_dict) df.head(10)
这里是输出结果。
你也可以用集合来查看标记的差异。
要查看代码,请到这个Colab笔记本。
结束语和后续步骤
根据生成的标记的种类,WPC似乎确实生成了在英语中更常见的子词标记–但不要让我相信这一观察。
这些算法彼此之间略有不同,在开发一个像样的NLP模型方面做得有些类似。但大部分性能取决于你的语言模型的使用情况、词汇量大小、速度和其他因素。
这就结束了我们对标记化算法的考察。深入研究的下一步是了解什幺是嵌入,标记化如何在创建这些嵌入中发挥重要作用,以及它们如何影响模型的性能。
对这些算法的进一步推进是SentencePiece算法,它是解决整个标记化问题的一种健康的方法。但是HuggingFace缓解了这个问题的大部分,甚至更好–他们在一个GitHub repo中实现了所有的算法。
参考资料和说明
如果你对我的分析或我在这篇文章中的任何工作有疑问,我强烈鼓励你查看这些资源,以准确了解每种算法的工作原理。
- 》,作者Taku Kudo
- – 研究论文,讨论了基于BPE压缩算法的不同分割技术。
Be First to Comment