通俗易懂的LLM(上篇)

前言

  2022年年底OpenAI发布ChatGPT,将LLM(Large Language Model)带向了一个新的高度,而2023年OpenAI继续放出大招:更强大的GPT-4问世,瞬间引爆了整个互联网圈。在这个大模型时代,作为一名NLPer,持续吸收着层出不穷的新技术,确实有些吃不消。俗话说,好记性不如烂笔头,在此记录下LLM相关技术及进展。顺便说一句,你可以说它不全面,但不能说它不通俗易懂。

一、Tuning

  虽然本篇博客的主要目的是介绍包括GPT系列在内的各种LLM的架构,但是在介绍LLM之前,我们有必要了解下Tuning(微调)的发展历程,它们推动着LLM朝向更智能的方向发展。
  目前学术界一般将NLP任务的发展分为四个阶段,即NLP四范式:

  • 第一范式:基于「传统机器学习模型」的范式,如TF-IDF特征+朴素贝叶斯等机器算法;
  • 第二范式:基于「深度学习模型」的范式,如word2vec特征+LSTM等深度学习算法,相比于第一范式,模型准确有所提高,特征工程的工作也有所减少;
  • 第三范式:基于「预训练模型+fine-tuning」的范式,如Bert+fine-tuning的NLP任务,相比于第二范式,模型准确度显著提高,模型也随之变得更大,但小数据集就可训练出好模型;
  • 第四范式:基于「预训练模型+Prompt+预测」的范式,如Bert+Prompt的范式相比于第三范式,模型训练所需的训练数据显著减少。

  在整个NLP领域,你会发现整个发展是朝着精度更高、少监督,甚至无监督的方向发展的。下面我们对第三范式、第四范式进行详细介绍。

1、Fine-Tuning(微调)

  Fine-Tuning是一种迁移学习,在自然语言处理(NLP)中,Fine-Tuning是用于将预训练的语言模型适应于特定任务或领域。Fine-Tuning的基本思想是采用已经在大量文本上进行训练的预训练语言模型,然后在小规模的任务特定文本上继续训练它。
  Fine-Tuning的概念已经存在很多年,并在各种背景下被使用。Fine-Tuning在NLP中最早的已知应用是在神经机器翻译(NMT)的背景下,其中研究人员使用预训练的神经网络来初始化一个更小的网络的权重,然后对其进行了特定的翻译任务的微调。
  经典的Fine-Tuning方法包括将预训练模型与少量特定任务数据一起继续训练。在这个过程中,预训练模型的权重被更新,以更好地适应任务。所需的Fine-Tuning量取决于预训练语料库和任务特定语料库之间的相似性。如果两者相似,可能只需要少量的Fine-Tuning,如果两者不相似,则可能需要更多的Fine-Tuning。
  Bert模型2018年横空出世之后,将Fine-Tuning推向了新的高度。不过目前来看,Fine-Tuning逐渐退出了tuning研究的舞台中心:LLM蓬勃发展,Fine-Tuning这种大规模更新参数的范式属实无法站稳脚跟。而更适应于LLM的tuning范式,便是接下来我们要介绍的Prompt-Tuning、Instruction-Tuning等。
  Fine-Tuning这块需要介绍的不多,不过Fine-Tuning的基座——PLM,比如Bert、Transformer,我们还是有必要了解的,这里给出两篇写的比较全面的博客,供大家参考:This post is all you need(上卷)——层层剥开TransformerThis post is all you need(下卷)——步步走进Bert

2、Prompt-Tuning(提示微调)

  在介绍Prompt-Tuning之前,我们有必要认识下In-context learning,Prompt-Tuning和In-context learning是prompt learning的两种模式。In-context learning是指在大规模预训练模型上进行推理时,不需要提前在下游目标任务上进行微调,即不改变预训练模型参数就可实现推理,其认为超大规模的模型只要配合好合适的模板就可以极大化地发挥其推理和理解能力。常用的In-context learning方法有few-shot、one-shot、zero-shot;Prompt-Tuning是指在下游目标任务上进行推理前,需要对全部或者部分参数进行更新,这里全部/部分的区别就在于预训练模型参数是否改变(其实本质上的Prompt-Tuning是不更新预训练模型参数的,这里有个特例方法称为Prompt-Oriented Fine-Tuning,其实该方法更适合称为升级版的Fine-Tuning,后面会详细介绍这个方法)。无论是In-context learning还是Prompt-Tuning,它们的目标都是将下游任务转换为预训练模型的预训练任务,以此来广泛激发出预训练模型中的知识。总的来说,基于Fine-Tuning的方法是让预训练模型去迁就下游任务。而基于Prompt-Tuning的方法可以让下游任务去迁就预训练模型。
  我们先以二分类的情感分析作为例子,描述Prompt-Tuning的工作原理。给定一个句子[CLS] I like the Disney films very much. [SEP] ,传统的Fine-Tuning方法是将其通过Bert获得 [CLS]表征之后再喂入新增加的MLP分类器进行二分类,预测该句子是积极的(positive)还是消极的(negative),因此需要一定量的训练数据来训练。而Prompt-Tuning则执行如下步骤:

  • 构建模板(Template Construction):通过人工定义、自动搜索、文本生成等方法,生成与给定句子相关的一个含有[MASK]标记的模板。例如It was [MASK].,并拼接到原始的文本中,获得Prompt-Tuning的输入:[CLS] I like the Disney films very much. It was [MASK]. [SEP]。将其喂入B模型中,并复用预训练好的MLM分类器(在huggingface中为BertForMaskedLM),即可直接得到[MASK]预测的各个token的概率分布;
  • 标签词映射(Label Word Verbalizer):因为[MASK]部分我们只对部分词感兴趣,因此需要建立一个映射关系。例如如果[MASK]预测的词是“great”,则认为是positive类,如果是“terrible”,则认为是negative类;此时会有读者思考,不同的句子应该有不同的template和label word,没错,因为每个句子可能期望预测出来的label word都不同,因此如何最大化的寻找当前任务更加合适的template和label word是Prompt-Tuning非常重要的挑战;
  • 训练:根据Verbalizer,则可以获得指定label word的预测概率分布,并采用交叉信息熵进行训练。此时因为只对预训练好的MLM head进行微调,所以避免了过拟合问题。

  基于上述内容,我们对prompt learning有个初步了解,接下来我们详细介绍下两种prompt learning:In-context learning和Prompt-Tuning。

2.1 In-context learning(上下文学习)

  In-context learning(ICL)又称为上下文学习,最早是在GPT-3《Language Models are Few-Shot Learners》中被提出来的。In-context learning(ICL)的关键思想是从类比中学习。下图给出了一个描述语言模型如何使用ICL进行决策的例子。首先,ICL需要一些示例来形成一个演示上下文。这些示例通常是用自然语言模板编写的。然后ICL将查询的问题(即你需要预测标签的input)和一个上下文演示(一些相关的cases)连接在一起,形成带有提示的输入(可称之为prompt),并将其输入到语言模型中进行预测。值得注意的是,与需要使用反向梯度更新模型参数的训练阶段的监督学习不同,ICL不需要参数更新,并直接对预先训练好的语言模型进行预测(这是与Prompt-Tuning不同的地方,ICL不需要在下游任务中Prompt-Tuning或Fine-Tuning)。它希望模型能自动学习隐藏在演示中的模式,并据此做出正确的预测。ICL

  • In-context learning的优势
    • 若干示例组成的演示是用自然语言撰写的,这提供了一个跟LLM交流的可解释性手段,通过这些示例跟模版让语言模型更容易利用到人类的知识;
    • 类似于人类类比学习的决策过程,举一反三;
    • 相比于监督学习,它不需要模型训练,减小了计算模型适配新任务的计算成本,更容易应用到更多真实场景。
  • In-context learning的流程:In-context learning可以分为两部分,分为作用于training跟inference阶段:
    • Training:在推理前,通过持续学习让语言模型的ICL能力得到进一步提升,这个过程称之为model warmup(模型预热),model warmup会优化语言模型对应参数或者新增参数,区别于传统的Fine-Tuning,Fine-Tuning旨在提升LLM在特定任务上的表现,而model warmup则是提升模型整体的ICL性能。

      • Supervised in-context training:为了增强ICL的能力,研究人员提出了通过构建in-context训练数据,进而进行一系列有监督in-context微调以及多任务训练。由于预训练目标对于In-context learning并不是最优的,Sewon Min等人提出了一种方法MetaICL《MetaICL: Learning to Learn In Context》,以消除预训练和下游ICL使用之间的差距。预训练LLM在具有演示样例的广泛的任务上进行训练,这提高了其few-shot能力,例如,MetaICL获得的性能与在52个独力数据集上进行有监督微调相当。
          此外,还有一个研究方向,即有监督指令微调,也就是后面要讲到的Instruction-Tuning。指令微调通过对任务指令进行训练增强了LLM的ICL能力。例如Google提出的FLAN方法《FINETUNED LANGUAGE MODELS ARE ZERO-SHOT LEARNERS》:通过在由自然语言指令模板构建的60多个NLP数据集上调整137B参数量的LaMDA-PT模型,FLAN方法可以改善zero-shot和few-shot ICL性能(具体可参考Finetuned Language Models are Zero-shot Learners笔记 - Instruction Tuning 时代的模型)。与MetaICL为每个任务构建若干演示样例相比,指令微调主要考虑对任务的解释,并且易于扩展。
      • Self-supervised in-context training:Supervised Learning指的是有一个model,输入是 x x x,输出是 y y y,要有label(标签)才可以训练Supervised Learning,比如让机器看一篇文章,决定文章是正面的还是负面的,得先找一大堆文章,标注文章是正面的还是负面的,正面负面就是label。Self-Supervised Learning就是机器自己在没有label的情况下,想办法做Supervised Learning。比如把没有标注的语料分成两部分,一部分作为模型的输入,一部分作为模型的输出,模型的输出和label越接近越好,具体参见2022李宏毅机器学习深度学习学习笔记第四周–Self-Supervised Learning。引申到self-supervised in-context training,是根据ICL的格式将原始数据转换成input-output的pair对数据后利用四个自监督目标进行训练,包括掩[MASK]预测,分类任务等。
         

        Supervised in-context training跟self-supervised in-context training旨在通过引入更加接近于In-context learning的训练目标从而缩小预训练跟ICL之间的差距。比起需要示例的In-context learning,只涉及任务描述的Instruction-Tuning更加简单且受欢迎。另外,在model warmup这个阶段,语言模型只需要从少量数据训练就能明显提升ICL能力,不断增加相关数据并不能带来ICL能力的持续提升。从某种角度上看,这些方法通过更新模型参数可以提升ICL能力也表明了原始的LLM具备这种潜力。虽然ICL不要求model warmup,但是一般推荐在推理前增加一个model warmup过程(解释一下:ICL最初的含义指的是大规模语言模型涌现出一种能力:不需要更新模型参数,仅仅修改输入prompt即添加一些例子就可以提升模型的学习能力。ICL相比之前需要对模型在某个特定下游任务进行Fine-Tuning大大节省了成本。之后ICL问题演变成研究怎么提升模型以具备更好更通用的ICL能力,这里就可以用上之前Fine-Tuning的方式,即指model warmup阶段对模型更新参数)。

    • Inference:很多研究表明LLM的ICL性能严重依赖于演示示例的格式,以及示例顺序等等,在使用目前很多LLM模型时我们也会发现,在推理时,同一个问题如果加上不同的示例,可能会得到不同的模型生成结果。

      • Demonstration Selection:对于ICL而言,哪些样本是好的?语言模型的输入长度是有限制的,如何从众多的样本中挑选其中合适的部分作为示例这个过程非常重要。按照选择的方法主要可以分为无监督跟有监督两种。
        • 无监督方法:首先就是根据句向量距离或者互信息等方式选择跟当前输入x最相似的样本作为演示示例,另外还有利用自适应方法去选择最佳的示例排列,有的方法还会考虑到演示示例的泛化能力,尽可能去提高示例的多样性。除了上述这些从人工撰写的样本中选择示例的方式外,还可以利用语言模型自身去生成合适的演示示例。

        • 监督方法:第一种是先利用无监督检索器召回若干相似的样本,再通过监督学习训练的Efficient Prompt Retriever进行打分,从而筛选出最合适的样本。此外还有基于Prompt Tuning跟强化学习的方式去选择样本。

      • Demonstration Ordering:挑选完演示示例后,如何对其进行排序也非常重要。排序的方法既有不需要训练的,也有根据示例跟当前输入距离远近进行排序的,也可以根据自定义的熵指标进行重排。
      • Demonstration Formatting:如何设计演示示例的格式?最简单的方式就是将示例们的 ( x , y ) (x,y) (x,y)对按照顺序直接拼接到一起。但是对于复杂的推理问题,语言模型很难直接根据 x x x推理出 y y y,这种格式就不适用了。另外,有的研究旨在设计更好的任务指令instruction作为演示内容(即Instruction-Tuning)。对于这两类场景,除了人工撰写的方式外,还可以利用语言模型自身去生成对应的演示内容。
  • In-context learning的模式:In-context learning包括三种模式,分别称作few-shot、one-shot以及zero-shot,三者的主要区别是prompt中包含的样本示例数量,下面简单介绍下这三种In-context learning模式:
    • Few-Shot:对下游任务,提供多条数据样例,论文中指出一般是10-100条;
    • One-Shot:few-shot的一种特殊情况,对下游任务,只提供一条数据样例;
    • Zero-Shot:是一种极端情况,对下游任务,不提供数据样例,只提供任务描述。

  以上内容对In-context learning做了简单介绍,详细内容大家可参考论文《A Survey on In-context Learning》《A Survey for In-context Learning》翻译。对于In-context learning及后面会讲到的Instruction-Tuning方法来说,如何设计输入的prompt是很重要的一点,有关prompt设计的方法,除了上面讲到的内容,这里还有一篇写的很好的文章供大家参考[译] Prompt Engineering: 循循善诱

2.2 Pattern-Verbalizer-Pair(PVP)

  ICL方法是在GPT-3中被提出的,这类方法有一个明显的缺陷是——其建立在超大规模的预训练语言模型上,此时的模型参数数量通常超过100亿,在真实场景中很难应用,因此众多研究者开始探索GPT-3的这套思路在小规模的语言模型(如Bert)上还是否适用?事实上,这套方法在小规模的语言模型上是可行的,但是需要注意:

  • 模型参数规模小了,prompt直接用在zero-shot上效果会下降(虽然GPT-3在zero-shot上效果也没有很惊艳,这也是后来Instruction-Tuning出现的原因),因此需要考虑将In-context learning应用在Fine-Tuning阶段,也就是后面要讲到的Prompt-Tuning。

  在介绍Prompt-Tuning之前,我们先介绍下实现Prompt-Tuning的重要组件——Pattern-Verbalizer-Pair(PVP)。Pattern-Verbalizer-Pair模式来源于大名鼎鼎的PET模型,PET(Pattern-Exploiting Training)出自《Exploiting Cloze Questions for Few Shot Text Classification and Natural Language Inference》。这里先简单介绍下论文的核心内容(下面蓝色字体内容可选择性学习,不感兴趣可直接跳到后面的PVP介绍部分):由于在实际任务中,模型往往只会接触到少量的labeled examples(few-shot learning),而直接将监督学习运用到小样本学习会使得模型表现不佳,针对这个问题,论文中提出了Pattern-Exploiting Training (PET),使用natural language patterns将input examples规范为完型填空形式的半监督训练机制。通过这种方法,成功地在few-shot settings上将task descriptions与标准监督学习结合。具体的步骤是:

  • 构建一组pattern,对于每一个pattern, 会使用一个PLM在小样本训练集上进行Fine-Tuning;
  • 训练后的所有模型的集合会被用来在大规模unlabeled dataset标注soft labels;
  • 在soft labels数据集上训练一个标准分类器。

  另外在该论文中,作者提出,在每一个PLM上只进行一次微调+soft labels生成,通常得到的新的数据集(即用soft labels标记的unlabeled dataset)会有很多错误的数据,因此扩展提出iPET模型(Iterative PET),即添加了迭代过程:首先随机从集成的预训练模型集合中抽取部分预训练模型,在未标注数据集(unlabeled dataset) D \mathcal{D} D 上标注数据,并扩增到初始有标签数据集 T \mathcal{T} T上,其次再根据扩增后的 T \mathcal{T} T分别微调预训练模型。上述过程一直迭代多次:

  • 假设初始化有n个预训练模型 M 0 = M 1 0 , … … , M n 0 \mathcal{M}^{0}=M^{0}_{1},……,M^{0}_{n} M0=M10,……,Mn0。在第 j j j轮迭代,则先随机从上一轮迭代获得的预训练模型集合中抽取 λ ⋅ ( n − 1 ) \lambda \cdot (n-1) λ(n1)个模型,记做 N \mathcal{N} N,其中 λ ∈ ( 0 , 1 ] 是超参数 \lambda\in(0, 1]是超参数 λ(0,1]是超参数
  • 其次使用该预训练集合,生成一个标注数据集:
    T N = { ( x , arg ⁡ max ⁡ l ∈ L s N ( l ∣ x ) ) ∣ x ∈ D }   \mathcal{T}_{\mathcal{N}} =\{(x, \arg\max_{l\in\mathcal{L}}s_{\mathcal{N}}(l|x))|x\in\mathcal{D}\}\ TN={(x,arglLmaxsN(lx))xD} 
    L \mathcal{L} L是所有类标签的集合。由上式可知,每次从每个类 l l l中挑选得分最高的样本,以避免引入大量的错误标注数据。其中 s N ( l ∣ x ) s_{\mathcal{N}}(l|x) sN(lx)表示每个样本通过预训练模型集合 N \mathcal{N} N推理后的得分(这个得分是 N \mathcal{N} N中每个预训练模型对样本 x x x推理后得分的加权结果,具体可参考原始PET过程);
  • 生成的标注数据集 T N \mathcal{T}_{\mathcal{N}} TN并不是完全用于下个迭代的训练,而是从 T N \mathcal{T}_{\mathcal{N}} TN中抽取一部分数据扩充到原始有标签数据集 T \mathcal{T} T中,扩充规模以固定常数 d ∈ N d \in N dN进行,同时要保持原始数据集中标签的比例不变,即对于每一个标签 l ∈ L l \in \mathcal{L} lL,从 T N \mathcal{T}_{\mathcal{N}} TN中选择 c j ( l ) − c 0 ( l ) c_{j}(l)-c_{0}(l) cj(l)c0(l)个标签为 l l l的样本构成数据集 T N ( l ) \mathcal{T}_{\mathcal{N}}(l) TN(l),其中 c j ( l ) = d ⋅ c j − 1 ( l ) c_{j}(l)=d \cdot c_{j-1}(l) cj(l)=dcj1(l) c j ( l ) c_{j}(l) cj(l)表示第 j j j轮迭代第 i i i个模型所需的标注数据集 T i j \mathcal{T}_{\mathcal{i}}^{j} Tij中标签为 l l l的样本数, c 0 ( l ) c_{0}(l) c0(l)表示初始有标签数据集 T \mathcal{T} T中标签为 l l l的样本数。因此:
    T i j = T ∪ U l ∈ L T N ( l )   \mathcal{T}_{\mathcal{i}}^{j} = \mathcal{T} \cup \textup{U}_{l \in \mathcal{L}}\mathcal{T}_{\mathcal{N}}(l)\ Tij=TUlLTN(l) 
    可以很容易得出该数据集对于标签 l l l包含 c j ( l ) c_{j}(l) cj(l)个样本。需要注意的是,虽然每一个预训练模型对应单独的标注数据集 T i j \mathcal{T}_{\mathcal{i}}^{j} Tij,但显而易见的是,它们之间的区别只是在于Pattern-Verbalizer-Pair(PVP)模板之间的区别,原始文本数据都是相同的。
  • 使用扩增后的标注数据集 T i j \mathcal{T}_{\mathcal{i}}^{j} Tij分别微调预训练模型。上述过程重新进行即可。

  上述内容具体可参考:论文解读:Exploiting Cloze Questions for Few Shot Text Classification and Natural Language Inference论文阅读:PET系列。论文中有关PET及IPET部分的介绍,不是本部分关心的重点,大家选择性学习。下面着重介绍本部分最关心,也是PET最核心的部分Pattern-Verbalizer-Pair(PVP),PET设计了两个很重要的组件:

  • Pattern(Template):记作 T \mathcal{T} T ,即上文提到的Template,其为额外添加的带有[mask]标记的短文本,通常一个样本只有一个Pattern(因为我们希望只有1个让模型预测的[mask]标记)。由于不同的任务、不同的样本可能会有其更加合适的pattern,因此如何构建合适的pattern是Prompt-Tuning的研究点之一;
  • Verbalizer:记作 V \mathcal{V} V,即标签词的映射,对于具体的分类任务,需要选择指定的标签词(label word)。例如情感分析中,我们期望Verbalizer可能是: V ( positive ) = great \mathcal{V}(\text{positive})=\text{great} V(positive)=great V ( negative ) = terrible \mathcal{V}(\text{negative})=\text{terrible} V(negative)=terrible(positive和negative是类标签)。同样,不同的任务有其相应的label word,但需要注意的是,Verbalizer的构建需要取决于对应的Pattern。因此如何构建Verbalizer是另一个研究挑战。
      上述两个组件即为Pattern-Verbalizer-Pair(PVP),一般记作 P = ( T , V ) \mathcal{P}=(\mathcal{T}, \mathcal{V}) P=(T,V),在后续的大多数研究中均采用这种PVP组件。学到这里,我们面临的最大疑问:对于下游任务,如何挑选合适的Pattern和Verbalizer?自2020年底至今,学术界已经涌现出各种方案试图探索如何自动构建PVP。其实也许在大多数人们的印象中,合适的Pattern才是影响下游任务效果的关键,Verbalizer对下游任务的影响并不大,而下面这个实验便很好的证明了Verbalizer的作用:如下图所示,以SST-2为例,相同的模板条件下,不同的label word对应的指标差异很大。Verbalizer设计对比实验
      构建Verbalizer的方法也有很多,不过这里不进行详细解释,感兴趣的同学可以参考Prompt-Tuning——深度解读一种新的微调范式,里面说明的比较详细。下面我们着重介绍构建Pattern的方法:Prompt-Tuning

