作者:Ethan Zhang | 发布时间:2026-07-30 | 标签:
大模型微调LoRAHugging FacePyTorch
随着模型参数膨胀至数十亿甚至万亿,全量微调(Full Fine-tuning)需要巨大的 GPU 显存(如 LLaMA-2 7B 需至少 50GB+),且训练时间长、成本高昂。更重要的是,对于许多下游任务,我们并不需要更新全部参数。
参数高效微调(PEFT,Parameter-Efficient Fine-Tuning)方法应运而生,包括 Adapter、Prefix Tuning、LoRA 等。它们只训练少量额外参数,却能取得与全量微调媲美的效果,且显存占用大幅降低,使得在单卡(如 24GB 的 RTX 3090)上微调 7B 模型成为可能。
LoRA 的核心思想是:在微调过程中,权重更新矩阵 ΔW 是低秩的。对于预训练权重矩阵 W₀ ∈ ℝ^(d×k),其更新 ΔW 可分解为两个小矩阵的乘积:ΔW = B·A,其中 B ∈ ℝ^(d×r),A ∈ ℝ^(r×k),且秩 r << min(d, k)。
在前向传播时,h = W₀ x + ΔW x = W₀ x + B A x。训练时只优化 A 和 B,原权重 W₀ 被冻结。这样可训练参数量从 d×k 骤降至 r×(d+k),例如 d=4096, k=4096, r=8,参数量从 16M 降至约 65k,减少 99.6%。
LoRA 可以应用于 Transformer 中的注意力权重(如 Q、K、V、O),也可应用于 MLP 层,实践中常对 Q 和 V 进行适配。
我们使用 Python 3.10+,PyTorch 2.0+,以及 Hugging Face 库。bash
# 创建虚拟环境(可选)
conda create -n lora python=3.10
conda activate lora
# 安装 PyTorch(根据 CUDA 版本选择)
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
# 安装核心依赖
pip install transformers datasets accelerate peft bitsandbytes
pip install sentencepiece protobuf # 用于某些 tokenizer
pip install wandb # 可选,用于日志记录如果你的显卡显存不足,可以启用 4-bit 量化(QLoRA),本文也将展示如何结合
bitsandbytes实现。
我们以 中文情感分类 或 法律文本分类 为例。为简化,使用 Hugging Face 上的公开数据集 glue 的 sst2(英文)或 ChnSentiCorp(中文)。这里我们采用一个自定义的模拟数据集,演示二分类任务:输入一段文本,输出“正面”或“负面”。
实际应用中,你可以替换为自己的数据集。我们直接构造一个简单的 DataFrame 并转为 Dataset
import pandas as pd
from datasets import Dataset
# 模拟数据(真实场景请从文件读取)
data = {
"text": [
"这部电影太棒了,演员演技出色!",
"剧情无聊,浪费时间。",
"画面精美,值得一看。",
"烂片,完全看不下去。",
"非常感人,强烈推荐。",
"一般般,没什么亮点。"
],
"label": [1, 0, 1, 0, 1, 0] # 1 正面,0 负面
}
df = pd.DataFrame(data)
dataset = Dataset.from_pandas(df)
# 划分训练集和测试集
dataset = dataset.train_test_split(test_size=0.2, seed=42)
train_dataset = dataset["train"]
eval_dataset = dataset["test"]
print(f"训练集大小: {len(train_dataset)}, 测试集大小: {len(eval_dataset)}")对于生成式模型,我们通常使用模板将输入转化为自然语言指令。例如:
<|user|>
{text}
<|assistant|>
{label}这里我们使用简单的分类提示词,直接微调模型使其输出“正面”或“负面”。为了演示,采用 text 作为输入,label 作为输出,并转为文本标签。
def preprocess_function(examples, tokenizer, max_length=128):
# 将标签转为文字
label_map = {0: "负面", 1: "正面"}
inputs = [f"请判断以下评论的情感倾向:{text} 答案:" for text in examples["text"]]
targets = [label_map[l] for l in examples["label"]]
# 对输入进行 tokenize
model_inputs = tokenizer(inputs, max_length=max_length, truncation=True, padding=False)
# 对目标进行 tokenize,并添加 EOS token
with tokenizer.as_target_tokenizer():
labels = tokenizer(targets, max_length=8, truncation=True, padding=False)
model_inputs["labels"] = labels["input_ids"]
return model_inputs
# 测试预处理
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen-1_8B") # 后续会替换为基础模型
# 如果模型没有 pad_token,设置 eos_token 为 pad_token
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
tokenized_train = train_dataset.map(
lambda x: preprocess_function(x, tokenizer),
batched=True,
remove_columns=train_dataset.column_names
)
tokenized_eval = eval_dataset.map(
lambda x: preprocess_function(x, tokenizer),
batched=True,
remove_columns=eval_dataset.column_names
)
# 设置数据整理器(动态填充)
from transformers import DataCollatorForSeq2Seq
data_collator = DataCollatorForSeq2Seq(tokenizer, model=None, padding=True)我们选择 Qwen-1.8B 或 LLaMA-2-7B 作为基础模型。考虑到显存,1.8B 可在 16GB 显卡上微调。这里以 Qwen-1.8B 为例(中文支持好)。若需英文模型,可换用 meta-llama/Llama-2-7b-hf。
启用 4-bit 量化以节省显存:
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
model_name = "Qwen/Qwen-1_8B" # 或 "meta-llama/Llama-2-7b-hf"
# 配置 4-bit 量化
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16
)
# 加载模型
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map="auto", # 自动分配到 GPU
trust_remote_code=True # Qwen 需要
)
# 加载 tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
# 如果模型没有 pad_token,但需要为左填充设置
tokenizer.padding_side = "right" # 根据模型习惯
# 确保模型在训练模式
model.train()使用 peft 库中的 LoraConfig 和 get_peft_model。
from peft import LoraConfig, get_peft_model, TaskType
# LoRA 配置
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM, # 因果语言模型
r=8, # 秩
lora_alpha=32, # 缩放系数
target_modules=["q_proj", "v_proj"], # 对于 Qwen,通常模块名为 c_attn 等,需查看模型结构
lora_dropout=0.05,
bias="none",
inference_mode=False,
)
# 适配模型
model = get_peft_model(model, lora_config)
# 打印可训练参数
model.print_trainable_parameters()
# 输出:trainable params: 约 4.7M (对于 1.8B 模型)注意:不同模型的
target_modules不同。对于 Qwen,其注意力层为c_attn(合并了 QKV),需查阅模型源码。更通用的做法是使用["q_proj", "v_proj"](LLaMA 风格)。若报错,可改为["c_attn"]或直接指定所有线性层。实践中也可使用target_modules="all-linear"(较新 PEFT 支持),但会显著增加参数量。
使用 Hugging Face Trainer 进行训练。
from transformers import Trainer, TrainingArguments
import os
# 输出目录
output_dir = "./qwen-lora-classifier"
training_args = TrainingArguments(
output_dir=output_dir,
num_train_epochs=3,
per_device_train_batch_size=4,
per_device_eval_batch_size=4,
gradient_accumulation_steps=2,
evaluation_strategy="steps",
eval_steps=50,
save_steps=50,
logging_steps=10,
learning_rate=2e-4,
fp16=True,
warmup_ratio=0.03,
lr_scheduler_type="cosine",
save_total_limit=2,
load_best_model_at_end=True,
metric_for_best_model="eval_loss",
greater_is_better=False,
report_to="none", # 不启用 wandb
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_train,
eval_dataset=tokenized_eval,
data_collator=data_collator,
tokenizer=tokenizer,
)
# 开始训练
trainer.train()训练完成后,模型权重(仅 LoRA 参数)会保存在 output_dir 中。
保存 LoRA 适配器:
model.save_pretrained(output_dir)
tokenizer.save_pretrained(output_dir)加载时,先加载基础模型,再加载 LoRA 权重:
from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained(
model_name,
device_map="auto",
trust_remote_code=True,
)
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
lora_model = PeftModel.from_pretrained(base_model, output_dir)
lora_model.eval()编写推理函数,生成输出并提取答案。
def predict(text, model, tokenizer, max_new_tokens=8):
prompt = f"请判断以下评论的情感倾向:{text} 答案:"
inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=128).to(model.device)
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=max_new_tokens,
do_sample=False, # 贪婪解码
pad_token_id=tokenizer.eos_token_id
)
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
# 提取答案部分(去除 prompt)
answer = response[len(prompt):].strip()
return answer
# 测试
test_texts = [
"画面美轮美奂,故事引人入胜。",
"剧情拖沓,演员演技尴尬。"
]
for t in test_texts:
pred = predict(t, lora_model, tokenizer)
print(f"输入: {t}\n预测: {pred}\n")预期输出应为“正面”或“负面”。如果效果不佳,可增加训练数据量或调整超参数。
["q_proj", "v_proj"] 或 ["q_proj", "k_proj", "v_proj", "o_proj"]。增加模块可提升效果,但增加参数量。accelerate 或 DeepSpeed 支持多卡训练。本文详细介绍了使用 LoRA 在单卡上微调大语言模型的完整流程,涵盖环境搭建、数据预处理、量化加载、LoRA 配置、训练和推理。通过这一方法,开发者可以在消费级 GPU 上快速适配大模型到自己的任务,无需昂贵的硬件资源。
LoRA 不仅适用于分类任务,也适用于指令微调、对话生成、代码生成等场景。未来还可以探索 QLoRA(结合 4-bit 量化)进一步压缩显存,或使用 AdaLoRA 动态调整秩。随着大模型生态的不断完善,参数高效微调将成为每个 AI 工程师的必备技能。
希望本文能为你打开大模型微调的大门,欢迎在评论区交流实践心得!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。