跳转到内容

第一节:建模

我们将语言模型视为一个黑盒进行分析:

接着我们查看了大型语言模型的训练数据(例如,The Pile):

我们将彻底揭开语言模型的奥秘,探讨大型语言模型是如何构建的。

今天重点讨论两个主题,分词模型架构

  1. 分词:字符串是如何被分割成词汇单元的。

  2. 模型架构:我们将主要讨论Transformer架构, 这是真正使大型语言模型成为可能的建模创新。

回顾一下,语言模型 是一个概率分布,它作用于一系列词汇单元上,每个词汇单元都来自某个词汇库

然而,自然语言并不是以词汇单元序列的形式出现的,而是以字符串的形式存在(具体来说,是 Unicode 字符的序列):

一个分词器将任何字符串转换为词汇单元序列。

这可能不是语言建模中最引人注目的部分,但在决定模型工作效果的好坏方面扮演着非常重要的角色。

最简单的解决方案是执行以下操作:

text.split(' ')

  • 这种方法不适用于像中文这样的语言,因为中文句子中单词之间没有空格:

我今天去了商店。 [释义:I went to the store.]

  • 然后是像德语这样有长复合词的语言(例如,Abwasserbehandlungsanlange)。

  • 即使在英语中,也有带连字符的单词(例如,father-in-law)和缩写词(例如,don’t),这些都需要被分割。 例如,宾夕法尼亚树库将don’t分割为don’t,这是一个基于语言学的选择,但并不明显。

因此,通过空格来识别单词的分割是相当有问题的。

什么构成了一个好的分词?

  • 我们不想要太多的词汇单元(极端情况下:字符或字节),否则序列变得难以建模。
  • 我们也不想要太少的词汇单元,否则单词之间就不会有参数共享(例如,mother-in-lawfather-in-law应该完全不同吗?)。 这在形态丰富的语言中尤其成问题(例如,阿拉伯语、土耳其语等)。
  • 每个词汇单元应该是一个在语言学上或统计学上有意义的单元。

Sennrich 等人,2015 将原本用于数据压缩的 字节对编码 (BPE) 算法应用于分词器的生成,从而创造出了一种最常用的分词器。

学习分词器。 直觉:从每个字符作为自己的词汇单元开始,结合频繁共现的词汇单元。

  • 输入:一个训练语料库(字符序列)。
  • 初始词汇库 为字符集合。
  • 当我们仍希望扩展 时:
    • 找出在 中共现次数最多的元素对
    • 将所有 的出现替换为新符号
    • 添加到 中。

示例:

  1. [t, h, e, ␣, c, a, r], [t, h, e, ␣, c, a, t], [t, h, e, ␣, r, a, t]
  2. [th, e, ␣, c, a, r], [th, e, ␣, c, a, t], [th, e, ␣, r, a, t] (th 出现 3 次)
  3. [the, ␣, c, a, r], [the, ␣, c, a, t], [the, ␣, r, a, t] (the 出现 3 次)
  4. [the, ␣, ca, r], [the, ␣, ca, t], [the, ␣, r, a, t] (ca 出现 2 次)

学习输出为:

  • 更新后的词汇库 :[a, c, e, h, t, r, ca, th, the]
  • 我们所做的合并(对应用分词器很重要):
    • t, h th
    • th, e the
    • c, a ca

应用分词器。 要对一个新字符串进行分词,按照相同的顺序应用合并:

  • [t, h, e, ␣, o, x]
  • [th, e, ␣, o, x]
  • [the, ␣, o, x]

Unicode

  • 一个问题是(特别是在多语言环境中),存在大量(144,697个)Unicode字符。
  • 我们当然不会在训练数据中看到所有字符。
  • 为了进一步减少数据稀疏性,我们可以在字节上运行 BPE 而不是 Unicode 字符 (Wang 等人,2019)。
  • 中文示例:

    今天 [释义:today] [x62, x11, 4e, ca]

单字模型(SentencePiece)

段落标题 单字模型(SentencePiece)

与仅仅根据频率进行分割不同,一个更“有原则”的方法是定义一个目标函数,以捕捉一个好的分词应该是什么样子。 我们现在来描述单字模型Kudo, 2018)。

  • 它是SentencePiece工具(Kudo & Richardson, 2018)支持的分词方法之一,与BPE并列。
  • 它被用来训练T5和Gopher模型。

给定一个序列 ,分词 是一个集合,其中

