使用 Unsloth 超高效微调 Llama 3.1

最先进的监督微调初学者指南

最近发布的 Llama 3.1 为模型提供了令人难以置信的性能水平,缩小了闭源和开源模型之间的差距。您可以针对特定用例微调 Llama 3.1,而不是使用冻结的通用 LLM,例如 GPT-4o 和 Claude 3.5,以更低的成本实现更好的性能和可定制性。

在本文中,我们将对监督微调进行全面概述。我们将将其与提示工程进行比较,以了解何时使用它有意义, 详细说明主要技术及其优缺点, 并介绍主要概念, 例如 LoRA 超参数, 存储格式, 和聊天模板.最后,我们将通过在 Google Colab 中使用 Unsloth 进行最先进的优化来微调 Llama 3.1 8B 来在实践中实现它。

本文中使用的所有代码都可以在 Google Colab 和 LLM 课程中找到。特别感谢 Daniel Han 回答我的问题。

🔧 监督微调

监督微调 (SFT) 是一种改进和定制预训练 LLM 的方法。它涉及在较小的指令和答案数据集上重新训练基础模型。主要目标是将预测文本的基本模型转换为可以遵循指示并回答问题的助手。SFT 还可以增强模型的整体性能、添加新知识或使其适应特定任务和领域。然后,微调的模型可以经历一个可选的偏好对齐阶段(请参阅我关于 DPO 的文章),以删除不需要的响应、修改其样式等。

下图显示了一个指令示例。它包括用于引导模型的系统提示、用于提供任务的用户提示以及模型预期生成的输出。您可以在 LLM 数据集 GitHub 存储库中找到💾高质量的开源指令数据集列表。

在考虑 SFT 之前,我建议尝试提示工程技术,例如少样本提示检索增强生成 (RAG)。在实践中,这些方法可以解决许多问题,而无需微调,使用闭源或开放权重模型(例如,Llama 3.1 Instruct)。如果这种方法不能满足您的目标(在质量、成本、延迟等方面),那么当指令数据可用时,SFT 就成为一个可行的选择。请注意,SFT 还提供额外的控制和可定制性等优势,以创建个性化的 LLM。

但是,SFT 有局限性。在利用基础模型中已有的知识时,它效果最好。学习全新的信息(如未知语言)可能具有挑战性,并导致更频繁的幻觉。对于基础模型未知的新域,建议先在原始数据集上持续预训练它。

另一方面,指令模型(即已经微调的模型)已经非常接近您的需求。例如,一个模型可能表现非常好,但声明它是由 OpenAI 或 Meta 而不是您训练的。在这种情况下,您可能希望使用偏好对齐来稍微引导指示模型的行为。通过为一小组指令(100 到 1000 个样本)提供选择和拒绝的样本,您可以强制 LLM 说您训练了它而不是 OpenAI。

⚖️ SFT技术

三种最流行的 SFT 技术是完全微调、LoRA 和 QLoRA。

完全微调是最直接的 SFT 技术。它涉及在指令数据集上重新训练预训练模型的所有参数。这种方法通常提供最佳结果,但需要大量的计算资源(需要几个高端 GPU 来微调 8B 模型)。因为它修改了整个模型,所以它也是最具破坏性的方法,并可能导致灾难性地忘记以前的技能和知识。

低秩自适应 (LoRA) 是一种流行的参数高效微调技术。它不是重新训练整个模型,而是冻结权重并在每个目标层引入小适配器(低秩矩阵)。这使得 LoRA 能够训练许多参数,这些参数大大低于完全微调 (小于 1%), 减少了内存使用和训练时间.这种方法是非破坏性的,因为原始参数被冻结,然后可以随意切换或组合适配器。

QLoRA(量化感知低秩适应)是 LoRA 的扩展,可节省更大的内存。与标准 LoRA 相比,它可额外减少高达 33% 的内存,因此在 GPU 内存受限时特别有用。这种效率的提高是以更长的训练时间为代价的,QLoRA 的训练时间通常比常规 LoRA 多 39%。

虽然 QLoRA 需要更多的训练时间,但其大量内存节省可以使其成为 GPU 内存有限的场景中唯一可行的选择。出于这个原因,我们将在下一节中使用这种技术来微调 Google Colab 上的 Llama 3.1 8B 模型。

🦙 微调 Llama 3.1 8B

