我们不知不觉已经将手机当成精神伴侣,每天形影不离。
人有七年之痒,而人与手机之间貌似没有这个梗。
因为手机只是一个随身而动的窗口,与世界沟通的窗口,里面有亲朋好友,还有更大的世界。
除非你厌世,否则不会厌倦手机。
但是手机缺少灵魂,换句话说,你离不开的并不是手机本身,而是里面的那个世界。
然而,接下来可能会有所改变,手机将被赋予生命
。
对,它会跟你沟通,会说话,对你有记忆,甚至有情绪。
你可能会想 AI 会不会产生意识?其实这与人类意识完全两码事情,根本不用担心。
我们应该担心的事情是 AI 会不会影响我们的饭碗。
这点我想是必然的事情,只是时间问题。对于目前来说,我们更应该想想如何用好它。而想要用好它,就需要了解它。
不管怎么样,在不久的将来,我们生活中即将出现一批硅基人。
对,说的就是 ChatGPT 以及它的同类。
一个人工智能技术驱动的自然语言处理工具,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,像人类一样来聊天交流,甚至能完成撰写邮件、视频脚本、文案、翻译、代码,写情书、润色论文等任务。
人工智能,一个貌似年轻但实际已经很有年头的概念。
只是近年来仿佛让人们看到了曙光。
七年前,AlphaGo 走出了第一步,现在 ChatGPT 正在走第二步。
第一步也许只是偶然,但如果第二步走稳了,意味着能够正常走起来。当然,具体能走向何方,还要看以后,还有很长的路要走。
这里头有两个公司,DeepMind 和 OpenAI。
一个背后是谷歌,一个背后是微软,似乎势均力敌,好戏可期。
有人说 DeepMind 缺席这场盛宴,是否意味着最大输家?这是否过于太以成败论英雄了?
DeepMind 切入 LLM 相对较晚,但也有自己的技术优势。ChatGPT 用到的技术之一强化学习正是 DeepMind 的强项,只是之前应用于其他应用。
更何况 OpenAI 现在只是暂时领先,2023 才刚开始。
另外,不要忘了,ChatGPT 背后一个核心工作 Transformer,正是谷歌 2017 年的力作。
对今天的主角就是它,变形金刚。
哦不,拿错道具了,是变换器。不过一般不翻译,就叫 Transformer。
为什么要讲它呢?因为 ChatGPT 里面的 GPT 一展开就是 Generative Pre-trained Transformer
,即生成式预训练变换器。
未来已来,是时候停下脚步,来了解一下即将出现在生活中的那个《Her》。
△ 科幻电影《Her》
1引言
概括地讲,自然语言处理 NLP 领域近年来有两次大的研究范式的转换。
第一次范式转换是从深度学习到两阶段预训练模型
主要是 Bert 和 GPT 的出现,解决了深度学习模型在 NLP 领域的两个主要问题,即数据不足和特征抽取能力不强。这次范式转换带来的影响大概有几个方面:
中间任务的自然融合。比如中文分词、词性标注、句法分析等,因为这些任务已经被预训练模型作为语言学特征融入模型了;自从 Bert、GPT 出现之后,这些中间任务就没必要单独处理了,因为通过大量数据的预训练,Bert、GPT 已经把这些中间任务转移到对 Transformer 的学习里了,此时我们完全可以端到端地直接解决那些最终任务,而无须对这种中间过程专门建模。比如中文分词,哪些字应该组成一个词,让 LLM 自己去学习就行了,未必要以人类理解的规则来分词,放手让模型自己去学合理的分词方式。
不同研究方向技术路线的统一。比如自然语言理解类任务和自然语言生成类任务,都可以用两阶段预训练模型作为基础框架,只需要在后面加上一些任务相关的模块即可。典型的自然语言理解类任务包括文本分类、句子关系、情感倾向等,这种任务本质上都是分类任务,即模型根据输入文本预测文本类别。自然语言生成也包含很多研究子方向,比如聊天、摘要、问答和翻译等。生成类任务的特点是给定输入文本,模型要生成一串文本。两者差异主要体现在输入输出形式上。自从 Bert、GPT 模型诞生后,特征抽取器都逐渐从 LSTM、CNN 统一到 Transformer 上。
第二次范式从预训练模型走向通用人工智能 AGI
第一次范式大致完成了技术上统一,然后朝着通用性人工智能发展。可以将 ChatGPT 的出现作为第二次范式转换的开端。这次范式转变可以让 LLM 成为人机交互接口,让语言模型适配人以自然语言形式的命令表达方式,而不是反过来让人去适配语言模型的指令模式。有了这一步,人类迈向 AGI 的路子似乎变得不再虚无缥缈了。
这里主要提两个技术,即 Prompt Tuning
和 Instruction Tuning
。Prompt Tuning
针对每个任务单独生成 prompt 模板,然后在每个任务上进行微调与评估,其中预训练模型参数是冻结的。Prompt 方法的发展思路,通过完形填空的方式发掘语言模型的能力,prompt 方法比传统的 finetune 好。LLM 已经被证明可以很好的应用到小样本学习任务,例如 OpenAI 提出的 GPT-3 在小样本(few-shot)场景上取得了不错的表现,但是在零样本(zero-shot)学习任务中表现不是很突出。
为了进一步提升模型的泛化能力和通用能力,诞生了一种新范式,即 Instruction Tuning
,通过以 Instrcution 为指导的大量任务进行学习,提升在未知任务的性能。Instruction Tuning
是一种新兴的 NLP 技术范式,它利用自然语言指令来激发语言模型的理解能力,从而实现在未见过的任务上的零样本或少样本表现。Instruction Tuning
与 Prompt Tuning
不同,后者是通过给语言模型提供一些特定的输入格式或提示来引导其产生正确的输出。Instruction Tuning
更加灵活和通用,它可以让语言模型根据不同的指令来执行不同的任务,而无需为每个任务单独存储模型权重。Instruction Tuning
也可以提高语言模型在对话任务上的泛化能力。
本文主要针对第一次范式中的核心技术 Transformer 展开介绍。为了让没有这方面基础的只有高中知识的读者也可以试着学习,我们找到了一篇外网讲得非常好的文章[1],以飨读者。
2Transformer
Transformers 在 2017 年的这篇论文中作为一种序列转换工具被引入,即将一个符号序列转换为另一个符号序列。一个典型的例子是翻译,如英语到德语。它也被修改为执行序列填充, 给定一个开始提示,以相同的方式和风格进行。它们已迅速成为自然语言处理研究和产品开发不可或缺的工具。
¸独热编码
我们的第一步是将所有单词转换为数字,以便我们可以对它们进行数学运算。
想象一下,我们的目标是创建响应我们的语音命令的计算机。我们的工作是构建将一系列声音转换为一系列单词的转换器。
我们首先选择我们的词汇表,即我们将在每个序列中使用的符号集合。在我们的例子中,将有两组不同的符号,一组用于表示人声的输入序列,另一组用于表示单词的输出序列。
现在,假设我们正在使用英语。英语中有数万个单词,也许还有几千个单词涵盖了计算机专用术语。这将使我们的词汇量达到十万中的大部分。将单词转换为数字的一种方法是从 1 开始计数,然后为每个单词分配一个编号。然后可以将单词序列表示为数字列表。
例如,考虑一种词汇量为三个的微型语言:files、find 和 my。每个单词都可以换成一个数字,也许 files = 1、find = 2 和 my = 3。那么由单词序列 [find , my , files] 组成的句子 “Find my files” 可以表示为数字序列 [2, 3, 1]。
这是将符号转换为数字的一种完全有效的方法,但事实证明还有另一种格式更容易让计算机使用,即单热编码。在 one-hot 编码中,一个符号由一个大部分为零的数组表示,与词汇表的长度相同,只有一个元素的值为 1。数组中的每个元素对应一个单独的符号。
考虑 one-hot 编码的另一种方法是,每个单词仍然被分配了自己的编号,但现在该编号是数组的索引。这是我们上面的示例,采用单热编码。
因此,“Find my files”这句话变成了一系列一维数组,将它们挤在一起后,开始看起来像一个二维数组。
注意,我将交替使用术语“一维数组”和“向量”。“二维数组”和“矩阵”也是如此。
¸点积
关于 one-hot 表示的一个真正有用的事情是它可以让我们计算点积。这些也有其他令人生畏的名称,如内积和标量积。要获得两个向量的点积,请将它们对应的元素相乘,然后将结果相加。
当我们使用独热词表示时,点积特别有用。任何 one-hot 向量与自身的点积都是 1。
并且任何 one-hot 向量与任何其他 one-hot 向量的点积为零。
前两个示例展示了如何使用点积来衡量相似性。作为另一个例子,考虑一个值向量,它表示具有不同权重的单词组合。一个 one-hot 编码的词可以用点积与它进行比较,以显示该词的表示有多强。
¸矩阵乘法
点积是矩阵乘法的组成部分,矩阵乘法是组合一对二维数组的一种非常特殊的方法。我们称这些矩阵中的第一个为 A,第二个为 B。最简单的情况,当 A 只有一行,B 只有一列时,矩阵乘法的结果就是两者的点积。
请注意 A 中的列数和 B 中的行数如何需要相同才能使两个数组匹配并计算点积。
当 A 和 B 开始增长时,矩阵乘法开始变幻莫测。要处理 A 中的多行,请分别对 B 的每一行进行点积。答案的行数与 A 一样多。
当 B 占用更多列时,将每列与 A 进行点积并将结果堆叠在连续的列中。
现在我们可以将其扩展为将任意两个矩阵相乘,只要 A 中的列数与 B 中的行数相同即可。结果将具有与 A 相同的行数和与 B 相同的列数。
矩阵乘法看作查表
注意矩阵乘法在这里如何充当查找表。我们的 A 矩阵由一堆单热向量组成。它们分别在第一列、第四列和第三列中。当我们完成矩阵乘法时,这用于按顺序拉出 B 矩阵的第一行、第四行和第三行。这种使用 one-hot 向量提取矩阵特定行的技巧是 transformer 工作原理的核心。
¸一阶序列模型
我们可以暂时搁置矩阵,然后回到我们真正关心的问题,即单词序列。想象一下,当我们开始开发我们的自然语言计算机接口时,我们只想处理三个可能的命令:
Show me my directories please.
Show me my files please.
Show me my photos please.
我们的词汇量现在是七个:{directories, files, me, my, photos, please, show}。
表示序列的一种有用方法是使用转移模型。对于词汇表中的每个单词,它会显示下一个单词可能是什么。如果用户一半时间询问照片,30% 时间询问文件,其余时间询问目录,转移模型将如下所示。远离任何单词的转换总和总是加起来为 1。
这种特殊的转移模型称为马尔可夫链,因为它满足马尔可夫性质,即下一个单词的概率仅取决于最近的单词。更具体地说,它是一阶马尔可夫模型,因为它只关注最近的单个单词。如果它考虑最近的两个词,它将是二阶马尔可夫模型。
事实证明,马尔可夫链可以方便地用矩阵形式表示。使用我们在创建 one-hot 向量时使用的相同索引方案,每一行代表我们词汇表中的一个词。每一列也是如此。矩阵转移模型将矩阵视为查找表。找到与你感兴趣的词相对应的行。每列中的值显示下一个出现该词的概率。因为矩阵中每个元素的值代表一个概率,所以它们都会落在 0 和 1 之间。因为概率总和为 1,所以每行中的值总和为 1。
在这里的转移矩阵中,我们可以清楚地看到我们三个句子的结构。几乎所有的转移概率都是 0 或 1。马尔可夫链中只有一处发生分支。在 my 之后,单词 directories、files 或 photos 可能会出现,每一个都有不同的概率。除此之外,对于接下来会出现哪个词没有任何不确定性。这种确定性通过转移矩阵中大部分为 1 和 0 来反映。
我们可以重新审视我们的技巧,即使用矩阵乘法与独热向量来提取与任何给定单词相关的转移概率。例如,如果我们只是想隔离哪个单词出现在 my 之后的概率,我们可以创建一个表示单词 my 的独热向量,并将它乘以我们的转移矩阵。这会拉出相应的行,并向我们展示下一个单词的概率分布。
¸二阶序列模型
仅根据当前单词预测下一个单词很困难。这就像在给出第一个音符后预测曲调的其余部分。如果我们至少能得到两个音符,我们的机会就会大很多。
我们可以看到它是如何在我们的计算机命令的另一种玩具语言模型中工作的。我们希望这个人只会看到两个句子,比例为 40/60。
Check whether the battery ran down please.
Check whether the program ran please.
马尔可夫链为此说明了一个一阶模型。
在这里我们可以看到,如果我们的模型查看最近的两个词,而不是一个,它可以做得更好。当它遇到 battery run 时,它知道下一个词是 down,当它看到 program run 时,它知道下一个词是 please。这消除了模型中的一个分支,减少了不确定性并增加了信心。回顾两个词将其变成二阶马尔可夫模型。它提供了更多的上下文来预测下一个单词。
为了突出两者之间的区别,这里是一阶转移矩阵,
这是二阶转移矩阵。
请注意二阶矩阵如何为每个单词组合(其中大部分未在此处显示)单独一行。这意味着如果我们从 的词汇量开始,那么转换矩阵有 行。
这给我们带来了更多信心:二阶模型中个数较多,分数较少。只有一行包含分数,我们模型中的一个分支。直觉上,查看两个单词而不是一个单词可以提供更多上下文,更多信息可以作为下一个单词猜测的基础。
¸带跳跃的二阶序列模型
当我们只需要回顾两个词来决定下一个词时,二阶模型效果很好。当我们不得不进一步回顾时呢?想象一下我们正在构建另一个语言模型。这个只需要代表两个句子,每个句子出现的可能性相同。
Check the program log and find out whether it ran please.
Check the battery log and find out whether it ran down please.
在这个例子中,为了确定哪个词应该跟在 ran 之后,我们必须回顾过去的 8 个词。如果我们想改进我们的二阶语言模型,我们当然可以考虑三阶和更高阶模型。然而,由于词汇量很大,这需要结合计算力才能执行。八阶模型的简单实现会有 行,对于任何合理的词汇来说都是一个荒谬的数字。
相反,我们可以引入一些技巧并设计二阶模型,但考虑最近单词与之前出现的每个单词的组合。它仍然是二阶的,因为我们一次只考虑两个词,但它允许我们进一步回溯并捕获长程依赖。这种带有跳过的二阶模型与完整的无数阶模型之间的区别在于,我们丢弃了大部分词序信息和前面词的组合。剩下的还是蛮给力的。
马尔可夫链现在完全让我们失望了,但我们仍然可以表示每对前面的单词和后面的单词之间的联系。在这里,我们放弃了数字权重,而是只显示与非零权重相关联的箭头。较大的权重用较粗的线表示。
这是它在转换矩阵中的样子。
此视图仅显示与预测 ran 之后的单词相关的行。它显示了词汇表中每个其他单词之前出现的最近单词 ran 的实例。仅显示相关值,所有空单元格都是零。
首先变得明显的是,当试图预测 ran 之后的单词时,我们不再只看一行,而是看一整套。我们现在已经离开了马尔可夫。每行不再代表序列在特定点的状态。相反,每一行代表许多特征中的一个,这些特征可以描述特定点的序列。最近的单词与之前出现的每个单词的组合构成了适用行的集合,可能是一个大集合。由于这种意义的变化,矩阵中的每个值不再代表概率,而是投票。将对投票进行汇总和比较以预测下一个单词。
接下来变得明显的是,大多数特征都无关紧要。大多数单词都出现在两个句子中,因此它们已被看到的事实无助于预测接下来会发生什么。它们都具有 0.5 的值。仅有的两个例外是 battery 和 program。它们有一些 1 和 0 的权重。特征 battery, ran 表示 ran 是最近的词,并且 battery 出现在句子的较早位置。此特征的权重为 1 与 down 相关联,权重为 0 与 please 相关联。同样,特征 program, ran 具有相反的一组权重。这种结构表明,句子中较早出现的这两个词对于预测下一个词是决定性的。
要将这组词对特征转换为对下一个词的估计,需要对所有相关行的值求和。按列相加,序列 Check the program log and find out whether it ran 为所有单词生成的总和为 0,除了 4 代表down和 5 代表please。序列 Check the battery log and find out whether it ran 相同,除了 5 表示 down 和 4 表示please。通过选择投票总数最高的单词作为下一个单词预测,该模型为我们提供了正确的答案,尽管它具有八个单词的深度依赖关系。
¸掩膜
更仔细地考虑,这是不令人满意的。4 票和 5 票的总票数相差不大。这表明该模型并不像它应该的那样自信。在更大、更有组织的语言模型中,很容易想象这种微小的差异可能会在统计噪声中丢失。
我们可以通过清除所有无信息的特征投票来加强预测。除了电池,运行和程序,运行。记住这一点很有帮助,我们通过将相关行与一个向量相乘来显示当前哪些特征处于活动状态,从而将相关行从转移矩阵中拉出来。到目前为止,对于这个例子,我们一直在使用此处显示的隐含特征向量。
每个特征都为 1,该特征是 ran 与其前面的每个单词的组合。它后面的任何词都不会包含在特征集中。在下一个单词预测问题中,这些还没有出现,因此使用它们预测接下来会发生什么是不合理的。并且这不包括所有其他可能的单词组合。对于这个例子,我们可以安全地忽略这些,因为它们都将为零。
为了改善我们的结果,我们还可以通过创建掩膜将无用的特征强制消除掉,即设为零。它是一个充满了 1 的向量,除了你想要隐藏或屏蔽的位置,这些位置被设置为零。在我们的例子中,我们想屏蔽除了 battery, ran 和 program, ran 之外的所有内容,这是仅有的两个有用的特征。
为了应用 mask,我们将两个向量逐个元素相乘。未屏蔽位置中的任何特征活动值都将乘以 1 并保持不变。屏蔽位置中的任何特征活动值都将乘以 0,因此强制为 0。
mask 具有隐藏大量转移矩阵的效果。它隐藏了 run 与除 battery 和 program 之外的所有内容的组合,只留下重要的特征。
在屏蔽了无用的特征之后,下一个词的预测变得更强。当单词 battery 出现在句子的前面时,预测 ran 之后的单词 down 的权重为 1,please 的权重为 0。原本 25% 的权重差异现在变成了无穷大百分比的差异。毫无疑问接下来是什么词。当 program 前面出现时,同样强烈的预测也会发生。
这种选择性掩蔽的过程是关于 transformer 的原始论文标题中提到的 attention。到目前为止,我们所描述的只是论文中如何实现注意力的近似值。它捕获了重要的概念,但细节有所不同,稍后会缩小差距。
¸小结
用 selective-second-order-with-skips 模型来思考 transformer 的作用是一个很有意思的思路,至少在解码器端是这样。它大致捕捉到了像 OpenAI 的 GPT-3 这样的生成语言模型正在做什么。它并没有讲述完整的故事,但它代表了故事的中心要旨。
接下来的部分将更多地介绍这种直观的解释与 transformer 的实现方式之间的差距。这些主要是由三个实际考虑因素驱动的。
计算机特别擅长矩阵乘法。整个行业都围绕着专门为快速矩阵乘法构建计算机硬件。任何可以表示为矩阵乘法的计算都可以变得非常高效。你不妨设想一下,这是一辆子弹头列车,如果你能把行李放进去,它会很快带你去你想去的地方。
每个步骤都需要是可微分的。到目前为止,我们一直在处理玩具示例,并且可以手动选择所有转移概率和掩码值,即模型的参数。在实践中,这些必须通过反向传播来学习,这取决于每个计算步骤都是可微分的。这意味着对于参数的任何微小变化,我们都可以计算模型误差或损失的相应变化。
梯度需要平滑且条件良好。所有参数的所有导数的组合就是损失梯度。在实践中,要使反向传播表现良好,需要平滑的梯度,也就是说,当你在任何方向上做小步时,斜率不会很快改变。当梯度条件良好时,它们的表现也会好得多,也就是说,它在一个方向上不会比另一个方向大得多。如果将损失函数想象成一幅风景,那么大峡谷就是一处条件差的风景。根据你是沿着底部还是向上行驶,将有非常不同的坡度。相比之下,经典 Windows 屏幕保护程序的连绵起伏的丘陵将具有良好的梯度效果。
如果构建神经网络的科学是创建可微分的积木块,那么它们的艺术就是以梯度不会变化太快且在每个方向上大致相同的方式堆叠各个部分。
¸注意力作为矩阵乘法
通过计算每个单词对到下一个单词的转移在训练中发生的频率,可以直接构建特征权重,但注意 mask 则不然。到目前为止,我们已经凭空提取了 mask 向量。Transformer 如何找到相关 mask 很重要。使用某种查找表是很自然的,但现在我们正努力将所有内容表示为矩阵乘法。我们可以使用我们上面介绍的相同查找方法,将每个单词的 mask 向量堆叠到一个矩阵中,并使用最近单词的独热表示来提取相关 mask。
在显示 mask 向量集合的矩阵中,为了清楚起见,我们只显示了我们试图提取的那个 mask 向量。
我们终于到了可以开始结合论文的地步了。此 mask 查找由注意力方程中的 项表示。
查询 表示感兴趣的特征,矩阵 表示 mask 的集合。因为它与 mask 一起存储在列中,而不是行中,所以在相乘之前需要转置。当我们全部完成时,我们将对此进行一些重要的修改,但在这个级别它捕获了 transformer 使用的可微分查找表的概念。
¸作为矩阵乘法的二阶序列模型
到目前为止,我们一直在处理的另一个步骤是转移矩阵的构造。我们已经清楚了逻辑,但不清楚如何用矩阵乘法来实现。
一旦我们得到了注意力步骤的结果,一个向量包含了最近的词和它之前的一小部分词,我们需要将其转化为特征,每个特征都是一个词对。注意 mask 为我们提供了所需的原材料,但它并没有构建那些词对特征。为此,我们可以使用单层全连接神经网络。
为了了解神经网络层如何创建这些词对,我们将手工制作一个看看。它将被人为地清理和程式化,其权重与实际中的权重没有任何相似之处,但它将展示神经网络如何具有构建这两个词对特征所必需的表达能力。为了保持小而干净,将只关注此示例中的三个参与词,电池、程序、运行。
在上面的层图中,我们可以看到权重如何将每个单词的存在和不存在组合成一个特征集合。这也可以用矩阵形式表示。
它可以通过矩阵乘法与表示到目前为止所见单词集合的向量来计算。
元素 battery 和 ran 为 1,元素 program 为 0,而元素 bias 始终为 1,它是神经网络的一个特征。通过矩阵乘法,代表 battery, ran 的元素为 1 ,代表 program, ran 的元素为 -1 。另一种情况的结果是类似的。
计算这些单词组合特征的最后一步是应用线性修正单元 (ReLU) 非线性。这样做的效果是用零替换任何负值。这会清除这两个结果,因此它们表示每个单词组合特征的存在(1)或不存在(0)。
有了这些操作,我们终于有了一个基于矩阵乘法的方法来创建多词特征。虽然我最初声称这些由最近的词和一个较早的词组成,但仔细观察这种方法就会发现它也可以构建其他特征。当特征创建矩阵被学习时,而不是硬编码,其他结构也可以被学习。即使在这个玩具示例中,也没有什么可以阻止创建像 battery、program、ran 这样的三个词组合。如果这种组合足够普遍,它可能最终会被表示出来。没有任何方法表明这些单词的出现顺序(至少现在还没有),但我们绝对可以利用它们的同时出现来进行预测。甚至可以使用忽略最近单词的单词组合,例如 battery, program。这些和其他类型的特征可能是在实践中创建的,暴露了我在声称 transformers 是一个选择性二阶带跳过序列模型时所做的过度简化。它的细微差别远不止于此,现在你可以准确地看到细微差别是什么。这不会是我们最后一次改变故事以融入更多微妙之处。
在这种形式下,多词特征矩阵准备好再进行一次矩阵乘法,即我们上面开发的带有跳跃的二阶序列模型。总的来说,如下所示的
特征创建矩阵
乘法,ReLU 非线性,和
转移矩阵
乘法
的操作序列是应用注意力后紧接着应用的前馈处理步骤。论文中的等式 2 以简明的数学公式显示了这些步骤。
该论文的图 1 架构图显示了这些作为前馈块(Feed Forward block)集中在一起。
¸序列填充
到目前为止,我们只讨论了下一个单词预测。为了让解码器生成一个长序列,我们需要添加一些片段。第一个是提示(prompt),即一些示例文本,它是为 Transformer 提供运行开始和构建其余序列的上下文。它被送入解码器,即上图中右侧的列,其中标记为“输出(右移)”。选择给出有趣序列的提示本身就是一门艺术,称为提示工程。这也是人类修改自己的行为来支持算法的一个很好的例子,而不是反过来。
一旦解码器有一个部分序列可以开始,它就会进行前向传递。最终结果是一组预测的单词概率分布,序列中的每个位置都有一个概率分布。在每个位置,分布显示了词汇表中每个下一个单词的预测概率。我们不关心序列中每个已建立单词的预测概率,因为他们已经在了。我们真正关心的是提示结束后下一个单词的预测概率。有几种方法可以选择该词应该是什么,但最直接的方法称为贪心算法,即选择概率最高的词。
然后将新的下一个单词添加到序列中,替换解码器底部的 “Outputs”,然后重复该过程,直到你想停下来为止。
我们还没有完全准备好详细描述的部分是另一种形式的掩蔽,确保当 transformer 做出预测时它只看后面,而不是前面。它应用于标记为“Masked Multi-Head Attention”的块中。当我们可以更清楚它是如何完成时,我们将在稍后重新讨论它。
¸嵌入(Embeddings)
正如我们到目前为止所描述的,transformer 太大了。对于假设为 50,000 的词汇量 ,所有单词对和所有潜在的下一个单词之间的转换矩阵将具有 50,000 列和 50,000 平方(25 亿)行,总计超过 100 万亿个元素。
问题不仅仅是矩阵的大小。为了建立一个稳定的转移语言模型,我们必须至少多次提供说明每个潜在序列的训练数据。这将远远超过即使是最有野心的训练数据集的容量。
那怎么办呢?幸运的是,这两个问题都有一个解决方法,即嵌入。
在一种语言的独热表示中,每个词都有一个向量元素。对于大小为 的词汇表,该向量是一个 维空间。每个单词代表该空间中的一个点,沿多个轴之一距原点一个单位。我还没有找到绘制高维空间的好方法,但下面有一个粗略的表示。
在嵌入中,这些词点全部被获取并重新排列(投影,用线性代数术语)到低维空间中。例如,上图显示了它们在二维空间中的样子。现在,我们不再需要 个数字来指定一个单词,而只需要 2 个。这些是新空间中每个点的 坐标。这是我们的玩具示例的二维嵌入的样子,以及一些单词的坐标。
一个好的嵌入将具有相似含义的词组合在一起。使用嵌入的模型在嵌入空间中学习模式。这意味着无论它学会用一个词做什么,都会自动应用到它旁边的所有词上。这具有减少所需训练数据量的额外好处。每个例子都提供了一点点学习,可以应用于整个单词邻域。
在这个插图中,我试图通过将重要组件放在一个区域(电池、日志、程序)、介词放在另一个区域(向下、出去)和靠近中心的动词(检查、查找、运行)来展示这一点。在实际嵌入中,分组可能不是那么清晰或直观,但基本概念是相同的。行为相似的单词之间的距离较小。
嵌入大大减少了所需的参数数量。然而,嵌入空间中的维度越少,原始单词的信息就会被丢弃得越多。语言的丰富性仍然需要相当多的空间来布置所有重要的概念,以免它们互相踩到脚趾。通过选择嵌入空间的大小,我们可以在计算负载与模型精度之间进行权衡。
将单词从其独热表示投影到嵌入空间涉及矩阵乘法。投影是矩阵最擅长的。从具有 1 行和 列的独热矩阵开始,然后移动到二维嵌入空间,投影矩阵将具有 行和两列,如下所示。
这个例子展示了一个 one-hot 向量,例如代表 battery,如何提取与其关联的行,其中包含单词在嵌入空间中的坐标。为了使关系更清晰,one-hot 向量中的 0 被省略,所有其他未从投影矩阵中拉出的行也是如此。完整的投影矩阵是密集的,每一行都包含与之关联的单词的坐标。
投影矩阵可以将 one-hot 词汇向量的原始集合转换为你想要的任何维度空间中的任何配置。最大的技巧是找到一个有用的投影,既将相似的词组合在一起,又能有足够的维度来分散它们。对于常见的语言,比如英语,有一些不错的预计算嵌入。此外,与 transformer 中的其他所有内容一样,它可以在训练期间学习。
在原始论文的图 1 架构图中,这里是嵌入发生的地方。
¸位置编码
到目前为止,我们假设单词的位置被忽略,至少对于最近单词之前的任何单词都是如此。现在我们开始使用位置嵌入来解决这个问题。
有几种方法可以将位置信息引入我们的嵌入式单词表示中,但在原始 transformer 中采用的方法是添加一个沿圆周摆动的机制。
单词在嵌入空间中的位置充当圆心。根据它在单词序列中的位置,向它添加一个扰动。对于每个位置,单词以不同的角度转动相同的距离,从而当在序列中移动时形成圆形图案。序列中彼此靠近的词具有相似的扰动,但相距较远的词将在不同方向上受到扰动。
由于圆是二维图形,表示圆形摆动需要修改嵌入空间的二维。如果嵌入空间包含两个以上的维度(几乎总是如此),则在所有其他维度对中重复圆形摆动,但具有不同的角频率,也就是说,在每种情况下,它在每个维度中扫出不同的旋转次数。在某些维度对中,摆动会扫过圆的许多旋转。在其他对中,它只会扫过一小部分旋转。所有这些不同频率的圆形摆动的组合可以很好地表示单词在序列中的绝对位置。
这里是试图以直觉方式去了解为什么会这样。它似乎以一种不会破坏单词和注意力之间学习关系的方式将位置信息添加到组合中。为了更深入地了解数学和含义,我推荐 Amirhossein Kazemnejad 的位置编码教程。
在规范架构图中,这些块显示了位置代码的生成及其对嵌入词的添加。
¸去嵌入(De-embeddings)
嵌入单词使它们的使用效率大大提高,但是一旦聚会结束,它们就需要从原始词汇表中转换回单词。去嵌入的完成方式与嵌入的方式相同,即从一个空间到另一个空间的投影,同样也是矩阵乘法。
去嵌入矩阵的形状与嵌入矩阵相同,但行数和列数翻转了。行数是我们要转换的空间的维度。在我们一直使用的例子中,它是我们嵌入空间的大小,2。列数是我们要转换到的空间的维度 — 完整词汇表的独热表示的大小,在我们的示例中为 13。
良好的去嵌入矩阵中的值不像嵌入矩阵中的值那样易于说明,但效果相似。当表示单词程序的嵌入向量乘以去嵌入矩阵时,相应位置的值很高。然而,由于投影到更高维空间的工作方式,与其他词相关的值往往不会为零。嵌入空间中最接近程序的词也将具有中高值。其他词的价值接近于零。并且可能会有很多带有负值的词。词汇空间中的输出向量将不再是独热或稀疏的。它将是密集的,几乎所有值都不为零。
没关系。我们可以通过选择与最高值关联的词来重新创建独热向量。此操作也称为 argmax,即给出最大值的参数(元素)。如上所述,这就是如何进行贪心序列补全。这是一个很棒的一关,但我们可以做得更好。
如果一个嵌入很好地映射到几个单词,我们可能不想每次都选择最好的一个。它可能只是比其他选择好一点点,添加一点多样性可以使结果更有趣。此外,有时在确定最终选择之前先看几个词并考虑句子可能的所有方向也很有用。为了做到这些,我们必须首先将我们的去嵌入结果转换为概率分布。
Softmax
argmax 函数在最高值取胜的意义上是“困难的”,即使它仅比其他值无限大。如果我们想同时考虑多种可能性,最好使用我们从 softmax 获得的“软”最大值函数。要获得向量中值 的 softmax ,请将 的指数 除以向量中所有值的指数之和。
由于三个原因,softmax 在这里很有用。首先,它将我们的去嵌入结果向量从任意一组值转换为概率分布。作为概率,比较不同单词被选中的可能性变得更容易,如果我们想进一步展望未来,甚至可以比较多单词序列的可能性。
其次,它使顶部附近的区域变薄。如果一个词的得分明显高于其他词,softmax 会夸大这种差异,使其看起来几乎像 argmax,获胜值接近 1,而所有其他词接近 0。但是,如果有几个单词都接近顶部,它会将它们全部保留为极有可能的结果,而不是人为地压倒接近第二名的结果。
第三,softmax 是可微分的,这意味着我们可以计算结果的每个元素将发生多少变化,给定任何输入元素的微小变化。这允许我们将它与反向传播一起使用来训练我们的变压器。
去嵌入变换(如下面的线性块所示)和 softmax 函数一起完成了去嵌入过程。
¸多头注意力
现在我们已经接受了投影(矩阵乘法)和空间(向量大小)的概念,我们可以重新审视核心注意力机制。如果我们可以更具体地了解每个阶段的矩阵形状,将有助于阐明算法。有一个重要数字的简短列表。
:词汇量。在我们的示例中为 13。通常在数万个。
:最大序列长度。在我们的示例中为 12。报纸上大概有几百个。例如,GPT-3 中为 2048。
d_model:整个模型中使用的嵌入空间的维数。例如,论文中为 512。
原始输入矩阵的构造方法是从句子中获取每个词的独热表示,并将它们堆叠起来,使每个独热向量都是它自己的行。生成的输入矩阵有 行和 列,我们可以将其缩写为 。
如前所述,嵌入矩阵有 行和 d_model 列,我们可以将其缩写为 。将两个矩阵相乘时,结果采用第一个矩阵的行数和第二个矩阵的列数。这使嵌入的单词序列矩阵的形状为 。
我们可以通过 transformer 跟踪矩阵形状的变化,作为跟踪正在发生的事情的一种方式。在初始嵌入之后,位置编码是相加的,而不是相乘的,所以它不会改变事物的形状。然后嵌入的词序列进入注意力层,并以相同的形状从另一端出来。(我们稍后会回到这些的内部工作原理。)最后,去嵌入将矩阵恢复到其原始形状,为序列中每个位置的词汇表中的每个单词提供概率。
为什么需要多个注意力头
终于到了面对我在第一次解释注意力机制时所做的一些简单假设的时候了。单词表示为密集嵌入向量,而不是独热向量。注意力不只是 1 或 0、开或关,还可以是介于两者之间的任何位置。为了让结果落在 0 和 1 之间,我们再次使用 softmax 技巧。它具有强制所有值位于我们的 [0, 1] 注意力范围内的双重好处,它有助于强调最高值,同时积极挤压最小值。这是我们之前在解释模型的最终输出时利用的差分 almost-argmax 行为。
关注 softmax 函数的一个复杂结果是它会倾向于关注单个元素。这是我们以前没有考虑过的限制。有时在预测下一个词时记住前面的几个词是很有用的,而 softmax 剥夺了我们这一点。这是模型的问题。
解决方案是让多个不同的注意力实例或头脑同时运行。这让 transformer 在预测下一个词时同时考虑几个先前的词。它带回了我们在将 softmax 引入图片之前所拥有的力量。
不幸的是,这样做确实会增加计算量。计算注意力已经是大部分工作,我们只是将它乘以我们想要使用的头数。为了解决这个问题,我们可以重新使用将所有内容投影到低维嵌入空间的技巧。这缩小了所涉及的矩阵,从而大大减少了计算时间。
要了解结果如何,我们可以继续查看矩阵形状。通过多头注意块的脉络来追踪矩阵形状需要三个以上的数字。
d_k:用于键和查询的嵌入空间中的维度。在论文中为 64。
d_v:用于值的嵌入空间中的维度。在论文中为 64。
h:头数。在论文中为 8。
嵌入单词序列作为后续所有内容的基础。在每种情况下,都有一个矩阵 、 和 (在架构图中都无用地显示为“线性”块),它将原始的嵌入词序列转换为值矩阵 、查询矩阵 、和键矩阵 。 和 具有相同的形状 ,但 可以不同,。它使事情有点混乱 和 在论文中是相同的,但它们不一定是。此设置的一个重要方面是每个注意力头都有自己的 、 和 转换。这意味着每个头都可以放大和扩展它想要关注的嵌入空间的部分,并且它可以与其他每个头所关注的不同。
每个注意力头的结果与 具有相同的形状。现在我们有 个不同的结果向量的问题,每个向量处理序列的不同元素。为了将它们合二为一,我们利用线性代数的力量,将所有这些结果连接成一个巨大的 矩阵。然后,为了确保它以与开始时相同的形状结束,我们使用形状 的另一个变换。
简明扼要地说明了所有这些。
¸回顾单头注意力
我们已经完成了上面注意力的概念说明。实际的实现有点混乱,但我们早先的直觉仍然有帮助。查询和键不再容易检查和解释,因为它们都被投射到它们自己的特殊子空间中。在我们的概念图中,查询矩阵中的一行代表词汇空间中的一个点,感谢 one-hot 表示,它代表一个且仅一个词。在它们的嵌入形式中,查询矩阵中的一行代表嵌入空间中的一个点,该点将靠近一组具有相似含义和用法的词。概念图将一个查询词映射到一组键,这些键又过滤掉所有未被关注的值。实际实现中的每个注意力头将一个查询词映射到另一个低维嵌入空间中的一个点。结果是注意力变成了词组之间的关系,而不是单个词之间的关系。它利用语义相似性(嵌入空间中的接近性)来概括它对相似词的了解。
通过注意力计算跟踪矩阵的形状有助于跟踪它在做什么。
查询和键矩阵 和 都具有形状 。由于 在乘法之前被转置, 的结果给出了 的矩阵。将该矩阵的每个元素除以 的平方根已被证明可以防止值的大小过于增长,并有助于反向传播表现良好。正如我们提到的,softmax 将结果硬塞进 argmax 的近似值,倾向于将注意力集中在序列中的一个元素而不是其余元素上。在这种形式中, 注意力矩阵粗略地将序列的每个元素映射到序列的另一个元素,表明它应该关注什么,以便获得最相关的上下文来预测下一个元素。它是一个最终应用于值矩阵 的过滤器,只留下参与值的集合。这具有忽略序列中之前出现的绝大多数内容的效果,并将聚焦在最需要注意的一个先验元素上。
理解这组计算的一个棘手部分是要记住,它正在为我们输入序列的每个元素、我们句子中的每个单词计算注意力,而不仅仅是最近的单词。它还在计算对前面单词的注意力。我们并不真正关心这些,因为他们的下一个词已经被预测和确定。它还在计算未来单词的注意力。这些目前还没有多大用处,因为它们太过遥远,而且它们的直接前任还没有被选中。但是这些计算可以通过间接路径影响对最近单词的注意力,因此我们将它们全部包括在内。只是当我们走到最后并计算序列中每个位置的单词概率时,
Mask 块引入强制约束,使得对于这种序列填充任务中后面的单词是不可见的。它避免了从虚构的未见词中引入任何奇怪错误。它粗糙而有效,手动将对所有超过当前位置的单词的注意力设置为负无穷大。在 The Annotated Transformer 中,一个对逐行显示 Python 实现的论文非常有用的伴侣,掩码矩阵是可视化的。紫色单元格显示不允许注意的地方。每行对应于序列中的一个元素。允许第一行关注其自身(第一个元素),但之后什么都不关注。允许最后一行处理自身(最后一个元素)和之前的所有内容。掩码是 矩阵。它不是与矩阵乘法一起应用,而是与更直接的逐元素乘法一起应用。这相当于手动将注意力矩阵掩码中的所有紫色元素设置为负无穷大。
注意力实现方式的另一个重要区别是,它利用单词在序列中呈现的顺序,并且将注意力表示为位置到位置关系,而不是单词到单词的关系。这在其 形状中很明显。它将序列中的每个元素(由行索引指示)映射到序列的某些其他元素(由列索引指示)。这有助于我们更容易地可视化和解释它在做什么,因为它在嵌入空间中运行。我们省去了在嵌入空间中寻找附近词来表示查询和键之间关系的额外步骤。
¸跳跃连接
注意力是 transformer 所做的最基本的部分。它是核心机制,我们现在已经遍历了它一个非常具体的层次。从这里开始的一切都是使其正常工作所必需的管道。它是让注意力拉动我们繁重的工作量的其余部分。
我们还没有解释的一件事情是跳跃连接。这些发生在多头注意块周围,以及标记为“添加和规范”的块中的元素明智的前馈块周围。在跳过连接中,输入的副本被添加到一组计算的输出中。注意块的输入被添加回其输出。元素前馈块的输入被添加到它的输出。
跳跃连接有两个目的。
首先是它们有助于保持梯度平滑,这对反向传播有很大帮助。注意力是一个过滤器,这意味着当它正常工作时,它会阻止大部分试图通过它的东西。这样做的结果是,如果许多输入碰巧落入被阻塞的通道,那么许多输入的微小变化可能不会对输出产生太大变化。这会在平坦的梯度中产生死点,但仍然离谷底不远。这些鞍点和脊线是反向传播的重要触发点。跳跃连接有助于解决这些问题。在注意力的情况下,即使所有的权重都为零并且所有的输入都被阻止,跳跃连接会将输入的副本添加到结果中,并确保任何输入的微小变化仍然会在结果中产生明显的变化。这可以防止梯度下降卡在远离好的解决方案的地方。
自 ResNet 图像分类器问世以来,跳跃连接变得流行,因为它们可以提高性能。它们现在是神经网络架构中的标准功能。在视觉上,我们可以通过比较有和没有跳跃连接的网络来看到跳跃连接的效果。下图显示了具有和不具有跳跃连接的 ResNet。当使用跳跃连接时,损失函数山丘的斜率更加温和和均匀。
skip connections 的第二个目的是特定于 transformers — 保留原始输入序列。即使有很多注意力头,也不能保证一个词会关注到它自己的位置。注意力过滤器有可能完全忘记最近的单词,转而关注所有可能相关的早期单词。跳跃连接采用原始单词并手动将其添加回信号中,这样就不会丢失或忘记它。这种鲁棒性的来源可能是 transformer 在如此多不同的序列完成任务中表现良好的原因之一。
¸图层归一化
归一化是与跳跃连接配合良好的步骤。没有理由必须将它们放在一起,但在一组计算(如注意力或前馈神经网络)之后放置时,它们都能发挥最佳作用。
层规范化的简短版本是将矩阵的值移动到均值为零并缩放为标准差为 1。
更长的版本是,在像 transformer 这样的系统中,有很多移动部分,其中一些不是矩阵乘法(例如 softmax 运算符或整流线性单元),重要的是值有多大,以及它们如何在正负之间平衡。如果一切都是线性的,可以将所有输入加倍,输出将是原来的两倍,一切都会正常进行。神经网络并非如此。它们本质上是非线性的,这使得它们非常具有表现力,但对信号的幅度和分布也很敏感。归一化是一种技术,已被证明可用于在整个多层神经网络的每一步中保持信号值的一致分布。
关于规范化,我最喜欢的一点是,除了像我刚才给出的高级解释之外,没有人完全确定它为何如此有效。
¸多层
当我们在上面奠定基础时,我们展示了一个注意块和一个精心选择权重的前馈块足以构建一个像样的语言模型。在我们的示例中,大多数权重为零,其中一些为 1,而且它们都是手工挑选的。从原始数据进行训练时,我们不会有这种奢侈。一开始权重都是随机选择的,大部分接近于零,少数不是我们需要的。距离我们的模型要表现良好所需的位置还有很长的路要走。
通过反向传播的随机梯度下降可以做一些非常了不起的事情,但它在很大程度上依赖于运气。如果只有一种方法可以得到正确的答案,只有一种网络运行良好所必需的权重组合,那么它不太可能找到自己的方法。但是,如果有很多路径可以找到一个好的解决方案,那么模型到达那里的可能性就会大得多。
只有一个注意力层(只有一个多头注意力块和一个前馈块)只允许一条通往一组好的 transformer 参数的路径。每个矩阵的每个元素都需要找到正确的值才能使事情顺利进行。它很脆弱,很可能会陷入一个远非理想的解决方案,除非对参数的初始猜测非常非常幸运。
Transformers 回避这个问题的方法是拥有多个注意力层,每个注意力层都使用前一个的输出作为其输入。跳跃连接的使用使整个管道对个别注意块失败或给出不稳定的结果具有鲁棒性。拥有倍数意味着还有其他人在等待填补空缺。如果一个人偏离了轨道,或者以任何方式未能发挥其潜力,就会有另一个下游有另一个机会来缩小差距或修复错误。该论文表明,层数越多性能越好,尽管在 6 层之后改进变得微乎其微。
考虑多层的另一种方法是将其视为传送带装配线。每个注意块和前馈块都有机会将输入拉下线,计算有用的注意矩阵并进行下一个单词预测。无论它们产生什么结果,有用与否,都会被添加回传送带,并传递到下一层。
这与传统的将多层神经网络描述为“深度”形成对比。由于跳跃连接,连续的层不会像提供冗余一样提供越来越复杂的抽象。在一层中错过的任何集中注意力、创建有用特征和做出准确预测的机会都可以被下一层抓住。工人成为流水线上的工人,每个人都尽其所能,但不必担心接住每一件,因为下一个工人会接住他们错过的。
¸解码器栈
到目前为止,我们已经小心地忽略了编码器堆栈(transformer 架构的左侧),而选择了解码器堆栈(右侧)。我们将在几段中解决这个问题。但值得注意的是,解码器本身就非常有用。
正如我们在序列完成任务描述中所布局的那样,解码器可以完成部分序列并将它们扩展到想要的程度。OpenAI 创建了生成式预训练 (GPT) 系列模型来执行此操作。他们在本报告中描述的架构应该看起来很熟悉。它是一个带有编码器堆栈的 transformer,它的所有连接都通过手术移除了。剩下的是一个 12 层的解码器堆栈。
每当遇到生成模型(如 BERT、ELMo 或 Copilot)时,都可能看到 transformer 的解码器。
¸编码器栈
我们所了解的关于解码器的几乎所有内容也适用于编码器。最大的区别是,最后没有做出明确的预测,我们可以用来判断其表现的对错。相反,编码器堆栈的最终产品是令人失望的抽象 — 嵌入空间中的一系列向量。它被描述为序列的纯语义表示,脱离了任何特定的语言或词汇,但这对我来说太浪漫了。我们可以肯定的是,它是一个有用的信号,可用于向解码器堆栈传达意图和意义。
拥有编码器堆栈可以充分发挥 transformer 的潜力,而不仅仅是生成序列,它们现在可以将序列从一种语言翻译为另一种语言。翻译任务的训练不同于序列完成任务的训练。训练数据需要原始语言的序列和目标语言的匹配序列。完整的原始语言通过编码器运行(这次没有屏蔽,因为我们假设我们在创建翻译之前看到整个句子)和结果,最终编码器层的输出作为输入提供给每个解码器层。然后解码器中的序列生成像以前一样进行,但这次没有启动它的提示。
¸交叉注意力
启动和运行完整 transformer 的最后一步是编码器和解码器堆栈之间的连接,即交叉注意力块。我们把它留到最后,多亏了我们奠定的基础,没有太多要解释的了。
Cross-attention 的工作原理与 self-attention 类似,只是键值矩阵 和值矩阵 基于最终编码器层的输出,而不是前一个解码器层的输出。查询矩阵 仍然是根据前一个解码器层的结果计算的。这是来自源序列的信息进入目标序列并引导其创建朝着正确方向发展的渠道。有趣的是,相同的嵌入式源序列被提供给解码器的每一层,支持连续层提供冗余并且都合作执行相同任务的概念。
¸Tokenizing
我们一路通过 transformer!我们对它进行了足够详细的介绍,应该不会留下任何神秘的黑盒子。有一些我们没有深入研究的实现细节。你需要了解它们才能为自己构建一个工作版本。最后的几个花絮与其说是关于 transformer 如何工作,不如说是关于让神经网络表现良好。Annotated Transformer 将帮助你填补这些空白。
我们还没有完全完成。关于我们如何开始表示数据,还有一些重要的事情要说。这与其说是算法的强大功能,不如说是深思熟虑地解释数据并理解其含义。
我们顺便提到,词汇表可以用高维独热向量表示,每个词都有一个元素。为了做到这一点,我们需要确切地知道我们要表示多少个词以及它们是什么。
一种天真的方法是列出所有可能的单词,就像我们可能在韦氏词典中找到的那样。对于英语,这将为我们提供数万个,具体数字取决于我们选择包含或排除的内容。但这是过于简单化了。大多数单词有多种形式,包括复数、所有格和变位。单词可以有替代拼写。除非你的数据经过非常仔细的清理,否则它会包含各种印刷错误。这甚至没有涉及自由格式文本、新词、俚语、行话和广阔的 Unicode 世界所带来的可能性。所有可能的单词的详尽列表将是不可行的。
一个合理的后备位置是让单个字符而不是单词作为构建块。详尽的字符列表完全在我们必须计算的能力范围内。然而,这有几个问题。在我们将数据转换到嵌入空间后,我们假设该空间中的距离具有语义解释,也就是说,我们假设靠得很近的点具有相似的含义,而距离较远的点则具有非常不同的含义。这使我们能够隐式地将我们对一个词的了解扩展到它的直接邻居,这是我们依赖于计算效率的假设,并且 transformer 从中汲取了一些泛化能力。
在单字层面,语义内容非常少。例如英语中有一些单字词,但并不多。表情符号是个例外,但它们并不是我们正在查看的大多数数据集的主要内容。这使我们处于无用的嵌入空间的不幸境地。
如果我们能够观察足够丰富的字符组合来构建语义上有用的序列,如单词、词干或单词对,那么理论上仍有可能解决这个问题。不幸的是,transformer 在内部创建的功能更像是一组输入对,而不是一组有序的输入。这意味着单词的表示将是字符对的集合,而没有强烈表示它们的顺序。transformer 将被迫不断地使用字谜,这使得它的工作变得更加困难。事实上,字符级表示的实验表明,transformer 在它们方面表现不佳。
字节对编码
幸运的是,有一个优雅的解决方案。称为字节对编码。从字符级表示开始,每个字符都被分配了一个代码,它自己的唯一字节。然后在扫描一些有代表性的数据后,将最常见的一对字节组合在一起并分配一个新字节,一个新代码。新代码被替换回数据,然后重复该过程。
例子
假设我们有一个语料库,其中包含单词(pre-tokenization之后)— old, older, highest, 和 lowest,我们计算这些词在语料库中的出现频率。假设这些词出现的频率如下:
{“old”: 7, “older”: 3, “finest”: 9, “lowest”: 4}
让我们在每个单词的末尾添加一个特殊的结束标记“”。
{“old”: 7, “older”: 3, “finest”: 9, “lowest”: 4}
在每个单词的末尾添加“”标记以标识单词边界能够让算法知道每个单词的结束位置(因为我们统计相邻字符对时不能把分别位于两个单词中的字符对算进去),这有助于算法查看每个字符并找到频率最高的字符配对。稍后我们将看到“”也能被算作字符对的一部分。
然后把 est 和 old 这些常见的词缀或字母组合找出来,根据频率排序,作为新的 token。解码时需要再次替换回去。
表示字符对的代码可以与表示其他字符或字符对的代码组合,以获得表示更长字符序列的新代码。代码可以表示的字符序列的长度没有限制。它们会根据需要增长,以表示通常重复的序列。字节对编码最酷的部分是推断要从数据中学习哪些长字符序列,而不是笨拙地表示所有可能的序列。它学习用单字节代码来表示像 transformer 这样的长词,但不会将代码浪费在类似长度的任意字符串上,例如 ksowjmckder。而且因为它保留了其单个字符构建块的所有字节代码,它仍然可以表示奇怪的拼写错误、新单词,甚至外语。
当使用字节对编码时,可以为其分配一个词汇量大小,并且它将不断构建新代码直到达到该大小。词汇量需要足够大,字符串足够长以捕获文本的语义内容。他们必须意味着什么。然后它们将足够丰富来为 transformer 供电。
在训练或借用字节对编码器后,我们可以用它来预处理输出数据,然后再将其送入 transformer 转换器。这将不间断的文本流分成一系列不同的块(希望其中大部分是可识别的单词)并为每个块提供简洁的代码。这是称为标记化的过程。
¸音频输入
现在回想一下,当我们开始整个冒险时,我们最初的目标是将音频信号或口头命令转换为文本表示。到目前为止,我们所有的例子都是在假设我们正在处理书面语言的字符和单词的情况下得出的。我们也可以将其扩展到音频,但这将更大胆地涉足信号预处理。
音频信号中的信息受益于一些重型预处理,以提取我们的耳朵和大脑用来理解语音的部分。该方法称为梅尔频率倒谱滤波,顾名思义,它的每一点都像巴洛克式的。如果你想深入了解引人入胜的细节,这里有一个图文并茂的教程。
预处理完成后,原始音频将变成一个向量序列,其中每个元素代表特定频率范围内音频活动的变化。它是密集的(没有元素为零)并且每个元素都是实值的。
从积极的方面来说,每个向量都为 transformer 提供了一个很好的“词”或标记,因为它意味着某些东西。它可以直接翻译成一组可识别为单词一部分的声音。
另一方面,将每个向量视为一个词是很奇怪的,因为每个向量都是独一无二的。同一组矢量值出现两次的可能性极小,因为声音的组合有很多微妙的不同。我们之前的 one-hot representation 和 byte pair encoding 策略没有帮助。
这里的技巧是注意像这样的密集实值向量是我们在嵌入单词后最终得到的。transformer 喜欢这种格式。为了利用它,我们可以像使用文本示例中的嵌入词一样使用 ceptrum 预处理的结果。这为我们节省了标记化和嵌入的步骤。
值得注意的是,我们也可以对我们想要的任何其他类型的数据执行此操作。许多记录的数据以一系列密集向量的形式出现。我们可以将它们直接插入到 transformer 的编码器中,就好像它们是嵌入的单词一样。
[1] https://e2eml.school/transformers.html