赞
踩
2022年年底OpenAI发布ChatGPT,将LLM(Large Language Model)带向了一个新的高度,而2023年OpenAI继续放出大招:更强大的GPT-4问世,瞬间引爆了整个互联网圈。在这个大模型时代,作为一名NLPer,持续吸收着层出不穷的新技术,确实有些吃不消。俗话说,好记性不如烂笔头,在此记录下LLM相关技术及进展。顺便说一句,你可以说它不全面,但不能说它不通俗易懂。
虽然本篇博客的主要目的是介绍包括GPT系列在内的各种LLM的架构,但是在介绍LLM之前,我们有必要了解下Tuning(微调)的发展历程,它们推动着LLM朝向更智能的方向发展。
目前学术界一般将NLP任务的发展分为四个阶段,即NLP四范式:
在整个NLP领域,你会发现整个发展是朝着精度更高、少监督,甚至无监督的方向发展的。下面我们对第三范式、第四范式进行详细介绍。
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(上卷)——层层剥开Transformer、This post is all you need(下卷)——步步走进Bert
在介绍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则执行如下步骤:
基于上述内容,我们对prompt learning有个初步了解,接下来我们详细介绍下两种prompt learning:In-context learning和Prompt-Tuning。
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)。它希望模型能自动学习隐藏在演示上下文中的模式,并据此做出正确的预测。
Training:在推理前,通过持续学习让语言模型的ICL能力得到进一步提升,这个过程称之为model warmup(模型预热),model warmup会优化语言模型的参数或者新增参数,区别于传统的Fine-Tuning,Fine-Tuning旨在提升LLM在特定任务上的表现,而model warmup则是提升模型整体的ICL性能。
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模型时,我们也会发现,推理时,同一个问题如果加上不同的示例,可能会得到不同的模型生成结果。
无监督方法:首先就是根据句向量距离或者互信息等方式选择跟当前输入x最相似的样本作为演示示例,另外还有利用自适应方法去选择最佳的示例排列,有的方法还会考虑到演示示例的泛化能力,尽可能去提高示例的多样性。除了上述这些从人工撰写的样本中选择示例的方式外,还可以利用语言模型自身去生成合适的演示示例。
监督方法:第一种是先利用无监督检索器召回若干相似的样本,再通过监督学习训练的Efficient Prompt Retriever进行打分,从而筛选出最合适的样本。此外还有基于Prompt Tuning跟强化学习的方式去选择样本。
以上内容对In-context learning做了简单介绍,详细内容大家可参考论文《A Survey on In-context Learning》、《A Survey for In-context Learning》翻译。对于In-context learning及后面会讲到的Instruction-Tuning方法来说,如何设计输入的prompt是很重要的一点,有关prompt设计的方法,除了上面讲到的内容,这里还有一篇写的很好的文章供大家参考[译] Prompt Engineering: 循循善诱。
ICL方法是在GPT-3中被提出的,这类方法有一个明显的缺陷是——其建立在超大规模的预训练语言模型上,此时的模型参数数量通常超过100亿,在真实场景中很难应用,因此众多研究者开始探索GPT-3的这套思路在小规模的语言模型(如Bert)上还是否适用?事实上,这套方法在小规模的语言模型上是可行的,但是需要注意:
在介绍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与标准监督学习结合。具体的步骤是:
另外在该论文中,作者提出,在每一个PLM上只进行一次微调+soft labels生成,通常得到的新的数据集(即用soft labels标记的unlabeled dataset)会有很多错误的数据,因此扩展提出iPET模型(Iterative PET),即添加了迭代过程:首先随机从集成的预训练模型集合中抽取部分预训练模型,在未标注数据集(unlabeled dataset) D \mathcal{D} D 上标注数据,并扩增到初始有标签数据集 T \mathcal{T} T上,其次再根据扩增后的 T \mathcal{T} T分别微调预训练模型。上述过程一直迭代多次:
上述内容具体可参考:论文解读:Exploiting Cloze Questions for Few Shot Text Classification and Natural Language Inference、论文阅读:PET系列。论文中有关PET及IPET部分的介绍,不是本部分关心的重点,大家选择性学习。下面着重介绍本部分最关心,也是PET最核心的部分Pattern-Verbalizer-Pair(PVP),PET设计了两个很重要的组件:
通过上个部分的介绍,我们已经了解,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 Prompt和Soft Prompt。
这里再给出一些常见下游任务的prompt设计:
Hard Prompt & Soft Prompt:承接上文,Hard Prompt和Soft Prompt的提出,是为了解决预训练模型过大,难以针对下游任务进行训练的痛点。目前常见的Hard Prompt和Soft Prompt方法,分为以下五种:
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推断任务为例,仅仅只是修改了模板,测试结果差异很明显,因此离散的模板存在方差大、不稳定等问题。
如何避免这种问题呢,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} Xe∈Rn×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} Pe∈Rp×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。
最后发现,非随机初始化方法要显著好于随机初始化,而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方法中的三个技巧点:
具体可参考: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。最上面的任务是预训练任务,下面三个任务为下游任务)。
另外论文中还给出了四种微调方案,如下图所示,[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 Learning,Prompt 如何更好地应用于工业界?。
至此,我们已经深入了解了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。
前文中已经多次提到过Instruction-Tuning,可以说在大规模语言模型领域,它是目前最火的研究范式,性能超过包括In-context learning在内的prompt learning。
回顾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的具体训练流程。
具体来说,作者提出的Finetuned Language Net(FLAN)模型将62个NLP任务分为12个簇,同一个簇内是相同的任务类型,如下图所示。
对于每个task,将为其手动构建10个独特template,作为以自然语言描述该任务的instructions。为了增加多样性,对于每个数据集,还包括最多三个“turned the task around/变更任务”的模板(例如,对于情感分类,要求其生成电影评论的模板)。所有数据集的混合将用于后续预训练语言模型做Instruction-Tuning,其中每个数据集的template都是随机选取的。如下图所示,Premise、Hypothesis、Options会被填充到不同的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的具体实现方式。
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和instruction都是用于指导模型生成输出的文本,但它们的目的和使用方式是不同的。Prompt更多地用于帮助模型理解任务和上下文,而Instruction则更多地用于指导模型执行具体操作或完成任务。
对于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的基础上,进一步挖掘模型理解任务的能力。(仅供参考)
随着LLM的越来越大,以及tuning技术的快速发展,LLM在包括情感分析在内的传统自然语言任务上表现越来越好,但是单纯的扩大LLM模型的参数量无法让模型在算术推理/常识推理/符号推理等推理任务上取得理想的效果。 如何提升LLM在这些推理任务上性能呢?在此前关于LLM的推理任务中,有两种方法:
但是这两种方法都有着局限性,前者微调计算成本太高,后者采用传统的输入输出样例在推理任务上效果很差,而且不会随着语言模型规模的增加而有实质性的改善。此时,Chain-of-Thought应运而生。下面我们根据三篇比较有代表性的论文,详细介绍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部分的一系列的推理步骤(人工构建)写出来后,再给出最终答案。逻辑就是希望模型学会一步一步的输出推理步骤,然后给出结果。
论文中首先在算数推理(arithmetic reasoning)领域做了实验,使用了5个数学算术推理数据集:GSM8K / SVAMP / ASDiv / AQuA / MAWPS,具体的实验过程这里不再赘述,感兴趣的同学可以直接参考论文,这里直接给出实验结论(如下图):
除此之外,论文中为了证明CoT的有效性,相继做了消融实验(Ablation Study)、鲁棒性实验( Robustness of Chain of Thought)、常识推理(Commonsense Reasoning)实验、符号推理(Symbolic Reasoning)实验,下面分别做以简单介绍:
消融实验:我们知道,消融实验是通过研究移除某个组件之后的性能,证明该组件的有效性。论文中通过引入CoT的三个变种,证明CoT的有效性,结果如下图所示:
鲁棒性实验:论文中通过annotators(标注者),exemplars(样例选择)和models(模型)三个方面对CoT进行了鲁棒性分析。如下图所示,总体结论是思维链普遍有效,但是不同的CoT构建方式/exemplars的选择/exemplars的数量/exemplars的顺序,在一定程度上影响着CoT的效果。
关于鲁棒性实验,论文中最后指出: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)的开山之作
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的。
前文已经提到过,传统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则解决了这一痛点,具体做法是:
总体来说,Auto-CoT是Manual-CoT和Zero-shot-CoT的结合体,如下图所示。实验证明,在十个数据集上Auto-CoT是可以匹配甚至超越Manual-CoT的性能,也就说明自动构造的CoT的问题、中间推理步骤和答案样例比人工设计的还要好,而且还节省了人工成本。
至此,我们详细介绍了三种CoT技术:Manual-CoT、Zero-shot-CoT以及Auto-CoT,有关CoT的技术还有很多,需要我们慢慢学习,后续持续更新。
通过前文的介绍,我们可以把Tuning分为两类:
我们知道,部分参数微调模式的提出,一方面是由于资源限制,无法更新整体大模型参数,另一方面,要保证在资源有限的条件下,能够尽可能的提升大模型在下游任务上的效果。目前,针对部分参数微调的研究,正处于蓬勃发展阶段,这个研究领域有个统一的名称:Parameter-Efficient Fine-Tuning (PEFT),即参数有效性微调,PEFT方法仅微调少量或额外的模型参数,固定大部分预训练参数,大大降低了计算和存储成本,同时最先进的 PEFT 技术也能实现了与全量微调相当的性能。前文提到的Prompt-Tuning,包括P-Tuning等,都可以视为PEFT的一种。总体来说,参数有效性微调可分为三个类别:
接下来,我们对其中流行的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}
hi∈Rd是每个时间步
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+1∣h≤i)=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}=
在训练时,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的主要训练流程,下面我们给出论文中通过实验得出的三个主要结论:
我们回顾下前文提到的parameter-efficient prompt tuning(下面简称为Prompt Tuning),其论文中有提到,它可以看作是Prefix-Tuning的简化版。总结下两者的不同点:
P-Tuning v2针对Prefix-Tuning、P-Tuning解决的问题:
P-Tuning v2的优点:
P-Tuning v2的核心点:
P-Tuning v2的其他优化及实施点:
实验环境: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 微调。
下载模型实现: 由于下载整体模型较慢,所以我们先下载模型实现,再手动下载模型参数文件。下载模型实现前,需先安装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
CUDA_VISIBLE_DEVICES=1 python cli_demo.py
cli_demo.py
和web_demo.py
中的tokenizer
和model
加载路径,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()
灾难性遗忘问题:在该数据集上进行微调后,会出现灾难性遗忘的情况,在数据集有限的情况下,目前通过实践总结出下面三种做法,可在一定程度上缓解灾难性遗忘
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}")
# 具体安装包
protobuf
transformers==4.30.2
cpm_kernels
torch>=2.0
gradio
mdtex2html
sentencepiece
accelerate
pip install rouge_chinese nltk jieba datasets
./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微调实践。模型文件同样可根据前文的方法进行获取,其中官方的模型可能存在更新,如果想顺利复现训练过程,建议从网盘进行下载。
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任务
load_in_8bit=True
和quantize(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
accelerate
、bitsandbytes
、scipy
、tensorboardX
四个安装包。ChatGLM2-6B + LoRA ⇒ \Rightarrow ⇒官方任务实践:实现了ChatGLM2-6B基于LoRA的微调流程。具体代码见LLM微调实践。模型文件同样可根据前文的方法进行获取,其中官方的模型可能存在更新,如果想顺利复现训练过程,建议从网盘进行下载。
# 切换路径
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
# 错误内容
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
accelerate
、bitsandbytes
、scipy
、tensorboardX
四个安装包;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 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必须加上,不然运行代码会报存储错误
vi ~/.bashrc
export PATH=/home/LLM/ChatGLM-FT/miniconda3/bin:$PATH
source ~/.bashrc
# 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-Tuning | LLM原理及实战经验分享 | LLM-实战经验 |
LLM-Tuning | ChatGLM-6B在真实任务上的应用 | ChatGLM-真实任务应用 |
LLM-Tuning | ChatGLM-6B/ChatGLM2-6B结合QLoRA实现LLM-Tuning | ChatGLM-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技术介绍 |
系统知识 | 对算法基础、算法应用进行全面总结 | 算法总结 |
按照并行方式,分布式训练一般分为数据并行和模型并行两种,当然也有数据并行和模型并行的混合模式。
以PyTorch框架为例,介绍几种分布式训练框架。
简介:单机多卡的分布式训练工具;数据并行模式。
原理:网络在前向传播的时候会将model从主卡(默认是逻辑0卡)复制一份到所有的device上,input_data会在batch这个维度被分组后加载到不同的device上计算。在反向传播时,每个卡上的梯度会汇总到主卡上,求得梯度的均值后,再用反向传播更新单个GPU上的模型参数,最后将更新后的模型参数复制到剩余指定的GPU中进行下一轮的前向传播,以此来实现并行。
参数简介:torch.nn.DataParallel(module, device_ids=None, output_device=None, dim=0)
os.environ["CUDA_VISIBLE_DEVICES"] = "2, 3"
,那么0卡(逻辑卡号)指的是2卡(物理卡号)。模型参数更新方式:
术语介绍:
使用示例:参考一文搞定分布式训练:dataparallel、distirbuted、deepspeed、accelerate、transformers、horovod
简介:既可单机多卡又可多机多卡的分布式训练工具;数据并行模式。
原理: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)
os.environ["CUDA_VISIBLE_DEVICES"] = "2, 3"
,那么0卡(逻辑卡号)指的是2卡(物理卡号));模型参数更新方式:
基本概念:假设我们有3台机子(节点),每台机子有4块GPU。我们希望达到12卡并行的效果。
使用示例:分布式训练框架介绍
DP vs DDP:
简介:默认情况下,大多数深度学习框架都采用32位浮点算法进行训练。2017年,NVIDIA研究了一种用于混合精度训练的方法,该方法在训练网络时将单精度(FP32,以32bits表示数字,即4bytes)与半精度(FP16,以16bits表示数字,即2bytes)结合在一起,并使用相同的超参数实现了与FP32几乎相同的效果。以PyTorch为例,可通过如下命令查看模型参数精度:
for name, param in model.named_parameters():
print(name, param.dtype)
关键词:AMP(自动混合精度)的关键词有两个:自动,混合精度。
适用硬件: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了),同时训练速度更快。
torch.HalfTensor的劣势就是:溢出错误,数值范围小(更容易Overflow / Underflow);舍入误差(Rounding Error),导致一些微小的梯度信息达不到16bit精度的最低分辨率,从而丢失。
综上可知,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回去)
综上,损失放大的思路是:
使用示例:分布式训练框架介绍
accelerate config --config_file ./accelerate_config.yaml
accelerate env --config_file ./accelerate_config.yaml
accelerate launch --config_file ./conf/accelerate_config.yaml train_accelerate.py
accelerate test --config_file ./accelerate_config.yaml
简介: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 States和Residual States。
存储大小:了解了存储分类,接下来了解下每种存储占用的内存大小。首先我们回忆下混合精度训练的过程,大致如下图所示:
现在,我们可以来计算模型在训练时需要的存储大小了,假设模型的参数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纳入统计范围,原因是:
ZeRO-DP:了解了存储种类以及它们所占的存储大小之后,接下来我们介绍下Deepspeed是如何优化存储的。这里提前透露下,ZeRO三阶段:ZeRO-1、ZeRO-2、ZeRO-3的实质是数据并行,因此我们也称之为ZeRO-DP,后面会介绍具体细节。首先我们应该清楚,在整个训练中,有很多states并不会每时每刻都用到,举例来说;
诸如此类,所以,ZeRO-DP想了一个简单粗暴的办法:如果数据算完即废,等需要的时候,我再想办法从个什么地方拿回来,那不就省了一笔存储空间吗?沿着这个思路,我们逐一来看ZeRO是如何递进做存储优化的。
做完 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 Φ | 120GB | 2 Φ \Phi Φ |
P o s P_{os} Pos | (2+2+ K N d \frac{K}{N_{d}} NdK) Φ \Phi Φ | 31.4GB | 3 Φ \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 Φ了。
做完 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 Φ | 120GB | 2 Φ \Phi Φ |
P o s P_{os} Pos | (2+2+ K N d \frac{K}{N_{d}} NdK) Φ \Phi Φ | 31.4GB | 3 Φ \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.6GB | 2 Φ \Phi Φ |
如图所示,和朴素DP相比,存储降了8倍,单卡通讯量持平。
做完 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 Φ | 120GB | 2 Φ \Phi Φ |
P o s P_{os} Pos | (2+2+ K N d \frac{K}{N_{d}} NdK) Φ \Phi Φ | 31.4GB | 3 Φ \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.6GB | 2 Φ \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.9GB | 3 Φ \Phi Φ |
如图所示,和朴素DP相比,用1.5倍的通讯开销,换回近120倍的显存。最终,我们可以看下论文中的总体对比图:
ZeRO-DP VS 模型并行:通过上述的介绍,大家可能会有疑问,既然ZeRO都把参数W给切了,那它应该是个模型并行,为什么却归到数据并行?其实ZeRO是模型并行的形式,数据并行的实质。
ZeRO-Offload:简单介绍一下ZeRO-Offload。它的核心思想是:显存不够,内存来凑。如果把要存储的大头卸载(offload)到CPU上,而把计算部分放到GPU上,这样比起跨机,既能降低显存使用,也能减少一些通讯压力。ZeRO-Offload的做法是:
具体切分如下图:
Accelerate vs Deepspeed:
通过前文对Tuning技术的介绍,我们能够了解到,Tuning技术依赖于LLM的发展,同时也在推动着LLM的发展。通常,LLM指的是包含数百亿(或更多)参数的语言模型,这些模型在大量的文本数据上训练。接下来我们介绍几个耳熟能详的大语言模型,其他LLM的相关内容可参考LLMSurvey、Open LLM Leaderboard
、开源大语言模型(LLM)汇总(持续更新中)
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
)
但是,
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
)
这就是常说的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(wL∣wL−N,⋯,wL−1),我们就能知道给定前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)=i∑logP(ui∣ui−k,⋯,ui−1;Θ)
其中:
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
)
其中:
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=u−k,⋯,u−1)是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}
hn∈Rm×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}
zn∈Rm×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(y∣x1,⋯,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,y∑P(y∣x1,⋯,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如何在上述四个任务上进行微调,如下图所示。
这里我们同样通过一个文本分类的例子,来介绍下GPT-1在下游任务上是如何微调的。例如下游任务是情感文本分类任务,包括喜、怒、哀、惧、其他五个类别,其中一个样本是【今天很开心】,真实标签是【喜】。通过前面的介绍,我们知道GPT-1在下游任务进行微调时,损失函数包含两部分,一部分是与预训练保持一致的下一个词预测损失,这部分就不介绍了。另一部分是分类损失,对于分类任务来说,我们最终也会获取到GPT-1最后一层的向量表征 h l ∈ R m × d h_{l}\in R^{m\times d} hl∈Rm×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} zl∈Rc,其中 c c c是类别数目;最后, z l z_{l} zl会经过softmax操作,获取【今天很开心】这段文本对应的每一个类别的概率值,我们的期望是【喜】的概率值要尽可能的大,也就是 z l z_{l} zl的第一个元素的值要尽可能大,这也就是我们的优化目标。
GPT-1特点:
GPT-1与ELMo,Bert的区别:
GPT-1的数据集:GPT-1使用了BooksCorpus数据集,这个数据集包含7000本没有发布的书籍。作者选这个数据集的原因有二:1)数据集拥有更长的上下文依赖关系,使得模型能学得更长期的依赖关系;2)这些书籍因为没有发布,所以很难在下游数据集上见到,更能验证模型的泛化能力。
GPT-2:我们知道,GPT-1和Bert的训练都是分两步走:pre-training + supervised fine-tuning。这套方法的缺点:
另外,在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的基础上做了一些改进,如下:
论文给了不同层数的模型,最大的模型称为GPT-2模型,参数有1.5B;最小的即是GPT-1,对标Bert-base;倒数第二小的对标Bert-large。不同模型大小如下:
模型训练:GPT-2只有预训练过程。
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-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)的区别在于:
具体来说,sparse attention除了相对距离不超过
k
k
k以及相对距离为
k
,
2
k
,
3
k
,
⋯
k,2k,3k,\cdots
k,2k,3k,⋯的token,其他所有token的注意力都设为0,如下图所示:
我们来具体观察一下,实际上图中的第二行就是涉及到的attention的token内容,可以看出首先关注了附近四个token,其次是
2
k
,
3
k
2k,3k
2k,3k距离的token,那么为什么这么做呢?使用 sparse attention的好处主要有以下两点:
论文中供训练了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(多个任务样本)三类。
GPT-3的数据集:GPT-3的训练数据包括低质量的Common Crawl,高质量的WebText2、Books1、Books2和Wikipedia。GPT-3根据数据集的不同质量赋予了不同的权值,权值越高的在训练的时候越容易抽样到(见下图)。为了清理脏数据,OpenAI做了以下的数据处理:
最终处理完成后使用的数据规模约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。
缺点:
GPT-3虽然在各大NLP任务以及文本生成的能力上令人惊艳,但是他仍然还是会生成一些带有偏见的,不真实的,有害的造成负面社会影响的信息,而且很多时候,他并不按人类喜欢的表达方式去说话。在这个背景下,OpenAI提出了一个概念“Alignment”,意思是模型输出与人类真实意图对齐,符合人类偏好。因此,为了让模型输出与用户意图更加对齐,就有了InstructGPT这个工作《Training language models to follow instructions with human feedback》。InstructGPT提出了一个理想化语言模型的三大目标:helpful(能帮助用户解决问题)、honest(不能捏造事实,不能误导用户)、harmless(不能对用户或环境造成物理、精神、社会层面的伤害)。
为了实现上述的目标,论文提出了一种基于人类反馈来微调语言模型的方法,使其能够更好地遵循用户的指示,并在各种任务上表现出更高的质量和可信度。基本流程分为以下三个步骤:
最终得到的InstrctGPT相较于GPT-3:
下面详细介绍下InstructGPT数据集构建以及训练流程。
InstructGPT数据集构建:InstructGPT数据集构建可以分为三个阶段。第一个阶段是为了构建初始的指令prompt数据集,具体做法是让标注人员构建下面三种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和RM Dataset都需要人工标注,区别在于前者的生成式的标注要比后者的判别式的标注贵很多,同样的标注时间和成本,联合前者和后者得到的数据要比只用前者得到的数据多很多,在这上面训练出来的模型性能可能会好一些。
InstructGPT训练流程:上文已经介绍,关于InstructGPT的训练流程,论文中分为了三个步骤:有监督微调,奖励模型训练,强化学习训练,如下图所示。实际上可以把它拆分成两种技术方案,一个是有监督微调(SFT),一个是基于人类反馈的强化学习(RLHF),下面我们简单介绍这两种技术方案。
奖励模型(RM):模型结构是把SFT模型最后的unembedding层(就是将模型输出的token embedding转换为logits的那一层)去掉,即最后一层不用softmax,改成一个线性层,这样训练好的RM模型就可以做到输入问题+答案,输出一个标量的分数。RM模型使用6B,而不是175B,主要原因是:
前文已经介绍,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)))]
其中,
也可以参考下图(有个小错误,就是
s
i
g
m
o
i
d
sigmoid
sigmoid函数是将值映射至
(
0
,
1
)
(0,1)
(0,1),而不是
(
−
1
,
1
)
(-1,1)
(−1,1),不过无伤大雅)。
论文中期望当回答
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平均到每一个答案组合上。除此之外,还有几点需要我们注意:
强化学习模型(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
)
)
]
其中:
整体的目标是最大化上述的目标函数,现在分别介绍下目标函数的每一项,也可以参考下面的图片:
最后再给出对目标函数的理解,优化目标是使得上述目标函数越大越好,通过上述介绍,我们知道, o b j e c t i v e ( ϕ ) objective(\phi) objective(ϕ)可分成三个部分,RM打分部分+KL散度部分+GPT-3预训练部分:
回顾下InstructGPT的训练流程,共包含两次对模型的微调:GPT-3模型 ⇒ \Rightarrow ⇒SFT模型 ⇒ \Rightarrow ⇒RL模型,其实这里始终都是同一个模型,只是不同过程中名称不一样。除此之外,在SFT模型 ⇒ \Rightarrow ⇒RL模型阶段,还会依赖于另一个在SFT模型基础上训练的RM模型。InstructGPT训练SFT、RM、RL三个模型的原因(参考ChatGPT技术解析):
最后,我们展示下论文中提到的InstructGPT性能对比结果,可以发现,参数量为13B的InstructGPT模型,性能都要远远好于参数量为175B的GPT-3模型:
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系列是如何进化的。
2022年3月,OpenAI团队又放大招,发布了更强的LLM:GPT-4。虽然无从得知GPT-4的训练细节,但是可以肯定的是,GPT-4采用了更大的模型结构,增加了更多的训练数据。我们可以通过官方博客GPT-4了解下GPT-4的强大能力。目前GPT-4的主要能力点如下:
毫不夸张的说,尽管需要耗费巨大的资源,但是目前国内外各大公司都在或多或少的参与着LLM的军备竞赛,这在一定程度上促进着NLP技术的发展。归因于此,目前已经有一系列LLM陆陆续续问世了。我们无法对这些LLM进行一一介绍,这里挑选一些我们在其基础上做过微调或有使用经验的模型,对它们进行简单介绍。
大模型 | 团队 | 发布时间 | 模型规模 | 是否开源 | Hugging Face | Github |
---|---|---|---|---|---|---|
ChatGLM-6B | 清华大学 | 2023 | 6B | 已开源,不可商用(获取许可证) | ChatGLM-6B | ChatGLM-6B |
ChatGLM2-6B | 清华大学 | 2023 | 6B | 已开源,不可商用(获取许可证) | ChatGLM2-6B | ChatGLM2-6B |
LLaMA2-7B | Meta | 2023 | 7B | 已开源,可商用 | LLaMA2-7B | LLaMA、Chinese-LLaMA-Alpaca-2 |
baichuan-7B | 百川智能 | 2023 | 7B | 已开源,可商用 | baichuan-7B | baichuan-7B |
文心一言 | 百度 | 2023 | 千亿 | 未开源,不可商用 | 暂无 | 暂无 |
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 引入了如下新特性:
有关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领域主流的预训练框架可以分为三种:
上述三种预训练架构的训练目标也略有不同:
三种预训练框架各有利弊,没有一种框架在以下三种领域的表现最佳:自然语言理解(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,⋯,szi−1],此时,可定义预训练目标为:
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}})]
θmaxEz∼Zm[i=1∑mlog pθ(szi∣xcorrupt,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,⋯,szi−1]进行拼接,预测生成的文本是跨度
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θ(szi∣xcorrupt,sz<i)=j=1∏lip(si,j∣xcorrupt,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]文本片段的采样设计。
最终通过以上方式,GLM自动学习一个双向编码器(Part A)和一个单向解码器(Part B)统一的模型。
Multi-Task Pretraining(多任务预训练):上述例子中,GLM掩盖了短跨度,适用于NLU任务。而论文的关注点是预训练一个能同时处理NLU和文本生成的模型。因此,论文研究了一个多任务预训练的设置。在这个设置中,增加一个生成较长文本的目标,与空白填充目标共同优化。具体来说,论文中考虑以下两个目标。
这两个新目标的定义与原目标相同。唯一不同的是的跨度数量和跨度长度。
至此,我们介绍了原始GLM的预训练原理及模型结构。更多细节大家可参考GLM: 自回归空白填充的通用语言模型预训练、ChatGLM官方博客。接下来,我们简单介绍下ChatGLM的基座模型:GLM-130B。
2023年7月,Meta推出了完全可商用的开源大模型LLaMA2。这里简单介绍下两代LLaMA的共有结构以及LLaMA2相较于初代LLaMA的优化点。
总的来说,LLM之所以主要都用Decoder-only架构,除了训练效率和工程实现上的优势外,一方面,在理论上是因为Encoder的双向注意力会存在低秩问题,这可能会削弱模型表达能力;另一方面,就生成任务而言,引入双向注意力并无实质好处。而Encoder-Decoder架构之所以能够在某些场景下表现更好,大概只是因为它多了一倍参数。所以,在同等参数量、同等推理成本下,Decoder-only架构就是最优选择了。具体参考LLM为什么都用Decoder only架构?。
就目前的发展趋势而言,大模型的训练基本就是按照Pretrain、Instruction-Tuning、RLHF三步走模式进行,因此技术上基本没什么问题,主要瓶颈存在于算力,当然对于中文大模型来说,获取高质量中文数据也是个问题。国内能耗费大规模成本进行大模型训练的厂商屈指可数,因此个人认为,未来的发展趋势,可能有以下四个方向:
了解LLM,看这一篇就够了!!!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。