LoRA 微调原理:两个低秩矩阵改了模型的哪一层

把 7B 模型全参微调塞进单卡,第一步就挂在 optimizer 状态上。换 LoRA 之后能跑起来了,但 r 从 8 加到 32,loss 反而更抖。这个反差不是玄学,它指向 LoRA 微调原理里最容易被误读的部分:可训练参数变少,不代表更新方向一定干净。这篇会拆到矩阵维度、梯度路径和 merge 状态三个层面,看看到底哪些权重在变。

现象:训练 loss 一路降,推理时一换措辞就崩

最常见的情况是这样:用几百条客服对话做指令微调,训练集 loss 一路降,验证集看起来还行;但把"查订单"改成"帮我看看单子到哪了",模型输出就不稳定,甚至开始重复。触发条件并不复杂,就是数据量小、领域窄,同时基座模型对这个任务的先验不够。很多人第一反应是过拟合,于是加 dropout、减 epoch,结果反而把仅有的少量泛化能力也削掉了。

另一种现象更隐蔽:训练末尾保存了 adapter,但推理脚本只加载了 base model,没有加载 adapter。模型当然"像没训过"。可即使加载了 adapter,如果保存前没有把 dropout 切到 eval 模式,或者训练代码里只保存了部分模块,推理表现也会和最后一个 epoch 的验证输出不一致。这些都不是 LoRA 本身失效,而是它把模型分成了"冻结主权重 + 低秩增量"两部分,部署时要显式处理两份状态。

为什么会这样:低秩不是省显存的白嫖技巧

直接原因在于 LoRA 的假设本身。它假设全参微调产生的增量 ΔW 是低秩的,也就是说,大部分变化集中在少数几个方向上。这个假设在格式改写、语气迁移、小范围指令跟随上经常成立。可一旦任务要求模型建立新的 token 分布,或者植入它没见过的实体关系,实际 ΔW 的秩可能远大于 r。A 和 B 两个小矩阵只能在低秩空间里互相挤压,优化器找不到一个既能拟合训练集又能泛化的稳定点。坑在这里:低秩是表达力约束,不是单纯的显存优化。

另一个直觉误区是"r 调大点不就好了"。r 增大确实会增加可训练容量,但参数总量是 r×(d_in+d_out),显存线性上涨。更隐蔽的是,r 变大以后,同样的 alpha 会对增量产生不同的缩放。若只调 r 不调学习率或 alpha,训练可能更慢,loss 曲线更容易震荡。所以在动手之前,数据侧也要尽量干净,别让低秩空间浪费在格式漂移上。下面这段用基座模型把口头请求改写成固定 JSON,就是减少这类噪声。

python 复制代码
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ['SILVAMUX_API_KEY'],
    base_url='https://www.silvamux.com/api/v1',
)

resp = client.chat.completions.create(
    model='qwen3.5-flash',
    messages=[
        {'role': 'system', 'content': '把输入改写成 JSON 训练样本,字段只有 instruction 和 output。'},
        {'role': 'user', 'content': '查一下订单状态'},
    ],
    temperature=0.2,
)
print(resp.choices[0].message.content)

内部机制:两个矩阵、一个缩放因子和一条梯度路径

LoRA 不给原权重 W0 添加任何梯度。它在原权重旁边挂两个矩阵:A 的形状是 d_in×r,B 是 r×d_out。前向计算为 h = W0x + (alpha/r)BAx。A 通常用高斯或 Kaiming 初始化,B 用零初始化。为什么 B 必须从零开始?如果 B 随机初始化,训练第一步就会在输出上叠加一个不可控的低秩扰动,预训练模型已经学好的分布会被直接打乱。B 用零初始化后,初始增量严格为零,loss 起点等于基座模型本身。这个细节很关键。

缩放因子 alpha/r 才是最容易被误读的地方。它出现在前向公式里,不是学习率的替代品。很多人把 r 从 8 提到 64,却让 alpha 固定为 16,有效增量被压成原来的四分之一。等价地说,低秩分支的输出被削弱,优化器需要更大的学习率才能推动 A、B 产生同样幅度的更新。merge 时这个缩放仍在,不是只在训练时生效。有人把 alpha 当"低秩学习率"去调,越调越偏,原因就在这里。

