简化Transformers:使用你理解的单词进行最先进的NLP—第1部分—输入篇

2023年10月07日 由 alex 发表 214 0

输入


什么样的输入?如果你正在构建一种语言模型,一种能够生成相关文本的软件(Transformers架构在不同场景下非常有用),输入就是文本。然而,计算机能够接收任何类型的输入(文本、图像、声音)并奇迹般地知道如何处理它吗?并不是这样。


我相信你肯定认识那些在言语表达上不怎么擅长但在数字方面很强的人。计算机有点像这样。它不能直接在CPU/GPU(计算发生的地方)中处理文本,但它确实可以处理数字!正如你很快将看到的,将这些单词表示为数字的方式是重要组成部分。


1


分词器


分词器是将文本语料库(所有你拥有的文本)转化为机器可以更好利用的较小部分的过程。假设我们有一组包含10,000篇维基百科文章的数据集。我们将每个字符进行转化(分词)。有多种方式可以对文本进行分词,下面我们来看看OpenAi的分词器是如何对以下文本进行分词的:


“很多单词会映射到一个标记,但有些单词不会:indivisible(不可分割的)。


像表情符号这样的Unicode字符可能会被分割为包含底层字节的多个标记:??。


经常在一起出现的字符序列可以被归为一组:1234567890。”


这是分词器的结果:


2


正如你所看到的,大约有40个单词。在这40个单词中,生成了64个令牌。有时令牌是整个单词,如“Many, words, map”,有时它是单词的一部分,如“Unicode”。为什么我们将整个单词分成更小的部分?为什么甚至要分割句子?我们本可以将它们保持联系。最后,它们都会转换为数字,所以对于计算机来说,令牌长度是3个字符还是30个字符,从计算机的角度来看有什么区别呢?


令牌有助于模型学习,因为文本是我们的数据,它们是数据的特征。对这些特征进行不同的工程处理将导致性能的变化。例如,在句子:“Get out!!!!!!!”中,我们需要决定多个“!”是否与一个“!”不同,或者它们是否具有相同的意义。在技术上,我们可以保持句子完整,但是想象一下看人群和逐个观察每个人,哪种情况下你会得到更好的洞察力?


现在我们有了令牌,我们可以构建一个查找字典,从而摆脱单词并使用索引(数字)代替。例如,如果我们的整个数据集是句子:“Where is god”,我们可以构建这种词汇表,它只是单词和表示它们的单个数字的key:value对。每当我们遇到单词“is”时,我们用1替换它。


词向量


我们在用数字表示单词的旅程中取得了重大进展。下一步将是从这些令牌生成语义表示的数字。为此,我们可以使用称为Word2Vec的算法。主要思想是你取一个数字向量,列表中的数字可以是任意大小,这个数字列表应该表示一个词的语义意思。想象一下一个数字列表,如[-2,4,-3.7,41 ... -0.98],它实际上表示了一个词的语义表达。它应该被创建成这样的方式,即如果我们在二维图上绘制这些向量,相似的词将比不相似的词更接近。


正如你在下图中看到的,“Baby”接近于“Aww”和“Asleep”,而“Citizen” / “State” / “America's”也在某种程度上分组在一起。


3


这些“数字列表”非常重要,所以它们在机器学习术语中有它们自己的名称,即嵌入(Embeddings)。为什么叫嵌入?因为我们正在执行一个嵌入(如此有创意)的过程,它是将一个术语从一种形式(单词)映射(翻译)到另一种形式(数字列表)的过程。


从现在开始,我们将把单词称为嵌入,正如前面解释的那样,它们是一组数字列表,用于表示训练要代表的任何单词的语义含义。


使用Pytorch创建嵌入


首先计算一下我们有多少个唯一令牌,为了简单起见,假设有2个。嵌入层的创建代码如下所示:


import torch.nn as nn
vocabulary_size = 2
num_dimensions_per_word = 2
embds = nn.Embedding(vocabulary_size, num_dimensions_per_word)
print(embds.weight)
---------------------
output:
Parameter containing:
tensor([[-1.5218, -2.5683],
        [-0.6769, -0.7848]], requires_grad=True)


现在我们有一个嵌入矩阵,这是一个2*2的矩阵,由N(0,1)正态分布的随机数生成(例如,均值为0,方差为1的分布)。


在更现实的情况下,我们可以期望一个更接近10k*512的矩阵,它用数字表示整个数据集。


vocabulary_size = 10_000
num_dimensions_per_word = 512
embds = nn.Embedding(vocabulary_size, num_dimensions_per_word)
print(embds)
---------------------
output:
Embedding(10000, 512)


这里的参数是指的是那些数值(比如-1.525等),只是它们会随着训练而改变。


这些数字是机器的学习内容,也就是机器正在学习的内容。当我们输入给它时,我们会用这些数字乘以输入,希望能得到一个好的结果。看吧,数字很重要。当你很重要时,你会有自己的名字,所以这些不仅仅是数字,它们就是参数。


为什么要使用512而不是5个参数呢?因为更多的数字意味着我们可能能够生成更准确的意义。因为更多的数字意味着更多的计算量、更多的计算能力、更昂贵的训练等等。512被发现是一个中间的好选择。


