Transformers参数高效微调之LoRA

简介

LoRA: Low-Rank Adaptation of Large Language Models是微软研究人员为处理微调大语言模型的问题而引入的一项新技术。具有数十亿个参数的强大模型(例如 GPT-3)为了适应特定任务或领域而进行微调的成本非常高。LoRA 建议冻结预先训练的模型权重并注入可训练层 (秩分解矩阵) 在每个变压器块中.这大大减少了可训练参数的数量和 GPU 内存要求,因为不需要为大多数模型权重计算梯度。研究人员发现,通过专注于大语言模型的Transformer注意力块,LoRA的微调质量与完整模型微调相当,同时速度更快需要的计算更少。

LoRA概念

比如你有一个装满乐高积木的大盒子。
这个大盒子里有各种积木,你可以用它们建造房子、汽车、太空飞船等等。
但因为这个盒子又大又重,搬运起来很麻烦。而且,大多数时候你并不需要用到所有积木来完成你的建造任务。
于是,你挑出一小盒你最喜欢、最常用的积木。这小盒子更轻便,便于携带,虽然不如大盒子功能全面,但它足够用来完成大多数你想建造的东西。
在这个比喻中,大盒子的乐高积木就像一个大型语言模型(如 GPT-4):
功能强大,可以处理很多任务,但也需要巨大的计算资源。
而小盒子的乐高积木就像 LoRA(低秩适应):
这是经过特定任务适配后,更小、更轻量化的模型版本。虽然功能不如完整模型全面,但更高效,也更容易使用。

什么是 LoRA(低秩适应)?
LoRA 的全称是 “Low-Rank Adaptation”(低秩适应)。
“低秩”(Low-Rank)是一个数学术语,用来描述生成这种更小、更轻量模型的方法。
你可以把“低秩”理解为只阅读一本书中标注的重点部分。
而“全秩”(Full-Rank)就像是把整本书从头到尾全部读完(全参训练)。

LoRA原理

在这里插入图片描述
这张图是网上实现LoRA原理图

简单理解:在模型的Linear层的旁边,增加一个“旁支”,这个“旁支”的作用,就是代替原有的参数矩阵W进行训练(左侧部分)。

通过图我们来直观地理解一下这个过程,输入 x x x,具有维度 d d d,举个例子,在普通的transformer模型中,这个 x x x可能是embedding的输出,也有可能是上一层transformer layer的输出,而 d d d一般就是768(大多数Bert的输出维度是768)。按照原本的路线,它应该只走左边的部分,也就是原有的模型部分。

而在LoRA的策略下,增加了右侧的“旁支”,也就是先用一个Linear层A,将数据从 d d d维降到 r r r维,这个 r r r也就是LORA的秩,是LoRA中最重要的一个超参数。一般会远远小于 d d d (见的比较多的是4、8),尤其是对于现在的大模型, d d d已经不止是768或者1024,例如LLaMA-7B,每一层transformer有32个head,这样一来 d d d就达到了4096.

接着再用第二个Linear层B,将数据从 r r r变回 d d d维。最后再将左右两部分的结果相加融合,就得到了输出的hidden_state。左侧是会冻结不做计算的。

这样使用LoRA的策略下,计算量会小得多,以小的代价微调出特定任务的模型。

代码实现

LoRA的微调是使用peft中的LoraConfig实现。
那些先看下里面的参数吧。
LoraConfig()参数说明
1、task_type:
描述: 用来指定 LoRA 要适用于的任务类型。不同的任务类型会影响模型中的哪些部分应用 LoRA 以及如何配置 LoRA。根据不同的任务,LoRA 的配置方式可能会有所不同,特别是在模型的某些特定模块(如自注意力层)上。

