大模型原理
大模型原理
绪论
大语言模型是一种由包含数百亿个及以上参数的深度神经网络构建的语言模型,通常使用自监督学习方法通过大量无标注文本进行训练。^1
2018 年以来,Google、OpenAI、Meta、百度、华为等公司和研究机构相继发布了 BERT、GPT等多种模型,这些模型在几乎所有自然语言处理任务中都表现出色。2019 年,大语言模型呈现爆发式的增长,特别是 2022 年 11 月 ChatGPT(Chat Generative Pre-trained Transformer)的发布,引起了全世界的广泛关注。用户可以使用自然语言与系统交互,实现问答、分类、摘要、翻译、聊天等从理解到生成的各种任务。大语言模型展现出了强大的对世界知识的掌握和对语言的理解能力。
语言模型(Language Model,LM)的目标就是对自然语言的概率分布建模。词汇表 V 上的语言模型,由函数 P(w1w2 · · · wm) 表示,可以形式化地构建为词序列 w1w2 · · · wm 的概率分布,表示词序列 w1w2 · · · wm 作为一个句子出现的可能性的大小。由于联合概率 P(w1w2 · · · wm) 的参数量巨大,因此直接计算 P(w1w2 · · · wm)非常困难。
为了减小 P(w1w2 · · · wm) 模型的参数空间,可以利用句子序列(通常是从左至右)的生成过程将其进行分解,使用链式法则可以得到
式1.1:

由此,w1w2 · · · wm 的生成过程可以看作单词逐个生成的过程。首先生成 w1,之后根据 w1 生成w2,然后根据 w1 和 w2 生成 w3,依此类推,根据前 m − 1 个单词生成最后一个单词 wm。例如,对于句子“把努力变成一种习惯”的概率计算,使用式 (1.1) 可以转化为
P(把 努力 变成 一种 习惯) = P(把) × P(努力|把) × P(变成|把 努力)×P(一种|把 努力 变成) × P(习惯|把 努力 变成 一种)
条件概率
通过上述过程,将联合概率 P(w1w2 · · · wm) 转换为多个条件概率的乘积。但是,仅通过上述过程模型的参数空间依然没有减小,P(wm|w1w2 · · · wm−1) 的参数空间依然是天文数字。为了解决上述问题,可以进一步假设任意单词 wi 出现的概率只与过去 n − 1 个词相关,即

满足上述条件的模型被称为 n 元语法或 n 元文法(n-gram)模型。其中,n-gram 表示由 n 个连续单词构成的单元,也被称为n 元语法单元。
虽然 n 元语言模型能缓解句子概率为零的问题,但语言是由人和时代创造的,具备无尽的可能性,再庞大的训练数据也无法覆盖所有的 n-gram,而训练数据中的零频率并不代表零概率。因此,需要使用平滑技术(Smoothing)解决,为所有可能出现的字符串分配一个非零的概率值,从而避免零概率问题。平滑是指为了产生更合理的概率,对最大似然估计进行调整的一类方法,也称为数据平滑(Data Smoothing)。平滑处理的基本思想是提高低概率事件,降低高概率事件,使整体的概率分布趋于均匀。这类方法通常被称为统计语言模型(Statistical Language Models,SLM)。
n 元语言模型从整体上看与训练数据规模和模型的阶数(考虑上下文的数量)有较大的关系,不同的平滑算法在不同情况下的表现有较大的差距。虽然平滑算法较好地解决了零概率问题,但是基于稀疏表示的 n 元语言模型仍然有以下三个较为明显的缺点。
(1)无法对长度超过 n 的上下文建模。
(2)依赖人工设计规则的平滑技术。
(3)当 n 增大时,数据的稀疏性随之增大,模型的参数量更是呈指数级增加,受数据稀疏问题的影响,其参数难以被准确学习。
此外,n 元文法中单词的离散表示也忽略了单词之间的相似性。因此,基于分布式表示和神经网络的语言模型逐渐成为研究热点。Bengio等人在 2000 年提出了使用前馈神经网络对 P(wi|wi−n+1 · · · wi−1)进行估计的语言模型。词的独热编码被映射为一个低维稠密的实数向量,称为词向量(Word Embedding)。此后,循环神经网络、卷积神经网络、端到端记忆网络 等神经网络方法都成功应用于语言模型建模。相较于 n 元语言模型,神经网络方法可以在一定程度上避免数据稀疏问题,有些模型还可以摆脱对历史文本长度的限制,从而更好地对长距离依赖关系建模。这类方法通常被称为神经语言模型(Neural Language Models,NLM)。
深度神经网络需要采用有监督方法,使用标注数据进行训练,因此,语言模型的训练过程也不可避免地需要构造训练数据。由于训练目标可以通过无标注文本直接获得,因此模型的训练仅需要大规模无标注文本。语言模型也成了典型的自监督学习(Self-supervised Learning)任务。互联网的发展,使得大规模文本非常容易获取,因此训练超大规模的基于神经网络的语言模型成为可能。
受计算机视觉领域采用 ImageNet 对模型进行一次预训练,使模型可以通过海量图像充分学习如何提取特征,再根据任务目标进行模型精调的预训练范式影响,自然语言处理领域基于预训练语言模型的方法逐渐成为主流。以 ELMo 为代表的动态词向量模型开启了语言模型预训练的大门。此后,以 GPT 和 BERT 为代表的基于 Transformer 结构 的大规模预训练语言模型的出现,使自然语言处理全面进入预训练微调范式新时代。将预训练模型应用于下游任务时,不需要了解太多的任务细节,不需要设计特定的神经网络结构,只需要“微调”预训练模型,使用具体任务的标注数据在预训练语言模型上进行监督训练,就可以取得显著的性能提升。这类方法通常被称为预训练语言模型(Pre-trained Language Models,PLM)。
2020 年,OpenAI 发布了由包含 1750 亿个参数的神经网络构成的生成式大规模预训练语言模型 GPT-3(Generative Pre-trained Transformer 3),开启了大语言模型的新时代。由于大语言模型的参数量巨大,在不同任务上都进行微调需要消耗大量的计算资源,因此预训练微调范式不再适用于大语言模型。研究人员发现,通过语境学习(In-Context Learning,ICL)等方法,直接使用大语言模型,就可以在很多任务的少样本场景中取得很好的效果。此后,研究人员提出了面向大语言模型的提示词(Prompt)学习方法,以及模型即服务范式(Model as a Service,MaaS)、指令微调(Instruction Tuning)等方法,在不同任务中都取得了很好的效果。与此同时,Google、Meta、BigScience、百度、华为等公司和研究机构纷纷发布了 PaLM、LaMDA、T0 等不同大语言模型。2022 年年底 ChatGPT 的出现,将大语言模型的能力进行了充分的展现,也引发了大语言模型研究的热潮。
Kaplan 等人在文献中提出了缩放法则(Scaling Laws),指出模型的性能依赖于模型的规模,包括参数量、数据集大小和计算量,模型的效果会随着三者的指数增加而平稳提升。如图 1.1 所示,模型的损失(Loss)值随着模型规模的指数增加而线性降低。这意味着模型的能力可以根据这三个变量估计,增加模型参数量,扩大数据集规模都可以使模型的性能可预测地提升。这为继续扩大大语言模型的规模给出了定量分析依据。

发展历程
截止2025年2月,大语言模型的发展可以粗略地分为如下三个阶段:基础模型阶段、能力探索阶段和突破发展阶段。

