hugging face参数高效微调peft源码解析

2023-10-17 07:50

本文主要是介绍hugging face参数高效微调peft源码解析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

大模型参数高效微调(PEFT) - 知乎

让天下没有难Tuning的大模型-PEFT技术简介 - 知乎

大模型参数高效微调技术原理综述(三)-P-Tuning、P-Tuning v2 - 知乎

你似乎来到了没有知识存在的荒原 - 知乎

大模型参数高效微调技术原理综述(六)-MAM Adapter、UniPELT - 知乎

GitHub - huggingface/peft: 🤗 PEFT: State-of-the-art Parameter-Efficient Fine-Tuning.

ChatGPT等大模型高效调参大法——PEFT库的算法简介 - 知乎

PEFT

PEFT:Parameter Efficient Fine-Tuning技术旨在通过最小化微调参数的数量和计算复杂度,来提高预训练模型在新任务上的性能,从而缓解大型预训练模型的训练成本。 

Prefix-Tuning(软提示/连续提示)

  1. 在每一层的token之前构造一段任务相关的tokens作为Prefix,训练时只更新Prefix部分的参数,而Transformer中的其他部分参数固定
  2. 一个Prefix模块就是一个可学习的id到embedding映射表,可以在多层分别添加Prefix模块
  3. 为了防止直接更新Prefix的参数导致训练不稳定的情况,在Prefix层前面加了MLP结构
  4. 作用阶段:所有transformer block的Attention注意力计算

Prompt-Tuning(软提示/连续提示)

  1. 可看做是Prefix-Tuning的简化版本,只在输入层加入prompt tokens,并不需要加入MLP进行调整
  2. 提出 Prompt Ensembling 方法来集成预训练语言模型的多种 prompts
  3. 在只额外对增加的3.6%参数规模(相比原来预训练模型的参数量)的情况下取得和Full-finetuning接近的效果
  4. 作用阶段:第一层transformer block的Attention注意力计算

P-Tuning(软提示/连续提示)

  1. P-Tuning只是在输入的时候加入Embedding,并通过LSTM+MLP对prompt embedding序列进行编码
  2. 根据人工设计模板动态确定prompt token的添加位置,可以放在开始,也可以放在中间
  3. 作用阶段:第一层transformer block的Attention注意力计算

P-Tuning V2(软提示/连续提示)

  1. 可看做是Prefix-Tuning的优化版本。在模型的每一层都添加连续的 prompts
  2. P-Tuning v2在不同规模和任务中都可与微调效果相媲美
  3. 移除重参数化的编码器(如:Prefix Tuning中的MLP、P-Tuning中的LSTM)、针对不同任务采用不同的提示长度、引入多任务学习等
  4. 作用阶段:所有transformer block的Attention注意力计算

Adapter(变体:AdapterFusion、AdapterDrop)

  1. 在Transformer Block中加入两层MLP,固定住原来预训练模型的参数不变,只对新增的Adapter结构进行微调
  2. 在只额外对增加的3.6%参数规模(相比原来预训练模型的参数量)的情况下取得和Full-finetuning接近的效果
  3. 作用阶段:Transformer Block主体

LoRA(变体:AdaLoRA、QLoRA)

  1. 在训练阶段,预训练参数W固定,只更新A和B参数,A和B模拟W的变化量。
  2. 在推理阶段,用A、B参数与原预训练参数相加替换原有预训练模型的参数。推理过程没有额外的参数量和计算量
  3. 相当于是用LoRA去模拟Full-finetune的过程,几乎不会带来效果损失
  4. 由于没有使用Prompt方式,避免了Prompt方法的一系列问题(Prompt的问题:难训练、序列长度受限、效果不如finetune),在推理阶段也没有引入额外的参数
  5. 作用阶段:Transformer Block主体query、value

BitFit

  1. 不需要对预训练模型做任何改动,只微调训练指定神经网络中的偏置Bias
  2. 参数量只有不到2%,但是效果可以接近全量参数
  3. 作用阶段:模型Backbone主体

PEFT方法总结

  1. 增加额外参数,如:Prefix Tuning、Prompt Tuning、Adapter Tuning及其变体
  2. 选取一部分参数更新,如:BitFit
  3. 引入重参数化,如:LoRA、AdaLoRA、QLoRA
  4. 混合高效微调,如:MAM Adapter、UniPELT


