给 AI 模型“贴壁纸“:LoRA 微调的原理与实战
微调时的参数变化是低秩的,可以用两个小矩阵的乘积来近似。但这一句话改变了整个大模型微调的格局。在 LoRA 之前,微调一个大模型需要几十张 GPU;在 LoRA 之后,一张消费级显卡甚至 CPU 就能搞定。它让"每个人都能定制自己的 AI"从口号变成了现实。第一篇:AI 怎么知道"酒店≈饭店" → 理解向量和训练原理第二篇:微调实战中的 7 个灵魂拷问 → 搞清楚工程细节第三篇:给 AI 贴壁纸(
上一篇文章里,我用全量微调的方式,直接修改了 m3e-base 模型的 1.02 亿个参数。效果不错,但我心里一直有个疑问——50 条训练数据,改 1 亿个参数,是不是有点"杀鸡用牛刀"了?这篇文章就从这个疑问出发,聊聊一种更聪明的微调方式:LoRA。
第一章:一亿个参数,你真的都需要改吗?
先回顾一下全量微调做了什么。
m3e-base 模型里有 1.02 亿个参数。全量微调时,每一个参数都参与训练,都可能被修改。
但我的训练数据只有 50 条。
50 条数据,改 1 亿个参数。这就像你只有 50 道考试题,却要调 1 亿个旋钮——绝大部分旋钮其实不需要动,动多了反而把模型"带偏"(过拟合)。
问题是:有没有办法只动"必要"的那部分参数?
2021 年,微软的研究团队给出了答案——LoRA(Low-Rank Adaptation,低秩适应)。
第二章:1 亿个参数长什么样
要理解 LoRA,先要知道这 1.02 亿个参数是怎么组织的。
m3e-base 基于 BERT 架构,像一栋 12 层的大楼:
m3e-base 模型(1.02 亿参数)
│
├── 地基:Embedding 层(词向量表) ~1600 万参数
│
├── 1 楼:Transformer 第 1 层 ~700 万参数
│ ├── Q (Query) 矩阵 768×768 589,824
│ ├── K (Key) 矩阵 768×768 589,824
│ ├── V (Value) 矩阵 768×768 589,824
│ ├── O (Output) 矩阵 768×768 589,824
│ ├── FFN 上行矩阵 768×3072 2,359,296
│ └── FFN 下行矩阵 3072×768 2,359,296
│
├── 2 楼:Transformer 第 2 层(结构同上) ~700 万参数
├── 3 楼:...
├── ...
└── 12 楼:Transformer 第 12 层 ~700 万参数
每一层有 6 个主要的矩阵(Q、K、V、O、FFN 上、FFN 下),每个矩阵里面装着几十万个参数。12 层加上地基,总共 1.02 亿。
其中 Q、K、V 是注意力机制的核心——模型就是通过这三个矩阵来"理解"词与词之间的关系的。当我们说"酒店"和"饭店"语义接近,这个知识就编码在这些矩阵的数值里。
全量微调时,这 12 层 × 6 个矩阵 = 72 个矩阵,全部拆开重新调整。
LoRA 说:不用全拆,挑几个关键的,贴层壁纸就行。
第三章:"低秩"到底是什么意思
LoRA 的全称是 Low-Rank Adaptation——“低秩适应”。"低秩"是关键词,但它听起来很学术。用大白话解释一下。
先看一个矩阵能干什么
以 Q 矩阵为例,它是 768×768 的,有 589,824 个参数。这个矩阵可以把一个 768 维的向量变换成另一个 768 维的向量。
589,824 个参数意味着它有 589,824 个"自由度"——可以做非常复杂、非常精细的变换。
微调时需要这么多自由度吗?
你用 50 条酒店数据微调,本质上是想让模型学会这几件事:
"标间" 靠近 "标准间"
"含双早" 靠近 "含早餐"
"西湖大酒店" 靠近 "西湖大饭店"
...
这些调整在 768 维空间里,可能只涉及几个方向上的移动。就像在一张世界地图上,你只想让"酒店"和"饭店"两个图钉靠近一点——你不需要重新画整张地图,只需要沿着一两个方向挪动就行。
这就是"低秩"的含义:微调所需的参数变化,维度远低于矩阵本身的维度。
一个比喻
全量微调:
你有一面 768×768 像素的 LED 屏幕
每个像素可以独立调节
→ 589,824 个旋钮
LoRA:
你只需要 8 个滤镜,叠加在屏幕上
每个滤镜覆盖整面屏幕,但效果不同
→ 768×8 + 8×768 = 12,288 个旋钮
→ 视觉效果几乎一样,旋钮少了 98%
这个 “8” 就是 LoRA 中的秩(rank),通常用字母 r 表示。r 越大,滤镜越多,表达能力越强,但参数也越多。实际中,r 取 4~16 就足够应对大多数微调任务。
第四章:LoRA 的数学(只有一行)
全量微调时,训练结束后,权重矩阵从 W 变成了 W’:
W' = W + ΔW
ΔW 是训练过程中学到的"改动",和 W 一样是 768×768 的大矩阵。
LoRA 的核心想法:ΔW 是低秩的,可以分解成两个小矩阵的乘积。
W' = W + A × B
W: 原始矩阵,768×768,冻结不动
A: 768×r 的小矩阵(r=8 时只有 6,144 个参数)
B: r×768 的小矩阵(r=8 时只有 6,144 个参数)
训练时只训练 A 和 B,W 完全不碰
画出来:
原始矩阵 W(冻结)
输入 ──→ ┌────────────┐ ──→ 输出
│ │ 768×768 │ ↑
│ │ 不训练 │ │
│ └────────────┘ │ 相加
│ │
│ LoRA 适配器(训练) │
└────→ ┌──────┐ ┌──────┐ ──┘
│768×8 │→│8×768 │
│ A │ │ B │
└──────┘ └──────┘
降维 升维
推理时:输入同时经过 W 和 LoRA,结果相加。训练时:梯度只更新 A 和 B,W 纹丝不动。
就这么简单。没有复杂的新架构,没有特殊的训练技巧,只是把"大矩阵的改动"分解成了"两个小矩阵的乘积"。
第五章:LoRA 插在哪里
模型每层有 6 个矩阵(Q、K、V、O、FFN 上、FFN 下),LoRA 可以选择插在其中任意几个上面。
通常的选择是 Q 和 V——这两个矩阵对语义理解影响最大:
12 层 Transformer,每层在 Q 和 V 上插入 LoRA
可训练参数 = 12层 × 2个矩阵 × (768×8 + 8×768)
= 12 × 2 × 12,288
= 294,912
≈ 30 万
对比全量微调的 1.02 亿,只有 0.29%
用之前的大楼比喻:
全量微调:12 层楼,每层 6 个房间,所有墙壁全拆了重砌
LoRA:12 层楼,只在每层的 2 个房间(Q 和 V)贴了一层薄壁纸
其他房间和墙壁完全不动
但住进去的感觉几乎一样
第六章:LoRA 的三个关键参数
在代码中,LoRA 的配置长这样:
LoraConfig(
r=8,
lora_alpha=16,
lora_dropout=0.1,
target_modules=["query", "value"],
)
只有三个需要你操心的参数:
r(秩)—— 适配器的"容量"
r=4: 参数少,学习能力弱,适合数据极少(<50条)的场景
r=8: 默认选择,大多数场景够用
r=16: 参数多,学习能力强,适合数据较多或任务复杂的场景
r=64: 接近全量微调的效果,但也接近全量微调的成本
越大越强,但也越容易过拟合。就像滤镜越多,调色越精细,但也越容易调过头。
lora_alpha(缩放系数)—— 适配器的"音量"
LoRA 的输出会除以 lora_alpha / r 来缩放。通常设为 r 的 2 倍就好:
r=8 → lora_alpha=16
r=16 → lora_alpha=32
不用想太多,这是个经验值。
target_modules(目标模块)—— 贴在哪个房间
target_modules=["query", "value"] # 最常见,性价比最高
target_modules=["query", "key", "value"] # 多加 K,效果略好
target_modules=["query", "key", "value", "output", "intermediate", "output_dense"]
# 全部都贴,接近全量微调
通常选 Q 和 V 就足够了。加的越多,参数越多,收益递减。
第七章:动手跑一个 LoRA 微调
和全量微调的 step3_finetune.py 相比,LoRA 版只多了一步——“给模型装适配器”。核心改动不到 20 行。
安装依赖
pip install peft # HuggingFace 的参数高效微调库
关键代码
from peft import LoraConfig, TaskType, get_peft_model
# 1. 照常加载原始模型
model = SentenceTransformer("moka-ai/m3e-base")
# 2. 配置 LoRA(这是唯一新增的部分)
lora_config = LoraConfig(
task_type=TaskType.FEATURE_EXTRACTION,
r=8,
lora_alpha=16,
lora_dropout=0.1,
target_modules=["query", "value"],
)
model[0].auto_model = get_peft_model(model[0].auto_model, lora_config)
# 3. 后面的训练代码和全量微调完全一样
# trainer.train() ...
运行时会打印参数统计,你会直观看到差异:
全量微调: LoRA 微调:
总参数量: 102,267,648 总参数量: 102,267,648
可训练参数: 102,267,648 (100%) 冻结参数: 101,975,040 (99.71%)
可训练参数: 292,608 (0.29%)
同样的模型,全量微调要调 1 亿个参数,LoRA 只调 30 万个。
保存方式
LoRA 训练完有两种保存选择:
选择 A:只保存适配器
output/lora-adapter/
├── adapter_config.json ← LoRA 配置
└── adapter_model.safetensors ← 适配器权重(几百 KB)
优点:极小,方便分发
缺点:加载时需要原始模型 + peft 库
选择 B:合并成完整模型
把 W' = W + A×B 算好,存成一个普通模型
output/final/
└── model.safetensors ← 完整模型(~400 MB)
优点:和普通模型一样使用,不依赖 peft
缺点:丧失了"可插拔"的优势
第八章:LoRA 最酷的地方——可插拔
全量微调的问题是:微调之后,原始模型就"回不去"了。如果你有酒店、餐饮、交通三个业务场景,就需要保存三个 400MB 的完整模型。
LoRA 不一样。原始模型从头到尾不动,不同场景只是换一个几百 KB 的适配器:
原始 m3e-base(400MB,永远不动)
│
├── 酒店适配器 (300 KB) → "标间" ≈ "标准间"
├── 餐饮适配器 (300 KB) → "炒饭" ≈ "蛋炒饭"
└── 交通适配器 (300 KB) → "高铁" ≈ "动车"
总存储:400 MB + 300 KB × 3 ≈ 401 MB
对比全量微调:400 MB × 3 = 1.2 GB
加载时,只需要:
from peft import PeftModel
base_model = load("moka-ai/m3e-base")
# 酒店场景
hotel_model = PeftModel.from_pretrained(base_model, "lora-adapter-hotel")
# 切换到餐饮场景(换个适配器就行)
food_model = PeftModel.from_pretrained(base_model, "lora-adapter-food")
就像手机壳——手机(原始模型)不变,根据场合换不同的壳(适配器)。
第九章:LoRA 和全量微调怎么选
| 全量微调 | LoRA | |
|---|---|---|
| 训练参数量 | 1.02 亿(100%) | ~30 万(0.29%) |
| 训练速度 | 基准 | 更快 |
| 内存占用 | ~3 GB | ~1.5 GB |
| 保存大小 | ~400 MB / 场景 | ~300 KB / 场景 + 400 MB 基础模型 |
| 效果上限 | 最高(天花板) | 接近全量微调(差距通常 < 2%) |
| 过拟合风险 | 数据少时容易过拟合 | 天然抗过拟合(参数少) |
| 多场景切换 | 每个场景一个完整模型 | 共享基础模型,切换适配器 |
选择建议
你的场景:
├── 模型 < 1 GB,数据 < 1 万条
│ └── 全量微调就行,简单直接,5~10 分钟搞定
│
├── 模型 > 1 GB,或者需要多场景切换
│ └── LoRA,省内存、省存储、可插拔
│
└── 模型 > 10 GB(大语言模型级别)
└── 必须 LoRA,全量微调根本跑不起来
对于 m3e-base(400MB)来说,全量微调完全跑得动,LoRA 更多是一种学习和体验。但当你面对 LLaMA、ChatGLM 这些几十 GB 的大模型时,LoRA 就不是"可选项"了——它是"唯一的选项"。
写在最后
LoRA 的论文只有 9 页,核心想法只有一句话:微调时的参数变化是低秩的,可以用两个小矩阵的乘积来近似。
但这一句话改变了整个大模型微调的格局。在 LoRA 之前,微调一个大模型需要几十张 GPU;在 LoRA 之后,一张消费级显卡甚至 CPU 就能搞定。它让"每个人都能定制自己的 AI"从口号变成了现实。
回头看这几篇文章的脉络:
第一篇:AI 怎么知道"酒店≈饭店" → 理解向量和训练原理
第二篇:微调实战中的 7 个灵魂拷问 → 搞清楚工程细节
第三篇:给 AI 贴壁纸(本篇) → 理解 LoRA,更高效地微调
从"这是什么"到"怎么跑"到"怎么更聪明地跑",一个程序员理解 AI 的路径,大概就是这样的。
更多推荐


所有评论(0)