为了有效地微调 Llama 3.1 8B 模型,我们将使用 Daniel 和 Michael Han 的 Unsloth 库。得益于其自定义内核,与其他选项相比,Unsloth 的训练速度提高了 2 倍,内存使用率提高了 60%,非常适合 Colab 等受限环境。不幸的是,Unsloth 目前仅支持单 GPU 设置。对于多 GPU 设置,我推荐流行的替代品,如 TRL 和 Axolotl(两者都包括 Unsloth 作为后端)。

在此示例中,我们将在 mlabonne/FineTome-100k 数据集上对其进行 QLoRA 微调。这是我使用 HuggingFaceFW/fineweb-edu-classifier 重新过滤的 arcee-ai/The-Tome(没有 arcee-ai/qwen2-72b-magpie-en)的子集。请注意,此分类器不是为指令数据质量评估而设计的,但我们可以将其用作粗略的代理。由此产生的 FineTome 是一个超高质量的数据集,包括对话、推理问题、函数调用等。

让我们先创建环境,测试环境为 WSL2,Ubuntu 22.04.3 LTS:

安装所有必需的库:

安装后,我们可以按以下步骤导入它们。

现在让我们加载模型。由于我们想使用 QLoRA,我选择了预量化的 unsloth/Meta-Llama-3.1-8B-bnb-4bit。与原始的 16 位精度模型 (16 GB) 相比,这款 meta-llama/Meta-Llama-3.1-8B 的 4 位精度版本明显更小 (5.4 GB),下载速度更快。我们使用 bitsandbytes 库以 NF4 格式加载。

加载模型时,我们必须指定最大序列长度,这会限制其上下文窗口。Llama 3.1 支持高达 128k 的上下文长度,但在此示例中,我们将将其设置为 2,048,因为它消耗更多的计算和 VRAM。最后,dtype 参数会自动检测您的 GPU 是否支持 BF16 格式,以便在训练期间提高稳定性(此功能仅限于 Ampere 和更新的 GPU)。

现在我们的模型已以 4 位精度加载,我们希望为使用 LoRA 适配器进行参数高效微调做好准备。LoRA 有三个重要参数:

  • Rank (r), 确定 LoRA 矩阵大小.等级通常从 8 开始,但可以上升到 256。更高的等级可以存储更多的信息,但会增加 LoRA 的计算和内存成本.我们在这里将其设置为 16。
  • Alpha (α),更新的比例因子。Alpha 直接影响适配器的贡献,通常设置为 Rank 值的 1 倍或 2 倍。
  • 目标模块: LoRA可应用于各种模型组件, 包括注意力机制 (Q, K, V矩阵), 输出投影, 前馈块, 和线性输出层.虽然最初专注于注意力机制, 将 LoRA 扩展到其他组件已经显示出好处.但是,适配更多模块会增加可训练参数的数量和内存需求。

在这里,我们设置 r=16,α=16,并针对每个线性模块以最大限度地提高质量。我们不使用辍学和偏见来加快训练速度。

此外,我们将使用秩稳定 LoRA (rsLoRA),它将 LoRA 适配器的比例因子修改为 1/√r 而不是 1/r。这可以稳定学习(特别是对于更高的适配器等级),并允许随着等级的增加提高微调性能。梯度检查点由 Unsloth 处理,用于将输入和输出嵌入卸载到磁盘并节省 VRAM。

使用此 LoRA 配置, 我们只会训练 80 亿个参数中的 4200 万个 (0.5196%)。这表明与完全微调相比,LoRA 的效率要高得多.

现在让我们加载并准备我们的数据集。指令数据集以特定格式存储:它可以是 Alpaca、ShareGPT、OpenAI 等。首先,我们想要解析此格式以检索我们的指令和答案。我们的 mlabonne/FineTome-100k 数据集使用 ShareGPT 格式,带有一个独特的“对话”列,其中包含 JSONL 中的消息。与 Alpaca 等更简单的格式不同,ShareGPT 非常适合存储多轮对话,这更接近用户与 LLM 的交互方式。

一旦我们的指令-答案对被解析,我们就想重新格式化它们以遵循聊天模板。聊天模板是一种构建用户和模型之间对话的方法。它们通常包括特殊令牌,用于识别消息的开头和结尾、谁在说话等。基础模型没有聊天模板,因此我们可以选择任何模板:ChatML、Llama3、Mistral 等。在开源社区中,ChatML 模板(最初来自 OpenAI)是一个流行的选项。它只是添加两个特殊标记(<|im_start|> 和 <|im_end|>)来指示谁在说话。

