从零构建LLM 附录E:LoRA 低秩适配

📅 2026-03-02📖 ~8 min readLLMLoRAFine-tuning
本文是 Build a Large Language Model From Scratch(Sebastian Raschka 著)附录E的学习笔记。第6章用「冻结 + 换头」实现了分类微调,但如果想解冻更多层又怕过拟合或显存不够怎么办?LoRA (Low-Rank Adaptation) 提供了一个优雅的方案:不改变原始权重,而是给每个线性层加一个极小的「旁路」,只训练这个旁路。在 GPT-2 上,LoRA 只需训练 2.6M 参数(原模型的 2.1%),却能达到甚至超过全量微调的效果。
← Ch7: 指令微调

1. LoRA 的核心思想

全量微调 (Full Fine-tuning) 的问题很直接:124M 参数的 GPT-2 Small 还好,但 7B、70B 的模型呢?微调所有参数需要巨大的显存和计算资源。

LoRA 的洞察是:微调时的权重变化量 ΔW 是低秩的 — 也就是说,虽然 ΔW 是一个大矩阵,但它的「有效维度」远小于它的实际维度。既然如此,就可以用两个小矩阵的乘积来近似它。

LoRA concept

LoRA 的核心思想:将权重更新分解为两个低秩矩阵的乘积 (图源: LLMs from Scratch)

原来需要更新的权重:Wupdated = W + ΔW(其中 ΔW 是 d×d 的矩阵)

LoRA 的做法:Wupdated = W + A · B(其中 A 是 d×r,B 是 r×d,r « d)

ℹ️ 参数量对比:一个 768×768 的权重矩阵有 589,824 个参数。如果 rank=8,LoRA 只需要 A(768×8) + B(8×768) = 12,288 个参数 — 仅为原来的 2%。而且原始权重 W 完全冻结,不需要计算梯度,显存开销大幅降低。


2. 低秩分解:用两个小矩阵代替一个大矩阵

LoRA 的初始化和缩放有讲究:

  • 矩阵 A:用 Kaiming 均匀初始化(保证初始输出有合理方差)
  • 矩阵 B:全零初始化 — 这确保训练开始时 A·B = 0,LoRA 的旁路不会干扰原始模型
  • 缩放因子alpha / rank,用来控制 LoRA 更新的幅度。alpha 通常等于 rank
LoRA matrix decomposition

LoRA 将 ΔW 分解为 A·B 两个低秩矩阵 — B 初始化为零确保起点等价于原模型 (图源: LLMs from Scratch)

计算过程:output = W·x + (alpha/rank) · A·B·x。原始线性层的前向传播不变,LoRA 只是额外加了一个低秩修正项。


3. 代码实现

书中将 LoRA 实现为三个组件:LoRALayer(核心低秩层)、LinearWithLoRA(包装器)、replace_linear_with_lora(全局替换函数)。

Python — LoRALayer + LinearWithLoRA
class LoRALayer(nn.Module):
    def __init__(self, in_dim, out_dim, rank, alpha):
        super().__init__()
        # A: Kaiming 初始化(保证方差合理)
        self.A = nn.Parameter(torch.empty(in_dim, rank))
        nn.init.kaiming_uniform_(self.A, a=math.sqrt(5))
        # B: 零初始化(确保初始 LoRA 输出为零)
        self.B = nn.Parameter(torch.zeros(rank, out_dim))
        self.alpha = alpha

    def forward(self, x):
        # 低秩修正:x → A → B,缩放后加到原始输出
        return self.alpha * (x @ self.A @ self.B)

class LinearWithLoRA(nn.Module):
    def __init__(self, linear, rank, alpha):
        super().__init__()
        self.linear = linear  # 原始线性层(冻结)
        self.lora = LoRALayer(
            linear.in_features, linear.out_features, rank, alpha
        )

    def forward(self, x):
        # 原始输出 + LoRA 修正
        return self.linear(x) + self.lora(x)

有了这两个类,接下来只需要一个函数遍历模型,将所有 nn.Linear 替换为 LinearWithLoRA