可选值:

  • “CAUSAL_LM”: 自回归语言模型(Causal Language Modeling)。适用于像 GPT 这样的自回归语言模型,这类模型通常在生成任务上使用。
  • “SEQ_2_SEQ_LM”: 序列到序列语言模型(Sequence-to-Sequence Language Modeling)。适用于像 T5 或 BART 这样的序列到序列模型,这类模型通常用于翻译、摘要生成等任务。
  • “TOKEN_CLS”: 标注任务(Token Classification)。适用于命名实体识别(NER)、词性标注等任务。
  • “SEQ_CLS”: 序列分类(Sequence Classification)。适用于句子分类、情感分析等任务。
  • “QUESTION_ANSWERING”: 问答任务(Question Answering)。适用于问答模型,如 SQuAD 等数据集中的任务。
  • “OTHER”: 适用于其他自定义任务,或者模型的任务类型不明确时。
    2、target_modules:
  • 描述: 指定应用 LoRA 的目标模型模块或层的名称。这些是模型中应用 LoRA 低秩分解的参数,通常是网络中的线性层(如 query, value 矩阵)。
  • 数据类型:Union[List[str], str]
  • 默认值: None
  • 典型值: [“query”, “value”] 或类似参数,具体依赖于模型结构。
    3、r(Rank Reduction Factor):
  • 描述:LoRA 的低秩矩阵的秩(rank)。r 是低秩矩阵的秩,表示将原始权重矩阵分解成两个更小的矩阵,其乘积近似原始权重矩阵。r 越小,模型的计算开销越低。
  • 数据类型:int
  • 典型值:通常在 4 到 64 之间。
    4、lora_alpha:
  • 描述:缩放因子,用于缩放 LoRA 的输出。通常在 LoRA 层的输出会被 lora_alpha / r 缩放,用来平衡学习效率和模型收敛速度。
  • 数据类型:int
  • 典型值:r 的 2 到 32 倍之间。
    5、lora_dropout:
  • 描述:应用于 LoRA 层的 dropout 概率。这个参数用来防止过拟合,特别是在小数据集上训练时,使用 dropout 可以提高模型的泛化能力。
  • 数据类型:float
  • 典型值:0.1 或者更低。
    6、bias:
  • 描述:用于控制是否训练模型的偏置项(bias)。可以设置为 none(不训练 bias)、all(训练所有 bias)、或者 lora_only(仅对 LoRA 层的偏置项进行训练)。
  • 数据类型:str
  • 典型值:none 或 lora_only。
    7、modules_to_save :
  • 描述: 指定除了 LoRA 层之外,还需要保存哪些额外的模块。这通常用于微调时只保存 LoRA 层的权重,同时保存某些特殊的模块(例如全连接层)。
  • 数据类型:Optional[List[str]]
  • 默认值: None
  • 典型值: [“classifier”, “pooler”] 或类似参数。
    8、init_lora_weights :
  • 描述: 控制 LoRA 层的权重是否在初始化时进行随机初始化。如果设置为 True,则会使用标准初始化方法;否则,将不进行初始化。
  • 数据类型:bool
  • 默认值: True
    9、inference_mode :
  • 描述: 如果设置为 True,则模型只在推理阶段使用 LoRA。此模式下,LoRA 的权重会被冻结,不会进行训练。适用于将微调后的模型用于推理场景。
  • 数据类型:bool
  • 默认值: False
代码实现
from datasets import Dataset
from transformers import AutoTokenizer, AutoModelForCausalLM, DataCollatorForSeq2Seq
from transformers import TrainingArguments, Trainer
from peft import LoraConfig, TaskType, get_peft_model

# 分词器
tokenizer = AutoTokenizer.from_pretrained("langboat_bloom-1b4-zh")


# 函数内将instruction和response拆开分词的原因是:
# 为了便于mask掉不需要计算损失的labels, 即代码labels = [-100] * len(instruction["input_ids"]) + response["input_ids"]
def process_func(example):
    MAX_LENGTH = 256
    input_ids, attention_mask, labels = [], [], []
    instruction = tokenizer(
        "\n".join(["Human: " + example["instruction"], example["input"]]).strip() + "\n\nAssistant: ")
    response = tokenizer(example["output"] + tokenizer.eos_token)
    input_ids = instruction["input_ids"] + response["input_ids"]
    attention_mask = instruction["attention_mask"] + response["attention_mask"]
    labels = [-100] * len(instruction["input_ids"]) + response["input_ids"]
    if len(input_ids) > MAX_LENGTH:
        input_ids = input_ids[:MAX_LENGTH]
        attention_mask = attention_mask[:MAX_LENGTH]
        labels = labels[:MAX_LENGTH]
    return {
        "input_ids": input_ids,
        "attention_mask": attention_mask,
        "labels": labels
    }


