大数跨境
0
0

图解大模型微调系列之:AdaLoRA,能做“财务”预算的低秩适配器

图解大模型微调系列之:AdaLoRA,能做“财务”预算的低秩适配器 极市平台
2023-09-18
0
↑ 点击蓝字 关注极市平台
作者丨猛猿
来源丨大猿搬砖简记
编辑丨极市平台

极市导读

 

无惧数学公式,图解AdaLoRA,简单明了搞懂原理和代码实现。 >>加入极市CV技术交流群,走在计算机视觉的最前沿

在“大模型微调系列”之前的文章中,我们介绍过LoRA的原理和代码实现。今天,我们就来看LoRA的改进版本:AdaLoRAAdaptive Low-Rank Adaptation)的原理。

我猜,如果你曾读过AdaLoRA的原始论文,是不是曾被满屏的数学公式劝退过🐶。其实,AdaLoRA这篇论文写得非常非常好,它的数学符号简洁明了地表达出了算法的全部细节,而背后的数学原理(主要是SVD分解)其实一点也不复杂。毕竟大家都说,深度学习的数学怎么能叫....

所以,大家不要怕这些数学表达符,本文会通过图解的方式做详细说明。

全文目录如下:
一、AdaLoRA在做一件什么事
1.1 LoRA是怎么做微调的
1.2 对所有模块都采用一个秩,是否合理
1.3 微调全程保持秩不变,是否合理
1.4 AdaLoRA总体改进目标

二、AdaLoRA的原理
2.1 SVD分解
2.2 让模型学习SVD分解的近似
2.3 图解AdaLoRA动态变秩的过程

三、AdaLoRA的loss设计四、AdaLoRA的重要性分数
4.1 单参数重要性分数
4.2 改进版单参数重要性分数
4.3 三元组重要性分数
五、动态调整矩阵的秩
5.1 调整函数
5.2 top_b策略

六、AdaLoRA训练流程总结(必看)

七、参考

一、AdaLoRA在做一件什么事

1.1 LoRA是怎么做微调的

假设原始参数为 , 全参数微调后对原始参数的改变量为 。在LoRA中, 我们用两个矩阵的乘积 来 近似 做SVD分解后的结果, 则模型的输出结果满足:

其中, r就是我们设定的矩阵的秩, 在微调过程中, 所有做lora适配器的module, 它们的r都是一致的, 且在训练过程中不会改变。在LoRA原始论文中, 作者最终选择对attention模块的 做低秩适配。

好,那么现在,问题来了:

  • 对所有模块都采用同一个秩r,这是合理的吗?
  • 在微调过程中,一直保持秩r不变,这是合理的吗?

我们来分别细看这两个问题。

1.2 对所有模块都采用同一个秩,是否合理

这个问题,早在AdaLoRA之前,LoRA的作者就意识到了,并做了如下实验:

图中亮色表示对应模块的intrinsic rank,从中可以发现, 的intrinsic rank要大于 因此LoRA的作者在这里就挖了个可以让后人来填的坑:不同的模块可以用不同的秩。

所以到了AdaLoRA这里,它的作者也做了两个实验:

实验(a)对应着左图,作者使用LoRA,在模型的每个layer中微调特定的某个模块,然后去评估模型的效果。可以直观发现,微调不同模块时效果差异显著。

实验(b)对应着右图,作者使用LoRA,对模型不同layer的所有模块做微调。例如1,2,3表示对模型前三层的所有模块做lora微调。可以发现,微调不同layer时模型效果差异显著,微调最后三层的效果最好。

这些实验都证明了一件事:在使用LoRA微调时,对模型的不同模块使用相同的秩,显然是不合理的

1.3 微调过程中一直保持秩不变,是否合理

对于这点,作者并没有做实验来说明,但我们可以从1.2的分析中得到一些思路。

在1.2中,我们通过实验证明“对不同模块采用不同秩”的必要性,那么接下来势必就要解决“每个模块的秩到底要设成多少”的问题。解答这个问题最直接的办法,就是交给模型去学。那模型学习的过程,肯定是个探索性的过程呀,理想情况下,你给模型一个初始化的秩,然后让它在训练过程中,学会慢慢调整这个秩,直到最优。 所以,在微调step的更新过程中,秩也不会保持不变。

