提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
因为本人在做大模型优化方面的研究,之前拆了ChatGLM2的源代码,看看能从哪些地方深入。结果刚拆完没多久,昨天,也就是10 月 27 日,智谱 AI 在 2023 中国计算机大会(CNCC)上发布了自研第三代对话大模型 ChatGLM3,这是智谱 AI 在今年内第三次对 ChatGLM 基座模型进行了深度优化。目前还没去拆它的源代码,所以也不太清楚和2代之间有什么区别。但2代的结构我觉得可以先发以下,顺便谈谈和1代的区别。
和ChatGPT类似,ChatGLM是基于GLM大模型的下游对话应用。GLM的全称是通用语言模型模型General Language model,是清华大学与智谱AI研发的中英双语大语言模型。官方API的ChatGLM是基于GLM-130B千亿基础模型,但官方也发布了GLM-6B小参数(62亿)版本,可在消费级显卡上部署。
一代发布的时间大概在一年多前,成果最早以一篇论文的形似和发布,有兴趣的可以去arxiv上看看原文(https://arxiv.org/pdf/2103.10360.pdf)。这篇文章的大概意思我稍稍总结了如下:
首先介绍一下目前基于transformer的大模型架构类别,主要有三类:
可以看到,自编码和自回归是两种不同的架构和自然语言处理应用思路。之前的语言模型各有优缺点,但没有一种框架能够在所有的自然语言处理任务中都表现出色。一些先前的工作尝试通过多任务学习的方式,将不同框架的目标结合起来,但由于自编码和自回归目标本质上的不同,简单的结合不能充分继承两者的优势。因此,清华大学提出了一种基于自回归空白填充的通用语言模型(GLM),来解决这个挑战。GLM 通过添加二维位置编码和允许任意顺序预测空白区域,改进了空白填充预训练,在自然语言理解任务上超越了 BERT 和 T5。
主要有三个特点
GLM 从输入文本中随机挖掉一些连续的词语(自编码思路),然后训练模型按照一定的顺序逐个恢复这些词语(自回归思路)。这种方法结合了自编码和自回归两种预训练方式的优点。
此外,GLM打乱了空白区域的预测顺序,并使用二维位置编码(第一个维度对span在原文本中的位置进行编码,第二个维度对token在span中的位置进行编码)。实验表明,GLM 在参数量和计算成本相同的情况下,能够在 SuperGLUE 基准测试中显著超越BERT,并且在使用相似规模的语料(158GB)预训练时,能够超越 RoBERTa 和 BART。GLM 还能够在自然语言理解和生成任务上显著超越 T5,而且使用的参数和数据更少。
GLM2虽然事实上却还残留encoder-decoder架构的要素,但理论上应该不是encoder-decoder架构了。除了在代码逻辑中根据模型配置判断是否是encoder-decoder架构时自行否认,也因为模型架构中GLMBlock只有一种命名为encoder的类,执行着两者的职能。所以我认为ChatGLM2是decoder-only架构,关于这一点如果有意见不同的可以在评论区阐述理由。
在实验室服务器上本地部署后(如果想知道怎么部署的之后可以单写一篇),用"你好"这个最简单的输入进行测试,得到整体流程如下
可以看到,这个模型在推理阶段主要由两层循环组成。
输入“你好”
(1)先被自动填充嵌入一个简单的prompt变成“[Round 1]\n\n问:你好\n\n答:”
(2)根据分词器的词典进行分词,模式采用的是wordpiece分词法,基本原理就是一个预先给定的词表中去套用输入文本,根据概率将给定文本分割成基本单元词片,然后用Int32的下标作为词片的id。
从上图可以看到,“[Round 1]\n\n问:你好\n\n答:”字符串在经过这一步时被处理成了一个长度为17的整数数组。其中前两个数字64790、64792是固定的开头标识。那么这个给定的分词表之后还会加上一些特殊的Token,最后形成一个长度为65024的词表。
Embedding层的参数是可训练,在你下载到本地的model文件夹中有7个二进制参数文件和一个映射表,表中可以查看ChatGLM2-6B的所有层的参数存在哪个二进制文件中。模型的Embedding层参数以及预训练好了,从图中可以看到,Embedding层的形状为65024*4096,即对第一步中提到的size=65024的词表中的每个词都可以映射为长度为4096的特征向量。
的所以只需要把第一步生成的形状为17*1的整数数组输入,即可得到一个17*4096的嵌入。由于ChatGLM2-6B支持同时输入多句话,所以真实的输入维度为[17,1,4096]分别对应序列长度(多句输入时会对所有句子padding统一到最长序列)、批数、嵌入的特征空间维度。
GLMBlock结构图如下。可以看到也是对Transformer进行了一个魔改,但主体还是一个注意力模块和一个MLP全连接模块。下面还是从输入流的角度看看block的结构,。
(1)首先一上来就是一个RMS归一化层。
(2)进入注意力模块,对输入数据进行QKV映射,得到输入数据的Query、Key、Value值,形状分别为[17,1,32,128]、[17,1,2,128]、[17,1,2,128],可以看到Key-value的形状保持一致。
(3)然后经过核心注意力运算,也就是缩放点积注意力层,Query和Key点积后消除量纲,再点积Value,随后reshape回[17,1,4096]的形式。
(4)离开attention模块、进入MLP模块前,要完成三个操作:Dropout、残差连接、后归一化,如图。这里的残差add的值是还没经过前归一化的原始输入。
(5)进入MLP模块:要经过两次变换,中间的激活函数是SwiGLU。在这一层,虽然输入、输出的维度还是4096,但在中间过程涨到了27392,极大丰富了表示能力。
(6)离开MLP后,也要经过Dropout和残差连接,这里加的是attention结束后、后归一化前的值。最后输出GLMBlock的是一个和输入Block的shape一样的矩阵。
(7)在for循环控制下,这个输出矩阵被当作下一轮的输入,一共要走28次。这28次的Block的参数都不一样,具体见参数映射表。
另:在28轮循环中,用一个Present变量收纳了每一轮的Key、Value值,但在单步调试过程中这些值始终没有被用到。除了用于分析中间过程外,我实在想不到该怎么解释这种情况,如果有知道的大佬烦请告知,不甚感激!
在第三步的28轮GLMBlcok循环后,最后一层block输出的attention scores会被用于输出处理。输出处理主要有三个组件:
第一个是RMS归一化,相当于弥补了Block中MLP层出来后没有进行的归一化。
第二个是嵌入的逆操作,将输出的[17,1,4096]维的嵌入向量还原为id值,变成一组65024长度的输出logits。这一步实际上是根据attention scores对65025的词表进行了一个概率估计的操作。
第三个是Softmax操作,将logits变为概率(和为1),并选择其中的最大值,输出其id。这个id就是这一轮28次循环生成的token的id。在我的实例中,这个id是36474,代表“你”。
在最外层的while true循环下,只要这个id不是2(
好烦,本来以为拆完了后可以推进下一步了,没想到被官方背刺了。下一步得去看看ChatGLM3的模型架构,如果改动较大的话也得做一个类似的推理流程,然后才能进入科研正轨,也就是拿这个做实验进行一些推理加速的idea印证。也不一定会出,xdm要的可以插个眼蹲一波。