第3章:LoRA实战入门
好,咱们进入正题。LoRA这东西,说白了就是给大模型打个「补丁」。你想想看,一个几百亿参数的模型,全量微调一次,光显存就能让你怀疑人生。我最早做BERT微调时,一张24G的卡勉强能跑,换成LLaMA直接爆显存——那感觉,就像你刚想跑步,鞋带先断了。
LoRA的出现,算是把这个问题给解了。它不碰原始权重,只在旁边加几条「小尾巴」。嗯,就是低秩矩阵。
3.1 LoRA原理:为什么只改一小块就够了?
先问个问题:大模型学到的知识,是不是所有参数都重要?
答案是否定的。我做过一个实验:把微调后的权重矩阵做SVD分解,发现前10%的奇异值就占了90%的能量。说白了,真正起作用的,是那些「主方向」。LoRA就是抓住了这个特点——它假设权重更新的「有效秩」很低。
数学上,原始前向传播是:
h = W₀x
LoRA把它改成:
h = W₀x + BAx
其中W₀是冻结的原始权重,B和A是两个小矩阵。B的维度是d×r,A的维度是r×k。r就是秩,一般取4、8、16。你看,原来W₀是d×k,参数量是d×k。现在多了个BA,参数量是d×r + r×k。当r远小于d和k时,这省下来的可不是一星半点。
核心直觉:LoRA假设大模型在特定任务上的「调整方向」是稀疏的。你不需要转动整个方向盘,只需要微调几个螺丝就够了。
我在项目中遇到过一件事:用LoRA微调一个7B模型做代码生成,r=8时效果已经和全量微调差不多,但显存占用从80G降到了16G。嗯,这就是低秩假设的威力。
3.2 低秩矩阵的数学直觉
咱们用个例子来理解。假设你有一个1000×1000的矩阵,全量更新就是100万个参数。但LoRA告诉你:这个更新矩阵可以分解成两个1000×8和8×1000的矩阵相乘。参数量变成1000×8 + 8×1000 = 16000个。省了98.4%。
为什么会这样?因为自然语言处理中的「任务适应」往往只需要在低维子空间里调整。你想想看,一个模型从「通用」变成「法律问答专家」,它需要改变的是对特定术语和逻辑的理解,而不是重新学习语言本身。
我习惯用「投影」来理解:A矩阵把输入x投影到低维空间,B矩阵再把它投影回来。中间那个r维空间,就是任务相关的「精华」所在。
| 秩 r | 参数量(d=4096, k=4096) | 节省比例 | 适用场景 |
|---|---|---|---|
| 1 | 8192 | 99.95% | 简单分类任务 |
| 4 | 32768 | 99.80% | 文本生成微调 |
| 8 | 65536 | 99.61% | 复杂指令微调 |
| 16 | 131072 | 99.22% | 多任务学习 |
我的经验:r=8是个不错的起点。如果任务简单,可以降到4;如果数据量大且任务复杂,试试16。别一上来就设64,那还不如直接全量微调呢。
3.3 在HuggingFace PEFT库中实现LoRA
好,理论说完了,咱们上手。PEFT库把LoRA封装得特别干净,几行代码就能跑起来。
先安装:
pip install peft transformers datasets
然后加载模型和分词器:
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model
model_name = "meta-llama/Llama-2-7b-hf"
model = AutoModelForCausalLM.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)
接下来配置LoRA:
lora_config = LoraConfig(
r=8,
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.1,
bias="none",
task_type="CAUSAL_LM"
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 输出:trainable params: 4,194,304 || all params: 6,742,609,920 || trainable: 0.062%
看到没?可训练参数只有0.062%。我刚开始用的时候也吓了一跳——这么少的参数,能行吗?结果跑完一个epoch,效果出奇的好。
避坑指南:我曾经在target_modules里只写了"q_proj",结果模型收敛特别慢。后来发现,只改query投影是不够的,最好把query和value一起改。有些任务甚至需要把key也加上。具体加哪些层,建议先跑个小实验试试。
训练代码和普通微调一样:
from transformers import TrainingArguments, Trainer
training_args = TrainingArguments(
output_dir="./lora-llama",
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-4,
num_train_epochs=3,
logging_steps=100,
save_steps=500,
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
)
trainer.train()
训练完后,保存和加载也很简单:
# 保存
model.save_pretrained("./lora-llama-final")
# 加载
from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained(model_name)
lora_model = PeftModel.from_pretrained(base_model, "./lora-llama-final")
小技巧:lora_alpha这个参数控制着LoRA的缩放比例。我习惯设成r的两倍,也就是alpha=2r。如果发现模型输出太「冲」,可以调低alpha;如果觉得微调效果不明显,就调高它。
3.4 本章知识体系
下面这张图,是我自己梳理的LoRA核心逻辑。你看一遍,应该就能把整个流程串起来了。
你看,整个流程其实就三步:原始权重走主路,LoRA分支走旁路,最后在加法节点汇合。训练时只更新旁路,推理时可以把旁路合并回主路——这样就不会增加推理延迟。
嗯,LoRA就是这么个东西。原理不复杂,但用好了能省下大把算力。我建议你拿到代码后,先拿一个小模型跑一遍,感受一下参数量的变化。等熟悉了,再上大模型。
记住一句话:LoRA不是银弹,但它是目前性价比最高的微调方案之一。
本章要点回顾:
- LoRA通过低秩分解,把全量微调变成只更新两个小矩阵
- 秩r控制着参数量和效果之间的平衡,建议从r=8开始
- PEFT库让LoRA实现变得极其简单,几行代码就能搞定
- 训练时只更新LoRA参数,推理时可合并回原始权重
公众号:蓝海资料掘金营,微信deep3321