LoRA: Low-Rank Adaptation of Large Language Models 笔记
问题背景
- ⼤模型通常包含数亿甚⾄数百亿个参数,对其进⾏微调需要⼤量的计算资源和存储空间。
- 在微调过程中,直接修改预训练模型的所有参数可能会破坏模型的原始性能。
- 存储和部署微调后的⼤模型需要⼤量存储空间,尤其是当需要在多个应⽤场景中部署不同微调版本时。
- 许多微调⽅法会增加推理阶段的计算延迟,影响模型的实时性应⽤。
LoRA
LoRA(Low-Rank Adaptation) 通过引⼊低秩矩阵分解,在减少计算资源和存储需求的同时,保持了预训练模型的初 始性能,稳定了微调过程,并降低了存储和部署成本。它特别适⽤于⼤规模模型的微调,在资源有限的环境中具有显 著的优势。
- 存储与计算效率:通过低秩适应(LoRA),可以显著减少所需存储的参数数量,并减少计算需求。
- 适应性与灵活性:LoRA⽅法允许模型通过只替换少量特定的矩阵A和B来快速适应新任务,显著提⾼任务切换的 效率。
- 训练与部署效率:LoRA的简单线性设计允许在不引⼊推理延迟的情况下,与冻结的权重结合使⽤,从⽽提⾼部署 时的操作效率。
在推理时,对于使用LoRA的模型来说,可直接将原预训练模型权重与训练好的LoRA权重合并,因此在推理时不存在额外开销。
原理
作用:这种初始化方法使得在训练初期,新增的部分△W=BA对原始权重Wpretrained的影响为零,从而不会破坏预训练模型的初始性能。
参数量计算
秩r << d,只有d×r + r×d参数需要训练,减少了计算梯度所需的内存和浮点运算量(FLOPS)。
假设我们有一个预训练的大模型,其中某个权重矩阵W的维度为d×d。
假设d=4096,即这个权重矩阵的尺寸为4096 x 4096。
原始权重矩阵W的参数数量为:d×d=4096×4096=16,777,216
使⽤ LoRA ⽅法
选择一个较小的秩r,例如r=16。在LoRA中,我们将权重矩阵分解为两个低秩矩阵A和B。
使用LoRA方法后,需要训练的参数数量为:
d×r + r×d=4096×16 + 16×4096
=131,072+131,072
=262,144
与原始模型相比,使用LoRA后的参数数量显著减少。
只需要训练原始参数数量的约1.56%。
应用于自注意力层
Q、K、V和O矩阵在信息传播和特征表示中起着关键作⽤:
-
查询与键的交互: Q和 K的交互决定了注意⼒分布,影响模型对输⼊序列的不同部分的关注度。
-
数值的加权求和: V矩阵通过加权求和操作,将注意⼒分布转化为具体的输出。
-
多头输出的整合: O矩阵整合多头注意⼒的输出,提供最终的特征表示。
LoRA通过将权重矩阵分解为两个低秩矩阵(例如W≈BA),减少了参数数量,降低了计算和存储成本,同时保持模型性能:
- 参数压缩:Q、K、V和O矩阵通常包含大量参数,LoRA的低秩分解显著减少了需要优化的参数数量。
- 性能保持:低秩矩阵能够捕捉到原始矩阵的主要信息,确保模型性能不受显著影响。
实现
变量:
import numpy as np
# 初始化矩阵 W, 预训练的模型参数
W = np.array([[4, 3, 2, 1],
[2, 2, 2, 2],
[1, 3, 4, 2],
[0, 1, 2, 3]])
# 矩阵维度
d = W.shape[0] # 4
# 秩
r = 2
# 随机初始化 A 和 B
np.random.seed(666)
# A 和 B 的元素服从标准正态分布
A = np.random.randn(d, r)
B = np.zeros((r, d))
# 定义超参数
lr = 0.01 # 学习率,用于控制梯度下降的步长。
epochs = 1000 # 迭代次数,进行多少次梯度下降更新。
函数:
# 定义损失函数
def loss_function(W, A, B):
'''
W:目标矩阵
A:矩阵分解中的一个矩阵,通常是随机初始化的。
B:矩阵分解中的另一个矩阵,通常是零矩阵初始化的。
'''
# 矩阵相乘,@是Python中的矩阵乘法运算符,相当于np.matmul(A, B)。
W_approx = A @ B
# 损失函数越小,表示 A 和 B 的乘积 W_approx越接近于目标矩阵 W
return np.linalg.norm(W - W_approx, "fro")**2
# 定义梯度下降更新
def descent(W, A, B, lr, epochs):
'''梯度下降法'''
# 用于记录损失值
loss_history = []
for i in range(epochs):
# 计算梯度
W_approx = A @ B
# 计算损失函数关于矩阵A的梯度
gd_A = -2 * (W - W_approx) @ B.T
# 计算损失函数关于矩阵B的梯度
gd_B = -2 * A.T @ ( W - W_approx)
# 使用梯度下降更新矩阵A和B
A -= lr * gd_A
B -= lr * gd_B
# 计算当前损失
loss = loss_function(W, A, B)
loss_history.append(loss)
# 每100个epoch打印一次
if i % 100 == 0:
print(f"Epoch: {i} , 损失: {loss:.4f}")
# 返回优化后的矩阵
return A, B, loss_history
运行
# 进行梯度下降优化
A, B, loss_history = descent(W, A, B, lr, epochs)
# 最终的近似矩阵
W_approx = A @ B
print(W_approx)
# 原始的矩阵 W
print(W)
QA
为什么需要低秩分解?
-
现代预训练模型虽然是过参数化的,但在微调时参数更新主要集中在⼀个低维⼦空间中。
-
参数更新 可以在低维度中进⾏优化,⾼维参数空间中的⼤部分参数在微调前后⼏乎没有变化。
-
低秩分解使参数优化更⾼效,但如果参数更新实际上在⾼维⼦空间中发⽣,可能会导致重要信息遗漏和LoRA⽅法 失效。
为什么初始化参数使⽤正态分布?
这样做的原因包括:
- 确保初始梯度的有效传播:正态分布初始化有助于在训练初期确保梯度有效传播,避免梯度消失或爆炸的问题。
- 提供⾜够的随机性:正态分布的随机初始化为模型提供了⾜够的随机性,从⽽能够探索更⼴泛的参数空间,增加了模型找到最优解的可能性。
- 平衡训练初期的影响:正态分布初始化的值⼀般较⼩,结合 B 初始化为零矩阵,可以在训练初期确保新增的偏置矩阵对原始预训练权重的影响为零,从⽽避免破坏预训练模型的初始性能。
为什么 A初始化服从正态分布?⽽B初始化为零矩阵?
- 如果B和A全部初始化为零矩阵,缺点是很容易导致梯度消失。
- 如果B和A全部正态分布初始化,那么在模型训练开始时,就会容易得到一个过大的偏移值△W,从而引起太多噪声,导致难以收敛。
参考
本文来自博客园,作者:漫漫长夜何时休,转载请注明原文链接:https://www.cnblogs.com/ag-chen/p/18431163