赞
踩
精调是将机器学习模型调整到特定应用的过程,这在实现一致和高质量的性能方面非常重要。在本文中,我们将讨论“低秩适应”(LoRA),这是最流行的精调策略之一。首先,我们将介绍理论,然后使用LoRA来精调语言模型,提高其问答能力。
在微调之前,输出是无意义的,模型会反复重复问题和虚假答案。在微调之后,输出变得清晰、简洁和准确。
**这对谁有用?**对于任何对学习最先进的机器学习方法感兴趣的人都有用。在本文中,我们将重点介绍语言建模,但LoRA是许多机器学习应用中的热门选择。
**这篇文章有多高级?**这篇文章对于初学者的数据科学家和爱好者来说是可以理解的,但其中包含了在高级应用中至关重要的主题。
**先决条件:**虽然不是必需的,但对大型语言模型(LLMs)有扎实的工作理解可能会很有用。欢迎参考我的文章《变压器——直观而全面的解释》了解更多信息:
您可能还想了解一下梯度是什么。我也有一篇关于这个的文章:
如果您对这些主题中的任何一个都不太自信,您仍然可以从本文中获得很多东西,但如果您感到困惑,这些文章可以帮助您。
随着机器学习的最新发展,对模型性能的期望也在增加,需要更复杂的机器学习方法来满足对性能的需求。在机器学习的早期阶段,构建一个模型并在单次训练中训练它是可行的。
训练,在其最简单的意义上。您将一个未经训练的模型,提供给它数据,并获得一个高性能的模型。
对于简单问题来说,这仍然是一种流行的策略,但对于更复杂的问题,将训练分为两个部分,即“预训练”和“微调”,可能会很有用。总体思路是在一个大规模数据集上进行初始训练,并在一个定制的数据集上对模型进行优化。
预训练和微调,是典型的单次训练策略的改进。
这种“预训练”然后“微调”的策略可以让数据科学家利用多种形式的数据,并使用大型预训练模型来完成特定任务。因此,预训练然后微调是一种常见且非常强大的范例。不过,它也存在一些困难,我们将在下面的部分进行讨论。
最基本的微调形式是使用与预训练模型相同的过程来微调新数据上的模型。例如,您可以在大量的通用文本数据上训练模型,然后使用相同的训练策略在更具体的数据集上微调该模型。
在最简单的形式中,预训练和微调是程序上相同的。您在一组数据上进行预训练模型,然后在另一组数据上进行微调。
这种策略可能很昂贵。LLMs绝对庞大,要使用这种策略进行微调,您需要足够的内存来存储不仅是整个模型,还有整个模型中每个参数的梯度(梯度是让模型知道调整参数方向的东西)。参数和梯度都需要存在于GPU上,这就是为什么训练LLMs需要如此多的GPU内存的原因。
“低秩适应”(LoRA)是一种“参数高效微调”(PEFT)的形式,它允许使用少量可学习参数对大型模型进行微调。LoRA采用了几个概念,当它们一起使用时,可以大幅改善微调:
我们可以将微调视为学习参数的变化,而不是调整参数本身。
我们可以尝试通过删除重复信息将这些变化压缩成较小的表示。
我们可以通过简单地将它们添加到预训练参数中来“加载”我们的变化。
如果这些概念让你感到困惑,不用担心;在接下来的章节中,我们将逐步介绍这些想法。
正如我们之前讨论的,微调的最基本方法是迭代地更新参数。就像正常的模型训练一样,你让模型进行推理,然后根据推理的错误程度更新模型的参数。
回想一下之前讨论过的反向传播图。这是微调的基本形式。
LoRA 对此有稍微不同的看法。与其将微调视为学习更好的参数,不如将微调视为学习参数变化。您可以冻结模型参数,就像它们一样,然后学习使模型在微调任务中表现更好所需的这些参数的变化。
这与训练非常相似。您让模型进行推断,然后根据推断的错误程度进行更新。但是,您不是更新模型参数,而是更新模型参数的变化。
在LoRA中,我们冻结模型参数,并创建一组描述这些参数变化的新值。然后,我们学习必要的参数变化,以在微调任务上表现更好。
你可能会认为这是一个有点愚蠢的抽象。LoRA的整个目的是要使微调变得更小、更快,那么添加更多的数据和额外的步骤如何使我们能够做到这一点呢?在下一节中,我们将讨论这个问题。
为了说明的方便,许多人将密集网络表示为一系列加权连接。每个输入都乘以一些权重,然后相加以创建输出。
一个密集网络的概念图,由连接权重的神经元列表组成。特定神经元的值将是所有输入乘以相应权重的总和。
从概念角度来看,这是一个完全准确的可视化,但在底层,实际上是通过矩阵乘法来实现的。一个称为权重矩阵的值矩阵与输入向量相乘,以创建输出向量。
为了让你了解矩阵乘法的工作原理。在上面的例子中,红点等于a₁₁•b₁₂ + a₁₂•b₂₂。正如你所看到的,这种乘法和加法的组合与神经元示例中的组合非常相似。如果我们创建正确形状的矩阵,矩阵乘法最终会与加权连接的概念完全一致。
思考一个密集网络,将其视为左侧的加权连接,以及右侧的矩阵乘法。在右侧的图表中,左侧的向量将是输入,中间的矩阵将是权重矩阵,右侧的向量将是输出。为了方便阅读,只包含了部分值。
从LoRA的角度来看,理解权重实际上是一个矩阵非常重要,因为矩阵具有某些可以用来压缩信息的特性。
你可以将矩阵(一个二维值数组)视为向量的行或列。现在让我们将矩阵视为向量的行。假设我们有一个由两个向量组成的矩阵,大致如下:
一个由两个向量组成的矩阵,以矩阵中的行表示。
这两个向量指向不同的方向。你不能将一个向量压缩和拉伸使其等于另一个向量。
每一行都是一个矩阵,作为一个向量绘制。无论蓝色向量被压缩或拉伸多少,它永远不会指向与红色向量相同的方向,反之亦然。
让我们加入第三个向量。
向量和指向完全相同的方向,而向量指向不同的方向。因此,无论如何压缩和拉伸向量和,它们都无法用来描述。因此,向量与和是线性无关的。然而,你可以拉伸使其等于,反之亦然,所以向量和是线性相关的。假设和指向稍微不同的方向。
现在和可以一起使用(通过一些压缩和拉伸)来描述,同样地,和可以用其他向量来描述。在这种情况下,我们会说所有向量都不是线性独立的,因为所有向量都可以用矩阵中的其他向量来描述。A``B``C``A``B
使用A和B来描述C。可以通过乘以一个负数来翻转B的大小,然后加到A上。
从概念上讲,线性独立的向量可以被认为包含不同的信息,而线性相关的向量之间包含一些重复的信息。
秩的概念是为了量化矩阵中的线性独立性。我将跳过细节,直接进入重点:我们可以将一个矩阵分解为一些线性独立的向量;这种矩阵的形式被称为“行阶梯形式”。
因此,矩阵可以包含一定程度的“重复信息”,即线性相关性。我们可以利用因子分解的思想,用两个较小的矩阵来表示一个大矩阵。类似于一个大数可以表示为两个较小数的乘积,一个矩阵可以被视为两个较小矩阵的乘积。
右侧的两个向量相乘后,等价于左侧的矩阵。尽管它们具有相同的值,但左侧的向量占据的空间大小只有右侧矩阵的40%。矩阵越大,越有可能节省空间。
如果你有一个大矩阵,具有显著的线性相关性(因此秩较低),你可以将该矩阵表示为两个相对较小的矩阵的乘积。这种分解的思想使得LoRA占用了如此小的内存空间。
LoRA将调整参数视为学习参数变化。然而,我们并不直接学习参数变化,而是学习参数变化矩阵的因子。
现在我们已经了解了LoRA的各个部分是如何工作的,让我们把它们整合起来。
首先,我们冻结模型参数。我们将使用这些参数进行推理,但不会更新它们。
我们创建了两个矩阵。这些矩阵的大小是这样的,当它们相乘时,它们的大小将与我们正在微调的模型的权重矩阵的大小相同。在一个大型模型中,有多个权重矩阵,您将为每个权重矩阵创建一个这样的配对。
the LoRA paper refferes to these matrices as matrix “A” and “B”. Together, these matices represent the leranable parameters during LoRA fine tuning.
We calculate the change matrix
LoRA论文将这些矩阵称为矩阵“A”和“B”。这些矩阵一起代表了LoRA微调过程中的可学习参数。
我们计算变化矩阵
然后我们将输入通过冻结的权重和变化矩阵传递。
我们根据两个输出的组合计算损失,然后根据损失更新矩阵A和B。
注意,虽然这里展示了变化矩阵以说明问题,但实际上它是即时计算的,从未被存储,这就是为什么LoRA的内存占用如此小的原因。实际上,在训练期间只存储模型参数、矩阵A和B以及A和B的梯度。
我们执行此操作,直到我们优化了变化矩阵的因素以进行微调任务。更新矩阵A和B的反向传播步骤比更新完整模型参数集的过程要快得多,因为A和B要小得多。这就是为什么尽管训练过程中有更多的操作,LoRA仍然通常比传统微调更快的原因。
当我们最终想要使用这个微调模型进行推断时,我们只需计算变化矩阵,并将变化添加到权重中。这意味着LoRA不会改变模型的推断时间。
在研究本文时,我发现很多人没有讨论一个概念上的不一致。将机器学习模型视为一个大箱子的权重是可以的,但实际上许多模型具有复杂的结构,不太像一个箱子。对我来说,像transformer这样的模型中的参数如何应用这个变化矩阵的概念并不明显。
在另一篇文章中,我介绍了Transformer的结构图。"Nx"符号表示左侧和右侧都会重复多次。这不是一个简单的权重矩阵,因此如何应用LoRA并不明显。图片来源:source
根据我目前的理解,对于Transformer模型,有两个要注意的事项:
通常,在Transformer的多头自注意力层中,密集网络(用于构建查询、键和值)的深度只有1。也就是说,只有一个输入层和一个由权重连接的输出层。
这些浅层密集网络是Transformer中大部分可学习参数,非常非常大。可能有超过100,000个输入神经元连接到100,000个输出神经元,这意味着描述其中一个网络的单个权重矩阵可能有10B个参数。因此,尽管这些网络的深度只有1,但它们非常宽,因此描述它们的权重矩阵非常大。
从LoRA在Transformer模型上的角度来看,这些是被优化的主要参数;你正在学习每个这些非常大但浅层的密集层的分解变化。正如之前讨论的那样,每个这些浅层密集层都有可以表示为矩阵的权重。
LoRA有一个超参数,称为,它描述了用于构建之前讨论的变化矩阵的和矩阵的深度。较高的值意味着更大的和矩阵,这意味着它们可以在变化矩阵中编码更多的线性独立信息。r``A``B``r``A``B
一个具有r值等于1和2的LoRA的概念图。在这两个例子中,分解的A和B矩阵导致相同大小的变化矩阵,但是r=2能够将更多线性独立的信息编码到变化矩阵中,因为A和B矩阵中包含更多信息。
事实证明,LoRA论文所做的核心假设,即模型参数的变化具有低隐式秩,是一个相当强的假设。微软(LoRA的出版商)的人员尝试了一些值,并发现即使是秩为一的矩阵也表现出色。r``A``B
从LoRA论文中得知:
通常,在选择时,我听到的建议是:当数据与预训练中使用的数据相似时,较低的r
值可能就足够了。当在非常新的任务上进行微调时,可能需要对模型进行重大的逻辑更改,这时可能需要较高的r
值。r
考虑到我们已经讨论了很多理论,你可能期望这是一个相当长的教程,但是我有好消息!HuggingFace有一个模块,可以使LoRA变得非常简单。
在这个例子中,我们将对一个预训练模型进行微调,用于问题回答。让我们开始吧。完整的代码可以在这里找到:
我们将使用一些超出简单PyTorch项目范围的模块。以下是它们的作用:
**bitsandbytes:**用于使用较小的数据类型表示模型,节省内存。
**datasets:**用于下载数据集
**accelerate:**一些模块的机器学习互操作性所需的依赖项
**loralib:**LoRA实现
**peft:**一般的“参数高效微调”模块,我们与LoRA的接口
**transformers:**用于下载和使用HuggingFace的预训练transformers。
# 安装所需的库
!pip install -q bitsandbytes datasets accelerate loralib
!pip install -q git+https://github.com/huggingface/peft.git git+https://github.com/huggingface/transformers.git
我们将使用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:
r: A和B矩阵的秩
lora_alpha: 这是一个相当有争议的参数。很多人对此有很多想法。你可以将其视为一个缩放因子,默认情况下应该等于r
,据我所知。
target_modules: 我们希望使用LoRA优化的模型部分。BLOOM模块有名为query_key_value
的参数,我们希望对其进行优化。
lora_dropout: dropout是一种隐藏输入以抑制模型过拟合的技术(称为正则化)。这是一个被隐藏的概率。
bias: 神经网络通常每个连接有两个参数,一个是“权重”,一个是“偏置”。在这个例子中,我们只训练权重。
task_type: 不是非常必要,在超类中使用。设置为PeftConfig
的CAUSAL_LM
,因为我们使用的是“因果”语言模型。
"""使用参数高效微调设置LoRA""" from peft import LoraConfig, get_peft_model # 定义LoRA在这个特定示例中的工作方式 config = LoraConfig( r=8, # LoRA的层数 lora_alpha=8, # LoRA的alpha值 target_modules=["query_key_value"], # 目标模块列表 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)是一个高质量、常用且许可证宽松的数据集。
"""加载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() # 开始训练
让我们保存我们的LoRA优化。在这个例子中,我们不必过于关注损失(模型中有多少误差),但它可以作为一个很好的指标。在这个例子中,我们训练了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))))
让我们来看几个例子,看看我们的精调模型在问题回答方面有多好:
例子1)
```python
context = "You are a monster, and you eat yellow legos." # 定义一个字符串变量context,表示上下文信息
question = "What is the best food?" # 定义一个字符串变量question,表示问题信息
make_inference(context, question) # 调用make_inference函数,传入上下文信息和问题信息作为参数
```python
示例2)
```python
context = "you are a math wizard" # 定义上下文,表示你是一个数学奇才
question = "what is 1+1 equal to?" # 定义问题,询问1+1等于多少
make_inference(context, question) # 调用make_inference函数进行推理
```python
我们只使用了一个560M参数的模型,所以它在基本推理方面并不出色。询问它1+1等于多少可能有点超出它的能力范围,但至少它失败得更优雅。
```python
context = "Answer the riddle" # 上下文是一个谜语的问题
question = "What gets bigger the more you take away?" # 问题是:越拿越多的东西会变得更大吗?
make_inference(context, question) # 调用make_inference函数进行推理
```python
就是这样!我们讨论了微调的概念,以及LoRA如何将微调视为学习参数变化,而不是迭代学习新参数。我们学习了线性独立性和秩,以及由于大多数权重矩阵的秩较低,变化矩阵可以用小因子表示。我们将所有内容整合在一起,逐步介绍了LoRA,然后使用HuggingFace PEFT模块在问答任务中实现了LoRA。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。