当前位置:   article > 正文

【语言大模型微调】LoRA — 尖端的大模型微调技术_lora微调

lora微调

在这里插入图片描述



前言

直接上干货:大语言模型可以做什么?
在这里插入图片描述


LoRA的核心思想:
**加粗样式**

提示:后面是主要内容:


一、什么是微调?

  随着机器学习的最新发展,对模型性能的期望也在增加,需要更复杂的机器学习方法来满足对性能的需求。在机器学习的早期阶段,构建一个模型并在单次训练中训练它是可行的。

在这里插入图片描述

  训练,在其最简单的意义上。您将一个未经训练的模型,提供给它数据,并获得一个高性能的模型。

  对于简单问题来说,这仍然是一种流行的策略,但对于更复杂的问题,将训练分为两个部分,即“预训练”和“微调”,可能会很有用。总体思路是在一个大规模数据集上进行初始训练,并在一个定制的数据集上对模型进行优化
在这里插入图片描述

  这种“预训练”然后“微调”的策略可以让数据科学家利用多种形式的数据,并使用大型预训练模型来完成特定任务。因此,预训练然后微调是一种常见且非常强大的范例。

  最基本的微调形式 是使用与预训练模型相同的过程来微调新数据上的模型。例如,您可以在大量的通用文本数据上训练模型,然后使用相同的训练策略,在更具体的数据集上微调该模型。

   以上策略十分昂贵 。LLMs绝对庞大,需要足够的内存来存储整个模型,以及模型中每个参数的梯度(梯度是让模型知道调整参数方向的东西)。参数和梯度都需要存在于GPU上,这就是为什么训练LLMs需要如此多的GPU显存的。

在这里插入图片描述

  

二、LORA 简介

   “低秩适应”(LoRA)是一种“参数高效微调”(PEFT)的形式,它允许使用少量可学习参数对大型模型进行微调 。LoRA改善微调的几个点:

  1. 将微调视为学习参数的变化(▲w),而不是调整参数本身(w)。

  2. 通过删除重复信息,将这些变化压缩成较小的表示。

  3. 通过简单地将它们添加到预训练参数中来“加载”新的变化。

2.1 微调作为参数变化

  正如之前讨论的,微调的最基本方法是迭代地更新参数。就像正常的模型训练一样,你让模型进行推理,然后根据推理的错误程度更新模型的参数。

  与其将微调视为学习更好的参数,LoRA 将微调视为学习参数变化:冻结模型参数,然后学习使模型在微调任务中表现更好所需的这些参数的变化。

  类似于训练,首先让模型推理,然后根据error进行更新。但是,不更新模型参数,而是更新模型参数的变化。

在这里插入图片描述

  在LoRA中,我们冻结模型参数,并创建一组描述这些参数变化的新值。然后,我们学习必要的参数变化,以在微调任务上表现更好。LoRA添加更多的数据和额外的步骤,如何使微调变得更小、更快?

2.2 参数变化压缩

**加粗样式**

  矩阵的秩是为了量化矩阵中的线性独立性。我们可以将一个矩阵分解为一些线性独立的向量;这种矩阵的形式被称为“行阶梯形式”。

在这里插入图片描述

  因此,矩阵可以包含一定程度的“重复信息”,即线性相关性。如果你有一个大矩阵,具有显著的线性相关性(因此秩较低),可以将该矩阵表示为两个相对较小的矩阵的乘积。这种分解的思想使得LoRA占用了如此小的内存空间。

  

三、LoRA微调的流程

  首先冻结模型参数。使用这些参数进行推理,但不会更新它们。然后创建两个矩阵,当它们相乘时,它们的大小将与我们正在微调的模型的权重矩阵的大小相同。在一个大型模型中,有多个权重矩阵,为每个权重矩阵创建一个这样的配对。
在这里插入图片描述

  LoRA将这些矩阵称为矩阵“A”和“B”。这些矩阵一起代表了LoRA微调过程中的可学习参数。
在这里插入图片描述

  然后将输入通过冻结的权重和变化矩阵传递:
在这里插入图片描述

  根据两个输出的组合计算损失,然后根据损失更新矩阵A和B:
