一、为什么需要Lora训练?
在自然语言处理领域,预训练大模型(如LLaMA、GPT等)往往包含数十亿甚至千亿级参数。传统全参数微调方法需要更新整个模型的权重,这不仅需要巨大的计算资源,还会导致极高的存储成本。例如,微调一个100亿参数的模型需要约40GB*4的显存,这对普通开发者来说是难以承受的。
Lora(Low-Rank Adaptation)提出了一种巧妙的解决方案:冻结原始预训练模型的参数,仅通过低秩分解矩阵来模拟参数更新。这种方式将训练参数量减少到原模型的0.1%以下,同时保持模型性能几乎无损。
二、Lora原理详解
1. 低秩分解的数学基础
假设预训练模型的某个权重矩阵为。在微调时,传统方法直接更新W为W′=W+ΔW,而Lora则对增量矩阵ΔW进行低秩分解:
其中,r是远小于d和k的秩(通常为4~64)。这种分解将参数数量从d×k(原始梯度更新) 减少到r×(d+k)。例如当d=1024,k=1024,r=8时,参数量从1M减少到16K(仅为1.6%)。
2. 前向传播过程
修改后的前向计算为:
h=Wx+BAx
其中,B和A是可训练的矩阵,而W被冻结。在训练过程中,只有B和A的梯度被计算和更新。
3. 为何低秩有效?
预训练模型通常位于一个低维任务空间中。Lora假设参数更新也具有低秩性质,因此低秩分解能够有效捕捉核心特征。这与奇异值分解(SVD)中主成分的概念相似。
从Figure2也可以看出,LoRA训练的模型与全参微调的结果相差不大,甚至更优。这种既省显存,又不失性能的方法妥妥的真香。
4 参数初始化
- 增加表⽰能⼒ :随机初始化A可以确保它具有丰富的表⽰能⼒,使得低秩矩阵A × B能够探索⼴泛的权重调整空间。随机初始化提供了不同的起始点,从⽽能够在训练过程中捕捉更多的特征。
- 避免对称性问题 :如果A初始为零或相同的值,可能会限制模型在调整过程中学习到多样化的特征。随机初始化打破了这种对称性,使得矩阵 能够在训练过程中学到更复杂的特征组合。
- 提供有效的梯度信息 :随机初始化 可以防⽌梯度更新过程中出现梯度消失的问题。初始化时提供的⾮零梯度信息可以加速收敛,帮助模型更快地找到适应任务的最优参数。
通过将B初始化为零矩阵以及将A随机初始化,LoRA 保证了模型在微调开始时的稳定性,并能逐 渐引⼊权重更新。这种初始化⽅式确保了模型在训练初期不会出现性能波动,同时提供了⾜够的参数 ⾃由度来进⾏有效的任务适配,从⽽使 LoRA 在微调过程中能够实现⾼效且稳定的性能提升。
三、代码实现详解(基于PyTorch)
1. 定义Lora层
import torch
import torch.nn as nn
class LoraLayer(nn.Module):
def __init__(self, original_layer, rank=8, alpha=16):
super().__init__()
self.original_layer = original_layer # 原始预训练层(如nn.Linear)
self.original_layer.requires_grad_(False) # 冻结原参数
d, k = original_layer.weight.shape
self.lora_A = nn.Parameter(torch.randn(d, rank)) # 低秩矩阵A
self.lora_B = nn.Parameter(torch.zeros(rank, k)) # 低秩矩阵B
self.scaling = alpha / rank # 缩放系数,用于稳定训练
def forward(self, x):
orig_output = self.original_layer(x)
lora_output = x @ (self.lora_A @ self.lora_B).T * self.scaling
return orig_output + lora_output
2. 替换模型中的线性层
假设我们选择对Transformer中的query和value层应用Lora:
def apply_lora(model, rank=8):
for name, layer in model.named_modules():
if 'query' in name or 'value' in name: # 选择特定层
if isinstance(layer, nn.Linear):
# 用Lora层包装原始层
new_layer = LoraLayer(layer, rank=rank)
# 替换模块(需要处理层次结构)
parent_name = '.'.join(name.split('.')[:-1])
parent = model.get_submodule(parent_name)
layer_name = name.split('.')[-1]
setattr(parent, layer_name, new_layer)
return model
3. 训练过程
model = AutoModelForCausalLM.from_pretrained('llama-7b')
model = apply_lora(model, rank=8) # 应用Lora
optimizer = torch.optim.AdamW(
params=[p for p in model.parameters() if p.requires_grad],
lr=1e-4
)
for batch in dataloader:
outputs = model(**batch)
loss = outputs.loss
loss.backward()
optimizer.step()
optimizer.zero_grad()
4. 权重合并(部署时)
训练完成后,可将Lora权重合并到原模型中以便推理:
with torch.no_grad():
for name, layer in model.named_modules():
if isinstance(layer, LoraLayer):
# 合并权重 ΔW = B*A
delta_w = layer.lora_B @ layer.lora_A * layer.scaling
layer.original_layer.weight += delta_w.T # 更新原层参数
四、Lora的超参数
1、应用LoRA的权重
由Table5可知,在可训练的参数量一致的情况下,同时训练 和 的性价比最高。
2、超参数r的选择
由Table6可知,选择更大的r性能差别不大,因此选择更低的r性价比更高。
3、实践经验
-
秩(Rank
r
)。4~32 即可。 -
缩放因子(Alpha
α
)。典型设置:α = 2r。
-
目标模块(
target_modules
)。Transformer架构:优先选择query
和value
投影矩阵(实验显示调整这两个模块可获得95%的全量微调效果) -
学习率(
learning_rate
)。LoRA微调:建议1e-4~5e-4
- 3~5轮。