GPT-2

views 915 words

GPT-2在文本生成上有着惊艳的表现, 其生成的文本在上下文连贯性和情感表达上都超过了人们的预期. 仅从模型架构而言, GPT-2 并没有特别新颖的架构, 它和只带有解码器的 transformer 模型很像. GPT-2 有着超大的规模, 是一个在海量数据集上基于 transformer 训练的巨大模型.

GPT-2 和语言建模

语言模型(language model)

简单说来, 语言模型的作用就是根据已有句子的一部分, 来预测下一个单词会是什么. 最著名的语言模型你一定见过, 就是手机上的输入法, 它可以根据当前输入的内容智能推荐下一个词.

从这个意义上说, 可以说 GPT-2 基本上相当于输入法的单词联想功能. OpenAI 的研究人员使用了一个从网络上爬取的 40GB 超大数据集https://skylion007.github.io/OpenWebTextCorpus/训练 GPT-2.

可以用「AllenAI GPT-2 Explorer」来体验 GPT-2 模型. 它可以给出下一个单词可能性排名前十的概率, 通过选择其中一个单词, 然后得到下一个可能单词的列表, 如此往复, 最终完成一篇文章的撰写.

使用 Transformers 进行语言建模

原始的 transformer 模型由编码器(encoder)和解码器(decoder)组成, 二者都是由被称为「transformer 模块」的部分堆叠而成. 这种架构在机器翻译任务中取得的成功证实了它的有效性, 值得一提的是, 这个任务之前效果最好的方法也是基于编码器-解码器架构的.

Transformer 的许多后续工作尝试去掉编码器或解码器, 也就是只使用一套堆叠得尽可能多的 transformer 模块, 然后使用海量文本、耗费大量的算力进行训练.

那么究竟能将这些模块堆叠到多深才有效?事实上, 这个问题的答案也就是区别不同 GPT-2 模型的主要因素之一, 如下图所示. 「小号」的 GPT-2 模型堆叠了 12 层, 「中号」24 层, 「大号」36 层, 还有一个「特大号」堆叠了整整 48 层.

与 BERT 的区别

兩者的差別則在於進行無監督式訓練時選用的訓練目標以及使用的模型有所不同:

  • GPT 選擇 Transformer 裡的 Decoder,訓練目標為一般的語言模型,預測下個单词
  • BERT 選擇 Transformer 裡的 Encoder,訓練目標則為了漏字填空以及下句預測

GPT-2 是使用「transformer Decoder模块」构建的, 而 BERT 则是通过「transformer Encoder」模块构建的. GPT-2 就像传统的语言模型一样, 一次只输出一个单词(token). 例子:

First Law of Robotics:

A robot may not injure a human being or, through inaction, allow a human being to come to harm.

gpt-2-output

这种模型之所以效果好是因为在每个新单词产生后, 该单词就被添加在之前生成的单词序列后面, 这个序列会成为模型下一步的新输入. 这种机制叫做自回归(auto-regression), 这也是令 RNN 模型效果拔群的重要思想.

gpt-2-autoregression-2

GPT-2, 以及一些诸如 TransformerXL 和 XLNet 等后续出现的模型, 本质上都是自回归模型, 而 BERT 则不然. 这就是一个权衡的问题了, 虽然没有使用自回归机制, 但 BERT 获得了结合单词前后的上下文信息的能力, 从而取得了更好的效果. XLNet 使用了自回归, 并且引入了一种能够同时兼顾前后的上下文信息的方法.

  1. 语言模型:Bert和GPT-2虽然都采用transformer,但是Bert使用的是transformer的encoder,即:Self Attention,是双向的语言模型;而GPT-2用的是transformer中去掉中间Encoder-Decoder Attention层的decoder,即:Marked Self Attention,是单向语言模型
  2. 结构:Bert是pre-training + fine-tuning的结构;而GPT-2只有pre-training。
  3. 输入向量:GPT-2是token embedding + prosition embedding;Bert是 token embedding + position embedding + segment embedding
  4. 参数量:Bert是3亿参数量;而GPT-2是15亿参数量
  5. Bert引入Marked LM和Next Sentence Prediction;而GPT-2只是单纯的用单向语言模型进行训练,没引入这两个
  6. Bert不能做生成式任务,而GPT-2可以