基础模型阶段主要集中于 2018 年至 2021 年。2017 年,Vaswani 等人提出了 Transformer架构,在机器翻译任务上取得了突破性进展。2018 年,Google 和 OpenAI 分别提出了 BERT 和GPT-1[2] 模型,开启了预训练语言模型时代。BERT-Base 版本的参数量为 1.1 亿个,BERT-Large 版本的参数量为 3.4 亿个,GPT-1 的参数量为 1.17 亿个。这在当时,比其他深度神经网络的参数量,已经有了数量级上的提升。2019 年 OpenAI 发布了 GPT-2[11],其参数量达到 15 亿个。此后,Google也发布了参数规模为 110 亿个的 T5[19] 模型。2020 年,OpenAI 进一步将语言模型的参数量扩展到 1750 亿个,发布了 GPT-3[13]。此后,国内也相继推出了一系列的大语言模型,包括清华大学的ERNIE[20]、百度的 ERNIE[21]、华为的 PanGU-α 等。此阶段的研究主要集中在语言模型本身,对仅编码器(Encoder Only)、编码器-解码器(Encoder-Decoder)、仅解码器(Decoder Only)等各种类型的模型结构都有相应的研究。模型大小与 BERT 类似,通常采用预训练微调范式,针对不同下游任务进行微调。这些模型参数量大都在 10 亿个以上,由于微调的计算量很大,这类模型的影响力在当时相较 BERT 类模型有不小的差距。
能力探索阶段集中于 2019 年至 2022 年,由于大语言模型很难针对特定任务进行微调,研究人员开始探索在不针对单一任务进行微调的情况下如何发挥大语言模型的能力。2019 年,Radford等人在文献 [11] 中使用 GPT-2 模型研究了大语言模型在零样本情况下的任务处理能力。在此基础上,Brown 等人在 GPT-3[13] 模型上研究了通过语境学习进行少样本学习的方法,将不同任务的少量有标注的实例拼接到待分析的样本之前输入语言模型,语言模型根据实例理解任务并给出正确的结果。基于 GPT-3 的语境学习在 TriviaQA、WebQS、CoQA 等评测集合中都展示出了非常强的能力,在有些任务中甚至超过了此前的有监督方法。上述方法不需要修改语言模型的参数,模型在处理不同任务时无须花费大量计算资源进行模型微调。仅依赖语言模型本身,其性能在很多任务上仍然很难达到有监督学习(Supervised Learning)的效果,因此研究人员提出了指令微调[23] 方案,将大量各类型任务统一为生成式自然语言理解框架,并构造训练数据进行微调。大语言模型能一次性学习数千种任务,并在未知任务上展现出很好的泛化能力。2022 年,Ouyang 等人提出了使用“有监督微调 + 强化学习”的 InstructGPT[24] 方法,该方法使用少量有监督数据就可以使大语言模型服从人类指令。Nakano 等人则探索了结合搜索引擎的问题回答方法 WebGPT[25]。这些方法在直接利用大语言模型进行零样本和少样本学习的基础上,逐渐扩展为利用生成式框架针对大量任务进行有监督微调的方法,有效提升了模型的性能。
突破发展阶段以 2022 年 11 月 ChatGPT 的发布为起点。ChatGPT 通过一个简单的对话框,利用一个大语言模型就可以实现问题回答、文稿撰写、代码生成、数学解题等过去自然语言处理系统需要大量小模型定制开发才能分别实现的能力。它在开放领域问答、各类自然语言生成式任务及对话上下文理解上所展现出来的能力远超大多数人的想象。2023 年 3 月 GPT-4 发布,相较于ChatGPT,GPT-4 有非常明显的进步,并具备了多模态理解能力。GPT-4 在多种基准考试测试上的得分高于 88% 的应试者,包括美国律师资格考试(Uniform Bar Exam)、法学院入学考试(LawSchool Admission Test)、学术能力评估(Scholastic Assessment Test,SAT)等。GPT-4o 是 OpenAI于 2024 年 5 月发布的多模态大模型,其中“o”代表“omni”即“全能”。它能接受文本、音频和图像组合输入并生成文本、音频和图像的任意组合输出,可处理 50 种语言,在 232 毫秒内对音频输入做出反应,性能较 GPT-4 有显著提升。2024 年 9 月 OpenAI 又推出的全新推理模型 GPT-o1,在复杂推理任务上表现卓越,能通过内部思维链模拟人类思考,在数学、科学等领域超越人类专家及 GPT-4o。国内外各大公司和研究机构相继发布了此类系统,包括复旦大学的 MOSS、阿里巴巴的 Qwen、深度求索的 DeepSeek、Google 的 Gemini、XAI 的 Grok、科大讯飞的星火大模型、智谱的 ChatGLM 等。
模型类型中,基础模型是指仅经过预训练的模型;对话模型是指在预训练模型基础上经过有监督微调和强化学习训练的模型,具备对话和完成任务的能力;推理模型是指专注于逻辑推理增强的大语言模型。



大语言模型的构建流程
根据 OpenAI 联合创始人 Andrej Karpathy 在微软 Build 2023 大会上公开的信息,OpenAI 使用的大语言模型构建流程如图1.3 所示,主要包含四个阶段:
- 预训练
- 有监督微调
- 奖励建模
- 强化学习
这四个阶段都需要不同规模的数据集及不同类型的算法,会产出不同类型的模型,所需要的资源也有非常大的差别。

预训练(Pretraining)阶段需要利用海量的训练数据(数据来自互联网网页、维基百科、书籍、GitHub、论文、问答网站等),构建包含数千亿甚至数万亿单词的具有多样性的内容。利用由数千块高性能 GPU 和高速网络组成的超级计算机,花费数十天完成深度神经网络参数训练,构建基础模型(Base Model)。基础模型对长文本进行建模,使模型具有语言生成能力,根据输入的提示词,模型可以生成文本补全句子。有一部分研究人员认为,语言模型建模过程中隐含地构建了包括事实性知识(Factual Knowledge)和常识性知识(Commonsense)在内的世界知识(World Knowledge)。根据文献 [39] 中的介绍,GPT-3 完成一次训练的总计算量是 3640PFLOPS,按照 NVIDIA A100 80GBGPU 和平均利用率达到 50% 计算,需要花费近一个月的时间使用 1000 块 GPU 完成。由于 GPT-3的训练采用 NVIDIA V100 32GB GPU,其实际计算成本远高于上述计算。文献 [29] 介绍了参数量同样是 1750 亿个的 OPT 模型,该模型训练使用 992 块 NVIDIA A100 80GB GPU,整体训练时间将近 2 个月。BLOOM[31] 模型的参数量也是 1750 亿个,该模型训练一共花费 3.5 个月,使用包含384 块 NVIDIA A100 80GB GPU 集群完成。可以看到,大语言模型的训练需要花费大量的计算资源和时间。LLaMA、Falcon、百川(Baichuan)等模型都属于基础语言模型。即便是 DeepSeek-V3[40]经过了大量的训练效率优化,甚至已经直接使用 PTX 进行汇编级优化,完成一次预训练仍然需要花费 266.4 万 H800 GPU 小时。由于训练过程需要消耗大量的计算资源,并很容易受到超参数影响,因此,如何提升分布式计算效率并使模型训练稳定收敛是本阶段的研究重点。
有监督微调(Supervised Fine Tuning,SFT),也称为指令微调,利用少量高质量数据集,通过有监督训练使模型具备问题回答、翻译、写作等能力。有监督微调的数据包含用户输入的提示词和对应的理想输出结果。用户输入包括问题、闲聊对话、任务指令等多种形式和任务。
例如:提示词:复旦大学有几个校区?
理想输出:复旦大学现有 4 个校区,分别是邯郸校区、新江湾校区、枫林校区和张江
校区。其中邯郸校区是复旦大学的主校区,邯郸校区与新江湾校区都位于杨浦区,枫林校区
位于徐汇区,张江校区位于浦东新区。
利用这些有监督数据,使用与预训练阶段相同的语言模型训练算法,在基础模型的基础上进行训练,得到有监督微调模型(SFT 模型)。经过训练的 SFT 模型具备初步的指令理解能力和上下文理解能力,能够完成开放领域问答、阅读理解、翻译、生成代码等任务,也具备了一定的对未知任务的泛化能力。由于有监督微调阶段所需的训练数据量较少,SFT 模型的训练过程并不需要消耗大量的计算资源。根据模型的大小和训练数据量,通常需要数十块 GPU,花费数天时间完成训练。SFT 模型具备了初步的任务完成能力,可以开放给用户使用,很多类 ChatGPT 的模型都属于该类型,包括 Alpaca[35]、Vicuna[41]、MOSS、ChatGLM-6B 等。很多这类模型的效果非常好,甚至在一些评测中达到了 ChatGPT 的 90% 的效果[35, 41]。当前的一些研究表明,有监督微调阶段的数据选择对 SFT 模型效果有非常大的影响[42],因此构造少量并且高质量的训练数据是本阶段的研究重点。
奖励建模(Reward Modeling)阶段的目标是构建一个文本质量对比模型。对于同一个提示词,SFT 模型对给出的多个不同输出结果的质量进行排序。奖励模型可以通过二分类模型,对输入的两个结果之间的优劣进行判断。奖励模型与基础模型和 SFT 模型不同,奖励模型本身并不能单独提供给用户使用。奖励模型的训练通常和 SFT 模型一样,使用数十块 GPU,通过数天时间完成训练。由于奖励模型的准确率对强化学习阶段的效果有至关重要的影响,因此通常需要大规模的训练数据对该模型进行训练。Andrej Karpathy 在报告中指出,该部分需要百万量级的对比数据标注,而且其中很多标注需要很长时间才能完成。图1.4 给出了 InstructGPT 系统中奖励模型训练样本标注示例[24]。可以看到,示例中文本表达都较为流畅,标注其质量排序需要制定非常详细的规范,标注者也需要认真地基于标注规范进行标注,需要消耗大量的人力。同时,保持众包标注者之间的一致性,也是奖励建模阶段需要解决的难点问题之一。此外,奖励模型的泛化能力边界也是本阶段需要重点研究的一个问题。如果奖励模型的目标是针对系统所有的输出都能够高质量地进行判断,那么该问题的难度在某种程度上与文本生成等价,因此限定奖励模型应用的泛化边界是本阶段需要解决的问题。