由于peft更在快速开发迭代中,代码变动可能会比较大,本文相关代码来自于peft的0.3.0版本。

        下面看一下hugging face peft开源代码中对于上述几种方法的实现。tuners目录下实现了PrefixTuning、PromptTuning、PTuning、Adapter、LoRA、AdaLoRA这些方法配置文件的构造、解析,新增训练参数模型的构造,各种PEFT方法配置文件类之间的继承关系,如下:

PeftConfig -> PromptLearningConfig -> (PrefixTuningConfig、PromptEncoderConfig、PromptTuningConfig)PeftConfig -> LoraConfig -> AdaLoraConfigPushToHubMixin -> PeftConfigMixin -> PeftConfig

        下面先看一下PrefixTuning、PromptTuning、PTuningV1模块的输入、输出情况:

Prefix-Tuning

PrefixEncoder(
  (embedding): Embedding(20, 18432)
)
在每一层transformer block的key和value的前面都加上virtual embedding,2 * layers * hidden = 2 * 12 * 768 = 18432,其中2就对应key和value 

from peft import PrefixEncoder, PrefixTuningConfig
import torchconfig = PrefixTuningConfig(peft_type="PREFIX_TUNING",task_type="SEQ_2_SEQ_LM",num_virtual_tokens=20,token_dim=768,num_transformer_submodules=1,num_attention_heads=12,num_layers=12,encoder_hidden_size=768,prefix_projection=False
)
print(config)# 初始化PrefixEncoder
prefix_encoder = PrefixEncoder(config)
print(prefix_encoder)

Prompt-Tuning

PromptEmbedding(
  (embedding): Embedding(20, 768)
)
只在输入层的原始序列上面添加prompt embedding 

from peft import PromptTuningConfig, PromptEmbedding
import torchword_embedding = torch.nn.Embedding(num_embeddings=100, embedding_dim=768)
config = PromptTuningConfig(peft_type="PROMPT_TUNING",task_type="SEQ_2_SEQ_LM",num_virtual_tokens=20,token_dim=768,num_transformer_submodules=1,num_attention_heads=12,num_layers=12,# encoder_hidden_size=768
)
print(config)# 初始化PromptEncoder
prompt_encoder = PromptEmbedding(config, word_embedding)
print(prompt_encoder)

PTuningV1

PromptEncoder(
  (embedding): Embedding(20, 768)
  (mlp_head): Sequential(
    (0): Linear(in_features=768, out_features=768, bias=True)
    (1): ReLU()
    (2): Linear(in_features=768, out_features=768, bias=True)
    (3): ReLU()
    (4): Linear(in_features=768, out_features=768, bias=True)
  )
)
只在输入层的原始序列上面添加prompt embedding,并且使用了MLP加强学习能力 

from peft import PromptEncoderConfig, PromptEncoder
import torchconfig = PromptEncoderConfig(peft_type="PREFIX_TUNING",task_type="SEQ_2_SEQ_LM",num_virtual_tokens=20,token_dim=768,num_transformer_submodules=1,num_attention_heads=12,num_layers=12,encoder_hidden_size=768,# prefix_projection=False
)
print(config)# 初始化PrefixEncoder
p_encoder = PromptEncoder(config)
print(p_encoder)

 PTuningV2

PrefixEncoder(
  (embedding): Embedding(20, 768)
  (transform): Sequential(
    (0): Linear(in_features=768, out_features=768, bias=True)
    (1): Tanh()
    (2): Linear(in_features=768, out_features=18432, bias=True)
  )
)
与PrefixTuning一样(实际上也是使用同一个类来实现的),在每一层transformer block的key和value的前面都加上virtual embedding,2 * layers * hidden = 2 * 12 * 768 = 18432,2对应的就是key和value 