Transformer 模块的演进

原始的 transformer 论文引入了两种类型的 transformer 模块, 分别是:Encoder 和 Decoder.

1. Encoder模块

首先是编码器(encoder)模块:

原始 transformer 论文中的Encoder模块可以接受长度不超过最大序列长度(如 512 个单词)的输入. 如果序列长度小于该限制, 就在其后填入预先定义的空白单词(如上图中的).

2. Decoder模块

其次是解码器(decoder)模块, 它与编码器模块在架构上有一点小差异——加入了一层使得它可以重点关注Encoder输出的某一片段, 也就是下图中的encoder-decoder self-attention层.

解码器在自注意力(self-attention)层上还有一个关键的差异:它将后面的单词掩盖掉了. 但并不像 BERT 一样将它们替换成特殊定义的单词, 而是在自注意力计算的时候屏蔽了来自当前计算位置右边所有单词的信息.

举个例子, 如果重点关注 4 号位置单词及其前续路径, 可以发现模型只允许注意当前计算的单词以及之前的单词

能够清楚地区分 BERT 使用的自注意力(self-attention)模块和 GPT-2 使用的带掩模的自注意力(masked self-attention)模块很重要. 普通的自注意力模块允许一个位置看到它右侧单词的信息(如下左图), 而带掩模的自注意力模块则不允许这么做(如下右图).

3. 只包含Decoder(Decoder-Only)的模块

在 transformer 原始论文发表之后, 一篇名为「Generating Wikipedia by Summarizing Long Sequences」的论文提出用另一种 transformer 模块的排列方式来进行语言建模——它直接扔掉了所有的 transformer Encoder模块. 这个早期的基于 transformer 的模型由 6 个 transformer Decoder模块堆叠而成:

图中所有的Decoder模块都是一样的, 可以看见, 它使用了带掩模的自注意力层. 请注意, 该模型在某个片段中可以支持最长 4000 个单词的序列, 相较于 transformer 原始论文中最长 512 单词的限制有了很大的提升.

这些解码器模块和 transformer 原始论文中的解码器模块相比, 除了去除了第二个自注意力层之外, 并无很大不同. 一个相似的架构在字符级别的语言建模中也被验证有效, 它使用更深的自注意力层构建语言模型, 一次预测一个字母/字符.

OpenAI 的 GPT-2 模型就用了这种只包含编码器(decoder-only)的模块.

GPT-2 内部机制

接下来, 将深入剖析 GPT-2 的内部结构, 看看它是如何工作的.

GPT-2 可以处理最长 1024 个单词的序列. 每个单词都会和它的前续路径一起「流过」所有的Decoder模块.

想要运行一个训练好的 GPT-2 模型, 最简单的方法就是让它自己随机工作(从技术上说, 叫做生成无条件样本(generating unconditional samples)). 换句话说, 也可以给它一点提示, 让它说一些关于特定主题的话(即生成交互式条件样本(generating interactive conditional samples)). 在随机情况下, 只简单地提供一个预先定义好的起始单词(训练好的模型使用<|endoftext|>作为它的起始单词, 这里称为<s>), 然后让它自己生成文字.

gpt2-simple-output-2

此时, 模型的输入只有一个单词, 所以只有这个单词的路径是活跃的. 单词经过每一层的处理, 最终得到一个向量. 向量可以对于词汇表的每个单词计算一个概率(词汇表是模型能「说出」的所有单词, GPT-2 的词汇表中有 50000 个单词). 在本例中, 选择概率最高的单词「The」作为下一个单词.

但有时这样会出问题——就像如果持续点击输入法推荐单词的第一个, 它可能会陷入推荐同一个词的循环中, 只有你点击第二或第三个推荐词, 才能跳出这种循环. 同样的, GPT-2 也有一个叫做「top-k」的参数, 模型会从概率前 k 大的单词中抽样选取下一个单词. 在之前的情况下, top-k = 1.

接下来, 将输出的单词「The」添加在输入序列的尾部构建新的输入序列, 让模型进行下一步的预测:

请注意, 第二个单词的路径是当前唯一活跃的路径了. GPT-2 的每一层都保留了它们对第一个单词的解释, 并且将运用这些信息处理第二个单词, GPT-2 不会根据第二个单词重新解释第一个单词.