1.4 AdaLoRA的总体改进目标

好,到此为止,AdaLoRA的总体改进目标就出来了:

找到一种办法,让模型在微调过程中,去学习每个模块参数对训练结果(以loss衡量)的重要性。然后,根据重要性,动态地调整不同模块的秩。

作者管这样的策略叫参数预算(parameter budget),作为一个学财会出身的人,这真是非常形象了。待训练的参数越多,训练代价越大,因此做好参数预算方案,集中训练最重要的那些参数,ROI才会越高。

二、AdaLoRA的原理

2.1 SVD分解

由于AdaLoRA重度依赖SVD分解原理,因此我把LoRA原理篇4.2节中的内容再copy一次。

如图, 矩阵 是我们需要做信息量检查的矩阵。假设在输入数据的特征空间中, 存在一组正交的单位向量 , 经过 的变换后, 它们变成另一组正交向量 , 其中 也是一组正交的单位向量, 分别表示对应方向上的模。上面这一顿变幻, 可以写成:

稍加改写, 就有:

不难发现, 中隐含了对“信息量”的提示。在本例中 经过 的转换投射到 上时, 强调了在 1 方向上蕴含的信息。

现在再宽泛一些, 如果我们能找到这样的一组 , 并令 矩阵的值从大到小进行排列, 那么我们不就能对 进行拆解,同时在拆解过程中,找出 所强调的那些特征方向了吗? 也就是说:

当我们找到这样的 矩阵后,我们再从这三者中取出对应的 top 行 (或列),不就相当于关注到 了 最强调的那几维特征, 进而就能用更低维的矩阵, 来近似表达 了? 按这种思维拆解 的方法, 我们 称为SVD分解(奇异值分解)。在本篇里我们不讲述它的具体方法,感兴趣的朋友们参考《线性代数》

