使用 Unsloth 超高效地微调 Llama 3.1

社区文章 发布于 2024 年 7 月 29 日

最先进的监督式微调入门指南

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

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

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

🔧 监督式微调

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

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

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

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

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

⚖️ SFT 技术

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

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

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

**QLoRA (Quantization-aware Low-Rank Adaptation)** 是 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 设置,我推荐流行的替代方案,如 TRLAxolotl(两者都包含 Unsloth 作为后端)。

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

我们首先安装所有必需的库。

!pip install "unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git"
!pip install --no-deps "xformers<0.0.27" "trl<0.9.0" peft accelerate bitsandbytes

安装完成后,我们可以像这样导入它们。

import torch
from trl import SFTTrainer
from datasets import load_dataset
from transformers import TrainingArguments, TextStreamer
from unsloth.chat_templates import get_chat_template
from unsloth import FastLanguageModel, is_bfloat16_supported

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

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

max_seq_length = 2048
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="unsloth/Meta-Llama-3.1-8B-bnb-4bit",
    max_seq_length=max_seq_length,
    load_in_4bit=True,
    dtype=None,
)

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

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

在这里,我们将 r=16,α=16,并针对每个线性模块以最大化质量。我们不使用 dropout 和偏差以加快训练速度。

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

model = FastLanguageModel.get_peft_model(
    model,
    r=16,
    lora_alpha=16,
    lora_dropout=0,
    target_modules=["q_proj", "k_proj", "v_proj", "up_proj", "down_proj", "o_proj", "gate_proj"], 
    use_rslora=True,
    use_gradient_checkpointing="unsloth"
)

通过这种 LoRA 配置,我们只训练 80 亿参数中的 4200 万(0.5196%)。这表明 LoRA 比全微调效率高得多。

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

一旦我们的指令-答案对被解析,我们希望将它们重新格式化以遵循**聊天模板**。聊天模板是组织用户和模型之间对话的一种方式。它们通常包含特殊标记来标识消息的开始和结束、谁在说话等。基本模型没有聊天模板,因此我们可以选择任何一个:ChatML、Llama3、Mistral 等。在开源社区中,ChatML 模板(最初来自 OpenAI)是一个流行的选择。它只是添加了两个特殊标记(<|im_start|><|im_end|>)来指示谁在说话。

如果我们将此模板应用于之前的指令样本,结果如下:

<|im_start|>system
You are a helpful assistant, who always provide explanation. Think like you are answering to a five year old.<|im_end|>
<|im_start|>user
Remove the spaces from the following sentence: It prevents users to suspect that there are some hidden products installed on theirs device.
<|im_end|>
<|im_start|>assistant
Itpreventsuserstosuspectthattherearesomehiddenproductsinstalledontheirsdevice.<|im_end|>

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

tokenizer = get_chat_template(
    tokenizer,
    mapping={"role": "from", "content": "value", "user": "human", "assistant": "gpt"},
    chat_template="chatml",
)

def apply_template(examples):
    messages = examples["conversations"]
    text = [tokenizer.apply_chat_template(message, tokenize=False, add_generation_prompt=False) for message in messages]
    return {"text": text}

dataset = load_dataset("mlabonne/FineTome-100k", split="train")
dataset = dataset.map(apply_template, batched=True)

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

  • **学习率**:它控制模型更新其参数的强度。太低,训练会很慢,并可能陷入局部最小值。太高,训练可能会变得不稳定或发散,从而降低性能。
  • **学习率调度器**:它在训练期间调整学习率 (LR),开始时使用较高的 LR 以实现快速初始进展,然后在后期阶段降低它。线性调度器和余弦调度器是两种最常见的选择。
  • **批处理大小**:在更新权重之前处理的样本数量。更大的批处理大小通常会导致更稳定的梯度估计并可以提高训练速度,但它们也需要更多的内存。梯度累积允许通过在多次前向/后向传递中累积梯度来有效地增加批处理大小,然后再更新模型。
  • **Epoch 数**:通过训练数据集的完整遍历次数。更多的 epoch 允许模型更多次地查看数据,可能带来更好的性能。然而,过多的 epoch 可能导致过拟合。
  • **优化器**:用于调整模型参数以最小化损失函数的算法。在实践中,强烈推荐使用 AdamW 8-bit:它与 32-bit 版本性能相同,同时使用更少的 GPU 内存。AdamW 的分页版本仅在分布式设置中才有趣。
  • **权重衰减**:一种正则化技术,它对损失函数中的大权重施加惩罚。它通过鼓励模型学习更简单、更可泛化的特征来帮助防止过拟合。然而,过多的权重衰减可能会阻碍学习。
  • **热身步数**:训练开始时,学习率从一个很小的值逐渐增加到初始学习率的阶段。热身有助于稳定早期训练,尤其是在学习率或批处理大小较大时,通过允许模型在进行大更新之前适应数据分布。
  • **打包**:批次具有预定义的序列长度。我们可以在一个批次中组合多个小样本,而不是为每个样本分配一个批次,从而提高效率。

