自 2022 年 11 月发布以来,ChatGPT 引发了关于大型语言模型(LLM)和人工智能能力的广泛讨论。现在,很少有人没听说过 ChatGPT 或用它做过实验了。虽然像 GPT、Gemini 或 Claude 这样的工具都非常强大,拥有数千亿(甚至上万)个参数,并在大量文本库中进行了预训练,但它们并不是万能的。在一些特定的任务中,这些模型存在不足。但是,我们并非没有解决这些任务的办法。我们可以利用较小的开源模型来发挥 LLM 的威力,使其适应我们的特定问题。
本文旨在简要介绍几个较小的开源 LLM,并解释 LLM 微调的两个关键概念: 量化和 LoRA。此外,我们还将介绍几个最流行的微调库以及代码示例,以便你可以快速将这些概念应用到你的使用案例中。让我们深入了解微调。
小 "大语言模型
对 LLM 进行微调的成本可能高得令人望而却步,尤其是对于参数数量较多的模型。根据经验,100 亿参数以下的模型通常可以进行微调,而不会遇到重大的基础设施挑战。然而,对于像 Llama 3 70B 这样的大型模型,则需要大量资源。微调像 Llama 3 这样的 70B 参数模型需要大约 1.5 TB 的 GPU vRAM。从这个角度来看,这个数量的 vRAM 相当于一个由大约 20 个 Nvidia A100 组成的集群,每个集群拥有 80GB 的 vRAM。假设硬件可用,这样一个集群的成本约为 40 万美元。
另外,也可以使用 AWS、Azure 或 GCP 等云提供商,但这种方法的成本也很高。例如,在 AWS 上使用 Nvidia A100 GPU 一小时的成本为 40 美元。如果要在 20 个 GPU 上对 70B 模型进行 5 天的微调,则需要花费约 10 万美元。
由于这些成本,大多数从业者主要使用参数少于 100 亿的小型 LLM。这些模型的训练成本更低,只需要 16GB 到 24GB 的 vRAM(用于更大的批量和更快的训练)。例如,我在 AWS 上使用 Nvidia A10 对 Mistral 7B 进行了塞尔维亚语的微调,用时不到 10 个小时,花费不到 20 美元。
当然,如果不进行量化,特别是量化到 4 位,7B 模型仍然无法在这么多的 vRAM 上适应和训练。
量化
如果使用完整的 32 位参数,我们仍然需要大量的 vRAM(按照凡人的标准)来训练 LLM,大约需要 150GB 左右。
量化技术通过将模型参数转换为低精度数据类型(如 8 位或 4 位)提供了一种解决方案,从而大大减少了内存消耗并提高了执行速度。这个概念很简单:将所有可能的 32 位数值映射到更小范围的有限数值(例如,8 位转换为 256)。这一过程可以形象地理解为将高精度数值围绕几个固定点进行分组,这些固定点代表其附近的数值。
低库自适应 (LoRA)
LoRA 是一种通过矩阵降维来更新模型权重的技术。由于在 LLM 中广泛使用的变换器在很大程度上依赖于矩阵,因此这种技术尤为重要。
更新模型权重时,需要调整这些矩阵中的参数。从概念上讲,这种调整可以看作是在原始矩阵中添加一个权重更新矩阵: W' = W + ΔW。LoRA 引入了一种新方法,将这一更新矩阵分解为两个较小的矩阵,相乘后近似于更新矩阵。在微调过程中,LoRA 不需要先创建更新矩阵,然后再进行分解,而是直接创建这两个较小的矩阵进行相乘。
下图是普通微调与 LoRA 微调的对比示意图,改编自 Sebastian Raschka 的博文。
LoRA 的主要优势在于,虽然近似的精确度略低,但却能显著提高内存和计算效率。例如,假设一个矩阵有 1000x1000 个参数,总计 100 万个参数。通过使用 1000x100 乘以 100x1000 矩阵的分解版本(精确度稍低),参数数只需 2*100k,从而减少了 80% 的参数。
量化和 LoRA 经常结合使用,形成所谓的 QLoRA。
无缝
如果要重新开始 LLM 微调,我会选择 Unsloth Python 库。Unsloth 为 LLM 微调量身定制了各种优化,并支持各种流行的 LLM,包括 Mistral、Llama 3、Gemma 等。例如,他们的免费层为 Mistral 提供了 12 种不同的微调优化,显著提高了 2.2 倍的速度。
下面的代码片段演示了如何使用 Unsloth 库对 Llama 3 8B 进行微调。
导入 4 位模型:
model, tokenizer = FastLanguageModel.from_pretrained(
model_name = "unsloth/llama-3-8b-bnb-4bit","unsloth/llama-3-8b-bnb-4bit",
max_seq_length = max_seq_length,
dtype = dtype,
load_in_4bit = load_in_4bit,
# token = "hf_...", # use one if using gated models like meta-llama/Llama-2-7b-hf
)
设置 LoRA:
model = FastLanguageModel.get_peft_model(
model,
r = 16, # Choose any number > 0 ! Suggested 8, 16, 32, 64, 12816, # Choose any number > 0 ! Suggested 8, 16, 32, 64, 128
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj",],
lora_alpha = 16,
lora_dropout = 0, # Supports any, but = 0 is optimized
bias = "none", # Supports any, but = "none" is optimized
# [NEW] "unsloth" uses 30% less VRAM, fits 2x larger batch sizes!
use_gradient_checkpointing = "unsloth", # True or "unsloth" for very long context
random_state = 3407,
use_rslora = False, # We support rank stabilized LoRA
loftq_config = None, # And LoftQ
)
初始化Hugging Face 的监督微调训练器:
trainer = SFTTrainer(
model = model,
tokenizer = tokenizer,
train_dataset = dataset,
dataset_text_field = "text","text",
max_seq_length = max_seq_length,
dataset_num_proc = 2,
packing = False, # Can make training 5x faster for short sequences.
args = TrainingArguments(
per_device_train_batch_size = 2,
gradient_accumulation_steps = 4,
warmup_steps = 5,
max_steps = 60,
learning_rate = 2e-4,
fp16 = not torch.cuda.is_bf16_supported(),
bf16 = torch.cuda.is_bf16_supported(),
logging_steps = 1,
optim = "adamw_8bit",
weight_decay = 0.01,
lr_scheduler_type = "linear",
seed = 3407,
output_dir = "outputs",
),
)
训练模型
trainer_stats = trainer.train()
监督微调训练器 (SFT)
在对 LLM 进行预训练后,下一个关键步骤就是监督微调。这一过程对于开发能理解并生成连贯反应的模型至关重要,而不仅仅是完成句子。
Hugging Face 的 SFT(监督微调训练器)和 PEFT(参数高效微调)等工具,以及 Tim Dettmers 的 BitsAndBytes,大大简化了对模型应用 LoRA、量化和微调等技术的过程。这些库简化了高级优化方法的实施,使开发人员和研究人员都能更方便、更高效地使用这些方法。
下面,你会发现 Unsloth、SFT 和 ORPO 的代码非常相似。这种相似性源于这些库背后的基本思想大体相同,不同之处主要在于库本身,也可能在于某些超参数。
导入 4 位模型
# Hugging Face model id
model_id = "meta-llama/Meta-Llama-3-8B"
model_id = "mistralai/Mistral-7B-v0.1"
# BitsAndBytesConfig int-4 config
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16 if use_flash_attention2 else torch.float16
)
# Load model and tokenizer
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
use_cache=False,
device_map="auto",
token = os.environ["HF_TOKEN"], # if model is gated like llama or mistral
attn_implementation="flash_attention_2" if use_flash_attention2 else "sdpa"
)
model.config.pretraining_tp = 1
tokenizer = AutoTokenizer.from_pretrained(
model_id,
token = os.environ["HF_TOKEN"], # if model is gated like llama or mistral
)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
设置 LoRA:
# LoRA config based on QLoRA paper
peft_config = LoraConfig(
lora_alpha=16,
lora_dropout=0.1,
r=64,
bias="none",
task_type="CAUSAL_LM",
target_modules=[
"q_proj",
"k_proj",
"v_proj",
"o_proj",
"gate_proj",
"up_proj",
"down_proj",
]
)
# Prepare model for training
model = prepare_model_for_kbit_training(model)
初始化Hugging Face 的监督微调训练器:
args = TrainingArguments(
output_dir="mistral-int4-alpaca","mistral-int4-alpaca",
num_train_epochs=1,
per_device_train_batch_size=6 if use_flash_attention2 else 2, # you can play with the batch size depending on your hardware
gradient_accumulation_steps=4,
gradient_checkpointing=True,
optim="paged_adamw_8bit",
logging_steps=10,
save_strategy="epoch",
learning_rate=2e-4,
bf16=use_flash_attention2,
fp16=not use_flash_attention2,
tf32=use_flash_attention2,
max_grad_norm=0.3,
warmup_steps=5,
lr_scheduler_type="linear",
disable_tqdm=False,
report_to="none"
)
model = get_peft_model(model, peft_config)
trainer = SFTTrainer(
model=model,
train_dataset=dataset,
peft_config=peft_config,
max_seq_length=2048,2048,
tokenizer=tokenizer,
packing=True,
formatting_func=format_instruction,
args=args,
)
训练模型
trainer.train()
赔率偏好优化 (ORPO)
在本文中,我们重点介绍了 LLM 的预训练和监督微调。然而,所有 SOTA LLM 都要经历另一个关键步骤:偏好调整。这一步发生在预训练和微调之后,在这一步中,你要告知模型哪些生成的输出是可取的,哪些是不可取的。偏好调整的常用方法包括从人类反馈中强化学习(RLHF)和直接偏好优化(DPO)。
2024 年 3 月出现了一种名为 "赔率偏好优化"(ORPO)的新方法,它将监督微调和偏好调整结合在一起。
导入 4 位模型:
# Model
base_model = "meta-llama/Meta-Llama-3-8B"
new_model = "OrpoLlama-3-8B"
# QLoRA config
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch_dtype,
bnb_4bit_use_double_quant=True,
)
# Load tokenizer
tokenizer = AutoTokenizer.from_pretrained(base_model)
# Load model
model = AutoModelForCausalLM.from_pretrained(
base_model,
quantization_config=bnb_config,
device_map="auto",
attn_implementation=attn_implementation
)
设置 LoRA:
# LoRA config
peft_config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
target_modules=['up_proj', 'down_proj', 'gate_proj', 'k_proj', 'q_proj', 'v_proj', 'o_proj']
)
model = prepare_model_for_kbit_training(model)
初始化 Hugging Face 的 ORPO Trainer:
orpo_args = ORPOConfig(
learning_rate=8e-6,8e-6,
beta=0.1,
lr_scheduler_type="linear",
max_length=1024,
max_prompt_length=512,
per_device_train_batch_size=2,
per_device_eval_batch_size=2,
gradient_accumulation_steps=4,
optim="paged_adamw_8bit",
num_train_epochs=1,
evaluation_strategy="steps",
eval_steps=0.2,
logging_steps=1,
warmup_steps=10,
report_to="wandb",
output_dir="./results/",
)
trainer = ORPOTrainer(
model=model,
args=orpo_args,
train_dataset=dataset["train"],"train"],
eval_dataset=dataset["test"],
peft_config=peft_config,
tokenizer=tokenizer,
)
训练模型
trainer.train()
结论
虽然 GPT、Gemini 或 Claude 等大型语言模型 (LLM) 功能强大,但其庞大的规模和资源需求使它们在许多任务中都不实用。为了解决这个问题,可以使用量化和低级自适应(LoRA)等技术对较小的开源 LLM 进行微调和定制,以满足特定需求。这些技术降低了内存消耗,提高了计算效率,使训练模型的成本更低,尤其是参数少于 10B 的模型。
Unsloth、Supervised Finetuning Trainer(SFT)和Odds Ratio Preference Optimization(ORPO)等工具简化了微调过程,使其更易于使用。例如,Unsloth 提供的优化可以显著加快训练速度,而 ORPO 则将监督微调与偏好调整相结合,以提高模型性能。
通过利用这些技术和工具,开发人员和研究人员可以根据自己的特定需求定制LLM,而无需支付与训练大型模型相关的高昂成本。这种方法使高级语言模型的访问民主化,并支持跨不同领域的广泛应用。