从 GPT 看大语言模型基本结构

2,483 阅读17分钟

背景

从 ChatGPT 发布以后,我们似乎看到了 AGI(通用人工智能)的曙光,不管是什么样的任务,从写文章到写代码,甚至是一些带推理和创意性的思考,基本上 ChatGPT 都能生成质量较高的答案,虽然仍然存在幻觉(AI Hallucination)的问题,但总体来说已然可以窥见一丝未来的影子。陆奇在他的演讲《我的大模型世界观》中说,任何复杂体系、系统都可以拆解成信息、模型和行动三个部分,互联网改变了信息传递的方式,引发了变革;而大模型将会彻底改变处理信息的模型的成本,毫无疑问将会带来更为深远的影响。本文将结合网络上的参考资料,对已经公开的 GPT3 的基本原理做简要的分析,形成对大语言模型的基本的知识框架,为我们后续理解、使用、调优甚至是开发大语言模型打好基础。

语言模型原理

虽然表现出了通用人工智能的特性,但从原理上来说,ChatGPT 背后的 GPT3.5、GPT4 只是一个语言模型,它的基本工作模式(推理过程)是,给定一串输入的文本,预测下一个输出的字符,再结合输出的字符与输入的文本中作为新的输入,循环往复,直到输出的是结束符为止。使用数学的等价表述就是,给定一个词典 V={,,机器,学习,语言,模型,...}V=\{猫,狗,机器,学习,语言,模型,...\}ω1ωnV\omega_1\cdots\omega_n\in V,计算 P(ωiω1,ω2ωi1)P(\omega_i | \omega_1, \omega_2 \cdots \omega_{i-1}) 的条件概率。

如何高效合理地计算这个概率呢?最基础的有基于机器学习的 N-Gram 方法,它使用统计学的方法,假设每一个词都只和前 n-1 个词有关,使用链式法则和条件概率的知识可以得到 P(wiwiN+1i1)=P(wiN+1i)P(wiN+1i1)P(w_i | w_{i-N+1}^{i-1}) = \frac{P(w_{i-N+1}^{i})}{P(w_{i-N+1}^{i-1})},从而可以在训练数据中使用频率统计的方式来得到概率。但 N-Gram 语言模型将每个词看作是离散的变量,而实际上词与词之间是存在相似性的(比如 猫和狗 肯定比 猫和电脑 更相关),这限制了模型的泛化能力,凡是训练集中没出现过的就不是句子。前馈神经网络语言模型通过结合词向量 + 前馈神经网络,很好的解决了词之间的相关性问题,且神经网络强大的学习能力也非常适合拟合概率分布,但是同样的,每个词只依赖前 N 个词的这个假设在自然语言中还是限制了模型的上限。循环神经网络(RNN)语言模型在基础神经网络之上引入了时序+记忆概念,很好的解决了模型上下文不够的问题,但是由于无法并行计算,模型的规模受到了限制。直到 Transformer 横空出世,基于自注意力机制、位置编码等特性让它天生就支持并行计算,可以将规模做的非常大,同时自注意力机制解决了长距离依赖等问题。目前市面上的大语言模型基本上都基于 Transformer 而搭建。

神经网络基础

在开始介绍基于 Transformer 的 GPT3 架构之前,我们需要先了解什么是神经网络。我们从一个最简单的象限分类的场景出发:假设已知四个点坐标和它们所处的象限信息(如下图所示),给定一个未知点坐标(2,2),如何预测它所处的象限,即正确的将它分到 I - IV 这四个类别中。

仅做示例,实际上这个分类边界我们已经知道了。在开始之前,推荐大家先回顾一下线性代数的基础 链接

我们需要构建一个最简单的两层的神经网络,输入是一个 1x2 的二维矩阵 X=[x,y]X = [x,y] ,输出是一个 1x4 的表示点落在各个象限的概率的矩阵 [PI,PII,PIII,PIV][P_I,P_{II},P_{III},P_{IV}] (输入和输出的矩阵形式取决于我们的任务),这个网络的结构是这样的:

可以将隐藏层形象的理解为 50 个神经元(具体的神经元个数可以根据任务自行设定),每一个神经元做的事情,就是将输入的特征做一次线性的映射得到输出的值 h1=(x,y).w1+b1h_1=(x,y) . \vec w_1+b_1输入层映射到隐藏层整体可以使用矩阵乘法表示为:

H=XW1+b1H=X \cdot W_1+\vec b_1

其中 W1W_1 是一个 2*50 的权重矩阵,偏置 b1\vec b_1 是一个长度为 50 的向量。隐藏层映射到输出层也是同理,W2W_2 是一个 50*4 的权重矩阵:

