当前位置: 首页 > news >正文

w~大模型~合集11

我自己的原文哦~ https://blog.51cto.com/whaosoft/12371157

#LLM~幻觉

最近,来自哈尔滨工业大学和华为的研究团队发表了一篇长达50页的综述,细致地盘点了有关LLM幻觉问题你该知道的所有事。

幻觉,老朋友了。

自打LLM进入我们的视野,幻觉问题就一直是一道坎,困扰着无数开发人员。

当然,有关大语言模型幻觉的问题已经有了无数研究。

最近,来自哈工大和华为的团队发表了一篇50页的大综述,对有关LLM幻觉问题的最新进展来了一个全面而深入的概述。

这篇综述从LLM幻觉的创新分类方法出发,深入探究了可能导致幻觉的因素,并对检测幻觉的方法和基准进行了概述。

这其中肯定也少不了业内比较有代表性的减轻幻觉的方法。

论文地址:https://arxiv.org/abs/2311.05232

下面,我们就来看一看本篇综述中主要讲了些什么内容。

想深入学习的朋友,可以移步文章底部的参考链接,阅读论文原文。

幻觉大分类

首先,先来看看有哪些种类的幻觉。

上图中,左边是事实性的幻觉。当LLM被问到谁是第一个在月球上漫步的人时,LLM编了个人物出来,甚至还说得有模有样。

右边则是文本摘要模型中的忠实度问题,可以看到LLM在看到这段新闻后,直接把年份概括错了。

在本篇综述中,研究人员深入分析了LLM中幻觉的起源,涵盖了从数据、训练到推理阶段的一系列促成因素。

在这一框架内,研究人员指出了与数据相关的潜在原因。例如,有缺陷的数据源和未优化的数据利用,或是在预训练和对齐过程中可能会诱发幻觉的训练策略,以及源于解码策略的随机性和推理过程中不完善的表征等等。

此外,研究人员还全面概述了专为检测LLM中的幻觉而设计的各种有效方法,以及与LLM幻觉相关的基准的详尽概述,和作为评估LLM产生幻觉的程度和检测方法有效性的试验平台。

下图即为本篇综述所涉及到的内容、前人研究,以及论文。

下图是一张更为详细的LLM幻觉种类图。

在事实型幻觉和忠实度幻觉下,还包括更为细致的分类。

事实型幻觉:

a)事实不一致

当问LLM,谁是第一位登月的人时,LLM回答说是加加林,而非阿姆斯特朗。这种属于答案与事实不一致,因为确有加加林其人,所以不属于捏造。

b)事实捏造

当让LLM介绍一下独角兽的起源时,LLM并没有指出世界上没有独角兽这种生物,反倒是编了一大段。这种现实世界中没有的,称之为捏造。

忠实度幻觉又包括:指令-答案的不一致、文本不一致,以及逻辑不一致。

a)指令-答案不一致

当LLM被要求翻译一个问句时,LLM输出的答案实际上回答了问题,没有进行翻译。因此是一种指令和答案的不一致。

b)文本不一致

这类不一致更多出现在概括类任务中。LLM可能会罔顾给出的文本,总结一个错的出来。

c)逻辑不一致

在被要求给出2x+3=11的方程解法时,第一步LLM指出,两边同时减去3,得到2x=8.接下来在两边除以2的操作中,LLM输出的答案是3.

8除以2怎么会等于3呢?

幻觉产生原理

数据

接下来,综述开始梳理有关幻觉产生原理的内容。

第一类,数据问题。

·错误信息和偏见。鉴于对大规模语料库的需求日益增长,启发式数据收集方法被用来有效收集大量数据。

这种方法在提供大量数据的同时,可能会无意中引入错误信息,增加出现模仿性错误的风险。此外,社会偏见也会在无意中被引入LLMs的学习过程。

这些偏差主要包括重复偏差和各种社会偏差(Social Biases)。

要知道,LLM预训练的主要目的是模仿训练分布。所以当LLM在事实不正确的数据上接受训练时,它们可能会无意中放大这些不准确的数据,从而可能导致事实不正确的幻觉。

神经网络,尤其是大型语言模型,具有记忆训练数据的内在倾向。研究表明,这种记忆趋势会随着模型规模的扩大而增强。

然而,在预训练数据中存在重复信息的情况下,固有的记忆能力就会出现问题。这种重复会使 LLM 从泛化转向记忆,最终产生重复偏差,即LLM会过度优先回忆重复的数据,导致幻觉,最终偏离所需的内容。

除了这些偏见,数据分布的差异也是产生幻觉的潜在原因。

下一种情况是,LLM通常会存在知识边界。

虽然大量的预培训语料库为法律硕士提供了广泛的事实知识,但它们本身也有局限性。这种局限性主要体现在两个方面:缺乏最新的事实知识和专业领域知识。

虽说LLM在通用领域的各种下游任务中表现出了卓越的性能,但由于这些通用型LLMs主要是在广泛的公开数据集上进行训练,它们在专业领域的专业知识受到缺乏相关训练数据的内在限制。

因此,当遇到需要特定领域知识的问题时,如医学和法律问题,这些模型可能会表现出明显的幻觉,通常表现为捏造事实。

此外,还有过时的事实知识。除了特定领域知识的不足,LLMs知识边界的另一个内在限制是其获取最新知识的能力有限。

蕴含在LLM中的事实知识具有明确的时间界限,随着时间的推移可能会过时。

这些模型一旦经过训练,其内部知识就永远不会更新。

而鉴于我们这个世界的动态性和不断变化的本质,这就构成了一个挑战。当面对超越其时间范围的领域知识时,LLMs往往会采用捏造事实或提供过去可能正确,但现在已经过时的答案的方法来试图「蒙混过关」。

下图中,上半部分即为LLM缺失特定领域内的专业知识——phenylketonuria(苯丙酮尿)。

下半部分即为最简单的一个知识过时的案例。2018年韩国平昌举办冬奥会,2022年北京举办冬奥会。LLM并没有有关后者的知识储备。

由此可见,LLM中与数据有关的幻觉主要源于错误的数据源和不佳的数据利用情况。数据源中的错误信息和固有偏差不仅会传播模仿性虚假信息,还会引入有偏差的输出,从而导致各种形式的幻觉。

在处理特定领域的知识或遇到快速更新的事实知识时,LLM所拥有知识的局限性就会变得很明显。

在数据利用方面,LLMs 往往会捕捉到虚假的相关性,在回忆知识(尤其是长尾信息)和复杂推理场景中表现出困难,从而进一步加剧幻觉。

这些挑战突出表明,亟需提高数据质量,增强模型更有效地学习和回忆事实知识的能力。

训练

现在,综述把目光转向LLM的训练阶段。

LLM的训练过程主要包括两个主要阶段:

预训练阶段,LLMs在这一阶段学习通用表征并捕捉广泛的知识。

对齐阶段,LLMs在这一阶段进行调整,以更好地使用户指令和人类的基本价值观保持一致。虽然这一过程使LLM 具备了还算不错的性能,但这些阶段中的任何不足都可能无意中导致幻觉的发生。

预训练是LLM的基础阶段,通常采用基于transformer的架构,在庞大的语料库中进行因果语言建模。

然而,固有的架构设计和研究人员所采用的特定训练策略,可能会产生与幻觉相关的问题。如上所说,LLM通常采用基于transformer的架构,遵循GPT建立的范式,它们通过因果语言建模目标获取表征,OPT和Llama-2等模型都是这一框架的典范。

除了结构缺陷,训练策略也起着至关重要的作用。值得注意的是,自回归生成模型的训练和推理之间的差异导致了暴露偏差(Exposure Bias)现象。

而在对齐阶段,一般涉及两个主要过程,即监督微调和从人类反馈中强化学习(RLHF),是释放LLM能力并使其符合人类偏好的关键一步。

虽然对齐能显著提高 LLM 响应的质量,但也会带来产生幻觉的风险。

主要分为两方面:能力不对齐和信念不对齐(Capability Misalignment、Belief Misalignment)。

如何检测幻觉?

检测LLM中的幻觉对于确保生成内容的可靠性和可信度来说至关重要。

传统的衡量标准主要依赖于词语重叠,无法区分可信内容和幻觉内容之间的细微差别。

这一挑战凸显了针对LLM幻觉采用更先进的检测方法的必要性。研究人员指出,鉴于这些幻觉的多样性,检测方法也相应地有所不同。 

这里仅详细介绍一例——

·检索外部事实

如下图所示,为了有效地指出LLM输出中不准确的事实,一种比较直观的策略是,直接将模型生成的内容与可靠的知识来源进行比较。

这种方法与事实检查任务的工作流程非常吻合。然而,传统的事实核查方法往往出于实用性考虑而采用了简化假设,导致在应用于复杂的现实世界场景时有可能会出现偏差。

在认识到这些限制因素以后,一些研究者提出,要更加重视真实世界的场景,即从时间受限、未经整理的网络资源中获取证据。

他们首创了一种全自动的工作流,集成多个组成部分,包括原始文档检索、细粒度检索、真实性分类等等。

当然,还有不少其他研究者提出了另外一些办法,比如FACTSCORE,专门用于长文本生成的细粒度事实度量。

其它方法还包括不确定性估计,如下图所示。

有关忠实度幻觉的检测,也有不少相关研究,如下图所示。

其中包括基于事实度量:通过检测生成内容与源内容之间的事实重叠度来评估忠实度。

基于分类器的度量:利用经过训练的分类器来区分生成内容与源内容之间的关联程度。

基于QA的度量方法:利用问题解答系统来验证源内容与生成内容之间的信息一致性。

不确定性估计:通过测量模型对其生成输出的置信度来评估忠实度。

基于prompt的度量方法:让LLM充当评估者,通过特定的prompt策略来评估生成内容的忠实度。

之后,哈工大团队还将较为前沿的减轻幻觉的方法进行了整理,针对上述提到的各类问题,分别提供可行的解决办法。

总结

总而言之,在论文的最后,哈工大的研究人员表示,在这份全面的综述中,他们对大型语言模型中的幻觉现象进行了深入研究,深入探讨了其潜在原因的复杂性、开创性的检测方法和相关基准,以及有效的缓解策略。

虽然开发者们在这个问题上已经有了不少进步,但大型语言模型中的幻觉问题仍然是一个令人关注的持续性问题,需要继续研究。

此外,本篇论文还可以作为推进安全可信的AI的指路明灯。

哈工大团队表示,希望通过对幻觉这一复杂问题的探索,为这些有志之士提供宝贵的见解,推动AI技术向更可靠、更安全的方向发展。

参考资料:

​​https://arxiv.org/abs/2311.05232​​

#LLM-FP4

这篇文章给出了大模型 FP 量化的解决方案。

大语言模型 (LLM) 压缩一直备受关注,后训练量化(Post-training Quantization) 是其中一种常用算法,但是现有 PTQ 方法大多数都是 integer 量化,且当比特数低于 8 时,量化后模型的准确率会下降非常多。想较于 Integer (INT) 量化,Floating Point (FP) 量化能更好的表示长尾分布,因而越来越多的硬件平台开始支持 FP 量化。而这篇文章给出了大模型 FP 量化的解决方案。文章发表在 EMNLP 2023 上。解决LLaMA、BERT等部署难题:首个4-bit浮点量化LLM来了

  • 论文地址:https://arxiv.org/abs/2310.16836
  • 代码地址:https://github.com/nbasyl/LLM-FP4

要了解本文,必须要先具备基本的有关 Floating Point Format 以及 Floating Point Quantization 的知识,首先 Floating Point Number 可以用以下公式表示:

s 代表正负符号位 (sign bit),m 代表尾数位 (mantissa bits),e 代表指数位 (exponent bits)。p 是一个介于 0 到 2^e - 1 之间的值,用来表示当前数字该被划分到哪一个指数区间,d 取 0 或 1 的值,用来表示第 i 个 mantissa bit。b 是 bias,一个用来调整 exponent 区间的整数值。

接下来介绍 Floating Point Quantization 是怎么运作的,首先输入值必须经过一个 scale and clip 的步骤,先把 input clip 到 Floating Point 能表示的最大区间 (±Qmax),如以下公式所示:

可以看到类似于 integer 量化,FP 量化也会加入一个 full-precision 的缩放因子 (scaling factor) 来缩放 input 到合适的区间。而缩放因子在运算矩阵乘法的时候,和低比特的矩阵乘法分开计算,所以并不会造成很大的 overhead。融入了这个 full-precision 的缩放因子之后,不同的 quantized tensor 能够被相应地 clip 到不同的最大最小值区间。在实际使用过程中,会根据输入 tensor 的值域确定需要的量化区间,然后利用公式 (4) 推导出相对应的 bias。注意公式 (4) 里的 bias 可以被用作实数值的缩放因子,见公式 (2)(3)。

Floating-Point Quantization 的下一个步骤是在决定好量化区间后把区间内的值分配到相对应的量化区间内,这个步骤称为 Compare and Quantize:

上图直观说明了量化的过程,当前的输入值,在用公式 5 比较过后,量化到不同的量化区间中。

在得到量化过的 activation 和 weight 后,这里的 scaling factor 提到前面先计算,而达到如下的 efficient matrix multiplication,完成矩阵乘法的加速:

接着本文指出 FP 量化的准确度,和 exponent bits 的设定以及量化的区间息息相关。

如下图所示,不同的 FP format (浮点数的指数位 / 尾数位设定) 之间存在巨大的量化误差差异,只有当选取合适的 FP format 时,FP Quantization 比 INT Quantization 能更好的表示长尾分布。这个现象也在之前的论文中得到验证 [1]。

而这篇文章提出了对应的解决方案,用一个 search-based 浮点量化算法,统筹搜索出最适合的浮点数的指数位 / 尾数位设定以及对应的量化区间。

除此之外,另一个同时出现在各种不同类别 Transformer 模型 (Bert,LLaMA,ViT) 中的现象也会严重影响量化的难度:那就是模型的 activation 中不同 channel 之间的数量级会有很高的差异,而同 channel 之间的量级十分一致。之前 LLM.int8 [2] 和 SmoothQuant [3] 也有类似的发现,不过这篇文章指出这个现象不仅仅存在于 LLM 中,并且在其他 Transformer 模型里也有类似现象 如下如所示,LLaMA 与 BERT 以及 DeIT-S 中的 activation 的分布都发现了类似的现象:

从图中可以看到,那些异常大的 channel 都比剩余的 channel 大很多,所以在量化 activation tensor 的过程中,量化的精度很大程度会被这些异常值决定,从而抑制其他 channel 值的量化区间,最终降低整体影响量化精度。这会导致量化的最终结果崩坏,尤其当比特数降到一定程度的时候。值得注意的是,只有 tensor-wise 和 token-wise 量化可以在 efficient matrix multipilication 的时候将 scaling factor 提取出来,而 channel-wise 量化是不支持 efficient matrix multipilication 的,见下图。

为了解决这个问题,同时维持高效率矩阵乘法 (Efficient Matrix Multiplication),本文利用少量的校正资料集,预先算出 activation 的每个 channel 的最大值,从而计算缩放因子。然后将这个缩放因子一拆为二,拆解成一个 per-tensor 的实数乘以 per-channel 的 2 的幂。而这个 2 的整数次方即用 FP 里的 exponent bias 表示。完整的过程可以用以下公式表示:

进一步地,在 calibration 完成之后,这个 per-channel exponent bias 就不再变化,因此可以和 weight quantization 一起进行预计算 (pre-compute),将这个 per-channel exponent bias 整合进量化后的 weights 中,提高量化精度。完整的过程如以下公式:

可以看到在 pre-shifted 后,原本 activation 中的 full-precision per-channel biases 的位置变成了一个 tensor-wise 的实数 scaling factor ,而被拆解出来的整数 per-channel biases 被移到了 weight 中原本 integer bias 的位置,如公式 4。     

从而这个方法 (pre-shifted exponent bias) 能在维持 efficient matrix multiplication 的原则下,更好得提高量化精度,方法的直观展示如下图所示:

最后本文展示 Floating Point Quantization (FPQ) 方法,在 LLaMA, BERT 以及 ViTs 模型上,4-bit 量化皆取得了远超 SOTA 的结果。特别是,这篇文章展示了 4-bit 量化的 LLaMA-13B 模型,在零样本推理任务上达到平均 63.1 的分数,只比完整精度模型低了 5.8 分,且比之前的 SOTA 方法平滑量高出了 12.7,这是目前少数已知可行的 4-bit 量化方案了。

#LLM~端侧方案

端侧LLM毫无疑问会成为各手机厂商在2024年的主战场。从国内各手机厂透露的信息来看,大家几乎都把希望寄托在了芯片厂身上,自身能做的、会做的工作太少。希望苹果的工作对国内厂商们有启发、借鉴意义。

论文链接:LLM in a flash: Efficient Large Language Model Inference with Limited Memory

1. Flash Memory and DRAM

在移动端设备中(如手机),DRAM可理解为“运行时内存”,Flash Memory可理解为“存储空间”。做一个简单的类比,在PC中,DRAM对应于内存;Flash Memory对应于硬盘存储(注意:仅仅是对应于,实现方案并不一样)。

在通常的LLM推理阶段,LLM都是直接加载到DRAM中的。一个7B半精度LLM,完全加载进DRAM所需的存储空间超过14GB。考虑到目前主流手机的DRAM最高也就16GB的水平,在端侧直接使用DRAM来加载7B LLM面临巨大挑战。

图1给出了一个移动端标准的存储结构示意图。

图1 移动端存储结构示意图

Flash Memory的特点是大存储,低带宽。也就是说,Flash Memory可存储的内容多(图中的100GB),但数据传输速率低(图中的1GB/s)。而DRAM的特点是小存储,高带宽。

现在的问题是:模型大小 > DRAM,所以无法将模型全部加载进DRAM。

苹果的解决方案是将LLM放在Flash Memory中,在每次需要进行推理时,仅仅将部分必要参数加载到DRAM中

苹果的整个方案重点解决两个问题:

  • 如何快速识别出哪些模型参数是必要的
  • 考虑到由Flash memory到DRAM的带宽较低,如何加快由Flash memory到DRAM的传输效率
    论文中从三个不同方面做了尝试,下面分别介绍。

2. 减少数据传输量

这部分介绍论文中采用的三种降低数据传输量的方法。

2.1 方法一:Selective Persistence Strategy

对于常见的LLM而言,它的模型参数主要由Attention参数MLP参数两部分构成,其中Attention参数占比约为1/3,MLP参数占比约为2/3。除此,还有参数量级可忽略不计的Embedding层的参数。

因为Attention参数量相对较少,所以苹果的方案是将Attention参数和Embedding层的参数直接加载到DRAM中。

这就是所谓的Selective Persistence Strategy,其意为:有选择性地把部分参数常驻在DRAM中。而这部分常驻的参数就是Attention参数和Embedding参数。原因是因为它们占比较小。

2.2 方法二:Anticipating ReLU Sparsity

这里主要借鉴了DejaVu的思路:MLP层的输出只有不到10%的值是激活状态(不为0)。一般把这种现象称为稀疏性。稀疏性越强,则非激活状态的值就越多。

所以我们也可把这句话“MLP层的输出只有不到10%的值是激活状态”简写作“MLP层的稀疏性超过90%”。

要注意,此处的稀疏性一般称为“Contextual Sparsity”。也就是说,MLP层的哪些神经元会激活,与当前的输入相关

苹果照搬了DejaVu的方法,使用一个两层MLP来预测哪些神经元会激活。方法也很简单,假设神经元个数为4096,只需要将MLP的输出层的大小设为4096,并为每一个输出使用sigmoid来做一个二分类即可(“选择”or“不选择”)。

注意1:不同Transformer层使用的预测模型不同。

注意2:同一个Transformer层中的MLP一般有两层。它们的激活神经元始终保持相同。

在能够准确预测的前提下,每次在推理时动态加载预测为激活神经元对应的参数即可。

这里有对DejaVu详细介绍的文章:[ICML'23] DejaVu:LLM中的动态剪枝

2.3 方法三:Sliding Window

根据2.2小节中介绍的稀疏性可知,在每一次LLM进行前向推理时,它都需要使用模型预测每一个MLP层中激活神经元的编号,并将所需的神经元所对应的权重由Flash memory加载到DRAM中。

因为LLM的推理阶段是逐token进行的,这意味着在生成不同token的时候,需要加载到DRAM中的MLP的参数也不同。

这就是Sliding Window的核心思想:保留处理过去k个token时的激活神经元所对应的参数在DRAM中,并在处理当前token时只对:1)部分多余的参数进行删除;2)缺少的参数进行加载。图2是原文中的示意图。

图2 Sliding Window示意图

图中上图表示在处理当前token “Was”之前,前5个token(k=5)的激活神经元(淡蓝色偏绿部分)。图中下图表示在处理当前token “Was”之时,需要新加入的神经元(蓝色部分)和需要删除的神经元(分红部分)。

Sliding Window的核心假设是LLM在处理相邻token时产生的稀疏性具有相似性。原文没有仔细分析和论证这个假设。

3 提高传输吞吐量

3.1 Bundling Columns and Rows

通常LLM中的MLP层包含两个全连层。在忽略激活函数的情况下,这两个全连层可以写为:

图3 将两个全连层的列与行拼接存储

这样做的好处是原本需要两次I/O,现在只需要一次了。虽然总的数据读取量并没有变,但读取大块、连续的数据会比读取大量小块、非连续数据更加高效,因此整体传输吞吐量提升了。

3.2 Bundling Based on Co-activation

这是一个原文尝试过,但被验证为无效的策略。既然原文提到了,所以这里也罗列出来。

原文中猜测某些神经元之间可能存在一些紧密联系。比如对于两个神经元a和b,当a激活时,b也会激活(或者当b激活时,a也会激活)。

因此可以通过分析来找到每个神经元的“closest friend”(与该神经元同时激活频率最高的其它某个神经元)。然后在存储Flash memory中存储时,也将它们的参数拼接存在一起。这样的读取效率更高。