强化学习(Reinforcement Learning,RL)阶段根据数十万条提示词,利用前一阶段训练的奖励模型,给出 SFT 模型对提示词回答结果的质量评估,并与语言模型建模目标综合得到更好的效果。该阶段使用的提示词数量与有监督微调阶段类似,数量在十万个量级,并且不需要人工提前给出该提示词所对应的理想回复。使用强化学习,在 SFT 模型的基础上调整参数,使最终生成的文本可以获得更高的奖励(Reward)。该阶段需要的计算量较预训练阶段也少很多,通常仅需要数十块GPU,数天即可完成训练。文献 [24] 给出了强化学习和有监督微调的对比,在模型参数量相同的情况下,强化学习可以得到相较于有监督微调好得多的效果。关于为什么强化学习相比有监督微调可以得到更好结果的问题,截至 2025 年 2 月还没有完整或得到普遍共识的解释。目前相对得到认可的观点是,强化学习使得模型具备更好的泛化能力[43]。同时,Andrej Karpathy 也指出,强化学习并不是没有问题的,它会使基础模型的熵降低,从而减少模型输出的多样性。经过强化学习方法训练后的 RL 模型,就是最终提供给用户使用、具有理解用户指令和上下文的类 ChatGPT 系统。由于强化学习方法稳定性不高,并且超参数众多,使得模型收敛难度大,叠加奖励模型的准确率问题,使得在大语言模型上有效应用强化学习非常困难。
大语言模型基础
语言模型的核心目标是对自然语言的概率分布进行建模,这一任务在自然语言处理研究中占据重要地位,是其基础性工作之一。大量研究围绕这一目标,从不同角度展开了探索,包括 n 元语言模型(n-gram Language Models)、神经语言模型和预训练语言模型等。这些研究在不同发展阶段对自然语言处理任务产生了深远影响。随着基于 Transformer 架构的语言模型不断发展,以及预训练-微调范式在各类自然语言处理任务中取得突破性成果,自 2020 年 OpenAI 发布 GPT-3 以来,大语言模型的研究逐步深入。尽管大语言模型参数规模庞大,并且通过有监督微调和强化学习可以完成众多任务,其理论基础仍然离不开对语言建模的核心研究。
transformer 结构
Transformer 结构[44] 是由 Google 在 2017 年提出并首先应用于机器翻译的神经网络模型架构。机器翻译的目标是从源语言(Source Language)转换到目标语言(Target Language)。Transformer结构完全通过注意力机制完成对源语言序列和目标语言序列全局依赖的建模。如今,几乎全部大语言模型都是基于 Transformer 结构的。本节以应用于机器翻译的基于 Transformer 的编码器和解码器结构为例介绍该模型。
基于 Transformer 的编码器和解码器结构如图2.1 所示,左侧和右侧分别对应着编码器(Encoder)和解码器(Decoder)结构,它们均由若干个基本的 Transformer 块(Block)组成(对应图中的灰色框)。这里 N× 表示进行了 N 次堆叠。每个 Transformer 块都接收一个向量序列 {xi}ti=1作为输入,并输出一个等长的向量序列作为输出 {yi}ti=1。这里的 xi 和 yi 分别对应文本序列中的一个词元(Token)的表示。yi 是当前 Transformer 块对输入 xi 进一步整合其上下文语义后对应的
输出。在从输入 {xi}ti=1 到输出 {yi}ti=1 的语义抽象过程中,主要涉及如下几个模块。
• 注意力层:使用多头注意力(Multi-Head Attention)机制整合上下文语义。多头注意力并行运行多个独立注意力机制,进而从多维度捕捉输入序列信息。它使得序列中任意两个单词之间的依赖关系可以直接被建模而不基于传统的循环结构,从而更好地解决文本的长程依赖问题。
• 位置感知前馈网络层(Position-wise Feed-Forward Network):通过全连接层对输入文本序列中的每个单词表示进行更复杂的变换。
• 残差连接:对应图中的 Add 部分。它是一条分别作用在上述两个子层中的直连通路,被用于连接两个子层的输入与输出,使信息流动更高效,有利于模型的优化。
• 层归一化:对应图中的 Norm 部分。它作用于上述两个子层的输出表示序列,对表示序列进行层归一化操作,同样起到稳定优化的作用。

嵌入表示层
对于输入文本序列,先通过输入嵌入层(Input Embedding)将每个单词转换为其相对应的向量表示。通常,直接对每个单词创建一个向量表示。Transformer 结构不再使用基于循环的方式建模文本输入,序列中不再有任何信息能够提示模型单词之间的相对位置关系。在送入编码器端建模其上下文语义之前,一个非常重要的操作是在词嵌入中加入位置编码(Positional Encoding)这一特征。具体来说,序列中每一个单词所在的位置都对应一个向量。这一向量会与单词表示对应相加并送入后续模块中做进一步处理。在训练过程中,模型会自动地学习到如何利用这部分位置信息。
为了得到不同位置所对应的编码,Transformer 结构使用不同频率的正余弦函数,如下所示。

其中,pos 表示单词所在的位置,2i 和 2i + 1 表示位置编码向量中的对应维度,d 则对应位置编码的总维度。通过上面这种方式计算位置编码有以下两个好处:第一,正余弦函数的范围是 [−1, +1],导出的位置编码与原词嵌入相加不会使得结果偏离过远而破坏原有单词的语义信息;第二,依据三角函数的基本性质,可以得知第 pos + k 个位置编码是第 pos 个位置编码的线性组合,这就意味着位置编码中蕴含着单词之间的距离信息。
使用 PyTorch 实现的位置编码参考代码如下:
class PositionalEncoder(nn.Module):
def __init__(self, d_model, max_seq_len = 80):
super().__init__()
self.d_model = d_model
# 根据pos和i创建一个常量PE矩阵
pe = torch.zeros(max_seq_len, d_model)
for pos in range(max_seq_len):
for i in range(0, d_model, 2):
pe[pos, i] = math.sin(pos / (10000 ** (i/d_model)))
pe[pos, i + 1] = math.cos(pos / (10000 ** (i/d_model)))
pe = pe.unsqueeze(0)
self.register_buffer('pe', pe)
def forward(self, x):
# 使得单词嵌入表示相对大一些
x = x * math.sqrt(self.d_model)
# 增加位置常量到单词嵌入表示中
seq_len = x.size(1)
x = x + Variable(self.pe[:,:seq_len], requires_grad=False).cuda()
return x
注意力层
自注意力(Self-Attention)操作是基于 Transformer 的机器翻译模型的基本操作,在源语言的编码和目标语言的生成中频繁地被使用,以建模源语言、目标语言任意两个单词之间的依赖关系。将由单词语义嵌入及其位置编码叠加得到的输入表示为 ${x_i ∈ R^d}^L_{i=1}$,为了实现对上下文语义依赖的建模,引入自注意力机制涉及的三个元素:查询 $q_i$(Query)、键 $k_i$(Key)和值 $v_i$(Value)。在编码输入序列的每一个单词的表示中,这三个元素用于计算上下文单词对应的权重得分。直观地说,这些权重反映了在编码当前单词的表示时,对于上下文不同部分所需的关注程度。具体来说,如图2.2 所示,通过三个线性变换 $W^Q ∈ R^{d×d_q}$,$ W^K ∈ R^{d×d_k} $, $W^V ∈ R^{d×d_v} $将输入序列中的每一个单词表示 $x_i$ 转换为其对应的$ q_i ∈ R^{d_q}$,$ k_i ∈ R^{d_k} $,$ v_i ∈ R^{d_v} $向量。对于输入$ {x_i ∈ R^d}^L_{i=1}$,Q、K 和 V 矩阵可以通过如下公式所示:


为了得到编码单词 $x_i$ 时所需要关注的上下文信息,通过位置 i 查询向量与其他位置的键向量做点积得到匹配分数$ q_i· k_1, q_i· k_2, · · · , q_i· k_t$。为了防止过大的匹配分数在后续 Softmax 计算过程中导致的梯度爆炸及收敛效率差的问题,这些得分会除以放缩因子 $\sqrt d $以稳定优化。放缩后的得分经过 Softmax 归一化为概率,与其他位置的值向量相乘来聚合希望关注的上下文信息,并最小化不相关信息的干扰。上述计算过程可以被形式化地表述如下:

其中$ Q ∈ R^{L×d_q}, K ∈ R^{L×d_k} ,V ∈ R^{L×d_v} $分别表示输入序列中的不同单词的 q, k, v 向量拼接组成的矩阵,L 表示序列长度,$Z ∈ R^{L×d_v}$ 表示自注意力操作的输出。为了进一步增强自注意力机制聚合上下文信息的能力,提出了多头注意力机制,以关注上下文的不同侧面。具体来说,上下文中每一个单词的表示 $x_i$ 经过多组线性 ${W_j^Q,W_j^K,W_j^V }^N_{j=1}$ 映射到不同的表示子空间中。公式 (2.6) 会在不同的子空间中分别计算并得到不同的上下文相关的单词序列表示 ${Z_j}^N_{j=1}$:

在此基础上,经过线性变换 $W^O ∈ R^{(Nd_v)×d}$ 用于综合不同子空间中的上下文表示并形成注意力层最终的输出 ${x_i ∈ R^d}^L_{i=1}$,可得到多头自注意力(Multi-Head Self-Attention)表示:

由此可见,自注意力机制使模型能够识别不同输入部分的重要性,而不受距离的影响,从而能够捕捉输入句子中的长距离依赖关系和复杂关系。使用 PyTorch 实现的自注意力层参考代码如下:
class MultiHeadAttention(nn.Module):
def __init__(self, heads, d_model, dropout = 0.1):
super().__init__()
self.d_model = d_model
self.d_k = d_model // heads
self.h = heads
self.q_linear = nn.Linear(d_model, d_model)
self.v_linear = nn.Linear(d_model, d_model)
self.k_linear = nn.Linear(d_model, d_model)
self.dropout = nn.Dropout(dropout)
self.out = nn.Linear(d_model, d_model)
def attention(q, k, v, d_k, mask=None, dropout=None):
scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(d_k)
# 掩盖那些为了补全长度而增加的单元,使其通过Softmax计算后为0
if mask is not None:
mask = mask.unsqueeze(1)
scores = scores.masked_fill(mask == 0, -1e9)
scores = F.softmax(scores, dim=-1)
if dropout is not None:
scores = dropout(scores)
output = torch.matmul(scores, v)
return output
def forward(self, q, k, v, mask=None):
bs = q.size(0)
# 利用线性计算划分成h个头
k = self.k_linear(k).view(bs, -1, self.h, self.d_k)
q = self.q_linear(q).view(bs, -1, self.h, self.d_k)
v = self.v_linear(v).view(bs, -1, self.h, self.d_k)
# 矩阵转置
k = k.transpose(1,2)
q = q.transpose(1,2)
v = v.transpose(1,2)
# 计算attention
scores = attention(q, k, v, self.d_k, mask, self.dropout)
前馈层
前馈层接收自注意力子层的输出作为输入,并通过一个带有 ReLU 激活函数的两层全连接网络对输入进行更复杂的非线性变换。实验证明,这一非线性变换会对模型最终的性能产生重要的影响。

其中 $W_1, b_1,W_2, b_2$ 表示前馈子层的参数。实验结果表明,增大前馈子层隐状态的维度有利于提高最终翻译结果的质量,因此,前馈子层隐状态的维度一般比自注意力子层要大。
使用 PyTorch 实现的前馈层参考代码如下:
class FeedForward(nn.Module):
def __init__(self, d_model, d_ff=2048, dropout = 0.1):
super().__init__()
# d_ff默认设置为2048
self.linear_1 = nn.Linear(d_model, d_ff)
self.dropout = nn.Dropout(dropout)
self.linear_2 = nn.Linear(d_ff, d_model)
def forward(self, x):
x = self.dropout(F.relu(self.linear_1(x)))
x = self.linear_2(x)
return x
残差连接与层归一化
由Transformer结构组成的网络结构通常都非常庞大。编码器和解码器均由很多层基本的Transformer 块组成,每一层中都包含复杂的非线性映射,这就导致模型的训练比较困难。因此,研究人员在 Transformer 块中进一步引入了残差连接与层归一化技术,以进一步提升训练的稳定性。具体来说,残差连接主要是指使用一条直连通道直接将对应子层的输入连接到输出,避免在优化过程中因网络过深而产生潜在的梯度消失问题:

其中 $x^l$ 表示第 l 层的输入,$f(·)$ 表示一个映射函数。此外,为了使每一层的输入/输出稳定在一个合理的范围内,层归一化技术被进一步引入每个 Transformer 块中:

其中 $µ$ 和 $σ$ 分别表示均值和方差,用于将数据平移缩放到均值为 0、方差为 1 的标准分布,$α$ 和$b$ 是可学习的参数。层归一化技术可以有效地缓解优化过程中潜在的不稳定、收敛速度慢等问题。
使用 PyTorch 实现的层归一化参考代码如下:
class Norm(nn.Module):
def __init__(self, d_model, eps = 1e-6):
super().__init__()
self.size = d_model
# 层归一化包含两个可以学习的参数
self.alpha = nn.Parameter(torch.ones(self.size))
self.bias = nn.Parameter(torch.zeros(self.size))
self.eps = eps
def forward(self, x):
norm = self.alpha * (x - x.mean(dim=-1, keepdim=True))
norm = norm / (x.std(dim=-1, keepdim=True) + self.eps) + self.bias
return norm
编码器与解码器结构
基于上述模块,根据图2.1 给出的网络架构,编码器端较容易实现。相比于编码器端,解码器端更复杂。具体来说,解码器的每个 Transformer 块的第一个自注意力子层额外增加了注意力掩码,对应图中的掩码多头注意力(Masked Multi-Head Attention)部分。这主要是因为在翻译的过程中,编码器端主要用于编码源语言序列的信息,而这个序列是完全已知的,因而编码器仅需要考虑如何融合上下文语义信息。解码器端则负责生成目标语言序列,这一生成过程是自回归的,即对于每一个单词的生成过程,仅有当前单词之前的目标语言序列是可以被观测的,因此这一额外增加的掩码是用来掩盖后续的文本信息的,以防模型在训练阶段直接看到后续的文本序列,进而无法得到有效的训练。
此外,解码器端额外增加了一个多头交叉注意力(Multi-Head Cross-Attention)模块,使用交叉注意力(Cross-Attention)方法,同时接收来自编码器端的输出和当前 Transformer 块的前一个掩码注意力层的输出。查询是通过解码器前一层的输出进行投影的,而键和值是使用编码器的输出进行投影的。它的作用是在翻译的过程中,为了生成合理的目标语言序列,观测待翻译的源语言序列是什么。基于上述编码器和解码器结构,待翻译的源语言文本经过编码器端的每个 Transformer块对其上下文语义进行层层抽象,最终输出每一个源语言单词上下文相关的表示。解码器端以自回归的方式生成目标语言文本,即在每个时间步 t,根据编码器端输出的源语言文本表示,以及前t − 1 个时刻生成的目标语言文本,生成当前时刻的目标语言单词。
class EncoderLayer(nn.Module):
def __init__(self, d_model, heads, dropout=0.1):
super().__init__()
self.norm_1 = Norm(d_model)
self.norm_2 = Norm(d_model)
self.attn = MultiHeadAttention(heads, d_model, dropout=dropout)
self.ff = FeedForward(d_model, dropout=dropout)
self.dropout_1 = nn.Dropout(dropout)
self.dropout_2 = nn.Dropout(dropout)
def forward(self, x, mask):
attn_output = self.attn(x, x, x, mask)
attn_output = self.dropout_1(attn_output)
x = x + attn_output
x = self.norm_1(x)
ff_output = self.ff(x)
ff_output = self.dropout_2(ff_output)
x = x + ff_output
x = self.norm_2(x)
return x
class Encoder(nn.Module):
def __init__(self, vocab_size, d_model, N, heads, dropout):
super().__init__()
self.N = N
self.embed = Embedder(vocab_size, d_model)
self.pe = PositionalEncoder(d_model, dropout=dropout)
self.layers = get_clones(EncoderLayer(d_model, heads, dropout), N)
self.norm = Norm(d_model)
def forward(self, src, mask):
x = self.embed(src)
x = self.pe(x)
for i in range(self.N):
x = self.layers[i](x, mask)
return self.norm(x)
使用 PyTorch 实现的解码器参考代码如下:
class DecoderLayer(nn.Module):
def __init__(self, d_model, heads, dropout=0.1):
super().__init__()
self.norm_1 = Norm(d_model)
self.norm_2 = Norm(d_model)
self.norm_3 = Norm(d_model)
self.dropout_1 = nn.Dropout(dropout)
self.dropout_2 = nn.Dropout(dropout)
self.dropout_3 = nn.Dropout(dropout)
self.attn_1 = MultiHeadAttention(heads, d_model, dropout=dropout)
self.attn_2 = MultiHeadAttention(heads, d_model, dropout=dropout)
self.ff = FeedForward(d_model, dropout=dropout)
def forward(self, x, e_outputs, src_mask, trg_mask):
attn_output_1 = self.attn_1(x, x, x, trg_mask)
attn_output_1 = self.dropout_1(attn_output_1)
x = x + attn_output_1
x = self.norm_1(x)
attn_output_2 = self.attn_2(x, e_outputs, e_outputs, src_mask)
attn_output_2 = self.dropout_2(attn_output_2)
x = x + attn_output_2
x = self.norm_2(x)
ff_output = self.ff(x)
ff_output = self.dropout_3(ff_output)
x = x + ff_output
x = self.norm_3(x)
return x
class Decoder(nn.Module):
def __init__(self, vocab_size, d_model, N, heads, dropout):
super().__init__()
self.N = N
self.embed = Embedder(vocab_size, d_model)
self.pe = PositionalEncoder(d_model, dropout=dropout)
self.layers = get_clones(DecoderLayer(d_model, heads, dropout), N)
self.norm = Norm(d_model)
self.out = nn.Linear(d_model, vocab_size)
def forward(self, trg, e_outputs, src_mask, trg_mask):
x = self.embed(trg)
x = self.pe(x)
for i in range(self.N):
x = self.layers[i](x, e_outputs, src_mask, trg_mask)
return self.out(self.norm(x))
基于 Transformer 的编码器和解码器结构整体实现的参考代码如下:
class Transformer(nn.Module):
def __init__(self, src_vocab, trg_vocab, d_model, N, heads, dropout):
super().__init__()
self.encoder = Encoder(src_vocab, d_model, N, heads, dropout)
self.decoder = Decoder(trg_vocab, d_model, N, heads, dropout)
self.out = nn.Linear(d_model, trg_vocab)
def forward(self, src, trg, src_mask, trg_mask):
e_outputs = self.encoder(src, src_mask)
d_output = self.decoder(trg, e_outputs, src_mask, trg_mask)
output = self.out(d_output)
return output
可以使用如下代码对上述模型结构进行训练和测试:
# 模型参数定义
d_model = 512
heads = 8
N = 6
src_vocab = len(EN_TEXT.vocab)
trg_vocab = len(FR_TEXT.vocab)
model = Transformer(src_vocab, trg_vocab, d_model, N, heads)
for p in model.parameters():
if p.dim() > 1:
nn.init.xavier_uniform_(p)
optim = torch.optim.Adam(model.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9)
# 模型训练
def train_model(epochs, print_every=100):
model.train()
start = time.time()
temp = start
total_loss = 0
for epoch in range(epochs):
for i, batch in enumerate(train_iter):
src = batch.English.transpose(0,1)
trg = batch.French.transpose(0,1)
# 将我们输入的英语句子中的所有单词翻译成法语
# 除了最后一个单词,因为它为结束符,不需要进行下一个单词的预测
trg_input = trg[:, :-1]
# 试图预测单词
targets = trg[:, 1:].contiguous().view(-1)
# 使用掩码代码创建函数来制作掩码
src_mask, trg_mask = create_masks(src, trg_input)
preds = model(src, trg_input, src_mask, trg_mask)
optim.zero_grad()
loss = F.cross_entropy(preds.view(-1, preds.size(-1)),
targets, ignore_index=target_pad)
loss.backward()
optim.step()
total_loss += loss.item()
if (i + 1) % print_every == 0:
print(f"Epoch [{epoch + 1}/{epochs}], Step [{i + 1}/{len(train_iter)}], "
f"Loss: {loss.item():.4f}, Time: {time.time() - temp:.2f}s")
temp = time.time()
return total_loss / len(train_iter)
生成式预训练语言模型 GPT
受到计算机视觉领域采用 ImageNet[9] 对模型进行一次预训练,使得模型可以通过海量图像充分学习如何提取特征,再根据任务目标进行模型微调的范式影响,自然语言处理领域基于预训练语言模型的方法也逐渐成为主流。以 ELMo[10] 为代表的动态词向量模型开启了语言模型预训练的大门,此后,以 GPT[11] 和 BERT[1] 为代表的基于 Transformer 的大规模预训练语言模型的出现,使得自然语言处理全面进入了预训练微调范式新时代。利用丰富的训练数据、自监督的预训练任务及 Transformer 等深度神经网络结构,预训练语言模型具备了通用且强大的自然语言表示能力,能够有效地学习到词汇、语法和语义信息。将预训练模型应用于下游任务时,不需要了解太多的任务细节,不需要设计特定的神经网络结构,只需要“微调”预训练模型,即使用具体任务的标注数据在预训练语言模型上进行监督训练,就可以取得显著的性能提升。
OpenAI 公司在 2018 年提出的生成式预训练语言模型(Generative Pre-Training,GPT)[11] 是典型的生成式预训练语言模型之一。GPT 的模型结构如图2.3 所示,它是由多层 Transformer 组成的单向语言模型,主要分为输入层、编码层和输出层三部分。

