使用 Spectrum 对语言模型进行选择性微调

社区文章 发布于 2024 年 9 月 3 日

Spectrum 是一种识别语言模型中最具信息量层的全新技术。基于此分析,您可以选择性地仅微调模型的一小部分,从而优化训练效率。

在本文中,我们将介绍 Spectrum,并演示如何通过微调 Phi-3.5-mini-instruct 来提升其在意大利语中的性能,我们将使用 Hugging Face TRL。最终模型为 💬🇮🇹 Phi-3.5-mini-ITA

本文提供了一个完整的操作指南;如果只需要代码,请参阅训练笔记本

🎯 Spectrum

直觉

在本文中,当我们提到“层”时,我们指的不是更高层次的 Transformer 层(model.layers.0model.layers.1 等)。 相反,我们指的是更低层次的层(model.layers.0.mlp.down_projmodel.layers.0.self_attn.o_proj 等), 每一层都与特定的权重矩阵相关联。

最近,出现了几种高效微调语言模型的技术,节省了计算资源和时间。

一种非常流行的方法是 QLoRa,它对原始模型进行量化,并在其之上训练低秩适配器。这种方法取得了令人印象深刻的结果(略逊于完全微调),同时只使用了 GPU 资源的一小部分。

然而,QLoRa 将低秩适应均匀地应用于整个模型。

如果我们能识别出信息量最大的层并只对它们进行微调呢?

这正是 Spectrum 所做的!

  • Spectrum 分析语言模型中所有层的权重矩阵,并计算每个层的信噪比 (SNR)。
  • 它使用随机矩阵理论和 Marchenko-Pastur 分布来区分信号和噪声。
  • 根据所选的百分比(例如 25%),Spectrum 会选择每种类型(例如,mlp.down_projself_attn.o_proj 等)中最具信息量的层。
  • 然后,您可以冻结整个模型,除了这些选定的层,并专注于对它们进行微调。

image/png

评估和结果

在论文中,作者使用 Spectrum-50 和 Spectrum-25 对 airoboros-3.1 数据集上的 Llama-3-8B 和 Mistral-7B-v0.1 进行了微调,并将结果与完全微调和 QLoRA 进行了比较。

Spectrum 在基准测试性能方面与完全微调具有竞争力,并优于 QLoRA。

在单个 GPU 上,QLoRA 的内存效率更高,而 Spectrum 在分布式训练设置(DeepSpeed ZeRO-3 和 FSDP)中表现出色。

image/png

许多令人印象深刻的语言模型都使用了这项技术进行训练:各种 Dolphin 模型Llama 3.1 Storm、以及 VAGO Solutions 的许多模型等等。

🇮🇹 使用 Spectrum 和 TRL 微调 Phi 3.5 mini

用例

让我们将 Spectrum 应用于一个具体的用例:提高 Phi-3.5-mini-instruct 的意大利语性能。这是一个很好的小型语言模型(3.82 亿参数),并且它在意大利语方面表现已经不错。

为了评估其意大利语能力,我们参考 Open ITA LLM Leadearboard,这是一个由 Samuele ColomboAlessandro Ercolani 维护的社区驱动项目。该排行榜使用 lm-evaluation-harness 框架,根据三个基准评估模型:MMLU_IT、ARC_IT 和 HELLASWAG_IT。

我们将使用 Spectrum 选择信息最丰富的层,然后使用 Hugging Face TRL 库训练它们。Spectrum 与 Aloxotl 开箱即用兼容,但手动使用 TRL 进行层选择是一个很好的学习体验。此外,TRL 是一个很棒的项目。

对于这个实验,我将使用一块 NVIDIA A6000 GPU(48GB 显存),但您可以通过调整梯度累积来适应更小的 GPU。

设置

首先,我们来安装必要的库。

pip install datasets transformers trl accelerate scipy

为了加快训练速度,我们还将安装 Flash Attention,它与现代 GPU 兼容。

pip install ninja packaging
MAX_JOBS=6 pip install flash-attn --no-build-isolation --upgrade

数据准备

为了提升模型在非英语语言上的性能,在训练数据中同时包含英语和目标语言会很有益。来自 VAGO SolutionsLLaMAntino-3 的模型已经证明了这一点。