但该方法之所以无效,主要原因是可能会存在某个神经元i,它是其它很多神经元的“closest friend”。这样导致的问题则是神经元i被额外传输了太多次,导致实际的I/O成本增加了。

4 Optimized Data Management in DRAM

虽然DRAM的数据读取速度比Flash memory快很多,但当需要对其中数据进行大量、高频读写时,它的时耗仍然不可忽略。在本文介绍的内容中,对DRAM的读写主要发生在对MLP层中所需神经元对应参数的删除与新增(参考图2)。

为此,论文中设计了一种特殊的数据结构来对DRAM中的数据做精细化管理。该数据结构的核心变量如下:

  • Matrix:按照“Bundling Columns and Rows”的方法存储激活神经元所对应的参数
  • bias:激活神经元所对应的bias参数
  • num_used:激活神经元的个数
  • last_k_active:过去k个token所对应的激活神经元编号
  • Pointer:当前行参数对应的神经元编号

图4 Optimized Data Management in DRAM

通过预分配一个足够大的空间,可以避免因反复分配而导致的额外开销。下面来说明基于该数据结构的一些操作的高效实现方法。

该矩阵中的行对应的是当前存储在DRAM中激活神经元的参数。前文提到(2.3小节),当处理新的token时,需要将不会被激活的神经元删除,并添加新的会被激活的神经元。所以最重要的两个操作是“删除”和“新增”。

当需要删除某个神经元时(如图4中左图标红部分,对应的是编号为10的神经元),只需将num_rows的数量减1,并将最后一行Copy至被删除行,结果如图4的中图所示。虚线框表示当前有效的数据。

当需要“新增”时,直接将其对应的参数由Flash memory中copy至该矩阵中即可,无需额外分配存储空间。

5. 实验结果

苹果这篇paper的主要关注点在于:让LLM在运行时内存受限的情况下能高效地跑起来。所以论文的实验主要对比了各种情况下I/O导致的时耗,如下图所示。

图5 实验结果

图5中的实验使用的是OPT 6.7B模型,半精度。表中第一行和第二行都是基准baseline。第一行假设初始模型全部在Flash memory中,那么为了完成一次完整的推理,需要将模型全部加载一遍,整个I/O耗时2130ms。

第二行对应于假设模型有一半参数提前在DRAM中的整个加载耗时。

第三行到第五行对应于应用了Predictor(2.2小节)、Windowing(2.3小节)和Bundling(3.1小节)后对应的耗时。

效率提升非常明显。

#LLM推理MLSys优化

近日,CMU Catalyst 团队推出了一篇关于高效 LLM 推理的综述,覆盖了 300 余篇相关论文,从 MLSys 的研究视角介绍了算法创新和系统优化两个方面的相关进展。

在人工智能(AI)的快速发展背景下,大语言模型(LLMs)凭借其在语言相关任务上的杰出表现,已成为 AI 领域的重要推动力。然而,随着这些模型在各种应用中的普及,它们的复杂性和规模也为其部署和服务带来了前所未有的挑战。LLM 部署和服务面临着密集的计算强度和巨大的内存消耗,特别是在要求低延迟和高吞吐量的场景中,如何提高 LLM 服务效率,降低其部署成本,已经成为了当前 AI 和系统领域亟需解决的问题。

来自卡内基梅隆大学的 Catalyst 团队在他们的最新综述论文中,从机器学习系统(MLSys)的研究视角出发,详细分析了从前沿的 LLM 推理算法系统的革命性变革,以应对这些挑战。该综述旨在提供对高效 LLM 服务的当前状态和未来方向的全面理解,为研究者和实践者提供了宝贵的洞见,帮助他们克服有效 LLM 部署的障碍,从而重塑 AI 的未来。

论文链接:https://arxiv.org/abs/2312.15234

该论文的第一作者是卡内基梅隆大学的 Xupeng Miao(苗旭鹏)博士后研究员,合作者还包括 Tianqi Chen 和 Zhihao Jia 助理教授。此外,其他学生作者也均来自于 CMU Catalyst Group 实验室,该实验室由 Zhihao Jia 与 Tianqi Chen(陈天奇)在 CMU 共同主持,致力于集成来自于机器学习算法、系统、硬件等多方面的优化技术,构造自动化的机器学习系统。此前,该实验室还推出了 SpecInfer, MLC-LLM, SpotServe [ASPLOS‘24] 等开源项目,推进 LLM 大模型相关系统的研究和应用。实验室主页:https://catalyst.cs.cmu.edu。

综述概览

该综述系统地审视了现有 LLM 推理技术,覆盖了 300 余篇相关论文,从算法创新系统优化两个方面展开介绍。论文以此为基础,对现有工作设计了一套清晰且详尽的分类法,突出了各种方法的优势和局限性,逐类别搜集整理并介绍了每种方法的相关论文。除此之外,论文还对当前的主流 LLM 推理框架在系统设计与实现方面进行了深入的对比和分析。最后,作者对未来如何继续提高 LLM 推理效率进行了展望,在技术层面提出了六大潜在发展方向

分类法

算法创新

这一节对提出的各种算法和技术进行了全面分析,旨在改进大规模 Transformer 模型推理的原生性能缺陷,包括解码算法架构设计、和模型压缩等等。

解码算法:在这一部分中,我们回顾了在图 2 中展示的几种 LLMs 推理优化过程的新颖解码算法。这些算法旨在减少计算复杂度,并提高语言模型推理在生成任务中的总体效率,包括:

  • 非自回归解码:现有 LLMs 的一个主要限制是默认的自回归解码机制,它逐个顺序生成输出 token。为解决这一问题,一种代表性的工作方向是非自回归解码 [97, 104, 108,271],即放弃自回归生成范式,打破单词依赖并假设一定程度的条件独立性,并行解码输出 token。然而,尽管这类方法解码速度有所提高,但大多数非自回归方法的输出质量仍不如自回归方法可靠。
  • 投机式推理:另一类工作是通过投机执行思想 [47] 实现并行解码。自回归 LLM 推理过程中的每个解码步骤都可以被视为带有条件分支的程序执行语句,即决定接下来生成哪个 token。投机式推理 [51, 155] 先使用较小的草稿模型进行多步解码预测,然后让 LLM 同时验证这些预测以实现加速。然而,将投机解码应用于 LLMs 时仍然存在一些实际挑战,例如,如何使解码预测足够轻量且准确,以及如何借助 LLMs 实现高效的并行验证。SpecInfer [177] 首次引入基于 tree-based speculative decoding 和 tree attention,并提出了一个低延迟 LLM 服务系统实现,该机制也被后续多个工作 [48, 118, 168, 185, 229, 236, 274, 310] 直接采用。
  • 提前退出:这类方法主要利用 LLMs 的深层多层结构,在中间层提前推出推理,中间层输出可以通过分类器转化成输出的 token,从而降低推理开销 [117, 147, 163, 167, 234, 272, 282, 291, 308],它们也被称为自适应计算 [68, 219]。
  • 级联推理:这类方法级联了多个不同规模的 LLM 模型,用于分别处理不同复杂度的推理请求,代表性工作包括 CascadeBERT [157] 和 FrugalGPT [53]。

架构设计:

  • 配置缩小:直接缩小模型配置。
  • 注意力简化:最近出现了很多研究工作,它们主要是将之前的长序列高效注意力机制 [240] 应用在 LLM 上,以缩短上下文,减少 KV 缓存,以及注意力复杂度,同时略微降低解码质量(如滑动窗口 [129, 299]、哈希 [198]、dilated [74]、动态选择等等)。表 1 中总结了一些近期的热门方法和之前的工作之间的对应关系。

  • 激活共享:这类方法主要是通过共享 attention 计算的中间激活来降低推理内存开销,代表性工作包括 MQA [220] 和 GQA [32]。
  • 条件计算:这类方法主要是指稀疏专家混合模型(Sparse MoE),比如最近大火的 Mistrial 7Bx8 模型就属于此类。
  • 循环单元:尽管 Transformer 已经替代了 RNN 模型,但考虑到注意力机制的二次复杂性,人们始终未曾放弃将 recurrent unit 机制重新引入 LLM 的尝试,比如 RWKV [200]、RetNet [235],以及状态空间模型 [91, 102, 103, 176] 等等。 

模型压缩:

  • 知识蒸馏:这类方法以大型的教师模型为监督,训练一个小型的学生模型。大多数之前的方法都在探索白盒蒸馏 [106, 133, 214, 233, 255],需要访问整个教师模型的参数。由于基于 API 的 LLM 服务(如 ChatGPT)的出现,一些黑盒蒸馏模型吸引了很多关注 [238,59, 273, 201, 313],这些模型通常具有更少的模型参数,与原始 LLMs(如 GPT-4 [195])相比,在各种下游任务上表现出了相当的性能。
  • 网络剪枝:过去几年中,网络剪枝方法 [180, 215, 215] 已被广泛研究,但并非所有方法都可以直接应用于 LLMs,需要考虑重新训练可能带来的过高计算成本,以及评估剪枝是否可以在底层系统实现上取得效率提升。大致上可以分为结构化剪枝 [80, 149, 174, 216, 172] 和半结构化稀疏化 [40, 87, 232, 251, 276] 等。

系统优化

本节研究 LLM 推理系统优化技术,以加速 LLM 推理,而不改变 LLM 计算语义。这一工作的目标是通过改进用于大型语言模型推理的底层系统和框架来提高系统效率,包括低比特量化、并行计算、内存管理、请求调度、和内核优化等等,详细内容可以参见论文原文。

软件框架

论文还对一些目前最先进的基于 GPU 的开源 LLM 推理系统进行了深入的分析,并从多个方面总结了它们在设计与实现伤的差异。

未来方向

  • 专用硬件加速器的发展:生成型 LLM 服务效率的显著提升可能在很大程度上依赖于专用硬件加速器的发展和提升,尤其是软硬协同设计方法。例如,让内存单元更加接近处理单元,或是针对 LLM 算法数据流优化芯片架构,这些硬件优化可以在很大程度上为 LLM 推理在软件层面带来便利和机会。
  • 高效有效的解码算法:开发更高效的解码算法可以显著提高服务效率。受对实时应用更快生成速度的需求驱动,一个有前途的方向是广义的投机式推理(generalized speculative inference),不仅会带来显著加速,同时保持相同的生成质量。正如 SpecInfer 中所指出的,广义的投机式推理中,用于生成草稿 token 的小模型可以被替换为任何快速的 token 生成方法,比如自定义函数、召回方法、甚至早停机制和非自回归解码等等。
  • 长上下文 / 序列场景优化:随着应用场景变得更加复杂,处理更长的上下文或序列的需求不断增长。服务长序列负载的 LLM 需要解决算法和系统两方面的挑战。在算法方面,它们依然面临长度泛化失效问题,甚至可能出现 “loss in the middle” 的情况。目前的解法主要是通过召回增强、序列压缩和缓存来尽可能缩短序列长度并保存相关信息。
  • 探索替代基础架构:尽管 Transformer 模型和自注意力机制目前主导着 LLM 领域,但探索替代架构是未来研究的一个有前景的方向。例如,一些最新研究探索了无注意力方法,使用纯 MLP(多层感知机)架构来替代注意力机制,可能会改变目前 LLM 推理优化的格局。
  • 在复杂环境中的部署探索:随着 LLM 应用的扩展,探索并优化它们在各种复杂环境中的部署成为一个关键的未来方向。这一探索不仅限于传统的基于云的部署,还包括边缘计算、混合计算(cloud+edge)、去中心化计算以及廉价的可抢占资源等。
  • 特定需求的自动适应:应用特定需求的多样性创造了一系列创新的 LLM 服务优化机会,例如模型微调(parameter-efficient fine-tuning)、向量数据库检索、多模态负载等等。这些独特的挑战也要求将 LLM 服务技术自动且顺利地集成到现有 IT 基础设施中,将优化空间扩展到整个 LLM 生命周期。

总结

总的来说,该综述不仅是对当前 LLM 服务优化研究的全面概述,也为未来在这一领域的探索和发展指明了方向。通过深入了解这些先进的解决方案,研究者和实践者可以更好地理解和应对在实际应用中部署大型语言模型时面临的挑战。

#大模型LLM~技术精要

抛开是否有财力做超大型LLM这个因素,如果单从技术角度看,国内外的差距主要来自于对LLM的认知以及未来应往何处去的发展理念的不同。

ChatGPT出现后惊喜或惊醒了很多人。惊喜是因为没想到大型语言模型(LLM,Large Language Model)效果能好成这样;惊醒是顿悟到我们对LLM的认知及发展理念,距离世界最先进的想法,差得有点远。我属于既惊喜又惊醒的那一批,也是典型的中国人,中国人善于自我反思,于是开始反思,而这篇文章正是反思的结果。

实话实说,国内在LLM模型相关技术方面,此刻,距离最先进技术的差距进一步加大了。技术领先或技术差距这事情,我觉得要动态地以发展的眼光来看。在Bert出现之后的一到两年间,其实国内在这块的技术追赶速度还是很快的,也提出了一些很好的改进模型,差距拉开的分水岭应该是在 GPT 3.0出来之后,也就是2020年年中左右。在当时,其实只有很少的人觉察到:GPT 3.0它不仅仅是一项具体的技术,其实体现的是LLM应该往何处去的一个发展理念。自此之后,差距拉得越来越远,ChatGPT只是这种发展理念差异的一个自然结果。所以,我个人认为,抛开是否有财力做超大型LLM这个因素,如果单从技术角度看,差距主要来自于对LLM的认知以及未来应往何处去的发展理念的不同。

国内被国外技术甩得越来越远,这个是事实,不承认也不行。前阵子网上很多人担忧说国内AI现在处于“危急存亡之秋”,我觉得倒也不至于这么严重。君不见,这个世界上,具备这么超前眼光的只有OpenAI一家吗?包括Google在内,其实对于LLM发展理念的理解,明显都落后OpenAI一个身位。现实是OpenAI表现过于优秀,把所有人都甩开了,不仅仅是国内。

我觉得,OpenAI对LLM在理念及相关技术方面,领先国外的Google、DeepMind大约半年到一年的时间,领先国内大概两年左右的时间。在LLM这个事情上,感觉梯队很明显,Google应该是排在第二位,最能体现Google技术眼光的是PaLM和Pathways,推出时间大概在22年2月到4月间,同一时期,OpenAI推出的却是InstructGPT,从这里就可以看出Google和OpenAI的差距了,至于为何这么说,你看了我后面的正文后大概能理解。DeepMind之前的重心一直在强化学习攻克游戏和AI for science这些方面,切入LLM其实很晚,应该是21年才开始重视这个方向,目前也处于追赶状态。Meta就更不用说了,重心一直不在LLM上,目前感觉也发力开始追赶。这还是目前做得最好的一批机构,尚且如此,更何况国内呢?我觉得情有可原。至于OpenAI关于LLM的理念是什么,我在本文的最后一部分,会谈谈我的认知。

本文梳理自GPT 3.0出现之后的主流LLM技术,在此之前的主流技术可以参考:

​​https://zhuanlan.zhihu.com/p/254821426​​

我相信看完这两篇文章,能够让您对LLM领域的技术脉络,LLM技术发展过程中出现过的不同发展理念,乃至未来可能的发展趋势,有比较清晰的认知。当然,很多地方讲的内容是我个人看法,有很大的主观性,错漏难免,所以还请谨慎参考。

本文试图回答下面一些问题:ChatGPT是否带来了NLP乃至AI领域的研究范式转换?如果是,那会带来怎样的影响?LLM从海量数据中学到了什么知识?LLM又是如何存取这些知识的?随着LLM规模逐步增大,会带来什么影响?什么是In Context Learning?为什么它是一项很神秘的技术?它和Instruct又是什么关系?LLM具备推理能力吗?思维链CoT又是怎么做的?等等,相信看完,能让您对这些问题有一个答案。

首先,在谈LLM技术现状前,先宏观地谈下我心目中的研究范式转换问题。这样,我们才能“先见森林,再见树木”,对具体技术为何会是如此变化有个更清晰的认知。

潮流之巅:NLP研究范式的转换

如果我们把时间线往前拉得更长一些,回到NLP领域的深度学习时代,在更长时间窗口内观察技术变迁及其影响,可能会更容易看清其中的一些关键节点。我个人认为,在最近10年来NLP领域的技术发展过程中,可能存在两次大的研究范型转换。

范式转换1.0:从深度学习到两阶段预训练模型

这个范式转换所涵盖的时间范围,大致在深度学习引入NLP领域(2013年左右),到GPT 3.0出现之前(2020年5月左右)。

在Bert和GPT模型出现之前,NLP领域流行的技术是深度学习模型,而NLP领域的深度学习,主要依托于以下几项关键技术:以大量的改进LSTM模型及少量的改进CNN模型作为典型的特征抽取器;以Sequence to Sequence(或叫encoder-decoder亦可)+Attention作为各种具体任务典型的总体技术框架。

在这些核心技术加持下,NLP领域深度学习的主要研究目标,如果归纳一下,是如何有效增加模型层深或模型参数容量。就是说,怎么才能往encoder和decoder里不断叠加更深的LSTM或CNN层,来达成增加层深和模型容量的目标。这种努力,尽管确实不断增加了模型层深,但是从解决具体任务的效果角度看,总体而言,不算很成功,或者说和非深度学习方法相对,带来的优势不算大。

深度学习之所以不够成功,我认为主要原因来自于两个方面:一方面是某个具体任务有限的训练数据总量。随着模型容量的增加,需要靠更大量的训练数据来支撑,否则即使你能把深度做起来,任务效果也做不上去。而在预训练模型出现之前,很明显这是NLP研究领域一个严重问题;另外一个方面是LSTM/CNN特征抽取器,表达能力不够强。意思是就算给你再多的数据也没用,因为你不能有效地吸收数据里蕴含的知识。主要应该是这两个原因,阻碍了深度学习在NLP领域的成功突围。

Bert/GPT这两个预训练模型的出现,无论在学术研究角度看,还是工业应用角度来看,都代表了NLP领域的一个技术飞跃,并带来了整个领域研究范式的转换。这种范式转换带来的影响,体现在两个方面:首先,是部分NLP研究子领域的衰退乃至逐步消亡;其次,NLP不同子领域的技术方法和技术框架日趋统一,在Bert出现后一年左右,技术栈基本收敛到两种技术模式中。关于这两点,我们分头来谈。

影响一:中间任务的消亡

NLP是一个宏观研究领域的统称,里面有五花八门具体的子领域与子方向,如果仔细分析,从任务的性质角度,可以把这些任务分成两大类:一类可以叫做“中间任务”,一类可以称为“最终任务”。

典型的中间任务包括:中文分词、词性标注、NER、句法分析、指代消解、语义Parser等,这类任务一般并不解决应用中的实际需求,大多数是作为那些解决实际需求任务的中间阶段或者辅助阶段存在的,比如几乎没有需求说,我要一个句法Parser,把这个句子的句法分析树给用户看看,用户不需要看到这些NLP的中间阶段处理结果,他只关心某个具体任务你有没有干好。“最终任务”包括比如文本分类、文本相似性计算、机器翻译、文本摘要等等,有很多。这类任务的特点是每个子领域都解决某个实际需求,任务结果基本能直接呈现给用户,比如用户确实存在给你一句英文,告诉他中文是什么的需求。

按理说,“中间任务”就不应该出现,而之所以会存在,这是NLP技术发展水平不够高的一种体现。在技术发展早期阶段,因为当时的技术相对落后,很难一步做好有难度的最终任务。比如机器翻译,早期技术要做好机器翻译是很困难的,于是科研人员就把难题分而治之,分解成分词、词性标注、句法分析等各种中间阶段,先把每个中间阶段做好,然后再拼起来完成最终任务,这也是没办法的事情。

但是自从Bert/GPT出现之后,其实就没有必要做这些中间任务了,因为通过大量数据的预训练,Bert/GPT已经把这些中间任务作为语言学特征,吸收到了Transformer的参数里,此时我们完全可以端到端地直接解决那些最终任务,而无须对这种中间过程专门建模。这里可能争议最大的是中文分词,其实道理也是一样的,哪些字应该组成一个词,这个其实你不用管,让LLM自己当特征去学就行了,只要对于解决任务有帮助,它自然会去学该学的合理分词方式,也未必一定要和我们人类理解的分词规则相同。

基于以上认知,其实在Bert/GPT一出现,你就应该得出这类NLP的中间阶段的任务,会逐步退出历史舞台这个结论。

影响二:不同研究方向技术路线的统一

在说明具体影响前,我们先讨论下另外一种NLP任务划分方式,这对于理解后面内容有帮助。如果对“最终任务”进一步进行分类,又大致可以分为两大不同类型的任务:自然语言理解类任务和自然语言生成类任务。如果排除掉“中间任务”的话,典型的自然语言理解类任务包括文本分类、句子关系判断、情感倾向判断等,这种任务本质上都是分类任务,就是说输入一个句子(文章),或者两个句子,模型参考所有输入内容,最后给出属于哪个类别的判断。自然语言生成也包含很多NLP研究子方向,比如聊天机器人、机器翻译、文本摘要、问答系统等。生成类任务的特点是给定输入文本,对应地,模型要生成一串输出文本。这两者的差异主要体现在输入输出形式上

自从Bert/GPT模型诞生后,出现了明显的技术统一趋向。首先,NLP中不同的子领域,其特征抽取器都逐渐从LSTM/CNN统一到Transformer上。其实,自Bert公开后不久,就应该意识到,这必然会成为技术趋势。至于其原因,在几年前我写的这篇:

放弃幻想,全面拥抱Transformer:自然语言处理三大特征抽取器(CNN/RNN/TF)比较

​​https://zhuanlan.zhihu.com/p/54743941​​

