上一篇文章里,我用全量微调的方式,直接修改了 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 的路径,大概就是这样的。

Logo

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

更多推荐