2.3 Prompt-Tuning

  通过上个部分的介绍,我们已经了解,Prompt-Tuning是用来自动构建pattern的方法,接下来我们根据使用场景的不同,分别介绍几种成熟的Prompt-Tuning方法。

  • Prompt-Oriented Fine-Tuning:这个就是前面提到的需要更新全部参数(包括预训练模型参数)的Prompt-Tuning方法。Prompt-Oriented Fine-Tuning训练方法的本质,上面已经提到过,其实是将目标任务转换为适应预训练模型的预训练任务,以适应预训练模型的学习体系。例如我们在Bert模型上做情感分类任务,正常的Fine-Tuning流程,是将训练文本经过Bert编码后,生成向量表征,再利用该向量表征,连接全连接层,实现最终的情感类别识别。这种方式存在一个显式的弊端:预训练任务与下游任务存在gap,我们知道Bert的预训练任务包括两个:MLM与NSP(具体可参考Bert预训练的任务MLM和NSP),简单来说,MLM任务是通过分类模型识别被MASK掉的词,类别大小即为整个词表大小;NSP任务是预测两个句子之间的关系;而Prompt-Oriented Fine-Tuning训练方法,是将情感分类任务转换为类似于MLM任务的[MASK]预测任务,具体来说,我们构建如下的prompt文本:prompt = It was [MASK].,将prompt文本与输入text文本text = The film is attractive.进行拼接生成It was [MASK].The film is attractive.,输入至预训练模型中,训练任务目标和MLM任务的目标一致,即识别被[MASK]掉的词。通过这种方式,可以将下游任务转换为和预训练任务较为一致的任务,已有实验证明,Prompt-Oriented Fine-Tuning相对于常规的Fine-Tuning,效果确实会得到提升(Prompt进行情感分类)。
      通过以上描述我们可以知道,Prompt-Oriented Fine-Tuning方法中,预训练模型参数是可变的。其实将Prompt-Oriented Fine-Tuning方法放在Prompt-Tuning这个部分合理也不合理,因为它其实是Prompt-Tuning+Fine-Tuning的结合体,将它视为Fine-Tuning的升级版是最合适的。Prompt-Oriented Fine-Tuning方法在Bert类相对较小的模型上表现较好,但是随着模型越来越大,如果每次针对下游任务,都需要更新预训练模型的参数,资源成本及时间成本都会很高,因此后续陆续提出了不更新预训练模型参数,单纯只针对prompt进行调优的方法,例如Hard PromptSoft Prompt
      这里再给出一些常见下游任务的prompt设计:
    常见任务的Prompt设计

  • Hard Prompt & Soft Prompt:承接上文,Hard Prompt和Soft Prompt的提出,是为了解决预训练模型过大,难以针对下游任务进行训练的痛点。目前常见的Hard Prompt和Soft Prompt方法,分为以下五种:

    • 人工构建(Manual Template):最简单的构建模板方法;
    • 启发式法(Heuristic-based Template):通过规则、启发式搜索等方法构建合适的模板;
    • 生成(Generation):根据给定的任务训练数据(通常是小样本场景),生成出合适的模板;
    • 词向量微调(Word Embedding):显式地定义离散字符的模板,但在训练时这些模板字符的词向量参与梯度下降,初始定义的离散字符用于作为向量的初始化;
    • 伪标记(Pseudo Token):不显式地定义离散的模板,而是将模板作为可训练的参数。
  • Hard Prompt:前面三种称为离散的模板构建法(记作Hard Template、Hard Prompt、Discrete Template、Discrete Prompt),其旨在直接与原始文本拼接显式离散的字符,且在训练中始终保持不变。这里的保持不变是指这些离散字符的词向量(Word Embedding)在训练过程中保持固定。通常情况下,离散法不需要引入任何参数。主要适用场景是GPT-3类相对较大的模型,Bert类相对较小的模型也可以用,只是个人觉得Bert等预训练模型,针对下游任务训练的成本并不是很高,完全可以同时微调预训练模型参数。上述三种Hard Prompt方法,实际场景中用的比较少,这里就不一一介绍了,大家有兴趣可以参考Prompt-Tuning——深度解读一种新的微调范式

  • Soft Prompt:后面两种则被称为连续的模板构建法(记作Soft Template、Soft Prompt、Continuous Template、Continuous Prompt),其旨在让模型在训练过程中根据具体的上下文语义和任务目标对模板参数进行调整。反观Hard Prompt方法,不论是启发式方法,还是通过生成的方法,都需要为每一个任务单独设计对应的模板,因为这些模板都是可读的离散的token,这导致很难寻找到最佳的模板。另外,即便是同一个任务,不同的句子也会有其所谓最佳的模板,而且有时候,即便是人类理解的相似的模板,也会对模型预测结果产生很大差异。例如下图,以SNLI推断任务为例,仅仅只是修改了模板,测试结果差异很明显,因此离散的模板存在方差大、不稳定等问题。Hard Prompt设计对比实验
      如何避免这种问题呢,Soft Prompt方法便是来解决这种问题的,其将模板转换为可以进行优化的连续向量,换句话说,我们不需要显式地指定这些模板中各个token具体是什么,只需要在语义空间中表示一个向量即可,这样,不同的任务、数据可以自适应地在语义空间中寻找若干合适的向量,来代表模板中的每一个词,相较于显式的token,这类token称为伪标记(Pseudo Token)。下面给出基于Soft Prompt的模板定义:

    假设针对分类任务,给定一个输入句子 x x x,连续提示的模板可以定义为:
    T = [ x ] , [ v 1 ] , [ v 2 ] , … , [ v m ] [ M A S K ]   \mathcal{T} =[x],[v_{1}],[v_{2}],…,[v_{m}][MASK]\ T=[x],[v1],[v2][vm][MASK] 其中 [ v 1 ] [v_{1}] [v1]则是伪标记,其仅代表一个抽象的token,并没有实际的含义,本质上是一个向量。

      总结来说:Soft Prompt方法,是将模板变为可训练的参数,不同的样本可以在连续的向量空间中寻找合适的伪标记,同时也增加模型的泛化能力。因此,连续法需要引入少量的参数并在训练时进行参数更新,但预训练模型参数是不变的,变的是prompt token对应的词向量(Word Embedding)表征及其他引入的少量参数。主要适用场景同Hard Prompt一致。目前具有代表性的三种Soft Prompt方法如下,下面我们进行逐一介绍:

    • Parameter-Efficient Prompt Tuning:该方法率先提出了伪标记和连续提示的概念,支持模型能够动态地对模板在语义空间内进行调整。主要针对的是NLU任务,形式化的描述如下:

      给定 n n n个token,记作 x 1 , . . . , x n x_{1}, ..., x_{n} x1,...,xn,通过一个预训练模型对应的embedding table,将 n n n个token表征为向量矩阵 X e ∈ R n × e X_{e} \in R^{n\times e} XeRn×e,其中 e e e是向量的维度(其与预训练模型的配置有关,例如Bert-base是768)。连续模板中的每个伪标记 v i v_{i} vi可以视为参数,也可以视为一个token,因此,可以通过另一个embedding table将 p p p个伪标记token表征为向量矩阵 P e ∈ R p × e P_{e} \in R^{p\times e} PeRp×e 。将文本和prompt进行拼接获得新的输入 [ P e : X e ] ∈ R ( p + n ) × e [P_{e} :X_{e}] \in R^{(p+n) \times e} [Pe:Xe]R(p+n)×e。这个新的输入将会进入T5的encoder-decoder结构来训练和推理。注意,只有prompt对应的向量表征参数 P e P_{e} Pe会随着训练进行更新。

        论文中提到,每个伪标记的初始化可以有下列三种情况,分别是Random Uniform,Sampled Vocab和Class Label。

      • Random Uniform:从均匀分布中随机进行初始化;
      • Sampled Vocab:从T5的语料库中选择最常见的5000个词汇,并从中选择词汇嵌入作为初始化;
      • Class Label:是将下游任务的标签对应的字符串表示的嵌入作为初始化,如果一个类有多个词,取词嵌入的平均表示作为一个prompt。假如标签数目不足,则从Sampled Vocab方案中继续采样补足。
         

        最后发现,非随机初始化方法要显著好于随机初始化,而Class Label效果相对更好,当然,只要模型足够大,这几种初始化方法的差异就比较小了。具体论文参考2021年谷歌发表的《The Power of Scale for Parameter-Efficient Prompt Tuning》

    • P-Tuning:P-Tuning是另一个具有代表性的连续提示方法,主要针对的是NLU任务,方法图如下所示(图中的 P i P_{i} Pi等价于上文的 v i v_{i} vi,表示伪标记),谷歌于2021年发表。P-Tuning结构
      P-Tuning方法中的四个技巧点:

      • 考虑到这些伪标记的相互依赖关系:认为 [ P 1 ] [P_{1}] [P1] [ P 2 ] [P_{2}] [P2]是有先后关系的,而transformer无法显式地刻画这层关系,因此引入Prompt Encoder(BiLSTM+MLP);
      • 指定上下文词:如果模板全部是伪标记,在训练时无法很好地控制这些模板朝着与对应句子相似的语义上优化,因此选定部分具有与当前句子语义代表性的一些词作为一些伪标记的初始化(例如上图中“capital”、“Britain”等);
      • 重参数(Reparameterization):具体到代码实现上,P-Tuning先通过一个Prompt Encoder表征这些伪标记后,直接将这些新的表征覆盖到对应的embedding table上,换句话说,Prompt Encoder只在训练时候会使用到,而在推理阶段则不再使用,直接使用构建好的embedding table;
      • 混合提示(Hydride Prompt):将连续提示与离散token进行混合,例如 [ x ] [ i t ] [ v 1 ] [ m a s k ] [x][it][v1][mask ] [x][it][v1][mask]
         

        具体可参考:2021年发表的《GPT Understands, Too》《论文解读:GPT Understands, Too》《细读经典:P-Tuning》

    • PPT(Pre-trained Prompt Tuning):Prompt-Tuning通常适用于低资源场景,但是由于连续的模板是随机初始化的,即其存在新的参数,少量样本可能依然很难确保这些模板被很好地优化。因此简单的方法就是对这些连续的模板进行预训练。PPT旨在通过先让这些连续提示在大量无标注的预训练语料进行预训练,然后将其加载到对应下游任务的PLM上进行训练。具体来说,作者对3种Prompt-Tuning的优化策略在few-shot learning问题上分别进行了效果对比,包括hard prompt和soft prompt结合、label到text映射方法选择以及使用真实单词的embedding进行soft prompt的随机初始化。通过对比实验发现,hard+soft prompt结合的方法可以提升效果,但是仍然比finetune效果差。Label到text的映射方法对于效果影响很大,选择能够表达label对应含义的常用单词会带来最好效果。而使用单词embedding进行soft prompt的初始化在大模型上并没有明显的效果提升。
        基于以上实验结果,作者提出了Pre-trained Pormpt Tuning解决few-shot learning问题,核心思路是对soft prompt进行预训练,得到一个更好的soft prompt初始化表示。对于每种类型的任务,设计一个和其匹配的预训练任务,得到soft prompt embedding的预训练表示。
        论文中以sentence-pair classification、multiple-choice classification、single sentence classification三种任务介绍了如何针对每种下游任务设计预训练任务学习soft prompt embedding。例如对于sentence-pair classification,作者设计了如下预训练任务。将2个句子对拼接在一起,如果两个句子来自同一个文档相邻两句话,则label为yes(完全一致);如果两个句子来自同一个文档但距离较远,则label为maybe;其他句子对label为no,如下图所示(图中的 P P P即连续的提示模板, < x > <x> <x>表示mask token。最上面的任务是预训练任务,下面三个任务为下游任务)。PPT核心思想  

        另外论文中还给出了四种微调方案,如下图所示,[a]展示了模型的预训练过程,[b]和[c]展示了两种主流的Fine-Tuning方法(前文已经介绍过),[d]展示了提示学习( Prompt Tuning, PT )方法,具体可参考2022年清华大学发表的《PPT: Pre-trained Prompt Tuning for Few-shot Learning》小样本学习:Pre-trained Prompt Tuning for Few-shot LearningPrompt 如何更好地应用于工业界?Tuning方案

2.4 Prompt-Tuning vs Fine-Tuning

  至此,我们已经深入了解了Fine-Tuning和Prompt-Tuning两种微调方法,也或多或少能观察到二者之间的区别,我们在这里进行下总结。众多周知,Prompt-Tuning是在Fine-Tuning后发展起来的,可以说是解决NLP领域各种下游问题更好的一种方式。要提出一个好的方式那必然是用来「解决另一种方式存在的缺陷或不足」,那我们就先从预训练模型PLM+Fine-Tuning范式说起,这个范式常用的结构是Bert+Fine-Tuning,这种范式若想要预训练模型更好的应用在下游任务,需要利用下游数据对模型参数微调;首先,模型在预训练的时候,采用的训练形式:自回归、自编码,这与下游任务形式存在极大的 gap,不能完全发挥预训练模型本身的能力,必然导致:较多的数据来适应新的任务形式(少样本学习能力差、容易过拟合)。其次,现在的预训练模型参数量越来越大,为了一个特定的任务去Fine-Tuning一个模型,会占用特别多的训练资源,对一些中小企业或者用户来说并不现实,也会造成资源的一定浪费。
  而Prompt-Tuning则很好的解决了这些问题,它将所有下游任务统一成预训练任务,以特定的模板,将下游任务的数据转成自然语言形式,充分挖掘预训练模型本身的能力。本质上就是设计一个比较契合上游预训练任务的模板,通过模板的设计来挖掘出上游预训练模型的潜力,让上游的预训练模型在尽量不需要标注数据的情况下比较好的完成下游的任务,即只需要少量数据的 Prompt Tuning,就可以实现很好的效果,具有较强的零样本/少样本学习能力。具体可参考Prompt-Tuning VS Fine-Tuning

3、Instruction-Tuning(指示微调)

  前文中已经多次提到过Instruction-Tuning,可以说在大规模语言模型领域,它是目前最火的研究范式,性能超过包括In-context learning在内的prompt learning。

3.1、Instruction-Tuning的提出

  回顾Instruction-Tuning的发展历程,首先是Google2021年的FLAN模型《FINETUNED LANGUAGE MODELS ARE ZERO-SHOT LEARNERS》,这篇文章明确提出Instruction-Tuning(指令微调)的技术,它的本质目的是想将 NLP 任务转换为自然语言指令,再将其投入模型进行训练,通过给模型提供指令和选项的方式,使其能够提升Zero-Shot任务的性能表现。
  Instruction-Tuning提出的动机在于大规模的语言模型如GPT-3可以非常好地学习few-shot,但它在zero-shot上却不那么成功。例如, GPT-3在阅读理解、问题回答和自然语言推理等任务上的表现很一般,作者认为一个潜在的原因是,如果在没有少量示例的zero-shot条件下,模型很难在prompts上表现很好,因为prompts可能和预训练数据的格式相差很大。
  既然如此,那么为什么不直接用自然语言指令做输入呢?通过设计instruction,让大规模语言模型理解指令,进而完成任务目标,而不是直接依据演示实例做文本生成。如下图所示,不管是commonsense reasoning任务还是machine translation任务,都可以变为instruction的形式,然后利用大模型进行学习。在这种方式下,当一个unseen task进入时,通过理解其自然语言语义可以轻松实现zero-shot的扩展,如natural language inference任务。
FLAN
FLAN
  接下来,我们介绍下FLAN的具体训练流程。
  具体来说,作者提出的Finetuned Language Net(FLAN)模型将62个NLP任务分为12个簇,同一个簇内是相同的任务类型,如下图所示。
FLAN-TASK
  对于每个task,将为其手动构建10个独特template,作为以自然语言描述该任务的instructions。为了增加多样性,对于每个数据集,还包括最多三个“turned the task around/变更任务”的模板(例如,对于情感分类,要求其生成电影评论的模板)。所有数据集的混合将用于后续预训练语言模型做Instruction-Tuning,其中每个数据集的template都是随机选取的。如下图所示,Premise、Hypothesis、Options会被填充到不同的template中作为训练数据。
FLAN-Template
  最后基于LaMDA-PT模型进行微调。LaMDA-PT是一个包含137B参数的自回归语言模型,这个模型在web文档(包括代码)、对话数据和维基百科上进行了预训练,同时有大约10%的数据是非英语数据。然后FLAN混合了所有构造的数据集在128核的TPUv3芯片上微调了60个小时。
  至此,我们详细介绍了包括FLAN在内的Instruction-Tuning方法,总结来说,Instruction-Tuning也是In-context learning的一种,只是Instruction-Tuning是将大模型在多种任务上进行微调,提升大模型的自然语言理解能力,最终实现在新任务上的zero-shot。目前另外一个采用了Instruction-Tuning技术的大规模语言模型是instructGPT,后面我们会详细介绍instructGPT的具体实现方式。

3.2、Fine-Tuning vs Prompt-Tuning vs Instruction-Tuning

  • Fine-Tuning:先在大规模语料上进行预训练,然后再在某个下游任务上进行微调,如Bert+Fine-Tuning;

  • Prompt-Tuning:先选择某个通用的大规模预训练模型,然后为具体的任务生成一个prompt模板以适应大模型进行微调,如GPT-3+Prompt-Tuning;

  • Instruction-Tuning:仍然在预训练语言模型的基础上,先在多个已知任务上进行指令微调,然后在某个新任务上进行zero-shot,如GPT-3+Instruction-Tuning;

  • Prompt-Tuning vs Instruction-Tuning:Prompt和instruction都是指导语言模型生成输出的文本片段,但它们有着不同的含义和用途。

    • Prompt通常是一种短文本字符串,用于指导语言模型生成响应。Prompt提供上下文和任务相关信息,以帮助模型更好地理解要求,并生成正确的输出。例如,在问答任务中,prompt可能包含问题或话题的描述,以帮助模型生成正确的答案。Prompt通常是人类设计的,以帮助模型更好地理解特定任务或领域;
    • Instruction通常是一种更详细的文本,用于指导模型执行特定操作或完成任务。Instruction可以是计算机程序或脚本,也可以是人类编写的指导性文本。Instruction的目的是告诉模型如何处理数据或执行某个操作,而不是简单地提供上下文或任务相关信息。
       

      因此,Prompt和instruction都是用于指导模型生成输出的文本,但它们的目的和使用方式是不同的。Prompt更多地用于帮助模型理解任务和上下文,而Instruction则更多地用于指导模型执行具体操作或完成任务。
    FT vs PT vs IT

      对于Prompt-Tuning和Instruction-Tuning还有一个不同点,就是prompt在没精调的模型上也能有一定效果(模型不经过Prompt-Tuning,直接针对下游任务进行推理),而Instruction-Tuning则必须对模型精调,让模型知道这种指令模式。但是,prompt也有精调,经过Prompt-Tuning之后,模型也就学习到了这个prompt模式,精调之后跟Instruction-Tuning有什么区别呢?这就是Instruction-Tuning巧妙的地方了,Prompt-Tuning都是针对一个任务的,比如做个情感分析任务的Prompt-Tuning,精调完的模型只能用于情感分析任务,而经过Instruction-Tuning多任务精调后,可以用于其他任务的zero-shot。
      这里聊一聊自己的见解,两者的对比主要是基于大模型。Prompt是通过对任务进行一定的描述,或者给一些示例(ICL),来完成既定任务目标,但是如果不给模型示例(zero-shot),prompt表现的很一般,这怎么办呢?能不能让大模型理解任务是做什么的,这样不用示例也能完成任务目标,instruction就是来做这个任务的,它为了让模型具备理解任务的能力,采用大量的指令数据,对模型进行微调,即Instruction-Tuning。因此,instruction和prompt的不同之处在于:instruction是在prompt的基础上,进一步挖掘模型理解任务的能力。(仅供参考)

4、Chain-of-Thought(思维链)

  随着LLM的越来越大,以及tuning技术的快速发展,LLM在包括情感分析在内的传统自然语言任务上表现越来越好,但是单纯的扩大LLM模型的参数量无法让模型在算术推理/常识推理/符号推理等推理任务上取得理想的效果。 如何提升LLM在这些推理任务上性能呢?在此前关于LLM的推理任务中,有两种方法:

  • 针对下游任务对模型进行微调;
  • 为模型提供少量的输入输出样例进行学习。

但是这两种方法都有着局限性,前者微调计算成本太高,后者采用传统的输入输出样例在推理任务上效果很差,而且不会随着语言模型规模的增加而有实质性的改善。此时,Chain-of-Thought应运而生。下面我们根据三篇比较有代表性的论文,详细介绍CoT的发展历程。

4.1、Manual-CoT(人工思维链)

  Manual-CoT是Chain-of-Thought技术的开山之作,由Google在2022年初提出《Chain-of-Thought Prompting Elicits Reasoning in Large Language Models》。其旨在进一步提高超大规模模型在一些复杂任务上的推理能力。其认为现有的超大规模语言模型可能存在下面潜在的问题:

  • 增大模型参数规模对于一些具有挑战的任务(例如算术、常识推理和符号推理)的效果并未证明有效;
  • 期望探索如何对大模型进行推理的简单方法。

  针对这些问题,作者提出了chain of thought (CoT)这种方法来利用大语言模型求解推理任务。
  下面这个例子可以很好的说明思维链到底在做什么。左图是传统的one-shot prompting,就是拼接一个例子在query的前面。右图则是CoT的改进,就是将example中的Answer部分的一系列的推理步骤(人工构建)写出来后,再给出最终答案。逻辑就是希望模型学会一步一步的输出推理步骤,然后给出结果。
CoT
  论文中首先在算数推理(arithmetic reasoning)领域做了实验,使用了5个数学算术推理数据集:GSM8K / SVAMP / ASDiv / AQuA / MAWPS,具体的实验过程这里不再赘述,感兴趣的同学可以直接参考论文,这里直接给出实验结论(如下图):
在这里插入图片描述

  • CoT对小模型作用不大:模型参数至少达到10B才有效果,达到100B效果才明显。并且作者发现,在较小规模的模型中产生了流畅但不符合逻辑的 CoT,导致了比Standard prompt更低的表现;
  • CoT对复杂的问题的性能增益更大:例如,对于GSM8K(baseline 性能最低的数据集),最大的GPT (175B GPT)和PaLM (540B PaLM)模型的性能提高了一倍以上。而对于SingleOp(MAWPS中最简单的子集,只需要一个步骤就可以解决),性能的提高要么是负数,要么是非常小;
  • CoT超越SOTA:在175B的GPT和540B的PaLM模型下,CoT在部分数据集上超越了之前的SOTA(之前的SOTA 采用的是在特定任务下对模型进行微调的模式)。

  除此之外,论文中为了证明CoT的有效性,相继做了消融实验(Ablation Study)、鲁棒性实验( Robustness of Chain of Thought)、常识推理(Commonsense Reasoning)实验、符号推理(Symbolic Reasoning)实验,下面分别做以简单介绍:

  • 消融实验:我们知道,消融实验是通过研究移除某个组件之后的性能,证明该组件的有效性。论文中通过引入CoT的三个变种,证明CoT的有效性,结果如下图所示:
    在这里插入图片描述

    • Equation only:把CoT中的文字去掉,只保留公式部分。结论:效果对于原始prompt略有提升,对简单任务提升较多,但和CoT没法比,特别是对于复杂任务,几乎没有提升。
    • Variable compute only:把CoT中的token全换成点(…)。 这是为了验证额外的计算量是否是影响模型性能的因素。结论:全换成点(…)后效果和原始prompt没什么区别,这说明计算量用的多了对结果影响很小(几乎没有影响),也说明了人工构建的CoT(token sequence)对结果影响很大。
    • Chain of thought after answer:把思维链放到生成结果之后。 这样做的原因是:猜测CoT奏效的原因可能仅仅是这些CoT简单的让模型更好的访问了预训练期间获得的相关知识,而与推理没啥太大关系。结论:CoT放到生成的答案之后的效果和benchmark没太大区别,说明CoT的顺序逻辑推理还是起到了很大作用的(不仅仅是激活知识),换句话说,模型确实是依赖于生成的思维链一步一步得到的最终结果。
  • 鲁棒性实验:论文中通过annotators(标注者),exemplars(样例选择)和models(模型)三个方面对CoT进行了鲁棒性分析。如下图所示,总体结论是思维链普遍有效,但是不同的CoT构建方式/exemplars的选择/exemplars的数量/exemplars的顺序,在一定程度上影响着CoT的效果。在这里插入图片描述

    • 不同人构建CoT:尽管每个人构建的CoT都不相同,但都对模型性能产生了正面的影响,说明CoT确实有效。但是另一方面,不同人给出的不同的CoT对最终结果的影响程度还是有很大不同的,说明如何更好的构建CoT是一个研究方向;
    • Exemplars样本的选择:不同的选择都会有提升,但是差异明显。特别是,在一个数据集上选择的exemplars可以用在其他数据集上,比如论文中的实验设置,对于同一种类型的问题,如算术推理,尽管在多个不同的数据集进行实验,但使用的是8个相同的exemplars,结果没有特别大的差异,说明exemplars不需要满足和test set有相同的分布;
    • Exemplars样本的顺序:整体影响不大,除了coin flip task,可能的原因是:同一个类别的多个exemplars连续输入模型使其输出产生了偏差(bias),例如把4个负样本放到4个正样本的后面输入到模型中,可能导致模型更加倾向于输出负label;
    • Exemplars样本的数量:对于标准prompt,增加exemplars的数量对最终结果的影响不大。对于CoT,增加exemplars对模型有影响(在某些数据集上),同时也不是越大越好;
    • 不同LLM上的效果: 对于一个LLM效果好的CoT exemplars set换到其他LLM上效果不一定好,也就是说CoT对模型的提升是无法在不同的LLM上传递的,这是一个局限。
       

      关于鲁棒性实验,论文中最后指出:Prompt Engineering仍然很重要,不同的prompt(CoT)的设计/数量/顺序都会对模型产生不同的影响,且方差还是很大的。 因此未来的一个方向可能是探索一种能够获取稳健CoT(Prompts)的范式。 或许可以用一个LLM自动生成CoT用于Prompting,后面我们将介绍这种技术:Auto-CoT。

  • 常识推理实验 & 符号推理实验:此处我们不做过多介绍,这里给出三种推理模式的exemplars示例(绿色:算数推理,橙色:常识推理,蓝色:符号推理),供大家参考:
    在这里插入图片描述

  这篇CoT开山之作首次提出思维链(CoT)的概念,思维链简单的说就是一系列中间推理步骤。这篇论文最大的贡献就是发现了在LLM生成推理任务的结果之前,先生成思维链,会使模型的推理性能有大幅度的提升,特别是在复杂的推理任务上,但是有个前提就是LLM的规模要大于10B,否则CoT没用甚至起副作用。CoT的一大好处是无需微调模型参数,仅仅是改变输入就可以改进模型的性能。随着LLM越来越大,高校和小企业可能无法承担训练LLM的成本,因此无法参与其中进行科研与实践,但CoT这个研究方向仍然可以做。对于CoT的更多细节,大家可参考《Chain-of-Thought Prompting Elicits Reasoning in Large Language Models》思维链(Chain-of-Thought, CoT)的开山之作

4.2、Zero-shot-CoT(零示例思维链)

  2022年6月东京大学和谷歌共同发表了一篇论文《Large Language Models are Zero-Shot Reasoners》,这是一篇关于预训练大型语言模型(Pretrained Large Language Models, LLMs)推理能力的探究论文。目前,LLMs被广泛运用在很多NLP任务上。同时,在提供了特定任务的示例之后,LLMs是一个非常优秀的学习者。随着思考链的提示方式(chain of thought prompting, CoT)被提出,对LLMs推理能力的探究上升到一个新的高度,这种提示方式可以引导模型通过示例中一步一步的推理方式,去解决复杂的多步推理,在数学推理(arithmetic reasoning)和符号推理(symbolic reasoning)中取得了SOTA的成果。作者在研究中发现,对拥有175B参数的GPT-3,通过简单的添加”Let’s think step by step“,可以提升模型的zero-shot能力。Zero-shot-CoT的具体格式如下图所示,论文中的具体细节这里不做过多赘述,感兴趣的同学可详读论文内容。需要注意一点的是,同等条件下,Zero-shot-CoT的性能是不及Manual-CoT的。
在这里插入图片描述

4.3、Auto-CoT(自动思维链)

  前文已经提到过,传统CoT的一个未来研究方向:可以用一个LLM自动生成CoT用于Prompting,李沐老师团队在2022年10月发表的论文《AUTOMATIC CHAIN OF THOUGHT PROMPTING IN LARGE LANGUAGE MODELS》证明了这一技术方向的有效性,称为Auto-CoT
  目前较为流行的CoT方法有两种,一种是Manual-CoT,一种是Zero-shot-CoT,两种方式的输入格式如下图所示。前文我们提到过,Manual-CoT的性能是要优于Zero-shot-CoT的,关键原因在于Manual-CoT包含一些人工设计的问题推理步骤答案,但是这部分要花费一定的人工成本,而Auto-CoT则解决了这一痛点,具体做法是:
在这里插入图片描述

  • 通过多样性选取有代表性的问题;
  • 对于每一个采样的问题拼接上“Let’s think step by step”(类似于 Zero-shot-CoT )输入到语言模型,让语言模型生成中间推理步骤和答案,然后把这些所有采样的问题以及语言模型生成的中间推理步骤和答案全部拼接在一起,构成少样本学习的样例,最后再拼接上需要求解的问题一起输入到语言模型中进行续写,最终模型续写出了中间的推理步骤以及答案。

  总体来说,Auto-CoT是Manual-CoT和Zero-shot-CoT的结合体,如下图所示。实验证明,在十个数据集上Auto-CoT是可以匹配甚至超越Manual-CoT的性能,也就说明自动构造的CoT的问题中间推理步骤答案样例比人工设计的还要好,而且还节省了人工成本。在这里插入图片描述
  至此,我们详细介绍了三种CoT技术:Manual-CoT、Zero-shot-CoT以及Auto-CoT,有关CoT的技术还有很多,需要我们慢慢学习,后续持续更新。

5、Parameter-Efficient Fine-Tuning (PEFT,参数有效性微调)

  通过前文的介绍,我们可以把Tuning分为两类:

  • 全参数微调:训练过程中更新包括模型在内的所有参数,例如Fine-Tuning、Prompt-Orient Fine-Tuning等;
  • 部分参数微调:训练过程中只更新部分模型参数,或者固定模型参数,只更新少量额外添加的参数,如Parameter-Efficient Prompt Tuning、P-Tuning等。

  我们知道,部分参数微调模式的提出,一方面是由于资源限制,无法更新整体大模型参数,另一方面,要保证在资源有限的条件下,能够尽可能的提升大模型在下游任务上的效果。目前,针对部分参数微调的研究,正处于蓬勃发展阶段,这个研究领域有个统一的名称:Parameter-Efficient Fine-Tuning (PEFT),即参数有效性微调,PEFT方法仅微调少量或额外的模型参数,固定大部分预训练参数,大大降低了计算和存储成本,同时最先进的 PEFT 技术也能实现了与全量微调相当的性能。前文提到的Prompt-Tuning,包括P-Tuning等,都可以视为PEFT的一种。总体来说,参数有效性微调可分为三个类别:

  • Prompt-Tuning:在模型的输入或隐层添加个额外可训练的前缀 tokens(这些前缀是连续的伪tokens,不对应真实的tokens),只训练这些前缀参数,包括prefix-tuning、parameter-efficient Prompt Tuning、P-Tuning等;
  • Adapter-Tuning:将较小的神经网络层或模块插入预训练模型的每一层,这些新插入的神经模块称为adapter(适配器),下游任务微调时也只训练这些适配器参数;
  • LoRA:通过学习小参数的低秩矩阵来近似模型权重矩阵的参数更新,训练时只优化低秩矩阵参数。

  接下来,我们对其中流行的PEFT算法进行详细介绍。