深入了解内部原理

1. 输入编码 (Input Encoding)

首先, 从模型的输入开始. GPT-2 同样从嵌入矩阵(embedding matrix)中查找单词对应的嵌入向量, 该矩阵也是模型训练结果的一部分.

每一行都是一个词嵌入向量(word embedding):一个能够表征某个单词, 并捕获其意义的数字列表. 嵌入向量的长度和 GPT-2 模型的大小有关, 最小的模型使用了长度为 768 的嵌入向量来表征一个单词.

所以在一开始, 需要在嵌入矩阵中查找起始单词<s>对应的嵌入向量. 但在将其输入给模型之前, 还需要引入位置编码(positional encoding)——一些向 transformer 模块指出序列中的单词顺序的信号. 1024 个输入序列位置中的每一个都对应一个位置编码, 这些编码组成的矩阵也是训练模型的一部分.

至此, 输入单词在进入模型第一个 transformer 模块之前所有的处理步骤就结束了. 如上文所述, 训练后的 GPT-2 模型包含两个权值矩阵:嵌入矩阵和位置编码矩阵.

将单词输入第一个 transformer 模块之前需要查到它对应的嵌入向量, 再加上 1 号位置位置对应的位置向量.

2. 堆叠 (Stack)

第一个 transformer 模块处理单词的步骤如下:首先通过自注意力层处理, 接着将其传递给神经网络层. 第一个 transformer 模块处理完但此后, 会将结果向量被传入堆栈中的下一个 transformer 模块, 继续进行计算. 每一个 transformer 模块的处理方式都是一样的, 但每个模块都会维护自己的自注意力层和神经网络层中的权重.

3. 自注意力机制

语言的含义是极度依赖上下文的, 比如下面这个机器人第二法则:

Second Law of Robotics:

A robot must obey the orders given it by human beings except where such orders would conflict with the First Law.

这三处高亮的单词指代的是其它单词. 除非知道这些词指代的上下文联系起来, 否则根本不可能理解或处理这些词语的意思. 当模型处理这句话的时候, 它必须知道:

  • 「it」指代robot
  • 「such orders」指代前半句话中人类给机器人下的命令, 即「人类给它的命令」
  • 「First Law」指机器人第一法则的完整内容

这就是自注意力机制所做的工作, 它在处理每个单词(将其传入神经网络)之前, 融入了模型对于用来解释某个单词的上下文的相关单词的理解. 具体做法是, 给序列中每一个单词都赋予一个相关度得分, 之后对他们的向量表征求和.

举个例子, 最上层的 transformer 模块在处理单词「it」的时候会关注「a robot」, 所以「a」、「robot」、「it」这三个单词与其得分相乘加权求和后的特征向量会被送入之后的神经网络层.

自注意力机制沿着序列中每一个单词的路径进行处理, 主要由 3 个向量组成:

  • 查询向量(Query 向量):当前单词的Query向量被用来和其它单词的Key向量相乘, 从而得到其它词相对于当前词的注意力得分. 只关心目前正在处理的单词的Query向量.
  • 键向量(Key 向量):Key向量就像是序列中每个单词的标签, 它使搜索相关单词时用来匹配的对象.
  • 值向量(Value 向量):Value向量是单词真正的表征, 当算出注意力得分后, 使用Value向量进行加权求和得到能代表当前位置上下文的向量.

例如在档案柜中找文件. Query向量就像一张便利贴, 上面写着你正在研究的课题. Key向量像是档案柜中文件夹上贴的标签. 当你找到和便利贴上所写相匹配的文件夹时, 拿出它, 文件夹里的东西便是Value向量. 只不过最后找的并不是单一的值向量, 而是很多文件夹值向量的混合.

将单词的查询向量分别乘以每个文件夹的键向量, 得到各个文件夹对应的注意力得分(这里的乘指的是向量点乘, 乘积会通过 softmax 函数处理).

将每个文件夹的值向量乘以其对应的注意力得分, 然后求和, 得到最终自注意力层的输出.

这样将值向量加权混合得到的结果是一个向量, 它将其 50% 的「注意力」放在了单词「robot」上, 30% 的注意力放在了「a」上, 还有 19% 的注意力放在「it」上.