from peft import PrefixEncoder, PrefixTuningConfig
import torchconfig = PrefixTuningConfig(peft_type="PREFIX_TUNING",task_type="SEQ_2_SEQ_LM",num_virtual_tokens=20,token_dim=768,num_transformer_submodules=1,num_attention_heads=12,num_layers=12,encoder_hidden_size=768,prefix_projection=True
)
print(config)# 初始化PrefixEncoder
prefix_encoder = PrefixEncoder(config)
print(prefix_encoder)

        PrefixTuning和PTuningV2在实现上基本上是一样的,其实就是一样的。下面是peft作者回复的关于PrefixTuning和PTuningV2在实现上的关系。

LoRA

from peft import LoraConfig, LoraModel, get_peft_model
from peft.tuners.lora import LoraLayer
import os
from transformers import AutoModelForSequenceClassification
from torch import nn
from typing import Any, List, Optional, Unionos.environ["HF_HOME"] = "./hf_downloads"model_name_or_path = "bert-base-chinese"
tokenizer_name_or_path = "bert-base-chinese"lora_config = LoraConfig(peft_type="LORA",task_type="SEQ_2_SEQ_LM",inference_mode=False,# 如果r=0,bias='all',就变成了了BitFit微调方法r=8,lora_alpha=16,lora_dropout=0.1,fan_in_fan_out=False,# bias -> none 所有层的bias都不微调# bias -> all 所有层的bias都微调# bias -> lora_only 只有LoRA相关层的bias进行微调# bias 对应BitFit高效微调方法bias='lora_only'
)model = AutoModelForSequenceClassification.from_pretrained(model_name_or_path)
model = get_peft_model(model, lora_config)
print(model)

         打印出来的model结构中包含LoRA在query和value部分添加的权重参数:

PeftModelForSeq2SeqLM((base_model): LoraModel((model): BertForSequenceClassification((bert): BertModel((embeddings): BertEmbeddings((word_embeddings): Embedding(21128, 768, padding_idx=0)(position_embeddings): Embedding(512, 768)(token_type_embeddings): Embedding(2, 768)(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)(dropout): Dropout(p=0.1, inplace=False))(encoder): BertEncoder((layer): ModuleList((0-11): 12 x BertLayer((attention): BertAttention((self): BertSelfAttention((query): Linear(in_features=768, out_features=768, bias=True(lora_dropout): ModuleDict((default): Dropout(p=0.1, inplace=False))(lora_A): ModuleDict((default): Linear(in_features=768, out_features=8, bias=False))(lora_B): ModuleDict((default): Linear(in_features=8, out_features=768, bias=False))(lora_embedding_A): ParameterDict()(lora_embedding_B): ParameterDict())(key): Linear(in_features=768, out_features=768, bias=True)(value): Linear(in_features=768, out_features=768, bias=True(lora_dropout): ModuleDict((default): Dropout(p=0.1, inplace=False))(lora_A): ModuleDict((default): Linear(in_features=768, out_features=8, bias=False))(lora_B): ModuleDict((default): Linear(in_features=8, out_features=768, bias=False))(lora_embedding_A): ParameterDict()(lora_embedding_B): ParameterDict())(dropout): Dropout(p=0.1, inplace=False))(output): BertSelfOutput((dense): Linear(in_features=768, out_features=768, bias=True)(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)(dropout): Dropout(p=0.1, inplace=False)))(intermediate): BertIntermediate((dense): Linear(in_features=768, out_features=3072, bias=True)(intermediate_act_fn): GELUActivation())(output): BertOutput((dense): Linear(in_features=3072, out_features=768, bias=True)(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)(dropout): Dropout(p=0.1, inplace=False)))))(pooler): BertPooler((dense): Linear(in_features=768, out_features=768, bias=True)(activation): Tanh()))(dropout): Dropout(p=0.1, inplace=False)(classifier): Linear(in_features=768, out_features=2, bias=True)))
)

        下面看一下模型里面的可训练参数有哪些,因为在LoraConfig中设置了bias参数为lora_only,所以所有lora层的bias参数也会被设置为可训练:

for name, param in model.named_parameters():if param.requires_grad:print(name)
base_model.model.bert.encoder.layer.0.attention.self.query.bias
base_model.model.bert.encoder.layer.0.attention.self.query.lora_A.default.weight
base_model.model.bert.encoder.layer.0.attention.self.query.lora_B.default.weight
base_model.model.bert.encoder.layer.0.attention.self.value.bias
base_model.model.bert.encoder.layer.0.attention.self.value.lora_A.default.weight
base_model.model.bert.encoder.layer.0.attention.self.value.lora_B.default.weight
base_model.model.bert.encoder.layer.1.attention.self.query.bias
base_model.model.bert.encoder.layer.1.attention.self.query.lora_A.default.weight
base_model.model.bert.encoder.layer.1.attention.self.query.lora_B.default.weight
base_model.model.bert.encoder.layer.1.attention.self.value.bias
base_model.model.bert.encoder.layer.1.attention.self.value.lora_A.default.weight
base_model.model.bert.encoder.layer.1.attention.self.value.lora_B.default.weight
base_model.model.bert.encoder.layer.2.attention.self.query.bias
base_model.model.bert.encoder.layer.2.attention.self.query.lora_A.default.weight
base_model.model.bert.encoder.layer.2.attention.self.query.lora_B.default.weight
base_model.model.bert.encoder.layer.2.attention.self.value.bias
base_model.model.bert.encoder.layer.2.attention.self.value.lora_A.default.weight
base_model.model.bert.encoder.layer.2.attention.self.value.lora_B.default.weight
base_model.model.bert.encoder.layer.3.attention.self.query.bias
base_model.model.bert.encoder.layer.3.attention.self.query.lora_A.default.weight
base_model.model.bert.encoder.layer.3.attention.self.query.lora_B.default.weight
base_model.model.bert.encoder.layer.3.attention.self.value.bias
base_model.model.bert.encoder.layer.3.attention.self.value.lora_A.default.weight
base_model.model.bert.encoder.layer.3.attention.self.value.lora_B.default.weight
base_model.model.bert.encoder.layer.4.attention.self.query.bias
base_model.model.bert.encoder.layer.4.attention.self.query.lora_A.default.weight
base_model.model.bert.encoder.layer.4.attention.self.query.lora_B.default.weight
base_model.model.bert.encoder.layer.4.attention.self.value.bias
base_model.model.bert.encoder.layer.4.attention.self.value.lora_A.default.weight
base_model.model.bert.encoder.layer.4.attention.self.value.lora_B.default.weight
base_model.model.bert.encoder.layer.5.attention.self.query.bias
base_model.model.bert.encoder.layer.5.attention.self.query.lora_A.default.weight
base_model.model.bert.encoder.layer.5.attention.self.query.lora_B.default.weight
base_model.model.bert.encoder.layer.5.attention.self.value.bias
base_model.model.bert.encoder.layer.5.attention.self.value.lora_A.default.weight
base_model.model.bert.encoder.layer.5.attention.self.value.lora_B.default.weight
base_model.model.bert.encoder.layer.6.attention.self.query.bias
base_model.model.bert.encoder.layer.6.attention.self.query.lora_A.default.weight
base_model.model.bert.encoder.layer.6.attention.self.query.lora_B.default.weight
base_model.model.bert.encoder.layer.6.attention.self.value.bias
base_model.model.bert.encoder.layer.6.attention.self.value.lora_A.default.weight
base_model.model.bert.encoder.layer.6.attention.self.value.lora_B.default.weight
base_model.model.bert.encoder.layer.7.attention.self.query.bias
base_model.model.bert.encoder.layer.7.attention.self.query.lora_A.default.weight
base_model.model.bert.encoder.layer.7.attention.self.query.lora_B.default.weight
base_model.model.bert.encoder.layer.7.attention.self.value.bias
base_model.model.bert.encoder.layer.7.attention.self.value.lora_A.default.weight
base_model.model.bert.encoder.layer.7.attention.self.value.lora_B.default.weight
base_model.model.bert.encoder.layer.8.attention.self.query.bias
base_model.model.bert.encoder.layer.8.attention.self.query.lora_A.default.weight
base_model.model.bert.encoder.layer.8.attention.self.query.lora_B.default.weight
base_model.model.bert.encoder.layer.8.attention.self.value.bias
base_model.model.bert.encoder.layer.8.attention.self.value.lora_A.default.weight
base_model.model.bert.encoder.layer.8.attention.self.value.lora_B.default.weight
base_model.model.bert.encoder.layer.9.attention.self.query.bias
base_model.model.bert.encoder.layer.9.attention.self.query.lora_A.default.weight
base_model.model.bert.encoder.layer.9.attention.self.query.lora_B.default.weight
base_model.model.bert.encoder.layer.9.attention.self.value.bias
base_model.model.bert.encoder.layer.9.attention.self.value.lora_A.default.weight
base_model.model.bert.encoder.layer.9.attention.self.value.lora_B.default.weight
base_model.model.bert.encoder.layer.10.attention.self.query.bias
base_model.model.bert.encoder.layer.10.attention.self.query.lora_A.default.weight
base_model.model.bert.encoder.layer.10.attention.self.query.lora_B.default.weight
base_model.model.bert.encoder.layer.10.attention.self.value.bias
base_model.model.bert.encoder.layer.10.attention.self.value.lora_A.default.weight
base_model.model.bert.encoder.layer.10.attention.self.value.lora_B.default.weight
base_model.model.bert.encoder.layer.11.attention.self.query.bias
base_model.model.bert.encoder.layer.11.attention.self.query.lora_A.default.weight
base_model.model.bert.encoder.layer.11.attention.self.query.lora_B.default.weight
base_model.model.bert.encoder.layer.11.attention.self.value.bias
base_model.model.bert.encoder.layer.11.attention.self.value.lora_A.default.weight
base_model.model.bert.encoder.layer.11.attention.self.value.lora_B.default.weight

        下面将LoRA参数合并到预训练参数中,合并之后的模型和原始预训练模型一模一样:

merged_model = model.merge_and_unload()
print(merged_model)
BertForSequenceClassification((bert): BertModel((embeddings): BertEmbeddings((word_embeddings): Embedding(21128, 768, padding_idx=0)(position_embeddings): Embedding(512, 768)(token_type_embeddings): Embedding(2, 768)(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)(dropout): Dropout(p=0.1, inplace=False))(encoder): BertEncoder((layer): ModuleList((0-11): 12 x BertLayer((attention): BertAttention((self): BertSelfAttention((query): Linear(in_features=768, out_features=768, bias=True)(key): Linear(in_features=768, out_features=768, bias=True)(value): Linear(in_features=768, out_features=768, bias=True)(dropout): Dropout(p=0.1, inplace=False))(output): BertSelfOutput((dense): Linear(in_features=768, out_features=768, bias=True)(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)(dropout): Dropout(p=0.1, inplace=False)))(intermediate): BertIntermediate((dense): Linear(in_features=768, out_features=3072, bias=True)(intermediate_act_fn): GELUActivation())(output): BertOutput((dense): Linear(in_features=3072, out_features=768, bias=True)(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)(dropout): Dropout(p=0.1, inplace=False)))))(pooler): BertPooler((dense): Linear(in_features=768, out_features=768, bias=True)(activation): Tanh()))(dropout): Dropout(p=0.1, inplace=False)(classifier): Linear(in_features=768, out_features=2, bias=True)
)

        下面以peft_model.py文件中PeftModelForSequenceClassification的forward函数实现为例,看一下在推理阶段如何对于PrefixTuning、PromptTuning、PTuningV1、PTuningV2、Adapter、LoRA进行操作。

        1、首先通过配置文件的所继承的父类类型来判断PEFT方法是否属于Prompt相关的,如果不是,就表示使用的是Adapter、LoRA等方法,直接执行推理。LoRA的具体推理计算过程后面再补充

        2、如果通过配置文件的所继承的父类类型判断PEFT方法属于Prompt相关的,因为要在transformer block的序列开始位置添加虚拟token的embedding,所以也要补全attention mask

        3、通过配置文件的类型来判断PEFT方法到底是PrefixTuning/PTuningV2,还是PromptTuning/PTuningV1。如果是PromptTuning/PTuningV1,则将虚拟token的embedding直接concat到原始输入序列的前面,送入base model模型进行推理。如果是PrefixTuning/PTuningV2,由于涉及到给每一个transformer block的key和value添加虚拟token的embedding,还需要使用_prefix_tuning_forward函数进行额外的处理。