5.1、PEFT介绍

  • Prefix-Tuning:Prefix-Tuning也是一种Prompt-Tuning,是最早提出soft-prompt的论文之一《Prefix-Tuning: Optimizing Continuous Prompts for Generation》,斯坦福大学于2021年发表。Prefix-Tuning在模型输入前添加一个连续的且任务特定的向量序列(continuous task-specific vectors),称之为前缀(prefix)。前缀同样是一系列“虚拟 tokens”,即没有真实语义。与更新所有 PLM 参数的全量微调不同,Prefix-Tuning固定PLM的所有参数,只更新优化特定任务的prefix。Prefix-Tuning与传统Fine-Tuning的对比图如下所示:
    在这里插入图片描述
      如下图所示,Prefix-Tuning有两种模式,一种是自回归模型(例如GPT-2),在输入前添加一个前缀得到 [ P R E F I X ; x ; y ] [PREFIX;x;y] [PREFIX;x;y];另一种是encoder-decoder模型(例如Bart),在编码器和解码器前加前缀得到 [ P R E F I X ; x ; P R E F I X ′ ; y ] [PREFIX;x;PREFIX^{'};y] [PREFIX;x;PREFIX;y]。接下来我们以GPT-2的自回归语言模型为例,介绍下Prefix-Tuning的流程。
      首先,对于传统的GPT-2模型来说,将输入 x x x和输出 y y y拼接为 z = [ x ; y ] z=[x;y] z=[x;y],其中 X i d x X_{idx} Xidx Y i d x Y_{idx} Yidx分别为输入和输出序列的索引, h i ∈ R d h_{i} \in R^{d} hiRd是每个时间步 i i i下的激活向量(隐藏层向量), h i = [ h i ( 1 ) ; … … ; h i ( n ) ] h_{i}=[h_{i}^{(1)}; ……;h_{i}^{(n)}] hi=[hi(1);……;hi(n)]表示在当前时间步的所有激活层的拼接, h i ( j ) h_{i}^{(j)} hi(j)是时间步 i i i的第 j j j层激活层。自回归模型通过如下公式计算 h i h_{i} hi,其中 ϕ \phi ϕ是模型参数:
    h i = L M ϕ ( z i , h < i )   h_{i} =LM_{\phi}(z_{i},h_{<i})\ hi=LMϕ(zi,h<i) 
    h i h_{i} hi的最后一层,用来计算下一个token的概率分布:
    p ϕ ( z i + 1 ∣ h ≤ i ) = s o f t m a x ( W ϕ h i ( n ) )   p_{\phi}(z_{i+1}|h_{≤i}) =softmax(W_{\phi}h_{i}^{(n)})\ pϕ(zi+1hi)=softmax(Wϕhi(n)) 
    其中 W ϕ W_{\phi} Wϕ是将 h i ( n ) h_{i}^{(n)} hi(n)根据词表大小进行映射。
      在采用Prefix-Tuning技术后,则在输入前添加前缀,即将prefix和输入以及输出进行拼接得到 z = [ P R E F I X ; x ; y ] z=[PREFIX;x;y] z=[PREFIX;x;y] P i d x P_{idx} Pidx为前缀序列的索引, ∣ P i d x ∣ |P_{idx}| Pidx为前缀序列的长度,这里需要注意的是,Prefix-Tuning是在模型的每一层都添加prefix(注意不是只有输入层,中间层也会添加prefix,目的增加可训练参数)。前缀序列索引对应着由 θ \theta θ参数化的向量矩阵 P θ P_{\theta} Pθ,维度为 ∣ P i d x ∣ × d i m ( h i ) |P_{idx}|\times dim(h_{i}) Pidx×dim(hi)。隐层表示的计算如下式所示,若索引为前缀索引 P i d x P_{idx} Pidx,直接从 P θ P_{\theta} Pθ复制对应的向量作为 h i h_{i} hi(在模型每一层都添加前缀向量);否则直接通过LM计算得到,同时,经过LM计算的 h i h_{i} hi也依赖于其左侧的前缀参数 P θ P_{\theta} Pθ,即通过前缀来影响后续的序列激活向量值(隐层向量值)。
    h i = { P θ [ i , : ] if    i ∈ P i d x L M ϕ ( z i , h < i ) otherwise h_{i}= \begin{cases} P_{\theta}[i,:]& \text{if} \ \ \ i\in P_{idx}\\ LM_{\phi}(z_{i},h_{<i})& \text{otherwise} \end{cases} hi={Pθ[i,:]LMϕ(zi,h<i)if   iPidxotherwise
      在训练时,Prefix-Tuning的优化目标与正常微调相同,但只需要更新前缀向量的参数。在论文中,作者发现直接更新前缀向量的参数会导致训练的不稳定与结果的略微下降,因此采用了重参数化的方法,通过一个更小的矩阵 P θ ′ P_{\theta}^{'} Pθ和一个大型前馈神经网络 MLP θ \text{MLP}_{\theta} MLPθ P θ P_{\theta} Pθ进行重参数化: P θ [ i , : ] = MLP θ ( P θ ′ [ i , : ] ) P_{\theta}[i,:]=\text{MLP}_{\theta}(P_{\theta}^{'}[i,:]) Pθ[i,:]=MLPθ(Pθ[i,:]),可训练参数包括 P θ ′ P_{\theta}^{'} Pθ MLP θ \text{MLP}_{\theta} MLPθ的参数,其中, P θ P_{\theta} Pθ P θ ′ P_{\theta}^{'} Pθ有相同的行维度(也就是相同的prefix length), 但不同的列维度。在训练时,LM 的参数 ϕ \phi ϕ被固定,只有前缀参数 P θ ′ P_{\theta}^{'} Pθ MLP θ \text{MLP}_{\theta} MLPθ的参数为可训练的参数。训练完成后, P θ ′ P_{\theta}^{'} Pθ MLP θ \text{MLP}_{\theta} MLPθ的参数被丢掉,只有前缀参数 P θ P_{\theta} Pθ被保存。
    在这里插入图片描述
      上述内容详细介绍了Prefix-Tuning的主要训练流程,下面我们给出论文中通过实验得出的三个主要结论:

    • 方法有效性:作者采用了Table-To-Text与Summarization作为实验任务,在Table-To-Text任务上,Prefix-Tuning在优化相同参数的情况下结果大幅优于Adapter,并与全参数微调几乎相同。而在Summarization任务上,Prefix-Tuning方法在使用2%参数与0.1%参数时略微差于全参数微调,但仍优于Adapter微调;
    • Full vs Embedding-only:Embedding-only方法只在embedding层添加前缀向量并优化,而Full代表的Prefix-Tuning不仅在embedding层添加前缀参数,还在模型所有层添加前缀并优化。实验得到一个不同方法的表达能力增强链条:discrete prompting < embedding-only < Prefix-Tuning。同时,Prefix-Tuning可以直接修改模型更深层的表示,避免了跨越网络深度的长计算路径问题;
    • Prefix-Tuning vs Infix-Tuning:通过将可训练的参数放置在 x x x y y y的中间来研究可训练参数位置对性能的影响,即 [ x ; I n f i x ; y ] [x;Infix;y] [x;Infix;y],这种方式成为infix-tuning。实验表明Prefix-Tuning性能好于 infix-tuning,因为prefix能够同时影响 x x x y y y的隐层向量,而infix只能够影响 y y y的隐层向量。
       

      我们回顾下前文提到的parameter-efficient prompt tuning(下面简称为Prompt Tuning),其论文中有提到,它可以看作是Prefix-Tuning的简化版。总结下两者的不同点:

    • 参数更新策略不同:Prompt Tuning只对输入层(Embedding)进行微调,而Prefix-Tuning是对每一层全部进行微调。因此parameter-efficient prompt tuning的微调参数量级要更小(如下图),且不需要修改原始模型结构;
    • 参数生成方式不同:Prompt Tuning与Prefix-Tuning及P-Tuning不同的是,没有采用任何的prompt映射层(即Prefix-Tuning中的重参数化层与P-Tuning中的prompt encoder),而是直接对prompt token对应的embedding进行了训练;
    • 面向任务不同:Pompt Tuning、P-Tuning以及后面要介绍的P-Tuning v2都是面向的NLU任务进行效果优化及评测的,而Prefix-Tuning针对的则是NLG任务。

在这里插入图片描述

  • P-Tuning v2:P-Tuning v2是2022年发表的一篇论文《P-Tuning v2: Prompt Tuning Can Be Comparable to Fine-tuning Universally Across Scales and Tasks》,总结来说是在Prefix-Tuning和P-Tuning的基础上进行的优化。下面我们简单介绍下P-Tuning v2方法。
    • P-Tuning v2针对Prefix-Tuning、P-Tuning解决的问题

      • Prefix-Tuning是针对于生成任务而言的,不能处理困难的序列标注任务、抽取式问答等,缺乏普遍性;
      • 当模型规模较小,特别是小于100亿个参数时,它们仍然不如Fine-Tuning。
    • P-Tuning v2的优点

      • P-Tuning v2在不同的模型规模(从300M到100B的参数)和各种困难的NLU任务(如问答和序列标注)上的表现与Fine-Tuning相匹配;
      • 与Fine-Tuning相比,P-Tuning v2每个任务的可训练参数为0.1%到3%,这大大降低了训练时间的内存消耗和每个任务的存储成本。
    • P-Tuning v2的核心点

      • NLU任务优化:主要针对NLU任务进行微调,提升P-Tuning v2在NLU任务上的效果;
      • 深度提示优化:参考Prefix-Tuning,不同层分别将prompt作为前缀token加入到输入序列中,彼此相互独立(注意,这部分token的向量表征是互不相同的,即同Prefix-Tuning一致,不是参数共享模式),如下图所示。通过这种方式,一方面,P-Tuning v2有更多的可优化的特定任务参数(从0.01%到0.1%-3%),以保证对特定任务有更多的参数容量,但仍然比进行完整的Fine-Tuning任务参数量小得多;另一方面,添加到更深层的提示,可以对输出预测产生更直接的影响。
        在这里插入图片描述
    • P-Tuning v2的其他优化及实施点

      • 重参数化:以前的方法利用重参数化功能来提高训练速度、鲁棒性和性能(例如,MLP的Prefix-Tuning和LSTM的P-Tuning)。然而,对于NLU任务,论文中表明这种技术的好处取决于任务和数据集。对于一些数据集(如RTE和CoNLL04),MLP的重新参数化带来了比嵌入更稳定的改善;对于其他的数据集,重参数化可能没有显示出任何效果(如BoolQ),有时甚至更糟(如CoNLL12)。需根据不同情况去决定是否使用;
      • 提示长度:提示长度在提示优化方法的超参数搜索中起着核心作用。论文中表明不同的理解任务通常用不同的提示长度来实现其最佳性能,比如一些简单的task倾向比较短的prompt(less than 20),而一些比较难的序列标注任务,长度需求比较大;
      • 多任务学习:多任务学习对P-Tuning v2方法来说是可选的,但可能是有帮助的。在对特定任务进行微调之前,用共享的prompts去进行多任务预训练,可以让prompts有比较好的初始化;
      • 分类方式选择:对标签分类任务,用原始的CLS+linear head模式替换Prompt-Tuning范式中使用的Verbalizer+LM head模式,不过效果并不明显,如下图。在这里插入图片描述
  • Adapter-Tuning《Parameter-Efficient Transfer Learning for NLP》这项2019年的工作第一次提出了Adapter方法。与Prefix-Tuning和Prompt Tuning这类在输入前添加可训练prompt embedding参数来以少量参数适配下游任务的方式不通,Adapter-Tuning 则是在预训练模型内部的网络层之间添加新的网络层或模块来适配下游任务。假设预训练模型函数表示为 ϕ w ( x ) \phi_{w}(x) ϕw(x),对于Adapter-Tuning,添加适配器之后模型函数更新为: ϕ w , w 0 ( x ) \phi_{w,w_{0}}(x) ϕw,w0(x) w w w是预训练模型的参数, w 0 w_{0} w0是新添加的适配器的参数,在训练过程中, w w w被固定,只有 w 0 w_{0} w0被更新。 ∣ w 0 ∣ ≪ ∣ w ∣ |w_{0}|\ll|w| w0w,这使得不同下游任务只需要添加少量可训练的参数即可,节省计算和存储开销,同时共享大规模预训练模型。在对预训练模型进行微调时,我们可以冻结在保留原模型参数的情况下对已有结构添加一些额外参数,对该部分参数进行训练从而达到微调的效果。
      论文中采用Bert作为实验模型,Adapter模块被添加到每个transformer层两次。适配器是一个 bottleneck(瓶颈)结构的模块,由一个两层的前馈神经网络(由向下投影矩阵、非线性函数和向上投影矩阵构成)和一个输入输出之间的残差连接组成。其总体结构如下(跟论文中的结构有些出入,目前没有理解论文中的结构是怎么构建出来的,个人觉得下图更准确的刻画了adapter的结构,有不同见解可在评论区沟通):在这里插入图片描述
      Adapter结构有两个特点:较少的参数、在初始化时与原结构相似的输出。在实际微调时,由于采用了down-project与up-project的架构,在进行微调时,Adapter会先将特征输入通过down-project映射到较低维度,再通过up-project映射回高维度,从而减少参数量。Adapter-Tuning只需要训练原模型0.5%-8%的参数量,若对于不同的下游任务进行微调,只需要对不同的任务保留少量Adapter结构的参数即可。由于Adapter中存在残差连接结构,采用合适的小参数去初始化Adapter就可以使其几乎保持原有的输出,使得模型在添加额外结构的情况下仍然能在训练的初始阶段表现良好。在GLUE测试集上,Adapter用了更少量的参数达到了与传统Fine-Tuning方法接近的效果。
  • LoRA:LoRA是又一种PEFT方法,微软于2022年发表《LORA: LOW-RANK ADAPTATION OF LARGE LANGUAGE MODELS》。我们依照下图以及论文,简单介绍下LoRA的实现原理。在这里插入图片描述
      LoRA原理其实并不复杂。简单理解一下,就是在模型的Linear层的旁边,增加一个“旁支”,这个“旁支”的作用,就是代替原有的参数矩阵 W W W进行训练。结合上图,我们来直观地理解一下这个过程,输入 x ∈ R d x\in R^{d} xRd,举个例子,在普通的transformer模型中,这个 x x x可能是embedding的输出,也有可能是上一层transformer layer的输出,而 d d d一般就是768或者1024。按照原本的路线,它应该只走左边的部分,也就是原有的模型部分。
      而在LoRA的策略下,增加了右侧的“旁支”,也就是先用一个Linear层 A A A,将数据从 d d d维降到 r r r,这个 r r r也就是LoRA的秩,是LoRA中最重要的一个超参数。一般会远远小于 d d d,尤其是对于现在的大模型, d d d已经不止是768或者1024,例如LLaMA-7B,每一层transformer有32个head,这样一来 d d d就达到了4096。接着再用第二个Linear层 B B B,将数据从 r r r变回 d d d维。最后再将左右两部分的结果相加融合,就得到了输出的hidden_state。
      对于左右两个部分,右侧看起来像是左侧原有矩阵 W W W的分解,将参数量从 d × d d\times d d×d变成了 d × r + d × r d\times r +d\times r d×r+d×r,在 r ≪ d r\ll d rd的情况下,参数量就大大地降低了。熟悉各类预训练模型的同学可能会发现,这个思想其实与Albert的思想有异曲同工之处,在Albert中,作者通过两个策略降低了训练的参数量,其一是Embedding矩阵分解,其二是跨层参数共享。在Albert中,作者考虑到词表的维度很大,所以将Embedding矩阵分解成两个相对较小的矩阵,用来模拟Embedding矩阵的效果,这样一来需要训练的参数量就减少了很多。
      LoRA也是类似的思想,并且它不再局限于Embedding层,而是所有出现大矩阵的地方,理论上都可以用到这样的分解。但是与Albert不同的是,Albert直接用两个小矩阵替换了原来的大矩阵,而LoRA保留了原来的矩阵 W W W,但是不让 W W W参与训练(Fine-Tuning是更新权重矩阵 W W W,LoRA中的 W = W 0 + B A W=W_{0}+BA W=W0+BA,但是 W 0 W_{0} W0不参与更新,只更新 A A A B B B),所以需要计算梯度的部分就只剩下旁支的 A A A B B B两个小矩阵。用随机高斯分布初始化A,用0矩阵初始化B,保证训练的开始此旁路矩阵是0矩阵,使得模型保留原有知识,在训练的初始阶段仍然表现良好。A矩阵不采用0初始化主要是因为如果矩阵A也用0初始化,那么矩阵B梯度就始终为0(对B求梯度,结果带有A矩阵,A矩阵全0,B的梯度结果必然是0),无法更新参数。
      从论文中的公式来看,在加入LoRA之前,模型训练的优化表示为:
    m a x Φ ∑ ( x , y ∈ Z ) ∑ t = 1 ∣ y ∣ l o g ( P Φ ( y t ∣ x , y < t ) ) max_{\Phi} \sum_{(x,y \in Z)}\sum_{t=1}^{|y|}log(P_{\Phi}(y_{t}|x,y_{<t})) maxΦ(x,yZ)t=1ylog(PΦ(ytx,y<t))
    其中,模型的参数用 Φ \Phi Φ表示。
      而加入了LoRA之后,模型的优化表示为:
    m a x Θ ∑ ( x , y ∈ Z ) ∑ t = 1 ∣ y ∣ l o g ( P Φ 0 + Δ Φ ( Θ ) ( y t ∣ x , y < t ) ) max_{\Theta} \sum_{(x,y \in Z)}\sum_{t=1}^{|y|}log(P_{\Phi_{0}+\Delta\Phi(\Theta)}(y_{t}|x,y_{<t})) maxΘ(x,yZ)t=1ylog(PΦ0+ΔΦ(Θ)(ytx,y<t))
    其中,模型原有的参数是 Φ 0 \Phi_{0} Φ0,LoRA新增的参数是 Δ Φ ( Θ ) \Delta\Phi(\Theta) ΔΦ(Θ)
      从第二个式子可以看到,尽管参数看起来增加了 Δ Φ ( Θ ) \Delta\Phi(\Theta) ΔΦ(Θ),但是从前面的max的目标来看,需要优化的参数只有 Θ \Theta Θ,而根 ∣ Θ ∣ ≪ ∣ Φ 0 ∣ |\Theta|\ll |\Phi_{0}| ∣Θ∣Φ0,这就使得训练过程中,梯度计算量少了很多,所以就在低资源的情况下,我们可以只消耗 Θ \Theta Θ这部分的资源,这样一来就可以在单卡低显存的情况下训练大模型了。这里再多说一点,通常在实际使用中,一般LoRA作用的矩阵是注意力机制部分的 W Q W_{Q} WQ W K W_{K} WK W V W_{V} WV矩阵(即与输入相乘获取 Q Q Q K K K V V V的权重矩阵。这三个权重矩阵的数量正常来说,分别和heads的数量相等,但在实际计算过程中,是将多个头的这三个权重矩阵分别进行了合并,因此每一个transformer层都只有一个 W Q W_{Q} WQ W K W_{K} WK W V W_{V} WV矩阵)。下面介绍下LoRA架构的优点:
    • 全量微调的一般化:LoRA 不要求权重矩阵的累积梯度更新在适配过程中具有满秩。当对所有权重矩阵应用 LoRA 并训练所有偏差时,将 LoRA 的秩 r r r设置为预训练权重矩阵的秩,就能大致恢复了全量微调的表现力。也就是说,随着增加可训练参数的数量,训练 LoRA 大致收敛于训练原始模型;
    • 没有额外的推理延时:在生产部署时,可以明确地计算和存储 W = W 0 + B A W=W_{0}+BA W=W0+BA,并正常执行推理。当需要切换到另一个下游任务时,可以通过减去 B A BA BA来恢复 W 0 W_{0} W0,然后增加一个不同的 B ′ A ′ B^{'}A^{'} BA,这是一个只需要很少内存开销的快速运算。最重要的是,与Fine-Tuning的模型相比,LoRA 推理过程中没有引入任何额外的延迟(将 B A BA BA加到原参数 W 0 W_{0} W0上后,计算量是一致的);
    • 减少内存和存储资源消耗:对于用Adam训练的大型Transformer,若 r ≪ d m o d e l r\ll d_{model} rdmodel,LoRA 减少2/3的显存用量(训练模型时,模型参数往往都会存储在显存中),因为不需要存储已固定的预训练参数的优化器状态,可以用更少的GPU进行大模型训练。在175B的GPT-3上,训练期间的显存消耗从1.2TB减少到350GB。在有且只有query和value矩阵被调整的情况下,checkpoint的大小大约减少了10000倍(从350GB到35MB)。另一个好处是,可以在部署时以更低的成本切换任务,只需更换 LoRA 的权重,而不是所有的参数。可以创建许多定制的模型,这些模型可以在将预训练模型的权重存储在显存中的机器上进行实时切换。在175B的GPT-3上训练时,与完全微调相比,速度提高了25%,因为我们不需要为绝大多数的参数计算梯度;
    • 更长的输入:相较P-Tuning等soft-prompt方法,LoRA最明显的优势,就是不会占用输入token的长度。
  • AdaLoRA:AdaLoRA是发表于2023年3月《ADAPTIVE BUDGET ALLOCATION FOR PARAMETEREFFICIENT FINE-TUNING》,论文并未仔细阅读,简单来说,论文中发现对不同类型权重矩阵或者不同层的权重矩阵应用LoRA方法,产生的效果是不同的,如下图所示。在这里插入图片描述
      在参数预算有限的情况下(例如限定模型可微调参数的数量),如何智能的选取更重要的参数进行更新,显得尤为重要。论文中提出的解决办法,是先对LoRA对应的权重矩阵进行SVD分解,即:
    W = W 0 + Δ = W 0 + B A = W 0 + P Λ Q   W=W_{0}+\Delta=W_{0}+BA=W_{0}+P\Lambda Q\ W=W0+Δ=W0+BA=W0+PΛQ 
    其中: Δ \Delta Δ称为增量矩阵, W ∈ R d 1 × d 2 W\in R^{d1 \times d2} WRd1×d2 P ∈ R d 1 × r P\in R^{d1 \times r} PRd1×r Q ∈ R r × d 2 Q\in R^{r \times d2} QRr×d2 Λ ∈ R r × r \Lambda\in R^{r \times r} ΛRr×r r ≪ m i n ( d 1 , d 2 ) r\ll min(d1,d2) rmin(d1,d2)。再根据重要性指标动态地调整每个增量矩阵中奇异值的大小。这样可以使得在微调过程中只更新那些对模型性能贡献较大或必要的参数,从而提高了模型性能和参数效率。具体可参考论文简介ADAPTIVE BUDGET ALLOCATION FOR PARAMETER- EFFICIENT FINE-TUNING
  • BitFit:BitFit(Bias-term Fine-tuning)发表于2022年BitFit: Simple Parameter-efficient Fine-tuning for Transformer-based Masked Language-models的思想更简单,其不需要对预训练模型做任何改动,只需要指定神经网络中的偏置(Bias)为可训练参数即可,BitFit的参数量只有不到2%,但是实验效果可以接近全量参数。

5.2、PEFT实践

  实验环境:2张A30卡(单卡显存24G),CentOS7。
  显存占用:如下表。

模型方案训练方案显存占用
ChatGLM-6B+P-Tuning v2单卡训练8G左右
ChatGLM2-6B+P-Tuning v2单卡训练8G左右
ChatGLM-6B+LoRA两卡DDP单卡13G左右
ChatGLM2-6B+LoRA两卡DDP单卡13G左右
ChatGLM-6B+LoRA+int8量化两卡流水线并行两卡13G左右
ChatGLM2-6B+LoRA+int8量化两卡流水线并行两卡27G左右
ChatGLM-6B+LoRA两卡Deepspeed单卡11G左右
  • ChatGLM-6B微调实践

    • ChatGLM-6B + P-Tuning v2 ⇒ \Rightarrow 官方任务实践【官方教程】ChatGLM-6B 微调

      • 模型下载:下载ChatGLM-6B模型的方法很多,这里介绍官方给出的最快下载方式。
        • 下载模型实现: 由于下载整体模型较慢,所以我们先下载模型实现,再手动下载模型参数文件。下载模型实现前,需先安装Git LFS,安装好之后再下载模型实现。

          GIT_LFS_SKIP_SMUDGE=1 git clone https://huggingface.co/THUDM/chatglm-6b
          
        • 手动下载模型参数文件

          • 脚本方式(推荐)

            git clone git@github.com:chenyifanthu/THU-Cloud-Downloader.git
            
            cd THU-Cloud-Downloader
            
            pip install argparse requests tqdm
            
            python main.py --link https://cloud.tsinghua.edu.cn/d/fb9f16d6dc8f482596c2/ --save ../chatglm-6b
            
          • 直接下载:从ChatGLM-6B中将所有文件下载下来,替换模型实现步骤下载的文件夹./chatglm-6b中的文件。

          • 百度网盘下载:为了防止官方微调模型,导致模型与训练代码不适配,在百度网盘保存了一份模型参数文件,优先级较低,大家按需提取。链接: ChatGLM-6B,提取码: 0314。

        • 下载训练代码ChatGLM-6B

          git clone git@github.com:THUDM/ChatGLM-6B.git
          

          同上文模型下载一致,官网代码存在更新的可能,若想顺利运行本项目,可从百度网盘下载代码。链接:ChatGLM-6B, 提取码: 0314。

        • 试用原始模型

          • 安装包
            pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
            
            # 具体安装包
            protobuf
            transformers==4.27.1
            cpm_kernels
            torch>=1.10
            gradio
            mdtex2html
            sentencepiece
            accelerate
            
          • 模型试用:进行简单试用的启动命令,不使用量化,单卡显存13G左右,使用8bit量化,单卡显存8G左右。
            CUDA_VISIBLE_DEVICES=1 python cli_demo.py
            
          • 注意
            • 模型路径:因为前文中,我们已经下载了chatglm-6B模型,因此使用原始模型进行试用时,需要修改模型下载路径,即将cli_demo.pyweb_demo.py中的tokenizermodel加载路径,THUDM/chatglm-6b修改为本地路径。后面包括训练在内的所有过程,都要注意这一点,就不重复赘述。在这里插入图片描述
        • 量化细节:如上图所示,量化的处理方式也进行了标记。量化操作一般用于推理,加快推理速度,训练过程一般不采用此操作。同时,量化操作是作用于部分参数,将这部分参数转换为8位整数表示,同时将requires_grad属性置为False

        • 训练前安装包

          pip install rouge_chinese nltk jieba datasets
          
        • 数据集下载Tsinghua Cloud。下载至目录./ptuning,ADGEN数据集任务为根据输入(content)生成一段广告词(summary)。

          {
              "content": "类型#上衣*版型#宽松*版型#显瘦*图案#线条*衣样式#衬衫*衣袖型#泡泡袖*衣款式#抽绳",
              "summary": "这件衬衫的款式非常的宽松,利落的线条可以很好的隐藏身材上的小缺点,穿在身上有着很好的显瘦效果。领口装饰了一个可爱的抽绳,漂亮的绳结展现出了十足的个性,配合时尚的泡泡袖型,尽显女性甜美可爱的气息。"
          }
          
        • 启动训练

          cd ./ptuning
          sh train.sh
          
        • 模型推理

          #!/usr/bin/env python3
          # -*- coding: UTF-8 -*-
          ################################################################################
          #
          # Copyright (c) 2023 Baidu.com, Inc. All Rights Reserved
          #
          ################################################################################
          """
          File    :   predict.py
          brief   :   brief
          Date    :   2023/07/03 08:00:52
          Author  :   zhangce06
          Contact :   zhangce06@baidu.com
          """
          
          
          from transformers import AutoConfig, AutoModel, AutoTokenizer
          import torch
          import os
          import platform
          import signal
          import readline
          
          # pre_seq_len = 128
          
          # 载入Tokenizer
          tokenizer = AutoTokenizer.from_pretrained("../../chatglm-6b-model", trust_remote_code=True)
          config = AutoConfig.from_pretrained("../../chatglm-6b-model", trust_remote_code=True, pre_seq_len=128)
          # config.pre_seq_len = pre_seq_len
          model = AutoModel.from_pretrained("../../chatglm-6b-model", config=config, trust_remote_code=True)
          
          CHECKPOINT_PATH = "output/adgen-chatglm-6b-pt-128-2e-2/checkpoint-3000"
          prefix_state_dict = torch.load(os.path.join(CHECKPOINT_PATH, "pytorch_model.bin"))
          new_prefix_state_dict = {}
          for k, v in prefix_state_dict.items():
              if k.startswith("transformer.prefix_encoder."):
                  new_prefix_state_dict[k[len("transformer.prefix_encoder."):]] = v
          model.transformer.prefix_encoder.load_state_dict(new_prefix_state_dict)
          
          # 之后根据需求可以进行量化
          # Comment out the following line if you don't use quantization
          model = model.quantize(4)
          model = model.half().cuda()
          model.transformer.prefix_encoder.float()
          model = model.eval()
          
          os_name = platform.system()
          clear_command = 'cls' if os_name == 'Windows' else 'clear'
          stop_stream = False
          
          def build_prompt(history):
              prompt = "欢迎使用 ChatGLM-6B 模型,输入内容即可进行对话,clear 清空对话历史,stop 终止程序"
              for query, response in history:
                  prompt += f"\n\n用户:{query}"
                  prompt += f"\n\nChatGLM-6B:{response}"
              return prompt
          
          def signal_handler(signal, frame):
              global stop_stream
              stop_stream = True
          
          def main():
              history = []
              global stop_stream
              print("欢迎使用 ChatGLM-6B 模型,输入内容即可进行对话,clear 清空对话历史,stop 终止程序")
              while True:
                  query = input("\n用户:")
                  if query.strip() == "stop":
                      break
                  if query.strip() == "clear":
                      history = []
                      os.system(clear_command)
                      print("欢迎使用 ChatGLM-6B 模型,输入内容即可进行对话,clear 清空对话历史,stop 终止程序")
                      continue
                  count = 0
                  for response, history in model.stream_chat(tokenizer, query, history=history):
                      if stop_stream:
                          stop_stream = False
                          break
                      else:
                          count += 1
                          if count % 8 == 0:
                              os.system(clear_command)
                              print(build_prompt(history), flush=True)
                              signal.signal(signal.SIGINT, signal_handler)
                  os.system(clear_command)
                  print(build_prompt(history), flush=True)
          
          if __name__ == "__main__":
              main()
          
        • 灾难性遗忘问题:在该数据集上进行微调后,会出现灾难性遗忘的情况,在数据集有限的情况下,目前通过实践总结出下面三种做法,可在一定程度上缓解灾难性遗忘

          • 学习率调整:通过调整学习率进行解决的灾难性遗忘问题
          • 采用LoRA方法:参见「ChatGLM-6B + LoRA ⇒ \Rightarrow 真实任务实践」;
          • 采用ChatGLM2-6B:ChatGLM2-6B确实比ChatGLM-6B强。使用相同的超参数进行微调训练,ChatGLM2-6B在上述的广告数据集上微调后,确实没有出现灾难性遗忘的问题。不过仍然存在其他问题,大家自行体验。下面简单介绍下,使用ChatGLM2-6B复用ChatGLM-6B进行P-Tuning v2流程需要注意的点。
            • 模型下载:模型下载方式同ChatGLM-6B相同,先下载模型实现ChatGLM2-6B,再下载模型参数文件ChatGLM2-6B,注意这里博主是直接手动下载的,脚本下载方式没有尝试成功,大家可以试一试。
              • 百度网盘下载:同样在百度网盘保存了一份模型参数文件,优先级较低,大家按需提取。链接: ChatGLM2-6B,提取码: 0625。
            • 下载训练代码:ChatGLM2-6B官方没有微调代码,因此微调代码博主还是采用的ChatGLM-6B的代码ChatGLM-6B,下载方式不变。如果只是试用ChatGLM2-6B,则可以下载ChatGLM2-6B的官方代码ChatGLM2-6B(百度网盘下载方式,链接:ChatGLM2-6B,提取码: 0625),试用方式也同ChatGLM-6B一致。不论是微调还是试用,记得更换模型文件路径。
              • 试用细节:ChatGLM-6B试用时,可以使用半精度FP16加载模型,命令是model.half(),ChatGLM2-6B则不用,因为其本身就是半精度状态。可通过如下命令查看模型参数的精度构成,可以发现,未使用FP16加载模型前,ChatGLM-6B的模型参数精度是FP16和FP32混合的,ChatGLM2-6B则只有FP16精度的参数。
                model = AutoModel.from_pretrained("../../chatglm-6b-model", trust_remote_code=True)
                for name, param in model.named_parameters():
                	if param.requires_grad == True:
                	    print(f"{name},------------,{param.dtype}")
                
            • 安装包:ChatGLM2-6B需要适配更高版本的transformers和pytorch,才能发挥推理性能的优势。因此,试用ChatGLM2-6B时,安装包如下:
              # 具体安装包
              protobuf
              transformers==4.30.2
              cpm_kernels
              torch>=2.0
              gradio
              mdtex2html
              sentencepiece
              accelerate
              
              如果需要微调ChatGLM2-6B,则同ChatGLM-6B一致,安装如下python包:
              pip install rouge_chinese nltk jieba datasets
              
            • 数据集下载:无变化,同ChatGLM-6B一致。
            • 启动训练:基本无变化,大体流程同ChatGLM-6B一致。有两个地方需要注意,一个是脚本./ptuning/train.sh中的各种文件路径按需调整;另一个是./ptuning/main.py文件line 220左右进行如下修改:
              # 适配ChatGLM1
              # context_length = input_ids.index(tokenizer.bos_token_id)
              # mask_position = context_length - 1
              # labels = [-100] * context_length + input_ids[mask_position+1:]
              
              # 适配ChatGLM2
              context_length = len(input_ids) - len(b_ids)
              mask_position = context_length
              labels = [-100] * context_length + input_ids[mask_position:]```
              
            • 模型推理:基本无变化,同样注意修改模型文件路径。
    • ChatGLM-6B + LoRA ⇒ \Rightarrow 官方任务实践:参考代码ChatGLM_Tuning,实现了ChatGLM-6B基于LoRA的微调流程。具体代码见LLM微调实践。模型文件同样可根据前文的方法进行获取,其中官方的模型可能存在更新,如果想顺利复现训练过程,建议从网盘进行下载。

      • LoRA配置参数
        r:lora矩阵的秩,矩阵A和矩阵B相连接的宽度,r<<d,以 int 表示。较低的秩会导致较小的更新矩阵和较少的可训练参数
        
        target_modules:模型中使用LoRA更新矩阵的模块,模型中常见的是,更新注意力模块
        
        lora_alpha :LoRA缩放因子
        
        bias :指定是否应训练bias 参数。"none":均不可;"all":均可;"lora_only":只有lora部分的bias可训练
        
        lora_dropout:lora层的dropout比率
        
        task_type:模型任务类型,例如CAUSAL_LM任务
        
        • 注意
          • 参数更新:模型经过LoRA配置加载后,可更新模型参数只有LoRA部分,且参数精度被重置为FP32;
          • 量化方式load_in_8bit=Truequantize(8)区别,LoRA微调时只能用前者,由bitsandbytes库提供;P-Tuning v2可以采用后者,参考量化方式区别
      • 训练启动方式
        • 数据并行
          # 切换路径
          cd chatglm-ft-lora/
          
          # 启动训练
          CUDA_VISIBLE_DEVICES=1,2 torchrun --nproc_per_node=2 train.py --train_args_file ./conf/chatglm2_6b_lora.json --model_name_or_path ../../chatglm2-6b-model/ --data_path ./data/AdvertiseGen/train.jsonl --max_input_length 128 --max_output_length 256
          
        • 模型(流水线)并行
          # 切换路径
          cd ./chatglm-ft-lora/
          
          # 启动训练
          CUDA_VISIBLE_DEVICES=1,2 python train.py --train_args_file ./conf/chatglm_6b_lora.json --model_name_or_path ../../chatglm-6b-model/ --data_path ./data/AdvertiseGen/train.jsonl --max_input_length 128 --max_output_length 256 --int8
          
          • 注意:进行模型并行训练时,需要注意一个问题,即安装包问题。
            • 安装包问题:采用模型并行时,还需安装acceleratebitsandbytesscipytensorboardX四个安装包。
    • ChatGLM2-6B + LoRA ⇒ \Rightarrow 官方任务实践:实现了ChatGLM2-6B基于LoRA的微调流程。具体代码见LLM微调实践。模型文件同样可根据前文的方法进行获取,其中官方的模型可能存在更新,如果想顺利复现训练过程,建议从网盘进行下载。

      • LoRA配置参数:同ChatGLM-6B;
      • 训练启动方式
        • 数据并行
          # 切换路径
          cd ./chatglm2-ft-lora/
          
          # 启动训练
          CUDA_VISIBLE_DEVICES=1,2 torchrun --nproc_per_node=2 train.py --train_args_file ./conf/chatglm2_6b_lora.json --model_name_or_path ../../chatglm2-6b-model/ --data_path ./data/AdvertiseGen/train.jsonl --max_input_length 128 --max_output_length 256
          
          • 注意:使用ChatGLM2-6B进行数据并行训练时,需要注意一个问题,即并行问题。
            • 并行问题:实际运行时,如果报错如下,说明显存不够了,我当时因为另一张卡并非完全空余,就修改了并行策略,只采用了单卡训练。
              # 错误内容
              RuntimeError: CUDA error: CUBLAS_STATUS_NOT_INITIALIZED when calling `cublasCreate(handle)`
              
              # 单卡训练
              CUDA_VISIBLE_DEVICES=1 torchrun --nproc_per_node=1 train.py --train_args_file ./conf/chatglm2_6b_lora.json --model_name_or_path ../../chatglm2-6b-model/ --data_path ./data/AdvertiseGen/train.jsonl --max_input_length 128 --max_output_length 256		```
              
        • 模型(流水线)并行
          # 切换路径
          cd chatglm2-ft-lora/
          
          # 启动训练
          CUDA_VISIBLE_DEVICES=1,2 python train.py --train_args_file ./conf/chatglm2_6b_lora.json --model_name_or_path ../../chatglm2-6b-model/ --data_path ./data/AdvertiseGen/train.jsonl --max_input_length 128 --max_output_length 256 --int8
          
          • 注意:进行模型并行训练时,需要注意两个问题,即安装包问题、模型源码修改问题。
            • 安装包问题:采用模型并行时,还需安装acceleratebitsandbytesscipytensorboardX四个安装包;
            • 模型源码修改问题:采用模型并行训练时,如果报错如下found at least two devices, cuda:1 and cuda:0!,是模型源码问题。如果采用官方模型,可能这个bug已经被修复,但是如果采用的是百度网盘下载的模型,这个问题可能会出现,因此需要解决掉。解决办法可参考bug修复。具体来说,对modeling_chatglm.py文件的955行代码附近做如下修改(只修改一行,其余不变):
              # 原代码
              loss = None
              if labels is not None:
                  lm_logits = lm_logits.to(torch.float32)
              
                  # Shift so that tokens < n predict n
                  shift_logits = lm_logits[..., :-1, :].contiguous()   
                  shift_labels = labels[..., 1:].contiguous() #<<<------------------看这里
                  # Flatten the tokens
                  loss_fct = CrossEntropyLoss(ignore_index=-100)
                  loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1))
              
                  lm_logits = lm_logits.to(hidden_states.dtype)
                  loss = loss.to(hidden_states.dtype)
              
              if not return_dict:
                  output = (lm_logits,) + transformer_outputs[1:]
                  return ((loss,) + output) if loss is not None else output
              
              return CausalLMOutputWithPast(
                  loss=loss,
                  logits=lm_logits,
                  past_key_values=transformer_outputs.past_key_values,
                  hidden_states=transformer_outputs.hidden_states,
                  attentions=transformer_outputs.attentions,
              )
              
              # 修改为
              loss = None
              if labels is not None:
                  lm_logits = lm_logits.to(torch.float32)
              
                  # Shift so that tokens < n predict n
                  shift_logits = lm_logits[..., :-1, :].contiguous()
                  shift_labels = labels[..., 1:].contiguous().to(shift_logits.device) #<<<--------------------看这里
                  # Flatten the tokens
                  loss_fct = CrossEntropyLoss(ignore_index=-100)
                  loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1))
              
                  lm_logits = lm_logits.to(hidden_states.dtype)
                  loss = loss.to(hidden_states.dtype)
              
              if not return_dict:
                  output = (lm_logits,) + transformer_outputs[1:]
                  return ((loss,) + output) if loss is not None else output
              
              return CausalLMOutputWithPast(
                  loss=loss,
                  logits=lm_logits,
                  past_key_values=transformer_outputs.past_key_values,
                  hidden_states=transformer_outputs.hidden_states,
                  attentions=transformer_outputs.attentions,
              )
              
    • ChatGLM-6B + LoRA + Accelerate + Deepspeed ⇒ \Rightarrow 官方任务实践:参考了代码LLM-tuning,实现了该流程,具体代码见LLM微调实践。ChatGLM2-6B可参考前文代码,对tokensize改写,进行适配训练即可。由于Deepspeed框架对环境依赖性很高,因此我们采用docker技术,构建cuda11.7+torch2.0.0+python3.10虚拟环境。Docker构建的具体方法参考Docker基础知识,此处简要介绍整体流程。

      • Docker容器构建
        # 运行容器
        docker run -itd -v 宿主机路径:容器路径 --shm-size=8gb --rm --runtime=nvidia --gpus all --network host --name GPU-Docker nvidia/cuda:11.7.1-devel-ubi8 /bin/bash
        
        # 进入容器
        docker exec -it GPU-Docker /bin/bash
        
        # 注
        --shm-size=8gb必须加上,不然运行代码会报存储错误
        
      • Python环境构建
        • Python安装:自行下载Python3.10版本的Miniconda ;
          • :记得在容器内设定Python环境变量
            vi ~/.bashrc
            export PATH=/home/LLM/ChatGLM-FT/miniconda3/bin:$PATH
            source ~/.bashrc
            
        • 虚拟环境构建:参考Python基础知识
        • 依赖包安装:以下所有安装包的版本都是推荐,可按实际情况自行调整。
          # torch安装
          pip install torch==2.0.0+cu117 torchvision==0.15.1+cu117 torchaudio==2.0.1 --index-url https://download.pytorch.org/whl/cu117
          
          # 其他模块安装
          pip install transformers==4.31.0
          pip install datasets==2.14.0
          pip install peft==0.4.0
          pip install accelerate==0.21.0
          pip install deepspeed==0.10.0
          pip install sentencepiece==0.1.99
          
        • 训练启动方式
          # 切换路径
          cd ./chatglm-ft-lora-dp/
          
          # 启动训练
          accelerate launch --config_file ./conf/accelerate_config.yaml
          
          • 模型加载说明
            • empty_init=False:目前如果使用Deepspeed进行训练,在加载ChatGLM模型时,参数empty_init必须置为False(参考empty_init问题),后续官方可能会更新源码,修复该问题;
            • trust_remote_code=True:加载模型代码时,加上此参数,防止报错;
            • torch_dtype=torch.float16,FP16加载模型;
            • args.base_model:模型文件路径,最后一定是以/结尾,如./chatglm-6b-model/./chatglm-6b-model会报错。
              model = AutoModel.from_pretrained(
                          args.base_model,
                          empty_init=False,
                          torch_dtype=torch.float16,
                          trust_remote_code=True
                      )
              
          • 注意:模型训练过程中,如果出现如下错误:ValueError: max() arg is an empty sequence,需要对deepspeed源码进行修改。
            # 源码路径
            ./miniconda3/envs/zhangce-dp/lib/python3.10/site-packages/deepspeed/runtime/zero/stage3.py
            
            # 原代码
            largest_partitioned_param_numel = max([
                max([max(tensor.numel(), tensor.ds_numel) for tensor in fp16_partitioned_group])
                for fp16_partitioned_group in self.fp16_partitioned_groups
            ])
            
            # 修改后代码
            largest_partitioned_param_numel = max([
                max([max(tensor.numel(), tensor.ds_numel) for tensor in fp16_partitioned_group])
                for fp16_partitioned_group in self.fp16_partitioned_groups if len (fp16_partitioned_group) > 0
            ])
            
  • 相关学习资源

    类别简介链接
    PEFT工具PEFT的官方介绍PEFT
    PEFT工具PEFT的简单使用PEFT: 在低资源硬件上对十亿规模模型进行参数高效微调
    LLM-TuningLLM原理及实战经验分享LLM-实战经验
    LLM-TuningChatGLM-6B在真实任务上的应用ChatGLM-真实任务应用
    LLM-TuningChatGLM-6B/ChatGLM2-6B结合QLoRA实现LLM-TuningChatGLM-6B+QLoRA
    LLM-Tuning关于LLM微调的一些知识点NLP大模型微调答疑
    LLM-Tuning作者对使用的ChatGLM+LoRA方案进行了代码解析ChatGLM+LoRA代码解析
    LLM-Tuning微调工具transformers.Trainer的参数解析Trainer参数解析
    LLM-基础作者针对LLM原理进行了知识总结LLM基础知识分享
    LLM-基础介绍了LLM多种性能优化方案的原理LLM性能优化方案
    LLM-Pretrain介绍千亿参数开源大模型BLOOM背后的技术BLOOM技术介绍
    系统知识对算法基础、算法应用进行全面总结算法总结

