在当今人工智能领域,“预训练 - 微调” 已经成为大模型落地的核心范式 —— 就像先让一个人读完从小学到大学的通识课程(预训练),再根据他要从事的职业(下游任务)进行针对性培训(微调)。从 BERT、GPT 到 LLaMA、ChatGLM,几乎所有主流大模型都依赖这一模式:通过海量无标注数据完成预训练,掌握语言理解、逻辑推理等通用能力;再用少量标注的下游任务数据微调,让模型适配具体场景(如情感分析、医疗问诊、法律文书生成)。然而,传统的 “全参数微调”(Full-Parameter Fine-Tuning)在这一过程中暴露出越来越多的局限性,这些问题不仅拉高了大模型的使用门槛,更成为制约其在垂直领域普及的关键瓶颈。而 LoRA(Low-Rank Adaptation,低秩适配)技术的出现,就像为大模型微调装上了 “高效引擎”,其核心创新是利用 “预训练模型权重更新的低秩特性”,通过低秩矩阵分解替代全量权重更新,用 “四两拨千斤” 的思路解决了全参微调的核心痛点,彻底改变了大模型的应用生态。

Paper: LoRA: Low-Rank Adaptation of Large Language Models

全参微调: 看似全面却布满瓶颈的微调方式

全参微调的逻辑很直接:在微调阶段,不冻结预训练模型的任何参数,让所有权重都根据下游任务数据重新更新。这种方式的优势是能让模型 “深度适配” 任务,理论上能达到最优性能,但在实际操作中,它的局限性如同三座大山,让绝大多数开发者望而却步。

  1. 巨量显存消耗

    全参微调对显存的消耗堪称恐怖,这背后的核心原因是:训练过程中,显存不仅要存储模型本身的参数张量,还要存储反向传播时产生的梯度张量和优化器状态(如 Adam 优化器需要存储动量 $m$ 和二阶矩 $v$),这三者加起来的体积远超参数本身。我们可以用一个具体例子直观感受:以 70 亿参数(7B)的大模型为例,若采用单精度(32位浮点数, float32)存储,每个参数需要 4 字节空间,那么模型参数本身就需要 $7B \times 4 = 28GB$ 显存;反向传播时产生的梯度与参数维度完全一致,同样需要 28GB;而 Adam 优化器的状态是参数的 2 倍($m$ 和 $v$ 各占一份),即 56GB。仅这三项加起来就需要 28+28+56=112GB 显存,更别提训练时还需要存储中间激活值(前向传播中产生的临时计算结果)—— 实际训练时,7B 模型全参微调至少需要 140GB 以上的显存,消费级显卡根本无法负担,这无疑为大模型应用设立了较高的门槛。

  2. 训练效率低下

    全参微调的第二个致命问题是速度慢、效率低。因为它需要更新模型的每一个参数,参数越多,每次训练迭代的计算量就越大。依然以 7B 模型为例:假设使用单张 A100 显卡,全参微调时每秒能处理的 token 数约为 300-500 个,完成一个包含 100 万 token 的数据集训练,需要的时间超过 50 小时;如果换成 13B 模型,速度会降到每秒 150-200 个 token,训练时间直接翻倍至 4 天以上。而 175B 模型的全参微调,即便用 16 张 A100 组成的集群,也需要几周甚至几个月才能完成 —— 这样的速度,根本无法满足企业 “快速迭代” 的需求。

    效率低下还体现在任务切换成本高上。全参微调是 “一对一” 的:一个任务对应一个微调后的模型。如果企业需要同时适配多个任务(如上午做商品标题生成,下午做用户评价分析,晚上做售后工单分类),就必须为每个任务单独训练一个全参微调模型。每个模型都需要占用几十 GB 的存储空间,切换任务时还要重新加载模型,不仅浪费存储资源,还会因加载时间过长影响业务响应速度。更麻烦的是,若后续任务数据更新(如新增了 1000 条售后工单),又要重新进行一次全参微调 —— 这种 “重复劳动”,让大模型的落地效率大打折扣。

    此外,全参微调在分布式训练中还会面临 “通信瓶颈”。当使用多块显卡训练时,每块显卡需要将自己计算的梯度同步给其他显卡,而全参微调的梯度数据量极大,会导致显卡间的通信耗时远超计算耗时,最终出现 “显卡在等数据,而不是在算数据” 的尴尬局面,进一步拖慢训练速度。

  3. 过拟合风险

    全参微调的第三个痛点,是在下游任务数据量较少时容易过拟合。预训练模型就像一个 “知识面极广但不专精” 的通才,微调的目的是让它在某个领域 “变专”—— 但如果这个领域的 “教材”(标注数据)太少,通才就会陷入 “死记硬背” 的误区,把教材里的细节(甚至噪声)当成通用规律。为什么会这样?因为全参微调的 “自由度太高”。模型的参数越多,需要的训练数据就越多才能约束它的学习方向。当数据量不足时,大量参数没有足够的 “监督信号”,就会朝着 “拟合噪声” 的方向更新,最终导致模型 “学偏”。而在实际场景中,很多垂直领域(如法律、医疗、金融)的标注数据都非常稀缺:法律文书需要律师标注,每条成本超过 100 元;医疗病例涉及隐私,难以大规模收集;金融研报需要分析师整理,数量有限。这些场景下,全参微调要么因数据不足无法使用,要么训练出的模型无法落地,成为 “实验室里的花瓶”。比如在医疗领域,某医院想微调一个 “罕见病诊断辅助模型”,但由于罕见病病例稀少,只收集到 500 条标注数据。用全参微调时,7B 模型的 175 亿参数会 “拼命” 学习这 500 条数据的特征:比如某条病例里患者的年龄是 23 岁,模型会错误地认为 “23 岁” 是该疾病的关键特征;某条病例的描述里有 “头痛伴恶心”,模型会把 “恶心” 当成诊断的必要条件。结果就是,模型在训练集上的准确率能达到 95%,但在实际临床数据上的准确率却不足 60%—— 它记住了训练数据的 “表象”,却没学会疾病诊断的 “本质规律”。

LoRA: 四两拨千斤的低秩适配器

面对全参微调的三大瓶颈,微软团队在 2021 年提出的 LoRA 技术,给出了一套极具创新性的解决方案。它的核心思路可以概括为:不碰预训练模型的 “主体结构”,只给它装一个 “小外挂”(低秩适配器),通过训练这个 “外挂” 来适配下游任务。就像给一台旧电脑升级,不换主板、CPU 这些核心部件,只加一个小型外接显卡,就能大幅提升游戏性能 —— 既省钱又高效。

预训练模型在特定下游任务的低秩性质

LoRA 的诞生,源于一个关键的实验发现:预训练模型在微调时,权重的更新矩阵 $\Delta W$(即微调后权重 $W$ 与原权重 $W_0$ 的差值),具有低内在秩(Low Intrinsic Rank)特性。简单来说,虽然 ΔW 是一个 $d \times d$ 的大矩阵(d 是模型隐藏层维度,比如 768、4096),但它的有效信息可以用一个秩为 $r$ 的矩阵来表示 ——$r$ 远小于 $d$(通常取 8、16、32)。

LoRA 的低秩假设并非凭空提出,而是基于 “过参数化模型的内在低秩特性” 的理论启发,以及多维度实证实验的验证,证明 “权重更新矩阵$\Delta W$的低秩性” 是普遍且可利用的。

  • 过参数化模型的内在低秩特性:

    • 内在维度理论$^{[1]}$: 过参数化神经网络(如大语言模型)的有效学习空间具有低 “内在维度”,即便通过随机投影将特征维度降至原维度的 1%,模型仍能保持 90% 以上的性能 —— 这暗示模型适配任务时,仅需调整低维子空间的特征;

      内在维度是指在随机子空间中能够有效解决任务的最小维度。具体而言,通过在参数空间的随机子空间中逐步增加维度,并观察任务(如分类或强化学习)的解何时首次出现,这个临界点即为内在维度。例如,MNIST 图像分类任务的内在维度约为 750,而 CIFAR-10 约为 7,500,这表明后者的优化难度显著更高。

      即使模型参数数量远大于内在维度,额外参数主要用于扩大解流形的维度,而非提升任务性能。例如,不同规模的神经网络在同一数据集上的内在维度差异很小。通过内在维度可定量比较不同领域(监督学习、强化学习)的任务难度。例如,解决倒立摆问题的难度仅为 MNIST 分类的 1/100,而 Atari Pong 游戏的难度与 CIFAR-10 相当。内在维度分析为网络压缩提供了理论依据。例如,通过仅优化低维子空间中的参数,可将模型压缩超过 100 倍而保持性能。

    • 预训练模型的低秩更新$^{[2]}$: 预训练语言模型在适配下游任务时,权重更新的梯度矩阵具有低秩结构,原因是预训练已覆盖通用特征,任务适配仅需微调 “任务特异的少量方向”,这些方向构成低秩子空间。

      预训练语言模型在微调时的参数更新具有低内在维度。这意味着即使模型参数规模庞大,真正影响下游任务性能的有效参数数量远低于模型总参数量。例如,GPT-2 在微调时的内在维度可能仅为数百,而非其数千万参数。Aghajanyan et al. 通过随机子空间训练和参数噪声注入验证了低内在维度的存在。具体而言,在微调过程中向参数添加高斯噪声或均匀噪声,模型仍能保持鲁棒性,这表明许多参数对任务优化并非必要。

    • LoRA 的进一步假设: 大模型适配任务时,权重更新矩阵$\Delta W$的内在秩远小于其维度d,因此可用低秩分解 $B \times A$ 近似表示,且性能损失可忽略。

  • 实验验证:

    LoRA 通过三组核心实验, 从 “性能 - 秩关系”“子空间重叠度”“参数效率” 三个维度验证了低秩假设的有效性:

    1. 低秩可满足性能需求:

      在 GPT-3 175B 的 WikiSQL(SQL 生成)和 MNLI(文本蕴含)任务中,即使$r=1$(秩为 1),LoRA 的性能已接近全参微调:

      • WikiSQL 任务:$r=1$时准确率 73.4%,$r=4$时达 73.7%,继续增大r(如 64)性能无提升;

      • MNLI 任务:$r=1$时准确率 91.3%,$r=4$时达 91.7%,与全参微调(89.5%)相比反而更高。

      这说明$\Delta W$的有效信息可由极低秩的矩阵捕捉,高秩部分多为训练噪声,无实际作用。

    2. 低秩与高秩子空间高度重叠:

      通过子空间相似性度量(基于 Grassmann 距离的归一化相似度 $\phi$),对比 $r=8$ 和 $r=64$ 的 LoRA 子空间:

      对 $A_{r=8}$ 和 $A_{r=64}$ 做奇异值分解 (Singular Value Decomposition, SVD),取前i个右奇异向量 $U_{A_{r=8}}^i$ 和 $U_{A_{r=64}}^j$,相似度 $\phi = \frac{|U_{A_{r=8}}^i{}^\top U_{A_{r=64}}^j|_F^2}{\min(i,j)}$(取值 0-1,1 表示完全重叠);

      $r=8$ 的前 1 个奇异向量与 $r=64$ 的前 1 个奇异向量相似度 > 0.5,前 4 个奇异向量重叠度 > 0.3,说明低秩子空间已包含高秩子空间的核心信息,高秩部分未新增有效方向。

      奇异值分解是线性代数中最基础也最通用的矩阵分解方法之一,适用于任意维度的矩阵(包括向量,可视为 1 行或 1 列的特殊矩阵)。它的核心是将一个矩阵拆解为 “正交矩阵 - 对角矩阵 - 正交矩阵” 的乘积形式,从而揭示矩阵的核心结构(如秩、主要变换方向、缩放强度)。

    3. 低秩适配的参数效率远超其他方法:

      在 GPT-3 175B 的 SAMSum(对话摘要)任务中,LoRA(4.7M 参数)的 Rouge-L 达 45.1,超过 AdapterH $^{[3]}$(7.1M 参数,45.1)和 PrefixLayer $^{[4]}$(20.2M 参数,43.5),且参数规模仅为全参微调(175B)的 $0.0027\%$。

LoRA 算法设计

  • 符号定义:

    • 预训练权重矩阵: $W_0 \in \mathbb{R}^{d_{\text{out}} \times d_{\text{in}}}$, 其中 $d_{\text{in}}$ 为输入特征维度, $d_{\text{out}}$ 为输出特征维度.

    • 低秩矩阵 $A$: 维度为 $\left({d_{\text{in}}} \times r\right)$, 负责将输入特征从 $d_{\text{in}}$ 维投影到 $r$ 维低秩子空间.

    • 低秩矩阵 $B$: 维度为 $\left({d_{\text{out}}} \times r\right)$, 负责将低秩子空间特征从 $r$ 维投影到 $d_{\text{out}}$ 维.

    • 输入特征向量: $x \in \mathbb{R}^{d_{\text{in}}}$ (单个 token 在序列中的特征).

    • 缩放因子: $\alpha \in \mathbb{R}^{+}$ (正实数), 用于控制低秩更新的幅度.

  • 前向传播:

    LoRA 的核心是 “冻结预训练权重,通过低秩矩阵的乘积模拟权重更新”,其前向传播分为 3 步:

    1. 预训练模型隐藏层原始输出:

      未引入 LoRA 时, 模型对输入 $x$ 的输出仅由预训练权重 $W_0$ 决定: $h_0 = W_0 \cdot x$, 其中 $h_0 \in \mathbb{R}^{d_{out}}$ 是预训练模型的某隐藏层 (例如 q_proj 层) 所学习到的通用特征表示. 在 LoRA 微调训练中 $W_0$ 被冻结, 所以 $h_0$ 在训练过程中始终保持不变.

    2. LoRA 低秩适配器输出:

      LoRA 通过低秩矩阵 A 和 B 的乘积模拟 “任务特异的权重更新”,其输出为:$h_{lora} = (\frac {\alpha} {r}) \cdot (B \cdot A) \cdot x$:

      1. $A \cdot x$ 将输入 $x$ 从 $d_{in}$ 维投影到低秩子空间, 得到 $r$ 维特征: $A \cdot x \in \mathbb{R}^r$.

      2. $B \cdot (A \cdot x)$ 将低秩特征投影回 $d_{out}$ 维, 得到 $B \cdot A \cdot x \in \mathbb{R}^{d_{out}}$, 这一步等价于用低秩矩阵 $B \times A$ (维度 $d_{out} \times d_{in}$) 对 $x$ 进行线性变换.

      3. 乘以缩放因子 $\left(\frac {\alpha} {r} \right)$, 控制低秩更新的幅度.

    3. LoRA 模块的最终输出:

      LoRA 的最终输出是 “预训练原始输出” 与 “低秩适配器输出” 的叠加:

      \[h = h_0 + h_{lora} = W_0 \cdot x + \left(\frac {\alpha} {r} \right) \cdot (B \cdot A) \cdot x\]

    纵观 LoRA 算法总体, 其涉及到的超参数有 2 个: $r$ 和 $\alpha$:

    • $r$:

      $r$(秩,Rank)是 LoRA 最核心的超参数,定义了低秩子空间的维度,其决定了低秩矩阵 $B \times A$ 所覆盖的权重更新空间, $r$ 越大, 低秩子空间的维度则越高, 其能够学习到更复杂的特征模式, 但参数量也相应地会更大 (参数量为 $d_{in} \cdot r + d_{out} \cdot r$), 当 $r = min(d_{in}, d_{out})$ 时, $B \times A$ 能够覆盖任意 $d_{in} \times d_{out}$ 矩阵 (即 LoRA 适配器满秩), 此时 LoRA 等价于作用在特定模块上的全参微调.

    • $\alpha$:

      $\alpha$(缩放因子)是 LoRA 的 “稳定性调节器”, 其核心作用是平衡低秩更新的幅度, 避免因r变化导致训练不稳定.

      低秩适配器的输出 $h_{\text{lora}}$ 的 “强度” 由 $\left(\frac {\alpha} {r} \right)$ 决定:更大的 $\alpha$ (例如 $2r$) 会导致更强的参数更新 (适用于少样本场景); 而更小的 $\alpha$ (例如 $r/2$) 意味着更弱的参数更新 (适合需避免过拟合的场景)

实现 LoRA (基于 PyTorch)

代码源: deeplotx.nn.lora.py:

from typing_extensions import override

import torch
from torch import nn

from deeplotx.nn.base_neural_network import BaseNeuralNetwork


class LoRA(BaseNeuralNetwork):
    def __init__(self, input_dim: int, output_dim: int, rank: int = 8, alpha: int = 16,
                 dropout_rate: float = .0, model_name: str | None = None, device: str | torch.device | None = None,
                 dtype: torch.dtype | None = None):
        super().__init__(in_features=input_dim, out_features=output_dim, model_name=model_name,
                         device=device, dtype=dtype)
        self._rank = rank
        self._alpha = alpha
        self._scaling = self._alpha / self._rank
        self._dropout = nn.Dropout(p=dropout_rate) if dropout_rate > .0 else nn.Identity()
        self.lora_A = nn.Linear(in_features=input_dim, out_features=rank, bias=False,
                                device=self.device, dtype=self.dtype)
        self.lora_B = nn.Linear(in_features=rank, out_features=output_dim, bias=False,
                                device=self.device, dtype=self.dtype)
        nn.init.normal_(self.lora_A.weight, mean=.0, std=.01)  #  A 矩阵用均值为 0、标准差为 0.01 的正态分布初始化, 增量矩阵提供初始的随机扰动,作为学习的 "起点".
        nn.init.zeros_(self.lora_B.weight)  # B 矩阵初始化为全零矩阵, 保证 LoRA 模块在初始状态下不干扰预训练模型的输出.
        self.w0 = None

    @override
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        if not isinstance(self.w0, nn.Module):
            raise ValueError('LoRA adapter was not mounted successfully.')
        original_out = self.w0(x)
        lora_out = self.lora_B(self._dropout(self.lora_A(x))) * self._scaling
        return original_out + lora_out

    @staticmethod
    def apply_to(model: nn.Module, target_modules: list[str] | str, rank: int = 8, alpha: int = 16,
                 dropout_rate: float = .0) -> nn.Module:
        if isinstance(target_modules, str):
            target_modules = [target_modules]
        for layer_name, module in model.named_modules():
            if any(_name in layer_name.split('.')[-1] for _name in target_modules):
                lora = LoRA(input_dim=module.in_features, output_dim=module.out_features,
                            rank=rank, alpha=alpha, dropout_rate=dropout_rate,
                            device=next(module.parameters()).device,
                            dtype=next(module.parameters()).dtype)
                lora.w0 = module
                parent_name = layer_name.rsplit('.', 1)[0] if '.' in layer_name else ''
                child_name = layer_name.split('.')[-1]
                parent_module = dict(model.named_modules())[parent_name] if parent_name else model
                setattr(parent_module, child_name, lora)
        for param in model.parameters():
            param.requires_grad = False
        for name, param in model.named_parameters():
            if 'lora_A.weight' in name or 'lora_B.weight' in name:
                param.requires_grad = True
        return model

参考文献

[1] Chunyuan Li, Heerad Farkhoor, Rosanne Liu, Jason Yosinski. Measuring the Intrinsic Dimension of Objective Landscapes. arXiv preprint, 2018.

[2] Armen Aghajanyan, Luke Zettlemoyer, Sonal Gupta. Intrinsic Dimensionality Explains the Effectiveness of Language Model Fine-Tuning. arXiv preprint, 2020.

[3] Neil Houlsby, Andrei Giurgiu, Stanislaw Jastrzebski, Bruna Morrone, Quentin de Laroussilhe, Andrea Gesmundo, Mona Attariyan, Sylvain Gelly. Parameter-Efficient Transfer Learning for NLP. arXiv preprint, 2019.

[4] Xiang Lisa Li, Percy Liang. Prefix-Tuning: Optimizing Continuous Prompts for Generation. arXiv preprint, 2021.