if __name__ == "__main__":
    # 加载数据集
    dataset = Dataset.load_from_disk("./alpaca_data_zh/")

    # 处理数据
    tokenized_ds = dataset.map(process_func, remove_columns=dataset.column_names)
    # print(tokenizer.decode(tokenized_ds[1]["input_ids"]))
    # print(tokenizer.decode(list(filter(lambda x: x != -100, tokenized_ds[1]["labels"]))))

    # 创建模型
    model = AutoModelForCausalLM.from_pretrained("langboat_bloom-1b4-zh", low_cpu_mem_usage=True)

    #打印模型参数,用于下部target_modules参数选用
    for name, parameter in model.named_parameters():
        print(name)

#LoraConfig不同模型这里使用的参数是不一样,这块代码不是通用的
    config = LoraConfig(task_type=TaskType.CAUSAL_LM, target_modules=".*\.1.*query_key_value", modules_to_save=["word_embeddings"])

    print("config:", config)

    model = get_peft_model(model, config)

    print("model:", model)

    print("print_trainable_parameters:", model.print_trainable_parameters())

    # 训练参数
    args = TrainingArguments(
        output_dir="./chatbot",
        per_device_train_batch_size=1,
        gradient_accumulation_steps=8,
        logging_steps=10,
        num_train_epochs=1
    )

    # trainer
    trainer = Trainer(
        model=model,
        args=args,
        train_dataset=tokenized_ds,
        data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True)
    )

    # 训练模型
    trainer.train()


    model = model.cuda()
    ipt = tokenizer("Human: {}\n{}".format("考试有哪些技巧?", "").strip() + "\n\nAssistant: ", return_tensors="pt").to(
        model.device)
    print(tokenizer.decode(model.generate(**ipt, max_length=128, do_sample=True)[0], skip_special_tokens=True))
### Transformer模型中的LoRA低秩适应 #### 实现方式 在Transformer架构中实施LoRA涉及对每一层的权重矩阵进行特定处理。具体来说,在标准的全连接层或自注意力机制中,原本的权重更新被替换为低秩近似形式。对于任意一层的权重矩阵 \( W \),其更新量 \( ΔW \) 被表示成两个较小维度矩阵乘积的形式: \[ ΔW = B A \] 这里 \( B ∈ R^{d×r} \), \( A ∈ R^{r×k} \)[^4]。这种设计使得即使原始模型非常庞大,实际参与训练并需要调整的部分也仅限于这些相对更小规模的新引入矩阵。 为了确保初始状态下不改变原有网络行为,\( A \) 使用随机高斯分布初始化而 \( B \) 则设为零向量;这意味着刚开始时 \( ΔW=BA \approx 0 \),即几乎不影响原模型输出。随着迭代次数增加,\( A \) 和 \( B \) 将逐渐学习到有助于改进性能的方向变化。 #### 原理阐述 LoRA的核心在于利用线性代数里的低秩分解理论来简化复杂的权值空间探索问题。当面对具有大量参数的大规模预训练模型时,直接对其进行端到端再训练不仅耗时而且容易过拟合。相反,通过限定增量部分为低秩结构,可以在保持良好泛化能力的同时大幅削减所需资源消耗。 这种方法特别适合那些已经经过充分预训练的基础模型,因为它们往往已经在广泛的数据集上积累了丰富的特征表达力。此时只需针对目标任务做少量针对性修改即可获得不错的效果提升。例如,在自然语言处理任务里,像BERT这样的通用语义编码器可以通过这种方式快速适配至不同应用场景下而不必重新构建整个框架[^3]。 #### 应用场景举例 - **指令调优**:如Gemma项目所展示那样,采用名为LoRA-IT的技术路径能够显著压缩模型体积(最高可达99.9%),这对于部署环境受限的情况极为有利。特别是移动设备或其他边缘计算节点上的实时交互式应用尤为受益匪浅[^2]。 - **多模态转换**:除了纯文本领域外,图像生成、语音合成等方面同样存在相似的需求——既希望继承已有成果又渴望灵活应对新挑战。借助LoRA策略可以有效地平衡这两方面的要求,促进跨媒体智能系统的快速发展[^1]. ```python import torch.nn as nn class LowRankAdapter(nn.Module): def __init__(self, input_dim, output_dim, rank): super(LowRankAdapter, self).__init__() self.A = nn.Parameter(torch.randn(rank, input_dim)) self.B = nn.Parameter(torch.zeros(output_dim, rank)) def forward(self, x): delta_W = self.B @ self.A / self.rank return x + delta_W.mm(x) # Example usage within a transformer layer adapter_layer = LowRankAdapter(input_dim=d_model, output_dim=d_model, rank=r) ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值