近日,一位来自新西兰的小哥Brendan Bycroft在技术圈掀起了一股热潮。他创作的一项名为大模型3D可视化的项目,不仅登上了Hacker News的榜首,而且其震撼的效果更是让人瞠目结舌。通过这个项目,你将在短短几秒钟内完全理解LLM(Large Language Model)的工作原理。
无论你是否是技术爱好者,这个项目都将给你带来前所未有的视觉盛宴和认知启迪。让我们一起来探索这个令人惊叹的创作吧!
简介
本项目中,Bycroft详细解析了OpenAI科学家Andrej Karpathy开发的一款轻量级GPT模型,名为Nano-GPT。作为一个缩小版的GPT模型,该模型仅拥有85000个参数。 当然,尽管这个模型比OpenAI的GPT-3或GPT-4小得多,但可谓是“麻雀虽小,五脏俱全”。
Nano-GPT GitHub:https://github.com/karpathy/nanoGPT
为了方便演示Transformer模型每一层,Bycroft为Nano-GPT模型安排了一个非常简单的目标任务:模型输入是6个字母"CBABBC",输出是按字母顺序排好的序列,例如输出 "ABBBCC".
我们称每个字母为token,这些不同的字母构成了词表 vocabulary:
对于这张表格来说,每个字母分别分配了一个下标token index。 这些下标组成的序列可以作为模型的输入:2 1 0 1 1 2
3D可视化中,每个绿色的单元代表经过计算的数字,而每个蓝色的单元则表示模型的权重。
序列处理中,每个数字首先会被转换为一个C维的向量,这个过程称为嵌入(embedding)。在Nano-GPT中,这个嵌入的维度通常为48维。通过这种嵌入操作,每个数字被表示为一个在C维空间中的向量,从而能够更好地进行后续的处理和分析。
embedding要经过一系列中间的模型层计算,这中间的模型层一般称为Transformers,最后到达底层。
「那么输出是什么呢?」
模型的输出是序列的下一个token。 所以在最后,我们得到了下一个token是A B C的概率值。
在这个例子里,第6个位置模型以较大概率输出了A。现在我们可以将A作为输入传入模型,重复整个过程就可以了。
此外还展示了GPT-2和GPT-3可视化效果。
GPT-3具有1750亿个参数,模型层足足有8列,密密麻麻没遍布了整个屏幕。
GPT-2模型的不同参数版本展现出了巨大的架构差异。这里以GPT-2(XL)的150亿参数和GPT-2(Small)的1.24亿参数为例。
需要注意的是,本可视化主要是侧重于模型推理(inference),而不是训练,因此它只是整个机器学习过程的一小部分。并且,这里假设模型的权重已经经过预训练,再使用模型推理来生成输出。
嵌入Embedding
前面有提到,如何使用一个简单的查找表(Lookup Table)将token映射为一串整数。
这些整数,即标记token index,这是模型中第一次也是唯一一次看到整数。之后,将使用浮点数(十进制数)进行运算。
这里,以第4个token(index 3)为例,看看其是如何被用于生成输入嵌入的第4列向量的。
首先使用token index (这里以B=1为例) 从Token Embedding matrix选择第二列,得到一个大小为C=48(48维)的列向量,称为token嵌入(token embedding)。
再从position embedding matrix选择第四列(「因为这里主要查看第4个位置的(t = 3)token B」),同样地,得到一个大小为C=48(48维)的列向量,称为位置嵌入(position embedding)。
需要注意的是,position embeddings和token embeddings都是模型训练得到的(由蓝色表示)。现在我们得到了这两个向量,通过相加我们就可以得到一个新的大小为C=48的列向量。
接下来,以相同的过程处理序列中的所有token,创建一组包含token值及其位置的向量。
由上图可以看出,对输入序列中的所有token运行此过程,会产生一个大小为TxC的矩阵。其中,T表示序列长度。C表示通道(channel),但也称为特征或维度或嵌入大小,在这里是48。这个长度C是模型的几个“超参数”之一,设计者选择它是为了在模型大小和性能之间进行权衡。
这个维度为TxC的矩阵,即为输入嵌入(input embedding),并通过模型向下传递。
小Tip: 随意将鼠标悬停在input embedding上的单个单元格上,可以查看计算及其来源。
层归一化Layer Norm
前面得到的input embedding矩阵即是Transformer层的输入。
Transformer层的第一步是对input embedding矩阵进行层归一化处理(layer normalization),这是对输入矩阵每一列的值分别进行归一化的操作。
归一化是深度神经网络训练中的一个重要步骤,它有助于提高模型在训练过程中的稳定性。
我们可以将矩阵的列单独分开来看,下面以第四列为例。
归一化的目标是使得每列的数值均值为0,标准差为1。为实现这一目标,需要计算每一列的均值和标准差,然后让每一列减去相应均值和除以相应标准差。
这里使用E[x]来表示均值, Var[x]来表示方差(标准差的平方)。epsilon(ε = 1×10^-5)是防止出现除0错误。
计算并存储归一化后的结果,然后乘以学习权重weight(γ)并加上偏置bias(β),进而得到最终的归一化结果。
最后,在输入嵌入矩阵(input embedding matrix)的每一列上执行归一化操作,就得到了归一化后的输入嵌入(normalized input embedding),并将其传递给自注意力层(self-attention)。
自注意力Self Attention
Self Attention层大概算是Transformer中最核心的部分了,在这个阶段,input embedding中的列可以相互“交流”,而其它阶段,各列都是独立存在的。
Self Attention层由多个个自注意力头组成,本例中有三个自注意力头。每个头的输入是input embedding的1/3部分,我们现在只关注其中一个。
第一步是从normalized input embedding matrix的C列中为每一列生成3个向量,分别是QKV:
Q: 查询向量Query vector
K: 键向量Key vector
V: 值向量Value vector
要生成这些向量,需要采用矩阵-向量乘法,外加偏置。每个输出单元都是输入向量的线性组合。
例如,对于查询向量,即是由Q权重矩阵的一行和输入矩阵的一列之间的点积运算完成的。
点积的操作很简单,就是对应元素相乘然后相加。
这是一种确保每个输出元素都能受到输入向量中所有元素影响的通用而简单的方法(这种影响由权重决定)。因此,它经常出现在神经网络中。
在神经网络中,这种机制经常出现是因为它允许模型在处理数据时考虑到输入序列的每个部分。这种全面的注意力机制是许多现代神经网络架构的核心,特别是在处理序列数据(如文本或时间序列)时。
我们对Q, K, V向量中的每个输出单元重复此操作:
我们如何使用我们的 Q(查询)、K(键)和 V(值)向量呢?它们的命名给了我们一个提示:‘键’和‘值’让人想起字典类型,键映射到值。然后‘查询’是我们用来查找值的手段。
在Self Attention的情况下,我们不是返回单个向量(词条),而是返回向量(词条)的某种加权组合。为了找到这个权重,我们计算一个Q向量与每个K向量之间的点积,再加权归一化,最后用它与相应的V向量相乘,再将它们相加。
以第6列为例(t=5),将从这一列开始查询:
由于attention matrix的存在,KV的前6列是可以被查询到的,Q值是当前时间。
首先计算当前列(t=5)的Q向量与之前各列(前6列)的K向量之间的点积。然后将其存储在注意力矩阵的相应行(t=5)中。
点积的大小衡量了两个向量间的相似度,点积越大越相似。
而只将Q向量与过去的K向量进行运算,使得它成为因果自注意力。也就是说,token无法‘看到未来的信息’。
因此,在求出点积之后,要除以sqrt(A),其中A是QKV向量的长度,进行这种缩放是为了防止大值在下一步的归一化(softmax)中占主导地位。
接下来,又经过了softmax操作,将值域范围缩小到了0到1。
最后,就可以得出这一列(t=5)的输出向量。查看归一化attention matrix的(t=5)行,并将每个元素与其他列的相应V向量相乘。
然后,我们可以将这些向量相加,得出输出向量。因此,输出向量将以高分列的V向量为主。
现在我们应用到所有列上。
这就是Self Attention层中一个头的处理过程。「因此,Self Attention的主要目标是每一列都想从其他列中找到相关信息并提取其值,它通过将其 Query 向量与那些其他列的 Key 进行比较来实现这一点。增加的限制是它只能向过去看。」
投影Projection
在Self Attention操作之后,我们会从每个头得到一个输出。这些输出是受Q和K向量影响而适当混合的V向量。要合并每个头的输出向量,我们只需将它们堆叠在一起即可。因此,在t=4时,我们将从3个长度为A=16的向量叠加形成1个长度为C=48的向量。
值得注意的是,在GPT中,头(A=16)内向量的长度等于 C/num_heads。这确保了当我们将它们重新堆叠在一起时,能得到原来的长度C。
在此基础上,我们进行投影,得到该层的输出。这是一个简单的矩阵-向量乘法,以每列为单位,并加上偏置。
现在我们有了Self Attention的输出。
我们没有将这个输出直接传递到下一阶段,而是将它以元素的方式添加到input embedding中。 这个过程,用绿色垂直箭头表示,被称为残差连接(residual connection)或残差路径(residual pathway)。
与Layer Normalization一样,残差网络对于实现深度神经网络的有效学习至关重要。
现在有了self-attention的结果,我们可以将其传递到Transformer的下一层:前馈网络。
多层感知机MLP
在Self Attention之后,Transformer模块的下一部分是MLP(多层感知机),在这里它是一个有两层的简单神经网络。
与Self Attention一样,在向量进入MLP之前,我们需进行层归一化处理。
同时,在MLP中,还需对每个长度为C=48的列向量(独立地)进行以下处理:
添加带偏置的线性变换(也就是矩阵-向量乘法并加上偏置的运算),转换为长度为 4 * C 的向量。
GELU 激活函数(逐元素应用)。
进行带偏置的线性变换,再变回长度为 C 的向量。
让我们追踪其中一个向量:
MLP具体处理如下:
首先进行矩阵-向量乘法运算并加上偏置,将向量扩展为长度为 4*C 的矩阵。(注意这里的输出矩阵是经过转置的,为了形象化)
接下来,对向量的每个元素应用GELU激活函数。这是任何神经网络的关键部分,我们需要在模型中引入了一些非线性。所使用的具体函数 GELU,看起来很像 ReLU 函数 max(0, x),但它有一个平滑的曲线,而不是尖锐的角。
然后,通过另一个带偏置的矩阵-向量乘法,将向量投影回长度C。
这里也有一个残差网络,与自注意力+投影部分一样,我们将MLP的结果按元素顺序添加到input中。
重复这些操作。
MLP层到此就结束了,我们最后也得到了transformer的输出。
Transformer
这就是一个完整的Transformer模块!
这些若干个模块构成了任何 GPT 模型的主体,每个模块的输出都是下一个模块的输入。
正如在深度学习中常见的,很难准确说出这些层各自在做什么,但我们有一些大致的想法:较早的层倾向于专注于学习低级特征和模式,而后面的层则学习识别和理解更高级的抽象和关系。在自然语言处理的背景下,较低的层可能学习语法、句法和简单的词汇关联,而较高的层可能捕捉更复杂的语义关系、话语结构和上下文依赖的含义。
Softmax
最后就是softmax操作,输出每个token的预测概率。
输出Output
最终,我们到达了模型的尾端。最后一个Transfomer的输出经过一层正则化处理,随后进行一次无偏置的线性变换。
这一最终变换将我们的每个列向量从长度C转换为词汇量大小的长度nvocab。因此,它实际上是为词汇表中的每个单词生成一个得分logits。
为了将这些得分转换为更加直观的概率值,需要先通过softmax来进行处理。如此一来,对于每一列,我们都得到了模型分配给词汇表中每个单词的概率。
在这个特定模型中,它实际上已经学会了所有关于如何对三个字母进行排序的答案,因此概率极大地倾向于正确答案。
当我们让模型随时间推进时,需要使用最后一列的概率来决定序列中下一个添加的token。例如,如果我们向模型中输入了六个token ,我们会使用第六列的输出概率。
这一列的输出是一系列概率值,我们实际上需要从中选出一个作为序列中的下一个token 。我们通过「从分布中采样」来实现这一点,即根据其概率随机选择一个token 。例如,概率为0.9的token会被选择的概率是90%。然而,我们也有其他选择,例如总是选择概率最高的 token 。
我们还可以通过使用温度参数来控制分布的「平滑度」。较高的温度会使分布更均匀,而较低的温度则会使其更集中于概率最高的token 。
我们通过在应用softmax之前,用温度参数来调整logits(线性变换的输出),因为softmax中的指数化对较大数值有显著的放大效果,使所有数值更接近将减少这种效果。
图片
还没有评论,来说两句吧...