在这里插入图片描述

  这些变化矩阵是即时计算的,从未被存储,这就是为什么LoRA的内存占用如此小的原因。实际上,在训练期间只存储模型参数、矩阵A和B以及A和B的梯度。

  我们执行此操作,直到我们优化了变化矩阵的因素以进行微调任务。更新矩阵A和B的反向传播步骤比更新完整模型参数集的过程要快得多,因为A和B要小得多。这就是为什么尽管训练过程中有更多的操作,LoRA仍然通常比传统微调更快的原因。

  当我们最终想要使用这个微调模型进行推断时,我们只需计算变化矩阵,并将变化添加到权重中。这意味着LoRA不会改变模型的推断时间:

在这里插入图片描述

  

四、LoRA在Transformer中的应用

  实际上许多大模型具有复杂的结构,不是一个整体结构。像Transformer这样的模型中的参数,如何应用LoRA?
在这里插入图片描述

  对于Transformer模型,有两个要注意的事项:

  1. 通常,在Transformer的多头自注意力层中,密集网络(用于构建查询、键和值)的深度只有1。也就是说,只有一个输入层和一个由权重连接的输出层。

  2. 这些浅层密集网络是Transformer中大部分可学习参数,非常非常大。可能有超过100,000个输入神经元连接到100,000个输出神经元,这意味着描述其中一个网络的单个权重矩阵可能有10B个参数。因此,尽管这些网络的深度只有1,但它们非常宽,因此描述它们的权重矩阵非常大。

  LoRA在Transformer模型上,要学习每个非常大但浅层的密集层的分解变化。

五、LoRA Rank

  LoRA有一个超参数,称为Rank,它描述了用于构建之前讨论的变化矩阵的深度。较高的值意味着更大的和矩阵,这意味着它们可以在变化矩阵中编码更多的线性独立信息。
在这里插入图片描述

  “r"参数可以被视为"信息瓶颈”。较小的r值意味着A和B可以用更小的内存占用编码较少的信息。较大的r值意味着A和B可以编码更多的信息,但内存占用更大
在这里插入图片描述

  一个具有r值等于1和2的LoRA的概念图。在这两个例子中,分解的A和B矩阵导致相同大小的变化矩阵,但是r=2能够将更多线性独立的信息编码到变化矩阵中,因为A和B矩阵中包含更多信息。

  事实证明,LoRA论文所做的核心假设,即模型参数的变化具有低隐式秩,是一个相当强的假设。微软(LoRA的出版商)的人员尝试了一些值,并发现即使是秩为一的矩阵也表现出色。

在这里插入图片描述

  LoRA论文中建议:当数据与预训练中使用的数据相似时,较低的r值可能就足够了。当在非常新的任务上进行微调时,可能需要对模型进行重大的逻辑更改,这时可能需要较高的r值

  

六、Python代码实现

  下面以HuggingFace的一个模块做例子,用LoRA对一个预训练模型进行微调,用于问题回答。代码 链接在这里:

6.1 安装依赖库:

pip install -q bitsandbytes datasets accelerate loralib
pip install -q git+https://github.com/huggingface/peft.git git+https://github.com/huggingface/transformers.git

# bitsandbytes:用于使用较小的数据类型表示模型,节省内存。
# datasets:用于下载数据集
# accelerate:一些模块的机器学习互操作性所需的依赖项
# loralib:LoRA实现
# peft:一般的“参数高效微调”模块,与LoRA的接口
# transformers:用于下载和使用HuggingFace的预训练transformers
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

  

6.2 加载预训练模型

  我们将使用BLOOM,这是一个开源且许可证宽松的语言模型。我们将使用5.6亿参数版本以节省内存,也可以将相同的策略应用于更大版本的BLOOM。

"""导入依赖项并下载预训练的bloom模型
"""

import torch
import torch.nn as nn
import bitsandbytes as bnb
from transformers import AutoTokenizer, AutoConfig, AutoModelForCausalLM

