前言
所谓微调,就是让一个已经预训练好的大型语言模型针对特定任务再调整一下参数。说白了,就是在跟任务相关的数据集上再训练一遍模型。至于微调多少、怎么调,取决于任务的复杂度和数据集的大小。在深度学习领域,微调是一项很成熟的技术,用来提升预训练模型的表现。除了大家熟悉的ChatGPT,市面上还有很多预训练模型都可以拿来微调。
PEFT是什么
PEFT的全称是Parameter-Efficient Fine-Tuning,由Hugging Face开源,是一套参数高效微调大模型的工具包。它集成了四种主流微调方法,核心思路很直接:只微调少量参数,就能达到接近全参数微调的效果。这意味着即便GPU资源有限,也能把大模型调起来。
微调方法
从大方向上看,微调可以分成全微调和部分微调两类:
- 全微调(Full Fine-tuning):对整个预训练模型的所有参数都进行更新和优化。这种方式适合任务与预训练模型差异较大的场景,或者需要模型高度灵活和自适应的时候。缺点很明显——计算资源和时间成本都很高,但效果通常也是最好的。
- 部分微调(Repurposing):只更新模型的顶层或少数几层,底层的通用知识保持不变。这适用于目标任务与预训练模型有一定相似性、或者数据集较小的情况。计算量比全微调少很多,但某些任务上性能可能会打折扣。
具体到操作层面,微调预训练模型有几种常见做法:
- 微调所有层:全部参数参与训练。
- 微调顶层:只动顶层,不动底层。
- 冻结底层:底层固定,只调顶层。
- 逐层微调:从底层开始一层一层地调,直到所有层都过一遍。
- 迁移学习:把预训练模型的知识迁移到新任务中,通常配合微调顶层或冻结底层来用。
Fine tuning
经典的Fine tuning方法,就是把预训练模型和少量特定任务的数据一起继续训练。训练过程中,预训练模型的权重会根据新任务进行调整。到底需要调多少?关键看预训练语料和任务语料的相似度。如果两者很接近,稍微调一下就行;如果差别很大,那就要多花些功夫。

Prompt Tuning(P-tuning)
Prompt Tuning是2021年谷歌在论文《The Power of Scale for Parameter-Efficient Prompt Tuning》中提出的。在参数高效微调方法里,Prompt tuning(也就是常说的P-Tuning)算是最简单的实现之一——固定模型前馈层的参数,只更新部分embedding参数,就能低成本地微调大模型。

经典的Prompt tuning并不更新底层模型的任何参数,而是把精力放在精心构造输入提示或模板上,让预训练模型按期望的方向生成输出。它的核心结构是用一个prompt encoder(BiLSTM+MLP)把一些伪提示(pseudo prompt)先编码成离散token,然后和input embedding拼接在一起。同时利用LSTM做重新参数化(Reparameterization)来加速训练,并引入少量自然语言提示的锚字符(比如Britain)来提升效果。最终结合(capital, Britain)生成结果,再优化encoder部分。

不过P-tuning v1有两个明显的短板:任务不通用,规模也不通用。在一些复杂的自然语言理解任务(NLU)上效果很差,而且预训练模型的参数量不能太小。具体来说,论文里提到了几个观察:
- Prompt长度的影响:模型参数达到一定量级时,prompt长度为1也能取得不错的效果,长度到20就非常好了。
- Prompt初始化方式的影响:Random Uniform方式明显弱于其他两种,但模型参数一旦足够大,这种差异就消失了。
- 预训练方式:LM Adaptation效果更好,但模型规模大到一定程度,差异也几乎没有了。
- 微调步数的影响:模型参数较小时,步数越多效果越好。同样地,当参数规模上去之后,zero shot也能有不错的表现。当参数达到100亿规模时,效果与全参数微调已经没什么区别。
代码示例:
from peft import PromptTuningConfig, get_peft_model
peft_config = PromptTuningConfig(task_type="SEQ_CLS", num_virtual_tokens=10)
model = AutoModelForCausalLM.from_pretrained(model_name_or_path, return_dict=True)
model = get_peft_model(model, peft_config)
Prefix Tuning
Prefix Tuning出自2021年的论文《Prefix-Tuning: Optimizing Continuous Prompts for Generation》。和全参数微调不同,它是在输入token之前构造一段任务相关的virtual tokens(称为Prefix),训练时只更新Prefix部分的参数,Transformer的其他部分全部固定。
这其实跟构造Prompt有点像,但区别在于:Prompt是人工显式构造的提示,无法更新参数;而Prefix是可学习的隐式提示。手动尝试最优的提示就像大海捞针,于是有了自动离散提示搜索的方法,但问题是提示是离散的,神经网络是连续的,所以找到的最优提示很可能只是次优解。