我们将使用混合了优质英语和意大利语指令/聊天数据:mlabonne/FineTome-100k + efederici/capybara-claude-15k-ita

步骤

  • 将数据集转换为通用格式。
  • 应用 Phi 3.5 mini 聊天模板。
  • 创建一个统一的数据集,并保留一小部分用于评估。
from datasets import load_dataset, Dataset, concatenate_datasets
from transformers import AutoTokenizer
import multiprocessing

# Load and process FineTome dataset
finetome_ds = load_dataset("mlabonne/FineTome-100k")["train"]
mapping_keys, mapping_values = {"from": "role", "value": "content"}, {"human": "user", "gpt": "assistant"}

def process_conversation(row):
    conv = row["conversations"]
    new_conv = [{mapping_keys[k]: mapping_values.get(v, v) for k, v in msg.items()} for msg in conv]
    return {"conversations": new_conv}

finetome_ds = Dataset.from_list([process_conversation(row) for row in finetome_ds])

# Load tokenizer and define template function
tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3.5-mini-instruct", trust_remote_code=True)

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

finetome_ds = finetome_ds.map(apply_template, batched=True).remove_columns("conversations").shuffle(seed=42)
finetome_ds = finetome_ds.add_column("origin", ["finetome"] * len(finetome_ds))

# Load and process Capybara Claude dataset
capyclaude_ds = load_dataset("efederici/capybara-claude-15k-ita", split="train")
capyclaude_ds = capyclaude_ds.map(apply_template, batched=True).remove_columns(["conversations", "hash"]).shuffle(seed=42)
capyclaude_ds = capyclaude_ds.add_column("origin", ["capyclaude"] * len(capyclaude_ds))

# Concatenate and split datasets
mixed_ds = concatenate_datasets([finetome_ds, capyclaude_ds]).shuffle(seed=42)
mixed_ds = mixed_ds.class_encode_column("origin").train_test_split(test_size=0.005, stratify_by_column="origin")

然后我们可以检查一个例子,看看它是怎样的。

# mixed_ds["train"][587]

{'text': '<|system|>\nYou are a helpful assistant, with no access to external functions.<|end|>\n<|user|>\nEdit the following sentence to make the tense of the verb consistent.\nHe had gone to the store yesterday evening.<|end|>\n<|assistant|>\nHe went to the store yesterday evening.<|end|>...|endoftext|>',
 'origin': 1}

最大序列长度

稍后,我们需要设置一个 `max_seq_length` 值,它表示训练期间要考虑的最大序列长度。较长的示例将被截断。

明智地选择这个值很重要,这样我们既不会截断太多相关信息,也不会浪费 GPU 资源。

让我们看看如果我们将 max_seq_length 设置为 2048 会发生什么。

from scipy.stats import percentileofscore
import multiprocessing

def calculate_lengths(batch):
    return {"conv_lengths": [len(tokenizer(text)["input_ids"]) for text in batch["text"]]}

conv_lengths = mixed_ds["train"].map(
    calculate_lengths,
    batched=True,
    batch_size=1000,
    num_proc=multiprocessing.cpu_count()
)["conv_lengths"]

chosen_length=2048

percentile = percentileofscore(conv_lengths, chosen_length)
print(percentile)
# 91.91453560724239

通过选择 2048 的最大长度,我们将仅截断 8% 的样本。很好!

加载原始模型

接下来,我们加载要训练的原始模型。

from transformers import AutoModelForCausalLM
import torch

model = AutoModelForCausalLM.from_pretrained(
    "microsoft/Phi-3.5-mini-instruct",
    use_cache=False,
    torch_dtype=torch.bfloat16,
    attn_implementation="flash_attention_2",
    device_map="auto",
    trust_remote_code=True
)
tokenizer.pad_token = tokenizer.unk_token
tokenizer.pad_token_id = tokenizer.convert_tokens_to_ids(tokenizer.pad_token)
tokenizer.padding_side = 'right'

