赞
踩
直接上干货:大语言模型可以做什么?
LoRA的核心思想:
提示:后面是主要内容:
随着机器学习的最新发展,对模型性能的期望也在增加,需要更复杂的机器学习方法来满足对性能的需求。在机器学习的早期阶段,构建一个模型并在单次训练中训练它是可行的。
训练,在其最简单的意义上。您将一个未经训练的模型,提供给它数据,并获得一个高性能的模型。
对于简单问题来说,这仍然是一种流行的策略,但对于更复杂的问题,将训练分为两个部分,即“预训练”和“微调”,可能会很有用。总体思路是在一个大规模数据集上进行初始训练,并在一个定制的数据集上对模型进行优化。
这种“预训练”然后“微调”的策略可以让数据科学家利用多种形式的数据,并使用大型预训练模型来完成特定任务。因此,预训练然后微调是一种常见且非常强大的范例。
最基本的微调形式 是使用与预训练模型相同的过程来微调新数据上的模型。例如,您可以在大量的通用文本数据上训练模型,然后使用相同的训练策略,在更具体的数据集上微调该模型。
以上策略十分昂贵 。LLMs绝对庞大,需要足够的内存来存储整个模型,以及模型中每个参数的梯度(梯度是让模型知道调整参数方向的东西)。参数和梯度都需要存在于GPU上,这就是为什么训练LLMs需要如此多的GPU显存的。
“低秩适应”(LoRA)是一种“参数高效微调”(PEFT)的形式,它允许使用少量可学习参数对大型模型进行微调 。LoRA改善微调的几个点:
将微调视为学习参数的变化(▲w),而不是调整参数本身(w)。
通过删除重复信息,将这些变化压缩成较小的表示。
通过简单地将它们添加到预训练参数中来“加载”新的变化。
正如之前讨论的,微调的最基本方法是迭代地更新参数。就像正常的模型训练一样,你让模型进行推理,然后根据推理的错误程度更新模型的参数。
与其将微调视为学习更好的参数,LoRA 将微调视为学习参数变化:冻结模型参数,然后学习使模型在微调任务中表现更好所需的这些参数的变化。
类似于训练,首先让模型推理,然后根据error进行更新。但是,不更新模型参数,而是更新模型参数的变化。
在LoRA中,我们冻结模型参数,并创建一组描述这些参数变化的新值。然后,我们学习必要的参数变化,以在微调任务上表现更好。LoRA添加更多的数据和额外的步骤,如何使微调变得更小、更快?
矩阵的秩是为了量化矩阵中的线性独立性。我们可以将一个矩阵分解为一些线性独立的向量;这种矩阵的形式被称为“行阶梯形式”。
因此,矩阵可以包含一定程度的“重复信息”,即线性相关性。如果你有一个大矩阵,具有显著的线性相关性(因此秩较低),可以将该矩阵表示为两个相对较小的矩阵的乘积。这种分解的思想使得LoRA占用了如此小的内存空间。
首先冻结模型参数。使用这些参数进行推理,但不会更新它们。然后创建两个矩阵,当它们相乘时,它们的大小将与我们正在微调的模型的权重矩阵的大小相同。在一个大型模型中,有多个权重矩阵,为每个权重矩阵创建一个这样的配对。
LoRA将这些矩阵称为矩阵“A”和“B”。这些矩阵一起代表了LoRA微调过程中的可学习参数。
然后将输入通过冻结的权重和变化矩阵传递:
根据两个输出的组合计算损失,然后根据损失更新矩阵A和B:
这些变化矩阵是即时计算的,从未被存储,这就是为什么LoRA的内存占用如此小的原因。实际上,在训练期间只存储模型参数、矩阵A和B以及A和B的梯度。
我们执行此操作,直到我们优化了变化矩阵的因素以进行微调任务。更新矩阵A和B的反向传播步骤比更新完整模型参数集的过程要快得多,因为A和B要小得多。这就是为什么尽管训练过程中有更多的操作,LoRA仍然通常比传统微调更快的原因。
当我们最终想要使用这个微调模型进行推断时,我们只需计算变化矩阵,并将变化添加到权重中。这意味着LoRA不会改变模型的推断时间:
实际上许多大模型具有复杂的结构,不是一个整体结构。像Transformer这样的模型中的参数,如何应用LoRA?
对于Transformer模型,有两个要注意的事项:
通常,在Transformer的多头自注意力层中,密集网络(用于构建查询、键和值)的深度只有1。也就是说,只有一个输入层和一个由权重连接的输出层。
这些浅层密集网络是Transformer中大部分可学习参数,非常非常大。可能有超过100,000个输入神经元连接到100,000个输出神经元,这意味着描述其中一个网络的单个权重矩阵可能有10B个参数。因此,尽管这些网络的深度只有1,但它们非常宽,因此描述它们的权重矩阵非常大。
LoRA在Transformer模型上,要学习每个非常大但浅层的密集层的分解变化。
LoRA有一个超参数,称为Rank,它描述了用于构建之前讨论的变化矩阵的深度。较高的值意味着更大的和矩阵,这意味着它们可以在变化矩阵中编码更多的线性独立信息。
“r"参数可以被视为"信息瓶颈”。较小的r值意味着A和B可以用更小的内存占用编码较少的信息。较大的r值意味着A和B可以编码更多的信息,但内存占用更大。
一个具有r值等于1和2的LoRA的概念图。在这两个例子中,分解的A和B矩阵导致相同大小的变化矩阵,但是r=2能够将更多线性独立的信息编码到变化矩阵中,因为A和B矩阵中包含更多信息。
事实证明,LoRA论文所做的核心假设,即模型参数的变化具有低隐式秩,是一个相当强的假设。微软(LoRA的出版商)的人员尝试了一些值,并发现即使是秩为一的矩阵也表现出色。
LoRA论文中建议:当数据与预训练中使用的数据相似时,较低的r值可能就足够了。当在非常新的任务上进行微调时,可能需要对模型进行重大的逻辑更改,这时可能需要较高的r值
下面以HuggingFace的一个模块做例子,用LoRA对一个预训练模型进行微调,用于问题回答。代码 链接在这里:
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
我们将使用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")
"""使用参数高效微调设置LoRA"""
from peft import LoraConfig, get_peft_model
# 定义LoRA在这个特定示例中的工作方式
config = LoraConfig(
r=8, # LoRA的层数,即A和B矩阵的秩
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模型
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}%")
将使用SQUAD数据集 来提高语言模型在问答任务上的性能。斯坦福问答数据集(SQUAD)是一个高质量、常用且许可证宽松的数据集。
from datasets import load_dataset # 导入load_dataset函数,用于加载数据集
qa_dataset = load_dataset("squad_v2") # 使用load_dataset函数加载SQUAD数据集,并将结果赋值给qa_dataset变量
模型将在特定的数据结构上,对语言模型进行微调。模型将期望以以下一般形式的文本作为输入:
# 代码注释
'''
CONTEXT: 上下文
{context}
QUESTION: 问题
{question}
ANSWER: 答案
{answer}</s>
'''
们将向模型提供上下文和问题,模型将被期望向我们提供答案。因此,我们将重新格式化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'])))
"""
# 导入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() # 开始训练
这个例子中,我们训练了100步,虽然在每一步中损失有一些随机变化,但总体上损失在训练过程中逐渐降低。
"""将LoRA微调的模型保存到本地
"""
# 定义模型的标识符
model_id = "BLOOM-560m-LoRA"
# 将微调后的模型保存到指定的路径
peft_model.save_pretrained(model_id)
然后检查文件在我们的文件系统中的大小:
# 使用ls命令查看指定目录下的文件和文件夹的详细信息
!ls -lh {model_id}
我们有一个经过微调的 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))))
让我们来看几个例子:
context = "You are a monster, and you eat yellow legos." # 定义一个字符串变量context,表示上下文信息
question = "What is the best food?" # 定义一个字符串变量question,表示问题信息
make_inference(context, question) # 调用make_inference函数,传入上下文信息和问题信息作为参数
示例2:
context = "you are a math wizard" # 定义上下文,表示你是一个数学奇才
question = "what is 1+1 equal to?" # 定义问题,询问1+1等于多少
make_inference(context, question) # 调用make_inference函数进行推理
因为只使用了一个560M参数的模型,所以它在基本推理方面并不出色。
context = "Answer the riddle" # 上下文是一个谜语的问题
question = "What gets bigger the more you take away?" # 问题是:越拿越多的东西会变得更大吗?
make_inference(context, question) # 调用make_inference函数进行推理
d
\sqrt{d}
d
1
8
\frac {1}{8}
81
x
ˉ
\bar{x}
xˉ
x
^
\hat{x}
x^
x
~
\tilde{x}
x~
ϵ
\epsilon
ϵ
ϕ
\phi
ϕ
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。