代码示例:
peft_config = PrefixTuningConfig(task_type="CAUSAL_LM", num_virtual_tokens=20)
model = AutoModelForCausalLM.from_pretrained(model_name_or_path, return_dict=True)
model = get_peft_model(model, peft_config)
在P-tuning的加持下,GPT在NLU领域的性能甚至可以超过BERT。下面是细致的对比:

P-tuning v2
V2版本基于P-tuning和prefix-tuning技术,引入了Deep Prompt Encoding和Multi-task Learning等策略进行优化。实验结果表明,只精调0.1%的参数,在330M到10B不同参数规模的LM模型上,都能取得和Fine-tuning相当的性能。

从论文标题《P-Tuning v2: Prompt Tuning Can Be Comparable to Fine-tuning Universally Across Scales and Tasks》就能看出来,P-Tuning v2的目标很明确:让Prompt Tuning在不同参数规模的预训练模型、不同下游任务上都能匹敌Fine-tuning。而之前的Prompt Tuning和P-tuning在这两个方面都有局限:
- 不同模型规模:只有预训练模型参数足够大时,才能达到和Fine-tuning类似的效果;参数规模较小时效果很差。
- 不同任务类型:在序列标注(sequence tagging)任务上表现都很差。

从v1到v2的可视化对比:蓝色部分代表参数冻结,橙色代表可训练部分。右侧的P-tuning v2把continuous prompt加在序列前端,并且在每一层都加入可训练的prompts。左图v1模型只把prompt插入input embedding中,可训练的参数受限于句子长度。此外P-Tuning v2还有以下改进:
- 移除了Reparamerization加速训练方式;
- 采用多任务学习优化:基于多任务数据集的Prompt进行预训练,再适配下游任务;
- 舍弃了词汇Mapping的Verbalizer,重新利用[CLS]和字符标签,跟传统finetune一样用cls或token的输出做NLU,增强通用性,可以适配到序列标注任务。
P-Tuning v2的几个关键设计因素:
- Reparameterization:Prefix Tuning和P-tuning中都有MLP来构造可训练的embedding。论文发现在自然语言理解领域,面对不同任务和数据集,这种方法可能带来完全相反的结论。
- Prompt Length:不同任务对应的最合适prompt长度不一样——简单分类任务下length=20最好,而复杂任务需要更长的prompt。
- Multi-task Learning:多任务对P-Tuning v2是可选的,但它能提供更好的初始化,进一步提升性能。
- Classification Head:使用LM head来预测动词是Prompt Tuning的核心,但在完整数据设置中并非必要,而且与序列标注不兼容。P-tuning v2采用和BERT一样的方式,在第一个token处应用随机初始化的分类头。
代码示例:
peft_config = PrefixTuningConfig(task_type="SEQ_CLS", num_virtual_tokens=20)
model = AutoModelForSequenceClassification.from_pretrained(model_name_or_path, return_dict=True)
model = get_peft_model(model, peft_config)
AdaLoRA
预训练语言模型中的不同权重参数对下游任务的贡献是不一样的。这就需要更智能地分配参数预算,在微调过程中高效地更新那些对模型性能贡献大的参数。具体来说,通过奇异值分解将权重矩阵分解为增量矩阵,并根据新的重要性度量动态调整每个增量矩阵中奇异值的大小。这样只更新那些贡献大或必要的参数,从而在提高模型性能的同时提升参数效率。
代码示例:
peft_config = AdaLoraConfig(peft_type="ADALORA", task_type="SEQ_2_SEQ_LM", r=8, lora_alpha=32, target_modules=["q", "v"], lora_dropout=0.01)
model = AutoModelForCausalLM.from_pretrained(model_name_or_path, return_dict=True)
model = get_peft_model(model, peft_config)
GPT4模型微调分类
1. Adapter-based Methods(基于适配器的方法)
《Parameter-Efficient Transfer Learning for NLP》提出了针对BERT的PEFT微调方式,拉开了PEFT研究的序幕。核心观点很直接:面对特定下游任务,如果做Full-Finetuning,效率太低;但如果只固定某些层、只微调接近下游任务的几层,效果又不够好。于是他们设计了Adapter结构,嵌入Transformer中。训练时固定预训练模型参数,只对新增的Adapter进行微调。为了保证高效性(引入尽可能少的参数),Adapter采用这样的结构:先经过一个down-project层将高维特征映射到低维,再经过非线性层,然后用up-project层将低维特征映射回原来的高维。同时设计了skip-connection结构,确保最差情况下能退化为identity(类似残差结构)。这种方法节省资源,不需要对整个模型微调。典型例子包括AdapterDrop、Parallel Adapter、Residual Adapter等。