5.3、大模型Fine-Tuning之分布式训练

  按照并行方式,分布式训练一般分为数据并行和模型并行两种,当然也有数据并行和模型并行的混合模式。

  • 模型并行:分布式系统中的不同GPU负责网络模型的不同部分。例如,神经网络模型的不同网络层被分配到不同的GPU(称作pipeline并行/流水线并行),或者同一层内部的不同参数被分配到不同GPU(称作tensor并行/张量并行);
  • 数据并行:不同的GPU有同一个模型的多个副本,每个GPU分配到不同的数据,然后将所有GPU的计算结果按照某种方式合并。

  以PyTorch框架为例,介绍几种分布式训练框架。

  • DataParallel(DP)
    • 简介:单机多卡的分布式训练工具;数据并行模式。

    • 原理:网络在前向传播的时候会将model从主卡(默认是逻辑0卡)复制一份到所有的device上,input_data会在batch这个维度被分组后加载到不同的device上计算。在反向传播时,每个卡上的梯度会汇总到主卡上,求得梯度的均值后,再用反向传播更新单个GPU上的模型参数,最后将更新后的模型参数复制到剩余指定的GPU中进行下一轮的前向传播,以此来实现并行。

    • 参数简介torch.nn.DataParallel(module, device_ids=None, output_device=None, dim=0)

      • module:是要放到多卡训练的模型;
      • device_ids:数据类型是一个列表, 表示可用的gpu卡号;
      • output_devices:数据类型也是列表,表示模型输出结果存放的卡号(如果不指定的话,默认放在0卡,即device_ids首位,这也是为什么多gpu训练并不是负载均衡的,一般0卡会占用的多,这里还涉及到一个小知识点:如果代码开始设定os.environ["CUDA_VISIBLE_DEVICES"] = "2, 3",那么0卡(逻辑卡号)指的是2卡(物理卡号)。
    • 模型参数更新方式

      • DataLoader把数据通过多个worker读到主进程的内存中;
      • 通过tensor的split语义,将一个batch的数据切分成多个更小的batch,然后分别送往不同的cuda设备;
      • 在不同的cuda设备上完成前向计算,网络的输出被gather到主cuda设备上(初始化时使用的设备),loss而后在这里被计算出来;
      • loss然后被scatter到每个cuda设备上,每个cuda设备通过BP计算得到梯度;
      • 然后每个cuda设备上的梯度被reduce到主cuda设备上,然后模型权重在主cuda设备上获得更新;
      • 在下一次迭代之前,主cuda设备将模型参数broadcast到其它cuda设备上,完成权重参数值的同步。
    • 术语介绍

      • broadcast:是主进程将相同的数据分发给组里的每一个其它进程;
      • scatter:是主进程将数据的每一小部分给组里的其它进程;
      • gather:是将其它进程的数据收集过来;
      • reduce:是将其它进程的数据收集过来并应用某种操作(比如SUM);
      • 补充:在gather和reduce概念前面还可以加上all,如all_gather,all_reduce,那就是多对多的关系了。
        在这里插入图片描述
    • 使用示例:参考一文搞定分布式训练:dataparallel、distirbuted、deepspeed、accelerate、transformers、horovod

  • DistributedDataParallel(DDP)
    • 简介:既可单机多卡又可多机多卡的分布式训练工具;数据并行模式。

    • 原理:DDP在各进程梯度计算完成之后,各进程需要将梯度进行汇总平均,然后再由rank=0的进程,将其broadcast到所有进程后,各进程用该梯度来独立的更新参数,而DP是梯度汇总到GPU0,反向传播更新参数,再广播参数给其他剩余的GPU。由于DDP各进程中的模型,初始参数一致 (初始时刻进行一次broadcast),而每次用于更新参数的梯度也一致,因此,各进程的模型参数始终保持一致。而在DP中,全程维护一个optimizer,对各个GPU上梯度进行求平均,在主卡进行参数更新,之后再将模型参数broadcast到其他GPU,相较于DP,DDP传输的数据量更少,因此速度更快,效率更高。

    • 参数简介torch.nn.parallel.DistributedDataParallel(module, device_ids=None, output_device=None, dim=0, broadcast_buffers=True, process_group=None, bucket_cap_mb=25, find_unused_parameters=False, check_reduction=False)

      • module:是要放到多卡训练的模型;
      • device_ids:是一个列表, 表示可用的gpu卡号;
      • output_devices:也是列表,表示模型输出结果存放的卡号(如果不指定的话,默认放在0卡,这也是为什么多gpu训练并不是负载均衡的,一般0卡会占用的多,这里还涉及到一个小知识点:如果程序开始加os.environ["CUDA_VISIBLE_DEVICES"] = "2, 3",那么0卡(逻辑卡号)指的是2卡(物理卡号));
      • dim:指按哪个维度进行数据的划分,默认是输入数据的第一个维度,即按batchsize划分(设数据数据的格式是B, C, H, W)。
    • 模型参数更新方式

      • process group(进程组)中的训练进程都起来后,rank为0的进程会将网络初始化参数broadcast到其它每个进程中,确保每个进程中的网络都是一样的初始化的值(默认行为,你也可以通过参数禁止);
      • 每个进程各自读取各自的训练数据,DistributedSampler确保了进程两两之间读到的是不一样的数据;
      • 前向和loss的计算如今都是在每个进程上(也就是每个cuda设备上)独立计算完成的;网络的输出不再需要gather到master进程上了,这和DP显著不一样;
      • 反向阶段,梯度信息通过all-reduce的MPI(Message Passing Interface,消息传递接口)原语,将每个进程中计算到的梯度reduce到每个进程;也就是backward调用结束后,每个进程中的param.grad都是一样的值;注意,为了提高all-reduce的效率,梯度信息被划分成了多个buckets;
      • 更新模型参数阶段,因为刚开始模型的参数是一样的,而梯度又是all-reduce的,这样更新完模型参数后,每个进程/设备上的权重参数也是一样的。因此,就无需DP那样每次迭代后需要同步一次网络参数,这个阶段的broadcast操作就不存在了。注意,Network中的Buffers (比如BatchNorm数据) 需要在每次迭代中从rank为0的进程broadcast到进程组的其它进程上。
    • 基本概念:假设我们有3台机子(节点),每台机子有4块GPU。我们希望达到12卡并行的效果。

      • 进程:程序运行起来就是进程。在DDP中,大家往往让一个进程控制一个GPU;反过来说,每个GPU由一个进程控制。因此12卡并行就需要同步运行的12个进程。因此后文中,只要提到进程,指的就是某台机子上的某个GPU在跑的程序;
      • 进程组:一个分布式任务对应了一个进程组。只有用户需要创立多个进程组时才会用到group来管理,默认情况下只有一个group;
      • world size:进程组中进程个数。也叫全局并行数。就是指总共想要用的GPU的个数。这里我们的world size就是12;
      • rank:当前进程序号。范围覆盖整个进程组:0 ~ world size-1,我们有12个GPU,各自跑1个进程,各自的进程号为0-11。进程号为0的进程叫做master,身份比较特别,需要留意;
      • local rank:每台机子上进程的序号,被各个机子用来区分跑在自己身上的进程。范围是0 ~ 某机子进程数-1。我们每台机子有4个GPU,因此三台机子上的local rank都是从0 ~ 3。在单机多卡的情况下,local rank与rank是相同的。
        在这里插入图片描述
    • 使用示例分布式训练框架介绍

    • DP vs DDP

      • DDP通过多进程实现的。也就是说操作系统会为每个GPU创建一个进程,从而避免了Python解释器GIL带来的性能开销。而DP是通过单进程控制多线程来实现的。还有一点,DDP也不存在前面DP提到的负载不均衡问题;
      • 参数更新的方式不同。DDP在各进程梯度计算完成之后,各进程需要将梯度进行汇总平均,然后再由rank=0的进程,将其broadcast到所有进程后,各进程用该梯度来独立的更新参数,而DP是梯度汇总到GPU0,反向传播更新参数,再广播参数给其他剩余的GPU。由于DDP各进程中的模型,初始参数一致 (初始时刻进行一次broadcast),而每次用于更新参数的梯度也一致,因此,各进程的模型参数始终保持一致。而在DP中,全程维护一个optimizer,对各个GPU上梯度进行求平均,在主卡进行参数更新,之后再将模型参数broadcast到其他GPU,相较于DP,DDP传输的数据量更少,因此速度更快,效率更高;
      • DDP支持all-reduce(指汇总不同GPU计算所得的梯度,并同步计算结果),broadcast,send和receive等等。通过MPI、GLOO实现CPU通信,通过NCCL实现GPU通信,缓解了进程间通信开销大的问题。
  • 自动混合精度训练(AMP):自动混合精度训练(automatic mixed-precision training)并不是一种分布式训练框架,通常它与其他分布式训练框架相结合,能进一步提升训练速度。下面我们简单介绍下AMP的原理,然后与DDP结合,给出AMP的使用范例。具体参考论文MIXED PRECISION TRAINING
    • 简介:默认情况下,大多数深度学习框架都采用32位浮点算法进行训练。2017年,NVIDIA研究了一种用于混合精度训练的方法,该方法在训练网络时将单精度(FP32,以32bits表示数字,即4bytes)与半精度(FP16,以16bits表示数字,即2bytes)结合在一起,并使用相同的超参数实现了与FP32几乎相同的效果。以PyTorch为例,可通过如下命令查看模型参数精度:

      for name, param in model.named_parameters():
              print(name, param.dtype)
      
    • 关键词:AMP(自动混合精度)的关键词有两个:自动,混合精度。

      • 自动:Tensor的dtype类型会自动变化,框架按需自动调整tensor的dtype,当然有些地方还需手动干预;
      • 混合精度:采用不止一种精度的Tensor,torch.FloatTensor和torch.HalfTensor。
    • 适用硬件:Tensor Core是一种矩阵乘累加的计算单元,每个tensor core时针执行64个浮点混合精度操作(FP16矩阵相乘和FP32累加)。英伟达宣称使用Tensor Core进行矩阵运算可以轻易的提速,同时降低一半的显存访问和存储。因此,在PyTorch中,当提到自动混合精度训练,指的就是在NVIDIA支持Tensor Core的CUDA设备上使用。

    • 原理:前面已介绍,AMP其实就是Float32与Float16的混合,那为什么不单独使用Float32或Float16,而是两种类型混合呢?原因是:在某些情况下Float32有优势,而在另外一些情况下Float16有优势。而相比于之前的默认的torch.FloatTensor,torch.HalfTensor的劣势不可忽视。这里先介绍下FP16优劣势。
        torch.HalfTensor的优势就是存储小、计算快、更好的利用CUDA设备的Tensor Core。因此训练的时候可以减少显存的占用(可以增加batchsize了),同时训练速度更快。

      • 减少显存占用:现在模型越来越大,当你使用Bert这一类的预训练模型时,往往模型及模型计算就占去显存的大半,当想要使用更大的batchsize的时候会显得捉襟见肘。由于FP16的内存占用只有FP32的一半,自然地就可以帮助训练过程节省一半的显存空间,可以增加batchsize了;
      • 加快训练和推断的计算:与普通的空间与时间Trade-off的加速方法不同,FP16除了能节约内存,还能同时节省模型的训练时间。在大部分的测试中,基于FP16的加速方法能够给模型训练能带来多一倍的加速体验;
      • 张量核心的普及(NVIDIA Tensor Core):低精度计算是未来深度学习的一个重要趋势。
         

        torch.HalfTensor的劣势就是:溢出错误,数值范围小(更容易Overflow / Underflow);舍入误差(Rounding Error),导致一些微小的梯度信息达不到16bit精度的最低分辨率,从而丢失。

      • 溢出错误:由于FP16的动态范围比FP32位的狭窄很多,因此,在计算过程中很容易出现上溢出(Overflow)和下溢出(Underflow),溢出之后就会出现"NaN"的问题。在深度学习中,由于激活函数的梯度往往要比权重梯度小,更易出现下溢出的情况。在训练后期,例如激活函数的梯度会非常小, 甚至在梯度乘以学习率后,值会更加小;
      • 舍入误差:指的是当梯度过小时,小于当前区间内的最小间隔时,该次梯度更新可能会失败。具体的细节如下图所示,由于更新的梯度值超出了FP16能够表示的最小值的范围,因此该数值将会被舍弃,这个权重将不进行更新。在这里插入图片描述
         

        综上可知,torch.HalfTensor存在一定的劣势。因此需要采取适当的方法,一方面可以利用torch.HalfTensor的优势,另一方面需要避免torch.HalfTensor的劣势。AMP即是最终的解决方案。

      • 混合精度训练:在某些模型中,FP16矩阵乘法的过程中,需要利用FP32来进行矩阵乘法中间的累加(accumulated),然后再将FP32的值转化为FP16进行存储。 换句不太严谨的话来说,也就是在内存中用FP16做储存和乘法从而加速计算,而用FP32做累加避免舍入误差。混合精度训练的策略有效地缓解了舍入误差的问题。
        在这里插入图片描述
          在这里也就引出了,为什么网上大家都说,只有Nvidia Volta结构的拥有Tensor Core的CPU(例如V100),才能利用FP16混合精度来进行加速。 那是因为Tensor Core能够保证FP16的矩阵相乘,利用FP16 or FP32来进行累加。在累加阶段能够使用FP32大幅减少混合精度训练的精度损失。而其他的GPU只能支持FP16的multiply-add operation。这里直接贴出原文句子:

        Whereas previous GPUs supported only FP16 multiply-add operation, NVIDIA Volta GPUs introduce Tensor Cores that multiply FP16 input matrices andaccumulate products into either FP16 or FP32 outputs

      • FP32权重备份:这种方法主要是用于解决舍入误差的问题。其主要思路,可以概括为:weights,activations,gradients等数据在训练中都利用FP16来存储,同时拷贝一份FP32的weights,用于更新。如下图:在这里插入图片描述
         
          可以看到,其他所有值(weights,activations, gradients)均使用FP16来存储,而唯独权重weights需要用FP32的格式额外备份一次。 这主要是因为,在更新权重的时候,往往公式: 权重 = 旧权重 + lr * 梯度,而在深度模型中,lr * 梯度这个值往往是非常小的,如果利用FP16来进行相加的话, 则很可能会出现上面所说的『舍入误差』的这个问题,导致更新无效。因此上图中,通过将weights拷贝成FP32格式,并且确保整个更新(update)过程是在FP32格式下进行的,如下所示:
        w e i g h t 32 = w e i g h t 32 + η ⋅ g r a d i e n t 32 weight_{32}=weight_{32}+\eta \cdot gradient_{32} weight32=weight32+ηgradient32
          看到这里,可能有人提出这种FP32拷贝weights的方式,那岂不是使得内存占用反而更高了呢?是的,FP32额外拷贝一份weights的确新增加了训练时候存储的占用。 但是实际上,在训练过程中,内存中占据大部分的基本都是activations的值,如下图所示。特别是在batchsize很大的情况下, activations更是特别占据空间。 保存activiations主要是为了在backward的时候进行计算。因此,只要activations的值基本都是使用FP16来进行存储的话,则最终模型与FP32相比起来, 内存占用也基本能够减半。 在这里插入图片描述

      • 损失放大(Loss Scale):即使采用了混合精度训练,还是存在无法收敛的情况,原因是激活梯度的值太小,造成了下溢出(Underflow)。Loss Scale主要是为了解决FP16 underflow的问题。刚才提到,训练到了后期,梯度(特别是激活函数平滑段的梯度)会特别小,如果用FP16来表示,则这些梯度都会变成0,因此导致FP16表示容易产生underflow现象。
          为了解决梯度过小的问题,论文中对计算出来的loss值进行scale,由于链式法则的存在,loss上的scale会作用在梯度上。这样比起对每个梯度进行scale更加划算。 scaled过后的梯度,就会平移到FP16有效的展示范围内。
          这样,scaled-gradient就可以一直使用FP16进行存储了。只有在进行更新的时候,才会将scaled-gradient转化为FP32,同时将scale抹去。论文指出, scale并非对于所有网络而言都是必须的。论文给出scale的取值在8 - 32k之间皆可。
          Pytorch可以通过使用torch.cuda.amp.GradScaler,通过放大loss的值来防止梯度的underflow(只在BP时传递梯度信息使用,真正更新权重时还是要把放大的梯度再unscale回去)
          综上,损失放大的思路是:

        • 反向传播前,将损失变化手动增大 2 k 2^{k} 2k倍,因此反向传播时得到的中间变量(激活函数梯度)则不会溢出;
        • 反向传播后,将权重梯度缩小 2 k 2^{k} 2k倍,恢复正常值。
    • 使用示例分布式训练框架介绍

  • Accelerate:DP简单且容易调试,DDP快但是难debug,且代码改动稍大,例如要开启后端通讯,数据sampler的方式也要改。有没有工具不仅代码改动量少,方便debug,而且训练起来快呢?其中一个答案就是Accelerate库,Accelerate库是大名鼎鼎的huggingface公司在2021年初推出的PyTorch分布式训练工具库,官方链接是 Accelerate。另外有篇比较好的说明文档是Accelerate
    • 简介:Accelerate是huggingface开源的一个方便将PyTorch模型迁移到multi-GPUs/TPU/FP16模式下训练的小巧工具。和标准的PyTorch方法相比,使用accelerate进行multi-GPUs/TPU/FP16模型训练变得非常简单(只需要在标准的PyTorch训练代码中改动几行代码就可以适应multi-GPUs/TPU/FP16等不同的训练环境),而且速度与原生PyTorch相比,非常之快。
    • 使用示例分布式训练框架介绍
      • 使用技巧HuggingFace——Accelerate的使用
        • accelerate config:通过在终端中回答一系列问题生成配置文件;
          accelerate config --config_file ./accelerate_config.yaml
          
        • accelerate env:验证配置文件的合法性;
          accelerate env --config_file ./accelerate_config.yaml
          
        • accelerate launch:运行自己的python文件;
          accelerate launch --config_file ./conf/accelerate_config.yaml train_accelerate.py
          
        • accelerate test:运行accelerate默认的神经网络模型来测试环境是否可以。
          accelerate test --config_file ./accelerate_config.yaml
          
  • Deepspeed:Deepspeed是Microsoft提供的分布式训练工具,适用于更大规模模型的训练,官方链接是DeepSpeed。这里我们详细介绍下Deepspeed的分布式原理,具体的使用示例可参考前文的PEFT实践部分。
    • 简介:DeepSpeed是一个由微软开发的开源深度学习优化库,旨在提高大规模模型训练的效率和可扩展性。DeepSpeed的核心技术是ZeRO(Zero Redundancy Optimizer,零冗余优化),通过ZeRO技术实现了数据并行。另外,DeepSpeed也支持模型并行(借用英伟达的Megatron-LM来为基于Transformer的语言模型提供张量并行功能,张量并行参考Megatron-LM;通过梯度累积来实现流水线并行,流水线并行参考Pipeline Parallelism)。

    • 原理:关于模型并行部分具体原理,大家自行查阅相关文档,这里不予过多介绍。接下来,我们着重介绍下DeepSpeed的核心技术ZeRO:ZeRO-1、ZeRO-2、ZeRO-3、ZeRO-Offload与ZeRO-Infinity,具体参考《ZeRO: Memory Optimizations Toward Training Trillion Parameter Models》《ZeRO-Offload: Democratizing Billion-Scale Model Training》《ZeRO-Infinity: Breaking the GPU Memory Wall for Extreme Scale Deep Learning》DeepSpeed ZeRO

      • 存储分类:首先,大模型训练的过程中,GPU需要存储的内容包括两大块:Model StatesResidual States

        • Model State:指和模型本身息息相关的,必须存储的内容,具体包括:
          • optimizer states:Adam优化算法中的momentum和variance;
          • gradients:模型梯度G;
          • parameters:模型参数W。
        • Residual States:指并非模型必须的,但在训练过程中会额外产生的内容,具体包括:
          • activations:激活值。在backward过程中使用链式法则计算梯度时会用到。有了它计算梯度会更快,但它不是必须存储的,因为可以通过重新做forward来计算算它。实际上,activations就是模型在训练过程中产生的中间值,举个例子: x 2 = w 1 ∗ x , y = w 2 ∗ x 2 x_{2}=w_{1} * x,y=w_{2} * x_{2} x2=w1xy=w2x2,假设上面的参数( w 1 w_{1} w1 w 2 w_{2} w2)和输入 x x x都是标量,在反向传播阶段要计算 y y y w 2 w_{2} w2的梯度,很明显是 x 2 x_{2} x2,这个 x 2 x_{2} x2就属于activations,也就是在前向阶段需要保存的一个中间结果。当然我们也可以不保存,当反向阶段需要用到 x 2 x_{2} x2时再重新通过forward过程临时计算;
          • temporary buffers: 临时存储。例如把梯度发送到某块GPU上做加总聚合时产生的存储。
          • unusable fragment memory:碎片化的存储空间。虽然总存储空间是够的,但是如果取不到连续的存储空间,相关的请求也会被fail掉。对这类空间浪费可以通过内存整理来解决。
      • 存储大小:了解了存储分类,接下来了解下每种存储占用的内存大小。首先我们回忆下混合精度训练的过程,大致如下图所示:在这里插入图片描述

        • 混合精度训练:简单来说,混合精度训练的流程有如下几步。
          • 存储一份FP32的parameter,momentum和variance(统称model states);
          • 在forward开始之前,额外开辟一块存储空间,将FP32的parameter减半到FP16 parameter;
          • 正常做forward和backward,在此之间产生的activations和gradients,都用FP16进行存储;
          • 将FP16的gradients转换为FP32的gradients,用FP32的gradients去更新FP32下的model states。 当模型收敛后,FP32的parameter就是最终的参数输出。
             

          现在,我们可以来计算模型在训练时需要的存储大小了,假设模型的参数W大小是 Φ \Phi Φ (根据参数量预估显存占用的方法参见参数量估计与显存估计,这里简单提下,比如6B的模型,使用FP16方式载入显存,所需显存大小:6B ∗ \ast 2 = 12G),则训练时对应的存储如下:
        在这里插入图片描述
          因为采用了Adam优化,所以才会出现momentum和variance,当然你也可以选择别的优化办法,这里为了通用,模型必存的数据大小为 K Φ K\Phi KΦ,因此总的存储大小为 ( 2 + 2 + K ) Φ (2+2+K)\Phi 2+2+KΦ。另外,这里暂不将activations纳入统计范围,原因是:

        • activations不仅与模型参数相关,还与batchsize相关;
        • activations的存储不是必须的。前文已经提到,存储activations只是为了在用链式法则做backward的过程中,计算梯度更快一些。但你永远可以通过只保留最初的输入X,重新做forward来得到每一层的activations(虽然实际中并不会这么极端);
        • 因为activations的这种灵活性,纳入它后不方便衡量系统性能随模型增大的真实变动情况。因此在这里不考虑它。
      • ZeRO-DP:了解了存储种类以及它们所占的存储大小之后,接下来我们介绍下Deepspeed是如何优化存储的。这里提前透露下,ZeRO三阶段:ZeRO-1、ZeRO-2、ZeRO-3的实质是数据并行,因此我们也称之为ZeRO-DP,后面会介绍具体细节。首先我们应该清楚,在整个训练中,有很多states并不会每时每刻都用到,举例来说;

        • Adam优化下的optimizer states只在最终做update时才用到;
        • 数据并行中,gradients只在最后做all-reduce和update时才用到;
        • 参数W只在做forward和backward的那一刻才用到。
           

          诸如此类,所以,ZeRO-DP想了一个简单粗暴的办法:如果数据算完即废,等需要的时候,我再想办法从个什么地方拿回来,那不就省了一笔存储空间吗?沿着这个思路,我们逐一来看ZeRO是如何递进做存储优化的。

        • ZeRO-1:即 P o s P_{os} Pos,优化状态分割。首先,从optimizer states开始优化。将optimizer states分成若干份,每块GPU上各自维护一份。这样就减少了相当一部分的显存开销。如下图:在这里插入图片描述
          整体数据并行的流程如下:
          • 每块GPU上存一份完整的参数W。将一个batch的数据分成3份,每块GPU各吃一份,做完一轮forward和backward后,各得一份梯度;
          • 对梯度做一次all-reduce,得到完整的梯度G,产生单卡通讯量 2 Φ 2\Phi 。对于all-reduce(reduce-scatter + all-gather)的通讯量,reduce-scatter操作发送和接收的通讯量为 Φ \Phi Φ,all-gather操作发送和接收的通讯量也为 Φ \Phi Φ,因此all-reduce的通讯录为 2 Φ 2\Phi 。注意,此处我们不去探寻单次发送和接收的通讯量为什么是 Φ \Phi Φ,感兴趣的同学可自行探索手把手推导Ring All-reduce的数学性质
          • 得到完整梯度G,就可以对W做更新。我们知道W的更新由optimizer states和梯度共同决定。由于每块GPU上只保管部分optimizer states,因此只能将相应的W(蓝色部分)进行更新。上述步骤可以用下图表示:在这里插入图片描述
          • 此时,每块GPU上都有部分W没有完成更新(图中白色部分)。所以我们需要对W做一次all-gather,从别的GPU上把更新好的部分W取回来。产生单卡通讯量 Φ \Phi Φ
             

          做完 P o s P_{os} Pos后,设GPU个数为 N d N_{d} Nd,显存和通讯量的情况如下:

        并行化技术显存显存(GB), Φ = 7.5 B \Phi=7.5B Φ=7.5B N d = 64 N_{d}=64 Nd=64 K = 12 K=12 K=12单卡通讯量
        朴素DP(2+2+ K K K Φ \Phi Φ120GB2 Φ \Phi Φ
        P o s P_{os} Pos(2+2+ K N d \frac{K}{N_{d}} NdK Φ \Phi Φ31.4GB3 Φ \Phi Φ

           如图所示, P o s P_{os} Pos在增加1.5倍单卡通讯开销的基础上,将单卡存储降低了4倍。这里需要说明下,有其他相关技术博客,给出的 P o s P_{os} Pos单卡通讯量是2 Φ \Phi Φ。其实虽然按照论文中定义,计算的通讯量是3 Φ \Phi Φ,但在官方代码的具体实现中,通讯量应该是2 Φ \Phi Φ,这是因为在第二个步骤中,由于每块GPU上只保管部分optimizer states,因此根本不需要对梯度做all-gather操作。因为即使每块GPU上有完整的梯度,在实际计算中有部分梯度也用不上。这样 P o s P_{os} Pos单卡通讯量就是2 Φ \Phi Φ

        • ZeRO-2:即 P o s + P g P_{os}+P_{g} Pos+Pg,优化状态与梯度分割。现在,更近一步,我们把梯度也拆开,每个GPU格子维护一块梯度。在这里插入图片描述
          此时,数据并行的整体流程如下:
          • 每块GPU上存一份完整的参数W。将一个batch的数据分成3份,每块GPU各吃一份,做完一轮foward和backward后,算得一份完整的梯度(下图中绿色+白色);
          • 对梯度做一次reduce-scatter,保证每个GPU上所维持的那块梯度是聚合更新后的梯度。例如对GPU1,它负责维护G1,因此其他的GPU只需要把G1对应位置的梯度发给GPU1做加总就可。汇总完毕后,白色块对GPU无用,可以从显存中移除。单卡通讯量为 Φ \Phi Φ。如下图所示。在这里插入图片描述
          • 每块GPU用自己对应的O和G去更新相应的W。更新完毕后,每块GPU维持了一块更新完毕的W。同理,对W做一次all-gather,将别的GPU算好的W同步到自己这来。单卡通讯量 Φ \Phi Φ
             

          做完 P o s + P g P_{os}+P_{g} Pos+Pg后,设GPU个数为 N d N_{d} Nd,显存和通讯量的情况如下:

        并行化技术显存显存(GB), Φ = 7.5 B \Phi=7.5B Φ=7.5B N d = 64 N_{d}=64 Nd=64 K = 12 K=12 K=12单卡通讯量
        朴素DP(2+2+ K K K Φ \Phi Φ120GB2 Φ \Phi Φ
        P o s P_{os} Pos(2+2+ K N d \frac{K}{N_{d}} NdK Φ \Phi Φ31.4GB3 Φ \Phi Φ
        P o s + P g P_{os}+P_{g} Pos+Pg(2+ 2 + K N d \frac{2+K}{N_{d}} Nd2+K Φ \Phi Φ16.6GB2 Φ \Phi Φ

           如图所示,和朴素DP相比,存储降了8倍,单卡通讯量持平。

        • ZeRO-3:即 P o s + P g + P p P_{os}+P_{g}+P_{p} Pos+Pg+Pp,优化状态、梯度与参数分割。现在,我们把参数也切开。每块GPU置维持对应的optimizer states,gradients和parameters(即W)。在这里插入图片描述
          数据并行的流程如下:
          • 每块GPU上只保存部分参数W。将一个batch的数据分成3份,每块GPU各吃一份;
          • 做forward时,对W做一次all-gather,取回分布在别的GPU上的W,得到一份完整的W,单卡通讯量 Φ \Phi Φ。forward做完,立刻把不是自己维护的W抛弃;
          • 做backward时,对W做一次all-gather,取回完整的W,单卡通讯量 Φ \Phi Φ。backward做完,立刻把不是自己维护的W抛弃;
          • 做完backward,算得一份完整的梯度G,对G做一次reduce-scatter,从别的GPU上聚合自己维护的那部分梯度,单卡通讯量 Φ \Phi Φ。聚合操作结束后,立刻把不是自己维护的G抛弃。
          • 用自己维护的O和G,更新W。由于只维护部分W,因此无需再对W做任何all-reduce操作。
             

          做完 P o s + P g + P p P_{os}+P_{g}+P_{p} Pos+Pg+Pp后,设GPU个数为 N d N_{d} Nd,显存和通讯量的情况如下:

        并行化技术显存显存(GB), Φ = 7.5 B \Phi=7.5B Φ=7.5B N d = 64 N_{d}=64 Nd=64 K = 12 K=12 K=12单卡通讯量
        朴素DP(2+2+ K K K Φ \Phi Φ120GB2 Φ \Phi Φ
        P o s P_{os} Pos(2+2+ K N d \frac{K}{N_{d}} NdK Φ \Phi Φ31.4GB3 Φ \Phi Φ
        P o s + P g P_{os}+P_{g} Pos+Pg(2+ 2 + K N d \frac{2+K}{N_{d}} Nd2+K Φ \Phi Φ16.6GB2 Φ \Phi Φ
        P o s + P g + P p P_{os}+P_{g}+P_{p} Pos+Pg+Pp 2 + 2 + K N d \frac{2+2+K}{N_{d}} Nd2+2+K Φ \Phi Φ1.9GB3 Φ \Phi Φ

           如图所示,和朴素DP相比,用1.5倍的通讯开销,换回近120倍的显存。最终,我们可以看下论文中的总体对比图:在这里插入图片描述

      • ZeRO-DP VS 模型并行:通过上述的介绍,大家可能会有疑问,既然ZeRO都把参数W给切了,那它应该是个模型并行,为什么却归到数据并行?其实ZeRO是模型并行的形式,数据并行的实质。

        • 模型并行,是指在forward和backward的过程中,我只需要用自己维护的那块W来计算就行。即同样的输入X,每块GPU上各算模型的一部分,最后通过某些方式聚合结果;
        • 但对ZeRO来说,它做forward和backward的时候,是需要把各GPU上维护的W聚合起来的,即本质上还是用完整的W进行计算。它是不同的输入X,完整的参数W,最终再做聚合。
      • ZeRO-Offload:简单介绍一下ZeRO-Offload。它的核心思想是:显存不够,内存来凑。如果把要存储的大头卸载(offload)到CPU上,而把计算部分放到GPU上,这样比起跨机,既能降低显存使用,也能减少一些通讯压力。ZeRO-Offload的做法是:

        • forward和backward计算量高,因此和它们相关的部分,例如参数W(FP16)、activations,就全放入GPU;
        • update的部分计算量低,因此和它相关的部分,全部放入CPU中。例如W(FP32)、optimizer states(FP32)和gradients(FP32)等。
           

        具体切分如下图:在这里插入图片描述

    • Accelerate vs Deepspeed

      • Accelerate是PyTorch官方提供的分布式训练工具,而Deepspeed是由Microsoft提供的分布式训练工具;
      • 最主要的区别在于支持的模型规模不同,Deepspeed支持更大规模的模型;
      • Deepspeed还提供了更多的优化策略和工具,例如ZeRO和Offload等;
      • Accelerate更加稳定和易于使用,适合中小规模的训练任务;
      • 目前Accelerate已经集成了Deepspeed及Megatron分布式技术,具体可详见前文的PEFT实践部分。
    • 资源分享大模型训练之微调篇大模型训练之框架篇

5.4、大模型知识问答

  • nB大小的模型,训练和推理时,显存占用情况?
    • 推理时显存的下限是2nGB ,至少要把模型加载完全;训练时,如果用Adam优化器,参考前文的2+2+12的公式,训练时显存下限是16nGB,需要把模型参数、梯度和优化器状态加载进来。
  • 如果有N张显存足够大的显卡,怎么加速训练?
    • 数据并行(DP),充分利用多张显卡的算力。
  • 如果显卡的显存不够装下一个完整的模型呢?
    • 最直观想法,需要分层加载,把不同的层加载到不同的GPU上(accelerate的device_map),也就是常见的PP,流水线并行。
  • 但PP推理起来,是一个串行的过程,1个GPU计算,其他GPU空闲,有没有其他方式?
    • 横向切分,流水线并行(PP),也就是分层加载到不同的显卡上;
    • 纵向切分,张量并行(TP),也称作模型并行(MP)。
  • 3种并行方式可以叠加吗?
    • 是可以的,DP+PP+TP,这就是3D并行。如果真有1个超大模型需要预训练,3D并行那是必不可少的,参考BLOOM模型的训练,DP+PP用DeepSpeed,TP用Megatron-LM。
  • 最主流的开源大模型?
    • ChatGLM-6B,prefix LM;
    • LLaMA-7B,causal LM。
  • prefix LM和causal LM的区别?
    • Attention Mask不同,前者的prefix部分的token互相能看到,后者严格遵守只有后面的token才能看到前面的token的规则。
  • 哪种架构是主流?
    • GPT系列就是Causal LM,目前除了T5和GLM,其他大模型基本上都是Causal LM。
  • 如何给LLM注入领域知识?
    • 第一种办法,检索+LLM,先用问题在领域数据库里检索到候选答案,再用LLM对答案进行加工;
    • 第二种方法,把领域知识构建成问答数据集,用SFT让LLM学习这部分知识。

二、LLM简介

  通过前文对Tuning技术的介绍,我们能够了解到,Tuning技术依赖于LLM的发展,同时也在推动着LLM的发展。通常,LLM指的是包含数百亿(或更多)参数的语言模型,这些模型在大量的文本数据上训练。接下来我们介绍几个耳熟能详的大语言模型,其他LLM的相关内容可参考LLMSurveyOpen LLM Leaderboard
开源大语言模型(LLM)汇总(持续更新中)

1、GPT系列(OpenAI)

1.1 GPT-1、GPT-2、GPT-3

  2017年,Google推出Transformer,利用attention完全替代过往深度学习中的Recurrence和Convolution结构,直白地展现出了“大一统模型”的野心,"xxx is all you need"也成了一个玩不烂的梗。
  2018年6月,OpenAI推出基于Transformer Decoder改造的第一代GPT(Generative Pre-Training),有效证明了在NLP领域上使用预训练+微调方式的有效性。紧随其后,同年10月Google推出基于Transformer Encoder部分的Bert,在同样参数大小的前提下,其效果领跑于GPT-1,一时成为NLP领域的领头羊。
  不甘示弱的OpenAI在4个月后,推出更大的模型GPT-2(GPT-1: 110M,Bert: 340M,GPT-2: 1.5B),同时,OpenAI也知道,光靠增加模型大小和训练数据集来获得一个和Bert差不多效果的模型,其实是没有技术含量的。于是,在GPT-2里,OpenAI引入zero-shot并证明了其有效性。
  此后,OpenAI在LLM上义无反顾地走了下去,在2020年6月推出巨人GPT-3,参数量高达175B,各类实验效果达到顶峰,据说一次训练费用为1200w美元,“贵”也成了普通工业界踏足GPT系列的壁垒之一。
  在正式介绍GPT系列模型之前,我们先介绍下语言模型的概念,语言模型是GPT系列模型的基座。什么是语言模型?简单来说,就是看一个句子是人话的可能性。专业一点来说,给定一个句子,其字符是 W = ( w 1 , w 2 , ⋯   , w L ) W=(w_{1},w_{2},\cdots,w_{L}) W=(w1,w2,,wL),那么,从语言模型来看,这个句子是人话的可能性就是:
P ( W ) = P ( w 1 , w 2 , ⋯   , w L ) = P ( w 1 ) P ( w 2 ∣ w 1 ) P ( w 3 ∣ w 1 , w 2 ) ⋯ P ( w L ∣ w 1 , w 2 , ⋯   , w L − 1 ) \begin{aligned} P(W)&=P(w_{1},w_{2},\cdots,w_{L})\\ &=P(w_{1})P(w_{2}|w_{1})P(w_{3}|w_{1},w_{2})\cdots P(w_{L}|w_{1},w_{2},\cdots,w_{L-1})\\ \end{aligned} P(W)=P(w1,w2,,wL)=P(w1)P(w2w1)P(w3w1,w2)P(wLw1,w2,,wL1)
但是, L L L太长就会很稀疏,直接算这个概率不好计算,我们就可以用近似计算:
P ( W ) = P ( w 1 , w 2 , ⋯   , w L ) = P ( w 1 ) P ( w 2 ∣ w 1 ) P ( w 3 ∣ w 1 , w 2 ) ⋯ P ( w L ∣ w 1 , w 2 , ⋯   , w L − 1 ) = P ( w 1 ) P ( w 2 ∣ w 1 ) ⋯ P ( w L ∣ w L − N , ⋯   , w L − 1 ) \begin{aligned} P(W)&=P(w_{1},w_{2},\cdots,w_{L})\\ &=P(w_{1})P(w_{2}|w_{1})P(w_{3}|w_{1},w_{2})\cdots P(w_{L}|w_{1},w_{2},\cdots,w_{L-1})\\ &=P(w_{1})P(w_{2}|w_{1})\cdots P(w_{L}|w_{L-N},\cdots,w_{L-1}) \end{aligned} P(W)=P(w1,w2,,wL)=P(w1)P(w2w1)P(w3w1,w2)P(wLw1,w2,,wL1)=P(w1)P(w2w1)P(wLwLN,,wL1)
这就是常说的N-gram统计语言模型,N通常是2,3,4。特别的,当N=1时,语言模型就退化为各个字符出现的概率之积。当N=4时语言模型就比较大了,实际应用中一般最大也就是4了。根据条件概率 P ( w L ∣ w L − N , ⋯   , w L − 1 ) P(w_{L}|w_{L-N},\cdots,w_{L-1}) P(wLwLN,,wL1),我们就能知道给定前N个字,下一个字是什么字的概率了。语言模型的评价指标可以采用PPL(困惑度,Perplexity,语言模型)。
  接下来,我们分别介绍GPT-1、GPT-2、GPT-3的模型原理及预训练方法等相关知识。

  • GPT-1:GPT-1是OpenAI在论文《Improving Language Understanding by Generative Pre-Training》中提出的生成式预训练语言模型。该模型的核心思想:通过二段式的训练,第一个阶段是利用语言模型进行预训练(无监督形式),第二阶段通过Fine-Tuning的模式解决下游任务(监督模式下)。GPT-1可以很好地完成若干下游任务,包括文本分类、自然语言推理、问答、语义相似度等。在多个下游任务中,微调后的GPT-1性能均超过了当时针对特定任务训练的SOTA模型。

    • 自然语言推理(Natural Language Inference 或者 Textual Entailment):判断两个句子是包含关系(entailment),矛盾关系(contradiction),或者中立关系(neutral);

    • 问答和常识推理(Question answering and commonsense reasoning):类似于多选题,输入一个文章,一个问题以及若干个候选答案,输出为每个答案的预测概率;

    • 语义相似度(Semantic Similarity):判断两个句子是否语义上是相关的;

    • 分类(Classification):判断输入文本是指定的哪个类别。
       
      下面具体介绍下GPT-1的模型结构及训练流程。

    • 模型结构:GPT-1基础架构是基于Transformer的Decoder部分,同时删除了Encoder-Decoder Attention层,只保留了Masked Multi-Head Attention层和Feed Forward层。Transformer结构提出之始便用于机器翻译任务,机器翻译是一个序列到序列的任务,因此Transformer设计了Encoder用于提取源端语言的语义特征,而用Decoder提取目标端语言的语义特征,并生成相对应的译文。GPT-1目标是服务于单序列文本的生成式任务,所以含弃了关于Encoder部分,包括Decoder的Encoder-Decoder Attention层。整体是12层的Transformer-Decoder变体,如下图所示:在这里插入图片描述
      除此之外,GPT-1还将attention的维数扩大到768(原来为512),将attention的头数增加到12个(原来为8个),将Feed Forward层的隐层维数增加到3072(原来为2048),总参数达到110M。GPT-1还优化了学习率预热算法,使用更大的BPE码表(词表大小为40478,478个base characters + 40000个结合的字符),激活函数ReLU改为对梯度更新更友好的高斯误差线性单元GeLU,将正余弦构造的位置编码改为了带学习的位置编码。

    • 模型训练:上文已经提到,GPT-1模型训练整体上分为两步:1)在大规模无标注文本数据上学习到一个高容量的语言模型;2)在标注数据上进行微调。其中第二步是针对具体的下游任务来进行训练的。

      • 无监督预训练:总体训练任务目标是根据已知的词预测未知的词。在这里设定一定的窗口大小,即根据有限的词预测下一个词:给定一个语料的句子序列 U = { u 1 , ⋯   , u n } \mathcal{U}=\{u_{1},\cdots,u_{n}\} U={u1,,un},已知前 k k k个词预测当前词 u i u_{i} ui,用一个标准的语言模型目标去极大化这个似然函数:
        L 1 ( U ) = ∑ i l o g P ( u i ∣ u i − k , ⋯   , u i − 1 ; Θ )   L_{1}(\mathcal{U})=\sum_{i} logP(u_{i}|u_{i-k},\cdots, u_{i-1};\Theta)\ L1(U)=ilogP(uiuik,,ui1;Θ) 
        其中: k k k是滑动窗口大小, Θ \Theta Θ是要优化的参数。 P ( u ) P(u) P(u)的计算方法是:
        h 0 = U W e + W p h i = t r a n s f o r m e r _ b l o c k ( h i − 1 ) , ∀ i ∈ [ 1 , n ] P ( u ) = s o f t m a x ( h n W e T ) \begin{aligned} h_{0}&=UW_{e}+W_{p}\\ h_{i}&=transformer\_block(h_{i-1}),\forall i\in[1,n]\\ P(u)&=softmax(h_{n}W_{e}^{T}) \end{aligned} h0hiP(u)=UWe+Wp=transformer_block(hi1)i[1,n]=softmax(hnWeT)
        其中: W e W_{e} We是词向量矩阵(token embedding matrix), W p W_{p} Wp是位置向量矩阵(position embedding matrix), U = u − k , ⋯   , u − 1 ) U=u_{-k},\cdots,u_{-1}) U=uk,,u1)是tokens的上下文向量(源代码中, u i u_{i} ui都是one-hot编码向量,相当于做一个查询操作, U U U存储索引, W e W_{e} We存储着词向量值), n n n是Decoder层的数量。
          上面是论文中的描述,我们举一个简单的例子,来说明GPT-1实际上是如何进行无监督预训练的。例如输入文本是:【今天很开心】,这段文本经过切词转换为一个个token后,输入GPT-1的transformer-decoder结构,在最后一层,会输出每个token对应的表征向量,即上文的 h n ∈ R m × d h_{n}\in R^{m\times d} hnRm×d,其中 m m m是token数量,这个例子中就是5, d d d是模型维度,GPT-1中就是768;接下来, h n h_{n} hn再经过一个全连接层,生成 z n ∈ R m × v z_{n}\in R^{m\times v} znRm×v,其中 v v v是词表的大小;最后, z n z_{n} zn会经过softmax操作,然后选取它每一行中数值最大的索引到词表中搜索对应的token,搜索到的token怎么用呢?我们的目标是下一个词的预测,输入是【今天很开心】,输出也是5个token,因为输入的第一个token是【今】,因此我们希望输出的第一个token是【天】,输入的第二个token是【天】,则希望输出的第二个token是【很】,依此类推,直到最后一个输入token【心】,不过因为它没有下一个词,所以在预训练过程中,不在我们的损失计算范围内。所以,我们会更新模型参数,尽可能的让最终的输出token的前四个字是【天很开心】,这就是预训练任务的整体流程。回过头来,我们也理解了为什么预训练叫做无监督训练,就是因为我们其实没有标注样本,而是拿下一个词当做标签进行模型训练,这种方式也被称作自监督训练。

      • 监督训练:当得到无监督的预训练模型之后,我们将它直接应用到有监督任务中继续训练。对于一个有标签的数据集 C \mathcal{C} C,每个实例有 m m m个输入token: { x 1 , ⋯   , x m } \{x^{1},\cdots,x^{m}\} {x1,,xm},它对应的标签是 y y y。首先将这些token输入到训练好的预训练模型中,获取最后一个transformer decoder的输出,得到最终的特征向量 h l m h_{l}^{m} hlm。然后再通过一个全连接层得到预测结果 y y y
        P ( y ∣ x 1 , ⋯   , x m ) = s o f t m a x ( h l m W y )   P(y|x^{1},\cdots,x^{m})=softmax(h_{l}^{m}W_{y})\ P(yx1,,xm)=softmax(hlmWy) 
        其中 W y W_{y} Wy为全连接层的参数。有监督的目标则是最大化下式的值:
        L 2 ( C ) = ∑ x , y P ( y ∣ x 1 , ⋯   , x m )   L_{2}(\mathcal{C})=\sum_{x,y}P(y|x^{1},\cdots,x^{m})\ L2(C)=x,yP(yx1,,xm) 
        注意:这里的 h l m h^m_l hlm是每一个词对应的Decoder输出拼接起来的, h l m = { h l < 1 > , ⋯   , h l < m > } h^m_l=\{h^{<1>}_l,\cdots,h^{<m>}_l\} hlm={hl<1>,,hl<m>} , h l < i > ,h^{<i>}_l ,hl<i>对应 x i x^{i} xi的嵌入表示。
         
        GPT-1的实验中发现,加入语言模型学习目标作为辅助任务,也就是损失函数中加入 L 1 L_{1} L1能带来两点好处:1)提升监督模型的泛化能力;2)加快收敛;因此,最终的优化目标如下( λ \lambda λ一般取0.5):
        L 3 ( C ) = L 2 ( C ) + λ L 1 ( C )   L_{3}(\mathcal{C})=L_{2}(\mathcal{C})+\lambda L_{1}(\mathcal{C})\ L3(C)=L2(C)+λL1(C) 

      • 下游任务:GPT-1论文中给出了四个下游适配任务,分别是文本分类、自然语言推理、问答、语义相似度,同时给出了针对这四个任务,如何进行针对性的微调。这四个任务虽然本质上都是属于自然语言理解的文本分类任务,但是GPT-1的结构是很适配做自然语言生成任务的。下面我们介绍下GPT-1如何在上述四个任务上进行微调,如下图所示。

        • 分类任务:将起始和终止token加入到原始序列两端,输入transformer中得到特征向量,最后经过一个全连接得到预测的概率分布;
        • 自然语言推理:将前提(premise)和假设(hypothesis)通过分隔符(Delimiter)隔开,两端加上起始和终止token。再依次通过transformer和全连接得到预测结果;
        • 语义相似度:输入的两个句子,正向和反向各拼接一次(由于相似性质是对称的,为了消除顺序的影响),然后分别输入给transformer,得到的特征向量拼接后再送给全连接得到预测结果;
        • 问答和常识推理:将个选项的问题抽象化为个二分类问题,即每个选项分别和内容进行拼接,然后各送入transformer和全连接中,最后选择置信度最高的作为预测结果。在这里插入图片描述

          这里我们同样通过一个文本分类的例子,来介绍下GPT-1在下游任务上是如何微调的。例如下游任务是情感文本分类任务,包括喜、怒、哀、惧、其他五个类别,其中一个样本是【今天很开心】,真实标签是【喜】。通过前面的介绍,我们知道GPT-1在下游任务进行微调时,损失函数包含两部分,一部分是与预训练保持一致的下一个词预测损失,这部分就不介绍了。另一部分是分类损失,对于分类任务来说,我们最终也会获取到GPT-1最后一层的向量表征 h l ∈ R m × d h_{l}\in R^{m\times d} hlRm×d,其中 m m m是token数量,这个例子中就是5, d d d是模型维度,GPT-1中就是768, l l l是模型层数;接下来, h l h_{l} hl的最后一行再经过一个全连接层(注意,预训练任务是 h l h_{l} hl整体都要经过全连接层,我们这里只需用到最后一个token,即图片中的Extract对应的向量表征),生成 z l ∈ R c z_{l}\in R^{c} zlRc,其中 c c c是类别数目;最后, z l z_{l} zl会经过softmax操作,获取【今天很开心】这段文本对应的每一个类别的概率值,我们的期望是【喜】的概率值要尽可能的大,也就是 z l z_{l} zl的第一个元素的值要尽可能大,这也就是我们的优化目标。

    • GPT-1特点

      • 优点:特征抽取器使用了强大的Transformer,能够捕捉到更长的记忆信息,且较传统的RNN更易于并行化;transformer的并行化可以参考浅析Transformer训练时并行问题
      • 缺点:GPT-1最大的问题就是传统的语言模型是单向的。
    • GPT-1与ELMo,Bert的区别

      • GPT-1与ELMo的区别
        • 模型架构不同:ELMo是浅层的双向RNN;GPT-1是多层的Transformer decoder;
        • 针对下游任务的处理不同:ELMo将词嵌入添加到特定任务中,作为附加功能;GPT则针对所有任务微调相同的基本模型。
      • GPT-1与Bert的区别
        • 预训练:GPT-1预训练的方式和传统的语言模型一样,通过上文,预测下一个单词;Bert会同时利用上下文的信息;
        • 模型效果:GPT-1因为采用了传统语言模型所以更加适合用于自然语言生成类的任务 (NLG),因为这些任务通常是根据当前信息生成下一刻的信息。而Bert更适合用于自然语言理解任务 (NLU)。当然这是之前的说法,现在chatgpt出来以后哪个更适合NLU任务还真不一定。GPT-1的模型参数为L=12,H=768,A=12,这个设置和后来Bert-base一模一样,但后者的效果要好上很多。原因之一是,GPT-1采用Mask-Attention结构,对模型和训练数据的要求会更高,因为模型能读到的信息只有上文。而采用普通attention的Bert在训练阶段就能同时读到上下文。这个性质决定了GPT模型越来越大的趋势。但是,长远来看,Masked-Attention是推动模型更好理解文字的重要手段,毕竟在现实中,我们更希望培养模型知上文补下文,而不是单纯地做完形填空。
        • 模型结构: GPT-1采用了Transformer的Decoder,而Bert采用了Transformer的Encoder。
    • GPT-1的数据集:GPT-1使用了BooksCorpus数据集,这个数据集包含7000本没有发布的书籍。作者选这个数据集的原因有二:1)数据集拥有更长的上下文依赖关系,使得模型能学得更长期的依赖关系;2)这些书籍因为没有发布,所以很难在下游数据集上见到,更能验证模型的泛化能力。

  • GPT-2:我们知道,GPT-1和Bert的训练都是分两步走:pre-training + supervised fine-tuning。这套方法的缺点:

    • 虽然借助预训练这一步提升性能,但是本质上还是需要有监督的Fine-Tuning才能使得模型执行下游任务;
    • 需要在下游任务上面有标注的数据。当我们只有很少量的可用数据 (即zero-shot的情况下) 时就很难有很好的效果。
       

      另外,在Bert模型提出之后,Encoder vs Decoder,Bert vs GPT-1,两两之间的比较就开始了,但是此时GPT-1仍处在劣势。Bert提出之后,除了生成任务外,NLP任务的范式基本就是Bert的预训练+Fine-Tuning了。OpenAI放弃了吗?并没有!我们知道,基于Decoder的模型,模型和数据量越大,效果越好。但OpenAI如果只做到这一点,从技术上来说又太逊色了,性价比也不高。因此,OpenAI从训练数据上进行改进,引入了zero-shot这一创新点,GPT-2就诞生了《Language Models are Unsupervised Multitask Learners》
      论文中认为现在的训练方式训练出来的模型只能算是一个小任务上的专家系统,而且还都不够鲁棒。造成这个问题的原因是模型都是在单一领域内的单一任务上进行训练的,缺乏泛化性。跟人一样,见识和知识太少时,就很难对事情有全面的了解。要解决这个问题,一个可行的思路是多任务学习,而且是大量不同领域的不同任务。但是,这样的多任务学习是有监督的训练,需要大量的数据,这个就比较难实现了。
      GPT-2在GPT-1的基础上,提出了新的发展思路来解决这个问题。简单来说,GPT-2的思路就是充分相信语言模型,不再对下游任务进行Fine-Tuning或者增加任务头了,就用预训练的语言模型来解决所有任务,直接做zero-shot的任务。具体来说,就是上高质量的大数据,堆叠更多的参数,不同任务改造成生成任务。
      GPT-2本质上还是一个语言模型,但是不一样的是,它证明了语言模型可以在 zero-shot 的情况下执行下游任务,也就是说,GPT-2在做下游任务的时候可以无需任何标注的信息,也无需任何参数或架构的修改。后来的GPT-3也是沿用了这个思路,这个时候,已经可以看出一些ChatGPT的影子了。

    • 模型结构:GPT-2的模型在GPT-1的基础上做了一些改进,如下:

      • 结构变化:对于每个sub-block:第一个layer norm层移到sub-block的输入部分,也就是attention之前,第二个layer norm层移到feed forward之前;对于整体模型架构,在最后一个sub-block后再加一个layer norm层;
      • 权重变化:采用一种改进的初始化方法,该方法考虑了残差路径与模型深度的累积。在初始化时将residual layers的权重按 N \sqrt{N} N 的因子进行缩放,其中 N N N是residual layers的数量;
        • 备注:这个改动其实没太看懂,residual layers就是一个相加操作,怎么会有参数呢?查阅了很多资料,源码也看了GPT-2,没看到权重缩放的流程。在此给一个本人的见解:根据这个操作的目的可知,是为了防止随着模型深度的累积,残差越加越大,因此认为这里的缩放指的是每次进行残差操作之前(即将输入和输出进行相加之前),先将输入进行缩放,缩放因子跟当前是整体结构的第几层有关,层数越大,累积的越大,所以应该缩放的越多。比如现在是整体结构的第五层,那么缩放因子 N N N就是5。上述内容只是本人的一个想法,如有大神更了解其中的原理,欢迎交流指正。);
      • 词表变化:词表大小设置为50257;
      • 输入变化:无监督预训练可看到的上下文的context由512扩展为1024;
      • 批次变化:训练时,batchsize大小从64调整为512。
        在这里插入图片描述

        论文给了不同层数的模型,最大的模型称为GPT-2模型,参数有1.5B;最小的即是GPT-1,对标Bert-base;倒数第二小的对标Bert-large。不同模型大小如下:在这里插入图片描述

    • 模型训练:GPT-2只有预训练过程。

      • 无监督训练:GPT-2的训练方式和GPT-1的训练方式相比,两者都是有预训练过程的,不过GPT-2只有预训练过程,在下游任务中,不采用Fine-Tuning方法,而是采用论文中提到的zero-shot方法。GPT-2采用这种模式,归因于GPT-2提出的核心思想:当一个语言模型的容量足够大数据量足够丰富时,它就足以覆盖所有的有监督任务,也就是说所有的有监督学习都是无监督语言模型的一个子集,仅仅靠训练语言模型便可以完成其他有监督学习的任务。
      • 下游任务:GPT-2如何让模型做下游任务呢?首先前文提到,GPT-2在大规模无监督训练过程学习到了任务相关的信息。作者是这么认为的:比如下游任务是英文翻译法文,那么如果模型在无监督预训练的过程中看过相关的文字 (例如"Mentez mentez, il en restera toujours quelque chose," which translates as, "Lie lie and something will always remain."这句话是训练的语料),那么模型就能够学会 (translate to french, english text, french text) 这样的下游任务。也就是说,原则上,通过大量的语料训练,语言建模能够学习到一系列下游任务,而不需要明确的监督信息。为什么可以这么讲呢?因为作者认为:下游任务 (有监督训练) 可以视为预训练过程 (无监督训练) 的一个子集。无监督目标的全局最优解也是有监督训练的全局最优解。当预训练规模足够大时,把无监督的任务训练好了,有监督的下游任务即不再需要额外训练,就是所谓的zero-shot。所以下面的问题就变成了:在实践中,我们如何能够优化无监督预训练过程以达到收敛。初步实验证实,足够大的语言模型能够在无监督的预训练过程之后做下游任务,但学习速度比显式监督方法慢得多。那么最后一个问题就是具体怎么去做下游任务呢?以英文翻译法文为例,我们需要在下游任务时预先告诉模型 “translate English to French”,即给模型一个提示 (Prompt)。
    • GPT-2的数据集:许多之前的工作是在单个文本域上训练语言模型,例如新闻文章,维基百科或小说等等。GPT-2则是希望使得训练数据集的领域和上下文更多一点。在网站上爬取文本是一个方案,比如说Common Crawl网站。虽然这些网站手机的数据集在量级上很大,但它们存在严重的数据质量问题,这上面的内容有很多是信噪比很低的,难以理解的内容。为了解决数据集质量的问题,GPT-2只爬取人类过滤之后的网页。但是,手动过滤的网络爬取很昂贵,所以GPT-2从社交媒体平台Reddit 上抓取了至少收到了3个karma的链接。karma可以被认为是一种启发式指标,用于判断其他用户是否认为该链接有趣、有教育意义或只是有趣。得到的这个数据集称之为WebText,是一个包含了4500万个链接的文本数据集。经过重复数据删除和一些基于启发式的清理后,它包含略多于800万个文档,总文本容量为40GB。作者从WebText中删除了所有维基百科文档,因为它可能涉及到 test evaluation tasks。目前全量的数据是没有开放下载的,可通过GPT-2训练数据集下载部分训练数据。

    • GPT-2特点

      • 优点:GPT-2相对GPT-1模型的亮点是支持zero-shot的设置,同时在zero-shot的多任务学习场景中展示出不错的性能。GPT-2首先构造了一个新的数据集:WebText,它是一个有百万级别文本的数据集。GPT-2自己是一个有着1.5B参数量的模型;GPT-2提出了新的NLP范式,强调通过更多的高质量训练数据训练高容量语言模型,从而无监督完成下游多任务。尝试以一种通用的语言模型的方法,去解决现有的大部分NLP任务;
      • 缺点:GPT-2在模型本身上没啥大的变化和创新。
  • GPT-3:GPT-2的最大贡献是验证了通过海量数据和大量参数训练出来的词向量模型有迁移到其它类别任务中而不需要额外的训练。但是很多实验也表明,GPT-2的无监督学习的能力还有很大的提升空间,甚至在有些任务上的表现不比随机的好。尽管在有些zero-shot的任务上的表现不错,但是我们仍不清楚GPT-2的这种策略究竟能做成什么样子。GPT-2表明随着模型容量和数据量的增大,其潜能还有进一步开发的空间,基于这个思想,诞生了我们下面要介绍的GPT-3《Language Models are Few-Shot Learners》
      GPT-2在GPT-1的基础上往前走了一大步:完全抛弃了微调,并采用了zero-shot的方式。Zero-shot的方式被GPT-2认证可行后,OpenAI就不得不开始考虑模型是否能真正做到强大了,毕竟现在只是和Bert持平而已。这一刻OpenAI开始悟过来,既然LLM要一路走到底,既然模型变大避免不了,那不如来得更彻底一些。GPT-3沿用了去除Fine-Tuning,只做通用语言模型的思路,同时技术上小做替换(sparse Transformer);对于下游任务,在不做微调的前提下采用了few-shot的方式(毕竟完全不给模型任何显性提示,效果确实没达到预期)。最终生成了一个大小高达175B的大模型,当然效果也是一骑绝尘的。

    • 模型结构:GPT-3的模型与GPT-2的模型基本一致,主要改进只有一点:

      • Sparse Attention:在模型结构中的注意力层,GPT-3采用Sparse Transformer中的Sparse Attention方案,sparse attention与传统self-attention(称为dense attention)的区别在于:

        • dense attention:每个 token 之间两两计算attention,复杂度 O ( n 2 ) O(n^{2}) O(n2)
        • sparse attention:每个token只与其他token的一个子集计算attention,复杂度 O ( n ∗ l o g n ) O(n*logn) O(nlogn)
           

        具体来说,sparse attention除了相对距离不超过 k k k以及相对距离为 k , 2 k , 3 k , ⋯ k,2k,3k,\cdots k2k3k的token,其他所有token的注意力都设为0,如下图所示:在这里插入图片描述
        我们来具体观察一下,实际上图中的第二行就是涉及到的attention的token内容,可以看出首先关注了附近四个token,其次是 2 k , 3 k 2k,3k 2k,3k距离的token,那么为什么这么做呢?使用 sparse attention的好处主要有以下两点:

        • 减少注意力层的计算复杂度,节约显存和耗时,从而能够处理更长的输入序列;
        • 具有“局部紧密相关和远程稀疏相关”的特性,对于距离较近的上下文关注更多,对于距离较远的上下文关注较少。
           
          关于Sparse Transformer的详细介绍可以参见OpenAI于2019年发表的论文《Generating Long Sequences with Sparse Transformers》
           

        论文中供训练了8个不通规模的模型,最大的一个称作为GPT-3:在这里插入图片描述

    • 模型训练:GPT-3也只有预训练过程。

      • 无监督训练:GPT-3仍采用GPT-2提出的仅做预训练、不做微调的思路。GPT-3采用了In-context learning。借用meta-learning(元学习)的思想,在pre-training期间让模型学习广泛的技能和模式识别能力,而在推理期间利用这些技能和能力迅速适配到期望的任务上。在之前的章节中,我们已经介绍过In-context learning,下面简单介绍下GPT-3中的In-context learning。
          In-context learning是这篇论文中介绍的一个重要概念,要理解In-context learning,我们需要先理解meta-learning(元学习)。对于一个少样本的任务来说,模型的初始化值非常重要,从一个好的初始化值作为起点,模型能够尽快收敛,使得到的结果非常快的逼近全局最优解。元学习的核心思想在于通过少量的数据寻找一个合适的初始化范围,使得模型能够在有限的数据集上快速拟合,并获得不错的效果。
          这里的介绍使用的是MAML(Model-Agnostic Meta-Learning)算法,正常的监督学习是将一个批次的数据打包成一个batch进行学习。但是元学习是将一个个任务打包成batch,每个batch分为支持集(support set)和质询集(query set),类似于学习任务中的训练集和测试集。
          对一个网络模型 f f f,其参数表示为 θ \theta θ,它的初始化值被叫做meta-initialization。MAML的目标则是学习一组meta-initialization,能够快速应用到其它任务中。MAML的迭代涉及两次参数更新,分别是内循环(inner loop)和外循环(outer loop)。内循环是根据任务标签快速的对具体的任务进行学习和适应,而外学习则是对meta-initialization进行更新。直观的理解,我用一组meta-initialization去学习多个任务,如果每个任务都学得比较好,则说明这组meta-initialization是一个不错的初始化值,否则我们就去对这组值进行更新。
          GPT-3中介绍的In-context learning则是元学习的内循环,基于语言模型的SGD则是外循环,如下图所示。
        在这里插入图片描述

      • 下游任务:在训练阶段,预训练通用的语言模型,使模型能够具备识别不同NLP任务的能力,此时模型具备了一定的ICL能力。而在推理阶段,依赖于模型的ICL能力,针对各NLP任务,向模型中输入特定上下文,上下文包括任务描述、若干个任务样本和任务提示,模型根据上下文进行推理给出任务输出。根据上下文包含的任务样本数量可进一步将上下文学习分为Zero-Shot(无任务样本)、One-Shot(仅一个任务样本)和Few-Shot(多个任务样本)三类。

        • Fine-Tunning(FT):FT利用成千上万的下游任务标注数据来更新预训练模型中的权重以获得强大的性能。但是,该方法不仅导致每个新的下游任务都需要大量的标注语料,还导致模型在样本外预测的能力很弱。虽然GPT-3从理论上支持FT,但论文中没这么做;
        • Few-Shot(FS):模型在推理阶段可以得到少量的下游任务示例作为限制条件,但是不允许更新预训练模型中的权重。FS过程的示例可以看下图中整理的案例。FS的主要优点是并不需要大量的下游任务数据,同时也防止了模型在Fine-Tuning阶段的过拟合。FS的主要缺点是不仅与Fine-Tuning的SOTA模型性能差距较大且仍需要少量的下游任务数据;
        • One-Shot(1S):模型在推理阶段仅得到1个下游任务示例。把1S独立于Few-Shot和Zero-Shot讨论是因为这种方式与人类沟通的方式最相似;
        • Zero-Shot(0S):模型在推理阶段仅得到一段以自然语言描述的下游任务说明。0S的优点是提供了最大程度的方便性、尽可能大的鲁棒性并尽可能避免了伪相关性。0S的方式是非常具有挑战的,即使是人类有时候也难以仅依赖任务描述而没有示例的情况下理解一个任务。但毫无疑问,0S设置下的性能是最与人类的水平具有可比性的。
          在这里插入图片描述
    • GPT-3的数据集:GPT-3的训练数据包括低质量的Common Crawl,高质量的WebText2、Books1、Books2和Wikipedia。GPT-3根据数据集的不同质量赋予了不同的权值,权值越高的在训练的时候越容易抽样到(见下图)。为了清理脏数据,OpenAI做了以下的数据处理:

      • 使用高质量数据作为正例,训练LR分类算法,对 CommonCrawl 的所有文档做初步过滤;
      • 利用公开的算法做文档去重,减少冗余数据;
      • 加入已知的高质量数据集;

      最终处理完成后使用的数据规模约570G。
      在这里插入图片描述
      如上图所示,在实际实验过程中,对不同数据集按照一定的比例进行采样,这个比例不是按照原始数据量多少来划分的,不然这里基本采样到的就都是Common Crawl的数据了,可以看到这里Common Crawl的数据量比其他几个多很多。进行采样的原因主要考虑到,就算做了一些数据清洗还是觉得Common Crawl的数据质量不如其他几个。最终采样的时候,虽然Common Crawl的数据量是其他几个数据集的上百倍,但是实际占比是60%,有40%的数据是能够保证质量的。

    • GPT-3的特点

      • 优点:GPT-3的强大之处在于它的泛化能力。不需要微调,只需要在输入序列里用自然语言表述任务要求,就可以让模型执行不同的子任务。GPT-3在部分任务上达到或超过了SOTA,并且验证了模型规模越大、任务效果越好,且大部分情况下,GPT-3的Few-Shot优于One-Shot和Zero-Shot。在这里插入图片描述

      • 缺点

        • 生成的内容存在重复或不合理的句子、段落,缺乏常识,在一些任务上表现一般,甚至和随机判断差不多;
        • 模型结构使用的Transformer解码器是一个单向自回归语言模型,所以在一些需要双向理解的NLP任务(比如文本蕴含)上表现不佳;
        • 语言模型底层原理还是根据前序词元预测下一个词元,没有考虑不同词元的权重;
        • 模型规模太大,计算资源成本较高,后续的一个方向是针对特定任务对模型进行知识蒸馏;
        • 和其他深度学习模型一样,模型的可解释性不强;
        • 此外,作者还展望了一下GPT-3可能带来的社会影响。比如它可能被拿来生成假新闻、垃圾邮件,以及论文造假。由于GPT-3 的训练数据来自网络,其中包含了一些性别、宗教、种族歧视的信息,导致GPT-3生成的文本也有同样的问题。