Y=HW2+b2Y = H \cdot W_2+ \vec b_2

如果从输入到隐藏层,隐藏层再到输出只是两次线性变换的话,根据线性代数的原理,多次线性变换是可以合并成一次的,这样的话,不管做几层神经网络,其实都是没有意义的。所以我们需要给隐藏层做一次非线性变换处理,即对隐藏层中的每一个值(标量),都通过一个非线性函数映射到新值,我们称它为激活函数,图中的 ReLU 就是一种激活函数,常用的激活函数的函数曲线如下图所示:

image.png

我们的目标是使用神经网络来实现推理,即给定一个输入 XX,通过 XYX \to Y 得到输出,Y 中最大的那个值就是概率最大的象限了。区别于常规的工程开发,机器学习编程的目的,是为了从数据中学习。具体来说,我们需要通过训练过程来得到合理的权重矩阵 WW 和偏置向量 b\vec b(默认的权重矩阵、偏置都是随机初始化的)。

搭建好网络结构后,第一步是使用训练数据进行训练,然后才是用模型(网络结构 + 权重的 checkpoint)来推理,训练的流程可能和推理的流程有细微的区别,这个需要区分开来。

矩阵 Y 的结果可能是形如 [3,1,0.1,0.5][3,1,0.1,0.5] 的常规数字,为了能够对输出结果做更好的衡量,我们需要通过下面的公式把它转成概率分布(满足所有值在 [0,1][0,1] 范围内,总和为 1,特征对概率的影响是乘性的,且易于求导,便于反向传播迭代):

Si=eiejS_i = \frac{e^i}{\sum e^j}

我们称这一层叫 softmax 层,通过 softmax 之后,我们得到了形如 [0.9,0.05,0.02,0.03][0.9, 0.05, 0.02, 0.03] 这样的概率分布。那么我们如何去衡量这个输出的好坏,从而实现我们的训练目的呢?比较直观的想法是,真实的概率分布其实是 [1,0,0,0][1,0,0,0],那么使用 10.9=0.11-0.9=0.1,这个数字越大,代表和实际情况偏离越严重。在实际使用过程中,我们会使用负对数来表示这个偏差,即 -log0.9=0.046。从函数曲线就可以看出来,概率越接近100%,损失就越接近0。这个就是交叉熵损失(Cross Entropy Loss)的基本计算方法。

image.png

得到损失函数之后,我们就可以通过反向传播去更新前置的每一个权重矩阵,反向传播公式的具体推导过程可以看这个教程。然后经过一轮一轮的迭代计算(训练)之后,我们就可以得到误差足够小,足以拟合我们任务场景的权重矩阵+偏置了。

神经网络是一种直观但却非常强大的学习数据特征的方式,以至于使用多层神经网络来进行特征提取和模型训练的方法都有了“深度学习“这个专有名词。本节介绍的案例又称前馈神经网络,是最简单的神经网络,但也是大语言模型的基本结构,所以理解它是理解大语言模型的基础。

GPT3 网络结构

理解了神经网络的基本概念,下面我们就可以逐层去拆解 GPT3 的网络架构(基本上就是对 Transformer 网络架构在语言模型场景的实践),学习大名鼎鼎的自注意力机制。下面是 Transformer 的架构图以及 GPT 的架构图,接下来我们会分层去讲解其中的原理:

image.png

GPT1.png

输入内容处理

还记得我们在语言模型这一节提到的,语言模型的基本原理是输入一个文本序列(Sequence),预测下一个输出的 Token。那么语言模型应该如何理解我们输入的内容,Transformer 又对这一过程做了什么样的优化呢?

Encoding

首先我们需要将输入的词转成神经网络可以处理的向量的形式,GPT 首先使用了非常好理解的 One-Hot 编码,即有一个 50257 长度的向量,除了第 i 位为 1,其它的位置均填充 0,通过这种方式可以记录一个拥有 50257 个词汇(GPT 使用的是 Byte Pair Encoding,实际上并不全是单词,而是文本里常出现的字符序列,比如 heroes、cap、es)的集合:

image.png

那么我们就可以把输入编码成一个 2048*50257 的矩阵(GPT3 能处理的上下文长度为 2048,现在最新的 GPT 3.5 已经支持 16K 了),其中暂未输出的内容使用空 <> 填充:

image.png

Embedding

