适合人群:会一点 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) 的实际输出为准。

六、实用技巧

  1. 先做基线:微调前先用同样的问题测试原模型,记录结果,方便对比;
  2. 留出测试集:从数据中拿出一部分不参与训练,用来检验泛化能力;
  3. 一次只改一个参数:调整学习率、轮数、r 值时逐个尝试,才能知道是什么带来的变化;
  4. 记录实验:把每次的参数和效果写进表格,避免重复踩坑;
  5. 评估要贴近真实场景:用你实际会遇到的输入测试,而不是只看训练数据。

七、总结

LoRA 微调的完整流程可以概括为:准备统一格式的数据 → 加载基础模型 → 套用对话模板 → 配置 LoRA → 训练 → 加载适配器测试。跑通一次之后,你会对“微调”有非常具体的认识,也能更理性地判断:什么问题适合微调,什么问题更适合提示词或 RAG。

参考资料

  • Hugging Face PEFT 官方文档:LoRA 章节
  • Hugging Face Transformers 官方文档:Trainer 章节
  • LoRA 原始论文:LoRA: Low-Rank Adaptation of Large Language Models
  • X 用户 @KirkDBorne 关于小语言模型构建与微调指南的分享
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