1.2 InstructGPT

  GPT-3虽然在各大NLP任务以及文本生成的能力上令人惊艳,但是他仍然还是会生成一些带有偏见的,不真实的,有害的造成负面社会影响的信息,而且很多时候,他并不按人类喜欢的表达方式去说话。在这个背景下,OpenAI提出了一个概念“Alignment”,意思是模型输出与人类真实意图对齐,符合人类偏好。因此,为了让模型输出与用户意图更加对齐,就有了InstructGPT这个工作《Training language models to follow instructions with human feedback》。InstructGPT提出了一个理想化语言模型的三大目标:helpful(能帮助用户解决问题)、honest(不能捏造事实,不能误导用户)、harmless(不能对用户或环境造成物理、精神、社会层面的伤害)。
  为了实现上述的目标,论文提出了一种基于人类反馈来微调语言模型的方法,使其能够更好地遵循用户的指示,并在各种任务上表现出更高的质量和可信度。基本流程分为以下三个步骤:

  • 步骤一:从OpenAI API中获取用户提交的指令prompt(后面提到的指令prompt都可理解为问题)和标注人员编写的指令prompt中收集了一个数据集,从收集到的指令prompt数据集中取出一些指令prompt,然后让标注人员标注对应的答案,再用这些数据微调GPT-3得到SFT模型;
  • 步骤二:输入指令prompt,让模型输出几个答案,然后让标注人员对答案进行排序,用这些排序数据训练一个奖励模型RM,能够对答案进行打分,打分的大小顺序满足训练使用的这些答案的顺序;
  • 步骤三:再输入一些指令prompt让STF模型去生成一些答案,把答案放到RM模型里面去打分,然后用PPO算法去优化SFT模型的参数使得它生成更高的分数,最后得到InstrctGPT。

  最终得到的InstrctGPT相较于GPT-3:

  • 可以更好地理解用户指示中隐含或显式地表达出来的目标、约束和偏好,并生成更符合用户期望和需求的输出;
  • 可以更有效地利用提示中提供的信息或结构,并在需要时进行合理推断或创造。
  • 可以更稳定地保持输出质量,并减少错误或失败率;

  下面详细介绍下InstructGPT数据集构建以及训练流程。

  • InstructGPT数据集构建:InstructGPT数据集构建可以分为三个阶段。第一个阶段是为了构建初始的指令prompt数据集,具体做法是让标注人员构建下面三种prompt:

    • Plain:只要求标注人员构建出一个任意的任务(也就是指令promot),并保证任务有足够的多样性;
    • Few-shot:要求标注人员构建出一个指令prompt,并给出多个符合该指令的query/response组合;
    • User-based:基于用户期望OpenAI API俱备的能力所提出的一些用例,要求标注人员构建出与这些用例相关的指令prompt。
       

      基于上面三种指令prompt,OpenAI团队训练了初始版本的InstructGPT模型,然后将这个InstructGPT模型放到Playground(Playground可理解为测试API,非生产API)里供用户使用,这就引申至InstructGPT数据集构建的第二个阶段:用户在使用过程中,会继续问一些问题,OpenAI团队将这些问题收集回来,并进行过滤等操作,具体来说,将每个用户ID的对应的指令prompt数量限制为200个,同时过滤掉个人信息,并根据用户ID拆分训练、验证和测试集(同一个用户问题会比较类似,不适合同时出现在训练集和验证集中)。可以看出,第一阶段和第二阶段是一个循环过程:先拿部分数据训练模型,然后通过模型获取新数据,再用新数据继续优化模型,这种思路也很适合我们以后的模型训练过程。
      至此,通过上述两阶段的处理,OpenAI团队已经获取了一定量的指令prompt(包括标注人员构建的prompt以及从用户侧收集的prompt),接下来即是针对不同训练任务构建不同的数据集,也就是第三阶段。基于第二阶段获取的指令prompt,构建三个数据集,分别用于后续的三个训练任务SFT、RM、RL。

    • SFT Dataset:标注人员根据指令prompt构造答案,将prompt和答案拼在一起,形成一段对话(prompt,answer),用于训练SFT模型。此部分数据量大约13k,包括人工标记prompt+用户收集prompt
    • RM Dataset:先将prompt输入SFT模型,标注人员再对SFT模型输出的答案进行排序,然后用这些排序数据(prompt,Rank)训练一个奖励模型RM,能够对答案进行打分。此部分数据量大约33k,包括人工标记prompt+用户收集prompt
    • RL Dataset:此部分数据集不需要标注,只需要从指令prompt数据集里面获取部分指令prompt,然后使用SFT和RM模型分别得到answer和RM给出的分数,构成三元组(prompt,answer,RM给出的分数),用于进一步采用PPO算法微调SFT模型。此部分数据量大约31k,只包括用户收集prompt
      在这里插入图片描述

      SFT Dataset和RM Dataset都需要人工标注,区别在于前者的生成式的标注要比后者的判别式的标注贵很多,同样的标注时间和成本,联合前者和后者得到的数据要比只用前者得到的数据多很多,在这上面训练出来的模型性能可能会好一些。

  • InstructGPT训练流程:上文已经介绍,关于InstructGPT的训练流程,论文中分为了三个步骤:有监督微调,奖励模型训练,强化学习训练,如下图所示。实际上可以把它拆分成两种技术方案,一个是有监督微调(SFT),一个是基于人类反馈的强化学习(RLHF),下面我们简单介绍这两种技术方案。
    在这里插入图片描述

    • 有监督微调(SFT): 以GPT-3模型为底座,在标注好的第一个数据集(问题+答案)上进行训练。具体来说,迭代轮数使用16个epoch,学习率使用余弦衰减,模型残差连接dropout率为0.2。由于只有13000个数据,1个epoch就过拟合,不过论文中证明了这个模型过拟合也没什么关系,甚至训练更多的epoch对后续是有帮助的,最终训练了16个epoch。
    • 基于人类反馈的强化学习(RLHF):此部分包含两个阶段,第一个阶段是RM模型训练,第二阶段是利用PPO算法继续微调SFT模型,两阶段相结合,即是RLHF过程。
      • 奖励模型(RM):模型结构是把SFT模型最后的unembedding层(就是将模型输出的token embedding转换为logits的那一层)去掉,即最后一层不用softmax,改成一个线性层,这样训练好的RM模型就可以做到输入问题+答案,输出一个标量的分数。RM模型使用6B,而不是175B,主要原因是:

        • 节省计算,更便宜;
        • 大模型175B-RM不稳定(大模型的通病,模型参数很多,很难收敛),因此不太适合在RL期间用作值函数。
           

        前文已经介绍,RM数据集在标注阶段,标注人员被要求对每一个prompt下的不同回答进行排序。如下图,某个prompt下有A、B、C三个回答,标注人员认为A>B>C。在训练阶段,假设一个prompt下有K个回答,则两两回答一组,组成一条训练数据,例如(prompt, A, B),则一共有 C k 2 C_{k}^{2} Ck2条训练数据。这些训练数据将组成一个batch,通过构造并最小化Pairwise Ranking Loss的方法,来训练奖励模型,整体过程如下:先以RM Dataset中的指令prompt作为输入,通过第一阶段微调好的SFT模型,生成K个不同的回答,形成<prompt,answer1>,<prompt,answer2>….<prompt,answerK>数据。然后,标注人员根据相关性、信息性和有害信息等标准,对K个结果进行排序,生成排序结果数据。接下来,使用这个排序结果数据进行pair-wise learning to rank模式进行训练,训练RM模型。RM模型接受一个输入<prompt,answer>,给出评价回答质量高低的奖励分数score。对于一对训练数据<answer1,answer2>,假设人工排序中answer1排在answer2前面,那么loss函数则鼓励RM模型对<prompt,answer1>的打分要比<prompt,answer2>的打分要高。在这里插入图片描述
        接下来我们根据模型的损失函数Pairwise Ranking Loss,详细了解下RM模型的训练过程。Pairwise Ranking Loss表达式如下所示:
        l o s s ( θ ) = − 1 C k 2 E ( x , y w , y l ) ∼ D [ l o g ( σ ( r θ ( x , y w ) − r θ ( x , y l ) ) ) ]   loss(\theta)=-\frac{1}{C_{k}^{2}}E_{(x,y_{w},y_{l})\sim D}[log(\sigma(r_{\theta}(x,y_{w})-r_{\theta}(x,y_{l})))]\ loss(θ)=Ck21E(x,yw,yl)D[log(σ(rθ(x,yw)rθ(x,yl)))] 
        其中,

        • x x x表示某个prompt;
        • y w y_{w} yw y l y_{l} yl分别表示该prompt下的任意一对回答,并且假设标注中 y w y_{w} yw的排序是高于 y l y_{l} yl的;
        • D D D表示该prompt下人类标注排序的所有两两回答组合;
        • r θ r_{\theta} rθ表示奖励模型;
        • σ \sigma σ表示 s i g m o i d sigmoid sigmoid函数。
           

        也可以参考下图(有个小错误,就是 s i g m o i d sigmoid sigmoid函数是将值映射至 ( 0 , 1 ) (0,1) (01),而不是 ( − 1 , 1 ) (-1,1) (11),不过无伤大雅)。
        在这里插入图片描述
         
        论文中期望当回答 y y y的排序相对较高时, r θ ( x , y ) r_{\theta}(x,y) rθ(x,y)的得分也能越高。为了不让K的个数影响训练模型,论文中在前面乘上 1 C k 2 \frac{1}{C_{k}^{2}} Ck21,将loss平均到每一个答案组合上。除此之外,还有几点需要我们注意:

        • K值的选择:论文中采用了K=9,而不是更小的值,比如4。原因在于:
          • 进行标注的时候,需要花很多时间去理解问题,但答案和答案比较相近,对9个答案做排序相较于对4个答案做排序多花的时间不到一倍。同时K=9生成的问答对是K=4的6倍( C 9 2 {C_{9}^{2}} C92=36, C 4 2 {C_{4}^{2}} C42=6),非常划算;
          • K=9时,每次计算RM模型的loss时需要都有36项 r θ ( x , y ) r_{\theta}(x,y) rθ(x,y)要计算,这个计算比较贵,但可以通过重复利用之前算过的值,使得只要计算9次就行,也就是说将9个答案对应的 r θ ( x , y ) r_{\theta}(x,y) rθ(x,y)计算出来之后,后面计算损失时,两两组合就可以了,这样就可以省下很多时间。
        • 训练数据输入模式选择:论文中将每个prompt的 C K 2 {C_{K}^{2}} CK2个响应对当成一个batch同时送入模型,而不是将单条 ( x , y w , y l ) (x,y_{w},y_{l}) (x,yw,yl)数据分别送入模型,原因在于:
          • 为了避免过拟合。这种按照同一个prompt作为一个batch的训练方式要比传统的按样本为batch的方式更不容易过拟合,因为这种方式每个prompt会且仅会输入到模型中一次;
          • 为了提升计算效率。在模型forward的过程中,最耗时的步骤是计算 r θ ( x , y ) r_{\theta}(x,y) rθ(x,y)。用batch方式时,该计算只需执行K次(因为模型参数没有更新,相同的(x, y)可以重复使用);采用单条方式时,需要计算 C K 2 {C_{K}^{2}} CK2次(因为一条计算更新一次模型,模型参数更新,相同的(x,y)需要重新计算 r θ ( x , y ) r_{\theta}(x,y) rθ(x,y))。因此,K越大时,采用batch的方式越划算,它在保证相对排序信息丰富的同时,又节省了计算效率。
        • 训练epoch选择:模型训练超过一个epoch后会过拟合,故只训练一个epoch。
      • 强化学习模型(RL):这个阶段先将RL模型的权重初始化为SFT模型的权重,然后通过改良后的PPO算法(PPO-ptx算法)继续对RL模型进行优化,最终得到InstructGPT。强化学习的大致流程可以总结为:模型在做出行动后,需要人来对模型进行反馈,然后模型做出对应的更新。具体来说,论文中训练RM就是为了学习人来对模型进行反馈,SFT模型在拿到prompt并生成对应的答案后,由RM进行打分,再根据这个打分去更新模型,然后用更新的模型生成新的答案,并进行下一步学习,这就是强化学习的过程。强化学习的目标函数 o b j e c t i v e ( ϕ ) objective(\phi) objective(ϕ)如下所示,RL模型最终的训练目标是让 o b j e c t i v e ( ϕ ) objective(\phi) objective(ϕ)越大越好。
        o b j e c t i v e ( ϕ ) = E ( x , y ) ∼ D π ϕ R L [ r θ ( x , y ) − β l o g ( π ϕ R L ( y ∣ x ) / π S F T ( y ∣ x ) ) ] + γ E x ∼ D p r e t r a i n [ l o g ( π ϕ R L ( x ) ) ] \begin{aligned} objective(\phi)=&E_{(x,y)\sim D_{\pi_{\phi}^{RL}}}[r_{\theta}(x,y)-\beta log(\pi_{\phi}^{RL}(y|x)/\pi^{SFT}(y|x))]+\\ &\gamma E_{x\sim D_{pretrain}}[log(\pi_{\phi}^{RL}(x))] \end{aligned} objective(ϕ)=E(x,y)DπϕRL[rθ(x,y)βlog(πϕRL(yx)/πSFT(yx))]+γExDpretrain[log(πϕRL(x))]
        其中:

        • π S F T \pi^{SFT} πSFT:即第一阶段,经过supervised fine-tuning的GPT-3模型,也就是SFT模型;
        • π ϕ R L \pi_{\phi}^{RL} πϕRL:强化学习中,模型称做policy, π ϕ R L \pi_{\phi}^{RL} πϕRL就是需要学习的模型,即最终的模型。初始时: π ϕ R L \pi_{\phi}^{RL} πϕRL= π S F T \pi^{SFT} πSFT
        • r θ r_{\theta} rθ:即第二阶段训练的RM模型。
           

        整体的目标是最大化上述的目标函数,现在分别介绍下目标函数的每一项,也可以参考下面的图片:

        • ( x , y ) ∼ D π ϕ R L (x,y)\sim D_{\pi_{\phi}^{RL}} (x,y)DπϕRL x x x是第RL Dataset数据集中的问题(指令prompt), y y y x x x通过 π ϕ R L \pi_{\phi}^{RL} πϕRL模型得到的答案;
        • r θ ( x , y ) r_{\theta}(x,y) rθ(x,y):对问题 x x x+答案 y y y,输入RM模型进行打分,目标是希望这个分数越高越好;
        • π ϕ R L ( y ∣ x ) \pi_{\phi}^{RL}(y|x) πϕRL(yx):问题 x x x通过 π ϕ R L \pi_{\phi}^{RL} πϕRL得到答案 y y y的概率,具体来说 π ( y ∣ x ) \pi(y|x) π(yx)是把模型输出 y y y的每一个token对应的softmax概率相乘得到的结果,下同;
        • π S F T ( y ∣ x ) \pi^{SFT}(y|x) πSFT(yx):问题 x x x通过 π S F T \pi^{SFT} πSFT得到答案 y y y的概率;
        • l o g ( π ϕ R L ( y ∣ x ) / π S F T ( y ∣ x ) ) log(\pi_{\phi}^{RL}(y|x)/\pi^{SFT}(y|x)) log(πϕRL(yx)/πSFT(yx)):KL散度,取值范围>=0,用于比较两个模型的输出分布是否相似,KL值越大,分布越不相似,分布相同时KL=0。在本阶段,论文中希望强化学习后得到的模型,在能够理解人类意图的基础上,又不要和最原始的模型输出相差太远(在每次更新参数后, π ϕ R L \pi_{\phi}^{RL} πϕRL会发生变化, x x x通过 π ϕ R L \pi_{\phi}^{RL} πϕRL生成的 y y y也会发生变化,而 r θ r_{\theta} rθ打分模型是根据 π S F T \pi^{SFT} πSFT模型的数据训练而来,如果 π ϕ R L \pi_{\phi}^{RL} πϕRL π S F T \pi^{SFT} πSFT差的太多,则会导致 r θ r_{\theta} rθ的分数输出不准确。因此需要通过KL散度来计算 π ϕ R L \pi_{\phi}^{RL} πϕRL生成的答案分布和 π S F T \pi^{SFT} πSFT生成的答案分布之间的距离,使得两个模型之间不要差的太远。)。参数 β \beta β则表示对这种偏差的容忍程度。偏离越远,就要从奖励模型的基础上得到越多的惩罚;
        • x ∼ D p r e t r a i n x\sim D_{pretrain} xDpretrain x x x是来自GPT-3预训练模型的数据;
        • l o g ( π ϕ R L ( x ) ) log(\pi_{\phi}^{RL}(x)) log(πϕRL(x)):表示将来自初始GPT-3中的数据送入当前强化模型下,同样,论文中希望在训练得到新模型之后,不能降低在原始任务上的能力,即不能太偏离原始任务,保证新模型的泛化性。 γ \gamma γ则是对这种偏离的惩罚程度。
          在这里插入图片描述
           

          最后再给出对目标函数的理解,优化目标是使得上述目标函数越大越好,通过上述介绍,我们知道, o b j e c t i v e ( ϕ ) objective(\phi) objective(ϕ)可分成三个部分,RM打分部分+KL散度部分+GPT-3预训练部分:

        • 将RL Dataset数据集中的问题 x x x,通过 π ϕ R L \pi_{\phi}^{RL} πϕRL模型得到答案 y y y
        • 把一对 ( x , y ) (x,y) (x,y)送进RM模型进行打分,得到 r θ ( x , y ) r_{\theta}(x,y) rθ(x,y),即第一部分打分部分,这个分数越高就代表模型生成的答案越好;
        • 在每次更新参数后, π ϕ R L \pi_{\phi}^{RL} πϕRL会发生变化, x x x通过 π ϕ R L \pi_{\phi}^{RL} πϕRL生成的 y y y也会发生变化,而 r θ ( x , y ) r_{\theta}(x,y) rθ(x,y)打分模型是根据 π S F T \pi^{SFT} πSFT模型的数据训练而来,如果 π ϕ R L \pi_{\phi}^{RL} πϕRL π S F T \pi^{SFT} πSFT差的太多,则会导致 r θ ( x , y ) r_{\theta}(x,y) rθ(x,y)的分数估算不准确。因此需要通过KL散度来计算 π ϕ R L \pi_{\phi}^{RL} πϕRL生成的答案分布和 π S F T \pi^{SFT} πSFT生成的答案分布之间的距离,使得两个模型之间不要差的太远。我们希望两个模型的差距越小越好,即KL散度越小越好,前面需要加一个负号,使得 o b j e c t i v e ( ϕ ) objective(\phi) objective(ϕ)越大越好。这个就是KL散度部分;
        • 如果没有第三部分,那么模型最终可能只对这一个任务能够做好,在别的任务上会发生性能下降。所以第三部分就把原始的GPT-3目标函数加了上去,使得前面两个部分在新的数据集上做拟合,同时保证原始的数据也不要丢,这个就是第三部分GPT-3预训练部分;
        • γ \gamma γ=0时,这个模型叫做PPO,当 γ \gamma γ不为0时,这个模型叫做PPO-ptx。InstructGPT更偏向于使用PPO-ptx;
        • 最终优化后的 π ϕ R L \pi_{\phi}^{RL} πϕRL模型就是InstructGPT的模型。
           

      回顾下InstructGPT的训练流程,共包含两次对模型的微调:GPT-3模型 ⇒ \Rightarrow SFT模型 ⇒ \Rightarrow RL模型,其实这里始终都是同一个模型,只是不同过程中名称不一样。除此之外,在SFT模型 ⇒ \Rightarrow RL模型阶段,还会依赖于另一个在SFT模型基础上训练的RM模型。InstructGPT训练SFT、RM、RL三个模型的原因(参考ChatGPT技术解析):

    • 需要SFT模型的原因:GPT-3模型不一定能够保证根据人的指示、有帮助的、安全的生成答案,需要人工标注数据进行微调;
    • 需要RM模型的原因:标注排序的判别式标注,成本远远低于生成答案的生成式标注;
    • 需要RL模型的原因:让模型借助强化学习的能力,更好的理解人类的意图。
       

      最后,我们展示下论文中提到的InstructGPT性能对比结果,可以发现,参数量为13B的InstructGPT模型,性能都要远远好于参数量为175B的GPT-3模型:在这里插入图片描述