One-Hot 编码虽然很好理解,但是 50257 的长度有点太大了,而且大量的 0 其实是在浪费空间,也并没有体现出 Token 与 Token 之间本身存在的一些关系,完全是离散的(上文介绍 N-Gram 时有提到这个问题)。为了解决这一问题,机器学习的一般做法是会使用一个权重矩阵,把高维的 One-Hot 编码映射为一个更低维的向量,同时也包含完整的信息。

Embedding 的一个形象的 case:三维的球面其实是二维的流形嵌入三维空间的结果,因为任何一个球面上的点都可以使用二维的经纬度来表达。

在 GPT3 里,它使用了一个 12288 维度的向量来表达每一个 Token,所以需要训练一个 50257*12288 的嵌入权重矩阵 WEW_E 来实现这一目的,最终得到一个 2048*12288 的输入矩阵:

image.png

Position Encoding

经过上两轮的处理,我们输入的序列已经成为了一个 2048*12288 的输入矩阵,但需要注意的是,在模型训练时,Transformer 架构需要并行计算每一个 Token 的结果,而不是像传统 RNN 一样,每一个 Token 的输入都要依赖前一个 Token 的输出。这就要求每个 Token 的向量内需要包含它所处的位置信息,并且需要有以下几点要求:

  1. 它能为每一个 Token 位置生成一个独一无二的编码
  2. 在不同长度的序列中,距离相同的 Token 之间的编码差值应该是相同的
  3. 模型应该有足够好的泛化性,位置编码的值应该是有界的,不能出现位置编码的值把特征给盖过去的情况
  4. 位置编码应该是确定的

Transformer 非常聪明的提供了一种基于正余弦函数的位置编码方法,巧妙的满足了上面的几点要求。使用数学语言表述,即对于我们 2048*12288 的输入矩阵而言,假设 t 表示在序列中的位置(1~2048),d 表示单个向量的维度(12288),那么位置编码 ptRd\vec p_t \in R^d 的定义为(注意,这里位置编码使用的是向量而不是一个普通的标量):

image.png

从第一行到第n行的位置编码向量可以图形化的表达为(从上到下分别代表第 i 行的位置编码向量):

image.png

上面提到的 1、3、4 条件应该可以直观的感受到,第 2 点可以通过高中数学的和差化积公式来推导,具体推导过程可以参考 链接

通过给每一个输入的 Token 向量叠加上位置编码,每个 Token 就拥有了本身的特征信息 + 位置信息,就可以为后续训练时的并行计算做好基础准备了。

注意力层

从 Transformer 的网络结构可以看到,在经过向量化(Embedding)处理,累加位置编码之后,我们的输入就进入了多头自注意力层(Multi-Head Self Attention)。下面我们会分别从注意力、自注意力和多头自注意力来逐步理解这一机制的大致原理。

image.png

注意力机制

提到注意力机制,必然会提到计算机视觉领域下面这张非常经典的图,这也非常符合我们人类的直觉:在有限的信息处理能力下,大脑会首先快速扫描一遍图像,获得需要关注的焦点,再对焦点投入更多的注意力,并忽略其他不重要的信息。

人类的视觉注意力.jpg

回到深度学习领域,它的目的也是从一堆信息中学习到关键知识。怎么做呢?假设我们有一个查询 Q(query,对应上面那个图的人类观察者),我们的目的是能够从数据 V (values)中获取到关键信息,那么计算的目标就是获取 Q 和 V 的相关度。如何计算呢?我们创建一个新的矩阵 K(keys)= V(一般数据都没有索引,可以让索引直接等于数据),对 Q 和 K 求相似度,再做一层 softmax 归一化,就得到了注意力权重矩阵,再使用注意力权重去乘上数据,就可以为关键数据提供更多的权重:

attention-计算图.png

自注意力机制

相比于注意力机制,自注意力机制的主要差异就是 Q 的取值来源于数据,即 Q、K、V 均来源于输入,为了便于理解,我们下面使用一个简化的数据来描述 Transformer 处理自注意力的流程。

假设我们的输入只有 3 个 Token,每个 Token 使用一个 512 维的向量表示,我们的模型会学习三个 512*512 的权重矩阵 WQ,WK,WVW_Q, W_K, W_V ,得到三个 3*512 的新矩阵 Q=WQA,K=WKA,V=WVAQ=W_Q \cdot A, K=W_K \cdot A, V=W_V \cdot A ,对 QQKTK^T 使用矩阵的点乘求得余弦相似度(Transformer 自注意力采用的是余弦相似度):

attention1.png

attention2.png

再做一次 softmax 就得到了每一个 token 和其他 token 关系的 3*3 的权重矩阵,这个三乘三的矩阵的含义可以可视化的表达如下(线越黑代表越相关,没有连线代表不相关),即权重矩阵的每一行代表了该位置的 token 和其他 token 的关联关系:

attention3.png

下面,我们使用这个注意力权重矩阵再点乘上我们的输入矩阵,就可以得到包含了每一个位置 Token 和其他 Token 的注意力信息的新的 Token 向量(这也是能并行训练的基础)。下面这个图假设注意力矩阵只有 1 和 0,即每一个 Token 只和另外一个 Token 相关,得到的结果是第一个 Token 仍然是自身,第二和第三个 Token 却完全交换了一下顺序(这里极端化成 0 1 反而很难理解了,其实相当于每个 Token 都按照相似度包含了其他 Token 的信息)。

attention4.png

多头注意力

在实践中,GPT 使用了多头注意力,简单来说,就是把上一环节输入的向量做了一次切分,使用切分后得到的矩阵分别做自注意力计算(即对输入矩阵做纵向的分割后独立计算),再把得到的矩阵纵向拼接起来。输出仍然是和单头的自注意力计算是一样的。按照论文的说法,多头保证了 attension 能注意到不同子空间的信息,捕捉到更加丰富的特征信息。(TODO: 对 Token 向量的纵向分割是把信息放入多个子空间这个说法暂时没有理解)

multiheaded.png

注:上图中把每一个子空间的权重矩阵都划到每个头内部了,实际上是在外层计算后直接分割到 96 个头里面,只用学习三个权重矩阵。

前馈神经网络层

图中的 Feed Forward 就是我们在神经网络基础里面讲到的最常见的带一个隐藏层的神经网络,拿到输入,乘上学习的权重,加上学习的偏置,再做一次激活,就得到了输出结果。具体的可以回去看上面的内容。

ff.png

Add & Norm 层

从架构图中可以看到,在多头注意力拼接输出和前馈神经网络输出后,都有一层 Add & Norm 层,这个逻辑非常简单,就是拿原始的输入,再直接矩阵加上输出的内容,得到新的输出,然后再做一次正则化(就是 softmax)后输出到下一层中。(这是从残差网络问世以来深度学习的常见做法,可以有效的解决深度神经网络的退化问题)

addnnorm.png

Decoder

到这里为止,Encoder 部分的逻辑就讲完了。但是 Encoder 得到的是一个矩阵,这个矩阵应该如何被消费,从而得到我们想要的输出结果呢?这里就要讲到 Decoder 部分了,也就是 Transformer 架构图中的右半部分。从图中可以看到,每一个 Decoder 子层都和上层输入以及 Encoder 部分的最终输出连接,对上层输入会做一次多头自注意力(和 Encoder 一样,值得关注的是训练时使用了 Masked Self Attention,将目标 Token 的下文做了遮盖,确保和推理时的行为保持一致,关于训练过程的特殊处理详情,参考链接),然后使用 Attention 之后的输出作为 QdQ_d ,使用 Encoder 输出的数据作为 Ke,VeK_e, V_e (K=V),再做一次注意力。之后和 Encoder 一样,做一次 Feed Forward。所有的层处理完之后,最后做一次反向的嵌入操作(图中的 Linear 层,WE1W_E^{-1} ?),得到和词汇表一致的向量维度,再做一次 SoftMax,就可以知道下一个概率最大的 Token 作为输出了。

image.png

下面的动图是一个基于 Transformer 的机器翻译的任务,可以很形象的说明 Encoder -> Decoder 的这一过程:

tf-动态生成.gif

tf-动态结果-2.gif

GPT 在训练和推理时,使用的都是 预测下一个词 的模式,那么对应到网络结构中,Encoder 处理完之后,Decoder 的 OutPuts 首先是一个 开始符号,此时可以认为没有包含任何信息,简单的认为它没有经过任何处理就到达了和 Encoder 输出做多头注意力的层上。此时,从 Encoder 输出到最终的 Decoder 输出过程可以简化如下:

unembedding1.png

完整架构

简化后整个流程的网络架构为:

fullarch-2.png

大功告成,给定一个输入文本串,推理得到下一个 Token 的流程就结束了。

总结

本文介绍了语言模型的基本原理,以及有代表性的几种语言模型的实现方式。同时作为深度学习的基础,介绍了神经网络的基本原理。之后结合 GPT 的公开资料,介绍了它核心的 Transformer 架构,包含它关键的自注意力机制,位置编码等等,以及 GPT 基于 Transformer 在语言模型上的一些相对早期实践,从中我们可以理解现代大语言模型的基本结构。

由于作者目前还没有开始尝试编码去实现这套流程,中间可能部分细节可能会不准确,欢迎各位大佬批评指正。


参考链接