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

Spectrum 是一种识别语言模型中最具信息量层的全新技术。基于此分析,您可以选择性地仅微调模型的一小部分,从而优化训练效率。
在本文中,我们将介绍 Spectrum,并演示如何通过微调 Phi-3.5-mini-instruct 来提升其在意大利语中的性能,我们将使用 Hugging Face TRL。最终模型为 💬🇮🇹 Phi-3.5-mini-ITA。
本文提供了一个完整的操作指南;如果只需要代码,请参阅训练笔记本。
🎯 Spectrum
直觉
在本文中,当我们提到“层”时,我们指的不是更高层次的 Transformer 层(model.layers.0
、model.layers.1
等)。 相反,我们指的是更低层次的层(model.layers.0.mlp.down_proj
、model.layers.0.self_attn.o_proj
等), 每一层都与特定的权重矩阵相关联。
最近,出现了几种高效微调语言模型的技术,节省了计算资源和时间。
一种非常流行的方法是 QLoRa,它对原始模型进行量化,并在其之上训练低秩适配器。这种方法取得了令人印象深刻的结果(略逊于完全微调),同时只使用了 GPU 资源的一小部分。
然而,QLoRa 将低秩适应均匀地应用于整个模型。
如果我们能识别出信息量最大的层并只对它们进行微调呢?
这正是 Spectrum 所做的!
- Spectrum 分析语言模型中所有层的权重矩阵,并计算每个层的信噪比 (SNR)。
- 它使用随机矩阵理论和 Marchenko-Pastur 分布来区分信号和噪声。
- 根据所选的百分比(例如 25%),Spectrum 会选择每种类型(例如,
mlp.down_proj
、self_attn.o_proj
等)中最具信息量的层。 - 然后,您可以冻结整个模型,除了这些选定的层,并专注于对它们进行微调。
评估和结果
在论文中,作者使用 Spectrum-50 和 Spectrum-25 对 airoboros-3.1 数据集上的 Llama-3-8B 和 Mistral-7B-v0.1 进行了微调,并将结果与完全微调和 QLoRA 进行了比较。
Spectrum 在基准测试性能方面与完全微调具有竞争力,并优于 QLoRA。
在单个 GPU 上,QLoRA 的内存效率更高,而 Spectrum 在分布式训练设置(DeepSpeed ZeRO-3 和 FSDP)中表现出色。
许多令人印象深刻的语言模型都使用了这项技术进行训练:各种 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 Colombo 和 Alessandro 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 Solutions 和 LLaMAntino-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 官方微调示例。
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 和注意力层,我们在这里也会这样做。
在 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=2
和gradient_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)
结果
模型的损失曲线看起来不错。
要体验该模型,您可以在此处试用: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 和 领英 上关注我。如果您发现任何错误或不准确之处,请随时与我联系。
主要参考文献
- Eric Hartford, Lucas Atkins, Fernando Fernandes Neto, David Golchinfar, Spectrum: 基于信噪比的定向训练, 2024。
- Tim Dettmers, Artidoro Pagnoni, Ari Holtzman, Luke Zettlemoyer, QLoRA: 量化 LLM 的高效微调, 2023。
- Marco Polignano, Pierpaolo Basile, Giovanni Semeraro, 高级基于自然语言的意大利语交互:LLaMAntino-3-ANITA, 2024。
- Marah Abdin, Sam Ade Jacobs, Ammar Ahmad Awanm, Jyoti Aneja et al., Phi-3 技术报告:一款可在您手机上本地运行的高性能语言模型