自监督训练
GPT 采用生成式预训练方法,单向意味着模型只能从左到右或从右到左对文本序列建模,所采用的 Transformer 结构和解码策略保证了输入文本每个位置只能依赖过去时刻的信息。
给定文本序列 $w = w_1, w_2, · · · , w_n$,GPT 首先在输入层中将其映射为稠密的向量:

其中,$v_i^t$ 是词 $w_i$ 的词向量,$v_i^p$ 是词 $w_i$ 的位置向量,$v_i$ 为第 i 个位置的单词经过模型输入层(第 0层)后的输出。GPT 模型的输入层与前文中介绍的神经网络语言模型的不同之处在于其需要添加位置向量,这是 Transformer 结构自身无法感知位置导致的,因此需要来自输入层的额外位置信息。
经过输入层编码,模型得到表示向量序列 $v = v_1, v_2, · · · , v_n$,随后将 v 送入模型编码层。编码层由 L 个 Transformer 模块组成,在自注意力机制的作用下,每一层的每个表示向量都会包含之前位置表示向量的信息,使每个表示向量都具备丰富的上下文信息,而且,经过多层编码,GPT能得到每个单词层次化的组合式表示,其计算过程表示为:

其中 $h^{(l)} ∈ R^{d×n}$ 表示第 l 层的表示向量序列,n 为序列长度,d 为模型隐藏层维度,L 为模型总层数。
GPT 模型的输出层基于最后一层的表示 $h^{(L)}$,预测每个位置上的条件概率,其计算过程可以表示为

其中,$W^e ∈ R^{|V|×d} $为词向量矩阵,|V| 为词表大小。
单向语言模型按照阅读顺序输入文本序列 w,用常规语言模型目标优化 w 的最大似然估计,使之能根据输入历史序列对当前词做出准确的预测:

其中 θ 代表模型参数。也可以基于马尔可夫假设,只使用部分过去词进行训练。预训练时通常使用随机梯度下降法进行反向传播,优化该负对数似然函数。
有监督下游任务微调
通过自监督语言模型预训练,使得 GPT 模型具备了一定的通用语义表示能力。下游任务微调(Downstream Task Fine-tuning)的目的是在通用语义表示的基础上,根据下游任务的特性进行适配。下游任务通常需要利用有标注数据集进行训练,数据集使用 D 进行表示,每个样例由输入长度为n 的文本序列 $x = x_1, x_2, · · · , x_n$ 和对应的标签 y 构成。
先将文本序列 x 输入 GPT 模型,获得最后一层的最后一个词所对应的隐藏层输出 $h_n^{(L)}$,在此基础上,通过全连接层变换结合 Softmax 函数,得到标签预测结果。

其中 $W^y ∈ R^{d×k}$ 为全连接层参数,k 为标签个数。通过对整个标注数据集 D 优化如下目标函数精调下游任务:

在微调过程中,下游任务针对任务目标进行优化,很容易使得模型遗忘预训练阶段所学习的通用语义知识表示,从而损失模型的通用性和泛化能力,导致出现灾难性遗忘(Catastrophic Forgetting)问题。因此,通常采用混合预训练任务损失和下游微调损失的方法来缓解上述问题。在实际应用中,通常采用式(2.13)进行下游任务微调:

其中 λ 的取值为 [0, 1],用于调节预训练任务的损失占比。
预训练语言模型实践
HuggingFace 是一个开源自然语言处理软件库,其目标是通过提供一套全面的工具、库和模型,使自然语言处理技术对开发人员和研究人员更易于使用。HuggingFace 最著名的贡献之一是transformers 库,基于此,研究人员可以快速部署训练好的模型,以及实现新的网络结构。除此之外,HuggingFace 提供了 Dataset 库,可以非常方便地下载自然语言处理研究中经常使用的基准数据集。本节将以构建 BERT 模型为例,介绍基于 HuggingFace 的 BERT 模型的构建和使用方法。
数据集准备
常见的用于预训练语言模型的大规模数据集都可以在 Dataset 库中直接下载并加载。例如,如果使用维基百科的英文数据集,可以直接通过如下代码完成数据获取:
from datasets import concatenate_datasets, load_dataset
bookcorpus = load_dataset("bookcorpus", split="train")
wiki = load_dataset("wikipedia", "20220301.en", split="train")
# 仅保留'text'列
wiki = wiki.remove_columns([col for col in wiki.column_names if col != "text"])
dataset = concatenate_datasets([bookcorpus, wiki])
# 将数据集切分为90%用于训练,10%用于测试
d = dataset.train_test_split(test_size=0.1
接下来,将训练和测试数据分别保存在本地文件中,代码如下所示:
def dataset_to_text(dataset, output_filename="data.txt"):
""" 将数据集文本保存到磁盘的通用函数中 """
with open(output_filename, "w") as f:
for t in dataset["text"]:
print(t, file=f)
# 将训练集保存为train.txt
dataset_to_text(d["train"], "train.txt")
# 将测试集保存为test.txt
dataset_to_text(d["test"], "test.txt")
训练词元分析器
BERT 采用 WordPiece 分词算法,根据训练数据中的词频决定是否将一个完整的词切分为多个词元。因此,需要先训练词元分析器(Tokenizer)。可以使用 transformers 库中的 BertWordPieceTokenizer 类来完成任务,代码如下示:

预处理数据集
在启动整个模型训练之前,还需要将预训练数据根据训练好的词元分析器进行处理。如果文档长度超过 512 个词元,就直接截断。数据处理代码如下所示:

truncate_longer_samples 布尔变量控制用于对数据集进行词元处理的 encode() 回调函数。如果该变量设置为 True,则会截断超过最大序列长度(max_length)的句子。如果该变量设置为 False,则需要将没有截断的样本连接起来,并组合成固定长度的向量。

模型训练
在构建处理好的预训练数据之后,就可以开始模型训练。代码如下所示:

训练完成后,可以得到如下输出结果:

模型使用
可以针对不同应用需求使用训练好的模型,以句子补全为例的代码如下所示:

通过上述代码可以得到如下输出:

大语言模型的结构
当前,绝大多数大语言模型都采用类似 GPT 的架构,使用基于 Transformer 结构构建的仅由解码器组成的网络结构,采用自回归的方式构建语言模型,但是在位置编码、层归一化位置、激活函数等细节上各有不同。文献[13] 介绍了 GPT-3 模型的训练过程,包括模型架构、训练数据组成、训练过程及评估方法。由于 GPT-3 并没有开放源代码,根据论文直接重现整个训练过程并不容易,因此文献[29] 介绍了根据 GPT-3 的描述复现的过程,构造并开源了系统 OPT(Open Pre-trained TransformerLanguage Models)。MetaAI 也仿照 GPT-3 的架构开源了 LLaMA 模型[34],公开评测结果及利用该模型进行有监督微调后的模型都有非常好的表现。GPT-3模型之后,OpenAI 就不再开源(也没有开源模型),因此并不清楚 ChatGPT 和 GPT-4 采用的模型架构。
本节将以 LLaMA 模型为例,介绍大语言模型架构在 Transformer 原始结构上的改进,并介绍Transformer 结构中空间和时间占比最大的注意力机制的优化方法。
LLaMA 的模型结构
文献 [34] 介绍了 LLaMA 采用的 Transformer 结构和细节,与 2.1 节介绍的 Transformer 结构的不同之处为采用了前置层归一化(Pre-normalization)方法并使用 RMSNorm 归一化函数(Root Mean Square Normalizing Function),激活函数更换为 SwiGLU,使用了旋转位置嵌入(Rotary Positional Embeddings,RoPE),使用的 Transformer 结构与 GPT-2 类似,如图2.4 所示。

接下来,分别介绍 RMSNorm 归一化函数、SwiGLU 激活函数和 RoPE 的具体内容和实现。
RMSNorm 归一化函数
为了使模型训练过程更加稳定,GPT-2 相较于 GPT 引入了前置层归一化方法,将第一个层归一化移动到多头自注意力层之前,将第二个层归一化移动到全连接层之前。同时,残差连接的位置调整到多头自注意力层与全连接层之后。层归一化中也采用了 RMSNorm 归一化函数[45]。针对输入向量 a,RMSNorm 函数的计算公式如下:

此外,RMSNorm 还可以引入可学习的缩放因子 gi 和偏移参数 bi,从而得到 $ai = \frac{ai}{RMS(a)}gi + bi$。RMSNorm 在 HuggingFace transformers 库中的代码实现如下所示:

SwiGLU 激活函数
SwiGLU[46] 激活函数是 Shazeer 在文献 [46] 中提出的,在 PaLM[14] 等模型中进行了广泛应用,并且取得了不错的效果,相较于 ReLU 函数在大部分评测中都有不少提升。在 LLaMA 中,全连接层使用带有 SwiGLU 激活函数的位置感知前馈网络的计算公式如下:

其中,σ(x) 是 Sigmoid 函数。图2.5 给出了 Swish 激活函数在参数 β 取不同值时的形状。可以看到,当 β 趋近于 0 时,Swish 函数趋近于线性函数 y = x;当 β 趋近于无穷大时,Swish 函数趋近于 ReLU 函数;当 β 取值为 1 时,Swish 函数是光滑且非单调的。在 HuggingFace 的 transformers库中 Swish 函数被 SiLU 函数[47] 代替。

RoPE
在位置编码上,使用旋转位置嵌入[48] 代替原有的绝对位置编码。RoPE 借助复数的思想,出发点是通过绝对位置编码的方式实现相对位置编码。其目标是通过下述运算给 q, k 添加绝对位置信息:

详细的证明和求解过程可以参考文献 [48],最终可以得到二维情况下用复数表示的 RoPE:

根据复数乘法的几何意义,上述变换实际上是对应向量旋转,所以位置向量称为“旋转式位置编码”。还可以使用矩阵形式表示:

根据内积满足线性叠加的性质,任意偶数维的 RoPE 都可以表示为二维情形的拼接,即

由于上述矩阵 Rd 具有稀疏性,因此可以使用逐位相乘 ⊗ 操作进一步提高计算速度。RoPE 在HuggingFace transformers 库中的代码实现如下所示:

模型整体框架
基于上述模型和网络结构可以实现解码器层,根据自回归方式利用训练数据进行模型训练的过程与 2.2.3 节介绍的过程基本一致。不同规模的 LLaMA 模型使用的超参数如表2.1 所示。由于大语言模型的参数量非常大,并且需要大量的数据进行训练,因此仅利用单个 GPU 很难完成训练,需要依赖分布式模型训练框架(第 4 章将详细介绍相关内容)。


注意力机制优化
在 Transformer 结构中,自注意力机制的时间和存储复杂度与序列的长度呈平方的关系,因此占用了大量的计算设备内存并消耗了大量的计算资源。如何优化自注意力机制的时空复杂度、增强计算效率是大语言模型面临的重要问题。一些研究从近似注意力出发,旨在减少注意力计算和内存需求,提出了稀疏近似、低秩近似等方法。此外,有一些研究从计算加速设备本身的特性出发,研究如何更好地利用硬件特性对 Transformer 中的注意力层进行高效计算。本节将分别介绍上述方法。
稀疏注意力机制
对一些训练好的 Transformer 结构中的注意力矩阵进行分析时发现,其中很多是稀疏的,因此可以通过限制 Query-Key 对的数量来降低计算复杂度。这类方法称为稀疏注意力(Sparse Attention)机制。可以将稀疏化方法进一步分成基于位置的和基于内容的两类。
基于位置的稀疏注意力机制的基本类型如图2.6 所示,主要包含如下五种类型。
(1)全局注意力(Global Attention):为了增强模型建模长距离依赖关系的能力,可以加入一
些全局节点。
(2)带状注意力(Band Attention):大部分数据都带有局部性,限制 Query 只与相邻的几个节
点进行交互。
(3)膨胀注意力(Dilated Attention):与 CNN 中的 Dilated Conv 类似,通过增加空隙获取更大
的感受野。
(4)随机注意力(Random Attention):通过随机采样,提升非局部的交互能力。
(5)局部块注意力(Block Local Attention):使用多个不重叠的块(Block)来限制信息交互。

现有的稀疏注意力机制,通常是基于上述五种基于位置的稀疏注意力机制的复合模式,图2.7给出了一些典型的稀疏注意力模型。Star-Transformer[50] 使用带状注意力和全局注意力。具体来说,Star-Transformer 只包括一个全局注意力节点和宽度为 3 的带状注意力,其中任意两个非相邻节点通过一个共享的全局注意力连接,相邻节点则直接相连。Longformer[51] 使用带状注意力和内部全局节点注意力(Internal Global-node Attention)。此外,Longformer 将上层中的一些带状注意力头部替换为具有膨胀窗口的注意力,在增加感受野的同时并不增加计算量。ETC(Extended TransformerConstruction)[52] 使用带状注意力和外部全局节点注意力(External Global-node Attention)。ETC稀疏注意力还包括一种掩码机制来处理结构化输入,并采用对比预测编码(Contrastive Predictive Coding,CPC)[53] 进行预训练。BigBird[54] 使用带状注意力和全局注意力,并使用额外的随机注意力来近似全连接注意力。此外,BigBird 揭示了稀疏编码器和稀疏解码器的使用可以模拟任何图灵机,这也在一定程度上解释了为什么稀疏注意力模型可以取得较好的结果。

基于内容的稀疏注意力机制根据输入数据创建稀疏注意力,其中一种很简单的方法是选择和给定查询(Query)有很高相似度的键(Key)。Routing Transformer[55] 采用 K-means 聚类方法,针对 $Query{q_i}^T_{i=1}$ 和$Key{k_i}^T_{i=1}$ 进行聚类,类中心向量集合为 ${µ_i}^k_{i=1}$,其中 k 是类中心的个数。每个 Query 只与其处在相同簇(Cluster)下的 Key 进行交互。中心向量采用滑动平均的方法进行更新:

其中 |µ| 表示在簇 µ 中向量的数量。
Reformer[56] 则采用局部敏感哈希(Local-Sensitive Hashing,LSH)的方法为每个 Query 选择Key-Value 对。其主要思想是使用 LSH 函数对 Query 和 Key 进行哈希计算,将它们划分到多个桶内,以提升在同一个桶内的 Query 和 Key 参与交互的概率。假设 b 是桶的个数,给定一个大小为$[D_k, b/2] $的随机矩阵 R,LSH 函数的定义为

当 $hq_i = hk_j$ 时,$q_i$ 才可以与相应的 Key-Value 对进行交互。
FlashAttention
NVIDIA GPU 中的不同类型的内存(显存)有不同的速度、大小及访问限制。这主要取决于它们物理上是在 GPU 芯片内部还是在板卡 RAM 存储芯片上。GPU 显存分为全局内存(Global Memory)、本地内存(Local Memory)、共享存储(Shared Memory,SRAM)、寄存器(Register)、常量内存(Constant Memory)、纹理内存(Texture Memory)六大类。图 2.8 为 NVIDIA GPU 的整体内存结构示意图。全局内存、本地内存、共享存储和寄存器具有读写能力。全局内存和本地内存使用的高带宽显存(High Bandwidth Memory,HBM)位于板卡 RAM 存储芯片上,该部分内存容量很大。所有线程都可以访问全局内存,而本地内存只能由当前线程访问。NVIDIA H100中全局内存有 80GB 空间,其访问速度虽然可以达到 3.35TB/s,但当全部线程同时访问全局内存时,其平均带宽仍然很低。共享存储和寄存器位于 GPU 芯片上,因此容量很小,并且只有在同一个 GPU 线程块(Thread Block)内的线程才可以并行访问共享存储,而寄存器仅限于同一个线程内部访问。虽然 NVIDIA H100 中每个 GPU 线程块在流式多处理器(Stream Multi-processor,SM)上可以使用的共享存储容量仅有 228KB,但是其速度比全局内存的访问速度快很多。

前文介绍了自注意力机制的原理,在 GPU 中进行计算时,传统的方法还需要引入两个中间矩阵 S 和 P 并存储到全局内存中。具体计算过程如下:

按照上述计算过程,需要先从全局内存中读取矩阵 Q 和 K,并将计算好的矩阵 S 写入全局内存,然后从全局内存中获取矩阵 S,计算 Softmax 得到矩阵 P,再将其写入全局内存,最后读取矩阵P 和矩阵 V ,计算得到矩阵 O。这样的过程会极大地占用显存的带宽。在自注意力机制中,GPU的计算速度比内存速度快得多,因此计算效率越来越受全局内存访问的制约。
FlashAttention[57] 利用 GPU 硬件中的特殊设计,针对全局内存和共享存储的 I/O 速度的不同,尽可能地避免从 HBM 中读取或写入注意力矩阵。FlashAttention 的目标是尽可能高效地使用 SRAM来加快计算速度,避免从全局内存中读取和写入注意力矩阵。达成该目标需要做到在不访问整个输入的情况下计算 Softmax 函数,并且后向传播中不能存储中间注意力矩阵。在标准 Attention 算法中,Softmax 计算按行进行,即在与 V 做矩阵乘法之前,需要完成 Q、K 每个分块中的一整行的计算。在得到 Softmax 的结果后,再与矩阵 V 分块做矩阵乘。而在 FlashAttention 中,将输入分割成块,并在输入块上进行多次传递,以增量的方式执行 Softmax 计算。
自注意力算法的标准实现将计算过程中的矩阵 S、P 写入全局内存,而这些中间矩阵的大小与输入的序列长度有关且为二次型。因此,FlashAttention 就提出了不使用中间注意力矩阵,通过存储归一化因子来减少全局内存消耗的方法。FlashAttention 算法并没有将 S、P 整体写入全局内存,而是通过分块写入,存储前向传播的 Softmax 归一化因子,在后向传播中快速重新计算片上注意力,这比从全局内存中读取中间注意力矩阵的标准方法更快。虽然大幅减少了全局内存的访问量,重新计算也导致 FLOPS 增加,但总体来看运行的速度更快且使用的显存更少。具体算法如代码2.1 所示,其中内层循环和外层循环所对应的计算可以参考图2.9。


多查询注意力
多查询注意力(Multi Query Attention)[58] 是多头注意力的一种变体。它的特点是,在多查询注意力中不同的注意力头共享一个键和值的集合,每个头只单独保留了一份查询参数,因此键和值的矩阵仅有一份,这大幅减少了显存占用,使其更高效。由于多查询注意力改变了注意力机制的结构,因此模型通常需要从训练开始就支持多查询注意力。文献 [59] 的研究结果表明,可以通过对已经训练好的模型进行微调来添加多查询注意力支持,仅需要约 5% 的原始训练数据量就可以达到不错的效果。包括 Falcon[60]、SantaCoder[61]、StarCoder[62] 在内的很多模型都采用了多查询注意力。
以 LLM Foundry 为例,多查询注意力的实现代码如下:

与 LLM Foundry 中实现的多头注意力代码相比,其区别仅在建立 Wqkv 层上:

多头潜在注意力
多头潜在注意力(Multi-Head Latent Attention,MLA)[63] 是在 DeepSeek-V2 中引入的注意力优化模型。多头潜在注意力通过在键值层利用低秩矩阵,实现对压缩潜在键值状态的缓存(更详细的 KV 缓存可以参考本书第 10 章内容),从而大幅减少了 KV 缓存大小,有效缓解了通信瓶颈。
具体来说,MLA 方法的核心是是将传统多头注意力中的键(Key)和值(Vale)进行低秩联合压缩,得到一个低秩表示形式,以减少键值(KV)缓存。设 d 为嵌入维度,$n_h$ 为注意力头的数量,$d_h$ 为每个头的维度,$h_t ∈ R^d$ 是注意力层中第 t 个词元的输入。标准的多头注意力机制(MHA)首先通过三个矩阵 $W^Q、W^K、W^V ∈ R^{d_hn_h×d}$ 生成 $q_t、q_t、q_t ∈ R^{d_hn_h}$。MLA 方法则通过如下公式对 KV 缓存进行压缩:

其中,$c^{KV}_t ∈ R^{dc}$ 是键和值的压缩潜在向量(Comressed Latent Vector);$d_c(≪ d_hn_h)$ 表示键值压缩维度;$W^{DKV} ∈ R^{d_c×d}$ 是下投影矩阵;而 $W^{UK},W^{UV} ∈ R^{d_hn_h×d_c}$ 分别是键和值的上投影矩阵。在推理过程中,MLA 方法只需要缓存 $c^{KV}_t$ ,因此其键值缓存仅有 $d_cl$ 个元素,其中 l 表示层数。
此外,在推理过程中,由于 $W^{UK}$ 可以合并到 $W^Q$ 中,$W^{UV}$ 可以合并到 $W^O$ 中,甚至无需
在注意力计算中真正获得键和值。为了在训练过程中减少激活内存,还可以进一步对查询(Query)
进行低秩压缩:

其中,$c^Q_t ∈ R^{d^′_c}$ 是查询的压缩潜在向量;$d^′_c(≪ d_hn_h)$ 表示查询压缩维度,$W^{DQ} ∈ R^{d^′_c×d}$ 和$W^{UQ} ∈ R^{d_hn_h×d^′_c}$ 分别是查询的下投影矩阵和上投影矩阵。
文献 [64] 还进一步在理论上证明了 MLA 方法在表现力上优于组查询注意力(Group QueryAttention,GQA)。当 MLA 和 GQA 使用相同大小的 KV 缓存时,MLA 表现出更强的能力。这是因为在某些情况下,MLA 能够在通道输出上展现更大的多样性,而 GQA 由于组内头部是复制的,导致组内所有头部的输出相同,无法捕捉到 MLA 所能处理的某些情况。文献 [64] 还提出了TransMLA 后训练方法,该方法能够将广泛使用的基于 GQA 的预训练模型(例如 LLaMA、Qwen、Mixtral)转换为基于 MLA 的模型。转换后,通过进一步训练,在不增加 KV 缓存大小的前提下有效提升模型的表现力。
混合专家模型
随着 GPT-4[65]、Mixtral-8x7B[66]、DeepSeek-V3[40] 等模型的相继推出,混合专家模型 (Mixed Expert Models,MoEs) 日益受到关注。依据大模型缩放法则,模型规模是提升性能的关键,然而规模扩大必然使计算资源大幅增加。因此,在有限计算资源预算下,如何用更少训练步数训练更大模型成为关键问题。为解决该问题,混合专家模型基于一个简洁的思想:模型不同部分(即“专家”)专注不同任务或数据层面。混合专家架构的引入使得训练具有数千亿甚至万亿参数的模型成为可能,如开源的 1.6 万亿参数的 Switch Transformers[67] 等。
在采用混合专家架构的大语言模型中,MoE 层通常由门控网络(Gating Network)G 和 N 个专家网络(Experts Network)${f_1, f_2, ..., f_N }$ 组成。门控网络充当着选择器的角色,也称为路由,它负责决定将哪些输入数据发送给哪些专家。专家网络则分别处理特定的不同子任务。在这一过程中,并非所有专家都同时运作,而是由门控网络依据数据特性,精准地将数据路由到与之最为相关的专家那里,最终再根据一个或者多个专家输出的结果综合得到整体的预测结果。在模型架构的设计中,MoE 层通常安置于每个 Transformer 模块中前馈层(FFN)。当模型不断扩大时,FFN层在计算方面的需求也越来越高。例如,在参数数量达 5400 亿的 PaLM[14] 模型中,90% 的参数都位于前馈网络层内。
混合专家架构中,每个专家网络 $f_i$ 通常由一个前馈层组成,其参数使用 $W_i$ 表示。对于给入的输入 X,其输出使用 $f_i(X;W_i)$ 表示。门控网络 G 通常使用线性 Softmax(Linear-Softmax)网络构成,使用 Θ 表示其参数,其输出使用 $G_i(x; Θ)$ 表示。混合专家模型按照门控网络(Gate)类型,可以从广义上讲可以分为三个大类:稀疏混合专家模型(Sparse MoE)、稠密混合专家模型(Dense MoE)、软混合专家模型(Soft MoE),如图2.10所示。
本节将按照门控网络类型类型的分类,分别介绍稀疏混合专家模型、稠密混合专家模型和软混合专家模型的定义、特点和代表性工作。

稀疏混合专家模型
稀疏混合专家模型,如图2.10(a) 所示,对于每个输入词元,在前向计算中仅激活专家集合中的一个子集。门控网络对专家子集进行选择,通过计算排名前 K 位专家的输出加权和来实现稀疏性。这个过程可以形式化的表示为:

其中,$g(x; Θ)$ 表示在进行 softmax 操作之前的门控值,$G(x; Θ)i$ 表示门控网络针对第 i 个专家的输出,$TopK(·, K)$ 函数的目标是保持向量的前 K 项不变,其它维度设置为 −∞。鉴于 softmax 函数自身所具有的独特性质,当把其中某些项设置为 −∞ 时,这些项所对应的值会近似等同于 0。超参数 K 是根据具体应用来选取的,常见的取值选择为 $K = 1^{[67, 69]}$ 或者 $K = 2^{[66, 70–72]}$。添加噪声项 $R{noise}$ 是训练稀疏混合专家层的一种常用策略,一方面,它能够为模型创造更多的探索空间,促使不同专家模块之间展开多样化的尝试与协作,挖掘出潜在的优化路径;另一方面,通过打破可能出现的局部最优情况,提高了整个混合专家训练过程的稳定性[67]。
由 Mixtral AI 公司推出的 Mixtral-8x7B 模型[66] 就采用了稀疏混合专家方式,与早期的 Mistral 7B 模型[73] 共享基础架构。但是,Mixtral-8x7B 模型使用了稀疏混合专家层代替每个 Transformer 块中的前馈层,每个稀疏混合专家层包含 8 个专家网络,门控网络每次激活 2 个专家。但是在 Mixtral-8x7B 模型中没有引入噪声项 Rnoise,每个专家网络则使用了 SwiGLU 结构[46]。由于采用了稀疏混合专家方式,虽然 Mixtral-8x7B 模型的总参数量大约 560 亿,但是每次仅使用 130 亿个活跃参数。并且,Mixtral-8x7B 模型在很多基准测试中,展现出了优于或等同于包含了 700 亿参数的 Llama-2-70B[37] 的性能。此外,众多大语言模型也都采用了稀疏混合专家架构,包括 Switch Transformer[67]、DeepSeekMoE[74]、AdaMoE[75]、Yuan 2.0-M32[76]、OpenMoE[77]、Qwen1.5-MoE-A2.7B[78] 等。更多相关模型可以参考文献 [68]。

稀疏混合专家模型中采用常规的门控策略时,分配给不同专家的词元可能需要一些共有知识或信息才能处理。因此,多个专家可能会在各自的参数中获取同样的知识,进而导致专家参数出现冗余。如果构建专门用于捕捉并整合不同情境下共有知识的共享专家,那么其他专家之间的参数冗余情况将可能得到缓解。这种冗余情况的缓解,有助于构建一个参数利用更高效且专家专业性更强的模型。因此,DeepSeekMoE[74] 提出了分离 $K_s$ 个专家作为共享专家的思路。无论门控网络所给出的结果如何,每个词元都将被确定性地分配给这些共享专家,如图 2.11 所示,深色块 SharedFFN 为共享专家,所有输入都会分配给共享专家。为保持计算成本恒定,其他经门控网络分配的专家中被激活专家的数量将减少 $K_s$ 个。
稀疏混合专家模型中的 MoE 层对于并行计算也十分友好,能更便捷地在单个 GPU 上实现高效计算。常规稠密模型中,全部参数都会参与对所有输入数据的处理流程。与之不同,稀疏混合专家模型具备的稀疏特性,使得计算仅在系统的特定局部展开。也就是说,并非所有参数在处理各个输入时都会被触发或启用,而是依据输入的具体特性与需求,仅有特定的部分参数集被唤起并运行。因此,在并行计算中可以有效利用上述特性。例如,Megablocks[79] 将 MoE 层的前馈网络运算转换为大型稀疏矩阵乘法,极大地提高了执行速度,并且能够很好地处理不同专家分配到的数量不等的词元情况。此外,MoE 层可以通过标准的模型并行技术分布到多个 GPU 上,还可以借助专家并行(Expert Parallelism,EP)[80] 实现特殊的分区策略。
稠密混合专家模型
稠密混合专家模型,如图2.10(b) 所示,对于每个输入词元,在前向计算中激活所有专家网络${f_1, ..., f_N }$。门控网络根据输入赋予专家不同的权重。这个过程可以形式化的表示为:

由于稠密混合专家模型在前向计算过程中会激活所有参数,不能降低模型计算量。因此,大语言模型采用稠密混合专家结构的并不多,主要包括 EvoMoE[81]、MoLE[82]、LoRAMoE[83] 以及 DS-MoE[84]等。
虽然稠密混合专家模型需要使用全部参数进行计算,并不能减少模型计算时间,但是研究人员却发现,如果能够将 LoRA 方法和 MoE 相结合,可以在占用很少 GPU 显存的同时,减少微调数据的大规模扩增与模型世界知识维持之间存在的冲突。有监督微调是大语言模型应用的一个关键步骤,当模型需要与更广泛的下游任务保持一致,或者希望显著提高在特定任务上的表现时,大规模增加微调数据通常成为解决方案。然而当指令数据的大规模扩增可能会破坏大语言模型中之前储存的世界知识,即世界知识遗忘。LoRAMoE[83] 采用融合混合专家和 LoRA 插件的思想,插件形式确保了在训练阶段冻结主模型,保证了主模型世界知识的完整性。

LoRAMoE 模型架构如图2.12所示。基于插件的微调能够将参数的改动集中在额外引入的插件中,从而保证了模型知识的完整性,有机会引入其他插件来通过与主模型的交互来缓解知识遗忘。LoRAMoE 引入了多个与前反馈神经网络并列的专家,并通过路由相连,如图2.12中标注了“火焰”符号的部分,这些部分也是需要在后续学习中进行参数学习的结构。LoRAMoE 在训练阶段使用局部平衡约束损失(Localized Balancing Constraint),这种约束能够让专家自动划分为两个组:使一部分专家在专注于做下游任务的同时,另一部分专家专注于将指令与主模型的世界知识对齐,以缓解世界知识遗忘。同时局部平衡约束还能防止单个专家组内的专家退化现象,使路由平衡地关注于单个专家组的所有专家,防止个别专家长期占据优势,而其他专家未被充分训练或使用。这有助于专家之间相互配合以提高下游任务能力。微调后的 LoRAMoE 中的路由能够根据数据类型灵活地关注相应的专家,并使专家们相互配合,在保证下游任务表现的同时,也几乎不丧失世界知识。
软混合专家模型
软混合专家模型,如图2.10(c) 所示,门控网络依然根据输入为各个专家分配不同的权重,但与稠密混合专家模型在前向计算中激活所有专家网络不同,软混合专家模型引入了融合前馈层(Merged FFN)。该方法通过门控网络分配的权重对不同专家的参数进行融合,仅对融合后的前馈层参数进行计算。这种设计既能在几乎不增加计算成本的情况下完成计算,又保留了稠密混合专家模型中可使用基于梯度的训练方法的优势。这个过程可以形式化的表示为:

其中,fmerged 表示融合前向层,其结构与其余专家网络 fi 的结构相同。SMEAR 算法[85] 就采用了这种软混合专家结构。
软混合专家模型始终只计算单个专家的输出,其计算成本可能与单专家稀疏混合模型相当,明显低于稠密混合专家模型。但是,软混合专家模型的平均操作仍然会产生不可忽视的计算成本。为了量化这一成本,文献 [85] 分析了 SMEAR 算法的计算复杂度。假设专家网络架构是一个从 d 维激活值投射到 m 维向量的稠密计算,随后经过非线性变换,再附加一个从 m 维投射回 d 维的稠密计算。为简便起见,这里忽略成本相对较小的非线性变换成本。假定输入是一个长度为 L 的激活值序列,其大小为 L×d。在这种情况下,计算合并专家的输出会产生大约 L×4×d× m 次浮点运算(FLOPs)的计算成本,而采用 N 个专家的稠密混合专家模型则需要 N × L × 4 × d × m 次浮点运算。此外,软混合专家模型还必须对 N 个专家的参数进行平均,这又会额外产生 N × 2 × d × m
次浮点运算的成本。整体上 SMEAR 算法的计算复杂度是 (L × 4 + N × 2) × d × m。综合整体计算成本,软混合专家模型计算复杂度仍然远低于稠密混合专家模型。