📕高效微调PEFT代码实战
2024-6-6
| 2025-8-12
字数 7584阅读时长 19 分钟
type
status
password
date
slug
summary
category
URL
tags
icon
参数高效微调方法仅对模型的一小部分参数(这一小部分可能是模型自身的,也可能是外部引入的)进行训练,便可以为模型带来显著的性能变化,一些场景下甚至不输于全量微调。
LiaIin V, Deshpande V, Rumshisky A. Scaling down to scale up. A guide to parameter-efficient fine-tuning[J]. arXiv preprint arXiv:2303.15647,2023.   高效微调方法
LiaIin V, Deshpande V, Rumshisky A. Scaling down to scale up. A guide to parameter-efficient fine-tuning[J]. arXiv preprint arXiv:2303.15647,2023. 高效微调方法
  • additive:增量模型。通过增加额外的参数或网络层,实现对预训练模型的微调。例如
  • selective:选择性方法。只对模型的部分层进行调整,减少了训练的参数数量。例如BitFit只更新bias
  • adapters:适配器。设计Adapter结构并嵌入Transformer中。(实际上就是在原始模型部分模块中增加新的网络结构,做旁路)
  • soft prompts: 软体提示。通过对向量的参数进行微调来适应不同的任务。
  • reparametrization-based:基于重参数化。通过寻找低维表示的权重矩阵,减少了需要训练的参数数量。
💡
安装peftpip install peft. peft库并不是所有模型都支持,可以通过PEFT methods页面查看peft库支持哪些模型

显存计算

测量工具

  • 安装库:accelerate, transformers
    • 被Transformers开发库支持的模型,计算值比较稳定, 比如Misteral, Gemma,Qwen1.5等,但是,源代码没有集成进Transformers的一些模型,可能会不可靠。

原理

notion image
在模型的前向传播(Forward)过程中,仅需依次计算各中间层的输出,并将这些中间结果保存下来,这些保存的中间结果被称为激活值(activations)在随后的反向传播阶段,这些激活值用于梯度的计算,从而实现模型参数的更新。而在推理阶段,由于不涉及梯度计算,仅需获取最终的输出结果,因此无需保存中间层的激活值。

模型框架

例如pytorch框架的cuda context会占用大约几百MB显存,与版本有关;

模型基础参数

例如上图中的又如在8B模型中,使用fp16,那么会占用16GB内存。

激活值

例如上图中的临时占用显存,在反向传播阶段,这些激活值用于梯度的计算。一旦当前迭代的反向传播完成,显存就会被收回。
transformer激活参数:层block、b为batch_size、s为序列长度、a为注意力头数。

梯度值

例如上图中的临时占用显存,当前训练迭代的参数更新完成后,下一轮迭代开始前被清空。
💡
使用 LoRA 微调时,激活值的存储量与全量微调完全相同。因为根据链式法则,计算某一层参数的梯度时,会用到前面所有激活层的参数。

优化器

许多优化器需要保存历史信息以调整更新步长。Lora会显著降低优化器的显存占用。
  • Momentum:需要保存上一次的梯度动量。
  • Adam:需要保存一阶矩(动量)和二阶矩(梯度平方的累积),即每个参数对应 2 个额外状态。
  • AdaGrad/RMSprop:需要保存梯度平方的累积(与参数同维度)。

降低显存占用方案

启用激活检查点

背景

模型结构

notion image
notion image

数据集格式

指令微调

数据集
输出
  • 输入input: <bos> Q1 Q2 A1 A2 A3
  • 标签labels: <pad> <pad> A1 A2 A3 <eos>
💡
输入input是QA的拼接,标签labels中Q的部分的被<pad>token填充(mask掉),以便后续计算loss时,Q不纳入计算。

对话微调

数据集
Q1A1 Q1A1Q2A2 Q1A1Q2A2Q3A3
Q1A1Q2A2Q3A3

示例

数据集

数据预处理

