一、概述
这篇由Brandon Rohrer撰写的技术文档《Transformers from Scratch》系统性地拆解了Transformer模型的核心原理和实现细节,通过层层递进的数学推导和可视化类比,揭示了Transformer如何通过矩阵运算的巧妙组合实现强大的序列建模能力,为理解现代大语言模型奠定了扎实的理论基础。
其主要内容总结如下:
1. 基础概念铺垫
• 从词向量编码(One-hot encoding)开始,通过点积运算和矩阵乘法原理,建立词汇间相似性度量的数学基础。
• 以马尔可夫链模型为切入点,逐步构建一阶、二阶序列预测模型,揭示语言模型的演进逻辑。
2. Transformer核心机制
• 注意力机制:通过矩阵乘法实现动态掩码(Masking),模拟选择性关注历史词汇的能力。关键创新在于通过可学习的参数矩阵(Q/K)实现上下文感知的注意力分配。
• 位置编码:采用正弦波编码为序列位置信息添加可学习的几何特征,突破传统RNN的顺序处理限制。
• 嵌入空间:通过降维投影将稀疏的one-hot向量转化为稠密语义空间,实现词汇关系的分布式表示。
3. 架构实现细节
• 多头注意力机制:并行处理多个语义子空间的注意力模式。
• 残差连接和层标准化:保障深层网络的稳定训练。
• 编码器-解码器结构:分离特征提取与序列生成过程。
• 前馈网络层:通过ReLU激活实现非线性特征组合。
4. 工程实践考量
• 矩阵运算的硬件加速优势
• 反向传播的梯度稳定性设计
• 字节对编码(BPE)等分词策略
• 训练数据的规模与质量要求
5. 模型演进逻辑
从简单的马尔可夫链逐步发展为:
• 带跳跃连接的二阶模型 → 注意力原型
• 可微分参数矩阵 → 动态上下文建模
• 位置感知嵌入 → 序列关系建模
二、从零开始构建 Transformer 模型
1. 独热编码(One-Hot)
起初,摆在我们面前的是浩如烟海的单词。我们的首要任务是把所有单词转换成数字,这样才能对它们进行数学运算。
假设我们的目标是打造一台能响应语音指令的计算机。我们的工作就是构建一个变换器(transformer),将一系列声音转换为一系列单词。
我们首先要选定词汇表,也就是在每个序列中会用到的符号集合。就我们的情况而言,会有两组不同的符号:一组用于输入序列,代表语音声音;另一组用于输出序列,代表单词。
目前,我们假设处理的是英语。英语中有成千上万的单词,可能还有几千个计算机专用术语。这意味着我们的词汇表规模将近十万。一种把单词转换为数字的方法是从1开始计数,给每个单词分配一个独一无二的数字。这样,一个单词序列就可以表示为一个数字列表。
例如,假设有一种小语言,其词汇表只有三个单词:files(文件)、find(查找)和my(我的)。每个单词都可以用一个数字来替换,比如files = 1,find = 2,my = 3。那么,“Find my files”(查找我的文件)这句话,其单词序列是[find, my, files],就可以用数字序列[2, 3, 1]来表示。
这是一种完全可行的将符号转换为数字的方法,但实际上,还有一种格式对计算机来说更易于处理,那就是独热编码。在独热编码中,一个符号由一个大多为零的数组表示,数组长度与词汇表大小相同,只有一个元素的值为1。数组中的每个元素都对应一个不同的符号。
另一种理解独热编码的方式是,每个单词仍然被分配一个属于自己的数字,但此时这个数字是一个数组的索引。下面是上述示例的独热编码表示。
因此,“Find my files”这句话就变成了一个一维数组序列,当你把它们拼接在一起后,就开始像一个二维数组了。
注意,我会交替使用“一维数组”和“向量”这两个术语。同样,“二维数组”和“矩阵”这两个术语也会交替使用。
2.点积
独热表示法的一个非常有用的地方在于,它让我们能够计算点积。点积还有其他一些听起来很专业的名称,比如内积和标量积。计算两个向量的点积,就是将它们对应的元素相乘,然后把结果相加。
在处理独热编码的单词表示时,点积尤其有用。任何一个独热向量与自身的点积为1。
而任何一个独热向量与其他独热向量的点积为0。
前面两个例子展示了点积如何用于衡量相似度。再举个例子,假设有一个值向量,它代表了不同权重的单词组合。通过将一个独热编码的单词与这个向量做点积,就能显示出该单词在这个组合中的体现程度。
3.矩阵乘法
点积是矩阵乘法的基础,矩阵乘法是一种将两个二维数组组合在一起的特殊方式。我们把其中第一个矩阵称为A,第二个矩阵称为B。在最简单的情况下,当A只有一行,B只有一列时,矩阵乘法的结果就是这两个数组的点积。
注意,A的列数和B的行数必须相同,这样两个数组才能匹配,点积运算才能进行。
当A和B的规模变大时,矩阵乘法就变得复杂起来。对于A中有多行的情况,需要分别计算B与A中每一行的点积。最终结果的行数与A的行数相同。
当B有更多列时,要计算A与B中每一列的点积,并将结果按列依次排列。
现在,我们可以将这个方法扩展到任意两个矩阵相乘,只要A的列数与B的行数相同就行。相乘结果的行数与A相同,列数与B相同。
如果你是第一次接触这些内容,可能会觉得过于复杂,但我保证,这些知识在后面会发挥作用。
4. 矩阵乘法作为查找表
注意这里矩阵乘法是如何起到查找表的作用的。我们的A矩阵由一堆独热向量堆叠而成。这些独热向量分别在第一列、第四列和第三列的值为1。当我们进行矩阵乘法运算时,这会按顺序提取出B矩阵的第一行、第四行和第三行。利用独热向量提取矩阵特定行的技巧,是Transformer工作原理的核心。
5. 一阶序列模型
我们可以暂时先把矩阵放在一边,回到我们真正关心的内容——单词序列。假设在开发自然语言计算机接口时,我们只需要处理三种可能的指令:
请给我展示我的目录。
请给我展示我的文件。
请给我展示我的照片。
此时我们的词汇表大小为7,包含:{directories(目录), files(文件), me(我), my(我的), photos(照片), please(请), show(展示)}。
一种表示序列的有效方法是使用转移模型。对于词汇表中的每个单词,它能显示出下一个单词可能是什么。如果用户有一半的概率询问照片,30%的概率询问文件,剩下的概率询问目录,那么转移模型就会是下面这样。从任何一个单词出发的转移概率之和始终为1。
这种特殊的转移模型被称为马尔可夫链,因为它满足马尔可夫性质,即下一个单词的概率只取决于最近的单词。更具体地说,这是一个一阶马尔可夫模型,因为它只考虑最近的一个单词。如果它考虑最近的两个单词,那它就是二阶马尔可夫模型。
注:语言模型中的典型表现
一阶模型:当处理句子"the cat sat on the"时:预测下一个词时只考虑"the"(可能出现重复问题)。
二阶模型:处理相同句子时:会考虑"on the"组合。
我们对矩阵内容的暂时搁置到此结束。事实证明,马尔可夫链可以很方便地用矩阵形式来表示。使用与创建独热向量时相同的索引方案,矩阵的每一行代表词汇表中的一个单词,每一列也是如此。矩阵转移模型将矩阵当作查找表来使用。找到与你感兴趣的单词对应的行,每一列的值表示下一个出现该单词的概率。由于矩阵中每个元素的值都代表一个概率,所以它们都介于0和1之间。因为概率总和始终为1,所以每行的值相加也总是等于1 。
从这个转移矩阵中,我们可以清晰地看到我们那三个句子的结构。几乎所有的转移概率不是零就是一。在马尔可夫链中,只有一处会出现分支情况。在“my”之后,可能会出现“directories”(目录)、“files”(文件)或“photos”(照片),且每个词出现的概率各不相同。除此之外,下一个出现的词没有任何不确定性。这种确定性体现在转移矩阵中大多是零和一。
我们可以回顾一下之前用独热向量进行矩阵乘法来提取与任意给定单词相关的转移概率的技巧。例如,如果我们只想找出“my”之后出现的单词的概率,我们可以创建一个代表单词“my”的独热向量,然后将其与转移矩阵相乘。这样就能提取出相关的行,从而展示出下一个单词的概率分布情况。
6. 二阶序列模型
仅依据当前单词来预测下一个单词是很困难的。这就好比只听了乐曲的第一个音符,就要预测接下来的旋律。但如果我们至少能依据两个音符来推测,那么准确率就会高很多。
我们可以通过另一个用于计算机指令的简单语言模型来了解这种情况。假设这个模型只处理两个句子,出现比例为40%和60%。
- 请检查电池是否没电了。
- 请检查程序是否运行了。
Check whether the battery ran down please.
Check whether the program ran please.
马尔可夫链可以用来展示这种一阶模型。
在这里我们可以看到,如果我们的模型查看最近的两个单词,而不只是一个,它就能做得更好。当模型遇到“battery ran”时,它就知道下一个单词会是“down”;当看到“program ran”时,下一个单词就会是“please”。这消除了模型中的一个分支,减少了不确定性,增加了预测的可信度。通过回顾两个单词,这就变成了一个二阶马尔可夫模型。它为预测下一个单词提供了更多的上下文信息。二阶马尔可夫链更难绘制,但下面这些关联展示了它的价值。
为了突出两者的差异,下面是一阶转移矩阵:
下面是二阶转移矩阵:
注意,二阶矩阵为每一个单词组合都单独设置了一行(这里大部分没有展示出来)。这意味着,如果我们的词汇表大小为N,那么转移矩阵就会有N²行。
这样做让我们的预测更有把握。二阶模型中1的数量更多,分数形式的概率值更少。只有一行存在分数形式的概率值,也就是模型中的一个分支。直观来讲,看两个单词而不是一个,能提供更多上下文信息,为猜测下一个单词提供更多依据。
7. 带跳跃的二阶序列模型
当我们只需回顾两个单词就能决定下一个单词时,二阶模型效果很好。但如果需要回顾更前面的单词呢?假设我们正在构建另一个语言模型,这个模型只需处理两个出现概率相同的句子:
- 请检查程序日志,看看程序是否运行了。
- 请检查电池日志,看看电池是否没电了。
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个单词。如果想改进我们的二阶语言模型,我们当然可以考虑三阶及更高阶的模型。然而,对于较大的词汇表来说,实现这些模型既需要创造力,又需要大量的计算资源。一个简单的八阶模型实现就会有N^8行,对于任何合理规模的词汇表而言,这都是一个庞大到离谱的数字。
相反,我们可以采用一种巧妙的方法,仍然构建二阶模型,但要考虑最近的单词与之前每个单词的组合。它仍然是二阶模型,因为我们一次只考虑两个单词,但这种方法能让我们回顾更前面的内容,捕捉长距离的依赖关系。这种带跳跃的二阶模型与完整的高阶模型的区别在于,我们舍弃了大部分单词顺序信息和之前单词的组合。即便如此,剩下的部分仍然很强大。
现在马尔可夫链完全不适用了,但我们仍然可以表示每一对前面的单词和后面跟随单词之间的联系。这里我们不再使用数值权重,而是只用箭头表示非零权重,权重越大,箭头越粗。
这是它在转移矩阵中的样子。
这个视图只展示了与预测“ran”后面的单词相关的行。它展示了最近的单词(ran)前面分别是词汇表中其他每个单词的情况。这里只显示了相关的值,所有空白单元格的值都是零。
首先明显可以看出的是,在预测“ran”后面的单词时,我们不再只看某一行,而是要看一整组行。我们现在已经超出了马尔可夫模型的范畴。每一行不再代表序列在某一特定点的状态,相反,每一行代表了众多可能描述序列在某一特定点的特征之一。最近的单词与之前每个单词的组合形成了一组适用的行,这组行可能数量众多。由于含义的这种变化,矩阵中的每个值不再代表概率,而是代表一种“投票”。我们将对这些投票进行求和并比较,以此来确定对下一个单词的预测。
接下来明显可以看出的是,大多数特征并不重要。大多数单词在两个句子中都出现了,所以看到这些单词对于预测下一个单词没有帮助。它们的值都是0.5。仅有的两个例外是“battery”和“program”。在我们试图区分的两种情况中,它们分别与1和0的权重相关联。“battery, ran”这个特征表明“ran”是最近的单词,并且“battery”在句子前面的某个地方出现过。这个特征与“down”相关联的权重是1,与“please”相关联的权重是0。同样,“program, ran”这个特征的权重则正好相反。这种结构表明,句子中前面出现的这两个单词对于预测下一个单词起着决定性作用。
为了将这组单词对特征转换为对下一个单词的估计,需要对所有相关行的值进行求和。逐列相加后,“Check the program log and find out whether it ran”这个序列,除了“down”得4分、“please”得5分之外,其他所有单词的得分都是0。“Check the battery log and find out whether it ran”这个序列也是如此,只是“down”得5分,“please”得4分。通过选择总票数最高的单词作为下一个单词的预测,尽管存在8个单词的深度依赖,这个模型还是能得出正确答案。
8. 掩码
经过更仔细的思考,我们会发现这并不令人满意。4和5的总票数差异相对较小,这表明模型的预测信心不足。在一个更大、更自然的语言模型中,很容易想象这种细微的差异会被统计噪声所掩盖。
我们可以通过剔除所有无信息价值的特征投票来提高预测的准确性。这里要剔除的是除了“battery, ran”和“program, ran”之外的其他特征。此时要记住,我们通过将转移矩阵与一个表示当前活跃特征的向量相乘,来提取转移矩阵中的相关行。到目前为止,在这个例子中,我们一直在使用这里给出的隐含特征向量。
该向量中,每个特征对应“ran”与它前面每个单词的组合,取值为1。而“ran”后面出现的单词不会包含在特征集中(在预测下一个单词的问题中,这些单词尚未出现,所以用它们来预测下一个单词是不合理的)。并且,该向量并不包含所有其他可能的单词组合。在这个例子中,我们可以放心地忽略这些组合,因为它们的值都为零。
为了改进结果,我们可以创建一个掩码,将无用的特征强制归零。掩码是一个向量,除了你想要隐藏或屏蔽的位置设为0,其余位置均为1。在我们的例子中,我们希望屏蔽除“battery, ran”和“program, ran”之外的所有特征,因为只有这两个特征是有用的。
应用掩码时,我们将两个向量对应元素相乘。未被掩码的位置上的特征活动值会乘以1,保持不变。而被掩码位置上的特征活动值会乘以0,从而被强制归零。
掩码起到了隐藏转移矩阵中很多内容的作用。它隐藏了“ran”与除“battery”和“program”之外所有单词的组合,只留下了关键的特征。
屏蔽掉无用特征后,对下一个单词的预测变得更加准确。当“battery”在句子中较早出现时,“ran”后面的单词被预测为“down”的权重为1,预测为“please”的权重为0。原本25%的权重差异变成了无穷大的差异。下一个单词是什么一目了然。当“program”在句子中较早出现时,对“ran”后面单词是“please”的预测也同样明确。
这种有选择的掩码过程就是原始Transformer论文标题中提到的注意力机制。到目前为止,我们所描述的只是论文中注意力机制实现方式的一个近似版本。它抓住了重要的概念,但具体细节有所不同。我们稍后会弥补这一差距。
9.知识驿站与分支指引
恭喜你读到这里。如果你想停下,也没问题。带跳跃的选择性二阶模型是理解Transformer工作原理的一个有效视角,至少从解码器的角度来看是这样。它大致描述了像OpenAI的GPT-3这样的生成式语言模型的运作方式。虽然它没有讲述全部内容,但抓住了核心要点。
接下来的部分将进一步阐述这种直观解释与Transformer实际实现之间的差异。这主要是基于三个实际考量。
1. 计算机非常擅长矩阵乘法运算。整个行业都围绕着制造专门用于快速进行矩阵乘法运算的计算机硬件展开。任何能够表示为矩阵乘法的计算,其效率都能得到极大提升。这就好比坐上了子弹头列车,只要你能利用好它,就能迅速抵达目的地。
2. 每一步计算都需要可微。到目前为止,我们一直在处理简单示例,可以自行设定所有的转移概率和掩码值这些模型参数。但在实际应用中,这些参数必须通过反向传播来学习,而反向传播依赖于每一步计算的可微性。这意味着,对于任何一个参数的微小变化,我们都能够计算出模型误差或损失的相应变化。
3. 梯度需要平滑且条件良好。所有参数的导数组合起来就是损失梯度。在实际操作中,要使反向传播正常工作,需要梯度是平滑的,即当你在任何方向上进行微小变动时,斜率不会快速变化。当梯度条件良好时,其表现也会更好,也就是说,梯度在各个方向上的大小不会有太大差异。如果你把损失函数想象成一片地形,大峡谷就属于条件不佳的地形,因为沿着谷底和沿着谷壁行进时,你所面临的斜率差异会很大。相比之下,经典Windows屏保中的起伏山丘则具有良好的梯度条件。
如果说构建神经网络的科学在于创造可微的基础模块,那么艺术就在于以一种使梯度变化不过快且在各个方向上大致相同的方式堆叠这些模块。
10. 注意力机制与矩阵乘法
通过统计训练中每个单词对/下一个单词转换出现的频率,很容易确定特征权重,但注意力掩码的确定并非如此简单。到目前为止,我们都是凭空设定掩码向量。Transformer如何找到相关的掩码至关重要。使用某种查找表是很自然的想法,但现在我们专注于将所有操作都表示为矩阵乘法。我们可以采用上述的查找方法,将每个单词的掩码向量堆叠成一个矩阵,然后利用最近单词的独热表示法来提取相关掩码。
在展示掩码向量集合的矩阵中,为了清晰起见,我们只展示了想要提取的那个掩码向量。
我们终于讲到了可以与原论文联系起来的内容。这种掩码查找操作在注意力公式中由这一项表示。
查询向量Q代表感兴趣的特征,矩阵K代表掩码集合。由于掩码是按列存储而非按行存储,所以在相乘之前,K需要进行转置(使用T运算符)。等我们把所有内容讲完后,会对这个公式进行一些重要修改,但在当前阶段,它体现了Transformer所使用的可微查找表的概念。
11. 作为矩阵乘法的二阶序列模型
到目前为止,我们一直含糊其辞的另一个步骤是转移矩阵的构建。我们已经清楚其中的逻辑,但还不清楚如何通过矩阵乘法来实现。
一旦我们得到注意力步骤的结果,即一个包含最近出现的单词以及在它之前的一小部分单词的向量,我们就需要将其转化为特征,其中每个特征都是一个单词对。注意力掩码为我们提供了所需的原始材料,但它并不能构建这些单词对特征。要做到这一点,我们可以使用单层全连接神经网络。
为了了解神经网络层如何创建这些单词对,我们将手工构建一个。它会人为地简洁且具有程式化特点,其权重与实际应用中的权重毫无相似之处,但它将展示神经网络如何具备构建这些双词对特征所需的表达能力。为了使其简洁明了,我们将只关注这个例子中的三个被关注的单词:“battery(电池)”、“program(程序)”、“ran(运行)” 。
在上面的层图中,我们可以看到权重是如何将每个单词的出现和未出现情况组合成一系列特征的。这也可以用矩阵形式来表示。
并且可以通过与一个表示到目前为止所见到的单词集合的向量进行矩阵乘法来计算。
“battery”和“ran”对应的元素为 1,“program(程序)”对应的元素为 0。偏差元素始终为 1,这是神经网络的一个特点。通过矩阵乘法运算,代表“battery,ran”的元素得到的值为 1,代表“program,ran”的元素得到的值为 -1。其他情况的结果类似。
计算这些单词组合特征的最后一步是应用修正线性单元(ReLU)非线性函数。这样做的效果是将任何负值都替换为零。这就处理好了这两个结果,使它们能够用 1 表示每个单词组合特征的存在,用 0 表示不存在。
在完成了这些操作之后,我们终于有了一种基于矩阵乘法来创建多词特征的方法。尽管我最初声称这些特征由最近的一个单词和一个更早的单词组成,但仔细研究这种方法后会发现,它也可以构建其他特征。当特征创建矩阵是通过学习得到的,而不是硬编码的时,就可以学习到其他结构。即使在这个简单的例子中,也没有什么能阻止创建像“battery,program,ran”这样的三词组合。如果这种组合出现得足够频繁,它很可能最终会被表示出来。虽然没有办法表明这些单词出现的顺序(至少目前还没有),但我们绝对可以利用它们的共现情况来进行预测。甚至还可以利用忽略最近单词的单词组合,比如“battery,program”。在实际应用中,可能会创建这些以及其他类型的特征,这也表明了我之前声称“Transformer 是一种带有跳跃的选择性二阶序列模型”时的那种过度简化。实际情况比那要更复杂一些,而现在你可以确切地看到复杂之处在哪里了。这不会是我们最后一次为了纳入更多细节而修正说法。
以这种形式,多词特征矩阵已经可以进行下一次矩阵乘法运算了,也就是我们上面开发的带有跳跃的二阶序列模型中的运算。总体而言,以下这一系列步骤:
1. 特征创建矩阵乘法,
2. ReLU非线性变换,以及
3. 转移矩阵乘法
就是在应用注意力机制之后要进行的前馈处理步骤。论文中的公式 2 以简洁的数学形式展示了这些步骤。
论文中的图 1 架构图将这些步骤整合在一起,称为前馈网络模块。
12.序列补全
到目前为止,我们只讨论了下一个单词预测。为了让我们的解码器生成一个长序列,还需要添加几个部分。首先是一个提示词,即一些示例文本,为 Transformer 提供一个起始点和上下文,以便在此基础上构建序列的其余部分。它会被输入到解码器中(也就是上面图中右侧标注为 “Outputs (shifted right,右移后的输出)” 的那一列)。选择一个能生成有趣序列的提示词本身就是一门艺术,这被称为提示工程。这也是人类调整自身行为以支持算法的一个很好的例子,而不是反过来。
一旦解码器有了一个部分序列来启动处理,它就会进行一次前向传播。最终结果是得到一组单词的预测概率分布,序列中的每个位置都对应一个概率分布。在每个位置上,该分布显示了词汇表中每个下一个单词的预测概率。我们并不关心序列中已确定单词的预测概率,因为它们已经确定了。我们真正关心的是提示词结束后的下一个单词的预测概率。有几种方法可以选择下一个单词,但最直接的方法叫做贪婪法,即选择概率最高的单词。
然后,新的下一个单词会被添加到序列中,替换解码器底部 “Outputs(输出)” 处的内容,然后重复这个过程,直到你不想再继续为止。
有一个部分我们还不准备详细描述,那就是另一种形式的掩码,它确保Transformer在进行预测时只看前面的内容,而不看后面的内容。这种掩码应用在标注为 “Masked Multi-Head Attention(掩码多头注意力)” 的模块中。等我们能更清楚地说明它是如何实现的时候,会再回过头来讨论这个问题。
13.嵌入
就我们目前所描述的情况而言,Transformer 的规模太大了。假设词汇表大小 N 为 50000,那么所有单词对与所有可能的下一个单词之间的转移矩阵将有 50000 列和 50000 的平方(25 亿)行,元素总数超过 100 万亿。即使对于现代硬件来说,这仍然是一个很大的挑战。
问题不只是矩阵的大小。为了构建一个稳定的转移语言模型,我们至少需要提供多次训练数据,以展示每一种可能的序列。这甚至会远远超出最庞大的训练数据集的容量。
幸运的是,对于这两个问题都有一个解决办法,那就是嵌入。
在一种语言的独热编码表示中,每个单词都有一个向量元素。对于大小为 N 的词汇表,该向量处于 N 维空间中。每个单词代表该空间中的一个点,沿着众多轴中的某一轴距离原点一个单位。我还没有找到一种很好的方法来绘制高维空间,但下面有一个粗略的表示。
在嵌入表示中,那些单词对应的点都被提取出来并重新排列(用线性代数的术语来说是“投影”)到一个低维空间中。例如,上面的图片展示了它们在二维空间中可能呈现的样子。现在,我们不再需要用N个数字来表示一个单词,而只需要两个数字。这两个数字就是新空间中每个点的(x,y)坐标。下面是我们这个简单示例的二维嵌入表示可能的样子,同时给出了几个单词的坐标。
一个好的嵌入会将意思相近的单词归为一组。使用嵌入的模型会在嵌入空间中学习模式。这意味着,模型对某个单词学到的任何知识会自动应用到与它紧邻的所有单词上。这样做还有一个额外的好处,就是减少了所需的训练数据量。每个示例所带来的少量学习成果可以应用到一整个单词“邻域”上。
在这个图示中,我试图通过将重要的组件类单词放在一个区域(“battery(电池)”、“log(日志)”、“program(程序)”),介词类单词放在另一个区域(“down(向下)”、“out(向外)”),以及将动词类单词放在中心附近(“check(检查)”、“find(找到)”、“ran(运行)”)来展示这一点。在实际的嵌入中,这些分组可能不会这么清晰或直观,但基本概念是相同的。行为相似的单词之间的距离会比较小。
嵌入极大地减少了所需的参数数量。然而,嵌入空间的维度越少,关于原始单词的信息被丢弃得就越多。一种语言的丰富性仍然需要相当大的空间来展示所有重要概念,这样它们才不会相互干扰。通过选择嵌入空间的大小,我们可以在计算负载和模型准确性之间进行权衡。
当得知将单词从独热编码表示投影到嵌入空间涉及矩阵乘法时,你可能并不会感到惊讶。投影正是矩阵最擅长的事情。从一个有一行N列的独热矩阵开始,要转换到一个二维的嵌入空间,投影矩阵将有N行和两列,如下所示。
这个例子展示了一个独热向量(例如表示“battery”的独热向量)是如何提取出与之相关的那一行的,这一行包含了该单词在嵌入空间中的坐标。为了使这种关系更清晰,独热向量中的零被隐藏了,投影矩阵中未被提取出来的所有其他行也被隐藏了。完整的投影矩阵是稠密的,每一行都包含与之相关的单词的坐标。
投影矩阵可以将原始的独热词汇向量集合转换为你想要的任何维度空间中的任何配置。最大的诀窍在于找到一个有用的投影方式,既能将相似的单词归为一组,又有足够的维度来将它们分开。对于一些常见的语言,比如英语,已经有一些不错的预计算好的嵌入表示。而且,和Transformer中的其他部分一样,嵌入也可以在训练过程中学习得到。
在原论文的图1架构图中,嵌入就是在这里发生的。
14.位置编码
此前,我们一直没有考虑单词的位置,特别是对于除最新单词外的其他所有单词。现在,我们将利用位置嵌入来处理这一问题。将位置信息融入单词嵌入表示有多种方法,而最初的Transformer模型采用的是添加周期性波动模式的方式。
在嵌入空间中,单词的位置充当一个圆的圆心。根据该单词在单词序列中的位置顺序,会向其添加一个扰动。对于每个位置,单词移动的距离相同,但角度不同,因此当你遍历整个序列时,就会形成一种圆形模式。在序列中位置相近的单词具有相似的扰动,而位置相距较远的单词则会在不同方向上受到扰动。
由于圆是一个二维图形,要表示这种周期性的波动模式就需要对嵌入空间的两个维度进行修改。如果嵌入空间的维度超过两个(实际情况几乎总是如此),那么这种周期性波动会在其他所有的维度对中重复出现,但角频率不同,也就是说,在每种情况下旋转的圈数是不同的。在某些维度对中,这种波动可能会使圆旋转很多圈。而在其他维度对中,它可能只旋转很小的一部分。所有这些不同频率的周期性波动的组合,能够很好地表示一个单词在序列中的绝对位置。
我仍在培养对其工作原理的直观理解。它似乎是以一种不会破坏单词之间已学习到的关系以及注意力机制的方式,将位置信息融入其中。如果你想深入研究其中的数学原理和意义,我推荐阿米尔侯赛因·卡泽姆内贾德(Amirhossein Kazemnejad)的位置编码教程。
https://kazemnejad.com/blog/transformer_architecture_positional_encoding/
在标准的架构图中,这些模块展示了位置编码的生成过程以及它与嵌入单词的相加操作。
15. 反嵌入
对单词进行嵌入处理能让它们在运算时效率大大提高,但一旦运算结束,就需要将它们从嵌入形式转换回原始词汇表中的单词。反嵌入的实现方式与嵌入相同,都是从一个空间投影到另一个空间,也就是进行矩阵乘法运算。
反嵌入矩阵的形状与嵌入矩阵相反,行和列的数量是颠倒的。行数是我们要转换的源空间的维度。在我们一直使用的例子中,它就是嵌入空间的大小,即二维。列数是我们要转换到的目标空间的维度——也就是完整词汇表的独热编码表示的维度,在我们的例子中是 13 维。
一个好的反嵌入矩阵中的值不像嵌入矩阵中的值那样容易解释,但效果是类似的。例如,当一个表示单词 “program(程序)” 的嵌入向量与反嵌入矩阵相乘时,对应位置的值会很高。然而,由于向高维空间投影的原理,与其他单词相关的值不会为零。在嵌入空间中与 “program” 最接近的单词也会有中等偏高的值。其他单词的值则接近零。而且很可能会有很多单词对应的值为负。词汇空间中的输出向量将不再是独热编码或稀疏的,而是稠密的,几乎所有的值都不为零。
这没关系。我们可以通过选择与最高值相关联的单词来重新创建独热向量。这个操作也被称为 “argmax”,即给出最大值的参数(元素)。这就是上面提到的贪婪法进行序列补全的方式。这是一个很好的初步方法,但我们还可以做得更好。
如果一个嵌入与多个单词都映射得很好,我们可能不想每次都选择得分最高的那个单词。它可能只比其他单词好那么一点点,而增加一点多样性可以使结果更有趣。此外,有时在确定最终选择之前,先向前看几个单词并考虑句子可能的所有走向是很有用的。为了做到这些,我们首先必须将反嵌入的结果转换为概率分布。
16.softmax函数(归一化指数函数)
argmax函数是一种“硬”选择函数,因为在这种函数下,即使某个值只比其他值大了极其微小的一点,它也会成为最终的选择结果。如果我们想要同时考虑多种可能性,那么使用一个“软”的求最大值函数会更好,这就是我们从softmax函数中所获得的特性。要计算一个向量中值x的softmax值,需将x的指数e^x除以该向量中所有值的指数之和。
softmax函数在这里有三个好处。首先,它将我们的反嵌入结果向量从一组任意的值转换为一个概率分布。作为概率,这样就更容易比较不同单词被选中的可能性,而且如果我们想进一步预测未来的多个单词序列,甚至还可以比较多词序列出现的可能性。
其次,它会突出高分区域。如果一个单词的得分明显高于其他单词,softmax函数会放大这种差异,使其看起来几乎就像argmax函数的结果一样,获胜的值接近1,而其他所有值都接近0。然而,如果有几个单词的得分都很接近最高分,它会将这些单词都保留为可能性较高的选项,而不会人为地淘汰那些紧随其后的次优结果。
第三,softmax函数是可微的,这意味着给定输入元素中的任何一个有微小变化时,我们都可以计算出结果中的每个元素会发生多大的变化。这使得我们可以将它与反向传播算法一起使用,来训练我们的Transformer模型。
如果你想深入理解softmax函数(或者如果你晚上难以入睡),这里有一篇关于它的更完整的文章。
反嵌入变换(如下图中显示为“Linear”模块)和softmax函数共同完成了反嵌入的过程。
17. 多头注意力机制
既然我们已经理解了投影(矩阵乘法)和空间(向量维度)的概念,那么我们就可以更有活力地重新审视核心的注意力机制了。如果我们能更具体地说明每个阶段中矩阵的形状,这将有助于我们更清晰地理解这个算法。这里有一些重要的参数数值。
lN:词汇表大小。在我们的例子中是13。一般来说会是几万的数量级。
ln:最大序列长度。在我们的例子中是12。在相关论文里大概是几百左右(他们没有明确说明)。在GPT-3中是2048。
ld_{model}:整个模型中使用的嵌入空间的维度数量。在论文中是512。
原始的输入矩阵是通过获取句子中每个单词的独热编码表示,然后将它们堆叠起来构建的,这样每个独热向量就成为矩阵的一行。最终得到的输入矩阵有n行和N列,我们可以简记为[n x N]。
正如我们之前所阐述的,嵌入矩阵有N行和d_{model}列,我们可以简记为[N \times d_{model}]。在进行两个矩阵相乘时,结果矩阵的行数取自第一个矩阵,列数取自第二个矩阵。这样一来,嵌入后的单词序列矩阵的形状就变为[n \times d_{model}]。
我们可以通过跟踪Transformer中矩阵形状的变化,来了解整个过程中发生了什么。在初始的嵌入操作之后,位置编码是通过相加的方式进行的,而不是矩阵乘法,所以它不会改变矩阵的形状。然后,嵌入后的单词序列进入注意力层,从另一端输出时矩阵形状保持不变。(我们稍后会再深入探讨这些内部的工作机制。)最后,反嵌入操作将矩阵恢复到其原始形状,为序列中每个位置上的词汇表中的每个单词都提供一个概率。
18. 为什么我们需要不止一个注意力头
终于到了要面对我在最初解释注意力机制时所做的一些过于简化的假设的时候了。单词现在是用稠密的嵌入向量来表示,而不是独热向量。注意力不仅仅是1或0,即开启或关闭两种状态,它还可以处于两者之间的任何值。为了使结果落在0到1之间,我们再次使用softmax技巧。它有两个好处,一是迫使所有的值都处于我们的[0, 1]注意力范围内,二是有助于突出最大值,同时大幅压缩最小值。这就是我们之前在解读模型最终输出时所利用的近似于argmax的可微特性。
在注意力机制中加入softmax函数会带来一个复杂的结果,那就是它往往会聚焦于单个元素。这是我们之前没有遇到过的一个限制。有时候,在预测下一个单词时,同时记住前面的几个单词是很有用的,而softmax函数却让我们失去了这个能力。这对模型来说是个问题。
解决办法是同时运行多个不同的注意力实例,也就是多个注意力头。这使得Transformer在预测下一个单词时能够同时考虑前面的几个单词。这让我们重新获得了在引入softmax函数之前所拥有的能力。
不幸的是,这样做确实会大幅增加计算量。计算注意力已经是模型计算工作的主要部分了,而现在我们还要将其乘以我们想要使用的注意力头的数量。为了解决这个问题,我们可以再次使用将所有内容投影到低维嵌入空间的技巧。这会缩小相关矩阵的规模,从而极大地减少计算时间。问题就这样解决了。
为了了解这是如何实现的,我们可以继续研究矩阵的形状。要跟踪多头注意力模块中矩阵形状的变化,还需要另外三个参数数值:
- d_{k}:用于键(keys)和查询(queries)的嵌入空间的维度。在论文中是64。
- d_{v}:用于值(values)的嵌入空间的维度。在论文中是64。
- h:注意力头的数量。在论文中是8。
嵌入单词组成的[n x dmodel]序列是后续所有操作的基础。在每种情况下,都存在一个矩阵Wv、Wq和Wk(在架构图中都毫无助益地显示为“Linear(线性)”模块),它们将原始的嵌入单词序列分别转换为值矩阵V、查询矩阵Q和键矩阵K。K和Q具有相同的形状,即[n x dk],但V的形状可能不同,为[n x dv]。在论文中dk和dv的值是相同的,这可能会让人有点困惑,但实际上它们并非必须相等。这种设置的一个重要方面是,每个注意力头都有自己的Wv、Wq和Wk变换矩阵。这意味着每个注意力头都可以放大或缩小嵌入空间中它想要关注的部分,而且每个注意力头所关注的部分可以与其他注意力头不同。
每个注意力头的输出结果都与V具有相同的形状。现在我们面临的问题是有h个不同的结果向量,每个向量都关注序列中的不同元素。为了将它们合并为一个向量,我们利用线性代数的方法,将所有这些结果连接成一个巨大的[n x h x dv]矩阵。然后,为了确保最终结果的形状与初始时相同,我们再使用一个形状为[h x dv x dmodel]的变换矩阵进行变换。
下面简洁地阐述一下上述所有内容。
19. 重新审视单头注意力机制
我们之前已经讲解过注意力机制的概念性示例。实际的实现方式会稍微复杂一些,但我们之前建立的直观理解仍然很有帮助。现在,查询(queries)和键(keys)不再那么容易检查和解释了,因为它们都被投影到了各自独特的子空间中。在我们的概念性示例中,查询矩阵的一行代表词汇空间中的一个点,由于采用独热编码表示,这个点只代表一个单词。而在嵌入形式下,查询矩阵的一行代表嵌入空间中的一个点,这个点会靠近一组含义和用法相似的单词。概念性示例将一个查询单词映射到一组键,这些键又会过滤掉所有未被关注的值。在实际实现中,每个注意力头会将一个查询单词映射到另一个更低维度的嵌入空间中的一个点。这样做的结果是,注意力变成了单词组之间的关系,而不是单个单词之间的关系。它利用语义上的相似性(在嵌入空间中的接近程度)来推广从相似单词中学到的知识。
通过跟踪注意力计算过程中矩阵的形状变化,有助于了解其具体的操作过程。
查询矩阵Q和键矩阵K的形状均为[n x dk]。由于在乘法运算之前对K进行了转置,所以\QK^T的结果得到一个[n x dk] x [dk x n ] = [n x n]的矩阵。研究表明,将这个矩阵的每个元素都除以dk的平方根,可以防止值的数量级无节制地增长,并且有助于反向传播算法更好地运行。正如我们提到的,softmax 函数会将结果近似为一个类似于 argmax 的形式,倾向于将注意力更多地集中在序列中的一个元素上。以这种形式,[n x n]的注意力矩阵大致将序列中的每个元素映射到序列中的另一个元素,表明为了获得预测下一个元素的最相关上下文,该元素应该关注什么。它就像一个过滤器,最终应用于值矩阵V,只留下被关注的值的集合。这就产生了忽略序列中前面大部分内容的效果,而将重点聚焦在对理解最有用的前一个元素上。
理解这一系列计算的一个棘手之处在于,要时刻记住它是在为输入序列中的每一个元素、句子中的每一个单词计算注意力,而不只是为最新出现的那个单词计算。它也在为前面的单词计算注意力。我们其实并不太在意这些计算结果,因为这些前面单词的下一个单词已经被预测出来并且确定了。它同样也在为后面尚未出现的单词计算注意力。不过,这些针对未来单词的注意力计算目前还没有太大作用,因为它们距离当前的计算还太遥远,而且它们紧邻的前一个单词都还没有被选定。但是,通过一些间接的途径,这些计算结果能够影响到对最新单词的注意力计算,所以我们把它们都包含在内。只是当我们最终完成计算并得出序列中每个位置的单词概率时,会舍弃掉大部分结果,只关注下一个单词的概率。
掩码(Mask)模块施加了这样一个限制:至少在这个序列补全任务中,我们不能“窥探”到未来的信息。它避免了引入来自想象中未来单词的任何奇怪干扰因素。这个方法虽然简单直接,但却很有效——手动将当前位置之后所有单词的注意力权重都设置为负无穷。在《带注释的Transformer》(这是一篇对原论文极有帮助的文章,逐行展示了Python实现)中,对掩码矩阵进行了可视化呈现。紫色的单元格表示不允许计算注意力的位置。每一行对应着序列中的一个元素。第一行只允许关注自身(即第一个元素),而不能关注后面的任何元素。最后一行则允许关注自身(即最后一个元素)以及它前面的所有元素。掩码是一个维度为[n x n]的矩阵。它的应用不是通过矩阵乘法,而是通过更简单直接的元素对元素的乘法操作。这样做就相当于手动进入注意力矩阵,然后将掩码中所有紫色单元格对应的元素都设置为负无穷。
注意力机制的实现方式中另一个重要的不同点在于,它利用了单词在序列中出现的顺序,并且将注意力表示为位置与位置之间的关系,而不是单词与单词之间的关系。这在其[n x n]的矩阵形状中体现得很明显。它将序列中由行索引表示的每个元素,映射到序列中由列索引表示的其他某个或某些元素上。这有助于我们更轻松地可视化和理解它的作用,因为它是在嵌入空间中进行操作的。我们无需再额外去寻找嵌入空间中相近的单词来表示查询和键之间的关系。
20.残差连接(跳跃连接)
注意力机制是Transformer模型运作中最基础的部分。它是核心机制,而我们现在已经在相当具体的层面上研究过它了。从这里开始往后的所有内容,都是为了让注意力机制能够良好运行所必需的“辅助设施”。正是这些其余的部分,使得注意力机制能够承担起我们繁重的计算任务。
我们还没有解释过的一个部分是残差连接。这些连接出现在多头注意力模块周围,以及标记为“Add and Norm(相加并归一化)”的模块中按元素的前馈网络模块周围。在残差连接中,会将输入的一个副本添加到一组计算的输出中。注意力模块的输入会被加回到其输出中。按元素的前馈网络模块的输入也会被添加到其输出中。
残差连接有两个作用。
第一个作用是,它们有助于保持梯度的平滑,这对反向传播算法有很大的帮助。注意力机制就像一个过滤器,这意味着当它正常工作时,会阻挡大部分试图通过它的信息。这样一来,如果许多输入中的微小变化恰好落入被阻挡的通道中,那么这些变化可能不会在输出中产生太大的改变。这就会在梯度中产生一些平坦的“死区”,但这些区域仍然离山谷底部很远。这些鞍点和山脊对于反向传播来说是很大的阻碍。残差连接有助于消除这些问题。就注意力机制而言,即使所有权重都为零,并且所有输入都被阻挡,残差连接也会将输入的副本添加到结果中,从而确保任何输入的微小变化仍会在结果中产生明显的变化。这能防止梯度下降在远离最优解的地方陷入停滞。
自从残差网络(ResNet)图像分类器出现以来,残差连接因其对性能的提升效果而广受欢迎。如今,它们已成为神经网络架构中的一个标准特性。从直观上看,我们可以通过比较有残差连接和没有残差连接的网络,来观察残差连接所产生的影响。下面这篇论文中的图展示了有残差连接和没有残差连接的残差网络。当使用残差连接时,损失函数曲线的斜率更加平缓且均匀。如果你想更深入地了解残差连接的工作原理以及原因,这篇文章中有更详细的论述。
残差连接的第二个作用是Transformer模型所特有的——保留原始输入序列。即使有很多个注意力头,也不能保证一个单词会关注其自身所在的位置。注意力过滤器有可能会完全忽略掉最新出现的单词,而更倾向于关注所有可能相关的前面的单词。残差连接会获取原始单词,并手动将其添加回信号中,这样一来,这个单词就不可能被丢弃或遗忘。这种稳定性或许是Transformer模型能够在众多不同的序列补全任务中表现出色的原因之一。
21.层归一化
归一化是与残差连接搭配得很好的一个步骤。它们并非一定要同时使用,但当把它们放置在一组计算(比如注意力机制或前馈神经网络)之后时,二者都能发挥出最佳效果。
简单来说,层归一化就是将矩阵中的值进行平移,使其均值为零,并进行缩放,使其标准差为一。
详细来说,在像Transformer这样的系统中,有很多不同的组成部分,其中一些操作并非矩阵乘法(比如softmax操作符或修正线性单元),此时值的大小以及它们在正负值之间的平衡情况就很重要。如果所有操作都是线性的,你可以将所有输入都翻倍,那么输出也会变为原来的两倍,并且一切都能正常运行。但神经网络并非如此。神经网络本质上是非线性的,这使得它们具有很强的表达能力,但同时也对信号的量级和分布很敏感。归一化是一种已被证明很有用的技术,它可以在多层神经网络的每一步中保持信号值的分布一致。它有助于参数值的收敛,并且通常会带来更好的性能表现。
关于归一化,我最喜欢的一点是,除了像我刚才给出的那种高层次的解释之外,没有人能完全确定它为什么会如此有效。如果你想更深入地探究这个问题,我曾写过一篇关于批量归一化的更详细的文章,批量归一化是Transformer中使用的层归一化的近亲。
https://e2eml.school/batch_normalization
22.多层结构
在我们前面奠定基础的时候,我们展示了一个注意力模块和一个经过精心选择权重的前馈模块足以构建一个不错的语言模型。在我们的例子中,大多数权重都是零,有一些是一,而且它们都是手动挑选的。当从原始数据进行训练时,我们就没有这样的便利了。一开始,所有权重都是随机选择的,大多数权重都接近于零,而那些不为零的权重很可能也不是我们所需要的。要让我们的模型表现良好,还有很长的路要走。
通过反向传播进行的随机梯度下降可以实现一些相当惊人的效果,但它在很大程度上依赖于运气。如果只有一种方法能得到正确答案,即只有一种权重组合能让网络良好运行,那么模型不太可能找到正确的方向。但如果有很多条路径都能通向一个好的解决方案,那么模型找到这个方案的机会就大得多。
仅有一个注意力层(即只有一个多头注意力模块和一个前馈模块)只允许有一种路径来找到一组好的Transformer参数。每个矩阵的每个元素都需要找到合适的值,才能使模型正常运行。这样的模型是脆弱的,很可能会陷入一个远非理想的解决方案中,除非对参数的初始猜测非常非常幸运。
Transformer解决这个问题的方法是使用多个注意力层,每个注意力层都将前一个注意力层的输出作为自己的输入。残差连接的使用使得整个流程对于单个注意力模块出现故障或给出异常结果的情况具有很强的鲁棒性。有多个注意力层意味着如果一个出现问题,还有其他层可以弥补不足。如果一个注意力层偏离了正轨,或者以任何方式未能发挥其潜力,那么在下游还有另一个注意力层有机会缩小差距或修正错误。论文表明,更多的层会带来更好的性能,尽管在达到6层之后,性能提升的幅度就变得很小了。
另一种理解多层结构的方式是将其看作是一条传送带式的装配线。每个注意力模块和前馈模块都有机会从传送带上获取输入,计算出有用的注意力矩阵并预测下一个单词。无论它们产生的结果是否有用,都会被重新添加到传送带上,并传递到下一层。
这与传统上对多层神经网络“深度”的描述形成了鲜明对比。由于有了残差连接,后续的层所提供的与其说是日益复杂的抽象,不如说是冗余信息。如果某一层错过了聚焦注意力、创建有用特征以及做出准确预测的机会,下一层总是能够弥补。这些层就像是装配线上的工人,每个工人都尽其所能,但不用担心会遗漏每一个细节,因为下一个工人会处理他们遗漏的部分。
23. 解码器堆叠
到目前为止,我们一直刻意忽略编码器堆叠部分(Transformer架构的左侧),而更关注解码器堆叠部分(Transformer架构的右侧)。我们会在接下来的几段内容中解决这个问题。但值得注意的是,仅解码器本身就非常有用。
正如我们在序列补全任务描述中所阐述的,解码器可以补全部分序列,并按照你的需求将其扩展到任意长度。OpenAI创建了生成式预训练(GPT)系列模型,就是为了实现这一点。他们在这份报告中描述的架构看起来应该很眼熟。它是一个Transformer模型,但经过“手术式”的处理,移除了编码器堆叠部分及其所有连接。剩下的是一个12层的解码器堆叠。
https://cdn.openai.com/research-covers/language-unsupervised/language_understanding_paper.pdf
每当你遇到像BERT、ELMo或Copilot这样的生成模型时,你很可能看到的是Transformer解码器部分在发挥作用。
https://arxiv.org/pdf/1810.04805v2
https://arxiv.org/abs/1802.05365
24. 编码器堆叠
我们所了解到的关于解码器的几乎所有内容也都适用于编码器。最大的区别在于,编码器在最后并不会做出明确的预测,因此我们无法据此判断其性能的好坏。相反,编码器堆叠的最终产物令人失望地抽象——它是嵌入空间中的一系列向量。有人将其描述为序列的一种纯粹语义表示,与任何特定的语言或词汇无关,但在我看来这有些过于理想化了。我们可以确定的是,对于向解码器堆叠传达意图和含义而言,它是一个有用的信号。
有了编码器堆叠,Transformer就充分发挥出了其潜力,不再仅仅是生成序列,现在它们还可以将序列从一种语言翻译(或转换)成另一种语言。在翻译任务上进行训练与在序列补全任务上进行训练是不同的。训练数据既需要源语言的一个序列,也需要目标语言中与之匹配的一个序列。完整的源语言序列会通过编码器(这次没有掩码,因为我们假定在进行翻译之前可以看到整个句子),最终编码器层的输出结果会作为输入提供给每个解码器层。然后,解码器中的序列生成过程和之前一样进行,但这次没有提示词来启动它。
25. 交叉注意力
让完整的Transformer模型运行起来的最后一步是编码器堆叠和解码器堆叠之间的连接,即交叉注意力模块。我们把它留到最后来讲,而且由于我们已经做了铺垫,剩下需要解释的内容也不多了。
交叉注意力的工作方式与自注意力类似,不同之处在于键矩阵K和值矩阵V是基于最终编码器层的输出,而不是前一个解码器层的输出。查询矩阵Q仍然是根据前一个解码器层的结果计算得出的。这是源序列中的信息进入目标序列并引导目标序列正确生成的通道。值得注意的是,相同的源序列嵌入表示会被提供给解码器的每一层,这支持了这样一种观点,即后续的层提供了冗余信息,并且都在协同完成同一项任务。
26.分词
我们终于完整地了解了Transformer模型!我们讲解得足够详细,应该不会再留下什么神秘的黑箱部分了。不过,还有一些实现细节我们没有深入探讨。如果你想自己构建一个能够运行的版本,就需要了解这些细节。这最后几点内容与其说是关于Transformer如何工作的,不如说是关于如何让神经网络良好运行的。《带注释的Transformer》会帮助你填补这些空白。
https://nlp.seas.harvard.edu/2018/04/03/attention.html
不过,我们还没有完全结束。关于我们最初如何表示数据,仍然有一些重要的内容需要说明。这是一个我非常关注但又容易被忽视的话题。这与其说是关于算法的强大能力,不如说是关于如何周全地解释数据并理解其含义。
我们顺便提到过,词汇表可以用高维的独热向量来表示,其中每个元素对应一个单词。为了做到这一点,我们需要确切地知道要表示多少个单词以及这些单词是什么。
一种简单的方法是列出所有可能的单词,就像我们在韦氏词典中可能找到的那样。对于英语来说,这会得到几万个单词,确切的数量取决于我们选择包含或排除哪些单词。但这是一种过于简单的做法。大多数单词有多种形式,包括复数形式、所有格形式和词形变化。单词可能有不同的拼写方式。而且,除非你的数据经过了非常仔细的清理,否则它会包含各种各样的拼写错误。这甚至还没有涉及到自由格式文本、新造词、俚语、行话以及庞大的Unicode字符集所带来的各种可能性。列出所有可能单词的详尽列表会长得让人无法处理。
一个合理的替代方案是让单个字符作为基本组成单元,而不是单词。列出所有字符的详尽列表完全在我们的计算能力范围内。然而,这样做存在几个问题。在我们将数据转换到嵌入空间后,我们假定该空间中的距离具有语义解释,也就是说,我们假定距离较近的点具有相似的含义,而距离较远的点含义则大不相同。这使我们能够隐含地将对一个单词的学习扩展到它附近的单词,这是我们为了提高计算效率所依赖的一个假设,并且Transformer也从中获得了一定的泛化能力。
在单个字符层面,语义内容非常少。例如,在英语中有一些单字符单词,但数量不多。表情符号是个例外,但它们并不是我们所处理的大多数数据集的主要内容。这让我们处于一个不幸的境地,即拥有一个不太有用的嵌入空间。
从理论上讲,如果我们能够观察到足够丰富的字符组合,以构建出像单词、词干或单词对这样具有语义意义的序列,也许仍然可以解决这个问题。不幸的是,Transformer内部创建的特征更像是一组输入对的集合,而不是有序的输入集合。这意味着一个单词的表示将是一组字符对,而它们的顺序并没有得到强烈体现。Transformer将不得不持续处理由相同字母组成但顺序不同的单词,这会使其工作难度大大增加。事实上,使用字符层面表示的实验表明,Transformer在处理这类表示时表现得不是很好。
27.字节对编码
幸运的是,有一个巧妙的解决办法,叫做字节对编码。从字符级别的表示开始,每个字符都被赋予一个代码,即它自己独特的字节。然后,在扫描了一些具有代表性的数据之后,最常见的字节对会被组合在一起,并被赋予一个新的字节,也就是一个新的代码。这个新代码会被替换回数据中,然后重复这个过程。
代表字符对的代码可以与代表其他字符或字符对的代码组合起来,从而得到代表更长字符序列的新代码。一个代码所能代表的字符序列的长度没有限制。只要有需要,为了表示常见的重复序列,它们的长度会不断增加。字节对编码的妙处在于,它从数据中推断出需要学习哪些长字符序列,而不是盲目地表示所有可能的序列。它学会了用单个字节代码来表示像“transformer”这样的长单词,但不会在类似长度的任意字符串(比如“ksowjmckder”)上浪费一个代码。而且,由于它保留了所有单个字符基本单元的字节代码,所以它仍然能够表示奇怪的拼写错误、新单词,甚至是外语。
当你使用字节对编码时,可以为其指定一个词汇表大小,它会不断生成新代码,直到达到该大小为止。词汇表大小需要足够大,这样字符字符串才能足够长,以捕捉文本的语义内容。它们必须有一定的意义。只有这样,它们才会足够丰富,能够为Transformer模型提供支持。
在训练好或借用了一个字节对编码器之后,我们可以在将数据输入Transformer模型之前,用它对数据进行预处理。这会将连续的文本流分割成一系列不同的块(其中大多数希望是可识别的单词),并为每个块提供一个简洁的代码。这个过程就叫做分词。
28.音频输入
现在回想一下,我们开启这整个探索之旅时的最初目标是将音频信号或语音指令转换为文本表示。到目前为止,我们所有的示例都是在假设我们处理的是书面语言的字符和单词的情况下进行的。我们也可以将其扩展到音频领域,但这需要在信号预处理方面进行更大胆的尝试。
音频信号中的信息需要经过一些复杂的预处理,才能提取出我们的耳朵和大脑用于理解语音的部分。这种方法叫做梅尔频率倒谱滤波(Mel-frequency cepstrum filtering),正如其名称所暗示的那样,它非常复杂。如果你想深入了解其中有趣的细节,这里有一篇配有丰富插图的教程。
当预处理完成后,原始音频会被转换为一系列向量,其中每个元素代表特定频率范围内音频活动的变化。这些向量是稠密的(没有元素为零),并且每个元素都是实数值。
从好的方面来看,每个向量对于Transformer模型来说都是一个很好的“单词”或标记,因为它具有一定的意义。它可以直接转换为一组可被识别为单词一部分的声音。
另一方面,将每个向量当作一个单词来处理有些奇怪,因为每个向量都是独一无二的。相同的一组向量值几乎不可能出现两次,因为声音的组合有太多微妙的差异。我们之前的独热编码表示和字节对编码策略在这里都派不上用场。
这里的诀窍是要注意到,像这样的稠密实数值向量正是我们在对单词进行嵌入处理后得到的结果。Transformer模型很适合处理这种格式的数据。为了利用这一点,我们可以像处理文本示例中嵌入后的单词那样,使用倒谱预处理的结果。这为我们省去了分词和嵌入的步骤。
值得注意的是,我们也可以对任何其他类型的数据进行这样的处理。许多记录的数据都是以稠密向量序列的形式存在的。我们可以直接将它们输入到Transformer模型的编码器中,就好像它们是嵌入后的单词一样。
29.总结
如果你还在跟随着我的讲解,谢谢你。我希望这一切都是值得的。我们的旅程到此结束。我们最初的目标是为我们想象中的语音控制计算机制作一个语音到文本的转换器。在这个过程中,我们从最基本的组成部分,即计数和算术开始,从头构建了一个Transformer模型。我希望下次你读到一篇关于最新的自然语言处理成果的文章时,你能够满意地点点头,因为你对其背后的原理已经有了相当不错的理解。
如何学习AI大模型?
我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。
我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将并将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。
第一阶段: 从大模型系统设计入手,讲解大模型的主要方法;
第二阶段: 在通过大模型提示词工程从Prompts角度入手更好发挥模型的作用;
第三阶段: 大模型平台应用开发借助阿里云PAI平台构建电商领域虚拟试衣系统;
第四阶段: 大模型知识库应用开发以LangChain框架为例,构建物流行业咨询智能问答系统;
第五阶段: 大模型微调开发借助以大健康、新零售、新媒体领域构建适合当前领域大模型;
第六阶段: 以SD多模态大模型为主,搭建了文生图小程序案例;
第七阶段: 以大模型平台应用与开发为主,通过星火大模型,文心大模型等成熟大模型构建大模型行业应用。
👉学会后的收获:👈
• 基于大模型全栈工程实现(前端、后端、产品经理、设计、数据分析等),通过这门课可获得不同能力;
• 能够利用大模型解决相关实际项目需求: 大数据时代,越来越多的企业和机构需要处理海量数据,利用大模型技术可以更好地处理这些数据,提高数据分析和决策的准确性。因此,掌握大模型应用开发技能,可以让程序员更好地应对实际项目需求;
• 基于大模型和企业数据AI应用开发,实现大模型理论、掌握GPU算力、硬件、LangChain开发框架和项目实战技能, 学会Fine-tuning垂直训练大模型(数据准备、数据蒸馏、大模型部署)一站式掌握;
• 能够完成时下热门大模型垂直领域模型训练能力,提高程序员的编码能力: 大模型应用开发需要掌握机器学习算法、深度学习框架等技术,这些技术的掌握可以提高程序员的编码能力和分析能力,让程序员更加熟练地编写高质量的代码。
1.AI大模型学习路线图
2.100套AI大模型商业化落地方案
3.100集大模型视频教程
4.200本大模型PDF书籍
5.LLM面试题合集
6.AI产品经理资源合集
👉获取方式:
😝有需要的小伙伴,可以保存图片到wx扫描二v码免费领取【保证100%免费】🆓