PromptTuning/PTuningV1源码

        PromptTuning和PTuningV1的共同点都是使用了浅层的Prompt,只在输入层使用。所以PromptTuning和PTuningV1在使用时的方式基本相同。

        PromptTuning的源码详见src/peft/tuners/prompt_tuning.py,里面包含了相关的配置项以及Encoder的创建过程。

        PTuningV1的源码详见src/peft/tuners/p_tuning.py,里面包含了相关的配置项以及Encoder的创建过程。

        PromptTuning/PTuningV1的推理过程就是将虚拟token的embedding直接concat到原始输入序列的前面,并对attention mask进行扩充,送入base model模型进行推理。对应上面讲到的2和3。

PrefixTuning/PTuningV2源码

        PrefixTuning和PTuningV2的共同点都是使用了深层的Prompt,在每层Transformer Block都使用,所以PrefixTuning和PTuningV2的实现放在一起。

        上面说到当执行的PEFT类型是PrefixTuning/PTuningV2时,由于要给每个Transformer Block的Key和Value前面都加上可以学习的virtual token embedding,需要使用_prefix_tuning_forward函数进行额外的处理。

        在看_prefix_tuning_forward函数之前先了解一些相关知识。自然语言处理任务可以分为Auto-Encoding(也叫NLU、自然语言理解、Masked Language Model)和Auto-Regressive(也叫NLG、自然语言生成、Language Model)。对于Auto-Encoding类型的任务,在模型的训练和预测阶段,self-attention都可以并行计算。对于Auto-Regressive类型的任务,在模型训练阶段通过使用attention mask也能够进行并行计算,但是在模型预测阶段,由于是时序生成任务,只能一步一步的生成,没办法并行。我们来看一下在生成阶段每个时间步有哪些计算:

        时间步t:

        Query:来自前一个时间步t-1时刻的输出,使用query矩阵(就是一个全连接层)转换之后得到

        Key:来自前t-1个时间步[1,……,t-1]的输出,使用key矩阵(就是一个全连接层)转换之后得到

        Value:来自前t-1个时间步[1,……,t-1]的输出,使用value矩阵(就是一个全连接层)转换之后得到

        时间步t + 1:

        Query:来自前一个时间步t时刻的输出,使用query矩阵(就是一个全连接层)转换之后得到

        Key:来自前t个时间步[1,……,t-1,t]的输出,使用key矩阵(就是一个全连接层)转换之后得到

        Value:来自前t个时间步[1,……,t-1,t]的输出,使用value矩阵(就是一个全连接层)转换之后得到

        可以看到,每个时间步的Query都和上一个时间步的输出相关,每一步都需要重新计算Query,但是key和value来自前t-1个时间步的输出相关,所以t+1时刻的key和value与t时刻的key和value在[1,……,t-1]时刻上的计算结果是相同的,也就是这些结果是可以复用的,在每个时刻可以复用前一时刻计算的key和value,然后追加上当前时刻新增的key和value,构成完整的key和value。

        在hugging face实现的self-attention模块中,为了复用decode生成阶段的key和value,会传入一个past_key_values参数,如果past_key_values不是None,表示前面时间步已经有计算结果了,直接复用上一步的结果,然后将当前时间步的key和value拼接上去,更新后的past_key_values将继续传递到下一个时间步。

        有了上面的背景知识,对于_prefix_tuning_forward函数中关于PrefixTuning/PTuningV2方法的实现就很好理解了,就是将生成的virtual token embedding通过past_key_values参数带入到transformer block的每一层,放在每一层key和value的前面。

LoRA源码

        LoRA算法的主要流程可以分为以下几步:

  1. 根据LoraConfig中的target_module参数在base model中找到需要进行Lora操作的module
  2. 为找到的target module添加Lora相关参数,将进行Lora操作之后的module封装成LoraLayer层,然后使用Lora后的新module替换掉base model中老的module。
  3. 在训练阶段,将新添加的Lora参数和bias参数(如果bias设置为all或者lora_only的话)设置为可训练的,requires_grad=True,其余预训练参数都冻结
  4. 在推理阶段,将所有参数设置为requires_grad=False,并将做了Lora操作的层进行参数融合,将新添加的Lora参数融合到预训练参数中

        上述这些流程在add_adapter函数中进行串联起来:

    def add_adapter(self, adapter_name, config=None):if config is not None:# 基于model config更新lora config,主要补充的是target_module参数model_config = self.model.config.to_dict() if hasattr(self.model.config, "to_dict") else self.model.configconfig = self._prepare_lora_config(config, model_config)self.peft_config[adapter_name] = config# 根据target module参数,在base model中寻找并更新目标模块,将lora相关内容更新到模型架构中self._find_and_replace(adapter_name)if len(self.peft_config) > 1 and self.peft_config[adapter_name].bias != "none":raise ValueError("LoraModel supports only 1 adapter with bias. When using multiple adapters, set bias to 'none' for all adapters.")# 设置只有lora相关的参数可以微调,冻结其他参数mark_only_lora_as_trainable(self.model, self.peft_config[adapter_name].bias)if self.peft_config[adapter_name].inference_mode:# 在推理模式下冻结所有参数_freeze_adapter(self.model, adapter_name)