1.3 ChatGPT

  InstructGPT是在GPT-3的基础上通过SFT+RLHF两个阶段训练完成;ChatGPT则是在GPT-3.5的基础上通过SFT+RLHF两个阶段训练完成,显著提升了模型的对话能力。SF和RLHF两个阶段,在InstructGPT章节中我们已经做了详细介绍,这里不做过多赘述。关于GPT-3和GPT-3.5,它们其实是两个模型系列,分别称为GPT-3系列和GPT-3.5系列,下面我们参考综述拆解追溯 GPT-3.5 各项能力的起源,简单展示下OpenAI团队所构建的GPT-3系列和GPT-3.5系列是如何进化的。
在这里插入图片描述

1.4 GPT-4

2022年3月,OpenAI团队又放大招,发布了更强的LLM:GPT-4。虽然无从得知GPT-4的训练细节,但是可以肯定的是,GPT-4采用了更大的模型结构,增加了更多的训练数据。我们可以通过官方博客GPT-4了解下GPT-4的强大能力。目前GPT-4的主要能力点如下:

  • GPT-4是多模态大模型,可支持图片或文本输入,输出是文本;
  • GPT-4的输入可接受8192个token。另存在变体模型,可接受输入32768个token;
  • GPT-4相较于ChatGPT,具有更广泛的应用,除了聊天机器人之外,还包括文本生成、摘要、翻译、问答系统等多个领域。而ChatGPT主要针对聊天机器人领域,为用户提供日常对话、问题解答、信息查询等服务。