训练时只有 A 和 B 接收梯度。反向传播从 loss 出发,经过 (alpha/r)BAx,只更新 A 和 B,W0 的参数不会累积 grad。因此 checkpoint 通常只存 adapter 的 state_dict,大小远小于全量权重。部署时有两种状态:合并模式把 W0 + (alpha/r)BA 直接写进原权重,推理路径不变;分离模式保留 adapter 分支,每次多算一次小矩阵乘法。合并后无法再原地解绑,想返回原模型必须提前备份主权重。这个状态转换在训练和部署之间经常被忽略。

边界条件:至少三种常规写法会失效的情况

第一,任务需要大量新事实。LoRA 的低秩约束决定了它更适合重组已有能力,而不是凭空写入新知识。如果基座模型不知道"某个订单状态码的含义",低秩增量必须在不大的子空间里硬造出映射。结果往往是模型背住了训练样本的措辞,换一种问法就答错。失效原因是秩不够,不是训练轮数不够。

第二,只给 attention 的 q 和 v 加 LoRA。很多默认配置只挂 q_proj 和 v_proj,这对一些格式型任务够用。但如果任务需要改变 MLP 中的信息加工方式,或者需要 k、o 配合,只改 q、v 就会在中间层形成瓶颈。训练后期 loss 停滞,不是学习率的问题,是模块没选对。失效原因是能力缺口和更新位置不匹配。

第三,量化基座与 merge 推理混用。4bit 或 8bit 模型保存的不是原始 W0,而是量化后的近似值。把 LoRA 增量 merge 回量化权重后再推理,量化误差会与低秩增量叠加,可能把 adapter 学到的变化吞掉。更稳妥的做法是保留 adapter 分离推理,不再对 merge 后的权重做一次量化。失效原因是两种近似叠加。

第四,alpha 与 r 的比值被无视。固定 alpha 只调 r,等于在修改前向缩放。这个前面已经说过,但它确实是训练脚本里最容易踩的边界。很多人复制默认配置,换一个 r 就开跑,曲线难看还怪数据。失效原因是缩放与容量耦合,不能独立调。

取舍结论:适合窄任务,不适合硬灌新知识

LoRA 真正舒服的场景,是任务缺口本身是低秩的:改变输出语气、统一格式、让模型按照某类结构回答、做小范围指令跟随。它的成本低,adapter 可以按任务拆分,方便 A/B 测试和回滚。代价是表达力有限,而且调试变量更多:r、alpha、目标模块、学习率、merge 状态,任何一个没对齐都会让结果跑偏。merge 后主权重被污染,回滚依赖备份。

如果任务需要植入大量新实体、新关系或者长链推理路径,LoRA 的低秩瓶颈会非常明显。此时不如换成更大基座,或者考虑全参微调和继续预训练。理解 LoRA 微调原理后,做决策就不是只看显存够不够,而是看任务缺口是不是真的能被低秩子空间兜住。这比省显存重要。

以上示例在千木SilvaMux的 OpenAI 兼容端点上验证,模型调用名为 qwen3.5-flash。文档参数以 silvamux.com/docs 为准。

相关推荐
桃西西呀1 小时前
LangGraph4j之七:子图与组合
人工智能·llm·ai编程
千木SilvaMux1 小时前
prompt injection 攻防为什么总在边界失效
llm·api
用户8314550980311 小时前
如一 Agent 架构解读(十九):错误的修养——工具报错是 Agent 唯一的自纠教材
人工智能·llm
桃西西呀1 小时前
LangGraph4j之六:消息状态与消息通道
人工智能·llm·ai编程
用户721746588261 小时前
Qwen-Image 2.1 的四条宣称拆成调用路径:透明挂在 edit 侧,164,919 与 301,400 两组像素读数
llm·aigc·agent
Aolith1 小时前
用 LLM 做简历分析,我踩过的坑比写过的代码还多
llm·全栈·deepseek
先吃饱再说1 小时前
大模型为什么总是“失忆”?—— 从零理解 Memory 模块的设计与实现
人工智能·llm·agent
goehou3 小时前
System Prompt 实战:从一段话到一份规格(附回归测试)
ai·llm·提示词·评测·system prompt
goehou4 小时前
模型网关实战:多模型路由、降级和成本对账的统一入口
网关·ai·llm·路由·成本