前言
总在AI的世界遇到很多新名词,一直在想有没有万变不离其宗的底层逻辑。我一直认为人的脑容量是很小的,站在技术的角度,记不了那么多内容的。
今天趁着假期的时间站在数据结构和算法的角度拆解一下 GPT。
本文 = 自己的理解 + 参考文章的翻译
学习资料:
站在一个较高的维度先来看下 Transformer
我们先把 Transformer 看作一个"单体黑盒"。在机器翻译应用程序中,输入是一种语言,输出是另一种语言。如下图所示:
扒开它的精美包装,我们可以看到的是:encoding component + connections + decoding component。
- encoding component = 一系列 encoders
- decoding component = 一系列 decoders
本文以6个encodes为例,事实上只要有利于你的实现,可以是任意数量的。
这些 encoders 结构相同,但是权重不同。每一个都可以划分成两个子层:
encoder 的输入首先进入 self-attention 层 -- 本层帮助 encoder 在 encode 一个 word 的时候能够关注到其他 word。稍后我们更加详细地分析 self-attention 。
self-attention 的输出被喂给 feed-forward neural network。每个单词都会独立地喂给完全相同的 feed-forward neural network。
decoder 拥有上述两层的同时,在中间还有一层 attention 层,这一层帮助 decoder 关注输入的句子的关联性。
张量(Tensors)是什么?
现在我们已经看到了 Transformer 的主要组成部分。下面我们来看一下向量/张量(vectors/tensors)是怎么在这些组件之间流动的,从而将训练好的 Transformer 的输入转化为输出。
与一般的 NLP 应用一样,我们首先使用嵌入算法将每个输入词转换为向量。
嵌入操作仅发生在最底层的 encoder 中。所有 encoder 的共同抽象是,他们接收一个向量列表,每个向量的长度为 512--对于底部 encoder 而言是 word embeddings,但是在其他encoder中,它将是正下层的 encoder 的输出。这个列表的长度是一个我们可以设置的超参数——大致上,可以把它设为训练数据集中最长句子的长度。
列表:
假设我们把“我喜欢苹果”分成三个词。
我 喜欢 苹果 ↓ ↓ ↓ 向量1 向量2 向量3为了方便展示,下面暂时用 4 维向量,数值都是我编的:
sentence_vectors = [ [0.1, 0.3, -0.2, 0.5], # “我”的向量 [0.6, -0.1, 0.4, 0.2], # “喜欢”的向量 [0.2, -0.5, 0.8, 0.1], # “苹果”的向量 ]此时列表的长度是 3。如果用本例”我喜欢苹果“,则这个例子的输入就是一个
3 × 512的矩阵:3 个词,每个词用 512 个数字表示。超参数:
我们为模型或训练过程设置的配置。
例如,假设我们作出这样的配置:
max_seq_length = 5 # 每条输入序列最多保留 5 个 token。
在经历 word embedding 之后,每个向量都要经过相同的两层处理,如下图所示:
这里我们可以看到 Transformer 中的一个关键特性,那就是每个位置的单词在 encoder 中都沿着他自己的路径流动。自注意力层中的这些路径之间存在依赖关系。然而,前馈层没有这些依赖关系,因此,在流经前馈层时,各种路径可以并行执行。
下面我们用一个简单的例子演示 encoder 子层级中发生的变化。
OK!开始 encode
正如我们上面提到的,encoder 接收向量列表作为输入。首先交给 self-attention 层处理,然后交给 feed-forwars neural network 处理,然后将输出交给下一个 encoder。
站在一个较高的维度来看 self-attention
不要被我频繁提及“自我关注”这个词所迷惑,好像这是一个每个人都应该熟悉的概念。让我们来详细了解一下它的运作原理。
本论文讲解了注意力机制,有兴趣的小伙伴自行研究。
下文中提到的论文均指本论文。
假如下面这句话是我们要翻译的:
”The animal didn't cross the street because it was too tired”
这句话中的 "it" 是什么呢?指的是 "street" 还是 "animal"?对人类来说这个问题很简单,但是对算法来说就不简单了。
当模型处理 "it" 的时候,self-attention 机制使其能够将 "it" 和 "animal" 关联起来。
当模型处理每个单词(输入序列中的每个位置)时,self-attention 机制使其能够查看输入序列中的其他位置,寻找有助于更好地编码该单词的线索。
如果你熟悉 RNN,可以想想,维护一个隐藏状态是如何让 RNN 将其先前处理的单词/向量的表示与当前正在处理的单词/向量的表示结合起来的。自注意力是 Transformer 用来将其他相关词语的“理解”融入到我们当前正在处理的词语中的方法。
RNN(Recurrent Neural Network):
RNN 一边写,一边记录hidden state,隐藏状态。这种隐藏状态用于表示前面已经处理过的信息。
它是怎样把前面的信息带过来的?
我 喜欢 苹果 ↓ ↓ ↓ x1 x2 x3这里的
x1、x2、x3,分别表示三个 token 的词嵌入向量。RNN 的处理过程可以写成下面这样。
h0是初始隐藏状态,通常可以设为全零向量。第 1 步:处理“我” 当前输入 x1 + 初始状态 h0 ↓ RNN 计算 ↓ 得到新状态 h1 第 2 步:处理“喜欢” 当前输入 x2 + 上一步状态 h1 ↓ RNN 计算 ↓ 得到新状态 h2 第 3 步:处理“苹果” 当前输入 x3 + 上一步状态 h2 ↓ RNN 计算 ↓ 得到新状态 h3这里的“+”表示一起参与计算,不是说直接把两个向量简单相加。关键就在于:处理“苹果”时,传进来的不只是“苹果”的向量,还有
h2。而h2是在处理“我”“喜欢”之后得到的状态,因此前面的信息有机会影响当前计算。 隐藏状态并不只是“上一个词的向量”,而是经过逐步更新的历史信息表示。本文引用RNN主要是为了表明:RNN 和 Transformer 都需要利用上下文,但实现方式不同。
查看 Tensor2Tensor notebook,可以在其中加载 Transformer 模型,并使用此交互式可视化工具对其进行检查。
Dive into Self-Attention
我们先来看看如何使用向量计算 self-attention,然后再来看看它是如何实际使用矩阵实现的。
self-attention 计算的第一步是从每个 encoder 的输入向量中创建三个向量。如下图所示,我们创建了a Query vector, a Key vector, and a Value vector。这些向量是通过将 embedding 向量乘以我们在训练过程中训练的三个矩阵而生成的。
Multiplying x1 by the WQ weight matrix produces q1, the "query" vector associated with that word. We end up creating a "query", a "key", and a "value" projection of each word in the input sentence.
什么是“query”、“key”和“value”向量?
它们是用于计算和思考注意力的抽象概念。继续阅读下文了解注意力的计算方法后,你就能掌握每个向量所扮演的角色。
self-attention 计算的第二步是计算得分。假设我们要计算本例中第一个词“Thinking”的self-attention。我们需要将输入句子中的每个词与这个词进行比较并给出分数。该分数决定了在对特定位置的单词进行编码时,应该给予输入句子的其他部分多少关注。
得分是通过分别计算 query vector 与待评分词的 key vector 的点积得到的。因此,如果我们正在处理位置 #1 的单词的自我注意力,则第一个分数将是 q1 和 k1 的点积。第二个分数将是 q1 和 k2 的点积。
第三步和第四步是将分数除以 8(论文中使用的关键向量维度 64 的平方根这样做可以获得更稳定的梯度。这里可能还有其他值,但这是默认值),然后将结果传递给 softmax 函数进行处理。Softmax 将分数标准化,使它们全部为正数且总和为 1。
softmax 分数决定了每个词在该位置的出现频率。显然,该位置的词将具有最高的 softmax 分数。
第五步是将每个值向量乘以 softmax 分数(准备将它们相加)。这里的想法是保持我们想要关注的词语的值不变,并淹没无关的词语(例如,通过将它们乘以 0.001 等很小的数字)。
第六步是将加权值向量相加。这就产生了该位置(第一个词)self-attention 层的输出。
self-attention 机制的计算到此结束。得到的向量可以传递给 feed-forward neural network 。然而,在实际实现中,为了加快处理速度,这种计算是以矩阵形式进行的。既然我们已经了解了单词层面的计算原理,那么现在让我们来看看具体情况。
自注意力矩阵计算
第一步是计算 Query 矩阵、Key 矩阵 和 Value 矩阵。我们通过将 word embeddings 打包成矩阵 X,并将其与我们训练好的权重矩阵(WQ、WK、WV)相乘来实现这一点。
Every row in the X matrix corresponds to a word in the input sentence. We again see the difference in size of the embedding vector (512, or 4 boxes in the figure), and the q/k/v vectors (64, or 3 boxes in the figure)
最后,由于我们处理的是矩阵,我们可以将步骤二到六合并为一个公式来计算自注意力层的输出。
The self-attention calculation in matrix form
多头注意力
论文中通过添加一种称为“multi-headed”注意力机制,进一步改进了 self-attention 层。这从两个方面提高了 attention 层的性能:
- 它扩展了模型关注不同位置的能力。在上面的例子中,z1 包含了其他所有编码的一小部分,但它可能主要由单词本身构成。如果我们要翻译像“The animal didn’t cross the street because it was too tired”这样的句子,那么知道“it”指的是哪个词就很有用了。
- 它为注意力层提供了多个“representation subspaces”。接下来我们将看到,采用 multi-headed attention 机制后,我们不仅会拥有一组 Query/Key/Value 权重矩阵,而是拥有多组。Transformer 使用八个注意力头,因此每个 encoder/decoder 最终都会有八组注意力头。每组权重矩阵都是随机初始化的。然后,经过训练后,每组都用于将input embeddings(or vectors from lower encoders/decoders)投影到不同的 representation subspace。
With multi-headed attention, we maintain separate Q/K/V weight matrices for each head resulting in different Q/K/V matrices. As we did before, we multiply X by the WQ/WK/WV matrices to produce Q/K/V matrices.
如果我们重复上述自注意力计算,每次使用不同的权重矩阵,重复八次,最终会得到八个不同的 Z 矩阵。
这将给我们带来一些挑战。feed-forward 层并不期望接收到八个矩阵,它期望的是一个矩阵(每个词对应一个向量)。因此,我们需要一种方法将这八个矩阵合并成一个矩阵。
我们该如何实现呢?我们将矩阵连接起来,然后乘以一个额外的权重矩阵WO。
这就是 multi-headed self-attention 的基本内容。这涉及的矩阵相当多。让我试着把它们全部放在一个图表里,这样我们就可以在一个地方查看它们了。
既然我们已经谈到了注意力头,让我们回顾一下之前的例子,看看在对例句中的单词“it”进行编码时,不同的注意力头分别集中在哪里:
As we encode the word "it", one attention head is focusing most on "the animal", while another is focusing on "tired" -- in a sense, the model's representation of the word "it" bakes in some of the representation of both "animal" and "tired".
使用位置编码表示序列的顺序
到目前为止,我们所描述的模型还缺少一种方法来考虑输入序列中单词的顺序。
为了解决这个问题,Transformer 会向每个input embedding 添加一个向量。这些向量遵循模型学习到的特定模式,这有助于模型确定每个单词的位置,或者序列中不同单词之间的距离。这里的想法是,将这些值添加到 embeddings 向量中,一旦它们被投影到 Q/K/V 向量中,并在点积注意力期间,就可以在嵌入向量之间提供有意义的距离。
To give the model a sense of the order of the words, we add positional encoding vectors -- the values of which follow a specific pattern.
如果我们假设 embedding 的维度为 4,那么实际的位置编码将如下所示:
A real example of positional encoding with a toy embedding size of 4.
What might this pattern look like?
在下图中,每一行对应于一个向量的位置编码。因此,第一行对应的向量就是我们要添加到输入序列中第一个词的 embedding 向量中的向量。每一行包含 512 个值——每个值介于 1 和 -1 之间。我们用颜色对它们进行了编码,以便可以看到其中的规律。
A real example of positional encoding for 20 words (rows) with an embedding size of 512 (columns). You can see that it appears split in half down the center. That's because the values of the left half are generated by one function (which uses sine), and the right half is generated by another function (which uses cosine). They're then concatenated to form each of the positional encoding vectors.
论文(第 3.5 节)中描述了位置编码的公式。您可以在 get_timing_signal_1d() 函数中看到生成位置编码的代码。但这并非位置编码的唯一方法。然而,它的优势在于能够扩展到前所未有的序列长度。
至此,我解决了我一直好奇的问题了,下面就不继续翻译了。有兴趣的小伙伴可以去阅读原文。
写在最后,Qwen 7B 中的 7B 指的是什么?
Qwen 7B ↓ 大约 70 亿个可训练数字 ↓ 这些数字分布在很多矩阵/向量里 ↓ 其中就包括: WQ WK WV WO 以及: Embedding 矩阵 FFN 的权重矩阵 LayerNorm 参数 输出层权重 ……