基于上述流程,Lora源码主要包括以下大的模块(类):

1、LoraConfig:Lora配置文件

        a)、LoraConfig中记录Lora相关的配置参数,其中重点看一下target_module、bias、modules_to_save这几个参数。

        b)、target_module参数用来指出base model中叫什么名字的参数需要执行Lora操作,在other.py中TRANSFORMERS_MODELS_TO_LORA_TARGET_MODULES_MAPPING列出了每个模型中需要进行Lora操作的参数名称。

        c)、bias对应的就是BitFit参数微调方法,就是在微调时是否微调bias参数:

# bias -> none 所有层的bias都不微调
# bias -> all 所有层的bias都微调
# bias -> lora_only 只有LoRA相关层的bias进行微调

        modules_to_save用来存放哪些模型参数是重新训练过的,保存模型的时候可以只保存这些参数,其他的都是预训练参数,可以不保存。

2、LoraLayer:用于记录Lora相关参数,初始化Lora部分的模型。在LoraLayer部分目前仅支持两种子模型的Lora操作,分别是nn.Embedding和nn.Linear(Conv1D等价于nn.Linear),下面看一下这部分主要的三个类:

        a)、LoraLayer:LoraLayer中实现了在Linear层中添加Lora操作的方法update_layer和在Embedding层中添加Lora操作的update_layer_embedding方法,这两个方法中根据原始参数形状初始化了Lora参数,分别在子类Linear和子类Embedding中调用。

        b)、Linear:Linear层继承nn.Linear和LoraLayer,实现了在nn.Linear层和Conv1D层(hugging face的transformers中实现)中使用Lora。使用nn.Linear创建和原始参数相同形状的参数,后面用来存储预训练的参数部分,使用LoraLayer的update_layer函数创建Lora新增的参数部分,这样就将一个Linear子模块改造成了添加了Lora的子模块。Linear层中还实现了merge和unmerge函数用来将Lora参数合入原始Linear层的预训练参数中,或者将Lora参数从原始Linear层的参数中分离出来。

        c)、Embedding:Embedding层继承nn.Embedding和LoraLayer,与Linear的实现几乎一样,Embedding也是创建了和原始Embedding参数相同形状的参数,后面用来存储预训练的参数部分,使用update_layer_embedding函数创建Lora新增的参数部分,这样就将一个Embedding子模块改造成了添加了Lora的子模块。Embedding层中也实现了merge和unmerge函数用来将Lora参数合入原始Embedding层的预训练参数中,或者将Lora参数从原始Embedding层的参数中分离出来。

3、LoraModel:基于上述组件,LoraModel可以拆解为以下部分:

        a)、遍历base model的所有子module,根据LoraConfig中的target_module参数找到需要进行Lora操作的module,在LoraModel的_find_and_replace函数中实现

        b)、基于上一步找到的target module,找到其父模块parent,并为找到的target module添加Lora相关参数,将进行Lora操作之后的module封装成LoraLayer层

备注:当前PEFT版本中支持进行Lora的层只有nn.Linear、nn.Embedding和Conv1D(来自hugging face的transformers库,实际上就是一个weight做了转置的nn.Linear层)。根据重参数化Reparameterization的思想(重参数化详见RepVGG),卷积操作也可以使用Lora,peft-main分支里面已经正在开发这个功能了。

        c)、根据找到的target module的父模块,使用Lora后的新module替换掉base model中对应的老的module,在_replace_module函数中实现

        d)、在训练阶段,将新添加的Lora参数和bias参数(如果bias设置为all或者lora_only的话)设置为可训练的,requires_grad=True,其余预训练参数都冻结

        e)、在推理阶段,将所有参数设置为requires_grad=False,并将做了Lora操作的层进行参数融合,将新添加的Lora参数融合到预训练参数中