如果我们将此模板应用于前面的指令示例,我们将得到以下结果:

在下面的代码块中,我们使用 mapping 参数解析 ShareGPT 数据集,并包含 ChatML 模板。然后,我们加载并处理整个数据集,以将聊天模板应用于每个对话。

现在,我们已准备好为运行指定训练参数。我想简要介绍一下最重要的超参数:

  • 学习率(Learning rate):它控制模型更新其参数的强度。太低,训练会很慢,并且可能会卡在局部最小值。太高,训练可能会变得不稳定或发散,从而降低表现。
  • LR 调度器(LR scheduler):它在训练期间调整学习率 (LR),从较高的 LR 开始以实现快速的初始进度,然后在后期阶段降低它。线性调度器和余弦调度器是两种最常见的选项。
  • 批量大小(Batch size):在权重更新之前处理的样本数量。较大的批量大小通常会导致更稳定的梯度估计,并可以提高训练速度,但它们也需要更多的内存。梯度累积通过在更新模型之前在多个前/后传递上累积梯度,从而有效地实现更大的批量大小。
  • Num epochs:通过训练数据集的完成传递次数。更多的时期使模型能够更多地查看数据,从而可能带来更好的性能。但是,过多的 epoch 会导致过拟合。
  • 优化器(Optimizer):用于调整模型参数以最小化损失函数的算法。在实践中,强烈建议使用 AdamW 8 位:它的性能与 32 位版本一样好,同时使用较少的 GPU 内存。AdamW 的分页版本仅在分布式设置中才有意义。
  • 权重衰减(Weight decay):一种正则化技术,将大权重的惩罚添加到损失函数中。它通过鼓励模型学习更简单、更可推广的特征来帮助防止过度拟合。然而,过多的权重衰减会阻碍学习。
  • 热身步骤(Warmup steps):训练开始时的一段时间,学习率从小值逐渐增加到初始学习率。预热可以帮助稳定早期训练,尤其是在学习率大或批量大的情况下,允许模型在进行大规模更新之前适应数据分布。
  • 包装(Packing):批次具有预定义的序列长度。我们可以将多个小样品合并到一个批次中,而不是为每个样品分配一个批次,从而提高了效率。

我在 Google Colab 上使用 A100 GPU(40 GB VRAM)在整个数据集(100k 个样本)上训练了模型。培训历时4小时45分钟。当然,您可以使用具有较少 VRAM 和较小批量大小的较小 GPU,但它们的速度并不快。例如,L4 大约需要 19 小时 40 分钟,而空闲 T4 则需要高达 47 小时。

在这种情况下,我建议只加载数据集的一个子集以加快训练速度。您可以通过修改前面的代码块来实现此操作,例如 dataset = load_dataset(“mlabonne/FineTome-100k”, split=“train[:10000]”) 以仅加载 10k 个样本。或者,您可以使用更便宜的云 GPU 提供商,例如 Paperspace、RunPod 或 Lambda Labs。

下面是代码中 SFTTrainerTrainingArguments 的参数详细解释:

SFTTrainer 参数:
  1. model=model: 指定要训练的模型,这通常是一个预训练的语言模型(如 GPT、BERT 等)。
  2. tokenizer=tokenizer: 指定与模型配套的分词器,确保文本被正确地编码和解码。
  3. train_dataset=dataset: 指定训练使用的数据集。
  4. dataset_text_field="text": 指定数据集中包含文本的字段名。在数据集中,这个字段包含了要用来训练模型的文本内容。
  5. max_seq_length=max_seq_length: 指定输入序列的最大长度。序列超过此长度将被截断,短于此长度的将被填充。
  6. dataset_num_proc=2: 指定在数据处理时使用的并行进程数。在数据预处理和加载时使用多个进程以加快速度。
  7. packing=True: 开启数据打包模式,这有助于更有效地利用批次中的空间,将多个短句子打包在同一个序列中,提高训练效率。
