趋近智
QLoRA(量化低秩适应)结合了低秩适应策略与模型量化,进一步提升了参数效率。具体来说,它在一个权重已量化(通常为4位精度)的基础模型上微调LoRA适配器。这大幅减少了微调所需的内存占用,使得在消费级硬件上微调大得多模型成为可能。
本实践练习将引导您完成使用QLoRA微调大型语言模型。我们将加载一个4位精度的预训练模型,配置LoRA适配器,准备数据集,并使用Hugging Face的transformers和peft库执行微调过程。目标是在有效管理内存限制的同时,实现特定任务的适应。
您应熟悉Python、PyTorch以及Hugging Face生态系统的基本组件(transformers、datasets)。请确保您已安装必要的库,特别是处理量化的bitsandbytes。
首先,让我们安装所需的库。QLoRA高度依赖bitsandbytes进行量化,peft用于LoRA的实现,accelerate用于方便的设备部署和分布式训练工具,以及transformers和datasets用于模型/数据处理。
pip install -q transformers datasets accelerate peft bitsandbytes trl
transformers:提供对预训练模型和Trainer API的访问。datasets:便于数据集的加载和处理。accelerate:简化PyTorch代码在各种硬件配置(CPU、GPU、多GPU)上的运行。peft:参数高效微调库,包含LoRA、QLoRA等的实现。bitsandbytes:对QLoRA很重要,支持4位量化和高效矩阵运算。trl:提供如SFTTrainer等工具,简化监督微调过程。QLoRA的主要思想是以量化格式(通常是4位)加载基础模型。我们使用transformers库中的BitsAndBytesConfig来指定加载模型时的量化参数。
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
# 指定预训练模型名称
model_name = "NousResearch/Llama-2-7b-chat-hf" # 示例:Llama-2 7B 对话模型
# 使用BitsAndBytesConfig配置量化
quantization_config = BitsAndBytesConfig(
load_in_4bit=True, # 启用4位量化
bnb_4bit_quant_type="nf4", # 使用NF4(正态浮点4)量化类型
bnb_4bit_compute_dtype=torch.bfloat16, # 将计算数据类型设置为bfloat16以提高速度
bnb_4bit_use_double_quant=True, # 启用嵌套量化以节省更多内存
)
# 使用量化配置加载模型
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=quantization_config,
device_map="auto", # 自动将模型层分布到可用的GPU/CPU上
trust_remote_code=True # 信任来自模型中心的模型代码执行(请谨慎使用)
)
# 加载与模型关联的分词器
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
# 设置填充标记(如果尚未设置,这是训练的常见要求)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right" # 通常对因果语言模型进行右侧填充
print(f"Model loaded: {model_name}")
print(f"Model memory footprint: {model.get_memory_footprint() / 1e9:.2f} GB")
BitsAndBytesConfig中的重要参数:
load_in_4bit=True:此标志激活4位加载。bnb_4bit_quant_type="nf4":指定量化数据类型。“nf4”(正态浮点4)通常推荐用于良好性能。另一个选项是“fp4”。bnb_4bit_compute_dtype=torch.bfloat16:虽然权重以4位存储,但计算(例如前向传播期间的矩阵乘法)通常以更高精度格式进行,例如bfloat16(或float16),以提高稳定性和速度。如果您的硬件支持bfloat16(如Ampere GPU及更新版本),通常优先选用它。bnb_4bit_use_double_quant=True:启用一种嵌套量化技术,其中量化常数也被量化,从而节省更多内存。device_map="auto"参数能智能地将模型层分布到可用的GPU和CPU内存中,使得加载可能无法完全放入单个GPU的模型成为可能。
对于本示例,我们使用databricks-dolly-15k数据集的一个子集,该数据集包含指令遵循示例。我们会将其格式化为适合微调聊天或指令遵循模型的提示模板。
from datasets import load_dataset
# 加载数据集的一个子集
dataset_name = "databricks/databricks-dolly-15k"
dataset = load_dataset(dataset_name, split="train[:500]") # 为演示目的使用一小部分
# 定义一个函数来格式化提示
def format_prompt(example):
# 简单的指令遵循格式
instruction = example.get("instruction", "")
context = example.get("context", "")
response = example.get("response", "")
if context:
prompt = f"""以下是描述任务的指令,以及提供更多上下文的输入。请编写一个适当完成请求的响应。
### 指令:
{instruction}
### 输入:
{context}
### 响应:
{response}"""
else:
prompt = f"""以下是描述任务的指令。请编写一个适当完成请求的响应。
### 指令:
{instruction}
### 响应:
{response}"""
# 我们需要将此作为名为“text”的列返回,供SFTTrainer使用
return {"text": prompt}
# 应用格式化函数
formatted_dataset = dataset.map(format_prompt)
print("示例格式化提示:")
print(formatted_dataset[0]['text'])
这种格式化会创建一个单一的文本字段,其中包含指令、上下文(如果可用)以及期望的响应,并用清晰的标记分隔。在监督微调期间,模型将基于这段组合文本进行训练。
现在,我们使用peft库中的LoraConfig来配置LoRA参数。我们指定要适应的层、低秩矩阵的秩r、缩放因子alpha以及其他超参数。
# 为k位训练准备模型(梯度检查点,层归一化缩放)
model = prepare_model_for_kbit_training(model)
# 配置LoRA
lora_config = LoraConfig(
r=16, # 更新矩阵的秩(值越高=参数越多)
lora_alpha=32, # LoRA缩放因子(alpha/r控制幅度)
target_modules=["q_proj", "v_proj", "k_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], # 应用LoRA的模块(特定于Llama-2架构)
lora_dropout=0.05, # LoRA层的Dropout概率
bias="none", # 不训练偏置参数
task_type="CAUSAL_LM" # 指定任务类型
)
# 将LoRA配置应用到量化模型
model = get_peft_model(model, lora_config)
# 打印可训练参数的百分比
model.print_trainable_parameters()
prepare_model_for_kbit_training(model):此辅助函数对模型进行必要修改,以实现稳定的k位训练,例如启用梯度检查点(通过在反向传播期间重新计算激活而不是存储它们来节省内存)并确保层归一化层兼容。r=16:秩的常见起始点。值越高会增加可训练参数的数量和潜在的表达能力,但也会增加计算成本。lora_alpha=32:通常设置为秩r的两倍,但可以调整。它用于缩放学习到的低秩更新。target_modules:这很重要。它指定了Transformer架构中注入LoRA矩阵的线性层的名称。这些名称取决于特定的模型架构(例如,Llama类模型的q_proj、v_proj)。您可能需要查看模型结构(print(model))以确定不同模型的正确模块名称。通常会目标设定注意力投影层(q_proj、k_proj、v_proj、o_proj)和前馈层(gate_proj、up_proj、down_proj)。lora_dropout:应用于LoRA权重的正则化。bias="none":通常,在LoRA设置中不训练偏置项。task_type="CAUSAL_LM":指定任务类型,确保模型架构得到正确处理(例如,用于顺序生成文本)。print_trainable_parameters()方法突出了PEFT/QLoRA的核心优势。它会显示总参数中只有极小一部分(通常不到1%)实际在训练。
示例对比显示了在7B参数模型上使用QLoRA时,可训练参数的显著减少。具体数字取决于基础模型和LoRA配置(
r、target_modules)。
我们使用transformers库中的TrainingArguments类来定义训练超参数,以及trl库中的SFTTrainer,它专门为指令遵循等监督微调任务设计。SFTTrainer通过内部处理数据格式化和打包来简化过程。
import transformers
from trl import SFTTrainer
# 配置训练参数
training_args = transformers.TrainingArguments(
output_dir="./qlora_finetuned_model", # 保存检查点和日志的目录
per_device_train_batch_size=4, # 每个GPU的批处理大小
gradient_accumulation_steps=4, # 积累4步梯度(有效批处理大小 = 4 * 4 = 16)
learning_rate=2e-4, # 学习率
num_train_epochs=1, # 训练轮次(根据数据集大小调整)
logging_steps=20, # 每20步记录训练指标
save_steps=50, # 每50步保存检查点
fp16=True, # 启用混合精度训练(如果支持,可使用bf16=True)
optim="paged_adamw_8bit", # 使用分页AdamW优化器以提高内存效率
lr_scheduler_type="cosine", # 学习率调度器类型
warmup_ratio=0.03, # 学习率调度器的预热比例
report_to="none", # 在此示例中禁用向Weights & Biases等服务报告
# SFTTrainer特定参数
max_seq_length=1024, # 打包的最大序列长度
dataset_text_field="text", # 数据集中包含格式化文本的列名
)
# 初始化SFTTrainer
trainer = SFTTrainer(
model=model, # 经过PEFT封装的量化模型
train_dataset=formatted_dataset, # 格式化后的训练数据集
args=training_args, # 训练参数
peft_config=lora_config, # LoRA配置
tokenizer=tokenizer, # 分词器
# 可选:您可以添加packing=True以提高效率,但这需要仔细处理序列长度
)
重要参数:
per_device_train_batch_size 和 gradient_accumulation_steps:这些控制有效批处理大小。由于大型模型存在内存限制,会使用较小的每设备批处理大小,并通过多步梯度累积来模拟更大的批处理。learning_rate:与完整微调相比,PEFT通常使用相对较高的学习率(例如1e-4到3e-4)。fp16=True(或bf16=True):启用混合精度训练,减少内存使用并加快计算速度。如果您的硬件(例如Ampere)支持bf16,请使用它,因为它通常对LLM训练更稳定。optim="paged_adamw_8bit":QLoRA从bitsandbytes提供的分页优化器中受益匪浅。这些优化器将优化器状态卸载到CPU内存,进一步减少GPU内存使用。max_seq_length:SFTTrainer特有,定义了分词后序列的最大长度。更长的序列需要更多内存。dataset_text_field="text":告知SFTTrainer数据集中哪一列包含用于训练的文本。现在,我们可以开始训练过程。
print("开始QLoRA微调...")
trainer.train()
print("训练完成。")
训练期间,请监控您的GPU内存使用情况。QLoRA应该使其显著低于完整微调。transformers的Trainer将输出日志,显示训练损失、学习率和轮次进度。持续时间将取决于数据集大小、硬件和训练配置。
训练完成后,我们保存训练好的适配器权重。请注意,我们只保存一小部分LoRA参数,而不是整个基础模型。
# 定义保存适配器权重的路径
adapter_output_dir = "./qlora_adapter_weights"
# 保存LoRA适配器权重
trainer.save_model(adapter_output_dir)
# 或者:model.save_pretrained(adapter_output_dir)
print(f"QLoRA适配器权重已保存到:{adapter_output_dir}")
这会创建一个包含adapter_model.bin文件和adapter_config.json的目录。这通常只有几兆字节或几十兆字节大小,这表明了PEFT方法的存储效率。
为了使用微调后的模型进行推断,我们首先再次加载原始的量化基础模型,然后使用PeftModel在其上应用已保存的适配器权重。
from peft import PeftModel
import time
# 重新加载基础量化模型(如果尚未在内存中)
# 确保使用与训练时相同的量化配置
base_model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=quantization_config,
device_map="auto",
trust_remote_code=True
)
# 通过将适配器权重合并到基础模型来加载PEFT模型
model_tuned = PeftModel.from_pretrained(base_model, adapter_output_dir)
model_tuned = model_tuned.eval() # 将模型设置为评估模式
# --- 推断示例 ---
# 准备一个示例提示(使用与训练相同的格式,但不包含响应)
instruction = "What is the difference between LoRA and QLoRA?"
prompt_template = f"""以下是描述任务的指令。请编写一个适当完成请求的响应。
### 指令:
{instruction}
### 响应:
"""
# 对输入提示进行分词
inputs = tokenizer(prompt_template, return_tensors="pt").to(model_tuned.device)
print("\n--- 正在生成响应 ---")
start_time = time.time()
# 使用微调模型生成文本
with torch.no_grad(): # 推断时禁用梯度计算
outputs = model_tuned.generate(
**inputs,
max_new_tokens=200, # 生成的新标记的最大数量
do_sample=True, # 启用采样
temperature=0.7, # 控制随机性(值越低 = 越确定)
top_k=50, # 采样时考虑前k个标记
top_p=0.95, # 使用核采样(累积概率截止)
eos_token_id=tokenizer.eos_token_id # 遇到EOS标记时停止生成
)
end_time = time.time()
# 解码生成的标记
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(response)
print(f"\n生成时间:{end_time - start_time:.2f} 秒")
这演示了如何将轻量级适配器加载到量化基础模型上进行推断。生成过程使用标准的transformers生成方法。将输出质量与基础模型对相同提示的输出进行比较,以评估微调的效果。
本实践演练演示了使用QLoRA微调LLM的核心步骤:
BitsAndBytesConfig)。LoraConfig)并将其应用于量化模型(get_peft_model)。SFTTrainer以及适当的TrainingArguments(包括分页优化器和混合精度)进行内存高效训练。QLoRA大幅降低了微调大型模型的硬件门槛,通过大幅减少训练期间模型权重和优化器状态的内存需求,使在更易获得的GPU设置上适应成为可能。这使其成为一种为特定下游任务定制大型语言模型的强大且实用的技术。
简洁的语法。内置调试功能。从第一天起就可投入生产。
为 ApX 背后的 AI 系统而构建
这部分内容有帮助吗?
© 2026 ApX Machine Learning用心打造