小模型 LoRA 微调入门:用 Hugging Face Transformers + PEFT 在单卡上完成第一次微调
适合人群:会一点 Python,想亲手体验“微调大模型”是怎么回事,但手上只有一张消费级显卡或免费云端 GPU 的同学。
一、背景
最近 X 上数据科学领域的知名账号 @KirkDBorne 多次分享了一本面向初学者的小语言模型构建与微调指南,引发不少讨论,核心观点是:小模型(几亿到几十亿参数)在特定任务上经过微调,往往能以很低的成本达到可用效果,不需要昂贵的硬件。
“微调”听起来门槛很高,但借助 Hugging Face 的开源工具链,完成一次最小规模的实验其实只需要几十行代码。本文用 LoRA(低秩适配)方法,带你走通从准备数据到加载微调结果的全过程。
先说明预期:本文目标是理解流程,示例数据量很小,训练出的模型不会有惊人的能力提升。
二、先理解几个概念
1. 全量微调 vs LoRA
全量微调会更新模型的所有参数,显存需求大。LoRA 的做法是冻结原模型,只在部分层旁边插入很小的可训练矩阵。可训练参数通常只占原模型的百分之一甚至更低,因此显存占用和训练时间都大幅下降。
2. 为什么选小模型
参数量在 0.5B~1.5B 左右的指令模型,在 8GB~16GB 显存的显卡上就能做 LoRA 实验,适合入门。
3. 微调能做什么、不能做什么
微调擅长让模型学会固定的输出风格、格式和特定任务的模式;如果你的目标是让模型“知道”大量新知识,检索增强(RAG)往往是更合适的方案。
三、准备工作
- Python 3.10 或更新版本;
- 一张支持 CUDA 的 NVIDIA 显卡(或使用提供免费 GPU 的在线笔记本环境);
- 安装依赖:
pip install torch transformers datasets peft accelerate
PyTorch 请按照官方安装说明选择与你的 CUDA 版本匹配的命令。
四、操作步骤
步骤 1:准备训练数据
假设我们希望模型把一句口语化的需求改写成规范的待办事项。新建 data.jsonl,每行一条样本:
{"instruction": "明天记得给老王打电话说合同的事", "output": "【待办】联系王先生确认合同事宜|截止:明天"}
{"instruction": "周五前把报销单交了", "output": "【待办】提交报销单|截止:周五"}
实际练习时建议准备至少几十到几百条格式一致的样本。数据质量比数量更重要:输出格式必须统一,否则模型学到的就是混乱。
步骤 2:加载模型和分词器
from transformers import AutoTokenizer, AutoModelForCausalLM
model_name = "Qwen/Qwen2.5-0.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype="auto")
这里选择一个 0.5B 的开源指令模型作为示例,你也可以换成其他小模型,注意阅读其许可协议。
步骤 3:把数据处理成训练格式
我们使用模型自带的对话模板拼接输入和输出:
from datasets import load_dataset
ds = load_dataset("json", data_files="data.jsonl")["train"]
def to_text(ex):
msgs = [
{"role": "user", "content": ex["instruction"]},
{"role": "assistant", "content": ex["output"]},
]
text = tokenizer.apply_chat_template(msgs, tokenize=False)
tokens = tokenizer(text, truncation=True, max_length=256)
tokens["labels"] = tokens["input_ids"].copy()
return tokens
ds = ds.map(to_text, remove_columns=ds.column_names)
为了简化,这里让模型对整段文本计算损失。更规范的做法是只对 assistant 部分计算损失,可以作为进阶练习。
步骤 4:配置 LoRA
from peft import LoraConfig, get_peft_model
config = LoraConfig(
r=8,
lora_alpha=16,
lora_dropout=0.05,
target_modules=["q_proj", "v_proj"],
task_type="CAUSAL_LM",
)
model = get_peft_model(model, config)
model.print_trainable_parameters()
print_trainable_parameters() 会打印可训练参数占比,你会看到它远小于总参数量,这正是 LoRA 省资源的原因。
r:低秩矩阵的秩,越大可训练参数越多;target_modules:插入 LoRA 的层名称,不同模型结构的命名可能不同,可以通过print(model)查看。
步骤 5:开始训练
from transformers import Trainer, TrainingArguments, DataCollatorForSeq2Seq
args = TrainingArguments(
output_dir="lora-out",
per_device_train_batch_size=4,
num_train_epochs=3,
learning_rate=2e-4,
logging_steps=10,
save_strategy="epoch",
)
trainer = Trainer(
model=model,
args=args,
train_dataset=ds,
data_collator=DataCollatorForSeq2Seq(tokenizer, padding=True),
)
trainer.train()
model.save_pretrained("lora-out/final")
训练过程中关注 loss 是否整体下降。如果显存不足,先把 batch size 调小。
步骤 6:加载并测试
from peft import PeftModel
base = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype="auto")
ft = PeftModel.from_pretrained(base, "lora-out/final")
msgs = [{"role": "user", "content": "下周一上午开会前把PPT改完"}]
inputs = tokenizer.apply_chat_template(msgs, add_generation_prompt=True, return_tensors="pt")
out = ft.generate(inputs, max_new_tokens=50)
print(tokenizer.decode(out[0][inputs.shape[1]:], skip_special_tokens=True))
对比微调前后的输出格式,就能直观感受到微调的效果。
进阶:如何判断这次微调是否有价值
跑通流程只是第一步,更重要的是判断微调到底有没有带来改进。这里分享一个简单可行的评估思路。
1. 设计固定的测试题
准备 20~50 条训练集中没有出现过的输入,覆盖常见情况和边界情况,例如没有明确截止时间的句子、包含多个任务的句子、带有错别字的口语表达。
2. 定义明确的评判标准
以本文的待办改写任务为例,可以检查三项:输出是否以【待办】开头;是否包含“截止”字段;任务描述是否忠实于原句、没有添加原文没有的信息。每一项都可以写成简单的规则自动判断,而不是凭感觉打分。
3. 对比三个版本
分别测试:原始模型直接回答、原始模型配合精心编写的提示词(附几条示例)、微调后的模型。如果“提示词 + 示例”已经能达到差不多的效果,那么对于这个任务来说,微调的必要性就不大;如果微调版本在格式稳定性上明显更好,并且推理时不再需要冗长的示例,微调的价值就体现出来了。
4. 关注副作用
在小数据集上反复训练,模型可能对其他类型的问题表现变差。可以准备几条与任务无关的普通问题一起测试,观察模型的通用能力是否明显下降。
养成“先定义评估、再动手训练”的习惯,比记住任何一个参数设置都更重要。
五、常见问题 FAQ
Q1:报错 CUDA out of memory?
降低 per_device_train_batch_size、缩短 max_length,或换更小的模型。也可以研究 QLoRA(4 比特量化加载 + LoRA)进一步节省显存。
Q2:loss 下降了,但输出还是不对?
检查数据格式是否一致、训练时的对话模板与推理时是否相同。样本太少时,可以适当增加训练轮数,但要警惕过拟合。
Q3:没有显卡能不能做?
纯 CPU 也能跑通极小规模的实验,但速度很慢。入门阶段可以使用提供 GPU 的在线笔记本环境。
Q4:训练出来的 LoRA 文件能单独分享吗?
可以,LoRA 权重通常只有几 MB 到几十 MB,使用时需要配合同一个基础模型加载。分享前请确认基础模型和训练数据的许可。
Q5:target_modules 填什么?
不同模型结构的层名称不同。常见的注意力层名称包括 q_proj、k_proj、v_proj、o_proj,以 print(model) 的实际输出为准。
六、实用技巧
- 先做基线:微调前先用同样的问题测试原模型,记录结果,方便对比;
- 留出测试集:从数据中拿出一部分不参与训练,用来检验泛化能力;
- 一次只改一个参数:调整学习率、轮数、r 值时逐个尝试,才能知道是什么带来的变化;
- 记录实验:把每次的参数和效果写进表格,避免重复踩坑;
- 评估要贴近真实场景:用你实际会遇到的输入测试,而不是只看训练数据。
七、总结
LoRA 微调的完整流程可以概括为:准备统一格式的数据 → 加载基础模型 → 套用对话模板 → 配置 LoRA → 训练 → 加载适配器测试。跑通一次之后,你会对“微调”有非常具体的认识,也能更理性地判断:什么问题适合微调,什么问题更适合提示词或 RAG。
参考资料
- Hugging Face PEFT 官方文档:LoRA 章节
- Hugging Face Transformers 官方文档:Trainer 章节
- LoRA 原始论文:LoRA: Low-Rank Adaptation of Large Language Models
- X 用户 @KirkDBorne 关于小语言模型构建与微调指南的分享
更多推荐


所有评论(0)