「微调大模型」这五个字,十个人里有九个想到的是几千块显卡和几百万条数据。其实现在主流的做法已经反过来了:冻结原模型,只训练几百万个额外参数,一张消费级显卡就能跑。这篇讲这条路线的最小闭环,以及最容易踩的几个坑。
判断直接放开头:除非你有近乎无限的算力,微调的首选是 LoRA,而不是全参数。
全参微调与 LoRA
全参微调是把模型所有权重都更新一遍,效果上限高,代价是显存和成本都拉满,一个 7B 模型全参微调要几十 GB 显存。LoRA(Low-Rank Adaptation)反着来:冻结原模型,只在注意力层里插入两组低秩矩阵 A 和 B,训练时只更新 A、B,推理时把 A×B 叠回原权重。7B 模型用 LoRA,可训练参数能压到原模型的 0.1% 到 1%,显存需求从几十 GB 掉到个位数 GB。
这两者的分界线很清晰:任务离预训练分布越远,越需要全参;任务只是让模型学会一种新的格式、语气或小领域,LoRA 就够。别一上来就冲着全参去,大多数业务场景用不上。
target_modules 别抄错
LoRA 插在哪些层,由 target_modules 决定。这是第一个高频坑:网上教程给 Llama 的例子写成 c_attn,那是 GPT-2 的层名,直接抄到 Llama、Mistral 上会静默失效——模型看起来在训,其实 A、B 挂在了不存在的层上,零有效学习。
正确做法是先打印自己模型的层名,确认注意力投影层的真实名字。Llama 系通常是 q_proj、v_proj、k_proj、o_proj 四个。不同的模型族层名不一样,这一步不能偷懒。
一段最小训练脚本
下面这段用 peft 给一个小模型挂 LoRA,训练一个情感分类任务。变量名、数据集、场景是我自己组织的,API 用法是公开的。
from transformers import AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments
from peft import LoraConfig, get_peft_model, TaskType
from datasets import Dataset
model_name = "meta-llama/Llama-3.2-3B-Instruct"
model = AutoModelForCausalLM.from_pretrained(model_name, device_map="auto")
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
lora_config = LoraConfig(
r=16,
lora_alpha=32, # 经验值 alpha = 2 * r
target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
lora_dropout=0.05,
bias="none",
task_type=TaskType.CAUSAL_LM,
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters() # 确认可训练占比落在 0.1% ~ 1%
def build_prompt(row):
return f"判断情感:{row['text']} 答案:"
train_data = Dataset.from_list([
{"text": "快递三天没到,客服还打不通", "label": "负面"},
{"text": "机器很好用,续航也久", "label": "正面"},
])
def tokenize(row):
return tokenizer(build_prompt(row) + row["label"], truncation=True, max_length=256)
train_data = train_data.map(tokenize, remove_columns=["text", "label"])
training_args = TrainingArguments(
output_dir="./lora_output",
num_train_epochs=2,
per_device_train_batch_size=4,
learning_rate=2e-4,
logging_steps=10,
save_strategy="epoch",
)
trainer = Trainer(model=model, args=training_args, train_dataset=train_data)
trainer.train()
model.save_pretrained("./lora_output")
两个要点。一是 print_trainable_parameters() 一定要看:可训练占比低于 0.1% 基本欠拟合,高于 2% 说明挂错了层或没冻结干净。二是 alpha 别乱设,经验值是 rank 的两倍,alpha 太小学习信号被压没,太大会训练不稳。
数据与模板
模型之外,数据决定微调的下限。三个高频坑都在数据侧。一是格式没对齐:指令模型训练时用的是特定的 chat template,你微调时也必须在同一套模板里喂数据,模板对不上,模型学到的格式就是乱的。二是质量比数量重要:几十条干净、一致、标注准确的样本,效果往往好过几百条乱七八糟的。三是不该训的 token 别训——指令微调通常只该让模型学「回答」那部分,而不是把用户的问题也当成要生成的文本一起优化,这靠 loss masking 把非生成目标的 token 屏蔽掉。
还有一个隐藏的认知偏差:训练 loss 掉得快不代表模型变好了。一定要把训练数据和验证数据分开,用模型没见过的样本看它到底学会没学会。loss 低但验证一塌糊涂,就是过拟合,数据太窄或者训太多轮。
QLoRA 与显存
显存还是不够,就上 QLoRA:把基座模型量化到 4-bit 或 8-bit 再挂 LoRA,显存再降约四倍。代价是精度略掉一点,训练也略慢——量化反量化有开销。所以 QLoRA 不是「更快的 LoRA」,是「显存不够时更省的 LoRA」,别把它当默认选项。
评估与遗忘
微调最容易漏的一步是评估。训练 loss 掉下去了不算数,要留一撮验证数据,再看模型在「原来的任务」上有没有退化。LoRA 只更新一小撮参数,但训练数据太窄,模型照样会学成一个只会输出某种风格、别的全忘的样子。所以评估要两头看:目标领域准不准,通用能力掉没掉。
值不值得微调
最后泼一盆冷水:很多人以为微调能「教会模型新知识」,其实 LoRA 更擅长的是「教会模型新格式、新语气、新输出规范」,不擅长往模型里灌全新的事实——那更适合用 RAG 检索。动手前先分清你要的是行为改变还是知识注入,选错方向,微调再久也是白练。