notion image
  • input_ids:输入的是真个原始文本,包括填充部分
  • attention_mask:需要计算注意力的地方为1,不需要的地方填充0(例如padding部分)。下面的padding_mask部分
    • labels:不需要计算loss的部分填充padding(一般padding为-100)。chatglm3在代码中写死了损失函数会忽略 labels 中值为 -100 的位置。

    创建模型

    GenerationConfig:在大语言模型(LLM)生成文本时用于控制生成过程和输出特性的配置类
    参数
    描述
    max_length
    限制生成文本的最大长度,避免生成过长的文本。
    min_length
    确保生成文本达到一定的最小长度,防止生成过短或不完整的内容。
    temperature
    用于控制生成文本的随机性。值越高,生成的文本越随机、多样化。
    top_k
    从概率排名前 k 的词中进行采样,有助于减少生成结果的随机性。
    top_p
    把模型输出的所有词按照概率从高到低进行排序,直到累加的概率和达到 p
    repetition_penalty
    对已经出现过的词施加惩罚,降低其再次出现的概率。
    eos_token_id
    指定序列结束的标记,当模型生成该标记时,生成过程结束。
    pad_token_id
    指定填充标记,用于在生成过程中填充序列到固定长度。
    num_beams
    在Beam Search中使用,指定束的数量。
    num_return_sequences
    指定生成的序列数量,用于一次性生成多个不同的结果

    添加Lora适配

    对自注意力+MLP层进行lora
    确定哪些参数使用lora微调
    💡
    添加lora后打印模型,可以看到lora微调层会添加lora_A、lora_B参数

    训练

    模型保存与读取

    技巧

    1. 一般使用几万条数据。在简单的信息提取任务中使用几千条数据,就达到了不错的效果。
    1. 13B模型的fp16 LoRA可能果优于4位qLora 30B模型。

    Freeze

    Freeze是冻结的意思,Freeze方法指的是参数冻结,对原始模型的大部分参数进行冻结,仅训练少部分的参数,这样就可以大大减少显存的占用,从而完成对大模型的微调。

    BitFit

    基本思想

    只更新bias的参数或者部分bias参数。对于Transformer模型而言,涉及到的bias参数有attention模块中计算query,key,value跟合并多个attention结果时涉及到的bias,Layernormalization层的bias参数,MLP层中的bias。
    模型中bias参数量的计算

    代码实战

    • 数据集https://huggingface.co/datasets/shibing624/alpaca-zh 指令微调
    • 预训练模型Langboat/bloom-1b4-zh
    • 参数高效微调方法BitFit

    提示工程

    基本思想

    Prompt-Tuning 的思想:冻结主模型全部参数,通过提示词提升模型效果; Prompt 存在两种形式,一种是 hard prompt, 一种是soft prompt。
    • hard prompt:人工指定输入
    • soft prompt:不用人工指定,进行自学习

    hard prompt

    假设我们将英语句子翻译成德语。我们可以通过各种不同的方式询问模型,如下所示。
    1. "Translate the English sentence '{english_sentence}' into German: {german_translation}”
    1. "English:'{english_sentence}'| German: {german_translation}”
    1. "From English to German::'{english_sentence}'→{german_translation}
    💡
    本质就是人工不断更换模板,尝试哪种提示词更加好用

    Prompt Tuning(soft prompt)

    notion image
    给定一系列个标记,对这些标记进行Embedding,形成一个矩阵,其中是嵌入空间的维度。我们的软提示表示为一个参数,其中是提示的长度。然后,我们的提示与嵌入后的输入连接起来,形成一个单一矩阵,接着像往常一样通过Transformers。在训练过程中只更新提示参数
    💡
    本质上,在训练数据前添加一段Embedding,当作提示词;并训练这段Embedding,提升模型效果。

    代码实战

    transformers 4.33.1
    peft 0.50
    accelerate 0.22.0
    notion image
    • num_virtual_tokens:指定虚拟Token数。在原理篇中,提到过提示虚拟 Token 的长度在20左右时的表现已经不错(超过20之后,提升Prompt token长度,对模型的性能提升不明显了);同样的,这个gap也会随着模型参数规模的提升而减小(即对于超大规模模型而言,即使提示虚拟 Token 长度很短,对性能也不会有太大的影响)
    • task_type:指定任务类型。如:条件生成任务(SEQ_2_SEQ_LM),因果语言建模(CAUSAL_LM)等。
    • prompt_tuning_init_text:用于初始化提示嵌入的文本,在使用文本(TEXT)初始化方法时使用。从词汇表中选择部分token作为prompt的初始值。
    • prompt_tuning_init:提示嵌入的初始化方法。PEFT支持文本(TEXT)和随机(RANDOM)初始化。
    整体代码

    P-Tuning(soft)

    P-Tuning 的思想:在 Prompt-Tuning 的基础上,对 Prompt 部分进行进一步的编码计算,加速收敛。具体来说, PEFT 中支持两种编码方式,一种是 LSTM, 一种是 MLP。
    💡
    对Prompt-Tuning的Embedding输入到LSTM/MLP在与input一起输入到大模型。
    notion image

    代码实战

    Prefix-Tuning(soft)

    生成类模型的解码逻辑是根据历史输入,每次预测一个新的token,然后将新的token加入输入,再预测下一个token。这个过程中,会存在大量的重复计算,因此可以将key和value的计算结果缓存,作为past_key_values输入到下一次的计算中,这一技术又被称之为kv_cache。Prefix-Tuning就是通过past_key_values的形式将可学习的部分放到了模型中的每一层,这部分内容又被称之为前缀。
    notion image

    代码实战

     

    p-tuning v2(soft)

    p-tuning v2没有强制嵌入矩阵重参数化。所以,peft没有专门实现p-tuning v2的代码。初始化PrefixEncoder时把self.prefix_projection置为False,就相当于p-tuning v2了。
    notion image
    notion image

    LoRa

    通过矩阵分解的方式,将原本要更新的大的矩阵变为两个小的矩阵。权重更新: 。训练时,输入分别与原始权重和两个低秩矩阵进行计算,共同得到最终结果,优化则仅优化 A 和 B 。训练完成后,可以将两个低秩矩阵与原始模型中的权重进行合并,合并后的模型与原始模型无异,避免了推理期间 Prompt 系列方法带来的额外计算量。
    notion image

    代码实战

    lora config常用参数

    • r:秩
    • lora_alpha:对lora权重进行缩放。,其中,和 是 LoRA 模块中引入的两个低秩矩阵,是低秩矩阵的秩,就是缩放参数。
    • lora_dropout:lora_dropout的默认值为0
    • bias:偏差可以是“无”、“全部”或“lora_only”。 当偏差类型设为 “无” 时,在 LoRA 微调过程中,模型里所有层的偏差项都不会被更新;偏差类型为 “全部” 时,在微调过程中,模型里所有层的偏差项都会被更新;若偏差类型是 “lora_only”,那么只有与 LoRA 模块相关的偏差项会被更新,而原始模型中其他层的偏差项保持不变。
    • modules_to_save:除了lora以外还要训练哪部分参数。这部分训练参数不用进行矩阵分解。

    整体代码

    保存模型

    QLora

    量化参数

     

    IA3

    简介

    IA3的思想:抑制和放大内部激活,通过可学习的向量对激活值进行抑制或放大。具体来说,会对K、V、FFN三部分的值进行调整,训练过程中同样冻结原始模型的权重,只更新可学习的部分向量部分。训练完成后,与Lora类似,也可以将学习部分的参数与原始权重合并,没有额外推理开销。

    原理

    图1:(IA)³ 以及T-Few方法中使用的损失项示意图。左图:(IA)³ 引入了学习向量、和,它们分别(通过逐元素乘法,用⊙表示)对注意力机制中的键值以及位置前馈网络中的内部激活进行缩放。右图:除了标准的交叉熵损失,我们还引入了负似然损失以降低错误输出的概率,以及长度归一化损失,该损失对所有输出选择的长度归一化对数概率应用标准的softmax交叉熵损失。
    图1:(IA)³ 以及T-Few方法中使用的损失项示意图。左图:(IA)³ 引入了学习向量,它们分别(通过逐元素乘法,用⊙表示)对注意力机制中的键值以及位置前馈网络中的内部激活进行缩放。右图:除了标准的交叉熵损失,我们还引入了负似然损失以降低错误输出的概率,以及长度归一化损失,该损失对所有输出选择的长度归一化对数概率应用标准的softmax交叉熵损失。

    模型结构

    我们采用可学习的向量对模型的激活值进行逐元素乘法(即缩放)。具体来说,我们考虑形式为 的自适应方式,其中 是学习到的特定任务向量, 表示逐元素乘法, 是长度为的激活序列。 的第 个元素是 。在初步实验中,我们发现在自注意力和编码器 - 解码器注意力机制中的键和值,以及位置式前馈网络的中间激活上引入缩放向量就足够了。具体来说,我们引入三个学习向量 ,它们在注意力机制中的应用方式为:
    在位置式前馈网络中的应用方式为 ,其中 是前馈网络的非线性函数。我们在每个Transformer层块中引入一组独立的 向量。对于一个有L层块的Transformer编码器,这总共会添加 个新参数;对于一个有L层块的解码器,则会添加 个新参数(其中的系数2是考虑到自注意力和编码器 - 解码器注意力的存在)。、\(l_{v}\) 和 \(l_{ff}\) 都初始化为1,这样在添加它们时,模型计算的整体函数不会改变。此外,我们还注意到,如果一个模型仅用于单个任务, 引入的修改也可以永久应用于权重矩阵,这样就无需进行逐元素乘法,模型的架构也保持不变。这是因为在 中进行的逐元素乘法总是与矩阵乘法同时出现,且 。在这种情况下,与原始模型相比,我们的方法不会产生额外的计算成本。

    损失函数

    • 正确的损失
      • 在该公式中,模型被训练以提高在给定输入序列的情况下,正确目标序列 的概率。
    • 错误损失
      • 其中 个错误目标序列中的第个。模型会被训练为给错误选择分配更低的概率。
    • 归一化损失
      • 对于给定的训练示例,可能的目标序列长度可能有很大差异,在多选任务中尤其如此。因此,基于概率对每个选择进行排名可能会 “偏向” 较短的选择,因为模型分配给每个标记的概率  。为纠正这一问题,我们在进行排序分类时考虑使用长度归一化,即将模型在每个可能答案选择上的得分除以该选择中的标记数量。
        然后,通过最小化softmax交叉熵损失来最大化正确答案选择的长度归一化对数概率:
    在使用 训练模型时,我们简单地将它们相加。这样做避免了引入任何在少样本设置中难以调整的超参数(在少样本设置中,实际大小的验证集必然很小)。
    💡
    我们在附录C中报告了对T0-3B的所有参数进行微调时,我们发现,添加 可将准确率从60.7% 提高到62.71%,同时包含 可进一步将准确率提高到63.3%。

    代码实战

    配置参数

    • task_type:指定任务类型。如:条件生成任务(SEQ_2_SEQ_LM),因果语言建模(CAUSAL_LM)等。
    • inference_mode:是否在推理模式下使用Peft模型。当 inference_mode 设为 True 时,模型会进入推理模式。在这个模式下,模型不会进行参数更新或者反向传播,仅依据输入来计算输出。这样做能显著减少计算资源的消耗,同时加快推理速度。
    • target_modules:要替换为 IA3 的模块名称列表或模块名称的正则表达式,例如,注意力块。在 PEFT 中支持的模型中默认的模块名如下所示:
      • feedforward_modules:target_modules 中被视为前馈(feedforward)层的模块名称列表或模块名称的正则表达式。
      • module_to_save:除了 IA3 层之外要设置为可训练并保存在最终检查点中的模块列表。

      Adapters

      简介

      在训练中只有绿色的部分是可训练的,别的部分的参数被锁定。实现中和设计中有几个很重要的细节:
      • 插入的 adapter 块在 feed-forward 后面,在残差链接前面,因此不影响 transformer block 残差连接在深度上的效果。
      • Adapter块首先将原始的 维特征投影到较小的维度 ,然后应用非线性再投影回 维。每层添加的参数总数(包括bias)为 。大大减少了训练参数
      • adapter 层本身是含有 skip-connection 的,因此初始化接近0的adpter块对 transformer block 来说相当于不变。这一点很重要,因为训练的初期模型相当于和原模型保持一致,对训练的稳定性非常重要。
      • 作者在训练中让 transformer bolck 的 layerNorm 层是不锁参的。
      Adapter首先将原始的d维特征投影到较小的维度 m,然后应用非线性再投影回d维。每层添加的参数总数(包括bias)为 。在实际训练过程中只用更新绿色部分。
      💡
      通过设置,限制每个任务添加的参数数量,使用的参数大约是原始模型参数的0.5−8%。
      notion image
       
    • LLM
    • NLP
    • DeepSeek系列llm微调实战
      Loading...