我们再通过一个代码例子,更直观地感受一下这种近似,大家注意看下注释(例子改编自:https://medium.com/@Shrishml/lora-low-rank-adaptation-from-the-first-principle-7e1adec71541)

import torch
import numpy as np
torch.manual_seed(0)

# ------------------------------------
# n:输入数据维度
# m:输出数据维度
# ------------------------------------
n = 10
m = 10

# ------------------------------------
# 随机初始化权重W
# 之所以这样初始化,是为了让W不要满秩,
# 这样才有低秩分解的意义
# ------------------------------------
nr = 10
mr = 2
W = torch.randn(nr,mr)@torch.randn(mr,nr)

# ------------------------------------
# 随机初始化输入数据x
# ------------------------------------
x = torch.randn(n)

# ------------------------------------
# 计算Wx
# ------------------------------------
y = W@x
print("原始权重W计算出的y值为:\n", y)

# ------------------------------------
# 计算W的秩
# ------------------------------------
r= np.linalg.matrix_rank(W)
print("W的秩为: ", r)

# ------------------------------------
# 对W做SVD分解
# ------------------------------------
U, S, V = torch.svd(W)

# ------------------------------------
# 根据SVD分解结果,
# 计算低秩矩阵A和B
# ------------------------------------
U_r = U[:, :r]
S_r = torch.diag(S[:r])
V_r = V[:,:r].t()

B = U_r@S_r # shape = (d, r)
A = V_r     # shape = (r, d)

# ------------------------------------
# 计算y_prime = BAx
# ------------------------------------
y_prime = B@A@x

print("SVD分解W后计算出的y值为:\n", y)

print("原始权重W的参数量为: ", W.shape[0]*W.shape[1])
print("低秩适配后权重B和A的参数量为: ", A.shape[0]*A.shape[1] + B.shape[0]*B.shape[1])

输出结果为:

原始权重W计算出的y值为:
 tensor([ 3.3896,  1.0296,  1.5606, -2.3891, -0.4213, -2.4668, -4.4379, -0.0375,
        -3.2790, -2.9361])
W的秩为:  2
SVD分解W后计算出的y值为:
 tensor([ 3.3896,  1.0296,  1.5606, -2.3891, -0.4213, -2.4668, -4.4379, -0.0375,
        -3.2790, -2.9361])
原始权重W的参数量为:  100
低秩适配后权重B和A的参数量为:  40

参数量变少了,但并不影响最终输出的结果。通过这个例子,大家是不是能更好体会到低秩矩阵的作用了呢~

2.2 让模型学习SVD分解的近似

从2.1中, 我们知道SVD分解后, 会将原矩阵 拆成 这样的形式, 其中 都是正交矩阵, 即 满足 。以 为二维矩阵为例, 这个SVD分解的式子长成下面这样:

LoRA的总体设计思想, , 也就是让模型去学习两个矩阵A和B, 用来近似SVD分解的结果, 同时将A和B的秩都统一设成

到了AdaLoRA这里, 就有新方法了, 我让模型去学习三个权重矩阵: , 直接去近似 真实的 SVD 分解结果, 也就是 , 这样也能达到目的。

2.3 AdaLoRA动态更新秩的过程

接下来,我们来仔细端详一下 。下图中描绘了AdaLoRA动态变秩的过程:

AdaLoRA变秩的整体流程如下:

(1)首先,我们初始化三个矩阵 。其中, 矩阵比较特殊, 其大部分元素为 0 , 只有对角线上的 个元素有值。所以实操中我们可将其视为长度为 的向量, 即

初始化时, 我们 初始化为 初始化为高斯随机矩阵。 这样做的目的和LoRA一样, 都是为了在训练 开始保证 是 0 , 以此避免引入噪声。

(2)然后,我们正常做forward和backward, 得到Loss和参数的梯度。(Loss的设计我们在后文细说)

(3)根据Loss和参数梯度, 我们可以对图中所示的每个三元组(triplets) 计算重要性分数 (importance scoring), 其中, 分别表示“第i列”和“第i行”。(重要性分数的计算方法我们在后文细说)

(4)根据计算出来的重要性分数, 我们将不重要的三元组挑选出来。(根据重要性分数篎选三元组的方法, 我们在后文细说)。

(5)对于不重要的三元组, 我们将其 值置0。 这样, 在下一次做forward时, 这个三元组里对应的P向量和Q向量相当于被mask掉了, 对Loss没有贡献。也就起到了变秩的效果。

(6)使用(2)中计算出来的梯度, 更新 的参数。

(7)然后, 使用更新完毕的 , 开启新一轮forward和backward, 重复上面步骤, 随时动态更新参数 的秩。

需要特别说明的是,为什么在(5)中,我们只是将 置0,而不是把整个三元组删掉呢? 因为前面说过,模型的学习是一个探索的过程,在一开始模型认为不重要的三元组,在后续过程中模型可能会慢慢学到它的重要性。因此,mask是比删掉更合理的方法。 也正是这个原因,我们在(6)中,不管三元组有没有被mask掉,我们都会正常用梯度更新P和Q。

好,理清了整体流程后,我们就可以来看细节了,在上述过程里,我们遗留了3个细节问题有待探讨:

  • AdaLoRA中,Loss要怎么设计?
  • AdaLoRA中,重要性分数要怎么算?
  • AdaLoRA中,如何根据重要性分数筛选不重要的三元组,进而动态调整矩阵的秩?

我们来分别细看这三个问题。

三、AdaLoRA的Loss设计

在第二部分中,我们以某一模块的更新做了举例,但是在实际应用中,需要做AdaLoRA适配的模块肯定不止一个假设我们有n个模块需要做AdaLoRA适配,现在我们来严谨定义一些数学表达符号

  • , 表示第 个需要做AdaLoRA适配的模块。其中 表示共有 个模块需要做 AdaLoRA适配
  • , 表示第 个模块的第 三元组。注意上文中的 表示。(怪我,画完图才发现这个gap, 但是不想改图了, 大家能理解就行)
  • : 表示第 个模块的第 个三元组的重要性分数
  • : 表示所有 个模块的P矩阵的集合
  • : 表示所有 个模块的 矩阵的集合(实操中不是矩阵, 是 维向量, 前文说过)
  • : 表示所有 个模块的 矩阵的集合
  • : 表示模型在训练集上的损失函数
  • : 表示模型最终的损失函数

细心的你可能已经发现了,这怎么有两个损失函数呢?那我们就直接来看最终损失函数的定义:

其中,

好理解, 它表示预测结果和真实标签间的差异。 那后面那项又是什么呢? 还记得我们在 2.1 中提过SVD分解相关的定义吗: 都必须是正交矩阵, 即满足

而在AdaLoRA中, 我们是寄希望于模型学出的 能满足这个性质, 所以在设计Loss时我们当然也要考虑它:当P和 偏离这个性质太远时, 我们在Loss中给予相应的惩罚, 就是惩罚力度, 也被称为正则项。

其实,这也是AdaLoRA相比于LoRA效果能更好的原因之一:LoRA中是让模型学习BA,去近似SVD分解的结果,但是在训练过程中,没有引入任何SVD分解相关的性质做约束,所以模型就可能学歪了(因此LoRA作者在文章中写了很多实验,证明学出来的BA在一定程度上能近似SVD分解,能取得较好的效果)。而AdaLoRA则是直接将这一束缚考虑到了Loss中。

四、AdaLoRA中的重要性分数

我们依然先明确几个数学表达:

  • : 表示第 个模块的第 个三元组的重要性分数。
  • : 表示第 个模块的 矩阵(再强调一下, 实操中不是矩阵是向量)的第 个元素。
  • : 表示第 个模块的P矩阵的第j行第i列个元素。
  • : 表示第 个模块的 矩阵的第 行第j列个元素。
  • .
  • : 表示模型的损失函数。

4.1 单参数重要性分数

在开始正式讲重要性分数怎么算之前,我们先来看一个问题:到底什么是重要性分数?

我们在1.4中说过,AdaLoRA的整体目标是要做参数预算(budget),也就是忽略不重要的参数,然后把训练资源给重要的参数,在AdaLoRA中,我们是通过“变秩”来实现这种预算的动态分配的。所以现在问题就变成:如何判断一个矩阵中的一个参数(我们将其表示为 )对模型训练是否重要?

一个最直观的想法就是:去看看这个参数对Loss的影响。所以,在前人的研究中,就提出过 “梯度*参数”(gradient-weight product)

这种算法:这种算法的设计思想非常直接:我去看看Loss在这个参数上的梯度,同时也考虑一下这个参数本身的大小,不就能综合判断出这个参数对模型的影响程度了吗?

4.2 改进:单参数重要性分数

4.1中的这个直观有效的想法,被广泛运用在前人做的参数剪枝的优化中,但它有一个显著的问题:我是在mini-batch上计算重要性分数的,不同step间重要性分数可能会受到mini-batch客观波动的影响,有啥办法能消除这种影响吗?

当然有啦,遇到这种消除波动的问题,我们肯定要祭出momentum🐶。

所以,AdaLoRA作者就提出了这样一种计算t时刻,单个模型参数重要性的方法

其中:

  • 表示根据4.1中的公式, 计算出的 时刻的单参数重要性
  • 表示前 个时刻该单参数重要性的平滑值
  • 表示当前值与平滑值之间的差异。这一项的意义是, 你也不能一股脑地去平滑, 也要考虑到重要性分数的真实波动情况

好,到这一步,我们就把计算单参数重要性分数的函数 讲清楚了! 知道了单参数的重要性分数,自然就可以知道整个三元组的重要性分数啦!

4.3 三元组重要性分数

在AdaLoRA中,三元组重要性分数计算方式如下:

其中,小写的s就是我们在4.2中定义的单参数计算函数。现在看这个公式,是不是很好理解呢:三元组的重要性分数 = 的重要性分数 + P矩阵中所有元素重要性分数的均值 + Q矩阵中所有元素重要性分数的均值。取均值的原因,是不希望参数量影响到重要性分数。

太好啦,到这一步为止,我们已经把AdaLoRA中难啃的Loss和三元组重要性分数讲完了,是不是比想象得简单很多?接下来我们来啃最后一块硬骨头:知道了三元组的重要性分数后,怎么动态调整矩阵的秩?

五、动态调整矩阵的秩

老规矩,在开始讲解前,我们再来明确几个数学符号:

  • : 表示第k个模块的P矩阵在t时刻的梯度
  • : 表示第 个模块的 矩阵在 时刻的梯度
  • : 表示第 个模块的 矩阵在 时刻的梯度

5.1 调整函数

前文说过,动态调整矩阵秩的核心,就是根据三元组重要性分数,对 矩阵中相应的 做置0处理。所以,我们就来看看 的置0策略。

(1)首先, 我们拿 , 先更新一波 , 即我们有:

其中, 是我们的学习率 (learning_rate)

注意, 这里 头上还顶着 时刻的标志, 而不是 , 也就是说, 我们对 做完梯度更新后的结果, 并不是 时刻的结果。我们做完置 0 后, 才是 时刻的结果。

(2)接着,我们按以下方式判断 矩阵中哪些元素应该置0,哪些元素应该保持为梯度更新后的结果:

这个看起来很复杂的公式,表达的意思很简单:

  • 时刻 矩阵, 是由 这个函数决定的, 这个函数的输入是梯度更新后的 , 以及第 个模块所有三元组的重要性分数 .
  • 这个函数具体就长成后面带大括号的那个样子。也就是重要性分数排在top_b的三元组, 它们的 保持原样, 其余的则置0
  • 最后, 这里的top_b也涉及一种策略, 那就是它的值是随着t的变动而变化的(例如 时, 我取的是top 时, 我取的是top b之类)。我们在下一小节细说这个策略

5.2 top_b策略

在开始讲top_b策略前,我们先来思考一个问题:为什么每次选出的重要三元组的个数,要随着时刻t的变动而变动?

这个问题的答案还是:模型的学习是探索性的过程。

在训练刚开始,我们逐渐增加top_b,也就是逐渐加秩,让模型尽可能多探索。到后期再慢慢把top_b降下来,直到最后以稳定的top_b进行训练,达到AdaLoRA的总目的:把训练资源留给最重要的参数。这个过程就和warm-up非常相似。

具体的策略在原始论文3.3节中有讲解,不难,所以我就不另外分析啦,大家可以自行阅读。以及本文的实验部分,我也不在这边写了,实验部分一句话总结就是相比LoRA确实有了不错的提升,大家可以自己去看细节。

六、AdaLoRA训练流程总结(必看)

最后,我们把AdaLoRA的整体训练流程总结一下:

  • (1) 拿到训练数据集, 确定好总训练步长 。根据总步长T设计好top_b的warm-up策略, 并设定好一系列超参, 同时也把 的初始化做好
  • (2)进入某个step的迭代
  • (3)给模型喂一份mini-batch,正常做forward和backward,计算loss和梯度
  • (4)(5)对某一个三元组,我们先计算其中每个参数的重要性(单参数重要性)
  • (6)根据单参数重要性,计算出整个三元组的重要性分数
  • (7)使用(3)中计算好的梯度,正常更新矩阵P和Q
  • (8)根据三元组重要性分数、动态调秩策略、top_b来判断要给哪些 置 0 , 其对应的三元组中的P和Q向量相当于被mask掉, 以此来实现动态调秩的目的。这番操作后, 我们得到更新的矩阵 。然后将 送入下一轮训练。

以此类推。😭妈呀敲字真的太累了啊。

七、参考

1、https://arxiv.org/abs/2303.10512
2、https://github.com/QingruZhang/AdaLoRA

公众号后台回复“数据集”获取100+深度学习各方向资源整理

极市干货

技术专栏:多模态大模型超详细解读专栏搞懂Tranformer系列ICCV2023论文解读极市直播
极视角动态欢迎高校师生申报极视角2023年教育部产学合作协同育人项目新视野+智慧脑,「无人机+AI」成为道路智能巡检好帮手!
技术综述:四万字详解Neural ODE:用神经网络去刻画非离散的状态变化transformer的细节到底是怎么样的?Transformer 连环18问!

点击阅读原文进入CV社区

收获更多技术干货

【声明】内容源于网络
0
0
极市平台
为计算机视觉开发者提供全流程算法开发训练平台,以及大咖技术分享、社区交流、竞赛实践等丰富的内容与服务。
内容 8155
粉丝 0
极市平台 为计算机视觉开发者提供全流程算法开发训练平台,以及大咖技术分享、社区交流、竞赛实践等丰富的内容与服务。
总阅读5.7k
粉丝0
内容8.2k