示例:

  • 训练数据(字符串):
  • 分词 (词汇库
  • 可能性:

算法

  • 从一个“足够大”的种子词汇库 开始。
  • 重复以下步骤:
    • 给定 ,使用EM算法优化
    • 计算每个词汇库中的词汇 ,捕捉如果从 中移除 ,可能性会减少多少。
    • 根据损失排序,并保留 中前80%的词汇。
  • GPT-2 和 GPT-3 使用了 BPE 分词,词汇量为 50K。
  • Jurassic 使用了 SentencePiece 分词,词汇量为 256K。

影响:

  • 给定相同的字符串,Jurassic 需要的词汇单元比 GPT-3 少 28%,因此它的处理速度是 GPT-3 的 1.4 倍。
  • Jurassic 和 GPT-3 都使用相同的上下文大小(2048),因此可以多输入 39% 的文本到提示中。

GPT-3 和 Jurassic 的分词示例(演示):

  • GPT-3:[Ab, raham, ␣Lincoln, ␣lived, ␣at, ␣the, ␣White, ␣House, .]
  • Jurassic:[Abraham␣Lincoln, ␣lived, ␣at␣the␣White␣House, .]

到目前为止,我们已经将语言模型定义为一系列词汇单元的概率分布 , 正如我们所看到的,这种方法非常优雅且强大(通过提示,语言模型原则上可以做任何事情,正如 GPT-3 所示)。 然而,在实践中,为了避免对整个序列进行生成性建模,对于特定任务来说,可能会更有效率。

上下文嵌入。 作为先决条件,主要的关键发展是将一系列词汇单元与相应的上下文嵌入序列相关联:

  • 如其名称所示,一个词汇单元的上下文嵌入取决于其上下文(周围的词);例如,考虑
  • 符号表示:我们将 视为嵌入函数(类似于序列的特征映射)。
  • 对于一个词汇单元序列 产生上下文嵌入

我们将语言模型的概念扩展为三种类型的模型。

仅编码器(BERT、RoBERTa 等)。 这些语言模型生成上下文嵌入,但不能直接用于生成文本。

这些上下文嵌入通常用于分类任务(有时大胆地称为自然语言理解任务)。

  • 示例:情感分类

  • 示例:自然语言推理

  • 优点:上下文嵌入 可以双向地依赖于左侧上下文()和右侧上下文()。
  • 缺点:不能自然地生成补全。
  • 缺点:需要更多的临时训练目标(掩码语言建模)。

仅解码器(GPT-2、GPT-3 等)。这些是我们标准的自回归语言模型, 给定一个提示 产生上下文嵌入和下一个词汇 的分布(并且递归地,对整个补全 )。

  • 示例:文本自动完成

  • 缺点:上下文嵌入 只能单向地依赖于左侧上下文()。
  • 优点:可以自然地生成补全。
  • 优点:简单训练目标(最大似然)。

编码器-解码器(BART、T5 等)。 这些模型在某些方面可以结合两者的优点: 它们可以使用双向上下文嵌入对输入 进行建模,并且可以生成输出

  • 示例:表格到文本生成

  • 优点:上下文嵌入 可以双向地依赖于左侧上下文()和右侧上下文()。
  • 优点:可以自然地生成输出。
  • 缺点:需要更多的临时训练目标。

现在我们来描述嵌入函数 的内部结构:

接下来,我们将介绍语言模型的模型架构,特别强调无处不在的 Transformer 架构。 对 Transformer 架构的阐述将基于这些CS221教程关于可微编程的幻灯片, 并且会稍微偏离标准介绍方式。

深度学习之美在于能够创建构建模块,就像我们用函数构建整个程序一样。 因此,我们希望能够用以下这样的函数来封装复杂性:

这个函数将包含参数,为了简化,我们将在函数体中包含它们,但在函数签名中省略。

接下来,我们将定义一系列构建模块,直到我们得到完整的 Transformer。

首先,我们必须将词汇单元序列转换成向量序列。 通过查询每个词汇单元在嵌入矩阵 (一个将从数据中学习得到的参数)中的位置,来完成这项工作:

定义 函数:

  • 将序列 中的每个词汇单元 转换为向量
  • 返回

这些正是过去上下文无关的词嵌入。 我们定义了一个抽象的 函数,它接受这些上下文无关嵌入, 并将它们映射到上下文嵌入

定义 函数:

  • 根据序列 中其他元素的处理,处理每个元素
  • [抽象实现(例如,)]

最简单的序列模型是基于前馈网络的 (Bengio 等人,2003) 应用于固定长度上下文,就像在 n-gram 模型中一样:

定义 函数:

  • 通过查看最后 个元素来处理序列 中的每个元素
  • 对于每个
    • 计算
  • 返回

第一个“真正”的序列模型是循环神经网络(RNN), 它是一个包括简单RNN、LSTM和GRU等模型的家族。 RNN的基本形式是递归地计算一系列隐藏状态

定义 函数:

  • 从左到右并递归地处理序列 并计算向量
  • 对于
    • 计算
  • 返回

实际执行繁重工作的模块是 , 它类似于有限状态机,接收当前状态 ,一个新的观测 , 并返回更新后的状态:

定义 函数:

  • 根据新的观测 更新隐藏状态
  • [抽象实现(例如,)]

有三种实现 的方式。 最早的RNN是简单RNN Elman, 1990, 它取 的线性组合, 并通过逐元素非线性函数 传递它 (例如,逻辑函数 或更现代的ReLU )。

定义 函数:

  • 基于简单的线性变换和非线性,根据新的观测 更新隐藏状态
  • 返回

由于定义的RNN仅依赖于过去,但我们可以通过反向运行另一个RNN使它们依赖于未来。 这些模型被 ELMoULMFiT 使用。

定义 函数:

  • 从左到右和从右到左处理序列
  • 计算从左到右:
  • 计算从右到左:
  • 返回

注意:

  • 简单RNN由于梯度消失问题而难以训练。
  • 长期短期记忆网络(LSTM)和门控循环单元(GRU)(两者都是 的实现)已被开发出来以解决这些问题。
  • 尽管嵌入 可以任意远地依赖于(例如,),但它不太可能以“清晰”的方式依赖于它 (有关更多讨论,请参阅 Khandelwal 等人,2018)。
  • 在某种意义上,LSTM真正将深度学习带入了NLP的全盛时期。

为了节省时间,我们将不讨论这些模型。

现在,我们将讨论Transformer(Vaswani 等人,2017), 这种序列模型真正促成了大型语言模型的起飞; 它们是仅解码器(GPT-2, GPT-3)、仅编码器(BERT, RoBERTa)和编码器-解码器(BART, T5)模型的构建模块。

有关学习Transformer的资源非常丰富:

强烈建议您阅读这些参考资料。 在本文中,我将力求走一条中间道路,强调伪代码函数和接口。

Transformer的核心是注意力机制, 它早期是为机器翻译开发的(Bahdananu 等人,2017)。

我们可以将注意力想象为一个“软”查找表,我们有一个查询 , 我们希望将其与序列 中的每个元素匹配:

我们可以将每个 通过线性变换想象成代表一个键值对:

并通过另一个线性变换形成查询:

键和查询可以比较以给出一个分数:

这些分数可以被指数化并规范化,形成关于词汇位置 的概率分布:

然后最终输出是对值的加权组合:

我们可以将这一切都简洁地写成矩阵形式:

定义 函数:

  • 通过将 与每个 比较来处理
  • 返回

我们可以想象有多个方面(例如,句法、语义)我们希望匹配。 为了适应性这一点,我们可以同时拥有多个注意力头,并且简单地组合它们的输出。

定义 函数:

  • 通过 个方面将 与每个 比较
  • 返回

自注意力层。 现在我们将用 替换 作为查询参数,产生:

定义 函数:

  • 将每个元素 与每个其他元素比较
  • 返回

前馈层。 自注意力允许所有词汇单元“相互交流”,而前馈连接提供:

定义 函数:

  • 独立处理每个词汇单元
  • 对于
    • 计算
  • 返回

提高可训练性。 我们快要完成了。 我们理论上可以直接采用 序列模型,并将其迭代 96 次来制造 GPT-3, 但那个网络将难以优化(因为同样困扰 RNN 的梯度消失问题,现在只是沿着深度方向)。 所以我们得做两个小把戏来确保网络是可训练的。

残差连接。 计算机视觉中的一个技巧是残差连接(ResNet)。 而不是应用某个函数

我们添加一个残差(跳过)连接,这样如果 的梯度消失,梯度仍然可以通过 流动:

层归一化。 另一个技巧是 层归一化, 它接收一个向量并确保其元素也不要太 大:

定义 函数:

  • 使每个 不太大也不太小

我们首先定义一个适配器函数,它接受一个序列模型 并使其“健壮”:

定义 函数:

  • 安全地将 应用于
  • 返回

最后,我们可以简洁地定义Transformer块如下:

定义 函数:

  • 在上下文中处理每个元素
  • 返回

位置嵌入。 您可能已经注意到,根据定义, 一个词汇单元的嵌入并不取决于它在序列中的位置, 所以两个句子中的 将具有相同的嵌入, 这是没有意义的。

为了解决这个问题,我们向嵌入中添加位置信息

定义 函数:

  • 添加位置信息
  • 定义位置嵌入:
    • 偶数维度:
    • 奇数维度:
  • 返回

GPT-3。 所有部分都就绪后,我们现在可以用一行定义大致的 GPT-3 架构, 只需将Transformer块堆叠 96 次: