Skip to content

13.1 LoRA 原理

当模型参数从数十亿增长到数千亿,全参数微调(Full Fine-Tuning)的代价变得令人望而却步——不仅需要存储完整的模型参数副本,还要为每个参数维护梯度和优化器状态(Adam 的一阶矩与二阶矩通常是参数量的 4 倍以上)。一个自然的疑问由此产生:微调真的需要更新所有参数吗?

研究表明,尽管预训练模型的参数空间维度极高,但模型在适应特定下游任务时,权重的变化实际上位于一个**极低的内在维度(Low Intrinsic Dimension)**空间中。换言之,微调所需的"有效自由度"远小于参数总数。**LoRA(Low-Rank Adaptation)**正是基于这一洞察,通过低秩矩阵分解来近似权重更新,以极少的可训练参数达到接近甚至媲美全参数微调的效果。

本节将从数学原理出发,详细剖析 LoRA 的核心机制,并从零实现一个完整的 LoRA 层,随后介绍其主流变体 QLoRA、AdaLoRA 和 DoRA。


13.1.1 低秩分解的数学基础

在 Transformer 架构中,线性层无处不在。注意力机制中的 WqWkWvWo,以及前馈网络中的 WupWdown,都是标准的矩阵乘法:

h=W0x

其中 W0Rdout×din 是预训练权重,xRdin 是输入向量。全参数微调意味着直接更新整个 W0,权重变化量记为 ΔW

h=(W0+ΔW)x=W0x+ΔWx

LoRA 的核心假设是:ΔW 具有极低的内在秩。也就是说,虽然 ΔW 是一个 dout×din 的大矩阵,但它可以被分解为两个小矩阵的乘积:

ΔW=BA

其中 BRdout×rARr×dinrmin(dout,din)。于是,引入 LoRA 后的前向传播变为:

h=W0x+αrBAx

LoRA 低秩分解示意图:预训练权重  被冻结,旁路的低秩矩阵  和  是唯一的可训练参数

图 13-1:LoRA 的重参数化结构。左侧的预训练权重 W0 完全冻结,右侧并联一对低秩矩阵 A(高斯初始化)和 B(零初始化),仅训练这对矩阵。

这里的 α 是一个缩放超参数,用于控制 LoRA 旁路对原始输出的贡献强度。实践中通常将 α/r 作为整体缩放因子。当 α=r 时,缩放因子为 1,LoRA 的输出直接叠加在原始输出上;当 α<r 时,LoRA 的贡献被抑制,微调更加保守。

参数量对比。 全参数微调需要更新 dout×din 个参数,而 LoRA 仅需更新:

ParamLoRA=r×din+dout×r=r(din+dout)

以 Llama-7B 的注意力层为例,din=dout=4096,全参数微调需要 409621677 万个参数。取 r=8,LoRA 仅需 8×(4096+4096)=65536 个参数,压缩比超过 250 倍


13.1.2 初始化策略与训练机制

LoRA 的初始化策略看似简单,实则经过精心设计:

  • 矩阵 A:使用高斯随机初始化(Kaiming 或标准正态分布)
  • 矩阵 B:初始化为全零矩阵

这一设计的目的是确保训练起始时 BA=0,从而保证 LoRA 注入后模型的初始输出与预训练模型完全一致:h=W0x+0x=W0x。这避免了在训练初期引入随机噪声,防止破坏已经习得的预训练表征。

为什么不能两者都初始化为零? 如果 AB 都为零矩阵,那么所有梯度也为零——这是一个鞍点,模型无法从中逃逸,学习完全无法进行。

为什么零初始化的是 B 而非 A 研究表明,将靠近输出端的矩阵 B 初始化为零(标准方式)允许使用更大的学习率,并能实现更高效的特征学习。反向方式(A 零初始化、B 随机初始化)虽然训练稳定,但学习能力受限,效果次优。