2、其他大模型

  毫不夸张的说,尽管需要耗费巨大的资源,但是目前国内外各大公司都在或多或少的参与着LLM的军备竞赛,这在一定程度上促进着NLP技术的发展。归因于此,目前已经有一系列LLM陆陆续续问世了。我们无法对这些LLM进行一一介绍,这里挑选一些我们在其基础上做过微调或有使用经验的模型,对它们进行简单介绍。

大模型团队发布时间模型规模是否开源Hugging FaceGithub
ChatGLM-6B清华大学20236B已开源,不可商用(获取许可证)ChatGLM-6BChatGLM-6B
ChatGLM2-6B清华大学20236B已开源,不可商用(获取许可证)ChatGLM2-6BChatGLM2-6B
LLaMA2-7BMeta20237B已开源,可商用LLaMA2-7BLLaMAChinese-LLaMA-Alpaca-2
baichuan-7B百川智能20237B已开源,可商用baichuan-7Bbaichuan-7B
文心一言百度2023千亿未开源,不可商用暂无暂无

2.1 ChatGLM

  ChatGLM是一个基于千亿基座模型GLM-130B开发的大语言模型,具有问答、多轮对话和代码生成功能。目前,ChatGLM有两个版本:千亿参数的ChatGLM-130B(内测版)和62亿参数的ChatGLM-6B(开源版,官方Github是ChatGLM-6B)。ChatGLM-6B是在2023年3月14日正式开源的,结合模型量化技术,用户可以在消费级的显卡上进行本地部署(INT4量化级别下最低只需6GB显存)。ChatGLM的技术基础是GLM-130B,这是一个包含多目标函数的自回归预训练模型,同时支持中文和英文,并且在多个自然语言处理任务上优于其他千亿规模的模型。
  ChatGLM的性能表现也十分出色。经过约1T标识符的中英双语训练,辅以监督微调(SFT)、反馈自助(RW)、人类反馈强化学习(RLHF)等技术,62 亿参数的ChatGLM-6B已经能生成相当符合人类偏好的回答。而千亿参数的ChatGLM则更进一步,在问答和对话方面具有更强大的能力。
  2023年6月25日,清华大学发布了ChatGLM2-6B,ChatGLM-6B的升级版本,在保留了了初代模型对话流畅、部署门槛较低等众多优秀特性的基础之上,ChatGLM2-6B 引入了如下新特性:

  • 更强大的性能:基于ChatGLM初代模型的开发经验,全面升级了ChatGLM2-6B的基座模型。ChatGLM2-6B使用了GLM的混合目标函数,经过了1.4T中英标识符的预训练与人类偏好对齐训练,评测结果显示,相比于初代模型,ChatGLM2-6B在MMLU(+23%)、CEval(+33%)、GSM8K(+571%) 、BBH(+60%)等数据集上的性能取得了大幅度的提升,在同尺寸开源模型中具有较强的竞争力;
  • 更长的上下文:基于FlashAttention技术,我们将基座模型的上下文长度(Context Length)由ChatGLM-6B的2K扩展到了32K,并在对话阶段使用8K的上下文长度训练,允许更多轮次的对话。但当前版本的 ChatGLM2-6B对单轮超长文档的理解能力有限,我们会在后续迭代升级中着重进行优化;
  • 更高效的推理:基于Multi Query Attention技术,ChatGLM2-6B有更高效的推理速度和更低的显存占用:在官方的模型实现下,推理速度相比初代提升了42%,INT4量化下,6G显存支持的对话长度由1K提升到了8K。

  有关ChatGLM2-6B更多的细节,大家可参考官方Github,ChatGLM2-6B。接下来我们先介绍下原始GLM的模型结构及预训练原理,再介绍下GhatGLM系列的基座模型:GLM-130B,如何在GLM基础上进行的优化调整。
  GLM(General Language Model)是清华大学在2022年发表的一篇论文中《GLM: General Language Model Pretraining with Autoregressive Blank Infilling》提出的模型。GLM模型被提出之前,NLP领域主流的预训练框架可以分为三种:

  • autoregressive自回归模型(AR模型):代表是GPT,本质上是一个从左到右的语言模型,常用于无条件生成任务(unconditional generation),在长文本生成方面取得了巨大的成功,比如自然语言生成(NLG)领域的任务。当扩展到十亿级别参数时,表现出了少样本学习能力。缺点是单向注意力机制,在NLU任务中,无法完全捕捉上下文的依赖关系;
  • autoencoding自编码模型(AE模型):代表是Bert,是通过某个降噪目标(如掩码语言模型)训练的语言编码器。自编码模型擅长自然语言理解(NLU)任务,常被用来生成句子的上下文表示,但不能直接应用于文本生成;
  • encoder-decoder(Seq2Seq模型):代表作T5,是一个完整的Transformer结构,包含一个编码器和一个解码器。采用双向注意力机制,通常用于条件生成任务(conditional generation),比如文本摘要、机器翻译等。2019)。它们通常被部署在条件生成任务中,如文本摘要和回应生成。T5通过编码器-解码器模型统一了NLU和有条件生成任务,但需要更多的参数来匹配基于BRET的模型的性能。

  上述三种预训练架构的训练目标也略有不同:

  • GPT的训练目标是从左到右的文本生成;
  • Bert的训练目标是对文本进行随机掩码,然后预测被掩码的词;
  • T5则是接受一段文本,从左到右的生成另一段文本。

  三种预训练框架各有利弊,没有一种框架在以下三种领域的表现最佳:自然语言理解(NLU)、无条件生成以及条件生成。GLM基于以上背景诞生了。GLM模型核心是Autoregressive Blank Infilling,结合了上述三种预训练模型的思想。

  • 预训练目标
    • Autoregressive Blank Infilling(自回归的空白填充):GLM是通过优化自回归空白填充目标来训练的。给定一个输入文本 x = [ x 1 , ⋯   , x n ] x = [x_{1}, \cdots, x_{n}] x=[x1,,xn],多个文本跨度(文本片段) { s 1 , ⋯   , s m } \{s_{1},\cdots, s_{m}\} {s1,,sm}被采样,其中每个跨度 s i s_{i} si对应于 x x x中一系列连续的token: [ s i , 1 , ⋯   , s i , l i ] [s_{i,1}, \cdots, s_{i,l_{i}}] [si,1,,si,li],其中 l i l_{i} li代表跨度 s i s_{i} si的长度。 x x x中的每一个跨度都会被一个[MASK]替换掉,从而生成一个被破坏的 x c o r r u p t x_{corrupt} xcorrupt。GLM模型以自回归的方式预测被破坏的文本中缺少的token,这意味着当预测一个跨度中缺少的token时,GLM既可以访问被破坏的文本 x c o r r u p t x_{corrupt} xcorrupt,又可以访问跨度中之前已经被预测的token。为了充分捕捉不同跨度之间的相互依存关系,GLM随机排列跨度的顺序。形式上,让 Z m Z_{m} Zm表示长度为 m m m的索引序列 [ 1 , 2 , ⋯   , m ] [1, 2, \cdots, m] [1,2,,m]所有可能排列的集合, s z < i s_{z_{<i}} sz<i代表 [ s z 1 , ⋯   , s z i − 1 ] [s_{z_{1}}, \cdots, s_{z_{i-1}}] [sz1,,szi1],此时,可定义预训练目标为:
      max ⁡ θ E z ∼ Z m [ ∑ i = 1 m l o g   p θ ( s z i ∣ x c o r r u p t , s z < i ) ] \max_{\theta}E_{z\sim Z_{m}}[\sum_{i=1}^{m}log\ p_{\theta}(s_{z_{i}}|x_{corrupt}, s_{z_{<i}})] θmaxEzZm[i=1mlog pθ(szixcorrupt,sz<i)]
      其中, z z z代表 Z m Z_{m} Zm中任意一个排列,也就是索引集合; { z 1 , ⋯   , z m } \{z_{1},\cdots,z_{m}\} {z1,,zm}代表 z z z中的索引元素; s z i s_{z_{i}} szi代表 { s 1 , ⋯   , s m } \{s_{1},\cdots, s_{m}\} {s1,,sm}中第 z i z_{i} zi个跨度。上述公式的含义就是:用被破坏的 x c o r r u p t x_{corrupt} xcorrupt,与 s z i s_{z_{i}} szi之前的跨度 [ s z 1 , ⋯   , s z i − 1 ] [s_{z_{1}}, \cdots, s_{z_{i-1}}] [sz1,,szi1]进行拼接,预测生成的文本是跨度 s z i s_{z_{i}} szi的概率越大越好,这也是典型的语言模型目标函数。
        另外论文中提到,生成任务都是按照从左到右的顺序生成每个空白处的标记,也就是说,生成跨度 s i s_{i} si的概率被分解为:
      p θ ( s z i ∣ x c o r r u p t , s z < i ) = ∏ j = 1 l i p ( s i , j ∣ x c o r r u p t , s z < i , s i < j ) p_{\theta}(s_{z_{i}}|x_{corrupt}, s_{z_{<i}})=\prod_{j=1}^{l_{i}}p(s_{i,j}|x_{corrupt}, s_{z_{<i}},s_{i<j}) pθ(szixcorrupt,sz<i)=j=1lip(si,jxcorrupt,sz<i,si<j)
        在构建好优化目标后,论文中通过以下技术实现该目标,即上述的自回归空白填补目标。输入的 x x x被分为两部分。Part A是被破坏的文本 x c o r r u p t x_{corrupt} xcorrupt,Part B由被mask的跨度组成。举个例子,如下图所示,假设原始的文本序列为 x = [ x 1 , x 2 , x 3 , x 4 , x 5 , x 6 ] x = [x_{1}, x_{2}, x_{3}, x_{4}, x_{5}, x_{6}] x=[x1,x2,x3,x4,x5,x6],采样的两个文本片段为 [ x 3 ] [x_{3}] [x3] [ x 5 , x 6 ] [x_{5}, x_{6}] [x5,x6],那么掩码后的文本序列 x c o r r u p t x_{corrupt} xcorrupt [ x 1 , x 2 , [ M ] , x 4 , [ M ] ] [x_{1}, x_{2}, [M], x_{4}, [M]] [x1,x2,[M],x4,[M]],也就是Part A。文本片段 [ x 3 ] [x_{3}] [x3] [ x 5 , x 6 ] [x_{5}, x_{6}] [x5,x6]用于组成Part B,同时需要对Part B的片段进行shuffle,也就是打乱文本片段的顺序(注意不是文本片段的内部顺序,而是文本片段之间的顺序),并且每个片段使用 [ S ] [S] [S]填充在开头作为输入,使用 [ E ] [E] [E]填充在末尾作为输出。最后,从开始标记 [ S ] [S] [S]开始依次解码出被掩码的文本片段,直至结束标记 [ E ] [E] [E]
        以上是实现自回归空白填补目标的大体流程。除此之外,还有有两点需要注意,一个是self-attention mask的设计,一个是[MASK]文本片段的采样设计。

      • self-attention mask
        • Part A中的词彼此可见,但不可见Part B中的词(下图(d)中蓝色框中的区域);
        • Part B中的词单向可见(下图(d)黄色和绿色的区域。黄色和绿色分别对应 [ x 3 ] [x_{3}] [x3] [ x 5 , x 6 ] [x_{5}, x_{6}] [x5,x6]两个文本片段,下同);
        • Part B可见Part A(下图(d)中黄色和绿色的区域);
        • 其余不可见(下图(d)中灰色的区域)
      • [MASK]文本片段采样:论文中随机对跨度的长度采样,采样分布属于泊松分布,其中 λ = 3 \lambda=3 λ=3,直到至少15%的原始token被mask。根据经验,论文中发现,15%的比例对于下游NLU任务的良好表现至关重要。
         

        最终通过以上方式,GLM自动学习一个双向编码器(Part A)和一个单向解码器(Part B)统一的模型。
      在这里插入图片描述

    • Multi-Task Pretraining(多任务预训练):上述例子中,GLM掩盖了短跨度,适用于NLU任务。而论文的关注点是预训练一个能同时处理NLU和文本生成的模型。因此,论文研究了一个多任务预训练的设置。在这个设置中,增加一个生成较长文本的目标,与空白填充目标共同优化。具体来说,论文中考虑以下两个目标。

      • 文档级:从文档中采样一个文本片段进行mask,且片段长度为文档长度的50%~100%。该目标旨在生成长文本;
      • 句子级:限制被mask的文本片段必须是完整的句子。多个文本片段(句子)被取样,以覆盖15%的原始token。这一目标是针对seq2seq任务,其预测往往是完整的句子或段落。
         

        这两个新目标的定义与原目标相同。唯一不同的是的跨度数量和跨度长度。

  • 模型结构:GLM使用Transformer架构,并对架构进行了一些修改。其中一个重要的创新点,是二维位置编码。
    • Layer Normalization:重新调整了LayerNorm和残差连接的顺序(先进行LayerNorm,再进行残差连接,类似于Pre-LN,不过GLM-130B训练时又调整为DeepNorm了);
    • 输出层:使用单个线性层进行输出token预测;
    • 激活函数:使用GeLU替换ReLU激活函数;
    • 二维位置编码:自回归空白填充任务的挑战之一是如何对位置信息进行编码。Transformer依靠位置编码来注入token的绝对和相对位置。论文中提出了二维位置编码来解决这一挑战。如上面图片所示,具体来说,每个token都有两个位置标识编码。第一个位置标识代表在被破坏文本 x c o r r u p t x_{corrupt} xcorrupt中的位置。对于被mask的跨度,它是相应的[MASK]的位置。第二个位置标识代表跨度内的位置。对于A部分的token,它们的第二个位置标识是0;对于B部分的token,它们的第二个位置标识是从1到跨度的长度。这两个位置标识通过可学习的嵌入表映射为两个向量,这两个向量都被添加到输入token的embedding表达中。

  至此,我们介绍了原始GLM的预训练原理及模型结构。更多细节大家可参考GLM: 自回归空白填充的通用语言模型预训练ChatGLM官方博客。接下来,我们简单介绍下ChatGLM的基座模型:GLM-130B。

  • 相对原始GLM的优化调整
    • Layer Normalization:使用DeepNorm(Post-LN的升级版)来提供模型训练的稳定性。下面是三种LN模式,其中 f f f代表FFN或Attention层。
      • Post-LN:原始Bert、GPT-1采用的Layer Normalization形式,训练不稳定,但效果较好,具体公式如下:
        x = L a y e r N o r m ( x + f ( x ) ) x=LayerNorm(x+f(x)) x=LayerNorm(x+f(x))
      • Pre-LN:GPT-2、GPT-3以及LLaMA采用的Layer Normalization形式都近似于Pre-LN,效果不如Post-LN,但稳定性较好,具体公式如下:
        x = x + L a y e r N o r m ( f ( x ) ) x=x+LayerNorm(f(x)) x=x+LayerNorm(f(x))
      • DeepNorm:集成Post-LN和Pre-LN的优点,具体公式如下:
        x = L a y e r N o r m ( α x + f ( x ) ) , ( α > 1 ) x=LayerNorm(\alpha x+f(x)), (\alpha>1) x=LayerNorm(αx+f(x)),(α>1)
    • Position Embedding:使用RoPE(旋转位置编码)替换2D Position Embedding;
    • Feed Forward Network(FFN):使用GeGLU替换GeLU 。
  • GLM-130B预训练配置
    • 自监督空白填充(95% tokens):通过不同的Mask策略,来使模型获得自编码和自回归的能力,具体来说:
      • 词Mask:30%的训练token进行词级别的Mask,Mask方式参考前文的跨度采样方法:跨度长度遵循泊松分布(λ=3),每个样本的跨度长度加起来最多为该样本长度的15%;
      • 句子及文档Mask:剩下70%的token进行句子或文档级别的Mask。
    • 多任务指令预训练(MIP,5%tokens):T5和ExT5研究表明,预训练中的多任务学习比微调更有帮助。因此,GLM-130B在预训练中包含各项指令数据集,包含语意理解、生成和信息抽取。为了保证模型的其他生成能力不受影响,用于MIP训练的数据集只占了5%。