中做了说明和分析,感兴趣的同学可参考。而且,目前Transformer不仅统一了NLP诸多领域,也正在逐步地替换图像处理各种任务中被广泛使用的CNN等其它模型的进程之中,类似的,多模态模型目前也基本都采用了Transformer模型。这种Transformer从NLP出发,攻城略地逐步统一AI越来越多领域的趋势,起始于2020年底出现的Vision Transformer (ViT) ,之后蓬勃发展,到目前已大获成功,且其继续向更多领域拓展的势头会越来越迅猛。

其次,大多数NLP子领域的研发模式切换到了两阶段模式:模型预训练阶段+应用微调(Fine-tuning)或应用Zero/Few Shot Prompt模式。更准确地说,NLP各种任务其实收敛到了两个不同的预训练模型框架里:对于自然语言理解类任务,其技术体系统一到了以Bert为代表的“双向语言模型预训练+应用Fine-tuning”模式;而对于自然语言生成类任务,其技术体系则统一到了以GPT 2.0为代表的“自回归语言模型(即从左到右单向语言模型)+Zero /Few Shot Prompt”模式。至于为何会分化成两条技术路线,有其必然性,关于这点我们放在后面解释。

这两种模式,看似比较相像,但其背后蕴含了迥异的发展思路,也会导向不同的未来发展方向。不过遗憾的是,我们中的绝大多数人,在当时都低估了GPT 这条发展路线的潜力,而把视觉中心聚焦到了Bert这种模式上。

范式转换2.0: 从预训练模型走向通用人工智能 (AGI,Artificial General Intelligence)

这个范式转换所涵盖的时间范围,大致在GPT3.0出现之后(20年6月左右),一直到目前为止,我们应该正处于这个范式转换过程中。

ChatGPT是触发这次范型转换的关键节点,但是在InstructGPT出现之前,其实LLM处于这次范式转换前的一个过渡期。

过渡期:以GPT 3.0为代表的“自回归语言模型+Prompting”模式占据统治地位

前面说过,在预训练模型发展的早期,技术框架收敛到了Bert模式和GPT模式这两种不同的技术范型,而且人们普遍更看好Bert模式一些,相当多数的后续技术改进,都是沿着Bert那条路走的。但是,随着技术的继续发展,你会发现,目前规模最大的LLM模型,几乎清一色都是类似GPT 3.0这种“自回归语言模型+Prompting”模式的,比如GPT 3、PaLM、GLaM、Gopher、Chinchilla、MT-NLG、LaMDA等,没有例外。为什么会这样呢?背后一定有其必然性,我认为可能主要源于两个原因。

首先,Google的T5模型,在形式上统一了自然语言理解和自然语言生成任务的外在表现形式。如上图所示,标为红色的是个文本分类问题,黄色的是判断句子相似性的回归或分类问题,这都是典型的自然语言理解问题。在T5模型里,这些自然语言理解问题在输入输出形式上和生成问题保持了一致,也就是说,可以把分类问题转换成让LLM模型生成对应类别的字符串,这样理解和生成任务在表现形式就实现了完全的统一。

这说明自然语言生成任务,在表现形式上可以兼容自然语言理解任务,若反过来,则很难做到这一点。这样的好处是:同一个LLM生成模型,可以解决几乎所有NLP问题。而如果仍然采取Bert模式,则这个LLM模型无法很好处理生成任务。既然这样,我们当然倾向于使用生成模型,这是一个原因。

第二个原因,如果想要以零示例提示语(zero shot prompting)或少数示例提示语(few shot prompting)的方式做好任务,则必须要采取GPT模式。现在已有研究(参考:On the Role of Bidirectionality in Language Model Pre-Training)证明:如果是以fine-tuning方式解决下游任务,Bert模式的效果优于GPT模式;若是以zero shot/few shot prompting这种模式解决下游任务,则GPT模式效果要优于Bert模式。这说明了,生成模型更容易做好zero shot/few shot prompting方式的任务,而Bert模式以这种方式做任务,是天然有劣势的。这是第二个原因。

但是问题来了:为什么我们要追求zero shot/few shot prompting这种方式来做任务呢?要解释清楚这个问题,我们首先需要搞清楚另外一个问题:什么样的LLM模型,对我们是最理想的?

上图展示了一个理想的LLM该有的样子。首先,LLM应该具备强大的自主学习能力。假设我们把世界上能获得的所有文本或者图片等不同类型的数据喂给它,它应该能够自动从中学习到里面包含的所有知识点,学习过程不需要人的介入,并且能灵活应用所学知识,来解决实际问题。因为数据是海量的,要吸收所有知识,就要非常多的模型参数来存储知识,所以这个模型必然会是一个巨无霸模型。

其次,LLM应该能解决NLP任何子领域的问题,而不仅支持有限领域,甚至它应该可以响应NLP之外其它领域的问题,最好是任意领域的问题都能得到很好地回答。

再者,当我们使用LLM解决某个具体领域问题的时候,应该用我们人类习惯的表达方式,就是说LLM应该理解人类的命令。这体现出让LLM适配人,而不是反过来,让人去适配LLM模型。人适配LLM的典型例子,比如绞尽脑汁去尝试各种不同的prompt,以试图找到好的提示语,才能很好地解决手头问题。关于这点,上图在人类和LLM交互的接口层,举了几个例子,说明什么是好的人使用LLM模型的接口形式。

看完这个理想中的LLM,我们再回头解释上面遗留的问题:为什么我们要追求zero shot/few shot prompting这种方式来做任务呢?有两个原因。

第一,这个LLM模型规模必然非常巨大,有能力作出这个模型,或改动这个模型参数的机构必然很少。而任务需求方是千千万万的中小机构甚至是个人,就算你把模型开源出来,他们也无力部署这个模型,更不用说再用Fine-tuning这种模式去修改模型参数了。所以,我们应该追求不修正模型参数,就能让任务需求方完成任务的方式,也就是应该采取prompt模式完成任务,而非Fine-tuning模式(由此可看出,soft prompting技术方向是违背这个发展趋势的)。模型制作方则将LLM作成公用服务,以LLM as Service的模式运行。作为服务支持方,考虑到千变万化的用户需求,所以LLM模型制作方更要追求让LLM能完成尽可能多类型的任务,这是附带的影响,也是为何超级大模型一定会追求走向AGI的现实因素。

第二,zero shot prompting也好,few shot prompting也好,甚至促进LLM推理能力的思维链(CoT,Chain of Thought)Prompting也好,就是上图中接口层中的现有技术。具体而言,zero shot prompting的初衷,其实就是人类和LLM的理想接口,直接用人类所习惯的任务表述方式让LLM做事情,但是发现LLM并不能很好地理解,效果也不好。经过继续研究,转而发现:对于某项任务,如果给LLM几个示例,用这些示例来代表任务描述,效果会比zero shot prompting好,于是大家都去研究更好的few shot prompting技术。可以理解为,本来我们希望LLM能够用人类常用的命令方式来执行某个任务,但是目前技术还做不到,所以退而求其次,用这些替代技术来表达人类的任务需求。

如果理解了上述逻辑,很容易得出如下结论:few shot prompting(也被称为In Context Learning)只是一种过渡时期的技术。如果我们能够更自然地去描述一个任务,而且LLM可以理解,那么,我们肯定会毫不犹豫地抛弃这些过渡期的技术,原因很明显,用这些方法来描述任务需求,并不符合人类的使用习惯。

这也是为何我将GPT 3.0+Prompting列为过渡期技术的原因,ChatGPT的出现,改变了这个现状,用Instruct取代了Prompting,由此带来新的技术范式转换,并产生若干后续影响。

影响一:让LLM适配人的新型交互接口

在理想LLM的背景下,我们再来看ChatGPT,能更好理解它的技术贡献。ChatGPT应该是目前所有的现有技术里,最接近理想LLM的技术方法。如果归纳下ChatGPT最突出特点的话,我会用下面八个字:“能力强大,善解人意”。

“能力强大”这一点,我相信应该主要归功于ChatGPT所依托的基础LLM GPT3.5。因为ChatGPT 尽管加入了人工标注数据,但是量级只有数万,这个规模的数据量,和训练GPT 3.5模型使用的几千亿token级别的数据量相比,包含的世界知识(数据中包含的事实与常识)可谓沧海一粟,几可忽略,基本不会对增强GPT 3.5的基础能力发挥什么作用。所以它的强大功能,应该主要来自于隐藏在背后的GPT 3.5。GPT 3.5对标理想LLM模型中的那个巨无霸模型。

那么,ChatGPT向GPT 3.5模型注入新知识了吗?应该是注入了,这些知识就包含在几万人工标注数据里,不过注入的不是世界知识,而是人类偏好知识。所谓“人类偏好”,包含几方面的含义:首先,是人类表达一个任务的习惯说法。比如,人习惯说:“把下面句子从中文翻译成英文”,以此表达一个“机器翻译”的需求,但是LLM又不是人,它怎么会理解这句话到底是什么意思呢?你得想办法让LLM理解这句命令的含义,并正确执行。所以,ChatGPT通过人工标注数据,向GPT 3.5注入了这类知识,方便LLM理解人的命令,这是它“善解人意”的关键。其次,对于什么是好的回答,什么是不好的回答,人类有自己的标准,例如比较详细的回答是好的,带有歧视内容的回答是不好的,诸如此类。这是人类自身对回答质量好坏的偏好。人通过Reward Model反馈给LLM的数据里,包含这类信息。总体而言,ChatGPT把人类偏好知识注入GPT 3.5,以此来获得一个听得懂人话、也比较礼貌的LLM。

可以看出,ChatGPT的最大贡献在于:基本实现了理想LLM的接口层,让LLM适配人的习惯命令表达方式,而不是反过来让人去适配LLM,绞尽脑汁地想出一个能Work的命令(这就是instruct技术出来之前,prompt技术在做的事情),而这增加了LLM的易用性和用户体验。是InstructGPT/ChatGPT首先意识到这个问题,并给出了很好的解决方案,这也是它最大的技术贡献。相对之前的few shot prompting,它是一种更符合人类表达习惯的人和LLM进行交互的人机接口技术。

而这必将启发后续的LLM模型,继续在易用人机接口方面做进一步的工作,让LLM更听话。

影响二:很多NLP子领域不再具备独立研究价值

就NLP领域而言,这次范式转换,意味着很多目前独立存在的NLP研究领域,将被纳入LLM的技术体系,进而不再独立存在,逐步消失。经过第一次范式转换,尽管NLP中很多“中间任务”,继续作为独立研究领域存在不再必要,但是大多数“最终任务”,仍然是以独立研究领域存在的,只是切换成在“预训练+fine-tuning”框架下,面对领域独有问题,陆续提出新的改进方案。

目前研究表明,很多NLP任务,随着LLM模型规模增长,效果会大幅提升。据此,我觉得可得到如下推论:大多数某领域所谓“独有”的问题,大概率只是缺乏领域知识导致的一种外在表象,只要领域知识足够多,这个所谓领域独有的问题,就可以被很好地解决掉,其实并不需要专门针对某个具体领域问题,冥思苦想去提出专用解决方案。也许AGI的真相超乎意料地简单:你只要把这个领域更多的数据交给LLM,让它自己学习更多知识即可。

在这个背景下,同时,ChatGPT证明了我们现在是可以直接去追求理想LLM模型的,那么,未来的技术发展趋势应该是:追求规模越来越大的LLM模型,通过增加预训练数据的多样性,来涵盖越来越多的领域,LLM自主从领域数据中通过预训练过程学习领域知识,随着模型规模不断增大,很多问题随之得到解决。研究重心会投入到如何构建这个理想LLM模型,而非去解决某个领域的具体问题。这样,越来越多NLP的子领域会被纳入LLM的技术体系,进而逐步消失。

我认为,判断某个具体领域是否该立即停止独立研究,其判断标准可采取以下两种方法,占其一即可:第一,判断某个任务,是否LLM的研究效果超过人类表现,对于那些LLM效果超过人类的研究领域,已无独立研究的必要。举个例子,GLUE与SuperGLUE测试集合里的很多任务,目前LLM效果已超过人类表现,与这个数据集合密切相关的研究领域,其实就没有继续独立存在的必要。第二,对比两种模式的任务效果,第一种模式是用较大的领域专用数据进行Fine-tuning,第二种是few-shot prompting或instruct-based方法。如果第二种方法效果达到或超过第一种方法,则意味着这个领域没有继续独立存在的必要性。如果用这个标准来看,其实很多研究领域,目前fine-tuning效果还是占优的(因为这种模式领域训练数据量大),看似还可独立存在。但是考虑到很多任务随着模型规模增大,few shot prompting效果持续增长,随着更大模型的出现,这个拐点很可能短期就会达到。

如果上述猜测成立,将意味着如下残酷事实:对于很多NLP领域的研究人员,将面临往何处去的选择,是继续做领域独有问题呢?还是放弃这种看似前途不大的方式,转而去建设更好的LLM?如果选择转向去建设LLM,又有哪些机构有能力、有条件去做这个事情呢?你对这个问题的回答会是什么呢?

影响三:更多NLP之外的研究领域将被纳入LLM技术体系

如果站在AGI的视角,参照之前描述的理想LLM模型,它所能完成的任务,不应局限于NLP领域,或某一两个学科领域,理想中的LLM应该是领域无关的通用人工智能模型,它现在在某一两个领域做得好,不代表只能做这些任务。ChatGPT的出现,证明了现在这个时期,我们去追求AGI是有可行性的,而现在是抛开“领域学科”这个思维束缚的时候了。

ChatGPT除了展示出以流畅的对话形式解决各种NLP任务外,也具备强大的代码能力。很自然的,之后越来越多其它的研究领域,也会被逐步纳入LLM体系中,成为通用人工智能的一部分。

LLM从NLP向外进行领域拓展,一个自然的选择就是图像处理及多模态相关任务。目前已经有些工作在尝试把多模态融入,让LLM成为一个支持多模态输入输出的通用人机接口,典型的例子包括DeepMind的Flamingo和微软的“Language Models are General-Purpose Interfaces”,上图展示了这种方式的概念结构。

我的判断是无论是图像还是多模态,未来被融入LLM成为好用的功能,可能比我们想象的进度要慢。主要原因在于:尽管图像领域最近两年也一直在模仿Bert预训练的路子,尝试引入自监督学习,释放模型自主从图像数据中学习知识的能力,典型技术就是“对比学习”和MAE,这是两条不同的技术路线。然而,从目前效果来看,尽管取得了很大的技术进步,但貌似这条路尚未走通,这体现在图像领域预训练模型应用到下游任务,带来的效果收益,远不如Bert或GPT应用在NLP下游任务那样显著。所以,图像预处理模型仍需深入探索,以释放图像数据的潜力,而这会迟滞它们被统一到LLM大模型的时间。当然,如果哪天这条路被趟通,大概率会复现NLP领域目前的局面,就是图像处理各个研究子领域可能会逐步消失,被融入到大型LLM中来,直接完成终端任务。

除了图像与多模态,很明显,其它领域也会逐渐被纳入到理想LLM中来,这个方向方兴未艾,是具备高价值的研究主题。

以上是我对范式转换的个人思考,接下来,我们来梳理下GPT 3.0之后LLM模型的主流技术进展。如理想LLM模型所示,相关的技术其实可以分为两大类;一类是关于LLM模型如何从数据中吸收知识,也包括模型规模增长对LLM吸收知识能力带来的影响;第二类是关于人如何使用LLM内在能力来解决任务的人机接口,包括In Context Learning和Instruct两种模式。思维链(CoT)prompting这种LLM推理技术,本质上也属于In Context Learning,因为比较重要,我就把它们单独拎出来讲一下。

学习者:从无尽数据到海量知识

从目前研究结果看,Transformer是足够强大的特征抽取器,尚不需要做特别的改进。那么通过预训练过程,Transformer学到了什么?知识是如何存取的?我们又如何修正错误知识?本节讲述这方面的研究进展。

求知之路:LLM学到了什么知识

LLM从海量自由文本中学习了大量知识,如果把这些知识做粗略分类的话,可以分为语言类知识和世界知识两大类。

语言类知识指的是词法、词性、句法、语义等有助于人类或机器理解自然语言的知识。关于LLM能否捕获语言知识有较长研究历史,自从Bert出现以来就不断有相关研究,很早就有结论,各种实验充分证明LLM可以学习各种层次类型的语言学知识,这也是为何使用预训练模型后,各种语言理解类自然语言任务获得大幅效果提升的最重要原因之一。另外,各种研究也证明了浅层语言知识比如词法、词性、句法等知识存储在Transformer的低层和中层,而抽象的语言知识比如语义类知识,广泛分布在Transformer的中层和高层结构中。

世界知识指的是在这个世界上发生的一些真实事件(事实型知识,Factual Knowledge),以及一些常识性知识(Common Sense Knowledge)。比如“拜登是现任美国总统”、“拜登是美国人”、“乌克兰总统泽连斯基与美国总统拜登举行会晤”,这些都是和拜登相关的事实类知识;而“人有两只眼睛”、“太阳从东方升起”这些属于常识性知识。关于LLM模型能否学习世界知识的研究也有很多,结论也比较一致:LLM确实从训练数据中吸收了大量世界知识,而这类知识主要分布在Transformer的中层和高层,尤其聚集在中层。而且,随着Transformer模型层深增加,能够学习到的知识数量逐渐以指数级增加(可参考:BERTnesia: Investigating the capture and forgetting of knowledge in BERT)。其实,你把LLM看作是一种以模型参数体现的隐式知识图谱,如果这么理解,我认为是一点问题也没有的。

“When Do You Need Billions of Words of Pre-training Data?”这篇文章研究了预训练模型学习到的知识量与训练数据量的关系,它的结论是:对于Bert类型的语言模型来说,只用1000万到1亿单词的语料,就能学好句法语义等语言学知识,但是要学习事实类知识,则要更多的训练数据。这个结论其实也是在意料中的,毕竟语言学知识相对有限且静态,而事实类知识则数量巨大,且处于不断变化过程中。而目前研究证明了随着增加训练数据量,预训练模型在各种下游任务中效果越好,这说明了从增量的训练数据中学到的更主要是世界知识。

记忆之地:LLM如何存取知识

由上可知,LLM确实从数据中学到了很多语言类及世界知识。那么,对于某条具体的知识,LLM把它存储到了哪里?又是如何提取出来的?这也是一个有意思的问题。

显然,知识一定存储在Transformer的模型参数里。从Transformer的结构看,模型参数由两部分构成:多头注意力(MHA)部分占了大约参数总体的三分之一,三分之二的参数集中在FFN结构中。MHA主要用于计算单词或知识间的相关强度,并对全局信息进行集成,更可能是在建立知识之间的联系,大概率不会存储具体知识点,那么很容易推论出LLM模型的知识主体是存储在Transformer的FFN结构里。

但这样的定位,粒度还是太粗,无法很好回答具体某条知识是如何存储与提取的,比如 “中国的首都是北京”这条知识,以三元组表达就是<北京,is-capital-of,中国>,其中“is-capital-of”代表实体间关系。这条知识它存储在LLM的哪里呢?

“Transformer Feed-Forward Layers Are Key-Value Memories”给出了一个比较新颖的观察视角,它把Transformer的FFN看成存储大量具体知识的Key-Value存储器。如上图所示(图左是原始论文图,其实不太好理解,可以看做了注释的图右,更好理解些),FFN的第一层是个MLP宽隐层,这是Key层;第二层是MLP窄隐层,是Value层。FFN的输入层其实是某个单词对应的MHA的输出结果Embedding,也就是通过Self Attention,将整个句子有关的输入上下文集成到一起的Embedding,代表了整个输入句子的整体信息。

而且这篇文章还指出,Transformer低层对句子的表层模式作出反应,高层对语义模式作出反应,就是说低层FFN存储词法、句法等表层知识,中层和高层存储语义及事实概念知识,这和其它研究结论是一致的。

要我猜,把FFN看成Key-Value存储器这种思路,很可能不是最终的正确答案,但是距离最终正确答案的距离,估计也不太远。

知识涂改液:如何修正LLM里存储的知识

既然我们已知具体的某条世界知识存储在某个或者某些FFN节点的参数里,自然会引发另外一个问题:我们能否修正LLM模型里存储的错误或者过时的知识呢?比如对于问题:“英国的现任首相是谁?”鉴于近年来英国首相频繁更迭,你猜LLM更倾向输出“鲍里斯”还是更青睐“苏纳克”?很明显训练数据中包含“鲍里斯”的数据会更多,这种情况很大可能LLM会给出错误回答,于是我们就有修正LLM里存储的过时知识的必要性。

如果归纳下,目前有三类不同方法来修正LLM里蕴含的知识:

第一类方法从训练数据的源头来修正知识。“Towards Tracing Factual Knowledge in Language Models Back to the Training Data”这篇文章的研究目标是:对于指定的某条知识,我们是否可以定位到是哪些训练数据导致LLM学会了这条知识?答案是肯定的,这意味着我们可以逆向追踪到某条知识对应的训练数据源头。如果利用这项技术,假设我们想要删除某条知识,则可首先定位到其对应的数据源头,删除数据源,然后重新预训练整个LLM模型,这样即可达成删除LLM中相关知识的目的。但是这里有个问题,如果修正一小部分知识,我们就需要重新做一次模型预训练,这样做明显成本太高。所以这种方法不会太有发展前景,可能比较适合那种对于某个特定类别数据的一次性大规模删除场合,不适合少量多次的常规知识修正场景,比如可能比较适合用来做去除偏见等去toxic内容的处理。

第二类方法是对LLM模型做一次fine-tuning来修正知识。一个直观能想到的方法是:我们可以根据要修正成的新知识来构建训练数据,然后让LLM模型在这个训练数据上做fine-tuning,这样指导LLM记住新的知识,遗忘旧的知识。这个方法简单直观,但是也有一些问题,首先它会带来灾难遗忘问题,就是说除了忘掉该忘的知识,还忘掉了不该忘的知识,导致这么做了之后有些下游任务效果下降。另外,因为目前的LLM模型规模非常大,即使是做fine-tuning,如果次数频繁,其实成本也相当高。对这种方法感兴趣的可以参考“Modifying Memories in Transformer Models”。

