[中译] 图解 GPT-2 (transformer 语言模型可视化)

已联系作者申请翻译授权,随后会更新授权。

今年,我们看到了机器学习的耀眼应用。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 的应用主要通过三个步骤。

  1. 为每个路径创建 query、key 和 value 向量。
  2. 对于每个输入 token,使用它的 query 向量对所有其他 key 向量进行打分;
  3. 将 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,所以模型无法被未来的词激活。

这种遮蔽通常通过一个矩阵来实现,称为注意力遮蔽(attention mask)。想象一下四个词的序列(例如 “robot must obey orders“)。在语言建模中,这个序列被四步吸收 — 每个单词一个(现在假设每个单词都是一个 token )。由于这些模型是分批工作的,所以我们可以假设这个模拟模型的批次大小为4,将整个序列(包括其四个步骤)作为一个批次进行处理。 在矩阵形式中,我们通过将 query 矩阵乘以 key 矩阵来计算分数。让我们把它可视化如下,除了单词,在该单元格中还有与该单词相关的 query(或 key)向量。

然后,我们将注意力遮罩三角形贴上。它将我们要屏蔽的单元格设置为负无限或一个非常大的负数(例如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 内部的情况有了更多的了解。

相关资源

鸣谢

感谢Lukasz KaiserMathias MüllerPeter J. LiuRyan SepassiMohammad Saleh 对本文早期版本的反馈。

评论或更正?请发推特给我@JayAlammar

来源





[中译] A Visual Guide to Using BERT for the First Time



在过去几年里,处理语言的机器学习模型法展迅速加快。这种进展已经从实验室毕业,开始助力一些领先的数字产品。一个很好的例子是 Google 的文章 recent announcement of how the BERT model is now a major force behind Google Search 。谷歌认为这一步(或者说应用于搜索的自然语言理解的进步)代表了 "过去五年中最大的飞跃,也是搜索历史上最大的飞跃之一"。

the biggest leap forward in the past five years, and one of the biggest leaps forward in the history of Search

这篇文章是一个简单的教程,介绍如何使用 BERT 的变体来对句子进行分类。这是一个足够基础的例子,作为第一个入门,但又足够深入,可以展示其中的一些关键概念。

在这篇文章的同时,我还准备了一个 Jupyter Notebook。你可以在这里看到Jupyter Notebook在colab上运行

SST2 数据集

在这个例子中,我们将使用的数据集是SST2,它包含了电影评论中的句子,每个句子都被标记为正面的(数值为1)或负面的(数值为0)。

模型:句子情感分类

我们的目标是创建一个模型,将一个句子(就像我们数据集中的句子一样),并产生1(表示该句子带有正面的情绪)或0(表示该句子带有负面的情绪)。我们可以把这个过程想象成这样。


这一模型其实是由两个模型组成的。
  • DistilBERT对句子进行处理,并将它从中提取的一些信息传递给下一个模型。DistilBERT是由 HuggingFace 团队开发并开源的一个较小版本的BERT。它是BERT的一个更轻更快的版本,其性能与BERT大致相当。
  • 下一个模型是 scikit learn 的逻辑回归模型将接收 DistilBERT 的处理结果,并将句子分为正向或负向(分别为1或0)。

我们在两个模型之间传递的数据是一个大小为768的向量。我们可以把这个向量看作是句嵌入,用它来进行分类。


如果你读过我之前的文章, Illustrated BERT,每个句子由 [CLS] 这一特殊 token 作为开头。

模型训练

虽然我们会使用两个模型,但只会训练逻辑回归模型。对于 DistillBERT,我们会使用一个预训练模型。不过这个模型没有对橘子分类作过训练或者微调。但是,我们从 BERT 训练的总体目标中,得到一些句子分类的能力。尤其是 BERT 的第一个位置的输出(与 [CLS] 这一 token 有关)。我相信这是由于 BERT 的第二个训练对象--下一句话分类。这个目标似乎是训练模型对第一个位置的输出进行句子意义上的封装。transformers 库为我们提供了 DistilBERT 的实现以及模型的预训练版本。

教程概述

本教程的计划是这样的:我们将首先使用训练好的 DistilBERT 来生成2000个句子的句嵌入。

在此之后,我们不会再用 DistilBERT。从这里开始都是 Scikit Learn。我们在这个数据集上做通常的训练/测试集拆分。

对 DistilBERT(模型#1)的输出进行训练/测试集拆分,可以创建在其上进行逻辑回归的训练和评估集(模型#2)。请注意,在现实中,在进行训练/测试集拆分之前 sklearn 会对例子进行随机排列,它不会只取数据集中出现的前75%的例子。

然后我们在训练集上训练逻辑回归模型。

单次预测如何计算

在深入代码并解释如何训练模型之前,让我们看看训练好的模型是如何计算其预测的。

我们尝试对 "a visually stunning rumination on love" 这个句子进行分类。第一步是使用 BERT 的 tokenizer 首先将单词分割成 tokens。然后,我们添加句子分类所需的特殊 token(分别是句首的 [CLS]和句尾的 [SEP])。

tokenizer 做的第三步是将每个 token 从 embedding 中替换成它的 id,这是我们通过训练模型得到的一个组件。阅读 The Illustrated Word2vec 了解词嵌入的背景。

请注意,tokenizer 只需一行代码就能完成所有这些步骤。

tokenizer.encode("a visually stunning rumination on love", add_special_tokens=True)

我们的输入可以传递给 DistilBERT 了。

如果你读过 Illustrated BERT ,这一步也可以这样可视化。

流经DistilBERT

通过DistilBERT传递输入向量可以像BERT一样。每个向量由768个浮点数组成。

因为这是一个句子分类任务,所以除了第一个向量(与[CLS] token 相关的那个)之外,我们忽略所有的向量。我们将这一个向量作为逻辑回归模型的输入。

从这里开始,逻辑回归模型的工作就是根据它从训练阶段学到的对这个向量进行分类。我们可以认为预测是这样的。

训练是我们下一节要讨论的内容。

代码

在本节中,我们将重点介绍训练这个句子分类模型的代码。包含所有这些代码的 Jupyter Notebook 可以在 colabgithub 上找到。

让我们从导入开始吧:

import numpy as np
import pandas as pd
import torch
import transformers as ppb # pytorch transformers
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import train_test_split

数据集在 github 上以文件的形式存在,所以我们直接将其导入 pandas dataframe 中即可。

df = pd.read_csv('https://github.com/clairett/pytorch-sentiment-classification/raw/master/data/SST2/train.tsv', delimiter='/t', header=None)

我们可以使用 df.head() 来查看 dataframe 的前五行。

df.head()

导入预训练的 DistilBERT 模型和 tokenizer。

model_class, tokenizer_class, pretrained_weights = (ppb.DistilBERTModel, ppb.DistilBERTTokenizer, 'DistilBERT-base-uncased')
# 想要用 BERT 代替 DistilBERT?使用下面这行
# model_class, tokenizer_class, pretrained_weights = (ppb.BertModel, ppb.BertTokenizer, 'bert-base-uncased')
# 加载预训练模型/tokenizer
tokenizer = tokenizer_class.from_pretrained(pretrained_weights)
model = model_class.from_pretrained(pretrained_weights)

现在我们可以对数据集进行 token 切分。请注意,我们要做的事情与上面有些不同。上面的例子只标记并处理了一个句子。这里,我们将对所有句子进行切分,并将其作为一个批处理(考虑到资源问题,Notebook 将处理一组较小的例子,比如2000个句子)。

Tokenization

 tokenized = df[0].apply((lambda x: tokenizer.encode(x, add_special_tokens=True)))

这就把每个句子变成了 id 的列表。



数据集目前是一个嵌套的列表(或 pandas Series/DataFrame )。在 DistilBERT 将其作为输入进行处理之前,我们需要通过将较短的句子用 token id 0 进行 padding,使所有的向量长度相同,你可以参考笔记本上的步骤,这是基本的 python 字符串和数组操作。

Padding 之后,我们就有一个矩阵/张量,可以传给BERT了。

用 DistilBERT 处理

现在我们从填充的 token 矩阵中创建一个输入张量,并将其发送给 DistilBERT

input_ids = torch.tensor(np.array(padded))
with torch.no_grad():
	last_hidden_states = model(input_ids)

运行这一步后,last_hidden_states持有 DistilBERT 的输出。它是一个 tuple,形状为(例子数、序列中的最大标记数、DistilBERT 模型中的隐藏单元数)。在我们的例子中,这将是2000(因为我们只限制了2000个例子),66(这是2000个例子中最长序列中的 tokens 数),768(DistilBERT 模型中的隐藏单元数)。

解开 BERT 的输出张量。

让我们来解开这个三维输出张量。我们可以先从它的尺寸开始。

重复一个句子的处理过程

每一行都与我们数据集中的一个句子相关联。回顾一下第一句话的处理步骤,我们可以认为它是这样的。

切开重要的部分

对于句子分类,我们只对 BERT 输出中的 [CLS] 这一 token 感兴趣,所以我们选择了这一切片,并丢弃其他所有的东西。

这就是我们如何对三维张量进行切分,得到我们感兴趣的二维张量。

# 切分所有序列的第一个位置的输出,取所有隐藏单位输出
features = last_hidden_states[0][:,0,:].numpy()

现在features是一个2维的 numpy 数组,包含了我们数据集中所有句子的句嵌入。

逻辑回归的数据集

现在我们已经有了 BERT 的输出,我们已经组装好了训练逻辑回归模型所需要的数据集。768列都是特征,以及我们刚刚从初始数据集中得到的标签。

我们用来训练逻辑回归的标注数据集。特征是 BERT 的输出向量的切片[CLS] 这一 token(位置#0)。每一行对应于我们数据集中的一个句子,每一列对应 Bert/DistilBERT 模型顶部 trnsformer 的前馈神经网络的一个隐藏单元输出。

  拆分完训练/测试集后,我们可以用逻辑回归模型对数据集进行训练。

labels = df[1]
train_features, test_features, train_labels, test_labels = train_test_split(features, labels)

它将数据集分割成训练/测试集。

接下来,我们在训练集上训练逻辑回归模型。

lr_clf = LogisticRegression()
lr_clf.fit(train_features, train_labels)

现在模型已经被训练好了,我们可以根据测试集对其进行评分。

lr_clf. score(test_features, test_labels)

这表明  该模型达到了81%左右的准确率。

分数基准

作为参考,目前这个数据集的最高准确率分数是96.8。DistilBERT 可以被训练来提高它在这个任务上的得分--这个过程叫做微调,它更新 BERT 的权重,使它在句子分类中取得更好的表现(我们可以称之为下游任务)。微调后的 DistilBERT 原来的准确率达到了90.7。全尺寸的 BERT 模型达到了94.9

Notebook

直接进入 Jupyter Notebook在 colab 上运行

就这样了! 这是与 BERT 的第一次接触。下一步就是到文档中去尝试一下微调。你也可以回过头来从 DistilBERT 切换到 BERT,看看效果如何。

感谢Clément DelangueVictor Sanh 和 Huggingface 团队为本教程的早期版本提供的反馈。


[中译] How GPT3 Works - Visualizations and Animations

原文地址:https://jalammar.github.io/how-gpt3-works-visualizations-animations/,原文作者已添加本文链接:)

(本文将在未来几天持续更新可视化)

一个经过训练的语言模型会生成文本。

我们可以选择一些文本作为输入传递给它,从而影响它的输出。

输出是由模型在扫描大量文本的训练期间 "学到 "的东西生成的。

训练是将模型暴露在大量文本中的过程。它已经做了一次并完成了。你现在看到的所有实验都是来自那个训练过的模型。据估计,它耗费了355个GPU年,花费了460万美元。

一个有3000亿个字符的数据集被用来生成模型的训练样本。例如,这是由上面那句话生成的三个训练样本。

你可以看到你如何在所有文本上滑动一个窗口,并生成很多例子。

当我们只给模型一个样本时:我们只给看特征,并让它预测下一个单词。

该模型的预测将是错误的。我们计算其预测中的错误,并更新模型,以便下次它做出更好的预测。

重复这个过程数百万次


现在让我们更详细地看看这些相同的步骤。

GPT3实际上一次只生成一个token的输出(现在我们假设一个token是一个词)。


请注意:这是对GPT-3工作原理的描述,而不是对它的新颖之处的讨论(主要是规模大得可笑)。其架构是基于的 transformer 解码器模型, 参见这篇论文

GPT3 极其巨大。它将从训练中学习到的内容编码成1750亿个参数。这些参数用于计算每次运行时生成的 token。

未经训练的模型以随机参数开始。训练以期找到更好的预测值。


这些数字是模型里面数百个矩阵的一部分。预测主要就是大量的矩阵乘法。

在我的YouTube 上的人工智能介绍中,我展示了一个简单的机器学习模型,它只有一个参数。为解读这个1750亿个参数的怪兽开了个好头。

为了理解这些参数是如何分布和使用的,我们需要打开模型看看里面的情况。

GPT3 的宽度是2048个 token。这是它的 "上下文窗口"。这意味着它沿着这2048条轨道处理 token。


让我们跟随紫轨,看看系统是如何处理"机器人"这个词并产生"A"的?

抽象的步骤:

  1. 将单词转换为代表单词的向量(数字列表)
  2. 计算预测值
  3. 将所得向量转换为单词

GPT3的重要计算发生在其96个 transformer 解码层的堆栈中。

看到这些层了吗?这就是 "深度学习 "中的 "深度"。

这些层中的每一层都有1.8亿个参数来进行计算。


你可以在我的博文图解GPT2中看到解码器内部一切的详细解释。

与GPT3的不同之处在于密集自注意层和稀疏自注意层的交替。

这是GPT3内输入和响应("Okay human")的X光片。注意,每一个token是如何通过整个层堆栈的。我们不关心第一个词的输出。当输入完成后,我们开始关心输出。我们把每个词都反馈到模型中。

在React代码生成的例子中,描述会是输入提示(绿色),此外还有几个对代码描述的例子吧。而 React 代码会像这里的粉色 token 一样一个个地生成。

我的假设是,将引例和描述作为输入,用特定的 token 将例子和结果分开,然后输入到模型中。


这种方式让人印象深刻。因为我们只要等到GPT3的微调推出。它的性能将更加惊人。

微调实际上是更新模型的权重,让模型在某项任务中表现得更好。