2.2 LLaMA

  2023年7月,Meta推出了完全可商用的开源大模型LLaMA2。这里简单介绍下两代LLaMA的共有结构以及LLaMA2相较于初代LLaMA的优化点。

  • 通用结构:LLaMA的具体结构见下图。
    • Layer Normalization:使用前置的RMSNorm;在BERT、GPT等模型中广泛使用的LayerNorm是如下形式:
      y = W ∗ x − M e a n ( x ) V a r ( x ) + ϵ + b y=W*\frac{x-Mean(x)}{\sqrt{Var(x)+\epsilon}}+b y=WVar(x)+ϵ xMean(x)+b
      RMSNorm(root mean square)发现LayerNorm的中心偏移没什么用(减去均值等操作)。将其去掉之后,效果几乎不变,但是速度提升了40%。RMSNorm公式为:
      y = W ∗ x M e a n ( x 2 ) + ϵ y=W*\frac{x}{\sqrt{Mean(x^{2})+\epsilon}} y=WMean(x2)+ϵ x
      注意除了没有减均值,加偏置以外,分母上求的是RMS而不是方差。另外LLaMA在Attention Layer和MLP的输入上使用了RMSNorm,相比在输出上使用,训练会更加稳定,类似于Pre-LN方式。
    • 位置编码:在Q、K上使用RoPE旋转式位置编码;
    • Causal Mask:使用causal mask保证每个位置只能看到前面的tokens;
    • 激活函数:使用SwiGLU替代ReLU。
      在这里插入图片描述
  • LLaMA2优化点:参考LLaMA2 vs LLaMALLaMA2介绍
    • 更多的训练语料:预训练语料从1万亿增加到2万亿tokens;
    • 更长的上下文:上下文长度从2048增加到4096;
    • 新增SFT过程:收集了10万人类标注数据进行SFT;
    • 新增RLHF过程:收集了100万人类偏好数据进行RLHF;
    • 调整Attention机制:和Falcon一样,使用了Group Query Attention,节省显存占用,同时提升计算速度。
      • Multi Head Attention(MHA):原始多头注意力机制,所有头各自保存独立的Q、K、V矩阵;
      • Multi Query Attention(MQA):所有的头之间共享同一份K和V矩阵,每个头只单独保留了一份Q矩阵参数,从而大大减少K和V矩阵的参数量;
      • Group Query Attention(GQA):没有像MQA一样极端,而是将Q分组,组内共享K、V矩阵。当group=1时,GQA等价于MQA。
        在这里插入图片描述

三、补充知识

1、LLM为什么都用Decoder only架构?

  总的来说,LLM之所以主要都用Decoder-only架构,除了训练效率和工程实现上的优势外,一方面,在理论上是因为Encoder的双向注意力会存在低秩问题,这可能会削弱模型表达能力;另一方面,就生成任务而言,引入双向注意力并无实质好处。而Encoder-Decoder架构之所以能够在某些场景下表现更好,大概只是因为它多了一倍参数。所以,在同等参数量、同等推理成本下,Decoder-only架构就是最优选择了。具体参考LLM为什么都用Decoder only架构?

  • 训练效率:Decoder-only架构只需要进行单向的自回归预测,而Encoder-Decoder架构需要进行双向的自编码预测和单向的自回归预测,计算量更大;
  • 工程实现:Decoder-only架构只需要一个模块,而Encoder-Decoder架构需要两个模块,并且需要处理两者之间的信息传递和对齐,实现起来更复杂;
  • 理论分析:Encoder的双向注意力会存在低秩问题,即注意力矩阵的秩随着网络深度的增加而降低(来自论文:Attention is not all you need: pure attention loses rank doubly exponentially with depth,结论是如果没有残差连接和MLP兜着,注意力矩阵会朝秩为1的矩阵收敛,最后每个token的表示都一样了,网络就废了),这可能会削弱模型的表达能力。而Decoder的单向注意力则不存在这个问题;
  • 生成任务:对于文本生成任务,Encoder的双向注意力并无实质好处,因为它会引入右侧的信息,破坏了自回归的假设。而Decoder的单向注意力则可以保持自回归的一致性。

2、NLP小知识点

  • Bert;自编码模型,适用于NLU(预训练任务主要挖掘句子之间的上下文关系),有关Bert的理论知识,可参考—步步走进Bert
  • GPT;自回归模型,适用于NLG(预训练任务主要用于生成下文),有关GPT系列的理论知识,可参考本文的GPT系列章节;
  • OOV:OOV 问题是NLP中常见的一个问题,其全称是Out Of Vocabulary,下面简要的说了一下OOV。
    • 定义:在自然语言处理过程中,通常会有一个字词库(vocabulary)。这个vocabulary或者是提前加载的,或者是自己定义的,或者是从当前数据集提取的。假设通过上述方法已经获取到一个vocabulary,但在处理其他数据集时,发现这个数据集中有一些词并不在现有的vocabulary中,这时称这些词是Out-Of-Vocabulary,即OOV;
    • 解决方法:Bert中解决OOV问题。如果一个单词不在词表中,则按照subword的方式逐个拆分token,如果连逐个token都找不到,则直接分配为[unknown]。
  • 对比学习:自监督学习(Self-supervised learning)可以避免对数据集进行大量的标签标注。把自己定义的伪标签当作训练的信号,然后把学习到的表示(representation)用作下游任务里。最近,对比学习被当作自监督学习中一个非常重要的一部分,被广泛运用在计算机视觉、自然语言处理等领域。它的目标是:将一个样本的不同的、增强过的新样本们在嵌入空间中尽可能地近,然后让不同的样本之间尽可能地远。SimCSE《SimCSE: Simple Contrastive Learning of Sentence Embeddings》是基于对比学习的表示学习方法,即采用对比学习的方法,获取更好的文本表征。SimCSE细节可参考论文精读-SimCSE

3、名词解释

  • LLM:Large Language Model,大型语言模型。
  • PLM:Pretrain Language Model,预训练语言模型。
  • RL:Reinforcement Learning,强化学习。
  • SFT:Supervised Fine-Tuning,有监督微调。
  • ICL:In-Context Learning,上下文学习。
  • Fine-Tuning :微调。
  • Prompt-Tuning:提示微调。
  • Instruction-Tuning:指示/指令微调。
  • NLU:Natural Language Understanding,自然语言理解。
  • NLG:Natural Language Generation,自然语言生成。
  • CoT:Chain-of-Thought,思维链。
  • OOV:out of vocabulary,超出词表外的词。
  • shifted right:指的是Transformer Decoder结构中,decoder在之前时刻的一些输出,作为此时的输入,一个一个往右移。
  • 重参数化:常规思想:对于网络层需要的参数是 Φ \Phi Φ,训练出来的参数就是 Φ \Phi Φ。重参数化方法:训练时用的是另一套不同于 Φ \Phi Φ的参数,训练完后等价转换为 Φ \Phi Φ用于推理。
  • PPL:困惑度(Perplexity),用于评价语言模型的好坏。
  • FCNN:Fully connected neural network,全连接神经网络。
  • FNN:Feedforward neural network,前馈神经网络。
  • DNN:Deep neural network,深度神经网络。
  • MLP:Multi-layer perceptron neural networks,多层感知机。
  • RM:Reward Model,奖励模型。
  • PPO,Proximal Policy Optimization,近端策略优化,简单来说,就是对目标函数通过随机梯度下降进行优化。
  • Emergent Ability:很多能力小模型没有,只有当模型大到一定的量级之后才会出现。这样的能力称为涌现能力。
  • AutoRegression Language Model:自回归语言模型。
  • Autoencoder Language Model:自编码语言模型。
  • CLM:Causal language modeling,因果语言建模,等价于AutoRegression Language Model。
  • AIGC:Artificial Intelligence Generated Content,生成式人工智能。
  • AGI:Artifical General Intelligence,通用人工智能。

4、对大模型时代的一些个人感想

  就目前的发展趋势而言,大模型的训练基本就是按照PretrainInstruction-TuningRLHF三步走模式进行,因此技术上基本没什么问题,主要瓶颈存在于算力,当然对于中文大模型来说,获取高质量中文数据也是个问题。国内能耗费大规模成本进行大模型训练的厂商屈指可数,因此个人认为,未来的发展趋势,可能有以下四个方向:

  • 统一大模型:头部企业逐步迭代出可用性很强的大语言模型(千亿级别),开放API或在公有云上供大家使用;
  • 垂直领域模型:部分企业逐步迭代出在相关垂直领域可用性很强的大语言模型(百亿级别),自用或私有化提供客户使用;
  • 并行训练技术:全新或更具可用性、生态更完整的并行训练技术/框架开源,满足大部分有训练需求的企业/个人使用,逐步实现人人都能训练大模型;
  • 颠覆:或许某一天,横空出世的论文推翻了目前的大模型发展路线,转而证明了人们更需要的是另一种大模型技术,事情就会变得有意思了。

总结

  了解LLM,看这一篇就够了!!!

  • 38
    点赞
  • 246
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值