4. 模型输出

当最后一个 transformer 模块产生输出之后(即经过了它自注意力层和神经网络层的处理), 模型会将输出的向量乘上嵌入矩阵. [50257 x 768] dot [768 x 1] => [50257 x 1]

嵌入矩阵的每一行都对应模型的词汇表中一个单词的嵌入向量. 所以这个乘法操作得到的结果就是词汇表中每个单词对应的注意力得分.

简单地选取得分最高的单词作为输出结果(即 top-k = 1). 但其实如果模型考虑其他候选单词的话, 效果通常会更好. 所以, 一个更好的策略是对于词汇表中得分较高的一部分单词, 将它们的得分作为概率从整个单词列表中进行抽样(得分越高的单词越容易被选中). 通常一个折中的方法是, 将 top-k 设为 40, 这样模型会考虑注意力得分排名前 40 位的单词.

这样, 模型就完成了一轮迭代, 输出了一个单词. 模型会接着不断迭代, 直到生成一个完整的序列——序列达到 1024 的长度上限或序列中产生了一个终止符.

一些解释和补充:

  • 「单词」(word)和「词」(token)是两个概念. 但事实上, GPT-2 使用字节对编码(Byte Pair Encoding)方式来创建词汇表中的词(token), 也就是说词(token)其实通常只是单词的一部分.
  • 上面举的例子其实是 GPT-2 在「推断/评价」(inference / evaluation)模式下运行的流程, 所以一次只处理一个单词. 在训练过程中, 模型会在更长的文本序列上进行训练, 并且一次处理多个词(token). 训练过程的批处理大小(batch size)也更大(512), 而evaluation时的批处理大小只有 1.
  • Transformer 模块使用了很多归一化(normalization)层, 这在训练中是很关键的.

图解自注意力机制

自注意力机制 (self-attention)

原始的自注意力机制, 它是在Encoder模块里计算的. 先看一个简易的 transformer 模块, 它一次只能处理 4 个词(token).

自注意力机制通过以下三个主要步骤来实现:

  1. 为每个路径(自下而上)创建查询、键和值向量.
  2. 对于每个输入的词, 通过使用其查询向量与其它所有键向量相乘得到注意力得分.
  3. 将值向量与它们相应的注意力得分相乘后求和

1. 创建查询、键和值向量

首先, 自注意力机制的第一步就是为每个词(token)路径(暂且忽略注意力头)计算三个向量:Query向量、Key向量、Value向量 (q1,k1,v1).

2. 注意力得分

计算出上述三个向量后, 在第二步中只用Query向量和Key向量. 这里重点关注第一个词, 将它的Query向量与其它所有的Key向量相乘, 得到四个词中的每个词的注意力得分.

3. 求和

现在, 可以将注意力得分与Value向量相乘. 在对其求和后, 注意力得分较高的值将在结果向量中占很大的比重.

注意力得分越低, 在图中显示的值向量就越透明. 这是为了表明乘以一个小的数是如何削弱向量值的影响的.

如果在每一个路径(自下而上)都执行相同的操作, 最终会得到一个表征每个词的向量, 它包括了这个词的适当的上下文, 然后将这些信息在 transformer 模块中传递给下一个子层(前馈神经网络):

掩模自注意力机制 (masked self-attention)

在掩模自注意力机制中, 除了第二步, 其余部分与自注意力机制相同. 假设模型输入只包含两个词, 我们正在观察第二个词. 在这种情况下, 后两个词都被屏蔽了. 因此模型会干扰计算注意力得分的步骤. 基本上, 它总是为序列中后续的词赋予 0 分的注意力得分, 因此模型不会在后续单词上得到最高的注意力得分:

通常使用注意力掩模矩阵来实现这种屏蔽操作. 想象一个由四个单词组成的序列(例如「robot must obey orders」(机器人必须服从命令))在语言建模场景中, 这个序列被分成四步进行处理(每个单词一步)(假设现在每个单词(word)都是一个词(token)). 由于这些模型都是批量执行的, 假设这个小型模型的批处理大小为 4 (batch size), 它将整个序列(包含 4 步)作为一个批处理.