TrainingArguments 参数:
  1. learning_rate=3e-4: 设置学习率,控制模型在每一步训练时权重的更新速度。3e-4 是一个常用的初始值,可以根据具体需求调整。
  2. lr_scheduler_type="linear": 指定学习率调度器类型。linear 表示学习率线性下降。
  3. per_device_train_batch_size=4: 每个设备(如 GPU)上训练时的批次大小。这里每次处理 4 个样本。
  4. gradient_accumulation_steps=4: 累积梯度的步骤数。即每 4 个批次计算一次梯度更新,这样相当于将批次大小扩展到 4×4=16。
  5. num_train_epochs=1: 训练的轮次,即遍历训练数据集的次数。这里设置为 1。
  6. fp16=not is_bfloat16_supported(): 是否使用混合精度训练(FP16)。如果系统不支持 BFloat16(较新硬件支持),则使用 FP16 训练来加速。
  7. bf16=is_bfloat16_supported(): 如果硬件支持 BFloat16(如较新的 NVIDIA GPU),则使用 BFloat16 进行训练。
  8. logging_steps=1: 每训练 1 个步骤记录一次日志,这可以帮助你跟踪训练进度。
  9. optim="adamw_8bit": 使用 8-bit 精度的 AdamW 优化器,可以减少显存使用并提升训练速度。
  10. weight_decay=0.01: 权重衰减(L2 正则化)系数,有助于防止模型过拟合。
  11. warmup_steps=10: 学习率预热的步骤数,在训练开始时逐渐增加学习率,避免训练初期不稳定。
  12. output_dir="output": 模型输出保存的路径。训练后的模型和日志将保存到该目录。
  13. seed=0: 随机种子,用于确保训练的可重复性。不同的种子可能会导致不同的训练结果。

现在模型已经训练好了,让我们用一个简单的提示来测试它。这不是一个严格的评估,而只是一个快速检查,以发现潜在的问题。我们使用 FastLanguageModel.for_inference() 来获得 2 倍的推理速度。

模型的响应是“9.9”,这是正确的!

现在让我们保存我们训练好的模型。如果您还记得有关 LoRA 和 QLoRA 的部分,我们训练的不是模型本身,而是一组适配器。Unsloth 中有三种保存方法: lora 仅保存适配器, 和 merged_16bit/merged_4bit 以 16 位/4 位精度将适配器与模型合并.

在下文中,我们将以 16 位精度将它们合并,以最大限度地提高质量。我们首先将其保存在本地的“model”目录中,然后将其上传到Hugging Face Hub。您可以在 mlabonne/FineLlama-3.1-8B 上找到经过训练的模型。

Unsloth 还允许您直接将模型转换为 GGUF 格式。这是为 llama.cpp 创建的量化格式,与大多数推理引擎兼容,例如 LM StudioOllama 和 oobabooga 的 text-generation-webui。由于您可以指定不同的精度(请参阅我关于 GGUF 和 llama.cpp 的文章),我们将遍历一个列表以q2_kq3_k_mq4_k_mq5_k_mq6_k、q8_0对其进行量化,并将这些量化上传到 Hugging Face。mlabonne/FineLlama-3.1-8B-GGUF 包含我们所有的 GGUF。

恭喜,我们从头开始微调了一个模型,并上传了量化数据,您现在可以在您最喜欢的推理引擎中使用。随意尝试 mlabonne/FineLlama-3.1-8B-GGUF 上可用的最终模型。现在该怎么办?以下是有关如何使用模型的一些想法:

  • 在 Open LLM 排行榜进行评估(您可以免费提交)或使用其他评估,例如在 LLM AutoEval 中。
  • 使用偏好数据集(如 mlabonne/orpo-dpo-mix-40k)将其与直接偏好优化对齐,以提高性能。
  • 使用 AutoQuant 以其他格式(如 EXL2、AWQ、GPTQ 或 HQQ)对其进行量化,以实现更快的推理或更低的精度。
  • 使用 ZeroChat 将其部署到 Hugging Face Space 上,用于经过充分训练以遵循聊天模板的模型(~20k 样本)。

结论

本文全面概述了监督微调以及如何在实践中将其应用于 Llama 3.1 8B 模型。通过利用 QLoRA 的高效内存使用,我们设法在 GPU 资源有限的超高质量数据集上微调 8B LLM。我们还为更大规模的运行提供了更有效的替代方案,并为进一步步骤提供了建议,包括评估、偏好对齐、量化和部署。

我希望本指南有用。如果你有兴趣了解更多关于LLMs的信息,我建议你查看LLM课程。如果您喜欢这篇文章,请在 X @maximelabonne 和 Hugging Face @mlabonne 上关注我。祝你好运,微调模型!

原文链接:使用 Unsloth 超高效微调 Llama 3.1 (huggingface.co)

完整的代码

实际运行结果

单卡,RTX4090 大概需要6个半小时左右

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部