深入浅出剖析 LoRA 技术原理( 三 )


在切换不同下游任务时,我们可以灵活从中移除低秩权重的部分 。例如我们先做下游任务A,做完后通过合并权重,并单独保留低秩权重 。当我们切换到下游任务B时,我们可以通过从中减去低秩权重部分,然后再开启新的LoRA微调 。也就是说,每个下游任务,都可以有自己的一套低秩权重 。
你可能想问,在每次微调结束后,我一定要把低秩权重合进中吗?我可以将“预训练权重”和“低秩权重”分开存储吗?当然没问题啦,LoRA是很灵活的,你完全可以根据自身需要,改写代码,决定权重的保存方式,只要掌握一个核心原则:不管是合还是不合,你总有办法能区分出预训练和LoRA的部分,就行 。在源码解读篇中,我们会再详细来看这点 。
恭喜你!到这一步你已经掌握了LoRA的架构,是不是很简单,是不是跃跃欲试?但是,作为一名合格的炼丹师,为了能对训练过程更好debug,我们还要需要更深入研究LoRA的原理 。
四、LoRA低秩适配的原理
在前文中,我们曾反复提到“秩”的概念,并说明LoRA的秩即为超参,同时,我们也不断强调是的近似 。在这一节中,我们将具象化地来看看“秩”,并说明为何是“近似”,在了解这些后,我们就能来解读超参的作用,并掌握一定的炼丹感觉了 。
4.1 什么是秩
我们首先来看一个矩阵A:
A = [[1, 2, 3],[2, 4, 6],[3, 6, 9]]
该矩阵中,row2 = row1 * 2,row3 = row1*3,也就是说,矩阵中的每一行,都可以通过第一行线性表示 。
我们再来看一个矩阵B:
B = [[1, 2, 3],[7, 11, 5],[8, 13, 8]]
该矩阵中,任意一行,总可以用其他两行的线性组合来表示 。
我们最后再来看一个矩阵C:
C = [[1, 0, 0],[0, 1, 0],[0, 0, 1]]
该矩阵中,任意一行,都不能从其余行的线性组合中推导而来 。
调用np..函数,我们可以算出任意矩阵的秩,上面三个矩阵的秩分别为:
A = np.array(A)B = np.array(B)C = np.array(C)print("Rank of A:", np.linalg.matrix_rank(A)) # 1print("Rank of B:", np.linalg.matrix_rank(B)) # 2print("Rank of C:", np.linalg.matrix_rank(C)) # 3
对矩阵A来说,由于只要掌握其中的任意一行,其余行都可以由这一行线性推导而来,因此A的秩是1 。
对矩阵B来说,由于只要掌握其中的任意两行,其余行都可以由这两行线性组合推导而来,因此B的秩是2 。
对矩阵C来说,由于必须完全掌握三行,才能得到完整的C,因此C的秩是3 。
看到这里,你是不是已经对秩有了感性的理解了?秩表示的是矩阵的信息量 。如果矩阵中的某一维,总可以通过其余维度线性推导而来,那么对模型来说,这一维的信息是冗余的,是重复表达的 。对A和B的情况,我们称为秩亏(rank ),对C的情况,我们称为满秩(full rank) 。更严谨的数学定义,大家可以参考《线性代数》(狗头) 。
有了对秩的这层认识,我们自然会想到,全参数微调中的增量权重可能也存在冗余的信息,因此我们并不需要用完整的d*d尺寸来表示它 。那么,我们要如何找出中真正有用的特征维度呢?SVD分解(奇异值分解),可以帮我们解决这个问题
4.2 SVD分解
如图,矩阵是我们需要做信息量检查的矩阵 。假设在输入数据的特征空间中,存在一组正交的单位向量,经过的变换后,它们变成另一组正交向量,其中也是一组正交的单位向量,分别表示对应方向上的模 。上面这一顿变幻,可以写成:
稍加改写,就有:
不难发现,中隐含了对“信息量”的提示 。在本例中经过的转换投射到上时,强调了在1方向上蕴含的信息 。