另外一类方法直接修改LLM里某些知识对应的模型参数来修正知识。假设我们想要把旧知识<英国,现任首相,鲍里斯>,修正到<英国,现任首相,苏纳克>。首先我们想办法在LLM模型参数中,定位到存储旧知识的FFN节点,然后可以强行调整更改FFN中对应的模型参数,将旧知识替换成新的知识。可以看出,这种方法涉及到两项关键技术:首先是如何在LLM参数空间中定位某条知识的具体存储位置;其次是如何修正模型参数,来实现旧知识到新知识的修正。关于这类技术的细节,可以参考“Locating and Editing Factual Associations in GPT”和“Mass-Editing Memory in a Transformer”。理解这个修正LLM知识的过程,其实对于更深入理解LLM的内部运作机制是很有帮助的。

规模效应:当LLM越来越大时会发生什么

我们知道,近年来,LLM模型规模在快速增长,目前效果最好的LLM模型,其参数规模大都超过了千亿(100B)参数规模。比如,OpenAI的GPT 3的规模为175B,Google的LaMDA规模为137B,PaLM的规模为540B,DeepMind的Gogher规模为280B等,不一而足。国内也有中文巨型模型,比如清华&智谱GLM规模130B,华为“盘古”规模200B,百度“文心”规模260B,浪潮“源1.0”规模245B。那么,一个很自然的问题就是:随着LLM模型规模不断增长,会发生些什么呢?

预训练模型的应用往往是两阶段的:预训练阶段,及具体场景应用阶段。在预训练阶段,其优化目标是交叉熵,对GPT这种自回归语言模型来说,也就是看LLM是否正确预测到了下一个单词;而场景应用阶段,一般要看具体场景的评价指标。一般我们的直觉是:如果LLM模型在预训练阶段的指标越好,自然它解决下游任务的能力就越强。然而,事实并非完全如此。现有研究已证明,预训练阶段的优化指标确实和下游任务表现出正相关关系,但是并非完全正相关。也就是说,只看预训练阶段的指标,来判断一个LLM模型是否够好,这是不够的。基于此,我们分头来看在这两个不同阶段,随着LLM模型增大,有什么影响。

首先,我们先看在预训练阶段,随着模型规模逐步增大,会发生什么。OpenAI在“Scaling Laws for Neural Language Models”中专门研究了这个问题,并提出LLM模型所遵循的“伸缩法则”(scaling law)。如上图所示,这个研究证明:当我们独立增加训练数据量、模型参数规模或者延长模型训练时间(比如从1个Epoch到2个Epoch),预训练模型在测试集上的Loss都会单调降低,也就是说模型效果越来越好。

既然三个因素都重要,那么我们在实际做预训练的时候,就有一个算力如何分配的决策问题:假设用于训练LLM的算力总预算(比如多少GPU小时或者GPU天)给定,那么是应该多增加数据量、减少模型参数呢?还是说数据量和模型规模同时增加,减少训练步数呢?此消彼长,某个要素规模增长,就要降低其它因素的规模,以维持总算力不变,所以这里有各种可能的算力分配方案。最终OpenAI选择了同时增加训练数据量和模型参数,但是采用早停策略(early stopping)来减少训练步数的方案。因为它证明了:对于训练数据量和模型参数这两个要素,如果只单独增加其中某一个,这不是最好的选择,最好能按照一定比例同时增加两者,它的结论是优先增加模型参数,然后才是训练数据量。假设用于训练LLM的算力总预算增加了10倍,那么应该增加5.5倍的模型参数量,1.8倍的训练数据量,此时模型效果最佳。

DeepMind的一项研究(参考:Training Compute-Optimal Large Language Models)更深入地探究了这个问题,其基本结论和OpenAI的结论差不多,比如确实需要同时增加训练数据量和模型参数,模型效果才会更好。而很多大模型在做预训练的时候,并没有考虑这一点,很多LLM大模型只是单调增加模型参数,而固定住了训练数据量,这个做法其实是不对的,限制了LLM模型的潜力。但是它修正了两者的比例关系,认为训练数据量和模型参数是同等重要的,也就是说,假设用于训练LLM的算力总预算增加了10倍,那么应该增加3.3倍的模型参数量,3.3倍的训练数据量,这样模型效果才最好。

这意味着:增加训练数据量的重要性,比我们之前所认为的,还要重要。基于这个认知,DeepMind在设计Chinchilla模型时,在算力分配上选择了另外一种配置:对标数据量300B、模型参数量280B的Gopher模型,Chinchilla选择增加4倍的训练数据,但是将模型参数降低为Gopher的四分之一,大约为70B。但是无论预训练指标,还是很多下游任务指标,Chinchilla效果都要优于规模更大的Gopher。

这带给我们如下启示:我们可以选择放大训练数据,并同比例地减少LLM模型参数,以达到在不降低模型效果的前提下,极大缩小模型规模的目的。缩小模型规模有很多好处,比如在应用的时候,推理速度会快很多等,无疑这是一个很有前途的LLM发展路线。

以上是从预训练阶段来看模型规模的影响,如果从LLM解决下游具体任务效果的角度来看,随着模型规模增大,不同类型的任务有不同的表现,具体而言,有以下三类情况。

第一类任务完美体现了LLM模型的scaling law,就是说随着模型规模逐步放大,任务的表现越来越好,如上图里的(a)图所示。这类任务通常符合如下共性:它们往往都是知识密集型任务,也就是说如果LLM模型包含的知识量越多,这类任务表现越好。而很多研究已经证明越大的LLM模型学习效率越高,也就是说相同训练数据量,模型越大任务效果越好,说明面对的即使是同样的一批训练数据,更大的LLM模型相对规模小一些的模型,从中学到了更多的知识。更何况一般情况下,在增大LLM模型参数的时候,往往会同步增加训练数据量,这意味着大模型可以从更多数据中学习更多的知识点。这些研究可以很好地解释上图,为何随着模型规模增大,这些知识密集型的任务效果越来越好。大多数传统的自然语言理解类任务,其实都属于这种知识密集型任务,而很多任务在近两年获得了极大的效果提升,甚至超过了人类表现。很明显,这大概率是LLM模型的规模增长带来的,而非归功于某项具体的技术改进。

第二类任务展现出LLM具备某种“涌现能力(Emergent Ability)”,如上图(b)所示。所谓“涌现能力”,指的是当模型参数规模未能达到某个阀值时,模型基本不具备解决此类任务的任何能力,体现为其性能和随机选择答案效果相当,但是当模型规模跨过阀值,LLM模型对此类任务的效果就出现突然的性能增长。也就是说,模型规模是解锁(unlock)LLM新能力的关键,随着模型规模越来越大,会逐渐解锁LLM越来越多的新能力。这是个很神奇的现象,因为它意味着如下让人对未来可报乐观预期的可能:或许很多任务,目前LLM还不能很好地解决,甚至站在现在这个时刻的我们看起来,LLM完全没有能力解决这类任务,但因LLM具备“涌现能力”,所以如果我们继续推大模型,也许某一天它的这项能力就被突然解锁了。LLM模型的规模增长会给我们带来意想不到的精彩礼物。

“Beyond the Imitation Game: Quantifying and extrapolating the capabilities of language models”这篇文章指出,这类体现出“涌现能力”的任务也有一些共性:这些任务一般由多步骤构成,要解决这些任务,往往需要先解决多个中间步骤,而逻辑推理能力在最终解决这类任务中发挥重要作用。思维链(Chain of Thought)Prompting是典型的增强LLM推理能力的技术,能大幅提升此类任务的效果,关于CoT技术,在随后小节内容会做解释,此处暂不展开。

问题是,为何LLM会出现这种“涌现能力”现象呢?上述文章以及“Emergent Abilities of Large Language Models”给出了几个可能的解释:

一种可能解释是有些任务的评价指标不够平滑。比如说有些生成任务的判断标准,它要求模型输出的字符串,要和标准答案完全匹配才算对,否则就是0分。所以,即使随着模型增大,其效果在逐步变好,体现为输出了更多的正确字符片段,但是因为没有完全对,只要有任何小错误都给0分,只有当模型足够大,输出片段全部正确才能得分。也就是说,因为指标不够平滑,所以不能体现LLM其实正在逐步改善任务效果这一现实,看起来就是“涌现能力”这种外在表现。

另外一种可能的解释是:有些任务由若干中间步骤构成,随着模型规模增大,解决每个步骤的能力也在逐步增强,但是只要有一个中间步骤是错的,最终答案就是错的,于是也会导致这种表面的“涌现能力”现象。

当然,上面的解释目前还都是猜想,至于为何LLM会出现这种现象,还需要进一步更深入的研究。

还有少部分任务,随着模型规模增长,任务的效果曲线展现出U形特性:随着模型规模逐渐变大,任务效果逐渐变差,但是当模型规模进一步增长,则效果开始越来越好,呈现出U形增长趋势,如上图所示的粉红色PaLM模型在两个任务上的指标走势。为何这些任务表现得如此特殊呢?“Inverse scaling can become U-shaped”这篇文章给出了一种解释:这些任务,内部其实隐含了两种不同类型的子任务,一种是真正的任务,另外一种是“干扰任务(distractor task)”。当模型规模小的时候,无法识别任意一种子任务,所以模型的表现跟随机选择答案差不多,当模型增长到中等规模的时候,主要执行的是干扰任务,所以对真正的任务效果有负面影响,体现为真正任务效果的下降,而当进一步增加模型规模,则LLM可以忽略干扰任务,执行真正的任务,体现为效果开始增长。

对于那些随着模型规模增大,效果一直下降的任务,如果采用思维链(CoT)Prompting,则部分任务的表现转换为遵循Scaling law,即模型规模越大效果越好,而其它任务则转换为U性增长曲线。这其实侧面说明了:此类任务应属于推理类型的任务,所以加入CoT后任务表现会发生质的变化。

人机接口:从In Context Learning到Instruct理解

一般我们经常提到的人和LLM的接口技术包括:zero shot prompting、few shot prompting、In Context Learning,以及Instruct。这些其实都是表达某个具体任务的描述方式。不过如果你看文献,会发现叫法比较乱。

其中Instruct 是ChatGPT的接口方式,就是说人以自然语言给出任务的描述,比如“把这个句子从中文翻译成英文”,类似这种。zero shot prompting我理解其实就是现在的Instruct的早期叫法,以前大家习惯叫zero shot,现在很多改成叫Instruct。尽管是一个内涵,但是具体做法是两种做法。早期大家做zero shot prompting,实际上就是不知道怎么表达一个任务才好,于是就换不同的单词或者句子,反复在尝试好的任务表达方式,这种做法目前已经被证明是在拟合训练数据的分布,其实没啥意思。目前Instruct的做法则是给定命令表述语句,试图让LLM理解它。所以尽管表面都是任务的表述,但是思路是不同的。

而In Context Learning和few shot prompting意思类似,就是给LLM几个示例作为范本,然后让LLM解决新问题。我个人认为In Context Learning也可以理解为某项任务的描述,只是Instruct是一种抽象的描述方式,In Context Learning是一种例子示范的例子说明法。当然,鉴于目前这几个叫法用的有点乱,所以上述理解仅代表个人看法。

所以我们此处只对In Context Learning和Instruct进行介绍,不再提zero shot和few shot了。

神秘的In Context Learning

Fine-tuning和In Context Learning表面看似都提供了一些例子给LLM,但两者有质的不同(参考上图示意):Fine-tuning拿这些例子当作训练数据,利用反向传播去修正LLM的模型参数,而修正模型参数这个动作,确实体现了LLM从这些例子学习的过程。但是,In Context Learning只是拿出例子让LLM看了一眼,并没有根据例子,用反向传播去修正LLM模型参数的动作,就要求它去预测新例子。既然没有修正模型参数,这意味着貌似LLM并未经历一个学习过程,如果没有经历学习过程,那它为何能够做到仅看一眼,就能预测对新例子呢?这正是In Context Learning的神奇之处。这是否让你想起了一句歌词:“只是因为在人群中多看了你一眼 再也没能忘掉你容颜”,而这首歌名叫“传奇”。你说传奇不传奇?

看似In Context Learning没从例子里学习知识,实际上,难道LLM通过一种奇怪的方式去学习?还是说,它确实也没学啥?关于这个问题的答案,目前仍是未解之谜。现有一些研究各有各的说法,五花八门,很难判断哪个讲述的是事实的真相,甚至有些研究结论还相互矛盾。这里提供几个目前的说法,至于谁对谁错,只能你自己把握了。当然,我认为追求这个神奇现象背后的真相,是一个好的研究课题。

总而言之,目前这还是一个未解之谜。

神奇的Instruct理解

我们可以把Instruct当作一种方便人类理解的任务表述,在这个前提下,目前关于Instruct的研究可以分成两种:偏学术研究的Instruct,以及关于人类真实需求描述的Instruct。

我们先来看第一种:偏学术研究的Instruct。它的核心研究主题是多任务场景下,LLM模型对Instruct理解的泛化能力。如上图中FLAN模型所示,就是说有很多NLP任务,对于每个任务,研究人员构造一个或者多个Prompt模版作为任务的Instruct,然后用训练例子对LLM模型进行微调,让LLM以同时学习多个任务。训练好模型后,给LLM模型一个它没见过的全新任务的Instruct,然后让LLM 解决zero shot任务,从任务解决得是否足够好,来判断LLM模型是否有对Instruct理解的泛化能力。

如果归纳下目前的研究结论(可参考“Scaling Instruction-Fine-tuned Language Models”/“Super-NaturalInstructions: Generalization via Declarative Instructions on 1600+ NLP Tasks”),能够有效增加LLM模型Instruct泛化能力的因素包括:增加多任务的任务数量、增加LLM模型大小、提供CoT Prompting, 以及增加任务的多样性。如果采取任意一项措施,都可以增加LLM模型的Instruct理解能力。

第二种是人类真实需求下的Instruct,这类研究以InstructGPT和ChatGPT为代表。这类工作也是基于多任务的,但是和偏向学术研究类工作最大的不同,在于它是面向人类用户真实需求的。为什么这么说呢?因为它们用于LLM多任务训练的任务描述Prompt,是从大量用户提交的真实请求中抽样而来的,而不是固定好研究任务的范围,然后让研究人员来写任务描述prompt。这里所谓的“真实需求”,体现在两个方面:首先,因为是从用户提交的任务描述里随机抽取的,所以涵盖的任务类型更多样化,也更符合用户的真实需求;其次,某个任务的prompt描述,是用户提交的,体现了一般用户在表达任务需求时会怎么说,而不是你认为用户会怎么说。很明显,这类工作改出来的LLM模型,用户体验会更好。

InstructGPT论文里,也拿这种方法和FLAN那种Instruct based方法做了比较。首先在GPT3上用FLAN提到的任务、数据以及Prompt模版进行微调,来在GPT 3上复现FLAN方法,然后和InstructGPT进行比较,因为InstructGPT的基础模型也是GPT3,所以只有数据和方法的差别,两者可比,结果发现FLAN方法的效果,距离InstructGPT有很大的差距。那么背后的原因是什么呢?论文分析数据后认为,FLAN方法涉及到的任务领域相对少,是InstructGPT涉及领域的子集,所以效果不好。也就是说,FLAN论文里涉及到的任务和用户真实需求是不符的,而这导致在真实场景下效果不够好。而这对我们的启示是:从用户数据中收集真实需求,这事情是很重要的。

In Context Learning和Instruct的联系

如果我们假设In Context Learning是用一些例子来具象地表达任务命令,Instruct是一种更符合人类习惯的抽象任务描述。那么,一个很自然的问题是:它们之间有什么联系吗?比如,我们是否能够提供给LLM完成某个任务的若干具体示例,让LLM找出其对应的自然语言描述的Instruct命令?

目前有零星的工作在探索这个问题,我认为这个方向是很有研究价值的。先说答案,答案是:Yes,LLM Can。“Large Language Models Are Human-Level Prompt Engineers”是做这个方向很有趣的工作,如上图所示,对于某项任务,给LLM一些示例,让LLM自动生成能够描述这项任务的自然语言命令,然后它再用LLM生成的任务描述去测试任务效果。它使用的基础模型是GPT 3和InstructGPT,经过这项技术加持后,LLM生成的Instruct的效果相比未采用这项技术的GPT 3 以及InstuctGPT来说,指标有极大地提升,而且在一些任务上超过人类的表现。

这说明了:具象的任务示例和任务的自然语言描述之间,有种神秘的内在联系。至于这种联系到底是什么?我们目前对此还一无所知。

智慧之光:如何增强LLM的推理能力

目前很多研究已证明LLM对于知识具有强大的记忆能力,但是,一般我们不会因为一个人记忆能力强,就说这人很聪明,是否具有强大的推理能力,往往是我们判断一个人是否聪明的重要标准。类似的,如果LLM的效果想让人觉得很惊艳,强大的推理能力是必备的。推理能力本质上是综合运用很多相关知识点,去推导出新知识或新结论。关于LLM的推理能力,是最近一年来LLM里最重要和热门的研究领域之一。于是,我们关心的问题就是:LLM具备推理能力吗?如果具备,那么它的推理能力够强吗?

这两个问题目前的答案似乎应该是:当模型规模足够大的时候,LLM本身是具备推理能力的,在简单推理问题上,LLM已经达到了很好的能力,但是复杂推理问题上,还需要更多深入的研究。

如果梳理现有LLM推理相关工作的话,我把它们归到两大类,体现出挖掘或促进LLM推理能力不同的技术思路:第一类研究比较多,可以统称为基于Prompt的方法,核心思想是通过合适的提示语或提示样本,更好地激发出LLM本身就具备的推理能力,Google在这个方向做了大量很有成效的工作。第二类做法是在预训练过程中引入程序代码,和文本一起参与预训练,以此进一步增强LLM的推理能力,这应该是OpenAI实践出的思路。比如ChatGPT肯定具备很强的推理能力,但它并不要求用户必须提供一些推理示例,所以ChatGPT强大的推理能力,大概率来源于使用代码参与GPT 3.5的预训练。

这两种思路其实大方向是迥异的:利用代码增强LLM推理能力,这体现出一种通过增加多样性的训练数据,来直接增强LLM推理能力的思路;而基于Prompt的方法,它并不会促进LLM本身的推理能力,只是让LLM在解决问题过程中更好地展示出这种能力的技术方法。可以看出,前者(代码方法)治本,后者治标。当然,两者其实也是互补的,但从长远看,治本的方法更重要。

基于Prompt的方法

这方面工作非常多,如果归纳一下的话,大致可以分为三条技术路线。

第一种思路是直接在问题上追加辅助推理Prompt。这种方法简单直接,但在众多领域都很有效。这个做法是由“Large language models are zero-shot reasoners”提出的,也被称为zero-shot CoT。具体而言,分为两个阶段(如上图所示),第一阶段在提问的问题上追加“Let’s think step by step”这句提示语,LLM会输出具体的推理过程;第二阶段,在第一阶段的问题后,拼接LLM输出的具体推理过程,并再追加Prompt=“Therefore, the answer (arabic numerals) is”,此时LLM会给出答案。如此简单的操作,却可以大幅增加LLM在各项推理任务中的效果,比如在数学推理测试集GSM8K上,加上提示语后,推理准确率直接从原先的10.4%提升到了40.4%,可谓神奇。

为什么LLM会具备给一句“Let’s think step by step”提示语,就能列出详细的推理步骤并算出答案呢?其原因目前尚无定论,我的猜测是:很可能因为预训练数据里面存在大量的此种数据,就是以“Let’s think step by step”开头,然后后面是详细的推理步骤,最后给出答案,而LLM在预训练的时候记住了这些模式。而当我们输入这个提示语的时候,激发LLM模糊得“回忆”起某些例子的推导步骤,于是即可模仿这些例子进行步骤推理并给出答案。当然这只是我的无依据推论,若事实真的如此,如果你看过后面介绍的标准CoT做法,会发现Zero-shot CoT 本质上和标准CoT很可能没什么区别,只是标准CoT由人工来写推理步骤的示例,而Zero-shot CoT大概率是通过提示语,激活了记忆中的某些包含推理步骤的示例,很可能是如此区别。而标准CoT效果比Zero-Shot CoT效果好也完全可以理解,因为毕竟靠LLM回忆示例,精准性估计不会太高,而人工给出的示例,准确性是有保障的,所以自然标准CoT效果会更好。

这侧面说明了一个道理,就是LLM本身是具备推理能力的,只是我们没有办法把它的这种能力激发出来而已,通过合适的提示语来进行两步提示,就在一定程度上可以释放出它的这种潜力。另外,对于中文,很可能存在另外一个黄金提示语,比如“详细解题思路如下”,类似这种,因为中文语料在讲解推理步骤的时候,经常用的引导句和“让我们一步一步来思考”应该是不同的,这是明显的西方说法,而探索出这个中文黄金提示语,其实也是很有必要的。

第二种思路一般被称为基于示例的思维链(few-shot CoT,Chain of Thought)Prompting。这个方向目前是LLM推理研究的主方向,很多工作都是在这个思路上做的,我们简单介绍几个效果显著的代表性工作,基本能代表CoT的技术发展方向。

CoT的主体思想其实很直白;为了教会LLM模型学会推理,给出一些人工写好的推理示例,示例里把得到最终答案前,一步步的具体推理步骤说清楚,而这些人工写的详细推理过程,就是思维链Prompting,具体例子可参照上图中蓝色文字部分。CoT的意思是让LLM模型明白一个道理;就是在推理过程中,步子不要迈得太大,否则很容易出错,改变思维模式,化大问题为小问题,步步为营,积小胜为大胜。最早明确提出CoT这个概念的文章是“Chain of thought prompting elicits reasoning in large language models”,论文发布于22年1月份,虽然做法很简单,但是应用CoT后LLM模型的推理能力得到了巨大提升,GSM8K数学推理测试集准确率提高到60.1%左右。当然,这种给出详细推理步骤和中间过程的思想,并非CoT最早提出的,更早一些的“scratchpad”技术(可参考:Show Your Work: Scratchpads for Intermediate Computation with Language Models)首先采用了类似的思路。