Python — 全局替换线性层
def replace_linear_with_lora(model, rank, alpha):
    """递归遍历模型,将所有 nn.Linear 替换为 LinearWithLoRA"""
    for name, module in model.named_children():
        if isinstance(module, nn.Linear):
            setattr(model, name,
                    LinearWithLoRA(module, rank, alpha))
        else:
            # 递归处理子模块
            replace_linear_with_lora(module, rank, alpha)

# 使用:先冻结原模型所有参数,再注入 LoRA
for param in model.parameters():
    param.requires_grad = False

replace_linear_with_lora(model, rank=8, alpha=8)

# 只有 LoRA 参数是可训练的
total_params = sum(p.numel() for p in model.parameters())
trainable_params = sum(p.numel() for p in model.parameters()
                       if p.requires_grad)
print(f"Total: {total_params:,}")       # 124,441,346
print(f"Trainable: {trainable_params:,}")  # 2,666,528 (2.1%)
print(f"Ratio: {trainable_params/total_params:.1%}")

4. 应用到 GPT 分类微调

书中在第6章的垃圾邮件分类任务上对比了三种微调策略:

方法 可训练参数 训练准确率 测试准确率
全量微调 (Ch6, 全部解冻) 124.4M (100%) 99.52% 95.67%
冻结 + 换头 (Ch6, 只解冻最后层) ~7.4M (~6%) 97.21% 95.67%
LoRA (rank=8) 2.7M (2.1%) 99.81% 96.67%
LoRA training results

LoRA 微调的训练和验证损失曲线 (图源: LLMs from Scratch)

结果令人印象深刻:LoRA 用仅 2.1% 的可训练参数,不仅匹配了全量微调的测试准确率 (96.67% vs 95.67%),训练准确率还更高 (99.81%)。这证明了微调时的权重变化确实是低秩的 — 不需要更新所有参数就能达到优异效果。

⚠️ LoRA 推理时可以「合并」:训练完成后,可以将 LoRA 权重合并回原始权重:W_merged = W + alpha * A @ B。合并后的模型结构和原始模型完全一样,推理速度零开销。这意味着可以为不同任务训练不同的 LoRA 适配器,按需合并,不增加推理成本。


5. 为什么这很重要

LoRA 是目前最主流的大模型微调技术之一。以下是它为什么如此重要:

  • 显存效率:全量微调需要存储所有参数的梯度和优化器状态。对于 7B 模型,这意味着约 56GB 显存。LoRA 只需要为 2% 的参数存储这些状态,显存需求降低到约 16GB — 一张消费级 GPU 就能搞定。
  • 多任务部署:可以为同一个基座模型训练多个 LoRA 适配器(翻译、摘要、代码生成等),每个只有几十 MB。推理时动态加载不同的适配器就能切换任务,无需部署多个完整模型。
  • QLoRA:LoRA 可以和量化 (Quantization) 结合使用。QLoRA 将基座模型量化到 4-bit,LoRA 适配器保持 16-bit,进一步将 7B 模型的微调显存降到约 6GB。
  • rank 的选择:rank 越高,LoRA 的表达能力越强但参数越多。书中用 rank=8 就达到了优异效果。实际使用中 rank=4~32 是常见范围。对于复杂任务(如代码生成),可能需要更高的 rank。
  • Hugging Face PEFT:实际工作中通常使用 Hugging Face 的 PEFT 库,它提供了 LoRA 的标准实现和更多变体(如 AdaLoRA、IA3 等),无需从头实现。

ℹ️ 总结 — LoRA 的工作流程:加载预训练模型 → 冻结所有原始权重 → 为每个线性层注入 LoRALayer (A·B 旁路) → 只训练 LoRA 参数 (约 2% 的参数量) → 训练完成后可选择合并回原始权重(推理零开销)。LoRA 的成功验证了一个深刻的直觉:大模型微调时的参数变化存在于一个低维子空间中,不需要「全面修改」就能实现任务适配。

本文是 Build a Large Language Model From Scratch (Sebastian Raschka) 的学习笔记。所有配图版权归原作者所有。代码基于原书示例,有简化和中文注释。

← Ch7: 指令微调

Related Posts