# 加载模型
model = AutoModelForCausalLM.from_pretrained(
    # "bigscience/bloom-3b",
    # "bigscience/bloom-1b1",
    "bigscience/bloom-560m",
    torch_dtype=torch.float16,
    device_map='auto',
)

# 加载用于此模型的分词器(将文本转换为模型的输入)
tokenizer = AutoTokenizer.from_pretrained("bigscience/tokenizer")

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

  

6.3 设置LoRA

"""使用参数高效微调设置LoRA"""

from peft import LoraConfig, get_peft_model

# 定义LoRA在这个特定示例中的工作方式
config = LoraConfig(
    r=8,                                 # LoRA的层数,即AB矩阵的秩
    lora_alpha=8,                        # LoRA的alpha值,可视为缩放因子,默认等于r
    target_modules=["query_key_value"],  # 目标模块列表,即LoRA优化的模型部分
    lora_dropout=0.05,                   # LoRA的dropout率
    bias="none",                         # 偏置类型。例子中,我们只训练权重
    task_type="CAUSAL_LM"                # 任务类型
)

# 实际上,这会覆盖内存中的模型,所以重命名仅用于可读性。
peft_model = get_peft_model(model, config)  # 获取经过参数高效微调的LoRA模型

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

6.4 检查内存的节省

  LoRA训练参数数量明显较少,即节省了内存消耗。

"""比较LoRA之前和之后的参数"""

trainable_params = 0  # 可训练参数的数量
all_param = 0  # 所有参数的数量

# 遍历所有参数
for _, param in peft_model.named_parameters():
    all_param += param.numel()  # 将参数数量添加到总数中
    if param.requires_grad:  # 如果参数需要梯度
        trainable_params += param.numel()  # 将参数数量添加到可训练参数中

# 打印结果
print(f"可训练参数数量:{trainable_params}")
print(f"所有参数数量:{all_param}")
print(f"可训练参数占比:{100 * trainable_params / all_param:.2f}%")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在这里插入图片描述

6.5 加载微调数据集

  将使用SQUAD数据集 来提高语言模型在问答任务上的性能。斯坦福问答数据集(SQUAD)是一个高质量、常用且许可证宽松的数据集。

from datasets import load_dataset  # 导入load_dataset函数,用于加载数据集
qa_dataset = load_dataset("squad_v2")  # 使用load_dataset函数加载SQUAD数据集,并将结果赋值给qa_dataset变量
  • 1
  • 2

  模型将在特定的数据结构上,对语言模型进行微调。模型将期望以以下一般形式的文本作为输入:

# 代码注释
'''
CONTEXT: 上下文
{context}

QUESTION: 问题
{question}

ANSWER: 答案
{answer}</s>
'''
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

  们将向模型提供上下文和问题,模型将被期望向我们提供答案。因此,我们将重新格式化SQUAD中的数据以符合此格式。

"""
"""Reformatting SQUAD to respect our defined structure
"""  # 定义一个函数用于重新格式化SQUAD数据集

# 定义一个函数用于重新格式化
def create_prompt(context, question, answer):
  if len(answer["text"]) < 1:  # 如果答案的文本长度小于1
    answer = "Cannot Find Answer"  # 将答案设置为"Cannot Find Answer"
  else:
    answer = answer["text"][0]  # 否则将答案设置为答案文本的第一个元素
  prompt_template = f"CONTEXT:\n{context}\n\nQUESTION:\n{question}\n\nANSWER:\n{answer}</s>"  # 创建一个模板,包含上下文、问题和答案
  return prompt_template  # 返回模板

# 将重新格式化的函数应用于整个数据集
mapped_qa_dataset = qa_dataset.map(lambda samples: tokenizer(create_prompt(samples['context'], samples['question'], samples['answers'])))
"""
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

6.6 使用LoRA在SQUAD上进行微调

# 导入transformers库
import transformers