在矩阵形式中, 通过将Query矩阵和Key矩阵相乘来计算注意力得分. 该过程的可视化结果如下所示, 下图使用的是与单元格中该单词相关联的Query(或Key)向量, 而不是单词本身:

在相乘之后, 加上注意力掩模三角矩阵(attention masked triangular matrix). 它将要屏蔽的单元格设置为负无穷或非常大的负数(例如, 在 GPT2 中为 -10 亿):

然后, 对每一行执行 softmax 操作, 从而得到在自注意力机制中实际使用的注意力得分:

此分数表的含义如下:

  • 当模型处理数据集中的第一个示例(第一行)时, 这里只包含了一个单词(「robot」), 所以 100% 的注意力都在该单词上.
  • 当模型处理数据集中的第二个示例(第二行)时, 这里包含了(「robot must」), 当它处理单词「must」时, 48% 的注意力会在「robot」上, 而另外 52% 的注意力会在「must」上.
  • 以此类推

GPT-2 的掩模自注意力机制 (GPT-2 Masked Self-Attention)

1. 模型Evaluation时:一次只处理一个词

可以通过掩模自注意机制的方式执行 GPT-2. 但是在evaluate模型时, 当模型每轮迭代后只增加一个新单词时, 沿着先前已经处理过的路径再重新计算词(token)的自注意力是效率极低的.

