【Lora微调详解】一篇文章带你搞懂Lora微调中的A、B矩阵,参数r,参数Alpha的详细解释与调参经验
这篇文章会用 “原理 + 数学 + 代码 + 直观类比 + 最佳实践” 的方式,带你彻底掌握 LoRA 中的:
A、B矩阵r(秩)α(alpha)α/r缩放比
初始化策略
其他关键参数(dropout、bias、target_modules)
一、LoRA 核心思想回顾
我们有一个大模型,其某层权重为 $ W \\in \\mathbb{R}^{d \\times k} $(例如 4096×4096),全量微调要更新这个 WWW,成本极高。
LoRA 的想法是:不直接改 WWW,而是加一个小的低秩增量:
ΔW=B⋅A其中 A∈Rd×r, B∈Rr×k, r≪d\\Delta W = B \\cdot A \\quad \\text{其中 } A \\in \\mathbb{R}^{d \\times r},\\ B \\in \\mathbb{R}^{r \\times k},\\ r \\ll dΔW=B⋅A其中 A∈Rd×r, B∈Rr×k, r≪d
最终前向传播变为:
h=x⋅(W+ΔW)=x⋅W+x⋅B⋅A⏟LoRA 增量h = x \\cdot (W + \\Delta W) = x \\cdot W + \\underbrace{x \\cdot B \\cdot A}_{\\text{LoRA 增量}}h=x⋅(W+ΔW)=x⋅W+LoRA 增量x⋅B⋅A
- 原始 WWW:冻结不动
- A,BA, BA,B:可训练的小矩阵
这就是“参数高效微调”的本质:只训练极少量参数,就能让大模型适应新任务。
二、A 和 B 矩阵详解
一句话总结(
在 LoRA 中:
- 矩阵 A:负责“捕捉输入中的新特征”(可理解为“激活路径”)
- 矩阵 B:负责“把这些特征映射回输出空间”(可理解为“重组输出”)
它们共同构成一个低秩增量:
ΔW=B⋅A\\Delta W = B \\cdot AΔW=B⋅A
其中 A 先学“怎么提取”,B 再学“怎么组合”,两者协同完成对原始权重的微调。
一、背景回顾:LoRA 是干嘛的?
我们有一个大模型,比如 Qwen2.5-VL-7B,它的某个线性层是:
h = x @ W # W 是原始权重,比如 [4096 → 4096]
全参数微调要更新这个 WWW,但参数太多(千万级),显存爆炸。
LoRA 的想法是:我不动 WWW,而是加一个小的增量 ΔW\\Delta WΔW:
h=x@(W+ΔW)h = x @ (W + \\Delta W)h=x@(W+ΔW)
而这个 ΔW\\Delta WΔW 不是随便学的,它是两个小矩阵的乘积:
ΔW=B⋅A其中 A∈Rd×r, B∈Rr×k, r≪d\\Delta W = B \\cdot A\\quad \\text{其中 } A \\in \\mathbb{R}^{d \\times r},\\ B \\in \\mathbb{R}^{r \\times k},\\ r \\ll dΔW=B⋅A其中 A∈Rd×r, B∈Rr×k, r≪d
这就是 LoRA 的核心公式。
二、A 和 B 到底长什么样?有什么区别?
我们以一个具体的例子来说明:
假设有一个 q_proj 层:
- 输入维度 d=4096d = 4096d=4096
- 输出维度 k=4096k = 4096k=4096
- LoRA 秩 r=64r = 64r=64
那么:
[4096 × 64][64 × 4096]所以:
- x⋅Ax \\cdot Ax⋅A:把输入 xxx 投影到一个 64 维的“LoRA 特征空间”
- (x⋅A)⋅B(x \\cdot A) \\cdot B(x⋅A)⋅B:再从这个特征空间投影回最终输出空间
A 是“降维器”(encoder),B 是“升维器”(decoder)
这就像你在做 PCA 或 AutoEncoder:
- A:编码器(encoder)→ 压缩信息
- B:解码器(decoder)→ 重构输出
只是这里不是为了压缩,而是为了高效地学习一个微小的增量变化。
三、A 和 B 的初始化方式不同(关键区别!)
这是很多人忽略但极其重要的点!
#在 peft 库中,默认初始化是:
# 矩阵 A:从正态分布初始化A = torch.randn(in_features, r) * std # 例如 N(0, 0.02)# 矩阵 B:全零初始化B = torch.zeros(r, out_features) # 初始为 0
为什么这么做?
因为:
- 初始时我们希望 ΔW=B⋅A=0\\Delta W = B \\cdot A = 0ΔW=B⋅A=0,这样模型行为完全由原始 WWW 决定(保持预训练能力)
- 如果 B=0B=0B=0,不管 AAA 是什么,ΔW=0\\Delta W = 0ΔW=0
- 所以 B 初始化为 0,A 初始化为小随机数
这样做的好处是:训练开始时模型输出不变,训练稳定;随着训练进行,B 逐渐学到有效的增量
举个形象的比喻
想象你在教一个大师画画(预训练模型),他已经很厉害了。
现在你想让他学会画猫:
- 你不能直接改他的大脑(冻结 WWW)
- 你给他戴一副“智能眼镜”(LoRA)
- 这副眼镜有两个镜片:
- A 镜片(前镜):观察画面,提取“耳朵、胡须”等猫的特征
- B 镜片(后镜):把这些特征融合,告诉大脑“该怎么改笔触”
- 这副眼镜有两个镜片:
刚开始,B 镜片是透明的(B=0B=0B=0),所以不影响作画。
训练过程中,B 学会如何利用 A 提取的特征来调整输出。
A 学“看什么”,B 学“怎么用”
四、数学表达:前向传播过程
给你完整的数学流程:
原始输出:h=x⋅WLoRA 增量:Δh=x⋅(B⋅A)总输出:hnew=x⋅W+x⋅B⋅A=x⋅(W+B⋅A)\\begin{align*}\\text{原始输出:} &\\quad h = x \\cdot W \\\\\\text{LoRA 增量:} &\\quad \\Delta h = x \\cdot (B \\cdot A) \\\\\\text{总输出:} &\\quad h_{\\text{new}} = x \\cdot W + x \\cdot B \\cdot A \\\\&\\quad = x \\cdot (W + B \\cdot A)\\end{align*}原始输出:LoRA 增量:总输出:h=x⋅WΔh=x⋅(B⋅A)hnew=x⋅W+x⋅B⋅A=x⋅(W+B⋅A)
注意顺序:是 x→A→Bx \\to A \\to Bx→A→B,所以:
- AAA 是第一个变换,必须能接收原始输入 xxx
- BBB 是最后一个变换,必须能输出到原输出空间
五、代码层面:A 和 B 是怎么创建的?
在 peft 源码中(简化版):
class LoraLayer: def __init__(self, in_features, out_features, r=64): self.lora_A = nn.Linear(in_features, r, bias=False) self.lora_B = nn.Linear(r, out_features, bias=False) # 初始化 nn.init.kaiming_uniform_(self.lora_A.weight, a=math.sqrt(5)) # 小随机数 nn.init.zeros_(self.lora_B.weight) # 全零 def forward(self, x): return x @ self.lora_B.weight @ self.lora_A.weight # 注意顺序:先A后B,但矩阵乘法反过来
注意!虽然逻辑上是 x⋅A⋅Bx \\cdot A \\cdot Bx⋅A⋅B,但在 PyTorch 中写成:
delta = x @ (lora_B.weight @ lora_A.weight)
因为矩阵乘法结合律:
(x⋅A)⋅B=x⋅(A⋅B)(x \\cdot A) \\cdot B = x \\cdot (A \\cdot B)(x⋅A)⋅B=x⋅(A⋅B),但 A 和 B 的权重要先相乘
六、训练时谁更重要?A 还是 B?
两者都重要,但角色不同:
实践建议:
- 不要冻结 A 或 B
- 保持两者都可训练
- 使用相同的优化器(如 AdamW)
七、缩放系数 αr\\frac{\\alpha}{r}rα 的作用
你还记得这个公式吗?
ΔW=αr⋅B⋅A\\Delta W = \\frac{\\alpha}{r} \\cdot B \\cdot AΔW=rα⋅B⋅A
它的作用是控制 LoRA 更新的强度。
比如:
- r=64,α=16r=64, \\alpha=16r=64,α=16 → 缩放 = 0.25 → 增量较小,保守
- r=64,α=64r=64, \\alpha=64r=64,α=64 → 缩放 = 1.0 → 增量较大,激进
相当于:你让 B 的输出乘以一个系数,控制它对最终结果的影响程度
所以即使 B 学得很快,也可以用 α/r\\alpha/rα/r 把它“压一压”,防止破坏原始模型能力。
八、图解:A 和 B 的数据流动
x (输入, 4096) │ ▼ ┌─────────┐ │ A │ ← LoRA_A: [4096×64],提取低维特征 │ (随机初值)│ └─────────┘ │ ▼ z (64维) │ ▼ ┌─────────┐ │ B │ ← LoRA_B: [64×4096],初始为0 │ (零初始化) │ └─────────┘ │ ▼ Δh (输出增量, 4096) │ ├───→ 加到原始输出 x@W 上 ▼ h_new = x@W + Δh
初始状态:B=0 → Δh=0 → 模型行为不变
训练中:B 逐渐非零 → Δh 生效 → 模型微调
九、常见误区澄清
总结:A 与 B 的核心区别表
[in_dim × r][r × out_dim]三、参数 r:LoRA 秩(Rank)
含义:
- rrr 是低秩分解的“中间维度”
- 控制 LoRA 模型的容量(表达能力)
参数量计算:
每个 LoRA 层参数数:
Params=r×(in+out)\\text{Params} = r \\times (\\text{in} + \\text{out})Params=r×(in+out)
例如:in=4096, out=4096, r=64 → 64×(4096+4096)=524,28864 \\times (4096 + 4096) = 524,28864×(4096+4096)=524,288
如果一个模型有 40 层,每层 7 个模块(q,k,v,o,gate,up,down)→ 总 LoRA 参数 ≈ 1.47 亿
但实际上由于共享结构,通常在 100万~300万 可训练参数之间。
如何选择 r?
📌 Qwen 官方推荐:r=64 或 r=128
四、参数 α(lora_alpha):缩放系数
含义:
控制 LoRA 增量 ΔW\\Delta WΔW 的强度。
最终公式是:
ΔW=αr⋅B⋅A\\Delta W = \\frac{\\alpha}{r} \\cdot B \\cdot AΔW=rα⋅B⋅A
所以 α\\alphaα 不是独立作用,而是和 rrr 一起决定缩放比例。
为什么需要 α\\alphaα?
因为:
- 当 rrr 很小时,B⋅AB \\cdot AB⋅A 的数值范围可能太小 → 影响太弱
- 我们想让 LoRA 的更新“力度”可控
👉 举个例子:
- r=8,α=16r=8, \\alpha=16r=8,α=16 → 缩放 = 16/8=2.016/8 = 2.016/8=2.0 → 增量被放大 2 倍
- r=64,α=16r=64, \\alpha=16r=64,α=16 → 缩放 = 16/64=0.2516/64 = 0.2516/64=0.25 → 增量被缩小
也就是说:α\\alphaα 可以补偿 rrr 太小带来的表达力不足
推荐的 α/r\\alpha/rα/r 比值(经验法则)
最佳实践:保持 α/r=0.5\\alpha/r = 0.5α/r=0.5 或 1.0
例如:
r=64, alpha=32→ 比值 0.5 (推荐)r=64, alpha=64→ 比值 1.0
建议尝试
alpha=32或64,提升学习效率
五、lora_dropout=0.05:防过拟合
作用:
在 LoRA 的 A 输出上加 dropout,防止过拟合。
x → A → Dropout → B → Δh
- 训练时启用
- 推理时自动关闭
推荐值:
六、bias=\"none\":是否微调偏置项
选项:
\"none\":不训练任何 bias( 推荐)\"all\":训练所有 bias\"lora_only\":只训练 LoRA 层内部的 bias
为什么用 \"none\"?
-
bias 参数量小
-
微调 bias 效果不明显
-
增加复杂度,容易过拟合
标准做法就是
bias=\"none\"
七、target_modules:哪些模块加 LoRA?
你在代码中写了:
target_modules=[\"q_proj\", \"k_proj\", \"v_proj\", \"o_proj\", \"gate_proj\", \"up_proj\", \"down_proj\"]
这是针对 Qwen 类模型(基于 Transformer) 的最佳选择。
各模块含义:
q_projk_projv_projo_projgate_proj, up_proj, down_projQwen 的 FFN 是 SwiGLU 结构:
FFN(x)=down_proj(Swish(gate_proj(x))⊗up_proj(x))\\text{FFN}(x) = \\text{down\\_proj}\\left(\\text{Swish}(\\text{gate\\_proj}(x)) \\otimes \\text{up\\_proj}(x)\\right)FFN(x)=down_proj(Swish(gate_proj(x))⊗up_proj(x))
所以这三个都必须微调!
八、总结:LoRA 参数最佳实践表
r(秩)lora_alpha2×r 或 rlora_dropoutbias\"none\"target_modulesinit for Binit for A你的配置:
r=64, alpha=16, dropout=0.05, bias=\"none\"
→ 唯一建议:把 alpha 改成 32 或 64,让学习更强一些。
九、你可以做的实验
实验 1:对比不同 alpha
实验 2:可视化 A 和 B 的权重分布
for name, param in peft_model.named_parameters(): if \"lora_A\" in name and \"weight\" in name: print(f\"{name}: mean={param.mean():.6f}, std={param.std():.6f}\") if \"lora_B\" in name and \"weight\" in name: print(f\"{name}: mean={param.mean():.6f}, std={param.std():.6f}\")
你应该看到:
- A:有非零均值
- B:训练初期接近 0,后期逐渐非零