我在 Google Colab 上使用 A100 GPU(40 GB VRAM)在整个数据集(10 万个样本)上训练了模型。训练耗时 4 小时 45 分钟。当然,您可以使用 VRAM 较小的 GPU 和较小的批次大小,但它们的速度远不及 A100。例如,在 L4 上大约需要 19 小时 40 分钟,在免费的 T4 上则需要惊人的 47 小时。

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

trainer=SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=dataset,
    dataset_text_field="text",
    max_seq_length=max_seq_length,
    dataset_num_proc=2,
    packing=True,
    args=TrainingArguments(
        learning_rate=3e-4,
        lr_scheduler_type="linear",
        per_device_train_batch_size=8,
        gradient_accumulation_steps=2,
        num_train_epochs=1,
        fp16=not is_bfloat16_supported(),
        bf16=is_bfloat16_supported(),
        logging_steps=1,
        optim="adamw_8bit",
        weight_decay=0.01,
        warmup_steps=10,
        output_dir="output",
        seed=0,
    ),
)

trainer.train()

模型训练完成后,我们用一个简单的提示来测试它。这并不是严格的评估,只是快速检查潜在问题。我们使用 `FastLanguageModel.for_inference()` 来获得 2 倍更快的推理速度。

model = FastLanguageModel.for_inference(model)

messages = [
    {"from": "human", "value": "Is 9.11 larger than 9.9?"},
]
inputs = tokenizer.apply_chat_template(
    messages,
    tokenize=True,
    add_generation_prompt=True,
    return_tensors="pt",
).to("cuda")

text_streamer = TextStreamer(tokenizer)
_ = model.generate(input_ids=inputs, streamer=text_streamer, max_new_tokens=128, use_cache=True)

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

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

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

model.save_pretrained_merged("model", tokenizer, save_method="merged_16bit")
model.push_to_hub_merged("mlabonne/FineLlama-3.1-8B", tokenizer, save_method="merged_16bit")

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

quant_methods = ["q2_k", "q3_k_m", "q4_k_m", "q5_k_m", "q6_k", "q8_0"]
for quant in quant_methods:
    model.push_to_hub_gguf("mlabonne/FineLlama-3.1-8B-GGUF", tokenizer, quant)

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

  • **评估**它在 Open LLM Leaderboard 上(您可以免费提交)或使用其他评估工具,例如 LLM AutoEval
  • 使用偏好数据集(如 mlabonne/orpo-dpo-mix-40k)通过直接偏好优化**对齐**它,以提升性能。
  • 使用 AutoQuant 将其**量化**为其他格式,如 EXL2、AWQ、GPTQ 或 HQQ,以实现更快的推理或更低的精度。
  • 在 Hugging Face Space 上使用 ZeroChat **部署**它,适用于已充分训练以遵循聊天模板的模型(约 2 万个样本)。

总结

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

希望本指南对您有所帮助。如果您对 LLM 感兴趣,建议查看 LLM 课程。如果您喜欢这篇文章,请在 X @maximelabonne 和 Hugging Face @mlabonne 上关注我。祝您微调模型顺利!

社区

您好,

我如何缩小模型大小?

谢谢!

请只在“Google Colab”中使用此功能!

!pip install --no-deps bitsandbytes accelerate xformers==0.0.29.post3 peft trl triton cut_cross_entropy unsloth_zoo
!pip install sentencepiece protobuf "datasets>=3.4.1" huggingface_hub hf_transfer
!pip install --no-deps unsloth

对于其他任何用途,请使用,

!pip install unsloth

注册登录 发表评论