前言:什么是 Fine-tuning
在机器学习中,微调是一种迁移学习方法,其中将预训练模型的权重训练到新数据上。微调可以在神经网络的一部分层上进行,也可以在整个网络上进行。在第一种情况下,未进行微调的层被 > “冻结”,在反向传播步骤中不会更新。对于某些结构,如卷积神经网络,通常会保持较早的层冻结,因为它们已经被证明可以捕捉较低级别的特征,而与此不同的是,后面的层通常关注与模型训练任务更相关的高级特征。
微调在自然语言处理(NLP)领域也很常见,尤其是在语言建模领域。像 OpenAI 的 GPT-2 这样的大型语言模型可以通过对下游 NLP 任务进行微调来获得比预训练模型通常能达到的更好结果。通常通过在大型和通用语料库上预训练的模型,将模型的参数作为起点,并添加从零开始训练的任务特定层来进行微调。完全微调模型也很常见,通常会产生更好的结果,但这是一种更加计> 算密集的方法。完全微调还更容易过拟合,并可能导致模型在离群值分布上表现不佳。微调通常使用有监督学习方法进行,但也有使用弱监督方法微调模型的技术。最近,通过人类反馈的强化学习也被用于微调像 ChatGPT(这是 GPT-3 的微调版本)和 Sparrow 这样的语言模型。
1. Pre-train + Fine-tuning
1.1 为什么要精调
- 对于数据集本身很小的情况,从头开始训练具有几亿、几十亿甚至上百亿参数的大型神经网络是不现实的,因为越大的模型对数据量的要求越大,过拟合无法避免。这时候如果还想用上大型神经网络的超强特征提取能力,只能靠精调已经训练好的模型。
- 可以降低训练成本(时间成本、计算架构成本、大语料成本):如果使用导出特征向量的方法进行迁移学习,后期的训练成本非常低,用低配 GPU 就可以训练。
- 前人花很大精力训练出来的模型在大概率上会比你自己从零开始搭的模型要强悍,没有必要重复造轮子。
人工智能的快速发展推动了大模型的广泛应用,在语言、视觉、语音等领域的应用效果已经越来越好。但是,训练一个大模型需要巨大的计算资源和时间成本(计算架构壁垒、大语料壁垒),为了减少这种资源的浪费,模型精调已经成为一种流行的技术。
模型精调是指在预训练模型的基础上,通过在小数据集上的训练来适应新的任务。
传统的线性过程思维是” 因果一一对应 “,我们设定了什么规则和指令,结果就是完全一一按照我们的规则 / 指令生成的,不会产生模糊和意外之外的结果。但是到了神经网络,尤其是超大型神经网络中,由于参数空间和语料库都十分巨大,因此在超高维的参数空间中,知识的储存是以一种很抽象 / 高维的形式存在。通俗上理解就是:大模型通过学习了大语料之后,储存了大语料中包含的深层次知识,这些知识在不同的子领域都具备一定程度的迁移能力。所谓精调,本质上就是通过新领域语料库对模型的权重参数进行指向性精调(或者是增加一些新的结构,在新的结构上进行精调),从而使新的大模型具备在新的垂直领域上的预测路径。
1.2 如何进行精调
AIGC(AI 芯片)的出现进一步加快了大模型的推广,可以提供更快的计算速度和更大的存储容量,精调主要有如下几种分类:
- 精调所有层:将预训练模型的所有层都参与精调,以适应新的任务。
- 精调顶层:只精调预训练模型的顶层,以适应新的任务。
- 冻结底层:将预训练模型的底层固定不变,只对顶层进行精调。
- 逐层精调:从底层开始,逐层精调预训练模型,直到所有层都被精调。
- 迁移学习:将预训练模型的知识迁移到新的任务中,以提高模型性能。(这种方法通常使用精调顶层或冻结底层的方法。)
以下使用 Paddle 实现五种精调方法:
1.2.1 精调所有层
1 | import paddle |
加载预训练模型
1 | model = GPT2ForPretraining.from_pretrained ('gpt2-medium-en') |
定义新的分类头
1 | class_num = 2 |
将新的分类头添加到模型中
1 | model.cls = cls |
通过精调所有层来适应新任务
1 | optimizer = paddle.optimizer.Adam (learning_rate=1e-5, parameters=model.parameters ()) |
1.2.2 精调顶层
1 | import paddle |
加载预训练模型
1 | model = GPT2ForPretraining.from_pretrained ('gpt2-medium-en') |
固定模型底层,只精调顶层
1 | for param in model.parameters (): |
定义新的分类头
1 | class_num = 2 |
将新的分类头添加到模型中
1 | model.cls = cls |
通过精调顶层来适应新任务
1 | for param in model.cls.parameters (): |
1.2.3 冻结底层
1 | import paddle |
加载预训练模型和分词器
1 | model = GPTForPretraining.from_pretrained ('gpt-cpm-large-cn') |
构造数据集和数据加载器
1 | train_ds = [[' 今天天气不错 '], [' 明天要下雨 '], [' 这个季节很适合旅游 ']] |
构造优化器和损失函数
1 | optimizer = paddle.optimizer.AdamW (parameters=model.parameters (), learning_rate=1e-4) |
冻结底层
1 | for layer in model.layers [:6]: |
精调模型
1 | for epoch in range (3): |
保存精调后的模型
1 | paddle.save (model.state_dict (), 'gpt-cpm-large-cn-finetuned |
1.2.4 逐层精调
1 | import paddle |
加载预训练模型和分词器
1 | model = GPTForPretraining.from_pretrained ('gpt-cpm-large-cn') |
构造数据集和数据加载器
1 | train_ds = [[' 今天天气不错 '], [' 明天要下雨 '], [' 这个季节很适合旅游 ']] |
构造优化器和损失函数
1 | optimizer = paddle.optimizer.AdamW (parameters=model.parameters (), learning_rate=1e-4) |
迁移学习精调模型
1 | for epoch in range (3): |
保存精调后的模型
1 | paddle.save(model.state_dict (), 'gpt-cpm-large-cn-finetuned-transfer-learning.pdparams') |
2. Lightweight-finetuning 技术
随着计算算力的不断增加,以 transformer 为主要架构的预训练模型进入了百花齐放的时代。经过海量数据训练的模型相比于一般的深度模型而言,包含更多的参数,动辄数十亿、数百亿。在针对不同下游任务做精调时,存储和训练这种大模型是十分昂贵且耗时的。更麻烦的是,如果每一个垂直领域都要重新训练一个新的” 庞然大物 “出来,显然在时间和空间上都是不可接受的。
为了解决这个问题,各种 lightweight-finetuning 的方法被提了出来,相比于全参数精调,只需要以一个较小的训练和存储代价就可以取得和全模型精调相当的结果,大幅度降低了资源和时间成本。
2.1 Adapters-tune
首先,adapter 方法的原理并不复杂,它是通过在原始的预训练模型中的每个 transformer block 中加入一些参数可训练的模块实现的。
假设原始的预训练模型的参数为 ω,加入的 adapter 参数为 υ,在针对不同下游任务进行调整时,只需要将预训练参数固定住,只针对 adapter 参数 υ 进行训练。通常情况下,参数量 υ<<ω, 因此在对多个下游任务调整时,只需要调整极小数量的参数,大大的提高了预训练模型的扩展性和实用性。
对于 adapter 模块的网络组成,不同文章中针对不同任务略有不同。但是比较一致的结论是,bottleneck 形式的两层全连接神经网络就已经可以满足要求。每个 transformer 层中有两个 adapter 模块,在每个 adapter 模块中,先将经过多头注意力和前馈层输出的 output 做一个降维的映射。经过一个非线性激活层后,再将特征矢量映射回原始的维度。在下游训练任务中,只更新 adapter 模块和 layer Norm 层(下图中的绿色部分)。
相比于预训练模型的全参数精调,Adapter 方法的优势十分明显:
- 针对不同下游任务可保持预训练模型不变,仅需训练 adapter 模块的少量参数,训练代价小,可移植性强。
- 对于不同任务的连续学习(continual learning)而言,由于在训练不同任务时只需要训练不同的 adapter,其他模型参数保持不变,避免了在学习新任务时对过往任务的遗忘。
huggingface 开源了 transformer 库,在原来框架的基础上增添了 adapter 模块的训练和扩展,只需要在原来的训练脚本中更改不超过两行的代码,就可以针对不同的下游任务无缝训练新的 adapter 模块,并且整个 adapter 模块的参数和原始的预训练模型参数是完全独立存储的。
3. Pre-train + Prompt-tuning + Predict
3.1 什么是 Prompt-Tuning
自从 GPT、EMLO、BERT 的相继提出,以 Pre-training + Fine-tuning 的模式在诸多自然语言处理(NLP)任务中被广泛使用,其先在 Pre-training 阶段通过一个模型在大规模无监督语料上预先训练一个预训练语言模型(Pre-trained Language Model,PLM),然后在 Fine-tuning 阶段基于训练好的语言模型在具体的下游任务上再次进行精调(Fine-tuning),以获得适应下游任务的模型。这种模式在诸多任务的表现上超越了传统的监督学习方法,不论在工业生产、科研创新中均作为新的主流方式。然而,这套模式也存在着一些问题:
- 在大多数的下游任务精调时,下游任务的目标与预训练的目标差距过大导致提升效果不明显
- 精调过程中依赖大量的监督语料等
因此,以 GPT-3、PET 为首提出了 Prompt-Tuning(一种基于预训练语言模型的新的精调范式),其旨在通过添加模板的方法来避免引入额外的参数,从而让语言模型可以在小样本(Few-shot)或零样本(Zero-shot)场景下达到理想的效果。
Prompt-Tuning 又可以称为 Prompt、Prompting、Prompt-based Fine-tuning 等。
简单的来说,Prompt-Tuning 的动机旨在解决目前传统 Fine-tuning 的两个痛点问题:
- 降低语义差异(Bridge the gap between Pre-training and Fine-tuning):预训练任务主要以 Masked Language Modeling(MLM)为主,而下游任务则重新引入新的训练参数,因此两个阶段的目标通常有较大差异。
- 避免过拟合(Overfitting of the head):由于在 Fine-tuning 阶段需要新引入额外的参数以适配相应的任务需要,因此在样本数量有限的情况容易发生过拟合,降低了模型的泛化能力。
3.2 Prompt-Tuning 的基本过程
Prompt-Tuning 执行如下步骤:
- 构建模板(Template Construction):通过人工定义、自动搜索、文本生成等方法,生成与给定句子相关的一个含有 [MASK] 标记的模板。例如 It was [MASK].,并拼接到原始的文本中,获得 Prompt-Tuning 的输入:[CLS] I like the Disney films very much. [SEP] It was [MASK]. [SEP]。将其喂入 BERT 模型中,并复用预训练好的 MLM 分类器,即可直接得到 [MASK] 预测的各个 token 的概率分布。
- 标签词映射(Label Word Verbalizer):因为 [MASK] 部分我们只对部分词感兴趣,因此需要建立一个映射关系。例如如果 [MASK] 预测的词是 “great”,则认为是 positive 类,如果是 “terrible”,则认为是 negative 类。不同的句子应该有不同的模板和标签词,因为每个句子可能期望预测出来的标签词都不同,因此如何最大化的寻找当前任务更加合适的模板和标签词是 Prompt-tuning 非常重要的挑战。
- 训练:根据 Verbalizer,则可以获得指定标签词的预测概率分布,并采用交叉信息熵进行训练。此时因为只对预训练好的 MLM head 进行精调,所以避免了过拟合问题。
引入的模板和标签词本质上也属于一种数据增强,通过添加提示的方式引入先验知识。prompt-tuning 相比于传统的 fine-tune 范式,最大的创新点就在于引入了”context template“的概念,通过预定义的模板,大幅度限定了模型精调的优化方向,减少了搜索空间,使得 fine-tune 出来的模型在具体的任务领域有更好的泛化性能,甚至具备 zero-shot 的能力。
3.3 Prompt-Tuning 的本质
最初的 Prompt-Tuning 主旨在于设计 Template 和 Verbalizer(即 Pattern-Verbalizer Pair)来解决基于预训练模型的小样本文本分类,然而事实上,NLP 领域涉及到很多除了分类以外其他大量复杂的任务,例如抽取、问答、生成、翻译等。这些任务都有独特的任务特性,并不是简单的 PVP 就可以解决的,
以下为三个关于 Prompt 的本质:
- 一种对任务的指令,可以作为一种信息增强。
- 一种对预训练任务的复用,实现基于 Prompt 的统一范式。
- 一种参数有效性学习。
3.3.1 一种对任务的指令,可以作为一种信息增强
简单的来说,就是告诉模型需要做什么任务,输出什么内容。
当数据集不同(乃至样本不同)的时候,我们期望模型能够自适应的选择不同的模板,这也相当于说不同的任务会有其对应的提示词信息。这一类具备任务特性的模板可以称之为指令(Instruction)。
下面展示几个任务设计的指令模板:
看似设计指令是一件容易的事情,但是在真实使用过程中,预训练模型很难 “理解” 这些指令,根据研究工作发现,主要总结如下几个原因:
- 预训练模型不够大:我们常使用的 BERT-base、BERT-large、RoBERTa-base 和 RoBERTa-large 只有不到 10 亿参数,相比于现如今 GPT-3、OPT 等只能算作小模型,有工作发现,小模型在进行 Prompt Tuning 的时候会比 Fine-tuning 效果差,是因为小模型很容易受到模板的影响。对比一下传统的 Fine-tuning,每个样本的输入几乎都是不同的,然而基于 Prompt 的方法中,所有的样本输入都会包含相同的指令,这就导致小模型很容易受到这些指令带来的干扰。
- 缺乏指令相关的训练:这些小模型在预训练阶段没有专门学习过如何理解一些特殊的指令。
3.3.2 一种对预训练任务的复用,实现基于 Prompt 的统一范式
我们需要思考,上述所讲的内容为什么要设计 Template(和 Verbalizer)。
回顾之前我们介绍的几个预训练语言模型,我们发现目前绝大多数的双向预训练语言模型都包含 Masked Language Modeling(MLM),单向预训练语言模型都包含 Autoregressive Language Modeling(ALM),这些任务是预训练目标,本质上是预测被 mask 的位置的词,在训练时让模型理解语言的上下文信息。之所以设计模板和指令,就是希望在下游任务时能够复用这些预训练的目标,避免引入新的参数而导致过拟合。
因此,我们可以将 Prompt 升华到一个新的高度,即 Prompt Tuning 的本质是复用预训练语言模型在预训练阶段所使用的目标和参数。
由于绝大多数的语言模型都采用 MLM 或 ALM 进行训练,所以我们现如今所看到的大多数基于 Prompt 的分类都要设计 Template 和 Verbalizer。那么我们是否可以极大化地利用 MLM 和 ALM 的先验知识在不同的下游任务上获得更好的表现?是否可以设计一个全新的预训练任务来满足一些下游任务的需求呢?
我们介绍几个充分利用这个思想的方法:
- 万物皆可生成:将所有任务统一为文本生成,极大化利用单向语言模型目标。
- 万物皆可抽取:将所有任务统一为抽取式阅读理解,并设计抽取式预训练目标。
- 万物皆可推理:将所有任务建模为自然语言推断(Natural Language Inference)或相似度匹配任务。
3.3.2.1 万物皆可生成 —— 基于生成的 Prompt 范式统一
在含有单向 Transformer 的语言模型中(例如 GPT、BART),都包含自回归训练目标,即基于上一个 token 来预测当前的 token,而双向语言模型中的 MLM 可以视为只生成一个 token 的自回归模型,为此,我们则可以将分类任务视为一种特殊的文本生成,并配上 Verbalizer,这样,所有的 NLP 任务都可以统一为生成任务。针对不同的任务,只需要提供对应的指令和模板即可(由于是使用单向语言模型,因此没有 mask token,需要生成的部分置于文本末尾)。
下面给出几个事例:
3.3.2.2 万物皆可抽取 —— 基于抽取式阅读理解的 Prompt 范式统一
基于生成的方法存在如下缺点:
- 必须让待生成的部分置于文本末尾,此时会约束指令和模板的设计,不利于灵活运用。
- 由于是开放式生成,生成的内容无法控制,且依赖于文本的长度等。
- 对于一些具有条件限制的任务,例如多项选择、信息抽取等,生成的内容或许不符合这些条件。例如在做实体抽取的时候,需要确保生成的实体是在文本中出现的。
为此,“万物皆可抽取” 的思想可以解决此类问题,其思想指将所有自然语言理解任务转换为抽取式阅读理解的形式,下面给出形式化的定义:
除了抽取式阅读理解任务外,其他 NLP 任务如何转换为这个形式呢?本质上还是在如何设计模板和指令。
下面给出几个事例:
可以发现,如果是分类型的任务,只需要通过指令和模板的形式将所有类别罗列起来即可。在训练时,可以采用两种方法:
- 设计抽取式预训练目标,在无标注语料上进行自监督训练。
- 按照阅读理解的形式统一所有任务范式,并混合所有任务进行 Cross-task Learning,再在新的任务上进行测试。
3.3.2.3 万物皆可推理 —— 基于 NLI 的 Prompt 范式统一
另外一个方法则是将所有任务建模为 NLI 形式,其与上文介绍的 MPT 比较类似,除了 MPT 以外,《Entailment as Few-Shot Learner》(EFL)和 NSP-BERT 也是类似的方法,其思想是复用 BERT 中的 Next Sentence Prediction(NSP)的预训练目标。下面给出几个事例:
3.3.3 参数有效性学习
实现 Prompt-Tuning 只需要考虑如何设计模板或指令,而模型和训练目标则都是复用预训练阶段的,即在整个训练过程中,无须添加任何参数(或只需要添加非常少量的与模板有关的参数),而其他参数都是训练好的。基于这个思想,我们再一次将 Prompt 升华到更高的层面 ——Prompt 的本质是参数有效性学习(Parameter-Efficient Learning,PEL)。
- 参数有效性学习的背景:在一般的计算资源条件下,大规模的模型(例如 GPT-3)很难再进行精调,因为所有的参数都需要计算梯度并进行更新,消耗时间和空间资源。为了解决这个问题,参数有效性学习被提出,其旨在确保模型效果不受太大影响的条件下尽可能地提高训练的时间和空间效率。
- 参数有效性训练:在参数有效性学习过程中,大模型中只需要指定或额外添加少量的可训练参数,而其余的参数全部冻结,这样可以大大提高模型的训练效率的同时,确保指标不会受到太大影响。
3.4 面向超大规模模型的 Prompt-Tuning
Prompt-Tuning 发展的过程中,有诸多工作发现,对于超过 10 亿参数量的模型来说,Prompt-Tuning 所带来的增益远远高于标准的 Fine-tuning,小样本甚至是零样本的性能也能够极大地被激发出来。
- 得益于这些模型的参数量足够大
- 训练过程中使用了足够多的语料
- 同时设计的预训练任务足够有效
下面介绍几个面向超大规模的 Prompt-Tuning 方法,分别为:
- 上下文学习 In-Context Learning(ICL):直接挑选少量的训练样本作为该任务的提示。
- 指令学习 Instruction-tuning:构建任务指令集,促使模型根据任务指令做出反馈。
- 思维链 Chain-of-Thought(CoT):给予或激发模型具有推理和解释的信息,通过线性链式的模式指导模型生成合理的结果。详见:ICL、CoT、ToT、AutoGPT
3.4.1 In-Context Learning(上下文学习)
目前,向语言模型通过 prompting 可以在小样本场景下得到很大的成功,例如 GPT-3。然而原始的语言模型在预训练时并没有针对 in-context 进行优化。
在 fine-tuning 阶段,给定一系列的训练任务,每一个任务都有相应的指令,并对应的少量样本(输入 / 输出对)。在测试阶段,给定一个新的不可见任务,包含对应的指令和少量样本(输入 / 输出对),旨在让模型能够对测试样本预测其类别。
如下图,给定一个情感分析任务:
3.4.2 Instruction-tuning(指令学习)
面向超大规模模型第二个 Prompt 技术是指令学习。Prompt 的本质之一是任务的一种指令,因此,在对大规模模型进行精调时,可以为各种类型的任务定义指令,并进行训练,来提高模型对不同任务的泛化能力。什么是指令呢?如下图所示:
假设是一个 Question Generation 任务,那么可以为这个任务定义一些指令:
- Title:任务的名称;
- Definition:任务的定义,说明这个任务的本质和目的;
- Things to avoid:说明这个任务的注意事项,例如需要避免什么等等;
- Positive / Negative Examples:给出正确和错误的例子,作为提示;
- Prompt:当前任务的提示信息;
当许多任务都按照这种模式定义好模板,让模型在指令化后的数据上进行精调,模型将可以学会如何看到指令做预测。
InstructionGPT:
三个步骤:(1)监督微调(SFT),(2)奖励模型(RM)训练,以及(3)通过该奖励模型上的近端策略优化(PPO)进行强化学习
4. LoRA(Low-Rank Adaptation)
精调大规模语言模型到特殊领域和任务是自然语言处理的重要课题之一。但随着模型规模的不断扩大,精调模型的所有参数(所谓 full fine-tuning)的可行性变得越来越低。关于什么是 LoRA 可以参考原始论文。
4.1 为何引入 LoRA
为解决精调大规模语言模型到不同领域和任务的挑战,如上文提到已有多种方案。但这些方法存在如下问题:
- Adapters 引入额外的推理延迟 (由于增加了模型层数)
- Prefix-Tuning 难于训练,且预留给 prompt 的序列挤占了下游任务的输入序列空间,影响模型性能
4.1.1 Adapter 引入推理延迟
显然,使用 Adapter 增加模型层数会增加推理的时长。
简单来说,adapter 就是固定原有的参数,并添加一些额外参数用于精调。Adapter 会在原始的 transformer block 中添加 2 个 adapter,一个在多头注意力后面,另一个这是 FFN 后面。显然,adapter 会在模型中添加额外的层,这些层会导致大模型在推理时需要更多的 GPU 通信,而且也会约束模型并行。这些问题都将导致模型推理变慢。
4.1.2 很难直接优化 Prompt
prefix-tuning 方法是受语言模型 in-context learning 能力的启发,只要有合适的上下文则语言模型可以很好的解决自然语言任务。但是,针对特定的任务找到离散 token 的前缀需要花费很长时间,prefix-tuning 提出使用连续的 virtual token embedding 来替换离散 token。
具体来说,对于 transformer 中的每一层,都在句子表征前面插入可训练的 virtual token embedding。对于自回归模型(GPT 系列),在句子前添加连续前缀,prefix-tuning 并没有添加太多的额外参数。但是,prefix-tuning 难以优化,且会减少下游任务的序列长度,一定程度上会影响模型性能。
4.2 LoRA 的原理简介
LoRA 本质是对大模型精调的一种方法,当预训练大模型很大时,重新训练所有模型参数的精调变得不可太行,例如 GPT3 的 175B。提出的 LoRA 采用低秩分解矩阵,冻结了预训练模型的权重,并将低秩分解矩阵注入到 transformer 的每一层,减少了训练参数量。
如上图所示们对于某个线性层而言,左边是模型原有的参数,在训练过程中是冻结不变的,右边是 LoRA 方法增加的低秩分解矩阵。
在原始 PLM 旁边增加一个旁路,做一个降维再升维的操作,来模拟所谓的 intrinsic rank。训练的时候固定 PLM 的参数,只训练降维矩阵 A 与升维矩阵 B。而模型的输入输出维度不变,输出时将 BA 与 PLM 的参数叠加。用随机高斯分布初始化 A,用 0 矩阵初始化 B,保证训练的开始此旁路矩阵依然是 0 矩阵。
训练过程中,优化器只优化右边这一部分的参数,两边的矩阵会共用一个模型的输入,分别进行计算,最后将两边的计算结果相加作为模块的输出。不同于之前的参数高效精调的 adapter:
- adapter 是在模块的后面接上一个 mlp,对模块的计算结果进行一个后处理。
- LoRA 是和模块的计算并行的去做一个 mlp,和原来的模块共用一个输入。
大模型其实是过参数化的, 有更小的一个内在维度,大模型在任务适配(instruction-tune)过程中,参数的改变量是低秩的。
总之,基于大模型的内在低秩特性,增加旁路矩阵来模拟全模型参数精调,LoRA 通过简单有效的方案来达成轻量精调的目的。
引申一下,GPT 的本质是对训练数据的有效压缩,从而发现数据内部的逻辑与联系,LoRA 的思想与之有相通之处,原模型虽大,但起核心作用的参数是低秩的,通过增加旁路,达到事半功倍的效果。