我们将介绍对NLP至关重要的经常被忽视的概念,如字节对编码,并讨论如何理解它们以获得更好的模型。
深度学习(DL)自然语言处理(NLP)的世界正在快速发展。我们试图在之前的一篇文章中捕捉到其中的一些趋势,如果你想了解这些发展的更多背景,你可以查看一下。两个最重要的趋势是Transformer(2017)架构和BERT(2018)语言模型,这是利用前一架构的最著名的模型。尤其是这两项发展,在帮助机器在广泛的语言阅读任务中表现得更好方面发挥了关键作用。
这些新发展的一个重要部分是他们如何消费在复杂的语言任务中表现良好所需的文本。我们经常跳过这个过程的这一部分,以获得酷的新模型的“更丰富”的核心。事实证明,这些输入步骤是一个完整的研究领域,有大量复杂的算法,所有这些算法都只是为了使更大的语言模型能够学习更高级别的语言任务。把它想象成排序大量数字数组所需的排序算法。虽然您只在Python脚本中使用排序函数,但整个行业的唯一目标是优化以获得越来越好的排序算法。但在我们深入研究这些算法的细节之前,我们首先需要了解为什么阅读文本对机器来说是一项艰巨的任务。
- Why is reading difficult for machines?
- Subword Tokenization
- Byte Pair Encoding (BPE)
- Unigram Subword Tokenization
- WordPiece
- SentencePiece
为什么机器很难阅读?
在你学会阅读之前,你可以理解语言。当你开始上学的时候,你已经可以和你的同学说话了,即使你不知道名词和动词的区别。在那之后,你学会了把你的语音语言变成书面语言,这样你就可以读写了。一旦你学会了将文本转化为声音,你就可以访问之前学习的单词含义库。
机器没有这种语音上的领先优势。在对语言一无所知的情况下,我们需要开发一些系统,使他们能够处理文本,而不像人类那样,已经能够将声音与单词的含义联系起来。这是一个经典的“鸡和蛋”问题:如果机器对语法、声音、单词或句子一无所知,它们如何开始处理文本?您可以创建规则,告诉机器处理文本,使其能够执行字典类型查找。然而,在这种情况下,机器没有学习任何东西,你需要有一个静态的数据集,包含每一个可能的单词组合及其所有语法变体。
我们不想训练机器查找固定的词典,而是希望教会机器识别和“阅读”文本,使其能够从这个动作中学习。换句话说,它读得越多,学到的东西就越多。人类通过利用他们以前学习语音的方式来做到这一点。机器没有这些知识可以利用,因此需要被告知如何将文本分解为标准单元来处理。它们使用一种称为“标记化”的系统来完成这一操作,在该系统中,文本序列被分解成更小的部分,或“标记”,然后作为输入输入到像BERT这样的DL NLP模型中。但是,在我们研究文本标记化的不同方法之前,让我们先看看我们是否真的需要使用标记化。
我们需要Tokenizers吗?
为了教像BERT或GPT-2这样的DL模型在NLP任务中表现良好,我们需要给它提供大量的文本。希望通过架构的具体设计,模型能够学习到一定程度的句法或语义理解。关于这些模型学习的语义理解水平,这仍然是一个积极研究的领域。人们认为,当他们开始深入研究更具体的语言领域信号时,他们会在神经网络的较低级别学习句法知识,然后在较高级别学习语义知识,例如医学与技术培训文本。
所使用的特定类型的体系结构将对模型能够处理哪些任务、学习速度和执行效果产生重大影响。例如,GPT2使用解码器架构,因为它的任务是预测序列中的下一个单词。相比之下,BERT使用编码器类型的架构,因为它被训练用于更大范围的NLP任务,如下一句预测、问答检索和分类。无论它们是如何设计的,都需要通过输入层向它们提供文本,以执行任何类型的学习。
一种简单的方法是简单地在文本出现在训练数据集中时提供文本。这听起来很简单,但有一个问题。我们需要找到一种以数学方式表示单词的方法,以便神经网络进行处理。
请记住,这些模型不懂语言。因此,如果他们对语言结构一无所知,就无法从文本中学习。对模型来说,这就像胡言乱语,什么也学不到。它不会理解一个单词从哪里开始,另一个单词在哪里结束。它甚至不知道什么构成一个词。我们通过首先学习理解口语,然后学习将语音与书面文本联系起来来解决这个问题。因此,我们需要找到一种方法来做两件事,以便能够将文本的训练数据输入到DL模型中:
将输入分成更小的块:模型对语言的结构一无所知,所以我们需要在将其输入模型之前将其分成块或标记。
将输入表示为向量:我们希望模型学习句子或文本序列中单词之间的关系。我们不想将语法规则编码到模型中,因为它们具有限制性,需要专业的语言知识。相反,我们希望模型学习关系本身,并找到一些理解语言的方法。要做到这一点,我们需要将标记编码为向量,其中模型可以在这些向量的任何维度上编码意义。它们可以用作输出,因为它们代表单词的上下文参考。或者,它们可以被馈送到其他层中,作为更高级别NLP任务(如文本分类)的输入,或者用于迁移学习。
在我们开始训练我们的模型以生成更好的向量之前,我们首先需要弄清楚我们需要实现哪种标记化策略,以便将我们的文本分解成小块。
如果你问说英语的人,他们很可能会说,把句子分成单词级的块或记号似乎是最好的方法。在某种程度上,他们是对的。在上图中,您可以看到,即使是单词标记化,创建标记也有一些困难:我们是忽略标点符号还是包括标点符号,还是编写特定规则以获得更连贯、更有用的单词标记?
因此,即使您制定了一种标准方法或规则集,使您能够将文本编码为单词标记,您仍然会遇到一些核心问题:
你需要大量的词汇量:当你处理单词标记时,你只能学习那些在你的训练词汇中的单词。使用模型时,任何不在训练集中的单词都将被视为未知单词,并通过可怕的“<UNK>”标记进行识别。因此,即使你在训练集中学会了“猫”这个词,最终的模型也不会识别复数“猫”。它不会把单词分解成子单词,所以它会错过像“talk”与“talks”与“gotled”和“talking”这样的单词。所有这些都需要单词级别的单独标记,这是一种非常低效的方法。尽管你可以通过对输入文本应用词干或旅语化来减少词汇量来解决这个问题,但你最终还是会在NLP管道中迈出额外的一步,并且可能仅限于某些语言。
我们将单词组合起来:另一个问题是,对于单词的确切组成部分,可能存在一些混淆。我们将“sun”和“flower”等单词组合成向日葵,并将“checking”或“runner up”等连字符组合成向日葵。这是一个单词还是多个单词?我们使用诸如“纽约”或“理学学士”之类的文本序列作为一个单元。作为孤立的词语,它们可能没有意义。
缩写词:随着社交媒体的兴起,我们有了“LOL”或“IMO”等词的缩写。这些是单词集还是新词?
有些语言不按空格分段:对于像英语这样用空格分隔单词的语言来说,把英语句子分解成单词很容易,但对于像汉语这样的所有语言来说,情况并非如此。在这些情况下,单词分离并不是一项微不足道的任务。
使用单词的另一种方法是简单地逐个字符地标记输入文本。通过这种方式,我们避免了单词标记化的许多陷阱。例如,我们现在可以避免未知单词之类的事情,因为在字符级别,您可以为任何单词创建嵌入。你将知道组成语言的字母表的所有元素,这样你就可以为这些元素的任何组合构建嵌入。
在上图中,我们可以看到有许多不同的方式可以执行这种字符编码。例如,我们可以忽略空格,简单地将每个字符和符号视为单个标记,并为每个标记提供一个单独的向量。或者,我们可能希望将我们的字符词汇限制为某些符号,从而删除撇号等符号。所有这些都忽略了单词的间距,所以我们可能想将一个符号分配给一个空间,并在为每个标记创建嵌入向量时使用它。
希望使用这些方法中的任何一种,拼写错误的单词或不寻常的单词拼写(例如cooooooool)应该看起来彼此相似,因为它们所学习的嵌入将紧密相连。这同样适用于同一动词的不同版本,例如“walk”、“walking”、“walk-ed”等。
然而,这种方法也有一些缺点:
缺乏意义:与单词不同,字符没有任何固有的意义,因此无法保证由此习得的表征会有任何意义。字母可能会组合成稍微不寻常的组合,这些组合不是正确的单词。更多的训练数据应该有助于缓解这种趋势,但我们仍然面临这样一种情况,即模型正在失去单词的语义特定特征。
增加输入计算:如果你使用单词级标记,那么你将把一个7个单词的句子钉入7个输入标记中。然而,假设平均每个单词(英语)有5个字母,您现在有35个输入要处理。这增加了您需要处理的输入规模的复杂性
限制网络选择:在字符级别增加输入序列的大小也会限制可以使用的神经网络类型。很难使用按顺序处理输入的体系结构,因为您的输入序列要长得多。然而,像BERT这样的新模型是基于并行处理输入的Transformer架构的,这意味着这不再是一个主要的限制。然而,它仍然会影响您对网络的选择,因为很难对角色执行某种类型的NLP任务。如果你正在训练词性(POS)标记器或分类器,那么在字符级别上工作会更加困难。你需要做更多的工作,而不是在单词水平上训练它来优化你的任务。
如果单词级别的标记不理想,并且字符级别的标记似乎有自己的问题,那么有什么替代方案?一种已被证明流行的替代方案是字符级别和单词级别之间的平衡,称为子单词级别标记化。
Subword Tokenization
我们想要一个能够处理缺失代币而不需要无限词汇的系统。换言之,我们想要一个通过有限的已知单词列表来处理无限潜在词汇的标记化方案。此外,我们不希望将所有内容分解为单个字符带来额外的复杂性,因为字符级别的标记化可能会失去单词级别的一些含义和语义细节。
我们解决这个问题的一种方法是思考如何重新使用单词,并从较小的单词中创建较大的单词。想想像“any”和“place”这样构成“anyplace”的词,或者像“anywhere”或“anyone”这样的复合词。你不需要为你的词汇表中的每个单词都输入一个条目。相反,你只需要记住几个单词,然后把它们放在一起创建其他单词。这需要更少的记忆和精力。这是子词标记化背后的基本思想。试着建立最小的子单词块集合,这样你就可以覆盖数据集中的所有单词,而不需要知道该词汇表中的每个单词。
为了使系统更加高效,子单词块甚至不需要是完整的单词。例如,你可以通过“un”+“将单词“不幸”改为“+”tun“+”ate“+”ly“。子单词标记化将根据单词频率将文本分成块。在实践中,常见的单词通常会被标记为完整的单词,例如“the”、“at”、“and”等,而稀有的单词会被分解成更小的块,并可用于创建相关数据集中的其余单词。
这里适用的另一个因素是允许的词汇量。这是由运行子词算法的人选择的。词汇量越大,你可以标记的常用词就越多。词汇表的大小越小,就需要越多的子单词标记来避免使用<UNK>标记。正是这种微妙的平衡,你可以尝试为你的特定任务找到最佳解决方案。
Byte Pair Encoding (BPE)
遵循上述方法的一种流行的子词标记化算法是BPE。BPE最初用于通过查找常见的字节对组合来帮助压缩数据。它也可以应用于NLP,以找到最有效的文本表示方式。我们可以看一个例子来看看BPE在实践中是如何工作的(下面的例子我使用了雷毛博客中的代码。如果你有兴趣更深入地了解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.”
首先,让我们看看单个单词出现的频率。本文中的单词出现频率如下:
你可以在这里看到的第一件事是,每个单词的末尾都有一个“</w>”标记。这是为了识别单词边界,以便算法知道每个单词的结尾。这一点很重要,因为子词算法会遍历文本中的每个字符,并试图找到频率最高的字符配对。接下来,让我们看看字符级标记的频率:
这里有几点需要注意。浏览表格,熟悉标记及其出现的频率。您可以看到“</w>”字符出现24次,这是有道理的,因为共有24个单词。第二个最频繁的标记是“e”字符,它出现了16次。总共有27个tokens。现在,BPE算法查看最频繁的配对,合并它们并进行另一次迭代。
什么是合并?
BPE子词算法的主要目标是找到一种用最少的标记表示整个文本数据集的方法。与压缩算法类似,您希望找到最好的方式来表示您的图像、文本或您正在编码的任何内容,它使用最少的数据量,或者在我们的情况下使用token。在BPE算法中,合并是我们尝试将文本“压缩”为子单词单元的方式。
合并通过识别最频繁表示的字节对来工作。在我们的例子中,字符与字节相同,但情况并非总是如此,例如,在某些语言中,字符将由多个字节表示。但为了我们的目的,为了简单起见,字节对和字符对是相同的。这些合并操作有几个步骤(其中一些我们已经在简单示例中执行过):
1、获取字数频率
2、获取初始token计数和频率(即每个字符出现的次数)
3、合并最常见的字节配对
4、将其添加到token列表中,并重新计算每个token的频率计数;这将随着每个合并步骤而改变
5、冲洗并重复,直到达到定义的token限制或设定的迭代次数(如我们的示例)
我们已经在上表中列出了单词和标记值。接下来,我们需要找到最常见的字节对,并将两者合并为一个新的token。一次迭代后,我们的输出如下所示:
在一次迭代之后,我们最频繁的配对是“d”和“e”。因此,我们将这些结合起来创建了我们的第一个子单词标记(不是单个字符)“de”。我们是如何计算的?如果你还记得我们之前计算的单词频率,你可以看到“de”是最频繁的配对。
如果你把“de”出现在中的单词的频率加起来,你会得到3+2+1+1=7,这是我们新的“de”标记的频率。由于“de”是一个新的token,我们需要重新计算所有token的计数。我们通过在合并操作之前从单个token的频率中减去新的“de”token的频率7来实现这一点。如果你仔细想想,这是有道理的。我们刚刚创建了一个新的token“de”。这种情况在我们的数据集中发生了7次。现在我们只想计算“d”和“e”不成对出现的次数。为此,我们从“e”的原始出现频率16中减去7,得到9。我们将“d”的原始频率12减去7,得到5。您可以在“迭代1”表中看到这一点。
让我们再进行一次迭代,看看下一个最频繁的配对是什么:
再次,我们添加了一个新的token,使token的数量达到29,因此我们实际上在2次迭代后增加了token的数量。这很常见;当我们开始创建新的合并对时,token的数量会增加,但当我们将这些token组合在一起并移除其他token时,token数量会开始减少。当我们在这里进行不同的迭代时,您可以看到这个数字的变化:
正如您所看到的,随着我们开始合并,token的数量最初会增加。然后它在34处达到峰值并开始减少。在这一点上,子字单元开始合并,我们开始消除合并对中的一个或两个。然后,我们将token构建成一种可以以最有效的方式表示整个数据集的格式。对于我们这里的例子,我们在大约70次迭代中使用18个token。事实上,我们已经从单个字符标记的起点重新创建了原始单词。最终的token列表如下所示:
这看起来熟悉吗?事实上,它应该是我们开始使用的原始单词列表。那么我们做了什么?我们通过从单个字符开始,并在多次迭代中合并最频繁的字节对标记,重新创建了原始单词列表(如果使用较小的迭代,您将看到不同的标记列表)。虽然这似乎毫无意义,但请记住,这是一个玩具数据集,目的是显示子词标记化所采取的步骤。在现实世界中的示例中,数据集的词汇表大小应该大得多。那么你就不可能为你的词汇表中的每个单词都有一个标记。
Probabilistic Subword Tokenization
对于BPE,我们使用单词的频率来帮助识别要合并哪些token以创建我们的token集。BPE确保最常见的单词将在新词汇表中表示为单个标记,而不太常见的单词则将分解为两个或多个子单词标记。为了实现这一点,BPE将在每一步中检查每一个潜在的选项,并根据最高频率选择要合并的token。通过这种方式,它是一个贪婪的算法,在迭代的每一步都优化为最佳解。
BPE贪婪方法的一个缺点是,它可能导致潜在的模糊最终token词汇表。您的BPE算法的输出是一个token集,类似于我们前面生成的token集。此标记集用于对模型输入的文本进行编码。当一个特定单词有多种编码方式时,就会出现问题。如何选择要使用的子单词单位?您没有任何方法来优先考虑首先使用哪些子词标记。作为一个简单的例子,假设我们的玩具示例的最终标记集是以下子单词标记:
输入句子中包含短语“深度学习”。然后,我们可以通过多种不同的方式使用我们的token集对此进行编码:
因此,尽管输入文本是相同的,但它可以由三种不同的编码表示。这对您的语言模型来说是个问题,因为生成的嵌入会有所不同。这三个不同的序列将显示为您的语言模型要学习的三个不同输入嵌入。这将影响你所学表达的准确性,因为你的模型会了解到“深度学习”这个短语出现在不同的上下文中,而事实上它应该是相同的关系上下文。为了解决这个问题,我们需要某种方法来对编码步骤进行排序或排定优先级,以便我们最终对相似的短语进行相同的标记编码。这很方便,是诸如unigram之类的概率子词模型的一个特征。
Unigram Subword Tokenization
我们已经看到,使用子词模式的频率进行标记化可能导致模糊的最终编码。问题是,在对任何新的输入文本进行编码时,我们无法预测哪个特定的token更有可能是最好的。幸运的是,需要预测最可能的文本序列并不是标记化的唯一问题。我们可以利用这些知识来构建一个更好的标记化器。
语言模型以某种形式支撑着当前所有的深度学习模型,如BERT或GPT2,其目标是能够预测给定初始状态的文本序列。例如,给定输入文本“FloydHub是构建、训练和部署深度???的最快方法”,您能预测下一个文本序列吗?是“深海”、“太空”、“深度…学习”还是“深度…睡眠”。一个经过良好训练的语言模型应该能够提供一个概率,在给定前面的文本序列的情况下,该概率最有可能。从我们自己的简单例子中,我们可以看到,我们考虑的单词越多,预测下一个单词就越准确。然而,我们考虑的单词越多,模型就变得越复杂,因为这增加了LM的维数,并使条件概率更难计算。
为了解决这种复杂性,最简单的方法是unigram模型,它只考虑当前单词的概率。下一个单词是“学习”的可能性有多大,只取决于“学习”一词出现在训练集中的概率。现在,当我们创建一个模型,试图从某个起点预测连贯的句子时,这并不理想。您可能希望使用具有较大训练序列的模型,例如查看前面2-3个单词的LM。这将有更好的机会生成一个更连贯的句子,正如我们从简单的例子中看到的那样。然而,子词模型的目标与试图预测完整句子的LM不同。我们只想要能产生明确标记化的东西。
2018年发表了一篇论文,描述了如何使用这种unigram LM方法来选择子单词标记。这是一篇很好的论文,因为它也描述了BPE方法,并介绍了它的优点和缺点。论文中有一些数学涵盖了事物的概率方面,但即使如此,也得到了很好的解释。unigram方法与BPE的不同之处在于,它试图在每次迭代中选择最有可能的选项,而不是最佳选项。要生成unigram子字token集,您需要首先定义token集的所需最终大小,以及一个起始种子子字token集合。您可以以类似于BPE的方式选择种子子词标记集,并选择最频繁出现的子字符串。一旦你有了这个,那么你需要:
1、计算每个子单词标记的概率
2、计算出一个损失值,如果每个subwork token都被丢弃,则会导致该损失值。损失是通过本文中描述的算法(期望最大化算法)计算出来的。
3、丢弃损失值最大的token。你可以在这里选择一个值,例如,根据损失计算,去掉底部10%或20%的子词标记。请注意,您需要保留单个字符,以便能够处理词汇表外的单词。
重复这些步骤,直到达到所需的最终词汇表大小,或者直到连续迭代后标记号没有变化。
迄今为止的故事
到目前为止,我们已经解释了为什么我们需要为深度学习NLP模型标记输入文本序列。然后,我们研究了一些常用的文本标记方法,然后我们回顾了最近两种以BPE和unigram形式的子词标记模型。了解后两种模型意味着你应该能够理解目前在深度学习NLP中使用的几乎所有标记化方法。
大多数模型要么直接使用这些,要么使用它们的某些变体。然而,仍然很难理解在最新和最棒的NLP深度学习模型中使用了哪种标记器。原因是他们可能会提到一个你没有听说过的token化模型。然后你可能会认为了解unigram或BPE模型是在浪费时间。但别担心,一切都没有失去!在大多数情况下,经过一点挖掘,你会发现这些“新”方法实际上与BPE或unigram非常相似。
这方面的一个例子是BERT中使用的标记器,它被称为“WordPiece”。我们将研究该算法,并展示它与前面讨论的BPE模型的相似之处。最后,我们将研究2019年最近发布的通用句子编码器多语言模型中使用的“句子片段”算法。“句子片段”汇集了我们所谈论的所有概念,因此它是总结我们迄今为止所涵盖内容的好方法。它还有一个很棒的开源repo,可以让你进行测试,所以我们可以通过一些代码示例。
WordPiece
子词标记化的世界就像深度学习NLP宇宙一样,在短时间内快速进化。因此,当BERT在2018年发布时,它包含了一种名为WordPiece的新子词算法。在最初的阅读中,你可能会认为你回到了原点,需要找出另一个子词模型。然而,WordPiece与BPE非常相似。
将WordPiece视为BPE方法和unigram方法之间的中介。如果你还记得的话,BPE取两个token,查看每对的频率,然后合并具有最高组合频率计数的对。它只考虑每一步中最频繁的配对组合,而不考虑其他。
另一种方法是检查合并特定对的潜在影响。您可以使用概率LM方法来实现这一点。在每个迭代步骤中,选择一个字符对,该字符对在合并后将导致可能性的最大增加。这是新兆对出现的概率减去两个单独token单独出现的概率之间的差。例如,如果“de”比“d”+“e”的概率更有可能发生。
这就是为什么我说WordPiece似乎是BPE和unigram方法之间的桥梁。它的总体方法类似于BPE,但它也使用unigram方法来确定何时合并token。
你可能想知道,它与unigram模型本身有什么不同?这是个好问题。主要区别在于WordPiece是一种贪婪的方法。它仍然试图自下而上地构建一个标记器,在每次迭代中选择最好的一对进行合并。WordPiece使用可能性而不是计数频率,但在其他方面也是类似的方法。相反,Unigram是一种完全概率的方法,它使用概率来选择要合并的对以及是否合并它们。它还删除了token,因为它们对unigram模型的总体可能性的影响最小。可以考虑丢弃正态分布尾部的token。请注意,单个字符标记永远不会被丢弃,因为它们将被用来构建潜在的词汇表外单词。
在unigram中,我们使用分布丢弃tokens,就像我们寻找正态分布的瘦“尾巴”一样。这具有较低的密度,或者在我们的情况下意味着这些是最不可能导致更好的标记化的标记。
深呼吸!
呜呜!我知道,对吧?谁想到深度学习NLP过程的这一部分会如此困难?这只是这些模型的第一步——我们甚至还没有到训练它们完成常见NLP任务的主要部分。所有这些都是为了以后的一两篇帖子!现在,让我们简要总结一下到目前为止我们所拥有的:
BPE:只使用出现的频率来识别每次迭代中的最佳匹配,直到达到预定义的词汇大小。
WordPiece:类似于BPE,使用频率出现来识别潜在的合并,但根据合并token的可能性做出最终决定
Unigram:一种完全概率模型,不使用频率出现。相反,它使用概率模型训练LM,去除对总体可能性提高最小的token,然后重新开始,直到达到最终的token极限。
很可能您可以将BPE模型与BERT一起使用,而不是WordPiece,并获得类似的结果。不要让我这么做,但这不会对最终结果产生太大影响。然而,在BERT开始获得广泛关注后,一个新的子词模型被发布,这可能是最近标记化进化周期的最后一步。这就是句子片段算法,我们现在将研究它,希望它能帮助我们将我们讨论过的所有概念结合在一起。
SentencePiece
SentencePiece基本上试图将所有的子词标记化工具和技术放在一个旗帜下。这有点像瑞士军刀的子词标记化。要成为一个像瑞士军队一样的工具,必须有能力解决多个问题。那么,句子片段解决了哪些问题:
1、所有其他模型都假设输入已经标记化:BPE和Unigram是很好的模型,但它们有一个共同的缺点——它们都需要将输入标记化。BPE需要对输入进行标记化,以便对每个字符(包括单词边界字符)进行标记化。只有这样,BPE才能计数频率并开始合并token。通常这是通过简单地进行单词级的标记化来完成的,但正如我们之前所讨论的,这是标记化的一个问题,因为并非所有语言都是空间分段的。类似地,unigram模型需要将其输入标记化,然后才能根据其概率分布开始丢弃标记。SentencePiece通过简单地接受原始文本中的输入,然后对该输入执行子词标记所需的一切(我们将在下面讨论)来处理此问题。
2、语言不可知:由于所有其他子词算法都需要对其输入进行预标记,这限制了它们在许多语言中的适用性。您必须为不同的语言创建规则,才能将它们用作模型的输入。这很快就会变得非常混乱。
3、解码很困难:由BPE和unigram等模型需要已经标记化的输入引起的另一个问题是,你不知道使用了什么编码规则。例如,空间是如何在您的token中编码的?编码规则是否区分了空格和制表符?如果你看到像[new]和[york]这样的两个标记在一起,你就无法知道原始输入是“new york”、“new york”、“new york”等等。所以你无法解码输入并将其返回到原始格式。当您试图重现结果或确认发现时,这会产生问题。
4、没有端到端的解决方案:这些只是其中的一些问题,这意味着BPE和unigram不是完全完整的或端到端解决方案。你不能只插入一个原始输入就得到一个输出。相反,它们只是解决方案的一部分。SentencePiece将端到端解决方案所需的一切汇集在一个整洁的包中。
SentencePiece是如何解决所有这些问题的?
SentencePiece使用了许多功能来解决上述所有问题,这些功能在相关论文和相应的GitHub回购中都有详细概述。这两个都是很好的资源,但如果你时间紧迫,那么浏览回购可能是快速了解《句子篇》及其所有相关的瑞士军队伟大之处的最佳方式。
目前,在深入研究一些代码示例之前,只需注意到SentencePiece用于解决上述缺点的一些技术就足够了:
将所有内容编码为unicode…:句子片段首先将所有输入转换为unicode字符。这意味着它不必担心不同的语言、字符或符号。如果它使用unicode,它可以以相同的方式处理所有输入,这使它可以与语言无关
…包括空格:为了解决分词问题,SentencePiece简单地将空格编码为unicode符号。具体来说,它将其编码为unicode值U+2581(对于我们这些不会说unicode的人来说,下划线为“_”)。这有助于解决语言不可知的问题和解码问题。由于空间是unicode编码的,因此它们可以很容易地反转或解码,并像普通语言字符一样处理(即学习)。这听起来像是一个简单的方法,我想是的,但最好的想法最终似乎是这样的
而且速度更快:谷歌著名地指出,“速度不仅仅是一个功能,它也是一个功能”。这不仅适用于搜索引擎,也适用于子词标记化。作为模型训练的一部分,阻止其他子词算法用于标记原始句子的问题之一是缺乏速度。如果您实时处理输入并对原始输入执行标记化,那么速度会太慢。SentencePiece通过使用BPE算法的优先级队列来加快速度,从而将其用作端到端解决方案的一部分。
使用这些和其他功能,SentencePiece能够在任何深度学习NLP模型的任何输入语言上提供快速而稳健的子词标记化。现在让我们看看它的实际操作。
SentencePiece in action
SentencePieceGithub repo提供了一些关于如何使用库的好例子。我们将尝试其中的一些,以展示我们已经谈到的一些子词算法。然而,句子片段库的内容比我们在几篇博客文章中所能了解的要多得多。我们将在这里介绍主要功能,使您能够根据需要进行更深入的挖掘,以进行进一步的探索。我们将介绍的主要内容有:
找到一个好的数据集:为了真正测试句子片段,我们想在一些数据上训练它。句子片段建议训练一本名为“Botchan”的小说。虽然这很好,但这是一部写于1906年的日本小说。所以我们可以找到一些更新的东西。我们可以使用lionbridge.ai提供的一个开源数据集。它包括NLP的数据集列表,我们可以从中选择60多万篇博客文章的博客语料库(我们只需要其中的一小部分)。它应该包含各种数据、常用词、俚语、拼写错误、名称和实体等。
训练BPE模型:在创建数据集后,我们可以训练BPE模式,这样我们就可以得到一个可以用于编码的BPE token列表。
训练一个Unigram模型:同样,我们可以在相同的数据上训练一个Unigram模型,这样我们就可以拥有特定于Unigram的token。
比较模型:使用经过训练的模型,我们将查看token并找出差异。我们还将对一些文本进行编码,看看它在两个模型中的外观。执行一些采样:正如我们所指出的,unigram模型是基于概率分布的,因此SentencePiece提供了从该分布中采样的函数。这些功能仅在unigram模型中可用。
获取训练数据
查看代码库,了解如何使用博客语料库进行训练。请注意,您可以在此处使用任何您喜欢的数据。这只是一个建议。下面步骤的所有代码都在笔记本中,因此您可以通过笔记本执行下面的所有步骤。
训练BPE模型
我们可以使用“句子片段”很容易地训练模型。现在我们不需要担心我们在训练cmd中使用的参数。
# 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')
需要注意的主要参数是“vocab_size”。我们将其设置为500只是一个例子,但您可以在这里选择任何您喜欢的内容。请记住,你的vocab 越大,你可以存储的常用单词就越多,但出于性能原因,你可能希望应用程序的vocab 更小。
一旦你训练好了你的模型,你只需要加载它,就可以开始了!
# makes segmenter instance and loads the BPE model file (bpe.model)
sp_bpe = spm.SentencePieceProcessor()
sp_bpe.load('bpe.model')
训练Unigram模型
现在我们只需要训练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')
让我们比较一下模型
您可以通过调用“encode_as_pieces”函数,使用经过训练的子单词标记对句子进行编码。让我们对以下句子进行编码:“这是一次测试”。
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']
下划线表示标记中有一个空格,并且区分大小写。因此,它将“Test”和“test”视为不同的token。请注意,第一个单词前面似乎有一个空格,尽管我们没有把它放在那里。它假设有一个,因为这个词在句子的开头。因此,对句子“This is a test”的编码将以相同的方式进行编码。
这里需要注意的有趣的事情是,在Unigram模型中没有“This”或“test”这个词,但在BPE中有“This(这个)”。在另一个数据集中,如果我们选择更大的vocab ,这些单词可能会更常见。对于一个博客,你会认为“this”会是一个流行的词。也许是大写字母“T”导致了它的编码方式不同?让我们试试“I think this is a test”。
输出如下:
BPE: ['▁I', '▁think', '▁this', '▁is', '▁a', '▁t', 'est']
UNI: ['▁I', '▁think', '▁this', '▁is', '▁a', '▁t', 'est']
所以有一个词来形容“this”!它的大写字母“T”不足以被编码为Unigram模型的特定标记(或者它没有增加足够的合并可能性来实现)。由于没有“test”这个词,模型通过“t”和“est”的组合创建了它。我们可以尝试在这个数据集中不经常使用的单词,如“Carbon dioxide”。
输出如下:
BPE: ['▁C', 'ar', 'b', 'on', '▁d', 'i', 'o', 'x', 'ide']
UNI: ['▁C', 'ar', 'b', 'on', '▁d', 'i', 'o', 'x', 'id', 'e']
所以看起来这不是一个常见的单词,因此它是由一些更常见的子单词标记和一些单字母标记组成的。
让我们看看我们所有的tokens
要查看创建的所有tokens,我们可以运行以下代码来查看完整列表。
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
我们将得到一个500个标记的列表(这是我们预定义的限制),它们应该代表最常见的单词,然后是最常见的子单词组合。您可以从代码中看到,我们使用了“id_to_piece”函数,该函数将token的id转换为相应的文本表示。如下所示:
['▁something',
'▁because',
'▁thought',
'▁really',
.
.
.
'9',
'*',
'8',
'6',
'7',
'$']
这一点很重要,因为句子片段使子单词过程是可逆的。你可以用ID或子单词标记来编码你的测试句子;你用什么取决于你。关键是,您可以将ID或标记完美地解码回原始句子,包括原始空格。以前,其他标记器不可能做到这一点,因为它们只是提供标记,而且不清楚使用了什么编码方案,例如,它们是如何处理空格或标点符号的?这是SentencePiece的一大卖点。
# decode: id => text
print("BPE {}".format(sp_bpe.decode_pieces(['▁This', '▁is', '▁a', '▁t', 'est'])))
print("BPE {}".format(sp_bpe.decode_ids([400, 61, 4, 3, 231])))
print("UNI {}".format(sp_uni.decode_pieces(['▁Thi', 's', '▁is', '▁a', '▁t', 'est'])))
print("UNI {}".format(sp_uni.decode_ids([284, 3, 37, 15, 78, 338])))
输出如下:
BPE This is a test
BPE This is a test
UNI This is a test
UNI This is a test
不管怎样,回到我们的token列表。最好看看不同模型创建的token列表,看看它们在哪里不同。为此,我们需要创建两个列表,一个包含Unigram tokens,另一个包含BPE tokens。
然后,我们可以简单地获得不在BPE列表中的Unigram token,反之亦然。看看每组中缺少哪些标记是很有趣的。这将告诉我们每个子词模型的不同方法。请记住,在查看这些标记时,“__”表示一个空间。当它没有出现时,意味着该单词是另一个单词的一部分,或者附加到句号、逗号或空格以外的其他符号。当你开始修改编码时,这将变得更加清晰。
如果我们看看Unigam模型中没有的一些BPE token,我们会看到类似“▁somet”和“ittle”。
diff_pairs = list(zip(uni_tok_diff, bpe_tok_diff))
diff_df = pd.DataFrame(diff_pairs,
columns=(["Unigram tokens not in BPE", "BPE tokens not in Unigram"]))
diff_df.head()
这些都是BPE贪婪方法的好例子。我们真的需要tokens吗?”▁somet”?Unigram模型一定已经计算出,使用它的总体效益小于简单使用“▁一些”以及其他一些子单词单元。但对于BPE,它只是在每一步检查最频繁的配对。
类似地,对于“ittle”,拥有这个token有效吗?如果你在训练数据文本中看到“小”出现159次。其中156个是“小”,其余的场合是一次“贬低”和两次“小”提及。如果我们有很多单词以“ittle”结尾,比如“whittle”、“tittle”和“skittle”,那么这可能是有意义的。但鉴于缺乏这些类型的单词,Unigram类型的方法似乎更有效。这表明了如果你想拥有最有效的子单词词汇表,使用Unigram方法的好处,
让我们做一些采样
最后但同样重要的是,让我们来看看Unigram采样功能。这仅适用于Unigram模型,因为BPE是一种基于频率的方法。SentencePiece 的采样功能允许您设置采样参数。默认情况下,你会得到最有效的标记化,但如果你愿意,你可以更改它。这些都是原始论文中提到的非常精细和高级的设置。“nbest”参数允许您从更多分割选项中进行选择。参数越高,将考虑的选项越多。虽然这是非常先进的,而且很难知道何时需要更改它,但您至少可以查看更改这些设置时返回的不同token。
# Can obtain different segmentations per request.
# There are two hyperparamenters for sampling (nbest_size and inverse temperature). see the paper [kudo18] for detail.
for n in range(10):
print(sp_uni.sample_encode_as_pieces('remembers', -1, 0.1))
输出如下所示:
['▁re', 'me', 'm', 'b', 'er', 's']
['▁', 're', 'm', 'e', 'm', 'b', 'e', 'r', 's']
['▁remember', 's']
['▁remember', 's']
['▁remember', 's']
['▁', 're', 'me', 'm', 'b', 'er', 's']
['▁', 'r', 'e', 'me', 'm', 'b', 'er', 's']
['▁re', 'me', 'm', 'b', 'e', 'r', 's']
['▁', 'r', 'e', 'me', 'm', 'b', 'er', 's']
['▁remember', 's']
你可以在论文的子词采样部分阅读更多关于这方面的内容,他们在那里讨论了“l-best”采样方法。
HuggingFace Tokenizers to the Rescue!
HuggingFace的那些伟人又做了一次。他们已经令人印象深刻的NLP库的最新添加是,是的,你们猜对了,标记化器。HuggingFace标记器的便利之处在于,它们隐藏了我们在这篇文章中讨论的许多复杂细节(别担心,了解它们的工作原理仍然非常有用,我保证这将帮助您使用HuggingFace标记器等库)。
正如我们所展示的,SentencePiece 库包含BPE和Unigram模型所需的一切。但如果你想使用其他模型,比如WordPiece,你需要单独设置。HuggingFace将所有这些都放在一个方便的GitHub仓库下。因此,让我们在博客数据上训练这些token,并展示它的易用性。
通过几行代码,您可以访问一系列的标记器,包括我们上面讨论过的那些。我们可以在同一个测试数据集上训练它,然后简单地打印token。
from tokenizers import (ByteLevelBPETokenizer,
BPETokenizer,
SentencePieceBPETokenizer,
BertWordPieceTokenizer)
tokenizer = SentencePieceBPETokenizer()
tokenizer.train(["../blog_test.txt"], vocab_size=500, min_frequency=2)
output = tokenizer.encode("This is a test")
print(output.tokens)
输出如下:
BPE: ['▁Th', 'is', '▁is', '▁a', '▁t', 'est']
您可以看到,这与我们通过SentencePiece实现的BPE算法的token不同:
BPE: ['▁This', '▁is', '▁a', '▁t', 'est']
这可能是由于您可以设置的参数不同,或者不同库的默认参数不同。你可以修改其中的一些,看看你是否能让token对齐。这显示了使用不同库的潜在问题。如果你使用了SentencePiece,而其他人使用了HuggingFace,那么它可能会以不同的方式标记输入,并对在相同数据上训练的同一模型产生不同的结果。
希望能够很容易地对齐参数,以便这些库可以互换使用。或者,无论你使用哪个库,为了保持一致性,最好在所有项目中都坚持使用它。现在说还为时过早,但其中一个库可能只是大多数人未来使用的实际标准。
结论
随着深度学习的快速发展,只看BERT和XLNet等模型的主要、丰富的核心是很容易的。然而,用于使这些模型能够处理文本的格式是它们学习的核心。了解子词标记器的基本知识将为您提供一种快速掌握该领域最新创新的方法。当你阅读学术媒体上热传的最新模型时,你不需要从头开始。如果你试图自己建立这些网络,或者在执行传统的NLP任务时处理多语言问题,这也会有所帮助。无论哪种方式,如果你对深度学习NLP领域感兴趣,了解一些关于模型(如句子片段)的知识将是一个有用的基础工具。
进一步阅读
一旦你开始深入了解每个模型的细节,标记化是一个令人惊讶的复杂话题。这似乎是它自己的独立研究领域,而不是更广为人知的领域,如LM架构和ELMo、BERT和Transformer模型等模型。因此,我倾向于广泛的来源,试图更好地了解该地区。以下是我发现的一些最有用的资源。因此,如果你想了解更多关于标记化的信息(或者你认为我错了!),我推荐以下材料:
- A Deep Dive into the Wonderful World of Preprocessing in NLP
- Byte Pair Encoding
- Tokenizing Chinese text
- Character encoding overview
- Tokenization tooling
- Google SentencePiece repo
- Unicode Normalization
本文翻译自《Tokenizers: How machines read》感兴趣的话可以自行阅读英文原文即可。