本文来自微信公众号“与数据同行”,作者:傅一平
OpenAI 最近开了自家的第一届开发者大会,发布的GPT-4 Turbo和GPTs等成果令人震撼。虽然国内各种大模型马不停蹄,但看了这个发布会,感觉跟ChatGPT的差距又变大了,ChatGPT是真的遥遥领先了。
对于许多人来说,理解其背后的核心技术——Transformer模型仍然是一个挑战。您是否曾经好奇,是什么使得GPT能够如此精准地生成文本、回答问题,甚至编写程序代码?
但Transformer这个东西很难说到底有没有一种简单、通俗地理解方式。我大概网上翻了一遍,几乎都是长篇大论,原因很简单,Transformer就不是简单几句话就能讲得清楚的。
放在之前,我个人的观点是要想系统而又透彻地理解 Transformer,至少要遵循下面这样一个思路(步骤):
1、首先,了解神经网络的基本概念,特别是一些NLP领域的基本知识,比如深度神经网络是如何训练的,文本是如何被表征的,序列文本信息的处理等。
2、Transformer主要解决了什么问题。重点关注的方面有(自)注意力机制,多头注意力,Transformer的内部结构。
3、动手实现一个Transformer应用。
第一点属于要求掌握一些背景知识,而第二点是Transformer的与众不同之处,第三点是有意向深入学习,甚至想在实践中用Transformer做点什么的人去关注。
现在,有了ChatGPT的助力,一切将变得不同。
我将以一种简单、通俗的方式解释Transformer模型的基本原理。无论您是AI领域的新手,还是对最新技术保持好奇的专业人士,您都会发现这里的内容既有启发性又易于理解。
我的文章共分为四大部分,如下所示,宏观部分是为了让你对整体网络框架有所理解,过程部分是让你知道数据是如何流转的,微观部分是对其中出现的基本概念进行更详细的解释,这应是全网最通俗的一篇介绍Transformer的文章了:
1、网络结构主要模块介绍(宏观)
2、Transformer 训练过程(过程)
3、Transformer 推理过程(过程)
4、核心概念详解(微观)
什么是 Transformer?
在传统的序列建模方法中,如循环神经网络(RNN)和卷积神经网络(CNN),在处理长序列均存在限制。
RNN在处理长序列时容易出现梯度消失或梯度爆炸的问题,模型难以捕捉到长距离的依赖关系,不能并行计算。
而CNN卷积操作通常要求输入具有固定的尺寸,在处理可变长序列时,为了使序列具有相同的长度,需要对较短的序列进行填充,导致计算效率低下。
2017年谷歌Brain团队发表了鼎鼎大名的文章“Attention Is All You Need”(注意力就是你所需要的一切),就是这篇文章提出了Transformer网络结构。
如下图所示,其通过自注意力机制(self-attention)等手段来解决上述问题,然后就有了大模型的爆发,读懂了这张图,就算入门了吧。
1、网络结构主要模块介绍
(1)输入嵌入层(Input Embedding)
输入(Inputs):这是模型的输入部分,通常是一串单词或符号的序列。
输入嵌入(Input Embedding):这一步将输入的序列(比如一句话中的每个单词)转换为嵌入表示。嵌入表示是一种高维的、能够捕捉到单词语义信息的向量形式。
位置编码(Positional Encoding ):由于Transformer是不依赖于序列的,位置编码用于给模型提供关于单词在序列中位置的信息。这些编码是添加到输入嵌入上的,确保即使在处理同时的输入时,模型也能利用单词的顺序信息。
(2)编码器层(Encoder,左边)
Nx:这表示有N个相同的编码器层堆叠在一起。每层都有两个主要子层。第一个是多头自注意力机制(Multi-Head Attention),第二个是前馈神经网络(Feed Forward)。
多头自注意力(Multi-Head Attention):注意力机制允许模型在处理每个词时考虑到输入序列中的所有词。多头部分意味着模型会并行地学习输入数据的不同表示。
残差连接和归一化(Add & Norm):每个注意力层后面都会有残差连接(Add),后面跟着层归一化(Norm)。残差连接有助于避免深层网络中的梯度消失问题,而层归一化则有助稳定训练过程并加快收敛速度。
前馈神经网络(Feed Forward):这是一个简单的全连接神经网络,它对自注意力层的输出进行处理,它由两个线性变换和一个非线性激活函数组成。
(3)解码器层(Decoder,右侧)
解码器同样包含多个相同的层,并且每层也有三个主要子层:掩蔽的多头自注意力机制(Masked Multi-Head Attention)、多头自注意力机制(Multi-Head Attention)和前馈神经网络(Feed Forward)。
掩蔽多头自注意力(Masked Multi-Head Attention):这与编码器中的多头自注意力机制相似,但有一个关键区别:为了保证解码的顺序性,掩蔽操作确保了位置i的预测只能依赖于位置i之前的输出,不会"看到"未来的信息。
多头自注意力(Multi-Head Attention):这个模块接受编码器的输出和掩蔽多头注意力的输出作为输入。这样,解码器可以关注输入序列的相关部分,并结合它自己的输出进行预测。
前馈神经网络(Feed Forward):与编码器中的相同。每个子层后面同样跟有加法和归一化步骤。为每个位置上的向量提供额外的非线性变换。
(4)输出嵌入层(Output Embedding)
解码器端的嵌入层将目标序列(在训练时是正确的输出序列,在推理时是已经生成的序列)转换为向量的形式。
(5)输出过程(Output)
线性层(Linear):解码器的最后输出通过一个线性层,这个层的作用是将解码器层的高维输出映射到一个更大的词汇空间,这个空间的每一个维度对应一个可能的输出单词。
Softmax层:线性层的输出通过Softmax函数转换成一个概率分布,这个分布表示每个可能的下一个单词的概率。
2、Transformer 训练过程
Transformer 训练的目标是通过对输入序列与目标序列的学习,生成目标序列。训练过程中,模型对数据的处理过程如下,大体可分为7 个步骤:
(1)序列化和批处理
原始文本数据被转换成数字化表示(通常是单词或子词的索引),然后分成多个批次进行处理。
(2)输入嵌入
批处理数据通过嵌入层将索引转换成高维的嵌入向量。
嵌入向量被送入位置编码模块,该模块加入位置信息以维持序列的顺序。
(3)编码器堆栈
编码器的每一层都接收上一层的输出作为输入,第一层的输入是位置编码后的嵌入向量。
每层编码器包括一个多头注意力子层和一个简单的前馈网络。
多头注意力子层通过自注意力机制处理序列的每个元素,然后是一个残差连接和层归一化。
接着是前馈网络,再次跟随一个残差连接和层归一化。
(4)解码器堆栈
解码器的每一层也接收上一层的输出作为输入,但解码器的第一层接收的是目标序列的嵌入向量(在训练时是正确的序列,采用教师强制)和位置编码。
解码器的每一层包含三个主要组件:掩蔽多头注意力子层(防止位置后看)、编码器-解码器注意力子层(将解码器的注意力集中到编码器的输出上)和前馈网络,每个子层后都有残差连接和层归一化。
(5)输出线性层和Softmax
解码器的输出通过一个线性层,将维度转换为词汇表大小。
接着,Softmax层将线性层的输出转换为概率分布,表示下一个词的预测。
(6)损失计算
通过比较模型的预测和实际的目标序列,计算交叉熵损失。
这个损失将用于反向传播过程,用于更新模型的权重。
(7)反向传播和优化
根据损失函数,通过反向传播计算关于模型参数的梯度。
使用优化算法(如Adam)调整模型参数,以最小化损失函数。
在训练过程中,以上步骤会重复多次,直到模型收敛或满足某些性能标准。在整个过程中,编码器负责理解输入序列的上下文,而解码器则用于生成输出序列,这种生成依赖于编码器的输出和之前已经生成的输出序列。
3、Transformer 推理过程
在推理过程中,我们只有输入序列,而没有目标序列作为输入传递给解码器。Transformer 推理的目标是仅通过输入序列产生目标序列,大体可分为7 个步骤:
(1)输入处理
与训练时类似,输入文本首先被序列化为索引的形式,然后转换为嵌入向量,并加上位置编码。
(2)编码器处理
编码器堆栈接收带有位置编码的嵌入向量,并通过其所有层进行处理。每一层都包括自注意力和前馈网络,以及残差连接和层归一化。
编码器的最终输出是一个上下文表示,包含了输入文本的信息。
(3)解码器处理
在推理阶段,解码器通常一次生成一个词。它以先前生成的词的嵌入(经过位置编码)作为输入。初始时,解码器可能接收一个起始符号(如 "")来开始生成过程。
解码器的每一层首先使用掩蔽多头注意力层防止未来信息的流入,然后与编码器的输出一起处理,使用编码器-解码器注意力层将注意力集中到编码器的输出,接着是前馈网络。同样,每个子层后面都跟着残差连接和层归一化。
(4)输出预测
解码器的输出通过一个线性层和Softmax层,转换成对下一个单词的预测概率分布。
(5)单词选择和序列更新
根据Softmax输出,选择概率最高的单词作为这一步的输出。
这个选中的单词被添加到序列中,并用于下一解码步骤的输入。
(6)序列生成的终止
这个过程一直重复,直到生成一个终止符号(如 ""),或者达到预设的最大长度限制。
(7)后处理
生成的序列可能会经过后处理,例如去除特殊符号,进行必要的解码(将索引转换回文本),并返回最终的可读文本。
在推理过程中,特别是在使用束搜索(beam search)等策略时,可能会同时考虑多个候选解码路径,并选择整体得分最高的路径作为最终输出。这有助于提高翻译或文本生成的质量。
4、核心概念详解
(1)输入嵌入(Input Embedding)
输入嵌入(Input Embedding)是将词汇表中的单词转换为机器可以理解的数值形式的过程。具体来说,它涉及将每个单词映射到高维空间中的向量。这个向量捕捉了单词的语义特征,使得在这个空间中语义相近的单词拥有相近的嵌入向量。
举个例子:
假设我们有一个简单的词汇表,包含四个单词:{'king': 0, 'queen': 1, 'man': 2, 'woman': 3}。在这个词汇表中,每个单词都有一个唯一的索引。在转换为输入嵌入之前,文本中的每个单词都会被转换成对应的索引。例如,句子 "king and queen" 将被转换为索引的序列 [0, 2, 1]。然后,我们会有一个嵌入矩阵,其中的每一行代表一个单词的嵌入向量。假设我们的嵌入维度是5,那么嵌入矩阵可能看起来像这样:
当我们要转换索引序列 [0, 2, 1] 为嵌入向量时,我们只需要选择对应索引的行。因此,"king and queen" 对应的嵌入向量序列就是:
这样,原始的文本数据就被转换成了模型可以处理的数值型数据。在实际的Transformer模型中,嵌入向量维度通常远远大于5,可以是几百到几千,以捕捉更丰富的语义信息。
(2)位置编码(Positional Encoding)
位置编码(Positional Encoding)在Transformer模型中,特别是其变体如GPT中,是通过向输入嵌入中添加一组固定的信号来实现的。对于每一个位置,都会有一个独特的位置编码,该编码与单词的嵌入相加,使得模型能够根据这些嵌入的总和来区分不同位置的单词。
在原始的Transformer模型中,位置编码是使用正弦和余弦函数的固定公式来生成的,每个维度的位置编码依赖于位置的正弦和余弦函数:
对于位置pos和维度序号i,位置编码PE(pos.i)使用以下公式定义,d是嵌入的维度:
PE(pos,2i)=sin(pos/10000^(2i/d))
PE(pos,2i+1)=cos(pos/10000^(2i/d))
下面举个例子:
假设我们有一个简单的句子:“Hello world”,我们想要对这个句子进行编码以输入到一个基于Transformer的模型中。为了简化,假设我们的嵌入维度是4,我们只考虑前两个位置的编码。
首先,我们将单词转换为嵌入向量,假设:
"Hello" -> [1.0, 0.5, 0.3, 0.9]
"world" -> [0.4, 0.2, 0.9, 0.7]
对于位置1("Hello"所在的位置):
PE(1,0)=sin(1/10000^(0/4))=sin(1)
PE(1,1)=cos(1/10000^(1/4))=cos(1)
PE(1,2)=sin(1/10000^(2/4))=sin(1)
PE(1,3)=cos(1/10000^(3/4))=cos(1)
位置 1 编码为 [0.84, 0.54, 0.84, 0.54],同理,位置 2 编码为[0.91, -0.42, 0.91, -0.42]。
然后我们将每个单词的嵌入向量与其对应位置的位置编码相加:
"Hello" with position encoding -> [1.0 + 0.84, 0.5 + 0.54, 0.3 + 0.84, 0.9 + 0.54]
"world" with position encoding -> [0.4 + 0.91, 0.2 - 0.42, 0.9 + 0.91, 0.7 - 0.42]
经过位置编码后的向量可能是这样的:
"Hello" with position encoding -> [1.84, 1.04, 1.14, 1.44]
"world" with position encoding -> [1.31, -0.22, 1.81, 0.28]
(3)多头自注意力(Multi-Head Attention)
自注意力(Self-Attention),也称为内部注意力,是一种注意力机制,它能够在序列内部的单个元素之间建立直接的依赖关系。这意味着模型在处理序列中的每一个元素时,都能考虑到序列中的其他元素,从而获取更全面的上下文信息。
如图所示,a为输入的序列,中间的方框是self attention操作,最终输出的b为依据a每个位置与其它位置关联程度加权后输出的序列【1】。
从细节来说,需要理解几个概念:
查询(Query):查询是你想要了解的信息或者你想要从文本中提取的特征。它类似于你对文本中的某个词语提出的问题或者你想要了解的内容。
键(Key):键是文本中每个词语的表示。它类似于每个词语的标识符或者关键信息,用于帮助计算查询与其他词语之间的关联程度。
值(Value):值是与每个词语相关的具体信息或特征。它类似于每个词语的具体含义或者特征向量。
在自注意力机制中,具体步骤是:
Step 1:从输入值a乘以矩阵Wq、Wk和Wv(这三个矩阵是模型参数,需要通过训练数据来学习)获取查询(Q)、键(K)、值(V)
Step 2:通过计算查询(Q)与键(K)之间的点积,来衡量查询与其他词语之间的关联程度,
然后,通过对这些关联程度进行归一化处理(一般采用softmax归一化),得到每个词语的注意力权重。
Step 3:最后,根据这些注意力权重,对每个词语的值(V)进行加权求和,得到一个新的表示,该表示会更加关注与查询相关的信息。
如图所示,a11是q1和k1点积计算并归一化后得到的关联程度,以此类推,最后再用关联程度与所有v进行加权求和,就能获得到与q1关联最大的信息b1,而其它的b2、b3、b4也是同样的计算方式【1】。
举个具体的例子:
在处理句子“猫坐在垫子上”时,自注意力机制允许模型在生成“坐”这个词的表示时,同时考虑到“猫”、“垫子”和“上”这些词,而不仅仅是依赖于局部或前一个词。通过这种机制,模型能更好地理解语言结构和意义,比如它可以了解“猫”是“坐”的主语,而“垫子”是动作发生的地点。
在传统的自注意力机制中,你只能使用一组查询(Q)、键(K)和值(V)来计算注意力权重。但是,在多头注意力机制中,你可以使用多组不同的Q、K和V来进行计算。
每个注意力头都有自己独立的一组Q、K和V,它们通过独立的线性变换来生成。这样,每个注意力头可以关注文本中不同的方面和特征,从而提取更丰富的信息。最后,多个注意力头的结果会被拼接在一起,并通过另一个线性变换进行整合,得到最终的输出。
下面给出一个多头自注意力的计算过程例子:
以句子“猫坐在垫子上”为例,让我们简化并走过多头自注意力的计算过程。为了简化,假设我们只有两个头,每个头的维度为2,这意味着我们的嵌入向量也会被缩减为2维。
初始化词嵌入(这些通常在实际情况下会有更高的维度):
猫 -> [1, 0]
坐 -> [0, 1]
在 -> [1, 1]
垫子 -> [0, 0]
上 -> [1, 1]
随机初始化Q、K、V矩阵(实际上是训练出来的):
Q矩阵 = [[1, 0], [0, 1]]
K矩阵 = [[0, 1], [1, 0]]
V矩阵 = [[0, 1], [1, 0]]
让我们计算头1的Q、K、V,头1的Q、K、V(假设与上面相同):
Q(坐) = [0, 1] * [[1, 0], [0, 1]] = [0, 1]
K = [[1, 0], [0, 1], [1, 1], [0, 0], [1, 1]] * [[0, 1], [1, 0]] = [[0, 1], [0, 1], [1, 1], [0, 0], [1, 1]]
V和K相同(简化情况)
计算注意力权重:
scores = Q(坐) * K^T = [0, 1] * [[0, 0, 1, 0, 1], [1, 1, 1, 0, 1]] = [1, 1, 1, 0, 1]
应用softmax得到注意力权重(假设softmax不改变权重,因为这里只是示例):
attention_weights = [0.2, 0.2, 0.2, 0, 0.2]
计算加权和:
V * attention_weights = [[0, 1], [0, 1], [1, 1], [0, 0], [1, 1]] * [0.2, 0.2, 0.2, 0, 0.2] = [0.4, 0.8]
这个向量代表了对“坐”这个词的加权上下文表示,反映了整个句子中不同词对“坐”词的贡献。在多头自注意力中,每个头都会进行这样的计算,然后我们会拼接或整合所有头的输出来得到最终的表示。
(4)残差连接和归一化(Add & Norm)
每个注意力层后面都会有残差连接(Add),后面跟着层归一化(Norm)。
残差连接(Add):这一步的作用是将子层的输入直接添加到其输出,这有助于解决深度网络中的梯度消失问题。它允许模型在深层中学习小的变化,而不会丢失输入信息。
假设我们有一个输入向量 x 和一个通过子层处理得到的输出向量 y。残差连接就是简单地将输入 x 直接加到输出 y 上,即z=x+y。
归一化层(Norm):归一化步骤是为了加速训练过程并提高稳定性。它通过规范化每个特征向量来确保网络中的激活在训练过程中保持相同的规模。
举例:在残差连接后,我们得到了向量 z。层归一化会计算 z 的均值和标准差,并用这些参数将 z 归一化,得到z'=(z-u)/o,其中u是均值,o是标准差。通常,这之后还会有一个缩放和位移操作,通常表示为LN(z)=Yz+B,其中Y和B是看学习的参数。
这两个步骤的组合对 Transformer 架构至关重要,因为它们允许模型深度增加而不会降低性能。在 NLP 任务如机器翻译和文本生成中,这种架构已经取得了显著的成功。
(5)前馈神经网络(Feed Forward)
前馈神经网络(Feed Forward Neural Network,FFNN)是一种基础的神经网络架构,在这种网络中,信息仅沿一个方向前进:从输入节点通过隐藏层(如果有的话)到输出节点,没有任何循环或反馈。
在Transformer模型的上下文中,前馈神经网络指的是一个由全连接层组成的子网络,它跟在多头注意力机制之后。通常,这个前馈网络包含两个线性变换,并且中间有一个非线性激活函数,如ReLU或GELU。
首先,它可能会将注意力层的输出通过一个线性层,使得模型能够重新组合这些特征。然后,通过非线性激活函数,如ReLU,增加模型的非线性处理能力。最后,另一个线性层将这些非线性特征转换成最终的输出格式。
相对于多头自注意力主要关注的是不同输入成分之间的关联性,如词与词之间的相互作用。而前馈神经网络(FFNN)在这个基础上进一步作用,它可以:
增加非线性:多头自注意力是一个相对线性的过程,而前馈神经网络通过激活函数如ReLU引入非线性,使得模型能够捕捉更复杂的特征和更抽象的表示。
整合信息:前馈神经网络可以将多头自注意力层学习到的不同方面的信息整合起来,形成更全面的表征。
创建新的特征组合:虽然多头自注意力可以对特征进行加权组合,但前馈神经网络能通过权重矩阵创建全新的特征表示,这些新的特征可能更适合解决特定任务。
独立处理每个位置:在Transformer模型中,前馈网络是独立应用于每个位置的。这意味着每个位置都可以根据自己的上下文信息经过非线性变换,而不是只依赖于与其他位置的关系。
总之,虽然多头自注意力能够捕捉输入中的位置关系,前馈神经网络则在此基础上对每个位置的特征表示进行进一步的加工和抽象化。两者结合使得Transformer模型能够有效处理序列数据,并在多种复杂任务中取得优异表现。
(6)线性层(Linear)
在Transformer模型中,"Linear"这一步骤指的是一个全连接的神经网络层,其作用是将从上一层(比如多头注意力(Multi-Head Attention)机制或前馈网络(Feed Forward))得到的特征转换为最终的输出格式。
全连接层(Linear Layer)通常执行以下操作:
变换维度:它可以将输入的特征向量的维度变换为另一个维度。例如,在解码器的最后,全连接层会将特征向量变换为词汇表大小的向量,每个维度对应于词汇表中的一个单词。
学习特征:通过这种线性变换,模型能够学习输入特征与输出之间的复杂映射关系。
准备Softmax:在输出层,这个全连接层的输出将作为Softmax函数的输入,Softmax函数随后将这些线性输出转换为概率分布。
假设我们有一个Transformer模型正在处理一个词汇表中有10000个单词的语言模型任务。在模型的某个阶段,我们得到了一个特征向量,该向量的维度为512,这是经过多头注意力机制处理后的结果。现在,我们希望将这些特征映射到一个新的向量中,其中每个维度对应于词汇表中的一个单词,以便我们可以计算每个单词作为下一个单词的概率。
这里,全连接层(Linear Layer)就起到了作用。全连接层有一个权重矩阵,大小为10000*512。当我们将512维的特征向量通过这个全连接层时,它会被变换成一个10000维的向量。这个过程可以表示为以下矩阵乘法操作:
输出向量=权重矩阵*输入向量+偏置向量,如果用具体的数值来表示它,可以是:输入向量x是512*1的列向量,权重矩阵W是10000*512的矩阵,偏置向量b是一个10000*1的列向量。
最后的输出向量是一个10000*1的列向量,它可以被送入Softmax函数来转换为一个概率分布,这样每个元素就代表了了词汇表中相应单词作为下一个单词出现的概率。
(7)Softmax层
Softmax层是一种将一组实数值映射成一组概率分布的函数,它通常用于神经网络的最后一层,特别是在处理多分类问题时。在Softmax层中,每个数值(通常是模型的输出)都通过指数函数变为正数,并且被归一化,使得所有数值之和为1,从而可以被解释为概率。
Softmax函数可以表示为:
Softmax层的价值在于:
概率解释:将模型的输出转换为概率分布,使得每个输出都可以解释为某个事件的概率。
可微性:Softmax函数是可微的,这意味着可以通过梯度下降等优化算法来训练模型。
决策边界:它在多类别的输出之间提供了明确的决策边界。对于给定的输入,Softmax层会增强最高概率的输出值,并压制其他值,使得模型可以更有信心地选择某个类别。
与损失函数协同:它通常与交叉熵损失函数结合使用,在分类问题中这样的组合可以高效地进行训练。
稳定性:使用Softmax函数可以避免数值计算中的稳定性问题,因为它将任意范围的输入值转换为(0,1)范围内的输出。
在Transformer模型中,Softmax层用于将解码器的输出(每个单词对应的分数)转换为下一个单词的概率分布,这是生成文本的关键步骤。
我对Transformer感兴趣,一方面是源于求知的需要,另一方面跟自己当前从事的大模型相关工作有关系。在大家喊着做大模型的时候,还是要静下来好好理解理解这个新事物,这才有了这篇学习笔记。
请转给有需要的人!
参考文献:
【1】作者:等壹 来源:知乎 为什么我还是无法理解transformer?
https://www.zhihu.com/question/596771388/answer/3263001703