此代码改编自 Phi-3.5-mini-instruct 官方微调示例

  • `use_cache` 设置为 `False`:缓存在推理时有用,但在训练期间会浪费内存(资源:#1#2#3)。

  • trust_remote_code 设置为 True:对于 transformers==4.44.2,这对于整合 Phi3ForCausalLM 中的一个小错误修复是必需的。有关更多详细信息,请阅读此讨论

  • 在训练期间,`pad_token` 被设置为 `unk` 而不是 `eos` 标记,以防止无休止的生成。此更改必须在训练后恢复。

  • 在训练时,`tokenizer.padding_side` 设置为 `right`(TRL `SFTTrainer` 要求)。此更改必须在训练后恢复:对于生成,`tokenizer.padding_side` 必须设置为 `left`。

确定要使用 Spectrum 训练的层

现在,让我们确定要使用 Spectrum 训练的层。

由于官方的 Spectrum 脚本无法在笔记本环境中运行,您需要在 shell 中运行它。

首先,我们安装 Spectrum。

git clone https://github.com/cognitivecomputations/spectrum.git
cd spectrum
pip install -r requirements.txt

然后我们启动脚本。

python spectrum.py --model-name <insert local or HF repo here> --top-percent <top % of snr ratios to target>

如果有人已经扫描了我们的模型并将结果上传到 Spectrum 仓库,您很幸运,您可以立即获得一个包含要训练参数的 YAML 文件。

否则,就像我们的实验一样,我们需要自己扫描模型。对于我们的实验,我们目标是模型中排名前 30% 的层。

python spectrum.py --model-name microsoft/Phi-3.5-mini-instruct --top-percent 30

我们将被要求提供扫描的批处理大小(默认为 1)。

然后我们将被问及要扫描的层类型。作者建议至少选择 MLP 和注意力层,我们在这里也会这样做。

image/png

在 A6000 GPU 上,批处理大小为 1 的情况下,我们的模型(3.82 亿参数)的计算时间不到 2 分钟。

我们最终得到了一个 YAML 文件,其中列出了信息量最大的前 30% 的层。

unfrozen_parameters:
- ^lm_head.weight$
- ^model.embed_tokens.weight$
# mlp.down_proj layers
- model.layers.2.mlp.down_proj
- model.layers.3.mlp.down_proj
...
# mlp.gate_up_proj layers
- model.layers.31.mlp.gate_up_proj
- model.layers.4.mlp.gate_up_proj
...
# self_attn.o_proj layers
- model.layers.0.self_attn.o_proj
- model.layers.1.self_attn.o_proj
...
# self_attn.qkv_proj layers
- model.layers.23.self_attn.qkv_proj
- model.layers.24.self_attn.qkv_proj
...

此 YAML 文件可直接用于 Aloxotl。

使用 TRL,我们需要再进行一些手动步骤。

我们加载 YAML 文件,定义一个简单的 `freeze_and_unfreeze_parameters` 实用函数并将其应用于我们的模型。

我们正在冻结所有模型参数,并解冻 Spectrum 选择的那些参数。

import re

with open("snr_results_microsoft-Phi-3.5-mini-instruct_unfrozenparameters_30percent.yaml", "r") as fin:
    yaml_parameters = fin.read()

unfrozen_parameters = []
for line in yaml_parameters.splitlines():
  if line.startswith("- "):
    unfrozen_parameters.append(line.split("- ")[1])

def freeze_and_unfreeze_parameters(model, unfrozen_parameters):
    # freeze all parameters
    for param in model.parameters():
        param.requires_grad = False
    # unfreeze Spectrum parameters
    for name, param in model.named_parameters():
        if any(re.match(unfrozen_param, name) for unfrozen_param in unfrozen_parameters):
            param.requires_grad = True

freeze_and_unfreeze_parameters(model, unfrozen_parameters)

# let's do a quick sanity check
for name, param in model.named_parameters():
    if param.requires_grad:
      print(name, param.requires_grad)

# model.embed_tokens.weight True
# model.layers.0.self_attn.o_proj.weight True
# model.layers.1.self_attn.o_proj.weight True
# model.layers.1.mlp.down_proj.weight True
# ...

一切看起来都很好,我们几乎可以开始训练我们的模型了。

配置 TRL SFTTrainer 并进行训练!

为了执行监督微调,TRL 提供了 SFTTrainer。让我们配置它。

from trl import SFTConfig, SFTTrainer

new_model_id="anakin87/Phi-3.5-mini-ITA"

cfg = SFTConfig(
    output_dir='./mymodel',
    overwrite_output_dir = True,
    hub_model_id=new_model_id,
    hub_strategy="every_save",
    save_strategy="steps",
    save_steps=500,
    save_total_limit=1,
    push_to_hub=True,
    logging_steps=20,
    max_seq_length=2048,
    dataset_text_field="text",
    remove_unused_columns=True,
    packing=True,    
    num_train_epochs=2,
    lr_scheduler_type="cosine",
    warmup_ratio=0.2,                       
    bf16=True,                              
    tf32=True,                              
    learning_rate=5.0e-06,
    per_device_train_batch_size=8,
)

sft_trainer = SFTTrainer(
    model=model,
    args=cfg,
    train_dataset=mixed_ds["train"],
    tokenizer=tokenizer
)

以下是关键配置的快速概览:

  • max_seq_length=2048:前面已解释。
  • dataset_text_field="text":我们准备好的数据集中文本字段的名称。
  • packing=True:这启用了示例打包,其中多个短示例被打包到相同的输入序列中,以提高训练效率。
  • learning_rate=5.0e-06:这低于指令微调的通常学习率。该值取自 Phi-3.5-mini-instruct 官方微调示例。这可能与该模型已经过微调的事实有关。我个人发现,较高的学习率(如 2e-5)可能会导致该模型的性能下降。
  • per_device_train_batch_size=8:此设置旨在充分利用我们的 A6000 GPU 的 48GB 显存。如果您使用的是较小的 GPU,请考虑使用梯度累积来减少计算负载。例如,您可以将 per_device_train_batch_size=2gradient_accumulation_steps=4 设置为以更少的 GPU 使用量实现类似的结果。

现在,让我们启动训练过程

sft_trainer.train()

正如我们之前提到的,一些分词器配置需要在训练后恢复

tokenizer.pad_token = tokenizer.eos_token
tokenizer.pad_token_id = tokenizer.convert_tokens_to_ids(tokenizer.eos_token)
tokenizer.padding_side = 'left'

tokenizer.push_to_hub(new_model_id)

结果

模型的损失曲线看起来不错。

image/png

要体验该模型,您可以在此处试用:https://huggingface.co/spaces/anakin87/Phi-3.5-mini-ITA。虽然我们的微调侧重于提高意大利语性能,但该模型是多语言的,也可以处理英语。

官方基准测试结果可在 Open ITA LLM 排行榜上找到。

模型 参数量 平均分 MMLU_IT ARC_IT HELLASWAG_IT
anakin87/Phi-3.5-mini-ITA 3.82 B 57.67 59.93 51.5 61.57
meta-llama/Meta-Llama-3.1-8B-Instruct 8.03 B 56.97 58.43 48.42 64.07
microsoft/Phi-3.5-mini-instruct 3.82 B 56.82 60.03 49.19 61.25

简而言之,我们模型的意大利语性能得到了提升,所以我们可以认为这次实验是成功的!🎉

训练在单个 A6000 GPU 上耗时约 14 小时。

根据我所做的其他实验,我发现仅进行一次训练迭代(而非两次)以及使用 Spectrum 选择排名前 25% 的层(而非 30%)时,也得到了相似的结果。

结论

本文概述了 Spectrum,一种用于选择语言模型中最具信息量层的技术。通过 Spectrum 识别的参数可用于选择性微调,从而实现更高效的训练,与完全微调相比,所需时间更少,资源消耗更低。

随后,我们演示了一个实际用例,即使用 Spectrum 和 TRL 对 Phi-3.5-mini-instruct 进行微调,并混合了英语和意大利语数据。由此产生的模型 Phi-3.5-mini-ITA 在意大利语方面显示出改进的性能。

如果您喜欢这篇文章,欢迎在 Hugging Face领英 上关注我。如果您发现任何错误或不准确之处,请随时与我联系。

主要参考文献

社区

注册登录 以发表评论