这篇关于hugging face参数高效微调peft源码解析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/223996

相关文章

Java汇编源码如何查看环境搭建

《Java汇编源码如何查看环境搭建》:本文主要介绍如何在IntelliJIDEA开发环境中搭建字节码和汇编环境,以便更好地进行代码调优和JVM学习,首先,介绍了如何配置IntelliJIDEA以方... 目录一、简介二、在IDEA开发环境中搭建汇编环境2.1 在IDEA中搭建字节码查看环境2.1.1 搭建步

在C#中合并和解析相对路径方式

《在C#中合并和解析相对路径方式》Path类提供了几个用于操作文件路径的静态方法,其中包括Combine方法和GetFullPath方法,Combine方法将两个路径合并在一起,但不会解析包含相对元素... 目录C#合并和解析相对路径System.IO.Path类幸运的是总结C#合并和解析相对路径对于 C

详解Spring Boot接收参数的19种方式

《详解SpringBoot接收参数的19种方式》SpringBoot提供了多种注解来接收不同类型的参数,本文给大家介绍SpringBoot接收参数的19种方式,感兴趣的朋友跟随小编一起看看吧... 目录SpringBoot接受参数相关@PathVariable注解@RequestHeader注解@Reque

Java向kettle8.0传递参数的方式总结

《Java向kettle8.0传递参数的方式总结》介绍了如何在Kettle中传递参数到转换和作业中,包括设置全局properties、使用TransMeta和JobMeta的parameterValu... 目录1.传递参数到转换中2.传递参数到作业中总结1.传递参数到转换中1.1. 通过设置Trans的

java如何调用kettle设置变量和参数

《java如何调用kettle设置变量和参数》文章简要介绍了如何在Java中调用Kettle,并重点讨论了变量和参数的区别,以及在Java代码中如何正确设置和使用这些变量,避免覆盖Kettle中已设置... 目录Java调用kettle设置变量和参数java代码中变量会覆盖kettle里面设置的变量总结ja

Java解析JSON的六种方案

《Java解析JSON的六种方案》这篇文章介绍了6种JSON解析方案,包括Jackson、Gson、FastJSON、JsonPath、、手动解析,分别阐述了它们的功能特点、代码示例、高级功能、优缺点... 目录前言1. 使用 Jackson:业界标配功能特点代码示例高级功能优缺点2. 使用 Gson:轻量

Java如何接收并解析HL7协议数据

《Java如何接收并解析HL7协议数据》文章主要介绍了HL7协议及其在医疗行业中的应用,详细描述了如何配置环境、接收和解析数据,以及与前端进行交互的实现方法,文章还分享了使用7Edit工具进行调试的经... 目录一、前言二、正文1、环境配置2、数据接收:HL7Monitor3、数据解析:HL7Busines

spring 参数校验Validation示例详解

《spring参数校验Validation示例详解》Spring提供了Validation工具类来实现对客户端传来的请求参数的有效校验,本文给大家介绍spring参数校验Validation示例详... 目录前言一、Validation常见的校验注解二、Validation的简单应用三、分组校验四、自定义校

SpringBoot中Get请求和POST请求接收参数示例详解

《SpringBoot中Get请求和POST请求接收参数示例详解》文章详细介绍了SpringBoot中Get请求和POST请求的参数接收方式,包括方法形参接收参数、实体类接收参数、HttpServle... 目录1、Get请求1.1 方法形参接收参数 这种方式一般适用参数比较少的情况,并且前后端参数名称必须

python解析HTML并提取span标签中的文本

《python解析HTML并提取span标签中的文本》在网页开发和数据抓取过程中,我们经常需要从HTML页面中提取信息,尤其是span元素中的文本,span标签是一个行内元素,通常用于包装一小段文本或... 目录一、安装相关依赖二、html 页面结构三、使用 BeautifulSoup javascript