在这种情况下, 处理第一个词(暂时忽略 <s>

GPT-2 保存了词「a」的Key向量和Value向量. 每个自注意力层包括了该词相应的Key和Value向量:

在下一次迭代中, 当模型处理单词「robot」时, 它不再需要为词「a」生成Query,Key and Value向量. 它只需要复用第一次迭代中保存的向量:

2. GPT-2 自注意力机制:1-创建queries, keys, and values

假设模型正在处理单词「it」. 对于下图中底部的模块来说, 它对该词的输入则是「it」的嵌入向量+序列中第九个位置的位置编码:

Transformer 中的每个模块都有自己的权重. 首先看到的是用于创建Query、Key和Value的权重矩阵.

自注意力机制将它的输入与权重矩阵相乘(并加上一个偏置向量, 这里不作图示).

相乘后得到的向量从基本就是单词「it」的Query、Key和Value向量连接(concatenation) 的结果.

将输入向量和注意力权重向量相乘(之后加上偏置向量)得到这个词的Query、Key和Value向量.

3. GPT-2 自注意力机制:1.5-分裂成注意力头

在前面的示例中, 忽略了「多头」的部分. 多头就是自注意力机制是在查询(Q)、键(K)、值(V)向量的不同部分多次进行的. 「分裂」注意力头指的是, 简单地将长向量重塑成矩阵形式. 在小型的 GPT-2 中, 有 12 个注意力头, 因此这是重塑矩阵中的第一维:

多个注意力头可以想象成下图(下图为 12 个注意力头中的 3 个的可视化结果):

4. GPT-2 自注意力机制:2-计算注意力得分

接下来介绍计算注意力得分的过程(这里只关注一个注意力头)(其它注意力头都进行类似的操作).

当前关注的词(token)可以与其它词的Key向量相乘得到注意力得分(在先前迭代中的第一个注意力头中计算得到):

5. GPT-2 自注意力机制:3-求和

现在可以将每个Value向量乘上它的注意力得分, 然后求和, 得到的是第一个注意力头的自注意力结果:

6. GPT-2 自注意力机制:3.5-合并多个注意力头

处理多个注意力头的方式是先将它们连接成一个向量:

但是这个向量还不能被传递到下一个子层. 需要将这个隐含状态的混合向量转变成同质(homogenous)的表示形式.

7. GPT-2 自注意力机制:4-投影

要让模型学习如何最好地将连接好的自注意力结果映射到一个前馈神经网络可以处理的向量. 下面是第二个大型权重矩阵, 它将注意力头的结果投影到自注意力子层的输出向量中:

通过这个操作, 可以生成能够传递给下一层的向量:

8. GPT-2 全连接神经网络:第一层

在全连接神经网络中, 当自注意力机制已经将合适的上下文包含在其表征中之后, 模块会处理它的输入词. 它由两层组成:第一层的大小是模型的 4 倍(因为小型 GPT-2 的大小为 768 个单元, 而这个网络将有 768*4=3072 个单元). 为什么是 4 倍呢?这只是原始 transformer 的运行大小(模型维度为 512 而模型的第一层为 2048). 这似乎给 transformer 模型足够的表征容量来处理目前面对的任务.

gpt2-mlp1

9. GPT-2 全连接神经网络:第二层-投影到模型的维度

第二层将第一层的结果投影回模型的维度大小(小型 GPT-2 的大小为 768). 这个乘法结果是该词经过 transformer 模块处理的结果.

gpt2-mlp-2

成功处理完单词「it」了!

回顾:

一个新的输入向量会遇到如下所示的权重矩阵:

而且每个模块都有自己的一组权重. 另一方面, 这个模型只有一个词嵌入矩阵和一个位置编码矩阵:

下面是对模型中的所有参数的详细统计结果:

出于某些原因, 该模型共计有 124M 个参数而不是 117M 个.

应用

机器翻译

进行翻译时, 模型不需要编码器. 同样的任务可以通过一个只有解码器的 transformer 来解决:

自动摘要生成

这是第一个训练decoder-only的 transformer 的任务. 也就是说, 该模型被训练来阅读维基百科的文章(没有目录前的开头部分), 然后生成摘要. 文章实际的开头部分被用作训练数据集的标签:

论文使用维基百科的文章对模型进行了训练, 训练好的模型能够生成文章的摘要:

迁移学习

在论文「Sample Efficient Text Summarization Using a Single Pre-Trained Transformer」中, 首先使用只包含Decoder的 transformer 在语言建模任务中进行预训练, 然后通过调优来完成摘要生成任务. 结果表明, 在数据有限的情况下, 该方案比预训练好的编码器-解码器 transformer 得到了更好的效果.

GPT2 的论文也展示了对语言建模模型进行预训练后取得的摘要生成效果.

音乐生成

Music Transformer采用了只包含Decoder的 transformer 来生成具有丰富节奏和动感的音乐. 和语言建模相似, 「音乐建模」就是让模型以一种无监督的方式学习音乐, 然后让它输出样本(此前称之为「随机工作」(rambling)).

在这种情境下是如何表征音乐的? 语言建模可以通过对字符、单词(word)、或单词(word)某个部分的词(token)的向量表征来实现. 面对一段音乐演奏(暂时以钢琴为例), 不仅要表征这些音符(notes), 还要表征速度(velocity)(衡量钢琴按键力度的指标).

一段演奏可以被表征为一系列的 one-hot 向量. 一个 MIDI 文件可以被转换成这样的格式. 论文中展示了如下所示的输入序列的示例:

这个输入序列的 one-hot 向量表征如下:

下面是论文中用来展示音乐 transformer 中自注意力机制的可视化图表:

这段作品中出现了反复出现的三角轮廓. 当前的Query向量位于后面一个「高峰」, 它关注前面所有高峰上的高音, 一直到乐曲的开头. 图中显示了一个查询向量(所有的注意力线来源)和正要处理的以前的记忆(突出了有更高 softmax 概率的音符). 注意力线的颜色对应于不同的注意力头, 而宽度对应于 softmax 概率的权重.

Performance

GPT-2 是 GPT 第二代:一樣是 Transformer Decoder 架構,但使用的數據及模型大小都直接乘上 10 倍. 有了如此龐大的數據與模型,在做完第一階段的無監督式訓練以後,GPT-2 的作者們決定做些瘋狂的事情:不再遵循兩階段遷移學習,直接做 zero-shot learning!這也就意味著直接把只看過 WebText 的 GPT-2 帶上考場,「裸測」它在多個跟 WebText 無關的 NLP 任務上的表現。而實驗結果如下:

由左至右分別為閱讀理解、翻譯、摘要以及問答任務

最右側的 GPT-2 模型(1542M)在多數特定的 NLP 任務上還是比不過專門為那些任務設計的神經網路架構(比方說閱讀理解的 DrQA + PGNet)。但 GPT-2 的作者們認為他們最大的貢獻在於展現了用大量無標註數據訓練巨大語言模型的潛力, 即給定越多參數以及越多樣、越大量的文本,無監督訓練一個語言模型或許就可讓該模型具備更強的自然語言理解能力,並在沒有任何監督的情況下開始學會解決不同類型的 NLP 任務.

Ref