# 定义trainer,用于训练模型
trainer = transformers.Trainer(
    model=peft_model, # 模型
    train_dataset=mapped_qa_dataset["train"], # 训练集
    args=transformers.TrainingArguments(
        per_device_train_batch_size=4, # 每个设备的训练批次大小
        gradient_accumulation_steps=4, # 梯度累积步数
        warmup_steps=100, # 热身步数
        max_steps=100, # 最大步数
        learning_rate=1e-3, # 学习率
        fp16=True, # 是否使用半精度浮点数
        logging_steps=1, # 日志记录步数
        output_dir='outputs', # 输出目录
    ),
    data_collator=transformers.DataCollatorForLanguageModeling(tokenizer, mlm=False) # 数据收集器
)

peft_model.config.use_cache = False  # 禁用缓存,以消除警告。在推理时请重新启用!
trainer.train() # 开始训练
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

在这里插入图片描述

6.7 检查LoRA大小

  这个例子中,我们训练了100步,虽然在每一步中损失有一些随机变化,但总体上损失在训练过程中逐渐降低
  

"""将LoRA微调的模型保存到本地
"""
# 定义模型的标识符
model_id = "BLOOM-560m-LoRA"
# 将微调后的模型保存到指定的路径
peft_model.save_pretrained(model_id)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

  然后检查文件在我们的文件系统中的大小:

# 使用ls命令查看指定目录下的文件和文件夹的详细信息
!ls -lh {model_id}
  • 1
  • 2

在这里插入图片描述

6.8 测试

  我们有一个经过微调的 LoRA 模型,让我们问它几个问题。首先,我们将定义一个辅助函数,该函数将接受一个上下文和问题,运行预测,并生成一个回答

"""用于比较结果的辅助函数"""

from IPython.display import display, Markdown

def make_inference(context, question):

    # 将输入转换为标记
    batch = tokenizer(f"**CONTEXT:**\n{context}\n\n**QUESTION:**\n{question}\n\n**ANSWER:**\n", return_tensors='pt', return_token_type_ids=False)
    # 将标记移动到GPU上进行推理
    batch = batch.to(device='cuda')

    # 使用经过微调的模型和原始模型进行推理
    with torch.cuda.amp.autocast():
        # 如果应用了LoRA,推理时间可能会更快,
        # 但是不应用LoRA可以让我同时进行微调前后的实验

        # 原始模型
        peft_model.disable_adapter_layers()
        output_tokens_raw = model.generate(**batch, max_new_tokens=200)

        # LoRA模型
        peft_model.enable_adapter_layers()
        output_tokens_qa = peft_model.generate(**batch, max_new_tokens=200)

    # 显示结果
    display(Markdown("# 原始模型\n"))
    display(Markdown((tokenizer.decode(output_tokens_raw[0], skip_special_tokens=True))))
    display(Markdown("\n# QA模型\n"))
    display(Markdown((tokenizer.decode(output_tokens_qa[0], skip_special_tokens=True))))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

  让我们来看几个例子:

context = "You are a monster, and you eat yellow legos."  # 定义一个字符串变量context,表示上下文信息
question = "What is the best food?"  # 定义一个字符串变量question,表示问题信息

make_inference(context, question)  # 调用make_inference函数,传入上下文信息和问题信息作为参数
  • 1
  • 2
  • 3
  • 4

在这里插入图片描述
  

  示例2:

context = "you are a math wizard"  # 定义上下文,表示你是一个数学奇才
question = "what is 1+1 equal to?"  # 定义问题,询问1+1等于多少

make_inference(context, question)  # 调用make_inference函数进行推理
  • 1
  • 2
  • 3
  • 4

在这里插入图片描述
  因为只使用了一个560M参数的模型,所以它在基本推理方面并不出色。

context = "Answer the riddle"  # 上下文是一个谜语的问题
question = "What gets bigger the more you take away?"  # 问题是:越拿越多的东西会变得更大吗?

make_inference(context, question)  # 调用make_inference函数进行推理
  • 1
  • 2
  • 3
  • 4

在这里插入图片描述

  

  

  

  

  

  

  







d \sqrt{d} d 1 8 \frac {1}{8} 81 x ˉ \bar{x} xˉ x ^ \hat{x} x^ x ~ \tilde{x} x~ ϵ \epsilon ϵ
ϕ \phi ϕ


声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/人工智能uu/article/detail/932882
推荐阅读
相关标签
  

闽ICP备14008679号