在 Kaggle Notebooks(甚至是你自己的电脑)上微调大型语言模型以解决实际任务

图片来源:DALL·E 3
用简单的词语和概念探索关于如何根据您的需求调整 LLM 的一些理论和想法
得益于上下文学习能力,如果您希望模型解决您的特定问题,生成式大型语言模型 (LLM) 是一个可行的解决方案。实际上,我们可以直接通过输入提示为 LLM 提供一些目标任务的示例,而这些示例并未对其进行明确训练。但是,这可能会令人不满意,因为 LLM 可能需要学习复杂问题的细微差别,而且您无法在提示中放入太多示例。此外,您可以在自己的设备上托管自己的模型,并控制您提供给外部来源的数据。解决方案是微调您的本地 LLM,因为微调会改变行为并增加您所选 LLM 模型的知识。
微调需要更多高质量的数据、更多计算和一些努力,因为您必须提示和编码解决方案。尽管如此,它会为您带来更不容易产生幻觉、可以托管在您的服务器甚至您的计算机上、并且最适合您希望模型发挥最佳性能的任务的 LLM。在这两篇短文中,我将介绍所有理论基础和工具,以便在 Kaggle notebook 中针对特定问题进行模型微调,每个人都可以轻松访问。理论部分很大程度上借鉴了 Sebastian Raschka 在 lightning.ai 社区博客文章中的著作,他在其中系统地探索了语言模型的微调方法。
由于我们将使用从 Kaggle Models 获取的 Llama 2 模型,因此您必须访问 Kaggle 上的模型页面 (https://www.kaggle.com/models/metaresearch/llama-2) 并按照那里的说明向 Meta 请求访问其模型(您可以使用此页面:https://ai.meta.com/resources/models-and-libraries/llama-downloads/)。
语言模型的微调已经有与基于解码器架构的生成模型(如 GPT)和基于编码器架构的以嵌入为中心的模型(如 BERT,BERT 中的 E 代表编码器)合作的历史。这涉及到在权重更新方面冻结语言模型的较大部分或较小部分,并在模型末尾附加一个机器学习分类器(通常是逻辑回归模型,但也可以是支持向量分类器、随机森林或 XGBoost 模型)或一些额外的神经网络架构。您保留的原始语言模型未冻结的部分越多,其中越多的部分,尤其是嵌入,将适应您的问题(您将获得更好的模型评估分数),但如果模型很大(并且 LLM 在权重和层方面都非常庞大),这将需要大量的计算能力,还需要大量数据,因为您需要大量证据来正确更新模型中的许多参数。假设您有一些已标记的任务示例,这在业务应用中极为常见,并且资源不多。在这种情况下,正确的解决方案是保持原始模型的大部分冻结,并更新其分类终端部分的参数。
因此,在大型语言模型方面存在越来越多的限制,因为您无法轻松拥有足够的计算能力和数据量来更新其层。幸运的是,近年来已经设计了各种巧妙的 LLM 微调方法,确保以最小的参数训练获得出色的建模结果。这些技术通常被称为参数高效微调方法或 PEFT。所有 PEFT 方法都涉及提示修改、适配器方法和参数更新
- 提示修改涉及更改输入提示以获得所需结果。可以通过硬更改(我们通过试错直接更改提示)或软更改(我们依靠反向传播来学习额外的自由嵌入张量,从而增强现有提示的嵌入)来实现。这些方法干预 LLM 架构的开端。
- 适配器方法涉及在 LLM 架构内部添加一些可通过反向传播更新并修改模型行为的自适应层。这些方法干预 LLM 架构的中间部分。
- 参数更新可能涉及网络权重的特定部分或网络本身,通过权重的低秩适应(https://arxiv.org/abs/2106.09685),这种方法“可以将可训练参数的数量减少 10,000 倍,并将 GPU 内存需求减少三倍”。
特别是,通过低秩适应(LoRA)的参数更新是一种通过将更新矩阵分成两个较小的矩阵来“破解”常规反向传播更新的方法,这两个较小的矩阵相乘可以返回原始更新矩阵。这类似于矩阵分解(如 SVD),其中通过允许原始矩阵内容不可避免的损失来实现降维。在我们的例子中,当针对特定任务训练 LLM 时,允许其原始复杂性损失实际上是允许的,以便 LLM 能够获得我们感兴趣的任务的专业知识。
因此,如果一个层的更新矩阵维度为 1,024 x 1,024,相当于 1,048,576 个数值,那么将其分解为两个大小分别为 1,024 x 16 和 16 x 1,024 的矩阵,相乘后可以返回类似于原始矩阵的东西,这将使需要处理的数值减少到 32,768。
这种矩阵分解留给神经网络的反向传播,超参数 r 允许我们指定用于适应的低秩矩阵的秩。较小的 r 对应于更简单的低秩矩阵,从而减少了适应的参数数量。因此,这可以加快训练速度,并可能降低计算需求。在 LoRA 中,选择较小的 r 值需要在模型复杂性、适应能力以及欠拟合或过拟合的可能性之间进行权衡。因此,对各种 r 值进行实验对于在 LoRA 参数之间取得适当平衡至关重要。
此外,微调完成后,如果我们保留用于更新的低秩矩阵(它们不占用太多空间),我们可以通过在微调后的原始 LLM 上进行乘法来重复使用它们,而无需直接更新模型本身的权重。此时,我们可以通过减小使用 LoRA 的 LLM 的大小来节省内存和磁盘空间。答案是对原始 LLM 进行量化,将其精度降低到 4 位精度。这就像压缩文件一样,同样地,LLM 保持压缩(即量化),只在需要计算 LoRA 矩阵缩减和更新时才进行扩展。通过这种方式,您可以在单个 GPU 上微调大型语言模型,同时保持微调后 LLM 的性能。这种方法被称为 QLoRA,基于 Tim Dettmers、Artidoro Pagnoni、Ari Holtzman 和 Luke Zettlemoyer 的工作(参见 https://arxiv.org/abs/2305.14314)。它也作为开源项目在 GitHub 上提供。
在本文的第二部分,我将提供关于处理 LLM 进行微调任务的实际方面的参考和见解,尤其是在 Kaggle Notebooks 等资源受限的环境中。我还将演示如何通过少量命令和最小的配置设置轻松地将这些技术付诸实践。
金融情感分析的实战微调
您可以在此 Kaggle Notebook 中找到本节的所有代码:微调 Llama-2 进行情感分析
在本关于在 Kaggle Notebooks 上微调 Llama 2 模型的实践教程中,我们将处理金融和经济信息的情感分析,展示如何使用有限且常用的资源来处理此类任务。金融和经济信息的情感分析对于企业来说具有高度相关性,原因有几个关键点,包括市场洞察(获取对市场趋势、投资者信心和消费者行为的宝贵洞察)、风险管理(识别潜在的声誉风险)以及投资决策(衡量利益相关者、投资者和公众的情绪,企业可以评估各种投资机会的潜在成功)。
在微调 Llama 2 等大型语言模型的技术细节之前,我们必须找到正确的数据集来展示微调的潜力。
特别是在金融和经济文本中,带注释的数据集非常稀少,许多数据集专门用于专有目的。2014 年,阿尔托大学商学院的学者们引入了一组约 5,000 句话来解决训练数据不足的问题(Malo, P., Sinha, A., Korhonen, P., Wallenius, J., & Takala, P., 2014,“好债还是坏债:检测经济文本中的语义倾向。”信息科学与技术协会期刊,65[4], 782–796 - https://arxiv.org/abs/1307.5336)。该集合旨在建立人工注释基准,作为评估替代建模技术的标准。参与注释者(16 位具有足够金融市场背景知识的人员)被指示仅从投资者的角度评估句子,评估新闻是否可能对股价产生积极、消极或中性影响。
FinancialPhraseBank 数据集是一个综合性集合,从散户投资者的角度捕捉金融新闻头条的情绪。该数据集包含“情感”和“新闻头条”两个关键列,有效地将情感分为消极、中性或积极。这个结构化数据集是分析和理解金融新闻中复杂情感动态的宝贵资源。自 2014 年在《信息科学与技术协会期刊》上发表论文以来,它已被用于各种研究和倡议。
这些数据可在 CC BY-NC-SA 3.0 DEED 许可证下使用,并可在 https://huggingface.co/datasets/financial_phrasebank 找到完整的详细描述和说明。还有几个 Kaggle 数据集镜像。在我们的示例中,我们从所有可用数据(4840 条来自英文金融新闻的按情感分类的句子)中抽取 900 个示例用于训练,900 个示例用于测试。训练集和测试集中的示例是平衡的,并且包含相同数量的积极、中性和消极样本。我们还使用大约一百个示例的样本,主要用于剩余的积极和中性示例(没有留下太多消极示例)用于训练期间的评估目的(我们只使用评估进行监控;不基于此样本做出任何决定)。
事不宜迟,我们只需指出 Kaggle notebook,其中所有单元格都逐步注释,展示如何构建分析
在本文中,我们将说明微调的逻辑步骤。从更宏观的角度来看,就像在任何机器学习项目中一样,您需要:
- 获取数据
- 为训练、验证和测试安排数据
- 实例化您的模型
- 评估您的模型现状
- 微调(训练)您的模型
- 评估您的模型
然而,在处理 LLM 时,有意义的做法是仅通过硬提示工程来评估模型,以建立一个对您的工作有意义的基准(如果您的 LLM 已经足够熟练地完成所需任务,您实际上无需进行进一步的微调)。
现在让我们深入了解模型实例化和微调的实际操作。
首先,使用的软件包有
- PyTorch 2.1.2(之前是 2.0.0)
- transformers 4.36.2(之前是 4.31)
- datasets 2.16.1
- accelerate 0.26.1(之前是 0.23.0)
- bitsandbytes 0.42.0(之前是 0.41.1)
至于 trl,我选择了一个 2024 年 1 月 22 日在 GitHub 上发布的 commit,而对于 peft,我检索了同一天发布的另一个 commit(因此这两个包都是最新鲜的)
然后,您需要定义要调整的 LLM。
model_name = "../input/llama-2/pytorch/7b-hf/1"
我们的选择是 Llama 2 7b-hf,Meta 的 7B 预训练模型,已转换为 Hugging Face Transformers 格式。Llama 2 构成了一系列预先存在且经过优化的生成文本模型,大小从 70 亿到 700 亿参数不等。Llama 2 采用增强的 Transformer 架构,作为自回归语言模型运行。其微调迭代包括监督微调 (SFT) 和带有人类反馈的强化学习 (RLHF),以确保符合人类的帮助性和安全性标准。除了是一个表现良好的 LLM 之外,选择该模型的原因在于它是 Llama 系列中最灵活的模型,因此最适合演示即使是较小的 LLM 也是微调专业任务的良好选择。
我们的下一步是定义 BitsAndBytes 配置。
compute_dtype = getattr(torch, "float16")
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=compute_dtype,
bnb_4bit_use_double_quant=False,
)
Bitsandbytes 是 Tim Dettmers 开发的一个 Python 包,它作为 CUDA 自定义函数(尤其是 8 位优化器、矩阵乘法 (LLM.int8()) 和量化函数)的轻量级封装。它允许运行以 4 位精度存储的模型:虽然 4 位 bitsandbytes 将权重存储在 4 位中,但计算仍然以 16 位或 32 位进行,并且可以在此处选择任何组合(float16、bfloat16、float32 等)。Bitsandbytes 背后的思想已由 Tim Dettmers、Artidoro Pagnoni、Ari Holtzman 和 Luke Zettlemoyer 在论文中正式提出(参见 https://arxiv.org/abs/2305.14314)。
您可以将其视为 LLM 的压缩器,它允许我们安全地将其存储在标准计算机或服务器的磁盘和内存中:神经网络以 4 位精度存储(具有更好性能的标准化 float 4),这可能会比典型的 32 位精度节省大量空间。此外,为了增加压缩,可以选择 bnb_4bit_use_double_quant(但在我们的示例中没有),它在初始量化之后实现二次量化,从而额外减少每个参数 0.4 位。然而,在网络上进行计算时,计算是根据我们定义的 bnb_4bit_compute_dtype 执行的,即 16 位精度,这是一种包含快速和精确计算的合适数值精度。这种解压缩阶段可能需要更多时间,具体取决于之前获得的压缩程度。
下一步,一旦初始化了 Bitsandbytes 压缩,就是使用 HuggingFace (HF) AutoModelForCausalLM 及其分词器加载我们的模型
model = AutoModelForCausalLM.from_pretrained(
model_name,
device_map="auto",
quantization_config=bnb_config,
)
model.config.use_cache = False
model.config.pretraining_tp = 1
tokenizer = AutoTokenizer.from_pretrained(model_name,
trust_remote_code=True,
)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
在这里,除了 quantization_config(Bitsandbytes 压缩)和设置为“auto”的 device_ap 以便您可以在系统上利用任何可用资源(CPU 或 GPU)之外,我们还必须注意到该模型的具体特点,即 pretraining_tp 参数必须设置为 1(HF 文档中声明的值,用于确保预训练结果的精确重现性)以及 use_cache 设置为 False(模型是否应返回上次的键/值注意力,Llama 不需要)。对于分词器,pad token 等同于 eos token(用于指示 token 序列结束的序列结束 token),并且填充侧设置为右侧,通常指示为在使用 Llama 模型时使用的右侧。
实例化模型后,我们必须准备训练阶段,这需要实施基于减少参数数量的 LoRA 策略,以便将原始 LLM 调整到我们的任务(有关更多详细信息,请参见上一篇文章)。
peft_config = LoraConfig(
lora_alpha=16,
lora_dropout=0.1,
r=64,
bias="none",
task_type="CAUSAL_LM",
target_modules="all-linear",
)
LoRA 配置指定了 PEFT 的参数。下面是我们使用的参数解释
- r:LoRA 更新矩阵的秩。缩减系数表示一种权衡:它越低,消耗的内存越少,但更新期间的近似值会增加。
- lora_alpha:LoRA 更新矩阵的学习率。经验法则:它应该是 r 值的两倍。
- lora_dropout:LoRA 更新矩阵的 dropout 概率。
- bias:要使用的偏差类型。可能的值为 none、additive 和 learned。我们选择 none,因为此选项会从 LoRA 模型中移除偏差,这可以将模型大小缩小多达 20%。
- task_type:模型正在训练的任务类型。可能的值是 CAUSAL_LM 和 MASKED_LM。许多人说这没有区别,但 CAUSAL_LM 是我们目的的正确选择。
最后,我们必须解释一下在 LoRAConfig 中添加参数 target_modules="all-linear"。LoraConfig 对象包含一个 target_modules 参数,它应表示为列表或数组。在您在线找到的一些示例中,目标模块通常是 [“query_key_value”];在其他地方,它们是其他东西(在我们的例子中是线性层,由“all-linear”字符串值表示),但总是指 Transformer 架构。选择哪些层进行微调实际上取决于您想要实现什么(以及什么对您的问题效果更好)。正如 LoRA 论文 (https://arxiv.org/abs/2106.09685) 中所述,Hu, Edward J., 等人。“Lora: Large language models 的低秩适应。” arXiv preprint arXiv:2106.09685 (2021), “_我们可以将 LoRA 应用于神经网络中任何权重矩阵的子集,以减少可训练参数的数量_”,并且 “_我们限制我们的研究仅将注意力权重适应于下游任务并冻结 MLP 模块……既为了简单起见,也为了参数效率_”。最后,论文指出 “_我们将在未来的工作中对 MLP 层、LayerNorm 层和偏差进行实证研究_”,这意味着您可以根据获得的结果和您的“参数预算”(您微调的层越多,所需的计算和内存就越多)来微调任何您想要的层。这在论文的第 7.1 节“_变压器中我们应该将 LORA 应用于哪些权重矩阵?_”中阐述得更清楚,其中论文作者的选择由他们的参数“预算”证明,但您不仅限于此,您必须根据您的架构和问题寻找整体最佳性能。
peft 中的默认 LoRA 设置遵循原始 LoRA 论文,将可训练权重合并到每个注意力块的查询和值层中。这是我在微调的第一个实现中完成的。然而,在 QLoRA 论文 (https://huggingface.ac.cn/papers/2305.14314) 中,研究表明,将可训练权重引入 Transformer 模型中的所有线性层可以提高性能,使其与完全微调的性能相匹配。鉴于模块的选择可能因架构而异,并且您必须在您选择的模型架构中手动搜索此类线性层,他们引入了一个用户友好的简写:只需指定 target_modules='all-linear',然后让其余的包来处理。
定义 LoRA 设置后,我们还需要单独设置训练参数。
training_arguments = TrainingArguments(
output_dir=output_dir,
num_train_epochs=3,
per_device_train_batch_size=1,
gradient_accumulation_steps=8,
optim="paged_adamw_32bit",
save_steps=0,
logging_steps=25,
learning_rate=2e-4,
weight_decay=0.001,
fp16=True,
bf16=False,
max_grad_norm=0.3,
max_steps=-1,
warmup_ratio=0.03,
group_by_length=True,
lr_scheduler_type="cosine",
report_to="tensorboard",
evaluation_strategy="epoch"
)
training_arguments 对象指定了模型训练的参数。以下是一些最重要的参数:
- output_dir:训练日志和检查点将保存的目录。
- num_train_epochs:模型训练的 epochs 数量。
- per_device_train_batch_size:每个设备上每个批次的样本数量。
- gradient_accumulation_steps:在更新模型参数之前累积梯度的批次数量。
- optim:用于训练模型的优化器。我们的选择是
paged_adamw_32bit 优化器,是 AdamW 优化器的一种变体,旨在提高 32 位 GPU 的效率。它通过将模型参数分解成更小的页面并分别优化每个页面来实现这一点。这可以减少优化器的内存使用并提高其在 32 位 GPU 上的性能。 - save_steps:保存检查点的步数。
- logging_steps:记录训练指标的步数。
- learning_rate:优化器的学习率。
- weight_decay:优化器的权重衰减参数。
- fp16:是否使用 16 位浮点精度。在 GPU 上训练时将 fp16 设置为 True,就像我们所做的那样,可以将内存使用量减少一半,训练速度提高 2 倍,训练成本减少一半。然而,它也可能降低训练模型的准确性,并使训练过程更加困难。
- bf16:是否使用 BFloat16 精度(不适用于我们的 GPU)。
- max_grad_norm:最大梯度范数。最大梯度范数是一个超参数,用于在训练期间控制梯度更新的幅度。它在训练中很重要,因为它可以帮助防止模型变得不稳定和通过进行过于强烈的更新而对训练数据过度拟合。
- max_steps:模型训练的最大步数。
- warmup_ratio:用于预热学习率的训练步骤比例,即逐渐将学习率从 0 增加到最终值的训练步骤比例。它在训练中很重要,因为预热有助于提高模型的稳定性和性能。
- group_by_length:是否按长度对训练样本进行分组以最大程度地减少填充并提高效率。
- lr_scheduler_type:要使用的学习率调度器类型。我们选择余弦调度器,它在训练开始时逐渐增加学习率,从而帮助模型快速学习数据的基本特征。然后,它在训练结束时逐渐降低学习率,这有助于模型收敛到更好的解决方案。
- report_to:报告训练指标的工具。我们选择使用 TensorBoard。
- evaluation_strategy:训练期间评估模型的策略。通过选择“epoch”,我们可以在每个 epoch 对评估数据集进行评估,这可以帮助我们判断训练和评估指标是否正在发散。
最后,我们可以定义训练本身,它委托给 trl 包中的 SFTTrainer。trl 是 HuggingFace 提供的一个库,提供了一套工具,用于通过强化学习和其他方法训练 transformer 语言模型,从监督微调步骤 (SFT)、奖励建模步骤 (RM) 到近端策略优化步骤 (PPO)。
trl 库现在还借助 setup_chat_format()
函数简化了为对话式 AI 任务设置模型和分词器的过程。此函数执行以下任务
- 引入特殊标记,例如
<s>
和<e>
,表示对话的开始和结束到分词器。 - 调整模型的嵌入层以适应这些新添加的标记。
- 定义分词器的聊天模板,负责将输入数据格式化为对话式结构。默认模板是 chatml,其灵感来自 OpenAI。
- 此外,用户可以选择指定
resize_to_multiple_of
参数,使其能够将嵌入层的大小调整为所提供参数的倍数(例如,64)。
以下是此函数的使用示例
from transformers import AutoModelForCausalLM, AutoTokenizer
# Load model and tokenizer
model = AutoModelForCausalLM.from_pretrained("facebook/opt-350m")
tokenizer = AutoTokenizer.from_pretrained("facebook/opt-350m")
# Set up the chat format with default 'chatml' format
model, tokenizer = setup_chat_format(model, tokenizer)
在微调期间向语言模型添加特殊标记至关重要,尤其是在训练聊天模型时。这些标记在描绘对话中的各种角色(例如用户、助手和系统)方面至关重要。通过策略性地插入这些标记,模型可以理解对话固有的结构组件和顺序流。
换句话说,set_chat_format 提供的设置有助于模型识别对话动态的细微差别。模型变得能够适应不同说话者之间的转换,并理解与每个角色相关的上下文线索。这种增强的意识对于模型在聊天环境中生成连贯、上下文适当的响应至关重要。
然而,至于训练,trl 包提供了 SFTTrainer,这是一个用于监督微调(简称 SFT)的类。SFT 是一种机器学习中常用的技术,尤其是在深度学习领域,用于将预训练模型调整到特定任务或数据集。
其通常工作方式如下:
预训练:最初,神经网络模型在大型数据集上进行训练,以完成通用任务,例如在 ImageNet 等数据集上进行图像分类。在此预训练阶段,模型学习识别数据中的高级特征和模式。在我们的例子中,我们正在利用 LLM,例如 Llama 2。
微调:预训练后,模型可以在较小的、特定于任务的数据集上进一步训练或微调。此微调过程涉及使用新数据集更新预训练模型的参数。但是,模型不会从头开始训练,而是从预训练期间学习到的权重开始。这使得模型能够通过调整其参数以更好地适应新数据,从而快速适应新任务或数据集。
监督:微调过程是受监督的,这意味着模型会获得来自任务特定数据集的带标签示例(输入-输出对)。这种监督指导学习过程并帮助模型提高其在特定任务上的性能。
当您拥有可用于目标任务的小型数据集时,监督微调特别有用,因为它利用了预训练模型中编码的知识,同时仍然适应新任务的细节。与从头开始训练模型相比,这种方法通常会导致更快的收敛和更好的性能,特别是当预训练模型已在大规模和多样化的数据集上进行训练时。
以下是我们 SFTTrainer 的设置
trainer = SFTTrainer(
model=model,
train_dataset=train_data,
eval_dataset=eval_data,
peft_config=peft_config,
dataset_text_field="text",
tokenizer=tokenizer,
args=training_arguments,
packing=False,
max_seq_length=1024,
)
SFTTrainer 对象使用以下参数进行初始化:
- model:要训练的模型。
- train_dataset:训练数据集。
- eval_dataset:评估数据集。
- peft_config:PEFT 配置。
- dataset_text_field:数据集中文本字段的名称(我们使用了 HuggingFace Dataset 实现)。
- tokenizer:要使用的分词器。
- args:我们之前设置的训练参数。
- packing:是否打包训练样本。
- max_seq_length:最大序列长度。
这基本上完成了我们的微调工作,因为剩下的就是训练本身,然后将更新后的模型保存到磁盘。
trainer.train()
trainer.model.save_pretrained("trained-model")
然而,如果我们不能重用或分享我们微调过的模型,我们就不能说一切都完成了。您如何保存您的微调模型并发布或重用它?
只需几个命令即可实现,尽管它们需要相当多的空闲 CPU 和 GPU 内存,这意味着如果我们在同一个 Kaggle notebook 上继续操作,我们需要进行一些清理。
在我们把微调过的 QLoRA 权重保存到磁盘后,事情就开始了。
trainer.save_model()
tokenizer.save_pretrained(output_dir)
这里的关键是,我们只是保存 QLoRA 权重,它们是我们原始模型(在我们的例子中是 LLama 2 7B)的修饰符(通过矩阵乘法)。实际上,在使用 QLoRA 时,我们只训练适配器而不是整个模型。因此,当您在训练期间保存模型时,您只保留适配器权重,而不是整个模型。
如果您想保存整个模型以便更轻松地与文本生成推理一起使用,您可以使用 merge_and_unload 方法将适配器权重合并到模型权重中。然后,您可以使用 save_pretrained 方法保存模型。这将创建一个默认模型,可用于推理任务。一个简单的命令即可实现合并,但首先,我们必须清理内存。
import gc
del [model, tokenizer, peft_config, trainer, train_data, eval_data, bnb_config, training_arguments]
del [df, X_train, X_eval]
del [TrainingArguments, SFTTrainer, LoraConfig, BitsAndBytesConfig]
for _ in range(100):
torch.cuda.empty_cache()
gc.collect()
删除不再使用的模型和数据后,我们使用 gc.collect() 垃圾回收内存,并使用 torch.cuda.empty_cache() 清理 GPU 内存缓存。
然后,我们可以继续合并权重并使用合并后的模型进行测试。
from peft import AutoPeftModelForCausalLM
finetuned_model = "./trained_weigths/"
compute_dtype = getattr(torch, "float16")
tokenizer = AutoTokenizer.from_pretrained("/kaggle/input/llama-2/pytorch/7b-hf/1")
model = AutoPeftModelForCausalLM.from_pretrained(
finetuned_model,
torch_dtype=compute_dtype,
return_dict=False,
low_cpu_mem_usage=True,
device_map=device,
)
merged_model = model.merge_and_unload()
merged_model.save_pretrained("./merged_model",safe_serialization=True, max_shard_size="2GB")
tokenizer.save_pretrained("./merged_model")
上述代码片段对 QLoRA 权重、原始模型及其相关分词器执行了多项任务。首先,导入了相关模块,包括 peft 包中的 AutoPeftModelForCausalLM,它依赖于 torch 和 transformers 库中的 AutoTokenizer 等现有组件。
然后定义了路径和配置,例如包含微调模型权重的目录(finetuned_model),并将计算数据类型设置为 float16 (compute_dtype)。分词器从 LLama 2 模型位置加载。随后,模型使用指定的配置加载,可能包括内存使用优化。加载后,模型经过合并和卸载过程,将 QLoRA 和原始权重合并在一起。此操作需要时间和相当多的内存。如果此处发生错误,那是因为您没有足够的可用内存(通常是:NotImplementedError: Cannot copy out of meta tensor; no data!)。只需重新检查您的内存情况(CPU 和 GPU;nvidia-smi 命令可能会有所帮助),更好地清理,回收内存垃圾,然后重试。
合并后的模型最终保存到指定目录,确保安全序列化并将分片大小限制为 2GB。此外,分词器与合并后的模型一起保存,以便将来使用。
就是这样。该模型现在存储在一个新目录中,可以加载并用于您需要的任何任务。最后一步,我们只需在测试集上测试模型。
一份分类报告突出显示:
Accuracy: 0.851
Accuracy for label 0: 0.913
Accuracy for label 1: 0.863
Accuracy for label 2: 0.777
Classification Report:
precision recall f1-score support
0 0.95 0.91 0.93 300
1 0.74 0.86 0.80 300
2 0.88 0.78 0.82 300
accuracy 0.85 900
macro avg 0.86 0.85 0.85 900
weighted avg 0.86 0.85 0.85 900
Confusion Matrix:
[[274 24 2]
[ 11 259 30]
[ 2 65 233]]
这绝对是相比于同一问题(完全相同的训练和测试数据)上返回 0.623 总体准确率的更简单基线模型 (用于情感分析的 LSTM 基线) 的一个显著改进。
使用 Mistral 7B Instruct v0.2 和 Phi-2 重新进行金融情感分析的实战微调
在对 Llama 7B 进行金融情感分析数据集的微调之后,该数据集在消费级、易于访问和免费的 GPU 上运行(https://www.kaggle.com/code/lucamassaron/fine-tune-llama-2-for-sentiment-analysis/),您可以重用相同的代码来微调最近出现的大型语言模型,例如
- Mistral 的 Mistral 7B Instruct v0.2
https://www.kaggle.com/code/lucamassaron/fine-tune-mistral-v0-2-for-sentiment-analysis - Microsoft 的 Phi-2
https://www.kaggle.com/code/lucamassaron/fine-tune-phi-2-for-sentiment-analysis
在本文中,我将介绍这些新型大型语言模型令人兴奋的特性,以及如何修改最初的 Llama 微调以适应它们各自的需求。
新模型
Mistral 7B Instruct v0.2 在其前身 Mistral 7B Instruct v0.1 的基础上构建,引入了精细的指令微调技术,提升了其能力。一切都始于 Mistral 开发的 Mistral 7B,这是一家由前 Google 的 DeepMind 和 Meta 员工创立的巴黎 AI 初创公司,旨在与 OpenAI 竞争大型语言模型和生成式 AI 的构建、训练和应用。
Mistral 7B 是一款拥有 7.3B 参数的模型,在其同类产品中脱颖而出,在所有基准测试中始终超越 Llama 2 13B,并在多项任务上与 Llama 1 34B 性能持平。它甚至在代码相关领域与 CodeLlama 7B 的熟练程度相媲美,同时在英语任务中保持其卓越性(但它可以出色地处理所有欧洲语言)。
为了达到如此卓越的性能水平,Mistral 7B 采用了两种创新技术:分组查询注意力(GQA)用于加速推理,以及滑动窗口注意力(SWA)用于以较低成本高效处理长序列。
GQA 通过并行分组和处理相关查询词来简化推理过程,从而减少计算时间并提高整体速度。另一方面,SWA 通过将长序列分解为更小的窗口并对每个窗口独立应用注意力机制来解决处理长序列的挑战,从而实现更高效的处理并降低内存消耗。
Mistral 7B Instruct 模型旨在针对特定任务进行微调,例如指令遵循、创意文本生成和问答,从而证明了 Mistral 7B 在微调方面的灵活性。需要注意的是,它没有内置的审查机制来过滤不适当或有害的内容。
Phi-2 是微软研究院开发的一种小型语言模型(LLM)。它只有 27 亿个参数,比其他 LLM 小得多。它的训练基于与 Phi-1 和 Phi-1.5 相似的语料库,重点关注“教科书质量”数据,包括来自 The Stack v1.2 的 Python 代码子集、来自 StackOverflow 的问答内容、来自 code_contests 的竞赛代码,以及由 gpt-3.5-turbo-0301 生成的合成 Python 教科书和练习。Phi-2 也未经过人类反馈强化学习的微调,因此没有任何类型的过滤。
代码调整
将 Mistral 7B Instruct 投入使用轻而易举(没有双关语)。您需要考虑的是,要使用指令微调,您需要将提示封装在 [INST] 和 [/INST] 标记之间。就是这样!微调过程结束后,结果显示在我们的测试集上,所有类别的情感检测都达到了最佳性能。
Accuracy: 0.868
Accuracy for label negative: 0.977
Accuracy for label neutral: 0.743
Accuracy for label positive: 0.883
Phi-2 需要更多的工作,因为它对指令的要求不那么严格,并且表现出非常特殊的行为。它倾向于将问题视为测验,甚至返回其原始学习中未被要求提供的文本元素。因此,在评估文本情感后,它会博学地开始讨论莫卧儿帝国。从网络获取答案最有效的方法是将响应标记限制至少为 3 个,以允许额外的空格和答案字母出现在预测之前(这是无法避免的),并将提示结构化为
以下短语的情绪是:“……”
解决方案:正确的选项是...
关于 Phi-2 的另一个重要事实是,在为 LoRA(低秩注意力)模块设置参数时,您需要声明要调整的目标模块,LoRA 是一种参数缩减技术,用于压缩 Transformer 模型中的注意力矩阵。在这里,我们发现有必要明确指定“Wqkv”和“out_proj”。“Wqkv”和“out_proj”是 Transformer 架构中用于注意力和输出投影的模块。
Wqkv 是一个三层前馈网络,它生成注意力机制的查询、键和值向量。这些向量用于计算注意力分数,注意力分数用于确定输入序列中每个单词与输出序列中每个单词的相关性。
out_proj 是一个线性层,用于将解码器输出投影到词汇空间中。该层负责将解码器的隐藏状态转换为词汇上的概率分布,然后用于选择要生成的下一个标记。
在 Phi-2 模型的上下文中,这些模块用于微调模型以完成指令遵循任务。通过微调这些模块,模型可以更好地理解和响应指令。
通过这样做,结果虽然比 Mistral Instruct 性能略低,但比 LLama 更好,而且模型尺寸更小。
Accuracy: 0.856
Accuracy for label negative: 0.973
Accuracy for label neutral: 0.743
Accuracy for label positive: 0.850
结论
至此,我们完成了在 Kaggle Notebooks(它也可以在消费级硬件上运行)中微调 Meta 的 LLama 2(以及 Mistral 和 Phi2)等 LLM 的步骤。对于许多机器学习问题,在掌握了运行学习的技术细节之后,一切都归结为对问题的良好理解、适当的数据准备以及一些实验,以使您的工具适应问题(必要时反之亦然)。
代码参考