训练流程。 LoRA 的训练过程非常直观:

  1. 冻结:将预训练模型的所有参数设为不可训练(requires_grad = False
  2. 注入:在目标线性层旁并联 LoRA 模块
  3. 训练:仅优化 AB 的参数,复用原有的训练数据和流程
  4. 合并:训练结束后,将 αrBA 加回 W0,部署时无额外开销

LoRA 在 Transformer 中的注入位置

图 13-2:LoRA 在 Transformer 中的适配位置。注意力层的 WqWkWvWo 和前馈网络的 WupWdown 均可注入 LoRA 旁路。原始权重(蓝色/雪花标记)被冻结,仅更新低秩矩阵(火焰标记)。

零推理延迟。 这是 LoRA 相对于 Adapter Tuning 和 Prefix Tuning 的关键优势。Adapter 在模型中串联插入额外层,增加推理延迟;Prefix Tuning 占用宝贵的上下文窗口。而 LoRA 的旁路结构允许在推理前将低秩矩阵合并回原始权重:

Wdeploy=W0+αrBA

部署后的模型结构与原始模型完全一致,不存在任何额外的推理开销。更进一步,不同任务可以共享同一个基模型,通过切换不同的 BA 矩阵来适配不同场景——这只需一次极轻量的矩阵加法操作。

多任务 LoRA 切换

图 13-3:LoRA 的模块化特性。同一个基模型可搭配多组针对不同任务微调的 LoRA 矩阵,实现即插即用的任务切换。


13.1.3 从零实现 LoRA

下面用 PyTorch 从零实现一个完整的 LoRA 层,并展示如何将其注入到现有模型中。

python
import torch
import torch.nn as nn


class LoRALayer(nn.Module):
    """LoRA 低秩适配层"""
    def __init__(self, in_features: int, out_features: int,
                 rank: int = 8, alpha: float = 16.0):
        super().__init__()
        self.rank = rank
        self.alpha = alpha
        self.scaling = alpha / rank  # 缩放因子

        # 矩阵 A:高斯初始化,将输入投影到低秩空间
        self.A = nn.Linear(in_features, rank, bias=False)
        nn.init.kaiming_uniform_(self.A.weight)

        # 矩阵 B:零初始化,将低秩空间映射回输出维度
        self.B = nn.Linear(rank, out_features, bias=False)
        nn.init.zeros_(self.B.weight)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        # B(A(x)) * scaling
        return self.B(self.A(x)) * self.scaling


class LinearWithLoRA(nn.Module):
    """将 LoRA 注入到标准线性层"""
    def __init__(self, linear: nn.Linear, rank: int = 8, alpha: float = 16.0):
        super().__init__()
        self.linear = linear  # 冻结的原始线性层
        self.lora = LoRALayer(
            linear.in_features, linear.out_features,
            rank=rank, alpha=alpha
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        # 原始输出 + LoRA 旁路输出
        return self.linear(x) + self.lora(x)

    def merge_weights(self):
        """将 LoRA 权重合并到原始权重中(部署时调用)"""
        with torch.no_grad():
            # W_deploy = W_0 + scaling * B @ A
            self.linear.weight += (
                self.lora.scaling
                * self.lora.B.weight @ self.lora.A.weight
            )
        self.lora = None  # 释放 LoRA 参数

将 LoRA 注入模型并计算参数量。 下面展示如何遍历模型、注入 LoRA,以及对比微调前后的参数量变化:

python
# 教学示例:展示核心逻辑,省略了部分 import 和辅助函数定义
def apply_lora(model: nn.Module, rank: int = 8, alpha: float = 16.0):
    """将 LoRA 注入模型中所有线性层"""
    for name, module in model.named_children():
        if isinstance(module, nn.Linear):
            # 冻结原始权重
            module.weight.requires_grad = False
            if module.bias is not None:
                module.bias.requires_grad = False
            # 替换为带 LoRA 的线性层
            setattr(model, name, LinearWithLoRA(module, rank, alpha))
        else:
            # 递归处理子模块
            apply_lora(module, rank, alpha)


# --- 使用示例 ---
class SimpleLLM(nn.Module):
    """简化的 Transformer 层,用于演示"""
    def __init__(self, d_model: int = 4096):
        super().__init__()
        self.q_proj = nn.Linear(d_model, d_model, bias=False)
        self.k_proj = nn.Linear(d_model, d_model, bias=False)
        self.v_proj = nn.Linear(d_model, d_model, bias=False)
        self.o_proj = nn.Linear(d_model, d_model, bias=False)

    def forward(self, x):
        q, k, v = self.q_proj(x), self.k_proj(x), self.v_proj(x)
        return self.o_proj(v)  # 简化,仅演示结构


model = SimpleLLM(d_model=4096)

# 注入前的参数量
total_before = sum(p.numel() for p in model.parameters())
print(f"注入前总参数量: {total_before:,}")  # 67,108,864

# 注入 LoRA(rank=8)
apply_lora(model, rank=8, alpha=16.0)

# 注入后的参数量统计
total_params = sum(p.numel() for p in model.parameters())
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
frozen = total_params - trainable

print(f"注入后总参数量: {total_params:,}")
print(f"可训练参数量 (LoRA): {trainable:,}")     # 262,144
print(f"冻结参数量: {frozen:,}")                  # 67,108,864
print(f"LoRA 参数占比: {trainable/frozen*100:.2f}%")  # 0.39%

输出如下,清晰展示了 LoRA 的极致参数效率:

注入前总参数量: 67,108,864
注入后总参数量: 67,371,008
可训练参数量 (LoRA): 262,144
冻结参数量: 67,108,864
LoRA 参数占比: 0.39%

仅用不到原始参数量 0.4% 的可训练参数,LoRA 就能让模型适应新任务。


13.1.4 秩的选择与实践经验

r 是 LoRA 最关键的超参数,它直接决定了低秩矩阵的表达能力。LoRA 原论文对不同秩的效果进行了系统实验,结果令人惊讶——即使 r=1,模型在某些任务上依然极具竞争力:

不同秩的实验结果

图 13-4:LoRA 在不同秩下的实验结果。即使 r=1,微调后的模型性能依然很强,这有力地证明了权重更新 ΔW 的内在秩极低。

实践中,秩的选择通常遵循以下经验法则:

场景推荐秩 r说明
简单分类/情感分析1 -- 8任务简单,低秩即可捕捉关键特征
通用 SFT16 -- 64适应指令跟随等中等复杂度任务
大规模 SFT(追求匹敌全参数微调)128 -- 256高容量适配,配合 target_modules="all-linear"
强化学习(GRPO/PPO)1 -- 32策略梯度每轮提取信息量有限,低秩足够

缩放因子 α 的设定。 α 通常设为 r 的 1 -- 2 倍。当 α=r 时缩放因子为 1,这是最常见的选择。也有研究(如 Rank-Stabilized LoRA / rsLoRA)建议将缩放因子设为 α/r 以获得更稳定的训练动态。

目标模块的选择。 研究表明,将 LoRA 应用到所有线性层(而非仅注意力层的 WqWv)能取得更好的效果,且增大秩并不能弥补"仅注入注意力层"所带来的性能差距。HuggingFace PEFT 库中可通过 target_modules="all-linear" 一键实现全线性层注入:

python
from peft import LoraConfig

peft_config = LoraConfig(
    r=16,
    lora_alpha=32,
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
    target_modules="all-linear",  # 注入所有线性层
)

13.1.5 LoRA 变体:QLoRA、AdaLoRA 与 DoRA

LoRA 的简洁设计为后续一系列变体奠定了基础。每一种变体都针对 LoRA 的某个局限性进行了改进。

QLoRA:量化 + LoRA。 标准 LoRA 虽然减少了可训练参数量,但基模型本身仍以 16 位精度加载,占用大量显存。QLoRA 的核心思想是将基模型权重量化到 4 位精度,再在其上叠加 16 位的 LoRA 适配器。

QLoRA 架构

图 13-5:Full Finetuning、LoRA 与 QLoRA 的显存分配对比。QLoRA 将基模型压缩至 4-bit,优化器状态可分页到 CPU,显存占用大幅降低。

QLoRA 引入了三项关键技术:

  1. NF4 量化(4-bit NormalFloat):利用预训练权重近似服从正态分布的特性,采用等概率分割(而非等步长分割)来设置量化刻度。在数据密集的零点附近量化点密集,在尾部则稀疏,实现了信息论意义上的最优表示。
  2. 双重量化(Double Quantization):对量化过程中产生的缩放因子再进行一次量化,将每参数平均位数从约 4.5 bit 进一步降低到约 4.1 bit。
  3. 分页优化器(Paged Optimizer):利用 NVIDIA 统一内存特性,将不在当前使用的优化器状态自动分页到 CPU,避免显存溢出。

QLoRA 成功地在单张 48GB GPU 上微调了 650 亿参数的模型,同时几乎保留了完整的 16 位微调性能。

AdaLoRA:自适应秩分配。 标准 LoRA 为所有适配层分配相同的秩 r——这忽略了不同层、不同模块的重要性差异。AdaLoRA 采用 SVD 形式的参数化 ΔW=PΛQ,通过动态评估每个奇异值分量的重要性,自动为关键模块分配更高的秩,为次要模块分配更低的秩。其训练流程分为三个阶段:

  1. 热身阶段:以高于目标的初始预算开始训练,让模型充分探索参数空间
  2. 预算削减阶段:按三次函数平滑地降低预算至目标值,基于重要性评分剪枝不重要的分量
  3. 微调阶段:固定预算,继续标准训练至收敛

AdaLoRA 用与 LoRA 相同的参数量,在多个基准测试上实现了性能超越。

DoRA:权重分解适配。 DoRA 从全新的视角审视 LoRA 与全参数微调的差距。通过将权重矩阵分解为**量级(Magnitude)方向(Direction)**两个分量,研究者发现了一个关键差异:

  • 全参数微调中,量级变化与方向变化呈负相关——模型可以灵活地独立调整二者
  • LoRA 中,二者呈强正相关——量级和方向被"捆绑"更新,限制了微调的灵活性

DoRA 分析:FT、LoRA 与 DoRA 的量级-方向关系对比

图 13-6:权重更新的量级变化 ΔM 与方向变化 ΔD 的关系。全参数微调(左)呈负相关,LoRA(中)呈正相关,DoRA(右)成功恢复了类似全参数微调的解耦特性。

DoRA 的解决方案是将量级和方向的更新显式解耦:

W=m可训练量级W0+BA可训练方向增量W0+BAc

DoRA 架构

图 13-7:DoRA 的工作流程。预训练权重被分解为量级向量 m 和方向矩阵 V。微调时,m 作为独立可训练参数学习量级变化,LoRA 低秩矩阵 BA 负责方向微调。

通过这种设计,DoRA 让低秩适配具备了接近全参数微调的灵活性,在多个任务上显著缩小了与全参数微调的性能差距。

下表总结了各变体的核心特性:

方法核心改进适用场景额外开销
LoRA低秩分解 ΔW=BA通用微调无推理延迟
QLoRA4-bit 量化基模型 + LoRA显存极度受限量化/反量化计算
AdaLoRASVD 参数化 + 自适应秩分配参数预算紧张时的精细调控重要性评分计算
DoRA解耦量级与方向更新追求接近全参数微调的性能额外的归一化计算

本节小结

  • LoRA 的核心思想是低秩分解:将权重更新 ΔW 分解为两个小矩阵的乘积 BA,仅训练这对矩阵,实现数百倍的参数压缩。
  • 初始化策略至关重要:A 高斯随机初始化、B 全零初始化,确保训练起始时不破坏预训练表征。
  • 缩放因子 α/r 控制 LoRA 旁路的贡献强度,r 决定低秩矩阵的表达能力。
  • LoRA 的零推理延迟特性(权重可合并)和模块化特性(多任务切换)使其成为大模型微调的事实标准。
  • QLoRA 通过量化进一步降低显存门槛,AdaLoRA 实现自适应秩分配,DoRA 解耦量级与方向更新——各变体针对不同需求提供了精准的优化方案。