CoT提出不久,很快在22年3月份,一项被称为“Self-Consistency”的改进技术就将GSM8K测试集准确率提高到74.4%,提出这项改进的论文是“Self-Consistency Improves Chain of Thought Reasoning in Language Models”。“Self-Consistency”的思路也很直观(参考上图):首先可以利用CoT给出几个写了推理过程的示例,然后要求LLM对给定的问题进行推理,如果是CoT,直接输出一个推理过程和答案,整个过程就结束了。“Self-Consistency”则不然,它要求LLM输出多个不同的推理过程和答案,然后采用投票的方式选出最佳答案,思路非常简单直接,但是效果也确实好。“Self-Consistency”其实是教导LLM学会这么一个道理:孔乙己说过茴香豆的“茴”字有四种写法,类似的,一个数学题的正确解法也可以有很多种,每个不同的推导过程都指向最终的答案。条条大路通罗马,虽说也有个别迷路走到北京的,但是迷路的毕竟是少数,看看大多数人走到哪里,哪里就是正确答案。简单的方法往往蕴含着深刻的哲学含义,是不是这道理?

再往后,“On the Advance of Making Language Models Better Reasoners”这个工作在“Self-Consistency”基础上,进一步集成了“从一个Prompt问题拓展到多个Prompt问题、检查推理中间步骤的正确性以及对多个输出的回答加权投票”这三个改进点,将GSM8K测试集准确率提高到83%左右。

第三种思路体现了一种分治算法的思想。当然这个所谓“分治”是我归纳的,别人没这么说。这种思路的核心思想是:对于一个复杂的推理问题,我们把它分解成若干容易解决的子问题,一一解决掉子问题后,我们再从子问题的答案推导复杂问题的答案。你看这确实比较类似分治算法的思想吧。我个人觉得,这种思路可能才是揭示问题本质、最终解决LLM复杂推理问题正宗的道路。我们以“Least-to-most prompting”技术为例来说明这种思路的一种具体实现方式,如上图所示:它分为两个阶段,第一个阶段,从原始问题我们可以得知最终要问的问题是什么,我们假设最终问题是Final Q,然后从原始问题填充Prompt模版:“如果要解决Final Q问题,那么我需要先解决”,然后把原始问题和这个Prompt交给LLM,让LLM模型给出答案,等于让LLM给出最终问题的前置子问题Sub Q;接下来我们进入第二个阶段,让LLM先回答刚才拿到的子问题Sub Q,并拿到对应的答案,然后原始问题拼接子问题Sub Q及对应答案,再去问LLM最终那个问题Final Q,此时LLM会给出最后的答案。如此这般,体现出拆解子问题,并从子问题的答案逐步找出最终答案的思路。

代码预训练增强LLM推理能力

以上是目前利用Prompt激发LLM模型推理能力的三种主流做法,而关于LLM的推理能力,目前还观察到一个有趣且费解的现象:除了文本外,如果能够加入程序代码一起参与模型预训练,则能大幅提升LLM模型的推理能力。这个结论从不少论文的实验部分都可以得出(可以参考:AUTOMATIC CHAIN OF THOUGHT PROMPTING IN LARGE LANGUAGE MODELS/Challenging BIG-Bench tasks and whether chain-of-thought can solve them等论文的实验部分)。

上图给出了一份实验数据,来自于论文“On the Advance of Making Language Models Better Reasoners”,其中GPT3 davinci就是标准的GPT 3模型,基于纯文本训练;code-davinci-002(OpenAI内部称为Codex)是同时在Code和NLP数据上训练的模型。如果比较两者效果,可以看出,不论采用具体哪种推理方法,仅仅是从纯文本预训练模型切换到文本和Code混合预训练模型,在几乎所有测试数据集合上,模型推理能力都得到了巨大的效果提升,比如我们以“Self Consistency”方法为例,在大多数据集合上的性能提升,都直接超过了20到50个百分点,这是很恐怖的性能提升,而其实在具体推理模型层面,我们什么也没做,仅仅是预训练的时候除了文本,额外加入了程序代码而已。

除了这个现象,从上图数据中,我们还可以得出其它一些结论,比如GPT 3这种纯文本预训练模型,其实是具备相当程度的推理能力的,除了在GSM8K这种数学推理上效果比较差外,其它推理数据数据集合表现也还可以,前提你需要采用合适的方法,来激发出它本身就具备的这种能力;再比如,text-davinci-002,也就是在code-davinci-002基础上加入instruct fine-tuning后的模型(就是加入InstructGPT或ChatGPT模型的第一步),其推理能力要弱于Codex,但是有其它研究表明它在自然语言处理任务又要强于Codex。而这貌似说明了,加入instruct fine-tuning,会损害LLM模型的推理能力,但是会在一定程度上提升自然语言理解能力。而这些结论其实都是很有意思的,也能启发后续进一步的思考和探索。

那么,一个自然的疑问是:为何预训练模型可以从代码的预训练中获得额外的推理能力?确切原因目前未知,值得深入探索。我猜测可能是因为原始版本的Codex(只使用代码训练,可参考文献:Evaluating Large Language Models Trained on Code)的代码训练是从文本生成代码,而且代码中往往包含很多文本注释,本质上这类似于预训练模型做了<文本,Code>两种数据的多模态对齐工作。而数据中必然包含相当比例的数学或逻辑问题的代码、描述和注释,很明显这些数学类或逻辑推理类的数据,对于解决下游数学推理问题是有帮助的,我猜大概率原因在此。

关于LLM推理能力的思考

上面介绍了LLM推理的主流技术思路和现有的一些结论,接下来谈谈我对LLM模型推理技术的思考,以下内容纯个人推断,没有太多证据,还请谨慎参考。我的判断是:虽然最近一年来,关于激发LLM的推理能力,这方面的技术进展很快,也取得了很大的技术进步,但是总体感觉是,我们可能走在正确的方向上,但是距离接触到真正的问题本质还有一段距离,对此要有更深入的思考和探索。

首先,我比较赞同上述分治算法的主体思路,对于复杂的推理问题,我们应该把它拆解成若干简单的子问题,因为子问题对于LLM来说回答正确的概率就大很多,让LLM一一回答子问题后,再逐步推导出最终答案。受到“Least-to-most prompting”技术的启发,如果进一步思考,我觉得LLM推理本质上很可能会是如下两种可能的其中之一:不断和LLM进行交互的图上推理问题,抑或是不断和LLM进行交互的程序流程图执行问题。

先说图上推理问题,如上图所示,假设我们有办法能够把复杂问题拆解成由子问题或者子步骤构成的图结构,图中的节点是子问题或者子步骤,图中的边代表了子问题之间的依赖关系,就是说只有回答好子问题A,才能回答子问题B,而且图中大概率存在循环结构,就是反复做某几个子步骤。假设我们能够得到上述的子问题拆解图,那么可以根据依赖关系,引导LLM一步一步按照图结构,回答必须首先回答的子问题,直到推导出最终答案。

再说程序流程图问题,参考上图,假设我们有办法把复杂问题拆解成子问题或子步骤,并产生一个由子步骤构成的类似程序流程图的结构,在这个结构里,有些步骤会反复执行多次(循环结构),有些步骤的执行需要进行条件判断(条件分支)。总而言之,在执行每个子步骤的时候和LLM进行交互,得到子步骤的答案,然后按照流程不断执行,直到输出最终答案。类似这种模式。假设这个思路大致正确的话,也许可以从这个角度来解释为何加入代码会增强预训练模型的推理能力:大概率因为<文本,代码>的多模态预训练模型,在模型内部是通过类似这种隐含的程序流程图作为两个模态的桥梁,将两者联系起来的,即由文本描述到隐含的流程图,再映射到由流程图产生具体的代码。也就是说,这种多模态预训练,可以增强LLM模型从文本构建出隐含的流程图并按照流程图执行的能力,也就是加强了它的推理能力。

当然,上述思路最大的问题是,我们如何根据文本描述的问题,能够靠LLM模型,或者其它模型,得到图结构或者流程图结构?这个可能是其中的难点。一种可能的思路就类似继续增强文本和更高质量的代码预训练,走隐式学习内部隐含结构的方法。而目前的CoT技术,如果套到上述思路来思考的话,可以这么理解:标准CoT,其实就是靠自然语言文本来描述图结构或者程序流程图的;而“Least-to-most prompting”技术,则是试图根据最后一个图节点,靠倒推来试图推导出其中的图结构,但是很明显,目前的方法限制了它倒推的深度,也就是说它只能推导出非常简单的图结构,这正是限制它能力的所在。

未来之路:LLM研究趋势及值得研究的重点方向

这里列出一些我个人认为比较重要的LLM研究领域,或值得深入探索的研究方向。

探索LLM模型的规模天花板

尽管继续推大LLM模型的规模,这事看似没有技术含量,但是其实这个事情异常重要。我个人判断,自从Bert出现以来,到GPT 3,再到ChatGPT,大概率这些给人印象深刻的关键技术突破,核心贡献都来自于LLM模型规模的增长,而非某项具体技术。说不定,揭开AGI真正的钥匙就是:超大规模及足够多样性的数据、超大规模的模型,以及充分的训练过程。再者,做超大规模的LLM模型,对技术团队的工程实现能力要求是非常高的,也不能认为这事情缺乏技术含量。

那么继续推大LLM模型规模,有什么研究意义呢?我觉得有两方面的价值。首先,如上所述,我们已知,对于知识密集型的任务,随着模型规模越大,各种任务的效果会越来越好;而对很多推理类型的有难度的任务,加上CoT Prompting后,其效果也呈现出遵循Scaling law的趋向。那么,很自然的一个问题就是:对于这些任务,LLM的规模效应,能将这些任务解决到何种程度?这是包括我在内,很多人关心的问题。其次,考虑到LLM具备的神奇的“涌现能力”,如果我们继续增加模型规模,它会解锁哪些让我们意想不到的新能力呢?这也是很有意思的问题。考虑到以上两点,我们仍然需要不断增大模型规模,看看模型规模对解决各类任务的天花板在哪里。

当然,这种事情也就只能说说,对99.99%的从业者来说,是没有机会和能力做这个事情的。要做这个事情,对研究机构的财力及投入意愿、工程能力、技术热情,都有极高的要求,缺一不可。能做这事情的机构,粗估下来,国外不超过5家,国内不超过3家。当然,考虑到成本问题,未来也许会出现“股份制大模型”,就是有能力的几家机构合作,群策群力,一起来共建超级大模型的现象。

增强LLM的复杂推理能力

正如之前对LLM推理能力的叙述,尽管LLM在最近一年推理能力得到了很大的提升,但是很多研究(参考:Limitations of Language Models in Arithmetic and Symbolic Induction/Large Language Models Still Can’t Plan)表明,目前LLM能够解决得比较好的推理问题,往往都相对简单,LLM的复杂推理能力仍然薄弱,比如即使是简单的字符拷贝推理或者加减乘除运算,当字符串或者数字非常长的时候,LLM推理能力会极速下降,再比如行为规划能力等复杂推理能力很弱。总而言之,加强LLM的复杂推理能力,应该是LLM未来研究中最重要的环节之一。

前文有述,加入代码加入预训练,这是一种直接增强LLM推理能力的方向。这个方向目前研究尚显不足,更像是实践经验的总结,探索背后的原理,并进而引入更多类型除代码外的新型数据来增强LLM的推理能力,这可能是更本质提升推理能力的方向。

LLM纳入NLP之外更多其它研究领域

目前的ChatGPT擅长NLP和Code任务,作为通向AGI的重要种子选手,将图像、视频、音频等图像与多模态集成进入LLM,乃至AI for Science、机器人控制等更多、差异化更明显的其它领域逐步纳入LLM,是LLM通往AGI的必经之路。而这个方向才刚刚开始,因此具备很高的研究价值。

更易用的人和LLM的交互接口

如前所述,ChatGPT的最大技术贡献即在此。但是很明显,目前的技术并不完美,肯定还有很多命令LLM理解不了。所以,沿着这个方向,寻找更好的技术,来让人类使用自己习惯的命令表达方式,而LLM又能听懂,这是个新的,且非常有前景的技术方向。

建设高难度的综合任务评测数据集

好的评测数据集,是引导技术不断进步的基石。随着LLM模型逐步增大,任务效果快速提升,导致很多标准测试集快速过时。也就是说,这些数据集合相对现有技术来说,太容易了,在没有难度的测试集合下,我们不知道目前技术的缺陷和盲点在哪里。所以构建高难度的测试集合,是促进LLM技术进步的关键所在。

目前行业应出现了一些新的测试集,有代表性的包括BIGBench、OPT-IML等。这些测试集合体现出一些特性,比如相对LLM现有技术具备一定的难度、综合了各种各样多种类型的任务等。

受到ChatGPT的启发,我觉得除此外应纳入另一考虑因素:体现真实用户需求。就是说,这些任务的表述由用户真实发起,这种方式构建出来的LLM模型,才能解决用户实际需求。

除此外,相信LLM会快速将能力溢出到NLP之外的领域,而如何融入更多其它领域的评测数据,也是需要提前去考虑。

高质量数据工程

对于预训练模型来说,数据是其根本,预训练过程可以理解为从数据中吸取其中所包含知识的过程。因此,我们需要进一步加强对高质量数据的挖掘、收集及清洗等工作。

关于数据,需要考虑两个方面:数据的质量和数量。而根据T5的对比实验,我们可以得出结论:在数量和质量两个因素里,质量优先,正确的道路应该是在保证数据质量的前提下,再去增大数据规模。

数据质量,包括数据的信息含量以及数据的多样性等多个衡量标准,比如Wiki明显就属于世界知识密度极高的高质量数据,这是从信息含量来说的;而增加数据类型的多样性,无疑是激发LLM各种新能力的根本,比如加入问答网站的数据,对于LLM的QA能力提升是有直接帮助的。多样化的数据赋予了LLM更好解决更多不同类型任务的能力,所以,这可能是数据质量里最关键的标准。

关于数据数量,原则上互联网上公开发布的数据都可以纳入LLM模型的预训练过程。那么,它的极限在哪里?“Will we run out of data? An analysis of the limits of scaling datasets in Machine Learning” 对此进行了估算,结论是到2026年左右,高质量的NLP数据将会用光,低质量NLP数据会在2030到2050年用光,而低质量图像数据会在2030到2060年用光。而这意味着:要么到时我们有新类型的数据源,要么我们必须增加LLM模型对数据的利用效率。否则,目前这种数据驱动的模型优化方式将会停止进步,或者收益减少。

超大LLM模型Transformer的稀疏化

目前规模最大的LLM中,有相当比例的模型采取了稀疏(Sparse)结构,比如GPT 3、PaLM、GLaM等,GPT 4大概率也会走稀疏模型路线。之所以采用Sparse 化的模型,主要好处是它可以极大减少LLM的训练时间和在线推理时间。Switch Transformer论文里指出:在相同算力预算的前提下,使用稀疏化Transformer,相对Dense Transformer,LLM模型的训练速度可以提升4倍到7倍。为何Sparse模型可以加快训练和推理时间呢?这是因为尽管模型参数巨大,但是对于某个训练实例,Sparse模型通过路由机制,只使用整个参数中的一小部分,参与训练和推理的活跃参数量比较少,所以速度快。

我认为未来超大的LLM模型大概率会收敛到稀疏模型。主要有两个原因:一方面,现有研究表明(参考:Large Models are Parsimonious Learners: Activation Sparsity in Trained Transformers),标准的Dense Transformer在训练和推理时,它本身也是稀疏激活的,就是说只有部分参数会被激活,大部分参数没有参与训练和推理过程。既然这样,我们不如直接迁移到稀疏模型;另外,毫无疑问LLM模型的规模会继续推大,而高昂的训练成本是妨碍其进一步扩大模型的重要阻力,使用稀疏模型可以极大降低超大模型的训练成本,所以随着模型规模越大,稀疏模型带来的收益越明显。考虑到这两个方面,大概率未来更大的LLM模型会采用稀疏模型方案。

那为何目前其它大规模模型不走稀疏模型的路线呢?因为Sparse模型存在训练不稳定、容易过拟合等问题,不太容易训练好。所以,如何修正稀疏模型面临的问题,设计出更容易训练的稀疏模型,是很重要的未来研究方向。 

取经之路:复刻ChatGPT时要注意些什么

如果希望能复刻类似ChatGPT这种效果令人惊艳的LLM模型,综合目前的各种研究结论,在做技术选型时需要重点权衡如下问题:

首先,在预训练模式上,我们有三种选择:GPT这种自回归语言模型,Bert这种双向语言模型,以及T5这种混合模式(Encoder-Decoder架构,在Encoder采取双向语言模型,Decoder采取自回归语言模型,所以是一种混合结构,但其本质仍属于Bert模式)。我们应选择GPT这种自回归语言模型,其原因在本文范式转换部分有做分析。目前看,国内LLM在做这方面技术选型的时候,貌似很多都走了Bert双向语言模型或T5混合语言模型的技术路线,很可能方向走偏了。

第二,强大的推理能力是让用户认可LLM的重要心理基础,而如果希望LLM能够具备强大的推理能力,根据目前经验,最好在做预训练的时候,要引入大量代码和文本一起进行LLM训练。至于其中的道理,在本文前面相关部分有对应分析。

第三,如果希望模型参数规模不要那么巨大,但又希望效果仍然足够好,此时有两个技术选项可做配置:要么增强高质量数据收集、挖掘、清理等方面的工作,意思是我模型参数可以是ChatGPT/GPT 4的一半,但是要想达到类似的效果,那么高质量训练数据的数量就需要是ChatGPT/GPT 4模型的一倍(Chinchilla的路子);另外一个可以有效减小模型规模的路线是采取文本检索(Retrieval based)模型+LLM的路线,这样也可以在效果相当的前提下,极大减少LLM模型的参数规模。这两个技术选型不互斥,反而是互补的,也即是说,可以同时采取这两个技术,在模型规模相对比较小的前提下,达到超级大模型类似的效果。

第四,超级大模型因为模型规模大,所以训练成本过高,导致很少有机构有能力去做这件事。而且由上文分析可见,继续不断推大LLM模型规模是肯定会发生、也应该去做的事情。于是,如何通过技术手段降低LLM的训练成本就很重要。LLM的特征抽取器Sparse化是有效降低模型训练及推理成本的技术选择。由此可见,随着模型越来越大,LLM模型Sparse化是一个应该考虑的选项。

第五,ChatGPT是目前最接近理想LLM的技术方案,而理想中的LLM应该是以一个几乎无所不能的基础通用大模型作为依托,来支持各种各样的上层任务类型。目前看,支持越来越多的任务类型,主要是通过增加LLM预训练数据的多样性来达成的,数据多样性越好,LLM能够支持的任务类型就越丰富。所以,应该重视通过增加数据多样性来增加LLM新能力的思路。

第六,易用的人机操作接口。人类用他们自己习惯的表达方式来描述任务,而LLM要能够理解这些Instruct的真实含义。另外,也要注意这些Instruct是符合人类真实需求的,就是说,要从最终用户那里收集任务表述方式,而不能靠研发人员自己的臆想或猜测。ChatGPT给我最大的启发其实是这一点,至于是否用增强学习我倒觉得不重要,其它替代技术应该也能做类似的事情。

ChatGPT:为什么是OpenAI

为什么是OpenAI作出了ChatGPT,而不是其它机构呢?我们在这里可以做个简单分析。

在本文开头,我们提到了OpenAI看待LLM的理念。OpenAI是怎么看待LLM的呢?回顾它不断推出的技术,可以看出,它其实从GPT 1.0开始,基本就坚定地把LLM看做是通往AGI的一条必由之路。具体而言,在OpenAI眼中,未来的AGI应该长这个样子:有一个任务无关的超大型LLM,用来从海量数据中学习各种知识,这个LLM以生成一切的方式,来解决各种各样的实际问题,而且它应该能听懂人类的命令,以便于人类使用。其实对LLM发展理念的理解,在前半部分,就是“构建一个任务无关的超大型LLM,让它从海量数据中学习各种知识”,这一点几乎是大家的共识,能体现出OpenAI眼光的其实是后半部分。

OpenAI的理念比较超前,对自我定位从一开始就定得比较高,始终坚定不移地探索上述方式是否可以实现AGI。OpenAI之所以能作出ChatGPT,胜在一个是定位比较高,另一个是不受外界干扰,态度上坚定不移。

我们可以回顾下它走的一些关键路程:GPT 1.0走的是生成模式的自回归语言模型路线,比Bert出来的还早些。Bert证明了:双向语言模型对于很多NLP理解类任务,效果比自回归这种单向语言模型效果更好。尽管如此,GPT 2.0并没有因此切换到双向语言模型这条路上,仍然走文本生成的路,而且开始尝试零示例(zero shot)prompt和少量示例(few shot)prompt。其实这时候, OpenAI心目中的AGI已经开始浮出水面,逐渐显示出轮廓了。只是因为zero shot/few shot效果比Bert+fine-tuning差的比较远,所以大家都没太当回事,甚至不理解它为什么要始终坚持走单向语言模型的路线。这个时候,我估计即使是OpenAI自己,也不一定能确保这条路肯定能走通。

