深蓝黑背景封面。左侧是 LoRA 结构分解图:上方一张灰色大矩阵卡片标注「W₀ 冻结不更新」,中间加号连接下方增量区,增量区里一个高瘦矩阵 B 标注「初始化为全 0」与一个扁平矩阵 A 标注「高斯随机初始化」相乘得到 ΔW,箭头指向结论条「训练起点 ΔW = B·A = 0,输出与原模型完全一致」;下方一行灰色小字注「若 B 也随机初始化,ΔW 起点是随机噪声,等价于先污染一遍预训练权重」。右侧大标题「LoRA 微调为什么把 B 矩阵初始化为全 0」,副标题「低秩增量从零起步:让训练的起点恰好是原模型」。左上标注「面试经典 · 参数高效微调」,右上圆角胶囊标注「Day 01 · 11-18」。 图 1:本文封面,B 全零守住起点、A 随机打破对称。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料整理,与任何公司的当前招聘无关。LoRA 几乎是大模型微调的默认选项,而「两个低秩矩阵分别怎么初始化」是追问率极高的细节题——背出「B 全零、A 高斯」只是及格线,讲清为什么才算过关。

先回顾 LoRA 的结构。

2021 年发表的 LoRA 论文提出的做法:冻结预训练权重 W₀,不直接学它的更新量,而是把更新量 ΔW 约束成两个低秩矩阵的乘积 B·A。A 是扁平矩阵负责把维度降到秩 r,B 是高瘦矩阵负责升维还原,前向时两路相加:h = W₀x + BAx。r 通常取 4 到 64,远小于原矩阵维度,可训练参数量因此下降一到两个数量级。推理时把 BA 合并回 W₀,不引入任何额外延迟——这是 LoRA 相对其他微调路线的招牌优势。

B 全零的第一层理由:训练的起点必须等于原模型。

训练第 0 步时 ΔW = B·A。B 全零意味着无论 A 取什么值,乘积恒为零矩阵,微调模型的初始输出与原预训练模型逐位一致。这一点为什么重要?预训练权重是海量语料训出来的,开局那一刻它就是全场最强的状态;微调要做的是在这个高起点上轻微挪动,而不是先打乱再找回。若 B 也随机初始化,ΔW 的起点就是一个随机非零矩阵,等价于先污染一遍预训练权重,训练初期要白白花步数修复人为引入的噪声,收敛变慢,极端数据量下还会拖累最终效果。

A 随机的第二层理由:打破对称,让梯度流得动。

那为什么不把 A 也置零、图个干净?因为那样训练直接卡死。看两个梯度的结构:B 的梯度正比于「上游梯度乘 Ax」,A 的梯度正比于「B 的转置乘上游梯度再乘 x」。若 A、B 全零,两个梯度都是零,参数永远不更新——这和深度网络全零初始化导致所有神经元功能等价、对称性无法打破是同一个病。正确的分工是:A 用高斯随机初始化,保证 B 的梯度里含有 A 的随机值,第一步 B 就能收到有效信号;A 的第一步梯度虽因 B 为零而落空,但 B 一旦更新成非零,第二轮起 A 的梯度也就通了。一个守起点,一个破对称,缺一不可。

用代码把这笔账算一遍。

下面这段程序用纯矩阵运算验证上面的推导:初始化后 ΔW 是否全零、起点输出是否与原模型一致、第一步两个梯度各自是否非零(Node.js 22.15.0,node lora.js):

function matMul(X, Y) {
  const Z = Array.from({ length: X.length }, () => Array(Y[0].length).fill(0));
  for (let i = 0; i < X.length; i++)
    for (let j = 0; j < Y[0].length; j++)
      for (let t = 0; t < Y.length; t++) Z[i][j] += X[i][t] * Y[t][j];
  return Z;
}
const r = 2, d = 4;
const gauss = () => Math.sqrt(-2 * Math.log(Math.random())) * Math.cos(2 * Math.PI * Math.random());
const A = Array.from({ length: r }, () => Array.from({ length: d }, gauss)); // 高斯随机
const B = Array.from({ length: d }, () => Array(r).fill(0));                  // 全零

const dW = matMul(B, A);
console.log('ΔW = B·A 是否全零:', dW.every(row => row.every(v => v === 0)));

const x = [1, -1, 0.5, 2], g = [1, 1, 1, 1];              // 输入与上游梯度
const Ax = A.map(row => row.reduce((s, w, j) => s + w * x[j], 0));
const gradB = g.map(gi => Ax.map(a => gi * a));           // ∂L/∂B = g·(Ax)ᵀ
const Btg = Array.from({ length: r }, (_, j) => B.reduce((s, row, i) => s + row[j] * g[i], 0));
const gradA = Btg.map(v => x.map(xj => v * xj));          // ∂L/∂A = (Bᵀg)·xᵀ
console.log('第一步 dL/dB 非零:', gradB.some(row => row.some(v => v !== 0)));
console.log('第一步 dL/dA 全零:', gradA.every(row => row.every(v => v === 0)));

实测输出:

ΔW = B·A 是否全零: true
第一步 dL/dB 非零: true
第一步 dL/dA 全零: true

三行输出对应三个论断:起点增量为零;B 先动;A 的梯度第一拍落空、随后被非零的 B 带动。把 B 也改成随机初始化再跑一次,ΔW 的 L2 范数会是一个明显的非零噪声量级,直观看到「起点被污染」。

深蓝黑背景概念图。顶部是 LoRA 前向通路图:输入 x 兵分两路,一路经灰色卡片「W₀ 冻结」直达加号,另一路经扁平矩阵 A(降维到秩 r)再高瘦矩阵 B(升维还原)汇入加号,合成输出 h。中部左侧一张初始化对照卡:A 行标注「高斯随机 N(0, σ²)」,理由是「打破对称,让 B 的梯度非零」;B 行以青色高亮标注「全零」,理由是「保证 ΔW = BA = 0,起点等价原模型」。中部右侧是四步推导链:B=0 则初始增量为零;前向输出逐位等于预训练模型;反向传播时 B 的梯度含 A 的随机值故非零;A 的初始梯度虽为零但第二轮起被 B 带动更新。底部总结条:B 置零守住起点、A 随机打破对称,两者缺一不可;另注秩 r 通常取 4~64,远小于原矩阵维度,可训练参数量因此下降一到两个数量级。 图 2:前向通路与初始化分工:A 破对称、B 守起点。

面试现场的答题结构。

先用一句话给出结论「A 高斯随机、B 全零」,然后分两层展开:B 置零保证 ΔW 起点为零、模型从原模型出发;A 随机打破对称、保证 B 的梯度非零。接着补两个反例——两个都零则梯度全零训不动,两个都随机则起点引入噪声。有余力再补工程事实:主流开源 PEFT 库的默认实现也是同一思路,A 用 Kaiming 均匀初始化、B 置零,让旁路在初始时是恒等变换,训练完成后可把 BA 合并回原权重消除推理延迟。答到「恒等变换起点」这一层,面试官就知道你是真懂而不是背答案。你自己跑 LoRA 时有没有试过改初始化方式?效果差了多少?欢迎聊聊。