2. Prompt-based Methods(基于提示的方法)
这个分支侧重于使用连续的提示(比如嵌入向量)来调整模型行为,而不是直接修改模型权重。这类方法通常用于生成任务(比如文本生成)。提示作为模型输入的一部分,经过训练后能激发模型产生特定的输出。典型例子包括Prefix-tuning、Prompt tuning等,上文已经详细介绍过。
3. Low-rank Adaptation(低秩适配)
低秩适配方法的核心思想是把模型权重的改变限制在一个低秩子空间内。通常是对权重矩阵进行分解,只微调其中一小部分参数。这既减少了计算资源消耗,又保留了足够的灵活性来学习新任务。LoRA及其变种(Q-LoRA、Delta-LoRA、LoRA-FA等)都属于这个类别。
4. Sparse Methods(稀疏方法)
稀疏方法只更新模型中一小部分参数,这些参数被选为最有可能影响任务性能的,其他参数保持不变。优点在于资源利用效率更高。典型例子包括Intrinsic SAID、Fish Mask、BitFit等。
5. Others(其他方法)
这一分支包含那些不易归类到上述类别的其他方法,或者结合了多种技术的混合方法。可能包括特定的结构改变、算法优化等,目的是提高微调效率或效果。
大模型微调步骤总结
大模型微调的方法很多,每种方法的流程、方式、准备工作和周期都不尽相同。但大部分微调流程都绕不开以下几个主要步骤和准备工作:
- 准备数据集:收集和整理与目标任务相关的训练数据。确保数据质量、标注准确,并做必要的清洗和预处理。
- 选择预训练模型/基础模型:根据目标任务性质和数据集特点,选择合适的预训练模型。
- 设定微调策略:根据任务需求和可用资源,决定是全微调还是部分微调,以及微调的层级和范围。
- 设置超参数:确定学习率、批量大小、训练轮数等。这些超参数的选择直接影响微调性能和收敛速度。
- 初始化模型参数:基于预训练模型的权重初始化微调模型。全微调时所有参数随机初始化;部分微调时只有顶层或少数层随机初始化。
- 进行微调训练:用准备好的数据集和策略训练模型,根据超参数和优化算法逐步调整参数以最小化损失函数。
- 模型评估和调优:训练过程中用验证集定期评估,并根据结果调整超参数或策略,提升模型性能和泛化能力。
- 测试模型性能:微调完成后,用测试集对最终模型评估,获得最终性能指标。
- 模型部署和应用:将微调好的模型部署到实际场景中,并根据实际需求做进一步优化和调整。