但是,这不妨碍它继续在这条路上往后走。GPT 3.0已经展示出了比较强大的zero shot/few shot prompt能力,这时候OpenAI心目中的AGI已经完全漏出水面,轮廓清晰,而且它的效果也证明了这条路,是有较大可能走得通的。GPT 3.0是一个决定LLM发展方向的叉路口和分水岭,与之对应的另外一条路是“Bert+fine-tuning”模式。在这个岔路口,不同的从业者选择走上了不同的道路,后面的技术差距也是从这里开始拉开的。很遗憾地是,国内很多从业者选择继续在“Bert+fine-tuning”这条路上往后走,这也是造成今天落后局面的一个关键时间节点。再往后,就是InstructGPT和ChatGPT了,OpenAI通过ChatGPT证明了一点;虽然我们距离真正的AGI,可能还有很长的路要走,但是通过超大LLM走向AGI这条路,目前看是可行的。

#LLM~找不到推理错误

今年,大型语言模型(LLM)成为 AI 领域关注的焦点。LLM 在各种自然语言处理(NLP)任务上取得了显著的进展,在推理方面的突破尤其令人惊艳。但在复杂的推理任务上,LLM 的表现仍然欠佳。

那么,LLM 能否判断出自己的推理存在错误?最近,剑桥大学和 Google Research 联合开展的一项研究发现:LLM 找不到推理错误,但却能使用该研究提出的回溯(backtracking)方法纠正错误。LLM 找不到推理错误,但却能纠正错误!

  • 论文地址:https://arxiv.org/pdf/2311.08516.pdf
  • 数据集地址:https://github.com/WHGTyen/BIG-Bench-Mistake

这篇论文引起了一些争论,有人提出异议,比如在 Hacker News 上,有人评论这篇论文的标题言过其实,有些标题党。也有人批评说其中提出的校正逻辑错误的方法基于模式匹配,而非采用逻辑方法,这种方法其实容易失败。

Huang 等人在论文《Large language models cannot self-correct reasoning yet》中指出:自我校正或许是能有效地提升模型输出的风格和质量,但鲜有证据表明 LLM 有能力在没有外部反馈的情况下识别和纠正自身的推理和逻辑错误。比如 Reflexion 和 RCI 都使用了基本真值的纠正结果作为停止自我校正循环的信号。

剑桥大学和 Google Research 的研究团队提出了一种新思路:不再把自我校正看作一个单一过程,而是分成错误发现和输出校正两个过程:

  • 错误发现是一种基础推理技能,已经在哲学、心理学和数学领域得到了广泛的研究和应用,并催生了批判性思维、逻辑和数学谬误等概念。我们可以合理地认为发现错误的能力也应该是 对 LLM 的一项重要要求。但是,本文结果表明:当前最佳的 LLM 目前还无法可靠地发现错误。
  • 输出校正涉及部分或完全修改之前生成的输出。自我校正是指由生成输出的同一模型来完成校正。尽管 LLM 没有发现错误的能力,但本文表明:如果能提供有关错误的信息(如通过一个小型的监督式奖励模型),LLM 可以使用回溯方法校正输出。

本文的主要贡献包括:

  • 使用思维链 prompt 设计方法,任何任务都可以变成错误发现任务。研究者为此收集并发布了一个 CoT 类型的轨迹信息数据集 BIG-Bench Mistake,该数据集由 PaLM 生成,并标注了第一个逻辑错误的位置。研究者表示,BIG-Bench Mistake 在它的同类数据集中,是首个不局限于数学问题的数据集。
  • 为了测试当前最佳 LLM 的推理能力,研究者基于新数据集对它们进行了基准评测。结果发现,当前 SOTA LLM 也难以发现错误,即便是客观的明确的错误。他们猜测:LLM 无法发现错误是 LLM 无法自我校正推理错误的主要原因,但这方面还有待进一步研究。
  • 本文提出使用回溯方法来校正输出,利用错误的位置信息来提升在原始任务上的性能。研究表明这种方法可以校正原本错误的输出,同时对原本正确的输出影响极小。
  • 本文将回溯方法解释成了「言语强化学习」的一种形式,从而可实现对 CoT 输出的迭代式提升,而无需任何权重更新。研究者提出,可以通过一个经过训练的分类器作为奖励模型来使用回溯,他们也通过实验证明了在不同奖励模型准确度下回溯的有效性。

BIG-Bench Mistake数据集

BIG-Bench 由 2186 个 CoT 风格的轨迹信息集合组成。每个轨迹由 PaLM 2-L-Unicorn 生成,并标注了第一个逻辑错误的位置。表 1 展示了一个轨迹示例,其中错误位于第 4 步。

这些轨迹来自 BIG-Bench 数据集中的 5 个任务:词排序、跟踪经过混洗的对象、逻辑推演、多步算术和 Dyck 语言。

他们使用 CoT prompt 设计法来调用 PaLM 2,使其解答每个任务的问题。为了将 CoT 轨迹分成明确的步骤,他们使用了论文《React: Synergizing reasoning and acting in language  models》中提出的方法,分开生成每一步,并使用了换行符作为停止 token。

在该数据集中,生成所有轨迹时,temperature = 0。答案的正确性由精确匹配决定。

基准测试结果 

表 4 报告了 GPT-4-Turbo、GPT-4 和 GPT-3.5-Turbo 在新的错误发现数据集上的准确度。

对于每个问题,可能的答案有两种情况:要么没有错误,要么就有错误。如有错误,则数值 N 则会指示第一个错误出现的步骤。

所有模型都被输入了同样的 3 个 prompt。他们使用了三种不同的 prompt 设计方法:

  • 直接的轨迹层面的 prompt 设计 
  • 直接的步骤层面的 prompt 设计 
  • CoT 步骤层面的 prompt 设计

相关讨论

研究结果表明,这三个模型都难以应对这个新的错误发现数据集。GPT 的表现最好,但其在直接的步骤层面的 prompt 设计上也只能达到 52.87 的总体准确度。

这说明当前最佳的 LLM 难以发现错误,即使是在最简单和明确的案例中。相较之下,人类在没有特定专业知识时也能发现错误,并且具有很高的一致性。

研究者猜测:LLM 无法发现错误是 LLM 无法自我校正推理错误的主要原因。

prompt 设计方法的比较

研究者发现,从直接轨迹层面的方法到步骤层面的方法再到 CoT 方法,无错误的轨迹准确度显著下降。图 1 展示了这种权衡。

研究者猜测其原因是模型生成的输出的数量。这三种方法涉及到生成越来越复杂的输出:直接的轨迹层面的 prompt 设计方法需要单个 token,直接的步骤层面的 prompt 设计方法每步需要一个 token,CoT 步骤层面的 prompt 设计每步需要多个句子。如果每次生成调用都有一定的概率识别出错误,那么对每条轨迹的调用越多,模型识别出至少一个错误的可能性就越大。

将错误位置作为正确性代理的少样本 prompt 设计

研究者探究了这些 prompt 设计方法能否可靠地决定一个轨迹的正确性,而不是错误位置。

他们计算了平均 F1 分数,依据为模型能否预测轨迹中是否存在错误。如果存在错误,则假设模型预测的是该轨迹是 incorrect_ans。否则就假设模型预测的是该轨迹是 correct_ans。

使用 correct_ans 和 incorrect_ans 作为正例标签,并根据每个标签的出现次数进行加权,研究者计算了平均 F1 分数,结果见表 5。

这个加权 F1 分数表明,对于确定最终答案的正确性而言,通过 prompt 寻找错误是一个很糟糕的策略。

回溯

Huang 等人指出 LLM 无法在没有外部反馈的情况下自我校正逻辑错误。但是,在许多真实世界应用中,通常没有可用的外部反馈。

研究者在这项研究中采用了一种替代方案:用一个在少量数据上训练的轻量级分类器替代外部反馈。与传统强化学习中的奖励模型类似,这个分类器可以检测 CoT 轨迹中的任何逻辑错误,然后再将其反馈给生成器模型以提升输出。如果想要最大化提升,可以进行多次迭代。

研究者提出了一种简单的回溯方法,可以根据逻辑错误的位置来提升模型的输出:

  • 模型首先生成一个初始的 CoT 轨迹。在实验中,设置 temperature = 0。
  • 然后使用奖励模型确定轨迹中错误的位置。
  • 如果没有错误,就转向下一个轨迹。如果有错误,则再次向模型输入 prompt 以执行相同的步骤,但这一次 temperature = 1,生成 8 个输出。这里会使用同样的 prompt 以及包含错误步骤之前所有步骤的部分轨迹。
  • 在这 8 个输出中,过滤掉与之前的错误一样的选项。再从剩下的输出中选择对数概率最高的一个。
  • 最后,用新的重新生成的步骤替换之前步骤,再重新设置 temperature = 0,继续生成该轨迹的剩余步骤。

相比于之前的自我校正方法,这种回溯方法有诸多优势:

  • 新的回溯方法不需要对答案有预先的知识。相反,它依赖于有关逻辑错误的信息(比如来自训练奖励模型的信息),这可以使用奖励模型一步步地确定。逻辑错误可能出现在 correct_ans 轨迹中,也可能不出现在 incorrect_ans 轨迹中。 
  • 回溯方法不依赖于任何特定的 prompt 文本或措辞,从而可减少相关的偏好。 
  • 相比于需要重新生成整个轨迹的方法,回溯方法可以通过复用已知逻辑正确的步骤来降低计算成本。
  • 回溯方法可直接提升中间步骤的质量,这可能对需要正确步骤的场景来说很有用(比如生成数学问题的解),同时还能提升可解释性。       

研究者基于 BIG-Bench Mistake 数据集实验了回溯方法能否帮助 LLM 校正逻辑错误。结果见表 6。

∆accuracy✓ 是指在原始答案是 correct_ans 时,在轨迹集合上的 accuracy_ans 之差。

∆accuracy✗ 则是对于 incorrect_ans 轨迹的结果。

这些分数结果表明:校正 incorrect_ans 轨迹的收益大于改变原本正确的答案所造成的损失。此外,尽管随机基准也获得了提升,但它们的提升显著小于使用真正错误位置时的提升。注意,在随机基准中,涉及步骤更少的任务更可能获得性能提升,因为这样更可能找到真正错误的位置。

为了探索在没有好的标签时,需要哪种准确度等级的奖励模型,他们实验了通过模拟的奖励模型使用回溯;这种模拟的奖励模型的设计目标是产生不同准确度等级的标签。他们使用 accuracy_RM 表示模拟奖励模型在指定错误位置的准确度。

当给定奖励模型的 accuracy_RM 为 X% 时,便在 X% 的时间使用来自 BIG-Bench Mistake 的错误位置。对于剩余的 (100 − X)%,就随机采样一个错误位置。为了模拟典型分类器的行为,会按照与数据集分布相匹配的方式来采样错误位置。研究者也想办法确保了采样的错误位置与正确位置不匹配。结果见图 2。

可以看到 ∆accuracy✓ 的损失在 65% 时开始趋于稳定。事实上,对于大多数任务,在 accuracy_RM 大约为 60-70% 时,∆accuracy✓ 就已经大于 ∆accuracy✗ 了。这表明尽管更高的准确度能得到更好的结果,但即便没有黄金标准的错误位置标签,回溯也依然有效。

#LLM~推理优化

近日,CMU Catalyst 团队推出了一篇关于高效 LLM 推理的综述,覆盖了 300 余篇相关论文,从 MLSys 的研究视角介绍了算法创新和系统优化两个方面的相关进展。

在人工智能(AI)的快速发展背景下,大语言模型(LLMs)凭借其在语言相关任务上的杰出表现,已成为 AI 领域的重要推动力。然而,随着这些模型在各种应用中的普及,它们的复杂性和规模也为其部署和服务带来了前所未有的挑战。LLM 部署和服务面临着密集的计算强度和巨大的内存消耗,特别是在要求低延迟和高吞吐量的场景中,如何提高 LLM 服务效率,降低其部署成本,已经成为了当前 AI 和系统领域亟需解决的问题。

来自卡内基梅隆大学的 Catalyst 团队在他们的最新综述论文中,从机器学习系统(MLSys)的研究视角出发,详细分析了从前沿的 LLM 推理算法系统的革命性变革,以应对这些挑战。该综述旨在提供对高效 LLM 服务的当前状态和未来方向的全面理解,为研究者和实践者提供了宝贵的洞见,帮助他们克服有效 LLM 部署的障碍,从而重塑 AI 的未来。

论文链接:https://arxiv.org/abs/2312.15234

该论文的第一作者是卡内基梅隆大学的 Xupeng Miao(苗旭鹏)博士后研究员,合作者还包括 Tianqi Chen 和 Zhihao Jia 助理教授。此外,其他学生作者也均来自于 CMU Catalyst Group 实验室,该实验室由 Zhihao Jia 与 Tianqi Chen(陈天奇)在 CMU 共同主持,致力于集成来自于机器学习算法、系统、硬件等多方面的优化技术,构造自动化的机器学习系统。此前,该实验室还推出了 SpecInfer, MLC-LLM, SpotServe [ASPLOS‘24] 等开源项目,推进 LLM 大模型相关系统的研究和应用。实验室主页:https://catalyst.cs.cmu.edu。

综述概览

该综述系统地审视了现有 LLM 推理技术,覆盖了 300 余篇相关论文,从算法创新系统优化两个方面展开介绍。论文以此为基础,对现有工作设计了一套清晰且详尽的分类法,突出了各种方法的优势和局限性,逐类别搜集整理并介绍了每种方法的相关论文。除此之外,论文还对当前的主流 LLM 推理框架在系统设计与实现方面进行了深入的对比和分析。最后,作者对未来如何继续提高 LLM 推理效率进行了展望,在技术层面提出了六大潜在发展方向

分类法

算法创新

这一节对提出的各种算法和技术进行了全面分析,旨在改进大规模 Transformer 模型推理的原生性能缺陷,包括解码算法架构设计、和模型压缩等等。

解码算法:在这一部分中,我们回顾了在图 2 中展示的几种 LLMs 推理优化过程的新颖解码算法。这些算法旨在减少计算复杂度,并提高语言模型推理在生成任务中的总体效率,包括:

  • 非自回归解码:现有 LLMs 的一个主要限制是默认的自回归解码机制,它逐个顺序生成输出 token。为解决这一问题,一种代表性的工作方向是非自回归解码 [97, 104, 108,271],即放弃自回归生成范式,打破单词依赖并假设一定程度的条件独立性,并行解码输出 token。然而,尽管这类方法解码速度有所提高,但大多数非自回归方法的输出质量仍不如自回归方法可靠。
  • 投机式推理:另一类工作是通过投机执行思想 [47] 实现并行解码。自回归 LLM 推理过程中的每个解码步骤都可以被视为带有条件分支的程序执行语句,即决定接下来生成哪个 token。投机式推理 [51, 155] 先使用较小的草稿模型进行多步解码预测,然后让 LLM 同时验证这些预测以实现加速。然而,将投机解码应用于 LLMs 时仍然存在一些实际挑战,例如,如何使解码预测足够轻量且准确,以及如何借助 LLMs 实现高效的并行验证。SpecInfer [177] 首次引入基于 tree-based speculative decoding 和 tree attention,并提出了一个低延迟 LLM 服务系统实现,该机制也被后续多个工作 [48, 118, 168, 185, 229, 236, 274, 310] 直接采用。
  • 提前退出:这类方法主要利用 LLMs 的深层多层结构,在中间层提前推出推理,中间层输出可以通过分类器转化成输出的 token,从而降低推理开销 [117, 147, 163, 167, 234, 272, 282, 291, 308],它们也被称为自适应计算 [68, 219]。
  • 级联推理:这类方法级联了多个不同规模的 LLM 模型,用于分别处理不同复杂度的推理请求,代表性工作包括 CascadeBERT [157] 和 FrugalGPT [53]。

架构设计:

  • 配置缩小:直接缩小模型配置。
  • 注意力简化:最近出现了很多研究工作,它们主要是将之前的长序列高效注意力机制 [240] 应用在 LLM 上,以缩短上下文,减少 KV 缓存,以及注意力复杂度,同时略微降低解码质量(如滑动窗口 [129, 299]、哈希 [198]、dilated [74]、动态选择等等)。表 1 中总结了一些近期的热门方法和之前的工作之间的对应关系。

  • 激活共享:这类方法主要是通过共享 attention 计算的中间激活来降低推理内存开销,代表性工作包括 MQA [220] 和 GQA [32]。
  • 条件计算:这类方法主要是指稀疏专家混合模型(Sparse MoE),比如最近大火的 Mistrial 7Bx8 模型就属于此类。
  • 循环单元:尽管 Transformer 已经替代了 RNN 模型,但考虑到注意力机制的二次复杂性,人们始终未曾放弃将 recurrent unit 机制重新引入 LLM 的尝试,比如 RWKV [200]、RetNet [235],以及状态空间模型 [91, 102, 103, 176] 等等。

模型压缩:

  • 知识蒸馏:这类方法以大型的教师模型为监督,训练一个小型的学生模型。大多数之前的方法都在探索白盒蒸馏 [106, 133, 214, 233, 255],需要访问整个教师模型的参数。由于基于 API 的 LLM 服务(如 ChatGPT)的出现,一些黑盒蒸馏模型吸引了很多关注 [238,59, 273, 201, 313],这些模型通常具有更少的模型参数,与原始 LLMs(如 GPT-4 [195])相比,在各种下游任务上表现出了相当的性能。   
  • 网络剪枝:过去几年中,网络剪枝方法 [180, 215, 215] 已被广泛研究,但并非所有方法都可以直接应用于 LLMs,需要考虑重新训练可能带来的过高计算成本,以及评估剪枝是否可以在底层系统实现上取得效率提升。大致上可以分为结构化剪枝 [80, 149, 174, 216, 172] 和半结构化稀疏化 [40, 87, 232, 251, 276] 等。

系统优化

本节研究 LLM 推理系统优化技术,以加速 LLM 推理,而不改变 LLM 计算语义。这一工作的目标是通过改进用于大型语言模型推理的底层系统和框架来提高系统效率,包括低比特量化、并行计算、内存管理、请求调度、和内核优化等等,详细内容可以参见论文原文。

软件框架

论文还对一些目前最先进的基于 GPU 的开源 LLM 推理系统进行了深入的分析,并从多个方面总结了它们在设计与实现伤的差异。

未来方向

  • 专用硬件加速器的发展:生成型 LLM 服务效率的显著提升可能在很大程度上依赖于专用硬件加速器的发展和提升,尤其是软硬协同设计方法。例如,让内存单元更加接近处理单元,或是针对 LLM 算法数据流优化芯片架构,这些硬件优化可以在很大程度上为 LLM 推理在软件层面带来便利和机会。
  • 高效有效的解码算法:开发更高效的解码算法可以显著提高服务效率。受对实时应用更快生成速度的需求驱动,一个有前途的方向是广义的投机式推理(generalized speculative inference),不仅会带来显著加速,同时保持相同的生成质量。正如 SpecInfer 中所指出的,广义的投机式推理中,用于生成草稿 token 的小模型可以被替换为任何快速的 token 生成方法,比如自定义函数、召回方法、甚至早停机制和非自回归解码等等。
  • 长上下文 / 序列场景优化:随着应用场景变得更加复杂,处理更长的上下文或序列的需求不断增长。服务长序列负载的 LLM 需要解决算法和系统两方面的挑战。在算法方面,它们依然面临长度泛化失效问题,甚至可能出现 “loss in the middle” 的情况。目前的解法主要是通过召回增强、序列压缩和缓存来尽可能缩短序列长度并保存相关信息。
  • 探索替代基础架构:尽管 Transformer 模型和自注意力机制目前主导着 LLM 领域,但探索替代架构是未来研究的一个有前景的方向。例如,一些最新研究探索了无注意力方法,使用纯 MLP(多层感知机)架构来替代注意力机制,可能会改变目前 LLM 推理优化的格局。
  • 在复杂环境中的部署探索:随着 LLM 应用的扩展,探索并优化它们在各种复杂环境中的部署成为一个关键的未来方向。这一探索不仅限于传统的基于云的部署,还包括边缘计算、混合计算(cloud+edge)、去中心化计算以及廉价的可抢占资源等。
  • 特定需求的自动适应:应用特定需求的多样性创造了一系列创新的 LLM 服务优化机会,例如模型微调(parameter-efficient fine-tuning)、向量数据库检索、多模态负载等等。这些独特的挑战也要求将 LLM 服务技术自动且顺利地集成到现有 IT 基础设施中,将优化空间扩展到整个 LLM 生命周期。

总结

总的来说,该综述不仅是对当前 LLM 服务优化研究的全面概述,也为未来在这一领域的探索和发展指明了方向。通过深入了解这些先进的解决方案,研究者和实践者可以更好地理解和应对在实际应用中部署大型语言模型时面临的挑战。

#LLM-Shearing

手把手教你剪「羊驼」,陈丹琦团队提出LLM-Shearing大模型剪枝法,给 Llama 2(羊驼)大模型剪一剪驼毛,会有怎样的效果呢?今天普林斯顿大学陈丹琦团队提出了一种名为 LLM-Shearing 的大模型剪枝法,可以用很小的计算量和成本实现优于同等规模模型的性能。

自大型语言模型(LLM)出现以来,它们便在各种自然语言任务上取得了显著的效果。不过,大型语言模型需要海量的计算资源来训练。因此,业界对构建同样强大的中型规模模型越来越感兴趣,出现了 LLaMA、MPT 和 Falcon,实现了高效的推理和微调。

这些规模不等的 LLM 适用于不同的用例,但从头开始训练每个单独的模型(即使是 10 亿参数小模型)还是需要大量计算资源,这对于大多数科研机构而言仍是很大的负担。

因此在本文中,普林斯顿大学陈丹琦团队试图解决以下问题:能否利用现有预训练 LLM 来构建一个规模更小、通用且在性能上有竞争力的 LLM,同时比从头开始训练需要的计算量少得多?