序列长度


在训练模型时,我们将把一大堆单词放在一起。这样做在计算上更有效率,并且它帮助模型在获取更多上下文时学习。如前所述,每个单词将用一个512维的向量(包含512个数字的列表)表示,每次我们将输入传递给模型时(也称为前向传递),我们将发送一系列的句子,而不仅仅是一个。例如,我们决定支持一个50个单词的序列。这意味着我们要取出一个句子中的x个单词,如果x > 50,我们会分割它并只取前50个,如果x < 50,我们仍然需要确保大小完全相同(我很快会解释为什么)。为了解决这个问题,我们添加填充,填充是一些特殊的虚拟字符串,添加到句子的剩余部分。例如,如果我们支持一个由7个单词组成的句子,并且句子是“Where is god”。我们添加了4个填充,所以输入模型的内容将变成“Where is god <PAD> <PAD> <PAD> <PAD>”。实际上,我们通常还会添加至少2个特殊的填充,以便模型知道句子的开始和结束位置,所以实际上会变成类似于“<StartOfSentence> Where is god <PAD> <PAD> <EndOfSentence>”。


为什么必须所有的输入向量都具有相同的大小呢?因为软件有“期待”,而矩阵有更严格的期待。你不能进行任何你想做的“数学”计算,它必须遵守某些规则,其中之一就是适当的向量大小。


位置编码


现在我们有了一种表示(和学习)词汇中单词的方法。让我们通过为单词编码位置来进一步改进。这为什么重要?因为如果我们拿这两个句子来说:


1. The man played with my cat


2. The cat played with my man


我们可以使用完全相同的嵌入来表示这两个句子,但是这两个句子有不同的含义。我们可以把这种数据看作是不考虑顺序的数据。如果我正在计算某个东西的总和,从哪里开始并不重要。在语言中,顺序通常很重要。这些嵌入包含语义意义,但不包含确切的顺序意义。它们以一定的方式保持了顺序,因为这些嵌入最初是根据某种语言逻辑创建的(baby出现在sleep附近,而不是state附近),但是同一个单词在不同上下文中可能有多个含义,更重要的是,当它处于不同上下文中时可能具有不同的含义。


将单词表示为无序文本是不够好的,我们可以通过添加位置编码来改进。我们通过计算每个单词的位置向量并将其与嵌入向量相加(求和)来实现这一点。位置编码向量必须具有相同的大小,以便可以相加。位置编码的公式使用了两个函数:对于偶数位置,使用正弦函数(例如第0个单词、第2个单词、第4个单词、第6个单词等),对于奇数位置,使用余弦函数(例如第1个单词、第3个单词、第5个单词等)。


可视化


通过观察这些函数(正弦函数为红色,余弦函数为蓝色),你也许可以想象为什么选择了这两个特定的函数。这些函数之间存在一些对称性,就像一个单词和它前面的单词之间存在一样,这有助于模型(表示)这些相关的位置。此外,它们的输出值范围在-1到1之间,这些非常稳定的数字可以很好地使用(它们不会变得非常大或非常小)。


4

5


在上述公式中,上行代表从0开始的偶数(i = 0),并继续成为偶数(21,22,2*3)。第二行以相同的方式表示奇数。


每个位置向量都是一个 number_of_dimensions (在我们的例子中为 512)向量,其数字从 0 到 1。


from math import sin, cos
max_seq_len = 50 
number_of_model_dimensions = 512

positions_vector = np.zeros((max_seq_len, number_of_model_dimensions))
for position in range(max_seq_len):
    for index in range(number_of_model_dimensions//2):
        theta = pos / (10000 ** ((2*i)/number_of_model_dimensions))
        positions_vector[position, 2*index ] = sin(theta)
        positions_vector[position, 2*index + 1] = cos(theta)
print(positions_vector)
---------------------
output:
(50, 512)



如果我们打印第一个单词,我们会发现0和1可以互换使用。


print(positions_vector[0][:10])
---------------------
output:
array([0., 1., 0., 1., 0., 1., 0., 1., 0., 1.])


第二个数字已经更加多样化。


print(positions_vector[1][:10])
---------------------
output:
array([0.84147098, 0.54030231, 0.82185619, 0.56969501, 0.8019618 ,
       0.59737533, 0.78188711, 0.62342004, 0.76172041, 0.64790587])


我们已经看到不同的位置导致不同的表示。为了将整个输入部分(在下面的图片中用红色方框标示)最终确定下来,我们将位置矩阵中的数字加到我们的输入嵌入矩阵中。这样我们最终得到一个与嵌入大小相同的矩阵,只不过这次数字包含了语义意义和顺序。


6




文章来源:https://medium.com/towards-data-science/transformers-part-2-input-2a8c3a141c7d
欢迎关注ATYUN官方公众号
商务合作及内容投稿请联系邮箱:bd@atyun.com
评论 登录
热门职位
Maluuba
20000~40000/月
Cisco
25000~30000/月 深圳市
PilotAILabs
30000~60000/年 深圳市
写评论取消
回复取消