已联系作者申请翻译授权,随后会更新授权。
今年,我们看到了机器学习的耀眼应用。OpenAI 的 GPT-2 表现出了令人印象深刻的能力,它能写出连贯而富有激情的文章,超过了我们对当前语言模型效果的预期。GPT-2 并不是一个特别新颖的架构 — 它的架构与仅有解码器的 transformer 非常相似。然而,GPT-2 是一个非常庞大的、基于 transformer 的模型,它是在一个庞大的数据集上训练出来的。本文将探究模型的架构:我们将深入了解其 self-attention ,然后看一下 transformer 在语言建模之外的应用。
本文的目标是扩充之前的文章 The Illustrated Transformer,用更多的可视化来解释 transformer 的内部工作原理,以及自最初的论文以来它们是如何发展的。我们希望这种可视化能够让以后基于 transformer 的模型更容易解释,因为它们的内部工作原理在不断进化。
第一部分:GPT-2和语言建模
那么到底什么是语言模型呢?
什么是语言模型
在 The Illustrated Word2vec 中,我们已经研究了什么是语言模型 — 它基本上是一个机器学习模型,它能够观察句子的一部分并预测下一个单词。最著名的语言模型是智能手机键盘,它能根据你当前输入的内容提示下一个单词。
就此而言,我们可以说 GPT-2 基本上与键盘预测下一个单词的功能类似,但它比你的手机所拥有的要大得多、复杂得多。 GPT-2 是在一个名为 WebText 的40GB的庞大数据集上进行训练的,作为研究工作的一部分,OpenAI 研究人员从互联网上抓取了这个数据集。比较语料库的大小,我使用的键盘应用 SwiftKey 占用了 78MB 的空间。训练过的 GPT-2 最小的变体,占用了 500MB 的存储空间。最大的 GPT-2 变体的大小是13倍,所以差不多会占用超过 6.5GB 的存储空间。
测试 GPT-2 的一个方法是使用 AllenAI GPT-2 Explorer。它使用 GPT-2 来显示下一个单词的十种预测(以及它们的概率)。你可以选择一个词,然后看到下一个预测列表,继续写这段话。
语言建模的 transformer
正如我们在 The Illustrated Transformer 中所看到的那样,最初的 transformer 模型是由一个编码器和一个解码器组成的 — 每一个都可以称之为 transformer block 的堆栈。这种架构是合理的,因为该模型处理的是机器翻译 — 这是一个编码器-解码器架构在过去取得成功的关键。
随后的很多研究工作中,该架构舍弃了编码器或解码器,只使用一叠 transformer block --将它们尽可能堆高,向它们输入大量的训练文本,并向它们灌输大量的计算量(训练其中一些语言模型需要数十万美元,比如 AlphaStar 可能需要数百万美元)。
我们可以把这些块堆得多高?这也是区分不同 GPT-2 模型尺寸的主要因素之一。
与BERT的一个区别
机器人第一定律
机器人不得伤害人类,也不得因不作为而使人类受到伤害。
GPT-2 是使用 transformer 解码器块构建的,而 BERT 则使用 transformer 编码器块。我们将在下面的一节中研究两者的区别。但两者之间有一个关键的区别是,GPT-2 和传统的语言模型一样,每次只输出一个 token 。例如,让我们看一个训练有素的 GPT-2 背诵机器人第一定律。
这些模型的实际工作方式是,在每个 token 产生后,就被添加到输入序列中。而这个新的序列就成为模型下一步的输入。这就是一个叫做 “自动回归 “的想法,这也是让 RNNs 变得极其有效的思想之一。
GPT-2,以及后来的一些模型,如TransformerXL和 XLNet,都是自动回归性质的。BERT不是。这是一种取舍。在失去自动回归的同时,BERT 获得了结合单词两边上下文的能力,以获得更好的结果。XLNet 集成了自动回归,同时找到了另一种方式来结合两边的上下文。
transformer block 的进化
最初的 transformer 论文介绍了两种类型的 transformer block。
编码器模块
首先是编码器块。
原 transformer 论文的编码器块可以接受输入,直到一定的最大序列长度(例如512个 token )。如果一个输入序列比这个限制短,也没关系,我们可以把序列的其余部分 pad 起来。
解码器模块
其次是解码器块,它与编码器块在架构上有一个小小的变化 — 让它关注编码器的特定段。
这里的 self-attention 层有一个关键的区别,那就是它屏蔽了后续的标记 — 不像 BERT 那样把单词改成[mask],而是通过干扰 self-attention 计算屏蔽了被计算位置右边的标记信息。
例如,如果我们要高亮第4号位置的路径,我们可以看到,它只允许添加现在和以前的标记。
很重要的一点是,要明确 self-attention(BERT使用的)和加 masking 的 self-attention( GPT-2 使用的)之间的区别。一个普通的 self-attention 块允许一个位置在它右边的标记处达到峰值。加 masking 的 self-attention 则可以防止这种情况发生。
仅有解码器的块。
在原论文之后, Generating Wikipedia by Summarizing Long Sequences 提出了另一种能够构建语言模型的 transformer block 的安排。这个模型放弃了transformer 编码器。为此,我们称这个模型为 “transformer-decoder“。这个早期的基于 transformer 的语言模型是由六个 transformer 解码器块堆叠而成的。
这些块与原来的解码器块非常相似,只是他们取消了第二个 self-attention 层。Character-Level Language Modeling with Deeper Self-Attention 也研究了类似的架构:创建一个每次预测一个字母/字符的语言模型。
OpenAI 的 GPT-2 模型使用了这些仅有解码器的块。
GPT-2 的内部
Look inside and you will see, The words are cutting deep inside my brain. Thunder burning, quickly burning, Knife of words is driving me insane, insane yeah. — Budgie
我们有一个训练好的 GPT-2 ,看看它是如何工作的。
运行 GPT-2 最简单的方法是让它自说自话(技术上称为生成无条件样本 )— 或者,我们可以给它提示,让它说某个特定话题(也就是生成交互式条件样本)。在自说自话的情况下,我们可以简单地把开始的 token 交给它,让它开始生成单词(训练后的模型使用<|endoftext|>
作为它的起始 token 。我们用<s>
来代替)。
该模型只有一个输入 token ,因此该路径将是唯一的活跃路径。该 token 在所有层中连续处理,然后沿着该路径产生一个向量。该向量可以根据模型的词汇量( GPT-2 是50000个单词)进行评分。在这种情况下,我们选择了概率最高的 token ,“the“。但是,我们当然也可以把事情混为一谈 — 你知道,如果你一直点击键盘应用中的建议词,它有时会陷入重复的循环,唯一的出路就是你点击第二个或第三个建议词。同样的情况也会发生在这里。 GPT-2 有一个叫做 top-k 的参数,我们可以用它来让模型考虑采样除 top-k 以外的词(top-k=1 的情况下)。
在下一步,我们将第一步的输出添加到我们的输入序列中,让模型进行下一个预测。
请注意,在这次计算中,只有第二条路径是活跃的。 GPT-2 的每一层都保留了自己对第一个 token 的解释,并将在处理第二个 token 时使用它(我们将在下一节关于 self-attention 的内容中详细介绍)。 GPT-2 不会根据第二个 token 重新解释第一个 token 。
更深入地了解内部
输入编码
让我们看看更多的细节,更深入地了解这个模型。让我们从输入开始。与我们之前讨论过的其他 NLP 模型一样,模型会在其嵌入矩阵中查找输入词的嵌入 — 这是我们作为训练模型的一部分得到的组件之一。
所以一开始,我们在嵌入矩阵中查找起始 token <s>
的嵌入。在将其交给模型中的第一个块之前,我们需要加入位置编码 — 一个向 transformer block 指示序列中词的顺序的信号。训练模型的一部分是一个矩阵,它包含了输入中1024个位置中每个位置的位置编码向量。
至此,我们已经涵盖了输入词在被交给第一个 transformer block 之前是如何处理的。我们还知道了构成训练后的 GPT-2 的两个权重矩阵。
栈的旅程
现在,第一个块可以通过先通过 self-attention 处理,然后通过其神经网络层来处理 token 。一旦第一个 transformer block 处理了 token ,它就会将其产生的向量送上堆栈,由下一个块处理。每个块的处理过程是相同的,但每个块在 self-attention 和神经网络子层中都有自己的权重。
self-attention 回顾
语言在很大程度上依赖于语境。例如,看看第二定律。
机器人第二定律
机器人必须服从人类给它的命令,除非这种命令会与第一法则冲突。
我们强调了句子中三处指代其他概念的表述。如果不结合这些表述所指的语境,就无法理解或处理这些词。当一个模型处理这个句子的时候,它必须知道:
- 它指的是机器人
- 这种命令指的是法律的前半部分,即 “人的命令“。
- 第一定律是指整个机器人第一定律。
这就是 self-attention 的作用。它在处理某个词之前,将模型对解释某个词的上下文和关联词的理解加入进去(通过神经网络)。它通过给片段中每个词的相关程度赋分,并加入到它们的向量表示。
举个例子,下图中的这个 self-attention 在处理 “它 “这个词的时候,它关注的是 “一个机器人“。它将传递给它的神经网络的向量是三个词的向量之和乘以它们的分数。
self-attention 过程
沿着每个 token 的路径进行 self-attention 处理。self-attention 重要组成部分是三个向量。
- Query:query是当前词对所有其他词进行评分的表示(使用它们的key)。我们只关心我们当前正在处理的 token 的 query。
- Key:key向量就像段中所有单词的标签。它们是我们搜索相关词时的匹配对象。
- Value:value向量是实际的单词表示,一旦我们对每个单词的相关程度进行了评分,这些值就是我们加起来表示当前单词的值。
一个粗略的比喻是,把它想成是在档案柜里搜索。query 就像一张便签,上面写着你要研究的主题。key 就像柜子里面文件夹的标签。当你把标签和便签匹配起来的时候,我们就会把这个文件夹的内容拿出来,这些内容就是 value 向量。只不过你要找的不仅仅是一个值,而是混合文件夹中的值的混合。
将 query 向量乘以每个 key 向量,就会产生每个文件夹的得分(技术上:点乘,然后用 softmax 计算)。
我们将每个值乘以它的分数,然后相加 — 得出我们的 self-attention 结果。
这种 value 向量加权混合的结果是,向量把50%的 “注意力“ 给了 robot 这个词,30%给了 a 这个词,19%给了 it 这个词。在后面的文章中,我们会更深入地了解 self-attention,我们先继续研究模型输出的堆栈。
模型输出
当模型中顶部的块产生输出向量时(自己的 self-attention ,然后是它的神经网络的结果),模型将该向量乘以嵌入矩阵。
回想一下,嵌入矩阵中的每一行对应于模型词汇中的一个词的嵌入。这个乘法的结果可以视为模型词汇中每个词的得分。
我们可以简单地选择得分最高的 token (top-k = 1)。但如果模型也考虑其他词,则会取得更好的效果。所以更好的策略是从整个列表中抽取一个词,用分数作为选择该词的概率(所以分数越高的词被选中的机会越大)。一个策略是将 top-k 设置为40,让模型考虑分数最高的40个单词。
这样,模型就完成了一次迭代,输出了一个词。该模型继续迭代,直到生成整个上下文(1024个标记),或直到产生一个序列结束的标记。
第一部分结束
这就是我们所看到的 GPT-2 的工作原理进行了分析。如果你很想知道 self-attention 层里面到底发生了什么,那么下面的 bonus 就是为你准备的。我创建它是为了用更多的可视化来描述 self-attention ,以方便描述今后的 transformer 模型(看着 TransformerXL和XLNet)。
我想指出这篇文章中的一些过于简单化的地方。
- 我互换着使用 “words“ 和 “token”。但实际上,GPT-2使用 Byte Pair Encoding 来创建其词汇中的 token,这意味着它们通常是单词的一部分。
- 我们展示的例子是在推理/评估模式下运行 GPT-2。这就是为什么它每次只处理一个单词的原因。在训练时,该模型将针对较长的文本序列进行训练,并同时处理多个 token。另外在训练时,模型会处理更大的批处理量(512),这与评估使用的批处理量的大小一致。
- 我在旋转/转换向量时采取了自由的策略,以更好地管理图像中的空间。在实现的时候,必须更加精确。
- transformer 使用了大量的层归一化(layer normalization),这是相当重要的。我们在 Illustrated Transformer 一文中已经注意到了其中的一些内容,但在这篇文章中更多的是侧重于 self-attention。
- 有时候,我需要显示更多的盒子来表示一个向量。我将这些表示为 “zoom in”。例如:
第二部分:图解 self-attention
在前面的文章中,我们用下面这张图来展示了 self-attention 被应用在网络中的一层里处理 “it“ 这个词。
在本节中,我们将看看这一点的实现细节。请注意,我们会通过它处理单个词的方式来研究,所以我们会展示许多单个词向量。实际的实现是通过将巨大的矩阵相乘来完成的,但我们想把重点放在词层面上。
self-attention (无遮蔽)
我们先来看一下编码器块中计算的原始 self-attention 。我们来看一个模拟的 transformer block ,一次只能处理四个 token 。
self-attention 的应用主要通过三个步骤。
- 为每个路径创建 query、key 和 value 向量。
- 对于每个输入 token,使用它的 query 向量对所有其他 key 向量进行打分;
- 将 value 向量乘以相关分数后相加。
1- 创建查询、键和值向量。
让我们把重点放在第一条路径上。我们将把它的 query,和所有的 key 进行比较。这样就会产生每个 key 的得分。self-attention 的第一步是计算每个 token 路径的三个向量(我们暂时忽略 attention head)。
2-打分
现在我们已经有了向量,我们只在步骤2中使用 query 和 key 向量。由于我们关注的是第一个 token,我们将它的 query 乘以所有其他的 key 向量,从而得出四个 token 的得分。
3-求和
现在我们可以将分数乘以 value 向量。分数高的值,在我们将它们相加后,将构成结果向量的很大一部分。
如果我们对每个路径进行同样的操作,我们最终会得到一个代表每个 token 的向量,其中包含该 token 的对应上下文。然后,这些会被呈现给 transformer block 中的下一个子层(前馈神经网络)。
图解掩蔽式 Self-Attention
现在我们已经了解了 transformer 的 self attention 步骤里的内容,我们继续看掩蔽式 self attention。掩蔽式 self-attention 与 self-attention 完全相同,除了第2步的时候。假设模型只有两个 token 作为输入,而我们正在观察第二个 token。在这种情况下,最后两个 token 被掩盖了。所以模型干扰了打分步骤。它将未来的 token 打分为0,所以模型无法被未来的词激活。
然后,我们将注意力遮罩三角形贴上。它将我们要屏蔽的单元格设置为负无限或一个非常大的负数(例如GPT-2中的-10亿)。
然后,在每一行上使用 softmax 生成 self-attention 的实际分数。
这个分数表的意思是:
- 当模型处理数据集中的第一个例子(第1行),其中只包含一个词(”robot”)时,100%的注意力将集中在这个词上。
- 当模型处理数据集中的第二个例子(第2行)时,其中包含了 “robot must“ 这两个词,当它处理 “must“ 这个词时,48%的注意力将放在 “robot” 上,52%的注意力将放在 “must” 上。
- 以此类推
GPT-2 的遮蔽式 self-attention
让我们来详细了解一下 GPT-2 的遮蔽式 self-attention。
评估时间:一次处理一个 token
我们可以让 GPT-2 完全按照遮蔽式 self-attention 的工作原理进行操作。但是在评估过程中,当我们的模型在每次迭代后只增加一个新词时,如果沿着先前的路径重新计算已经处理过的 token 的 self-attention ,效率会很低。
在这种情况下,我们处理第一个token(暂时忽略<s>
)。
GPT-2持有a
token 的 key 和 value 向量。每一个自关注层都保持着该 token 的 key 和 value 向量。
在下一次迭代中,当模型处理 robot
这个词时,它不需要为 a
token 查询query、key 和 value 。它只是重用第一次迭代中保存的 query、key 和 value。
GPT-2 self-attention:1 - 创建query、key 和 value
我们假设模型正在处理 it
这个词。如果我们说的是底层的块,那么它对该 token 的输入将是it
的嵌入和#9槽的位置编码。
transformer 中的每一个块都有自己的权重(在后面的文章中分解)。我们首先遇到的是我们用来创建 query、key 和 value 的权重矩阵。
乘法的结果是一个向量,这个向量基本上是 it
这个词的 query、key 和 value 向量的连接。
GPT-2 self-attention:1.5 - 分割成 attention heads
前面的例子直接分析了 self-attention ,忽略了“多头“机制。我们现在对这个概念进行一些说明:self-attention 是在 query,key 和 value 向量的不同部分进行多次。“拆分“ attention heads 只是将长向量重塑成一个矩阵。small GPT-2 有12个 attention heads ,所以这是重塑矩阵的第一维。
在前面的例子中,我们看了一个 attention heads 里面的情况。一种看待多个 attention heads 的方法是这样的(如果我们要只可视化十二个 attention heads 中的三个)。
GPT-2 self-attention:2 - 打分
现在我们可以继续打分了 — 我们只看其中一个 attention head(其他的都在进行类似的操作)。
现在这个 token 可以对其他 token 的所有 key 进行评分(在之前的迭代中,在 attention heads #1中计算过)。
GPT-2 self-attention:3 - 求和
正如我们之前所看到的,我们现在将每个值与其分数相乘,然后将它们相加,得出attention head #1的 self-attention 结果。
GPT-2self-attention:3.5 - 合并 attention
我们处理各种 attention heads 的方法是,我们先把它们合并成一个向量。
但是向量还没有准备好被发送到下一个子层。我们需要先把这个由隐藏状态组成的怪物变成一个同质的表示。
GPT-2 self-attention:4 - 投影
我们会让模型学习如何最好地将合并的 self-attention 结果映射成前馈神经网络可以处理的向量。下面是我们的第二个大权重矩阵,它将 attention heads 的结果投射到 self-attention 子层的输出向量中。
有了这个,我们就产生了我们可以沿着下一层发送的向量。
GPT-2全连接神经网络:第1层
全连接的神经网络是指块在 self-attention 将适当的上下文包含在其表示中后,处理其输入 token。它是由两层组成的。第一层是模型大小的四倍(因为GPT-2小是768,所以这个网络会有768*4=3072个单元)。为什么是四倍呢?这是原来 transformer 的尺寸(模型尺寸是512,该模型中的第1层是2048)。这为 transformer 模型提供了足够的表示能力来处理目前的任务。
GPT-2全连接神经网络:第2层 - 投射到模型维度
第二层将第一层的结果投影回模型维度(small GPT-2 为768)。这个乘法的结果就是这个 token的 transformer block 的结果。
你成功了!
这就是我们将进入的最详细的 transformer block 版本! 你现在几乎已经掌握了语言模型 transformer 内部发生的绝大部分情况。总结一下,我们的输入向量会遇到这些权重矩阵。
而每个块都有自己的这些权重集。另一方面,该模型只有一个 token 嵌入矩阵和一个位置编码矩阵。
如果你想看模型的所有参数,那么我在这里统计了一下。
由于某些原因,他们加起来的参数是124M,而不是117M。我不知道为什么,但在发布的代码中似乎就是这么多(如果我错了,请纠正我)。
第三部分:超越语言建模
仅仅是解码器的 transformer 不断显示出超越语言建模的前景。有很多应用已经显示出它的成功,这些应用可以用类似上面的可视化来描述。让我们通过查看其中的一些应用来结束这篇文章吧。
机器翻译
不需要编码器来进行翻译。同样的任务也可以由一个仅有解码器的 transformer 来完成。
文本总结
这就是第一个只用解码器的 transformer 被训练的任务。也就是说,它被训练成阅读一篇维基百科文章(没有目录前的开头部分),并对其进行总结。文章的实际开头部分被用作训练数据集的标签。
论文针对维基百科的文章对模型进行了训练,因此训练出来的模型能够对文章进行总结。
迁移学习
Sample Efficient Text Summarization Using a Single Pre-Trained Transformer 一文首先对语言建模进行预训练,然后对解码器进行微调,以进行总结。事实证明,在有限的数据环境下,它比预训练的编码器-解码器 transformer 取得了更好的效果。
GPT-2论文还展示了在语言建模上预训练模型后的总结结果。
音乐生成
Music transformer 使用一个仅有解码器的 transformer 来生成具有表现力的音乐。音乐建模就像语言建模一样 — 只是让模型以一种无监督的方式学习音乐,然后让它采样输出(就是我们前面所说的 “漫不经心”)。
你可能会好奇,在这种情况下,音乐是如何被表示的。请记住,语言建模可以通过字符、单词或作为单词一部分的标记的向量表示来完成。对于音乐的表现(我们先想想钢琴),我们不仅要表示音符,还要表示速度 — 衡量钢琴键被按下的力度。
一个表演就是一系列的 one-hot 向量。一个 midi 文件可以转换为这样的格式。本文的输入序列示例如下。
这个输入序列的 one-hot 向量表示方法是这样的。
我喜欢文中的一个可视化,展示了 music transformer 中的 self-attention。我在这里给它加了一些注释。
如果你不清楚这种对音符的表述,请看这个视频。
结论
至此,我们对 GPT-2,以及对它的母体 — 纯解码 transformer 的探索之旅就结束了。我希望你在看完这篇文章后,对 selt-attention 有了更好的理解,对 transformer 内部的情况有了更多的了解。
相关资源
- OpenAI 的 GPT-2 Implementation。
- 请查看 huggingface 的 pytorch-transformers 库,除了GPT-2,它还实现了BERT、Transformer-XL、XLNet 等前沿的 transformer 模型。
—
鸣谢
感谢Lukasz Kaiser、Mathias Müller、Peter J. Liu、Ryan Sepassi 和Mohammad Saleh 对本文早期版本的反馈。
评论或更正?请发推特给我@JayAlammar