研究者探索利用结构化剪枝来实现目标。这里的问题是,对于通用 LLM,剪枝后的模型会出现性能下降,尤其是在剪枝后没有大量计算投入的情况。他们使用的高效剪枝方法可以用来开发规模更小但仍具有性能竞争力的 LLM,并且与从头开始训练相比,训练需要的计算量也大大减少。

  • 论文地址: https://arxiv.org/abs/2310.06694
  • 代码地址: https://github.com/princeton-nlp/LLM-Shearing
  • ModelsSheared-LLaMA-1.3B, Sheared-LLaMA-2.7B

在对 LLM 进行剪枝之前,研究者确定了两个关键技术挑战,一是如何确定最终的性能强大、推理高效的剪枝结构?LLM 目前的结构化剪枝技术没有指定的目标结构,导致剪枝后模型在性能和推理速度方面不理想;二是如何继续预训练剪枝后的模型以达到预期性能?他们观察到,与从头开始训练模型相比,使用原始预训练数据来训练会导致不同域出现不同的损失减少。

针对这两个挑战,研究者提出了「LLM - shearing」算法。这种新颖的剪枝算法被称为「定向结构化剪枝」,它将源模型剪枝为指定的目标架构,该结构通过现有预训练模型的配置来确定。他们表示,该剪枝方法在源模型中搜索子结构,并在资源受限的情况下最大程度地保持性能。此外设计一种动态批量加载算法,它能根据损失减少率按比例加载每个域的训练数据,从而高效利用数据并加速整体性能的提升。

最终,研究者将 LLaMA2-7B 模型剪枝成了两个较小的 LLM,分别是 Sheared-LLaMA-1.3B 和 Sheared-LLaMA-2.7B,证实了其方法的有效性。

他们仅仅使用 500 亿个 token(即 OpenLLaMA 预训练预算的 5%)进行剪枝和继续预训练,但对于 11 个代表性下游任务(如常识、阅读理解和世界知识)以及开放式生成的指令调整,这两个模型的性能仍然优于其他同等规模的流行 LLM,包括 Pythia、INCITE 和 OpenLLaMA。

不过要提到一点,在这篇论文发布 Sheared-LLaMA-3B 的时候,最强 3B 开源模型的纪录已经被 StableLM-3B 打破了。

此外,下游任务性能轨迹表明,使用更多 token 来进一步训练剪枝后的模型,将带来更大的收益。研究者只对最多 70 亿参数的模型进行了实验,但 LLM-shearing 具有高度通用性,可以在未来的工作中扩展到任何规模的大型语言模型。

方法介绍

给定一个现有的大模型 M_S(源模型),本文目标是研究如何有效地生成一个更小、更强的模型 M_T(目标模型)。该研究认为这需要两个阶段来完成:

  • 第一阶段将 M_S 剪枝为 M_T,虽然这样减少了参数数量,但不可避免地导致性能下降;
  • 第二阶段持续预训练 M_T,使其性能更强。

结构化剪枝

结构化剪枝可以去除模型大量参数,从而达到压缩模型并加速推理的效果。然而,现有的结构化剪枝方法会导致模型偏离常规架构的配置。例如 CoFiPruning 方法产生的模型具有不统一的层配置,与标准的统一层配置相比,这样会产生额外的推理开销。

本文对 CoFiPruning 进行了扩展,以允许将源模型剪枝为指定的任何目标配置。例如,本文在生成 2.7B 模型时使用 INCITE-Base-3B 架构作为目标结构。

此外,本文还在不同粒度的模型参数上学习一组剪枝掩码( pruning mask),掩码变量如下所示:

每个掩码变量控制是否剪枝或保留相关的子结构。例如,如果对应的 z^layer= 0,则需要删除这个层。下图 2 说明了剪枝掩码如何控制被剪枝的结构。

剪枝之后,本文通过保留与每个子结构中的掩码变量相关的最高得分组件来最终确定剪枝后的架构,并继续使用语言建模目标对剪枝后的模型进行预训练。

动态批量加载

该研究认为对剪枝后的模型进行大量预训练是很有必要的,这样才能恢复模型性能。

受其他研究的启发,本文提出了一种更有效的算法,即动态批量加载,其可以根据模型性能简单地动态调整域比例。算法如下:

实验及结果

模型配置:本文将 LLaMA2-7B 模型作为源模型,然后进行结构化剪枝实验,他们将 LLaMA2-7B 压缩成两个较小的目标尺寸 2.7 B 和 1.3B 参数,并将剪之后的模型与相同尺寸的模型进行了性能比较,包括 OPT-1.3B、Pythia-1.4B、OPT-2.7B、 Pythia-2.8B、INCITE-Base-3B、OpenLLaMA-3B-v1、OpenLLaMA-3B-v2。表 8 总结了所有这些模型的模型体系结构细节。

数据:由于 LLaMA2 的训练数据并不是公开访问的,因此本文使用了 RedPajama 数据集 。表 1 提供了本文模型和基线模型使用的预训练数据。

训练:研究者在所有实验中最多使用了 16 个 Nvidia A100 GPU (80GB)。

SHEARED-LLAMA 优于同等大小的 LM

本文表明,Sheared-LLaMA 明显优于现有的类似规模的 LLM,同时只使用一小部分计算预算来从头开始训练这些模型。

下游任务:表 2 展示了 Sheared-LLaMA 和类似大小的现有预训练模型的零样本和少样本在下游任务上的性能。

指令调优:如图 3 所示,与同等规模的所有其他预训练模型相比,指令调优的 Sheared-LLaMA 实现了更高的获胜率。

图 4 显示了 INCITEBase-3B 模型开始时的精度要高得多,但其性能在持续的预训练过程中趋于稳定。

分析

最后,研究者对本文方法的优势进行了分析。

动态批量加载的有效性

其中,研究者从以下三个方面的影响来分析动态批量加载的有效性:(1) 跨域的最终 LM 损失,(2) 整个训练过程中每个域的数据使用情况,(3) 下游任务性能。结果均基于 Sheared-LaMA-1.3B 算法。

跨域损失差异。动态批量加载的目的是平衡各域的损失降低率,使损失在大致相同的时间内达到参考值。图 5 中绘制了模型损耗(原始批量加载和动态批量加载)与参考损耗之间的差异,相比之下,动态批量加载能均匀地减少损失,各域的损失差异也非常相似,这表明数据使用效率更高。

数据使用情况。表 3 对比了 RedPajama 的原始数据比例和动态加载的域数据使用情况(图 7 展示了整个训练过程中域权重的变化)。与其他域相比,动态批量加载增加了 Book 和 C4 域的权重,这表明这些域更难恢复剪枝模型。 

下游性能。如图 6 所示,与在原始 RedPajama 分布上训练的模型相比,使用动态批量加载训练的剪枝模型获得了更好的下游性能。这表明,动态批量加载所带来的更均衡的损失减少可以提高下游性能。

与其他剪枝方法的对比

此外,研究者将 LLM-shearing 方法与其他剪枝方法进行了比较,并报告了验证困惑度,它是衡量整体模型能力的一个有力指标。

由于计算上的限制,下面的实验控制了所有比较方法的总计算预算,而不是将每种方法运行到最后。

如表 4 所示,在相同稀疏度下,本文的目标剪枝模型的推理吞吐量比非均匀剪枝 CoFiPruning 模型更高,但困惑度略高。

其他分析

表 5 显示,在控制 token 总量的情况下,增加剪枝开销可以持续改善困惑度。然而,由于剪枝比持续的预训练更昂贵,研究者将 0.4B 的 token 分配给剪枝。

#LLMの推理过程

以LLAMA为例

本文借助llama这个模型快速入门LLM的推理过程,技术细节很多都是通用的,也适合其他的LLM。本篇也可以作为CV快速入门NLP简易指南。

这篇也算是总结的性质,仅需要的知识点都列出来,简单介绍一番。可能说的不够详细,想看具体细节的小伙伴可参考文中引用的相关详细文章。

本篇也可以作为CV快速入门NLP简易指南(当然也是老潘的个人笔记)。当然只是推理哈,不涉及到训练,仅是从部署的角度考虑这个模型,个人也不是NLP专家,如对文中内容有疑问,欢迎讨论。

什么是LLM

llama是LLM(LLM指大语言模型)模型的一种结构,llama这种模型的任务就是在阅读前n个单词后预测句子中下一个单词,输出取决于过去和现在输入,与未来无关。

过程大概是这样,每次输入模型会带上上一次输出的结果(不同于CV模型,CV模型输入只需要一次即可,一次推理即可得到结果):

LLM-生成

一般来说,LLM模型主要由两个块组成:

  • 编码器(左侧):编码器接收输入并构建其表示形式(特征)。这意味着该模型被优化为从输入中获取理解(比如输入文本判断这段话是高兴还是难受)。
  • 解码器(右侧):解码器使用编码器的表示形式(特征)以及其他输入来生成目标序列。这意味着该模型被优化用于生成输出。

编码器和解码器

这些部分都可以根据任务独立使用:

  • Encoder-only models:适用于需要理解输入的任务,例如句子分类和命名实体识别。
  • Decoder-only models:适用于生成性任务,如文本生成
  • Encoder-decoder models or sequence-to-sequence models:适用于需要输入的生成性任务,例如翻译或摘要。whao~开发板商城~aiot物联网设备

LLAMA属于Decoder-only models,只有decoder层。

llama相关的知识点

llama的decoder部分的结构取自transformer,关于transformer的介绍知乎上太多,这里不赘述了,想详细理解transformer的建议看知乎上的其他优秀文章以及b站沐神的讲解

transformer的结构如下:

transformer

对于llama来说,只用了decoder部分,重点关注这几个概念:

  • Tokenization 分词器
  • Embedding 嵌入层
  • Positional Encoding 位置编码
  • Self-attention 自注意力机制
  • Multi-head attention 多头注意力与采用掩码机制的多头注意力
  • Batch Norm & Layer Norm 批标准化/层标准化   llama用的是RMSNorm
  • ResNet 残差网络

具体的可以查阅相关的博文,这里同样不进行赘述,只会简单描述下过程。拿到llama模型后,主要关注:

  • 模型的结构,包含哪些算子哪些op,模型复杂度
  • 模型的前后处理,前后处理实现细节,模型的执行方式
  • 模型各种参数配置以及其他一些细节

llama的整个执行过程分了好多步,涉及到很多前后处理,前后处理又涉及到很多细节的步骤,之后也会介绍。

先过一下知识点。

分词器、token、embeding

主要是分词、编码、Tokenizer(tokenization)、embed(embedding)的过程。

分词过程

什么是分词?也就是Tokenizer的作用。

分词器可将原始文本转换为由token组成的文本的初始数值表征。

分词器之所以是模型的重要构成部分之一,是因为模型可借此妥善应对人类语言的复杂性。例如,分词器可将凝集性语言中的词分解为更易管理的组成部分、处理原始语料库中不存在的新词或外来词/特殊字符,并确保模型生成紧凑(尽可能精简)的文本表征。

每种语言都有可选用的数量众多且各不相同的分词器。大部分基于 Transformer 的架构均使用经过训练的分词器,这些分词器旨在充分缩短典型序列的长度。WordPiece(应用于 BERT)、SentencePiece(应用于 T5 或 RoBerta)等分词器同样具有多个变体,这是因为众多的语言和不同的专业领域(如医疗健康语料库)采用了这些分词器,且这些分词器在训练方式上也有所不同(选用不同的最大标记数,或以不同的方式处理大小写)。

然后看回代码。

首先看tokenizer,运行llama的时候我们会调用​​tokenizer = AutoTokenizer.from_pretrained(args.model, use_fast=False)​​。

如果我们模型传入的是llama的某个模型(llama-7b)那么返回的就是LlamaTokenizer:

class LlamaTokenizer(PreTrainedTokenizer):  """  Construct a Llama tokenizer. Based on byte-level Byte-Pair-Encoding.  
...

这个类是LLAMA模型的分词器(tokenizer)的实现,基于字节级的字节对编码(Byte-Pair Encoding)。这个分词器的主要功能是将文本字符串转换为模型可以理解的数字序列,反之亦然。这里假设输入为 '"this is a python code:"'  。

具体我们看干了些啥,创建好tokenizer之后我们执行:​​input_ids = tokenizer.encode(args.text, return_tensors="pt").to(dev)​​,这里又分两步:

  • 第一步是 Converts a string in a sequence of tokens (string), using the tokenizer. Split in words for word-based vocabulary or sub-words for sub-word-based vocabularies (BPE/SentencePieces/WordPieces). 这里调用 ​​self.sp_model.encode(text, out_type=str)​​,sp_model就是sentencepiece中的一个函数,执行完出来变为  ['▁"', 'this', '▁is', '▁a', '▁python', '▁code', ':"']
  • 第二步将token string转变为 token id  -> Converts a token string (or a sequence of tokens) in a single integer id (or a sequence of ids), using the vocabulary. 具体就是个for循环,对之前分好的tokens一个一个转。

最后我们得到:

input_ids  
tensor([[   0,  376, 1366,  338,  263, 3017,  775, 6160]], device='cuda:0')  
input_ids.shape  
torch.Size([1, 8])

至于如何转换为embedding,之后会调用:

​inputs_embeds = self.embed_tokens(input_ids)​​​,其中embeds的shape是​​torch.Size([1, 8, 4096])​​。

在自然语言处理(NLP)中,嵌入(Embedding)是一种将离散变量(如单词、短语、或者文档)转换为连续向量的方法。这种转换的目的是让计算机能更好地理解和处理自然语言数据。embedding矩阵的本质是一个查找表 ,每个单词会定位这个表中的某一行,而这一行就是这个单词学习到的在嵌入空间的语义。

自注意力 Self-Attention

Transformer模型的一个关键特点是使用了称为注意力层的特殊层。“Attention Is All You Need”。

这一层会告诉模型,在处理每个单词的表示时,要对你传递给它的句子中某些单词特别关注(并且忽略其他单词)。

把这个概念放到背景中,考虑从英语翻译成法语的任务。给定输入“你喜欢这门课程”,翻译模型还需要关注相邻单词“你”,以获得正确翻译动词“like”的形式,因为在法语中,“like”根据主语不同而有不同变化形式。然而,句子其余部分对于该单次翻转来说没有用处。同样地,在翻转“This”时,模型也需要注意到单次“course”,因为“This”根据相关名字是否男性或女性而有不同意思表达方式。再次强调,在更复杂的句子(和更复杂的语法规则)中,模型需要特别关注可能出现在句子较远位置上以正确翻译每个单次的单词。

同样的概念适用于与自然语言相关的任何任务:一个单次本身具有意义,但是该意义深受上下文影响,这可以是正在研究的单次之前或之后的任何其他单次(或多个)。

Self-attention是Transformer的核心,其允许模型考虑到序列中的其他标记,以便更好地理解每个标记的上下文。每个标记的新表示形式是由它自己和其他标记的交互得到的。

位置编码

由于Transformer的结构没有考虑到标记的顺序,所以我们需要加入位置编码来给模型提供词元在序列中的位置信息。这些编码会被添加到词嵌入向量中。

多头注意力(Multi-head Attention)

多头注意力是对自注意力机制的扩展。它将自注意力分解为多个“头”,每个头在不同的表示空间中学习和应用自注意力。这允许模型同时捕捉到各种不同类型的信息。在有掩码的多头注意力中,掩码被用于阻止模型查看某些不应该看到的信息,例如在生成新的标记时阻止查看未来的信息。现在基本都使用MHA,一般不用单头。

批标准化(Batch Norm) & 层标准化(Layer Norm)

这些都是用于正规化激活的技术,可以加速学习,提高模型的性能。

批标准化是在整个批次的数据上进行标准化,而层标准化则是在单个数据样本上进行标准化。RMSNorm是一种新的归一化方法,是对LayerNorm的一个改进,没有做re-center操作(移除了其中的均值项),可以看作LayerNorm在均值为0时的一个特例。

残差网络(ResNet)

老熟人了。通过在网络中添加跳跃连接(或称为“skip”连接),可以使得模型更容易地学习到恒等映射,从而避免了训练深度网络时常见的梯度消失问题。在Transformer中,每个子层(如自注意力层和前馈神经网络层)都有一个对应的残差连接,并且每个子层的输出都会进行层标准化。

LLAMA的模型结构

我们可以很轻易的通过huggingface代码库中看到llama的模型结构。

以hugging库中的7B模型为例,运行​​model = LlamaForCausalLM.from_pretrained(model, torch_dtype='auto')​​后,可以通过print看模型结构:

LlamaForCausalLM(  (model): LlamaModel(  (embed_tokens): Embedding(32000, 4096, padding_idx=31999)  (layers): ModuleList(  (0-31): 32 x LlamaDecoderLayer(  (self_attn): LlamaAttention(  (q_proj): Linear(in_features=4096, out_features=4096, bias=False)  (k_proj): Linear(in_features=4096, out_features=4096, bias=False)  (v_proj): Linear(in_features=4096, out_features=4096, bias=False)  (o_proj): Linear(in_features=4096, out_features=4096, bias=False)  (rotary_emb): LlamaRotaryEmbedding()  )  (mlp): LlamaMLP(  (gate_proj): Linear(in_features=4096, out_features=11008, bias=False)  (down_proj): Linear(in_features=11008, out_features=4096, bias=False)  (up_proj): Linear(in_features=4096, out_features=11008, bias=False)  (act_fn): SiLUActivation()  )  (input_layernorm): LlamaRMSNorm()  (post_attention_layernorm): LlamaRMSNorm()  )  )  (norm): LlamaRMSNorm()  )  (lm_head): Linear(in_features=4096, out_features=32000, bias=False)  
)

7B有32个​​LlamaDecoderLayer​​,每个Decoder包含一个LlamaAttention和LlamaMLP,然后是LlamaRMSNorm和head部分,核心的结构是transformer。

先看核心的​​LlamaDecoderLayer​​,7B有32个,而30B的话有60个,30B和7B的差别也就是decoder的个数和decoder的不同配置。

7B-config和30B-config区别

看下7B模型的config,可以看到模型类型为float16,use_cache设置为true

{  "architectures": [  "LLaMAForCausalLM"  ],  "bos_token_id": 0,  "eos_token_id": 1,  "hidden_act": "silu",  "hidden_size": 4096,  "intermediate_size": 11008,  "initializer_range": 0.02,  "max_sequence_length": 2048,  "model_type": "llama",  "num_attention_heads": 32,  "num_hidden_layers": 32,  "pad_token_id": 0,  "rms_norm_eps": 1e-06,  "torch_dtype": "float16",  "transformers_version": "4.27.0.dev0",  "use_cache": true,  "vocab_size": 32000  
}

运行pipeline

llama的运行流程和大多数的LLM一样,流程如下:     

  • 分词encode,我理解相当于预处理
  • 输入input_ids后模型开始运行,这里会for循环运行好多次
  • 运行完后得到logits进行后处理预测下一个token
  • 循环往复直到所有要求数量的token都输出或者输出遇到了end_id

LLM整体流程图,来自huggingface

再回顾下LLAMA的模型组成:

class LlamaModel(LlamaPreTrainedModel):  def __init__(self, config: LlamaConfig):  super().__init__(config)  self.padding_idx = config.pad_token_id  self.vocab_size = config.vocab_size  self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx)  self.layers = nn.ModuleList([LlamaDecoderLayer(config) for _ in range(config.num_hidden_layers)])  self.norm = LlamaRMSNorm(config.hidden_size, eps=config.rms_norm_eps)  self.gradient_checkpointing = False  # Initialize weights and apply final processing  self.post_init()  ...

主要部分就三个:

  • embed_tokens   嵌入层,它将输入的词(或者词的索引)映射到一个高维空间中,使得具有相似语义的词在这个空间中的距离更近。这个嵌入层在训练过程中会被优化,以更好地捕捉单词之间的语义关系。这里使用的嵌入层是一个 PyTorch 的 nn.Embedding 层,它将大小为 config.vocab_size 的词汇表中的每个词映射到一个 config.hidden_size 维的向量。self.padding_idx 是用于指定在嵌入矩阵中,哪个索引是用于填充的,通常这个填充索引对应的嵌入向量会被初始化为零,并在训练过程中保持为零
  • layers   num_hidden_layers个解码器
  • norm   RMSNorm 归一化函数,和layernorm类似

第一步 分词

调用​​tokenizer.encode(args.text, return_tensors="pt").to(DEV)​​,具体流程如下:

输入prompt -> '"this is a python code:"'

->  ['▁"', 'this', '▁is', '▁a', '▁python', '▁code', ':"']

->  [376, 1366, 338, 263, 3017, 775, 6160]

-> {'input_ids': tensor([[   0,  376, 1366,  338,  263, 3017,  775, 6160]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1]])}

-> 然后开始generation,开始第二步

第二步 配置

主要就是配置一些信息和做预处理。

设置min_length、max_length、top_p、temperature进入model.generate

-> 处理、配置、验证generation_config 设置 generation parameters

-> 设置模型的输入

inputs_tensor, model_input_name, model_kwargs = self._prepare_model_inputs(   inputs, generation_config.bos_token_id, model_kwargs   )

bos_token_id 是 0, inputs就是刚才传过来的input_ids。

-> 设置模型的参数

model_kwargs["attention_mask"] = self._prepare_attention_mask_for_generation(   inputs_tensor, generation_config.pad_token_id, generation_config.eos_token_id   )

其中 pad_token_id为 -1,eos_token_id为1,一般从config中获取。

-> 设置 max_length -> input_ids_seq_length = input_ids.shape[-1]

-> 决定generation mode

-> prepare distribution pre_processing samplers

-> prepare stopping criteria

-> 设置好 sample_gen_mode、 prepare logits warper

-> expand input_ids with num_return_sequences additional sequences per batch

设置好的模型参数

第三步 sample

继续准备模型的输入,调用​​self.prepare_inputs_for_generation​​(如果提供了past_key_values,那么input_ids = input_ids[:, -1:],同时根据attention_mask和是否提供past_key_values计算出position_ids,也就是说提供了past_key_values的话,input_ids可以少计算很多)返回model_inputs。

model_inputs

-> 开始进入auto-regressive generation的循环,是个while True

进入LlamaForCausalLM函数,输入刚才的​​model_inputs​​,主要包含input_ids、attention_mask、position_ids、past_key_values

-> 根据是否存在 past_key_values 更新 past_key_values_length 和 seq_length_with_past

-> 判断inputs_embeds是否存在判断是否需要调用​​self.embed_tokens​​,也就是说如果自己提供了embeds就不需要在这里单独embed了

这里调用 inputs_embeds = self.embed_tokens(input_ids),embeds的shape是​​torch.Size([1, 8, 4096])​​,8代表输入input_ids的长度,我这里是8

-> 调用_prepare_decoder_attention_mask函数,调用后的attention_mask维度为​​torch.Size([1, 1, 8, 8])​​:

attention_mask = self._prepare_decoder_attention_mask(  attention_mask, (batch_size, seq_length), inputs_embeds, past_key_values_length  )

进入一个for循环,  因为llama有很多​​self.layers = nn.ModuleList([LlamaDecoderLayer(config) for _ in range(config.num_hidden_layers)])​​​,都是一模一样的​​LlamaDecoderLayer​​:

for idx, decoder_layer in enumerate(self.layers):  if output_hidden_states:  all_hidden_states += (hidden_states,)  # 如果存在past_key_value,则一并传进去  past_key_value = past_key_values[idx] if past_key_values is not None else None  layer_outputs = decoder_layer(  hidden_states,  attention_mask=attention_mask,  position_ids=position_ids,  past_key_value=past_key_value,  output_attentions=output_attentions,  use_cache=use_cache  )  hidden_states = layer_outputs[0]  if use_cache:  next_decoder_cache += (layer_outputs[2 if output_attentions else 1],)  if output_attentions:  all_self_attns += (layer_outputs[1],)

这里插播个结构分析,我们重点看LlamaDecoderLayer函数。

LlamaDecoderLayer

函数的传入的参数:​​hidden_states [1,8,4096]、attention_mask [1,1,8,8]、position_ids [1,8]、past_key_value [[]] or None​​。

执行的程序也很简单,顺序执行以下步骤:

  • 首先归一化 self.input_layernorm(hidden_states)
  • 然后经过 self_attn
  • 残差 hidden_states = residual(之前residual = hidden_states) + hidden_states(经过self_attn后)
  • 全连接 self.post_attention_layernorm(hidden_states) -> self.mlp(hidden_states)
  • 继续残差 hidden_states = residual(之前经过self_attn后) + hidden_states(经过全连接后)

代码如下:

residual = hidden_states  
hidden_states = self.input_layernorm(hidden_states)  
# Self Attention  
hidden_states, self_attn_weights, present_key_value = self.self_attn(  hidden_states=hidden_states,  attention_mask=attention_mask,  position_ids=position_ids,  past_key_value=past_key_value,  output_attentions=output_attentions,  use_cache=use_cache,  
)  
hidden_states = residual + hidden_states  
# Fully Connected  
residual = hidden_states  
hidden_states = self.post_attention_layernorm(hidden_states)  
hidden_states = self.mlp(hidden_states)  
hidden_states = residual + hidden_states  
outputs = (hidden_states,)  
if output_attentions:  outputs += (self_attn_weights,)  
if use_cache:  outputs += (present_key_value,)  
return outputs

然后我们看下核心的LlamaAttention:

LlamaAttention

这个就是Multi-headed attention from 'Attention Is All You Need' paper。这个类的成员变量如下:

class LlamaAttention(nn.Module):  def __init__(self, config: LlamaConfig):  super().__init__()  self.config = config  self.hidden_size = config.hidden_size  self.num_heads = config.num_attention_heads # head的数量 这里是32  self.head_dim = self.hidden_size // self.num_heads  # head的大小 这里是128  self.max_position_embeddings = config.max_position_embeddings  if (self.head_dim * self.num_heads) != self.hidden_size:  raise ValueError(  f"hidden_size must be divisible by num_heads (got `hidden_size`: {self.hidden_size}"  f" and `num_heads`: {self.num_heads})."  )  # 线性层self.q_proj, self.k_proj, self.v_proj将输入hidden_states映射到num_heads * head_dim的维度,以分别获得查询、键、值tensor。  self.q_proj = nn.Linear(self.hidden_size, self.num_heads * self.head_dim, bias=False)  self.k_proj = nn.Linear(self.hidden_size, self.num_heads * self.head_dim, bias=False)  self.v_proj = nn.Linear(self.hidden_size, self.num_heads * self.head_dim, bias=False)  self.o_proj = nn.Linear(self.num_heads * self.head_dim, self.hidden_size, bias=False)  self.rotary_emb = LlamaRotaryEmbedding(self.head_dim, max_position_embeddings=self.max_position_embeddings)  ...

其中,多头机制的自注意力:

  • self.num_heads定义了attention head的数量
  • self.head_dim定义了每个head的大小,是hidden_size除以num_heads
  • 线性层self.q_proj, self.k_proj, self.v_proj将输入hidden_states映射到num_heads * head_dim的维度,以分别获得查询、键、值tensor

操作代码如下:

def forward(  self,  hidden_states: torch.Tensor,  attention_mask: Optional[torch.Tensor] = None,  position_ids: Optional[torch.LongTensor] = None,  past_key_value: Optional[Tuple[torch.Tensor]] = None,  output_attentions: bool = False,  use_cache: bool = False,  
) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple[torch.Tensor]]]:  bsz, q_len, _ = hidden_states.size()  query_states = self.q_proj(hidden_states).view(bsz, q_len, self.num_heads, self.head_dim).transpose(1, 2)  key_states = self.k_proj(hidden_states).view(bsz, q_len, self.num_heads, self.head_dim).transpose(1, 2)  value_states = self.v_proj(hidden_states).view(bsz, q_len, self.num_heads, self.head_dim).transpose(1, 2)  kv_seq_len = key_states.shape[-2]  if past_key_value is not None:  kv_seq_len += past_key_value[0].shape[-2]  cos, sin = self.rotary_emb(value_states, seq_len=kv_seq_len)  query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin, position_ids)  # [bsz, nh, t, hd]  if past_key_value is not None:  # reuse k, v, self_attention  key_states = torch.cat([past_key_value[0], key_states], dim=2)  value_states = torch.cat([past_key_value[1], value_states], dim=2)  past_key_value = (key_states, value_states) if use_cache else None  attn_weights = torch.matmul(query_states, key_states.transpose(2, 3)) / math.sqrt(self.head_dim)  if attn_weights.size() != (bsz, self.num_heads, q_len, kv_seq_len):  raise ValueError(  f"Attention weights should be of size {(bsz * self.num_heads, q_len, kv_seq_len)}, but is"  f" {attn_weights.size()}"  )  if attention_mask is not None:  if attention_mask.size() != (bsz, 1, q_len, kv_seq_len):  raise ValueError(  f"Attention mask should be of size {(bsz, 1, q_len, kv_seq_len)}, but is {attention_mask.size()}"  )  attn_weights = attn_weights + attention_mask  attn_weights = torch.max(attn_weights, torch.tensor(torch.finfo(attn_weights.dtype).min))  # upcast attention to fp32  attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query_states.dtype)  attn_output = torch.matmul(attn_weights, value_states)  if attn_output.size() != (bsz, self.num_heads, q_len, self.head_dim):  raise ValueError(  f"`attn_output` should be of size {(bsz, self.num_heads, q_len, self.head_dim)}, but is"  f" {attn_output.size()}"  )  attn_output = attn_output.transpose(1, 2)  attn_output = attn_output.reshape(bsz, q_len, self.hidden_size)  attn_output = self.o_proj(attn_output)  if not output_attentions:  attn_weights = None  return attn_output, attn_weights, past_key_value

其中两点需要注意:

  • ​query_states = self.q_proj(hidden_states).view(bsz, q_len, self.num_heads, self.head_dim).transpose(1, 2)​​​就是多头计算,得到的结果query_states、key_states、value_states的维度是​​torch.Size([1, 32, 8, 128])​​,32代表头的数量、8是输入的input_ids长度,128代表头的大小,剩下的计算也是多头自注意力的部分,不多介绍了
  • 如果提供了past_key_value,则利用cache的机制,直接​​torch.cat([past_key_value[0], key_states], dim=2)​​即可,每次传入的input_ids只有最新的一个id

好了回到刚才的pipeline:

接着for循环完32个decoder层之后,需要进行norm操作:​​hidden_states = self.norm(hidden_states)​​。

输出后得到​​outputs.logits​​​维度为​​torch.Size([1, 8, 32000])​​,接下来对这个logits进行操作,以下是相关代码,比较长:

next_token_logits = outputs.logits[:, -1, :]  
# pre-process distribution  
next_token_scores = logits_processor(input_ids, next_token_logits)  
next_token_scores = logits_warper(input_ids, next_token_scores)  
...  
probs = nn.functional.softmax(next_token_scores, dim=-1)  
next_tokens = torch.multinomial(probs, num_samples=1).squeeze(1)  # logits_processor中执行的操作  class MinLengthLogitsProcessor(LogitsProcessor):  [`LogitsProcessor`] enforcing a min-length by setting EOS probability to 0.  def __init__(self, min_length: int, eos_token_id: Union[int, List[int]]):  if not isinstance(min_length, int) or min_length < 0:  raise ValueError(f"`min_length` has to be a non-negative integer, but is {min_length}")  if isinstance(eos_token_id, int):  eos_token_id = [eos_token_id]  if not all([isinstance(i, int) for i in eos_token_id]) or any([i < 0 for i in eos_token_id]):  logger.warning(f"`eos_token_id` has to be a list of positive integers, but is {eos_token_id}")  self.min_length = min_length  self.eos_token_id = eos_token_id  def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor) -> torch.FloatTensor:  cur_len = input_ids.shape[-1]  if cur_len < self.min_length:  for i in self.eos_token_id:  scores[:, i] = -float("inf")  return scores  
...  
# logits_warper调用的三个函数  
class TemperatureLogitsWarper(LogitsWarper):  def __init__(self, temperature: float):  if not isinstance(temperature, float) or not (temperature > 0):  raise ValueError(f"`temperature` has to be a strictly positive float, but is {temperature}")  self.temperature = temperature  def __call__(self, input_ids: torch.Tensor, scores: torch.Tensor) -> torch.FloatTensor:  scores = scores / self.temperature  return scores  class TopKLogitsWarper(LogitsWarper):  def __init__(self, top_k: int, filter_value: float = -float("Inf"), min_tokens_to_keep: int = 1):  if not isinstance(top_k, int) or top_k <= 0:  raise ValueError(f"`top_k` has to be a strictly positive integer, but is {top_k}")  self.top_k = max(top_k, min_tokens_to_keep)  self.filter_value = filter_value  def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor) -> torch.FloatTensor:  top_k = min(self.top_k, scores.size(-1))  # Safety check  # Remove all tokens with a probability less than the last token of the top-k  indices_to_remove = scores < torch.topk(scores, top_k)[0][..., -1, None]  scores = scores.masked_fill(indices_to_remove, self.filter_value)  return scores  class TopPLogitsWarper(LogitsWarper):  def __init__(self, top_p: float, filter_value: float = -float("Inf"), min_tokens_to_keep: int = 1):  top_p = float(top_p)  if top_p < 0 or top_p > 1.0:  raise ValueError(f"`top_p` has to be a float > 0 and < 1, but is {top_p}")  self.top_p = top_p  self.filter_value = filter_value  self.min_tokens_to_keep = min_tokens_to_keep  def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor) -> torch.FloatTensor:  sorted_logits, sorted_indices = torch.sort(scores, descending=False)  cumulative_probs = sorted_logits.softmax(dim=-1).cumsum(dim=-1)  # Remove tokens with cumulative top_p above the threshold (token with 0 are kept)  sorted_indices_to_remove = cumulative_probs <= (1 - self.top_p)  if self.min_tokens_to_keep > 1:  # Keep at least min_tokens_to_keep  sorted_indices_to_remove[..., -self.min_tokens_to_keep :] = 0  # scatter sorted tensors to original indexing  indices_to_remove = sorted_indices_to_remove.scatter(1, sorted_indices, sorted_indices_to_remove)  scores = scores.masked_fill(indices_to_remove, self.filter_value)  return scores

其中主要的过程:

  • 将 logits 传递给 logits_processor 和 logits_warper,在这两个方法中进行一些预处理过程,例如添加惩罚项或对概率分布进行修改,使得生成的结果更符合期望,具体调用了(这里用到了temperature参数,作用是调节模型生成的随机性,temperature通常被用于控制softmax函数的形状,从而影响生成序列的多样性,当temperature值接近0时,模型趋向于输出最可能的单个结果,也就是模型的输出趋向于确定性。这种情况下,所有的概率质量都集中在概率最大的那个输出上,其他的输出的概率几乎为0,当temperature值比较大(大于1)时,模型趋向于输出更多样化的结果,也就是增加了模型输出的随机性。在这种情况下,不同的输出之间的概率差异减小,使得即使概率较小的输出也有可能被选中)
  • TopKLogitsWarper类是一个用于处理模型输出分数(scores)的工具,主要用于进行所谓的“Top-K截断”。在自然语言生成的过程中,Top-K截断是一种常见的技巧,它的目标是在每个生成步骤中,只保留K个最可能的输出选项,而忽略其他的选项。这种方法可以降低生成过程的复杂性,并且可以减少不太可能的输出的干扰。
  • TopPLogitsWarper类实现了被称为"Top-p(或nucleus)抽样"的策略。该策略用于限制模型在每个生成步骤中所考虑的可能输出的范围。在Top-p抽样中,我们不再固定考虑概率最高的K个输出,而是根据概率分布的累积分布函数(CDF)来选择可能的输出。我们设置一个阈值P,然后选择输出,直到它们的累积概率大于等于P。由于这个方法根据概率分布动态地调整输出的数量,所以它可以更好地处理不同的分布情况,从而在某些情况下可以生成更自然的文本。
  • 最后,使用 softmax 函数将经过预处理的 logits 转换为概率分布,并利用 multinomial 方法从中采样得到下一个 token。最后,将该 token 添加到原始输入序列中,并进行下一次迭代,生成新的文本内容。如果需要记录中间变量,则将它们存储在相应的变量中,以便之后访问根据得到的新token,随后更新input_ids得到下一个token_id之后

得到next_tokens之后,执行以下代码:

input_ids = torch.cat([input_ids, next_tokens[:, None]], dim=-1)  
if streamer is not None:  streamer.put(next_tokens.cpu())  
model_kwargs = self._update_model_kwargs_for_generation(  outputs, model_kwargs, is_encoder_decoder=self.config.is_encoder_decoder  
)  # if eos_token was found in one sentence, set sentence to finished  
if eos_token_id_tensor is not None:  unfinished_sequences = unfinished_sequences.mul(  next_tokens.tile(eos_token_id_tensor.shape[0], 1).ne(eos_token_id_tensor.unsqueeze(1)).prod(dim=0)  )  # stop when each sentence is finished, or if we exceed the maximum length  
if unfinished_sequences.max() == 0 or stopping_criteria(input_ids, scores):  if not synced_gpus:  break  else:  this_peer_finished = True

其中:

  • 根据条件判断语句对生成结果进行一些后处理,例如将已完成的序列末尾添加 padding token,更新 model inputs 和 length 等等
  • 如果已经生成完整的句子,则将其标记为 finished,并检查是否满足终止条件。当全部句子都结束或达到最大长度时,停止生成

最后通过分词器进行decode即可得到所有结果,这个是支持batch的:

print(tokenizer.batch_decode(generated_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0])

过程差不多就是这样的,整体pipeline结束。

落地相关

部署的时候,除了模型要搞好,确保模型可以输出正常结果的设置参数也需要整明白,需要暴露出来以便上游去调节。

CV模型的话,我们一般可调节的就是预处理的均值方差、图片RGB还是BGR(通道什么的)、输入数据范围,图像resize大小什么的;后处理的话,如果是检测模型,那就是iou阈值、nms阈值什么的。

对于llama来说,可调节的参数就有点多了。

举个例子,随便拿出一个gradio展示的LLM模型,可调节的参数如下:

  • seed   seed如果固定了,我们的输出也就固定了,我们使用chatgpt的时候,seed每次应该不一样
  • gen mode   sample 一般来说就是用这个 do_sample  ;greed  貌似这个最快;beam search  这个效果最好
  • end_id 模型训练的时候设置的结束id,模型在预测的时候,如果预测下一个token是end_id的话就应该终止预测了,就是这段话说完了
  • start_id 一般来说是在tokenizer中设置,在将输入文本encode的时候,设置的第一个token id

padding

LLM和CV模型一样,组batch的时候需求输入大小一致,比如[4,3,128,128],而NLP中输入的是input_ids,padding的方法如下:

input_sentences = [  "DeepSpeed is a machine learning framework",  "He is working on",  "He has a",  "He got all",  "Everyone is happy and I can",  "The new movie that got Oscar this year",  "In the far far distance from our galaxy,",  "Peace is the only way",  
]  
tokenizer.pad_token = 0  
input_tokens = tokenizer.batch_encode_plus(input_sentences, return_tensors="pt", padding=True)

bad_words和stop_words

LLM在生成token的时候需要避免一些:

  • 不想生成的token
  • 遇到就停止的token

kv-cache

kv-cache是LLM推理过程的一个优化点,可以减少计算,不过需要额外显存去存这些cache。

总结

可以参考hugglingface搭建服务的规则和一些细节,一般要支持:

  • 对于stream模式,要支持多个同时的请求
  • 对于非stream模式,需要支持多batch输入,也可以支持组batch
  • 对于stream模式,如果服务端认为生成token到结尾了,则可以主动断开连接并且返回终止符给客户端

一些概念

unconditional generation

在自然语言处理(NLP)中,"unconditional generation"是指模型在没有特定输入或提示的情况下生成文本。对比之下,"conditional generation"是指模型在给定某些输入或提示(例如,开头的一段文本或特定的任务描述)的情况下生成文本。

举个例子,如果我们让一个语言模型写一个故事,但不给它任何具体的主题或开头,这就是无条件生成。如果我们给模型一个特定的开头,比如"在一个深深的森林里,有一个小屋……",然后要求模型继续写下去,这就是有条件生成。

"start token"和"end token"是用来标识生成任务的开始和结束的特殊标记。在GPT-J(我在2021年9月之前的知识库中没有关于GPT-J的信息,因此我不确定这是何种模型)中,对于无条件生成任务,开始标记和结束标记可能是相同的。这可能意味着,在没有输入或提示的情况下开始和结束文本生成。

context len

在自然语言处理(NLP)中,"context window"(上下文窗口)是一种常见的概念,它指的是在处理某个词或词组时考虑的前后相关词汇的范围。这个范围可以是固定的,也可以是动态的,取决于具体的模型和任务。

例如,如果我们使用一个大小为5的上下文窗口来处理一个词,那么我们会考虑这个词前后各2个词作为其上下文。这样做的目的是获取更多的上下文信息,以便更好地理解和处理这个词。此外,某些类型的模型,如Transformer模型,实际上可以看作是没有固定上下文窗口大小的,因为它们使用的机制(如自注意力机制)允许它们在处理每个词时考虑整个输入序列的所有词。

值得注意的是,上下文窗口的大小和处理策略可能会对模型的性能和结果产生显著影响。较大的上下文窗口可能会捕获更多的长距离依赖关系,但也可能增加模型的计算复杂度。相反,较小的上下文窗口可能会减少计算复杂度,但可能无法捕获一些重要的长距离依赖关系。因此,选择合适的上下文窗口大小和处理策略通常需要根据具体的任务和数据进行调整。

参考

文中所有图片来源于以下的链接:

Transformer论文逐段精读【论文精读】(https://www.bilibili.com/video/BV1pu411o7BE/?spm_id_from=333.880.my_history.page.click&vd_source=eec038509607175d58cdfe2e824e8ba2)

​​https://chat.openai.com/​​

​​https://huggingface.co/learn/nlp-course/chapter1/3?fw=pt​​

​​https://www.eet-china.com/mp/a256824.html​​


http://www.mrgr.cn/news/59594.html

相关文章:

  • adb常见指令以及问题解决
  • 浪潮云启操作系统(InLinux)bcache缓存实践:理解OpenStack环境下虚拟机卷、Ceph OSD、bcache设备之间的映射关系
  • 【MySQL】提高篇—视图与存储过程:使用触发器(Triggers)进行自动化操作
  • C++基础:三个字符串也能搞大小?
  • 从本地到云端:跨用户请求问题的完美解决方案
  • 一致角色的视频且唇形同步中文配音和免费音效添加
  • .net core 读取 appsettings.json 值
  • 基于SSM轻型卡车零部件销售系统的设计
  • 对角两对双差速轮AGV的动力学解算
  • SpringBoot中yaml配置文件中文字符异常以及将多个独立的IDEA项目整合到一个项目里当做模块的处理
  • visual studio断点无法命中
  • 【学术论文投稿】Windows11开发指南:打造卓越应用的必备攻略
  • Oracle 第7章:数据完整性约束
  • 10月27日
  • 【前端面试】Typescript
  • 【遗传算法】基于遗传模拟退火算法的风电功率聚类分析
  • springboot094基于web的酒店客房管理系统(论文+源码)_kaic
  • 利用编程思维做题之计算二叉树最大宽度
  • 《战场车辆及部件损毁识别与评估的神经网络新路径》
  • MirrorMaker2配置后同步数据至目标集群的topic都加上一个源集群别名的前缀A.
  • C++从入门到起飞之——红黑树封装map和set 全方位剖析!
  • 冥冥中有定数,一个可能的趋势转换,数据分析,玄学
  • 基于Springboot+Vue的企业绩效考核管理系统 (含源码数据库)
  • 获取大麦网关键词列表数据接口技术贴
  • NumPy 数组合并与修改示例解析
  • 面包种类图像分割系统:多层面改进