LoRA(Low-Rank Adaptation)的主要思想是节约微调时需要调整的参数量,LoRA思想中使用两个低秩的矩阵来替代原需要微调的参数矩阵,假设原参数矩阵的维度为 W ∈ R ( M × N ) W \in R(M\times N) W∈R(M×N) , 那么可以使用两个秩为 r ( r ≪ m i n ( M , N ) ) r(r\ll min(M,N)) r(r≪min(M,N))的矩阵进行表示 A ∈ R ( M × r ) , B ∈ R ( r × N ) A\in R(M\times r), B\in R(r\times N) A∈R(M×r),B∈R(r×N), 则 Δ W = A ⋅ B \Delta W=A\cdot B ΔW=A⋅B
那么该如何初始化两个低秩矩阵呢?
在原始 LoRA 论文和主流实现中,两个矩阵的初始化方式如下:
1. 矩阵 A 的初始化
高斯分布初始化(Gaussian initialization):
- A 使用均值为 0,标准差为 1 / r 1/\sqrt r 1/r 的高斯分布随机初始化
- 数学表达式: A ∽ N ( 0 , 1 / r ) A \backsim N(0, 1/r) A∽N(0,1/r)
A = torch.nn.init.normal_(torch.empty(M, R), mean=0, std=1.0 / math.sqrt(R))
这种初始化方式遵循 He 初始化的思想,有助于在前向传播过程中保持方差稳定。
2. 矩阵 B 的初始化
零初始化(Zero initialization):
- B 初始化为全零矩阵
- 数学表达式:B = 0
B = torch.zeros(R, N)
为什么这样初始化?
这种初始化组合有几个重要的优点:
1. 训练初期保持原模型行为
由于 B 初始化为零矩阵,在训练开始时:
- ΔW = A·B = A·0 = 0
- 这意味着微调开始时,模型的行为与原预训练模型完全相同
- 微调过程可以被视为从预训练模型平滑过渡的过程
2. 稳定的训练过程
- A 使用缩放的高斯分布初始化,有助于保持梯度和激活值的方差稳定
- B 的零初始化避免了训练初期引入随机噪声
- 这种组合使得训练过程更加稳定,减少了发散的风险
3. 有效的梯度流动
- 尽管 B 初始化为零,但由于 A 非零,梯度仍然可以有效地流过这些参数
- 这种设计使得模型可以从预训练权重开始,逐渐学习必要的适应性变化
代码实现示例
以下是 LoRA 矩阵初始化的 PyTorch 实现示例:
import torch
import math
class LoRALayer(torch.nn.Module):
def __init__(self, in_features, out_features, rank=4, alpha=1.0):
super().__init__()
self.rank = rank
self.alpha = alpha
self.scaling = alpha / rank
# 创建 A 矩阵 (M x R)
self.A = torch.nn.Parameter(torch.empty(in_features, rank))
# 创建 B 矩阵 (R x N)
self.B = torch.nn.Parameter(torch.empty(rank, out_features))
# 初始化
self.reset_parameters()
def reset_parameters(self):
# A 初始化为高斯分布
torch.nn.init.normal_(self.A, mean=0, std=1.0 / math.sqrt(self.rank))
# B 初始化为零
torch.nn.init.zeros_(self.B)
def forward(self, x):
# LoRA 适配: x @ (A @ B) * scaling
return (x @ self.A @ self.B) * self.scaling
LoRA 的变体初始化方法
除了标准初始化方法外,还有一些变体和改进:
1. 使用 α 缩放因子
在许多 LoRA 实现中,会引入一个额外的缩放因子 α:
- 更新公式变为:ΔW = (α/R)·A·B
- 其中 α 是一个超参数,通常设置为 R 的倍数(如 α=R 或 α=2R)
- 这样可以控制 LoRA 更新的大小,而不改变初始化方差
# 应用缩放因子
delta_w = (alpha / R) * (A @ B)
2. SVD 初始化
一些研究提出使用奇异值分解(SVD)来初始化 LoRA 矩阵:
- 对预训练权重的变化估计进行 SVD: Δ W _ e s t ≈ U ⋅ Σ ⋅ V T ΔW\_est ≈ U·Σ·V^T ΔW_est≈U⋅Σ⋅VT
- 取前 R 个奇异值和对应的奇异向量
- 设置 A = U ⋅ Σ ( 1 / 2 ) A = U·Σ^{(1/2)} A=U⋅Σ(1/2)和 B = Σ ( 1 / 2 ) ⋅ V T B = Σ^{(1/2)}·V^T B=Σ(1/2)⋅VT
这种方法可能在某些任务上提供更好的起点,但计算成本更高。
3. 任务特定初始化
根据特定任务的需求调整初始化:
- 对于某些任务,可能希望一开始就有非零的 ΔW
- 可以根据任务先验知识初始化 B 为非零值
- 或者使用小规模任务特定数据预训练 LoRA 参数
不同框架中的实现
1. Hugging Face PEFT 库
在 Hugging Face 的 PEFT(Parameter-Efficient Fine-Tuning)库中,LoRA 的初始化遵循标准方法:
# 来自 PEFT 库的简化代码
def reset_lora_parameters(self, adapter_name):
if adapter_name in self.lora_A:
# 初始化 A 为高斯分布
nn.init.normal_(self.lora_A[adapter_name].weight, mean=0, std=1 / self.r)
# 初始化 B 为零
nn.init.zeros_(self.lora_B[adapter_name].weight)
2. Microsoft LoRA 实现
微软的原始 LoRA 实现也使用相同的初始化策略:
# 来自 Microsoft LoRA 的简化代码
def reset_parameters(self):
torch.nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5))
torch.nn.init.zeros_(self.lora_B)
实践建议
- 保持标准初始化:在大多数情况下,A 使用高斯初始化、B 使用零初始化是最佳选择
- 调整 R 和 α:
- 增大 R 可以提高模型容量,但会增加参数量
- 调整 α 可以控制 LoRA 更新的幅度,而不改变初始化方差
- 特定层的差异化处理:
- 对不同层使用不同的 R 值(如注意力层和 MLP 层)
- 对关键层使用更大的 R 值
- 监控训练稳定性:
- 如果训练不稳定,可以尝试降低学习率或 α 值
- 确保梯度裁剪以防止梯度爆炸
实践场景
Stable Diffusion,LLM 等
总结
LoRA 矩阵的标准初始化方式是:
- 矩阵 A:使用均值为 0,标准差为 1 / r 1/\sqrt r 1/r 的高斯分布初始化
- 矩阵 B:初始化为全零矩阵
这种初始化组合确保了:
- 训练开始时模型行为与原预训练模型一致(因为 ΔW = A·B = 0)
- 训练过程稳定,避免了随机噪声
- 梯度可以有效流动,使模型能够逐渐学习必要的适应性变化
通过这种设计,LoRA 能够在极少量参数的情况下有效适应下游任务,同时保持训练的稳定性和效率。
参考资料
- https://arxiv.org/pdf/2106.09685
– 关注公众号,持续更新:北北文的自留地