
医疗机构如何防范API漏洞威胁
低秩适应(LoRA)是训练定制大型语言模型(LLM)中广泛使用且效果显著的技术之一。对于对开源LLM感兴趣的朋友来说,这是一项非常值得掌握的基础技术。
上个月,我发表了一篇关于LoRA实验的文章,这些实验是基于我和Lightning AI团队共同维护的开源Lit-GPT库进行的。这篇在AI前沿的文章旨在探讨我从这些实验中得到的主要教训。同时,我还会回答一些与此主题相关的常见问题。如果你对微调自定义LLM感兴趣,我希望这些见解能在长远的时间里为你节省一些时间——这里没有使用双关语。。
简而言之,本文讨论了以下几个主要观点:
此外,我将解答关于LoRA的10个常见问题:
在上一期的AI专栏中,我曾提到,如果大家有兴趣,我愿意撰写一篇更通用的介绍,包括LoRA从零开始的代码实现。根据大家的反馈,这个想法非常受欢迎,因此我计划在未来分享一篇更深入探讨LoRA的文章。目前,本文主要聚焦于使用LoRA的更广泛理念和经验——提供一个宏观的视角。
大型语言模型(LLM)因其庞大的规模,在训练过程中更新所有模型权重可能会非常耗费GPU内存。
以一个拥有70亿参数的大型语言模型为例,我们用权重矩阵W来表示这些参数。(实际上,模型的参数分布在多个层的不同矩阵中,但为了简化讨论,这里我们以单个权重矩阵为例)。在反向传播过程中,我们计算出一个ΔW矩阵,它包含了我们希望在训练期间如何更新原始权重W以最小化损失函数的信息。
权重的更新过程可以表示为:
W 更新 = W + ΔW
如果权重矩阵W包含70亿参数,那么权重更新矩阵ΔW也包含70亿参数,计算矩阵ΔW可能会非常消耗计算资源和内存。
Hu 等人提出的 LoRA 方法 replace 将权重变化 ΔW 分解为低秩表示来解决这个问题。具体来说,LoRA不需要显式计算ΔW。相反,LoRA在训练期间直接学习ΔW的分解表示,这就是节省资源的关键,如下图所示。
如上所述,ΔW的分解意味着我们用两个较小的LoRA矩阵A和B来表示大矩阵ΔW。如果A的行数与ΔW相同,B的列数与ΔW相同,我们可以将这种分解表示为ΔW = AB(即矩阵A和B的乘积)。
这种分解能节省多少内存呢?这取决于超参数r的值。例如,如果ΔW有10,000行和20,000列,那么它需要存储200,000,000个参数。如果我们选择r = 8,那么A将有10,000行和8列,B将有8行和20,000列,这样A和B总共需要存储的参数数为10,000×8 + 8×20,000 = 240,000个参数,大约是原来参数数的1/830。
当然,A和B无法像ΔW那样捕捉所有信息,但这是有意为之的设计。使用LoRA时,我们假设模型在预训练阶段需要W是一个全秩的大型矩阵,以包含预训练数据集中的所有知识。然而,在微调LLM时,我们并不需要更新所有权重,也不需要用比ΔW更少的参数来捕捉适应的核心信息;因此,我们通过AB进行低秩更新。
在使用LoRA进行多次实验后,我注意到尽管大型语言模型(LLM)的训练本身具有随机性,尤其是在GPU上进行模型训练时,基准测试的结果在不同运行之间却异常一致。这种一致性为其他比较研究提供了坚实的基础。
请注意,这些结果是在默认设置下,使用较小的r=8得到的。具体的实验细节可以在我另一篇文章中找到。
Dettmers等人提出的QLoRA,即量化LoRA,是一种在微调过程中进一步减少内存使用的技术。在反向传播阶段,QLoRA将预训练的权重量化到4位精度,并利用分页优化器来管理内存峰值。
实际上,我发现使用QLoRA可以节省33%的GPU内存。然而,由于QLoRA中预训练模型权重的额外量化和去量化步骤,训练运行时间增加了39%。
以下是使用16位浮点数的默认LoRA的对比数据:
而使用4位量化浮点数的QLoRA的数据如下:
此外,我观察到模型性能几乎没有受到影响,这使得QLoRA成为常规LoRA训练的一个可行替代方案,特别是在面临常见的GPU内存瓶颈时。
学习率调度器在训练过程中逐渐降低学习率,这有助于优化模型的收敛性能,并防止在最小化损失函数时超出最小值点。
余弦退火是一种流行的学习率调度器,它根据余弦曲线来调整学习率。这种调度器以较高的学习率开始,然后逐渐下降,最终以类似余弦函数的方式趋近于零。一个常见的变体是半周期余弦退火,它在训练过程中只完成一个半周期的余弦曲线,如下图所示。
作为实验的一部分,我在LoRA微调脚本中加入了余弦退火学习率调度器,并发现它显著提升了SGD的性能。然而,对于Adam和AdamW优化器,余弦退火的影响较小,几乎没有明显差异
关于SGD相对于Adam的潜在优势,我将在下一节中进一步讨论。
Adam和AdamW优化器在深度学习中仍然是热门选择,尽管在使用大型模型时,它们会占用大量内存。这是因为Adam优化器为每个模型参数维护两个移动平均值:一个是梯度的第一个矩(平均值),另一个是梯度的第二个矩(非中心方差)。换句话说,Adam优化器在内存中为每个模型参数存储两个额外的值。如果我们使用的是一个拥有70亿参数的模型,则在训练期间需要跟踪额外的140亿参数。
与此不同,SGD优化器在训练过程中不需要跟踪任何额外参数。那么,在训练大型语言模型(LLM)时,将Adam替换为SGD对峰值内存需求有什么优势呢?
在我的实验中,使用AdamW和LoRA默认设置(r=8)训练的70亿参数Llama 2模型需要14.18 GB的GPU内存。相反,使用SGD训练相同的模型仅需14.15 GB的GPU内存。换句话说,节省的内存(0.03 GB)几乎微不足道。
为什么节省的内存如此之少?这是因为使用LoRA时,我们只有少量可训练参数。例如,当r=8时,在70亿Llama 2模型的6,738,415,616个参数中,只有4,194,304个是可训练的LoRA参数。
如果仅从数字上看,4,194,304个可训练参数听起来仍然很多,但经过计算,我们发现这些参数的内存占用为4,194,304 × 2 × 16位 = 134.22兆位 = 16.78兆字节。我们观察到的0.03 GB(30 MB)差异是由于存储和复制优化器状态所产生的额外开销。这里的2表示Adam存储的额外参数数量,16位指的是模型权重的默认精度。
然而,如果我们将LoRA的r增加到256(我在后续实验中已经这样做),Adam和SGD优化器之间的内存差异将变得更加明显:
总的来说,当LoRA的r值较小时,替换Adam优化器为SGD可能不值得。然而,当我们增加r值时,这种替换可能会变得更加有意义。
在传统深度学习中,我们经常对训练集进行多次迭代,这种对训练集的重复遍历称为训练周期(epoch)。例如,在训练卷积神经网络时,通常会执行数百个训练周期。那么,多周期训练是否也适用于指令微调呢?
当我将包含50k示例的Alpaca指令微调数据集的迭代次数增加一倍(相当于两个训练周期)时,我注意到模型性能有所下降。
我的结论是,多周期训练可能对指令微调并无益处,因为它可能导致结果变差。我在包含1k示例的LIMA数据集中也观察到了相同的现象。这种性能下降可能是由于过度拟合导致的,这还需要进一步的研究和调查。
上表展示了仅对选定的权重矩阵(即每个转换器层中的Key和Value权重矩阵)启用LoRA的实验结果。此外,我们也有能力为Query权重矩阵、投影层、多头注意力块间的其他线性层以及线性输出层启用LoRA。
若我们为所有这些额外的层启用LoRA,在7B Llama 2模型中,可训练参数的数量将从4,194,304增加到20,277,248,即增加了5倍。这同时会带来更高的内存需求,从14.18 GB提升至16.62 GB。尽管如此,这种设置仍有望显著提升模型的性能。
然而,我的实验存在一个局限,即仅探索了两个设置:一是仅对查询和值权重矩阵启用LoRA,二是为所有层启用LoRA。在未来的实验中,探索其他组合可能会很有价值。例如,探究仅为投影层启用LoRA是否真正有益将是一个有趣的课题。
正如原始LoRA论文所描述的,LoRA引入了一个额外的缩放系数,用于在前向传递期间将LoRA权重应用于预训练权重。这个缩放过程涉及到我们之前讨论过的rank参数r,以及另一个超参数α(alpha),其应用方式如下:
scaling = alpha / r
weight += (lora_B @ lora_A) * scaling
如上代码公式所示,LoRA权重的影响随着缩放系数的调整而变化。
以往的实验通常使用r=8和α=16,这导致了2倍的缩放。在将LoRA应用于LLM时,选择α作为r的两倍是一个常见的经验法则,但我好奇这是否也适用于较大的r值。换句话说,“α = 2×rank”似乎确实是一个较好的起点。然而,在特定模型和数据集的组合中,例如r=256和α=128(缩放0.5倍)时,性能甚至更好。
我进行了r=32、r=64、r=128和r=512的实验,但为了简洁,我省略了这些结果,因为r=256时性能最佳。
通常选择α为r的两倍可能会得到较好的结果,但尝试不同的比例也是值得的。
一个重要的收获是,LoRA技术使我们能够在单个GPU上微调拥有70亿参数的大型语言模型(LLM)。以使用最佳配置(r=256和α=512)的QLoRA为例,使用AdamW优化器,在50k训练样本(这里指的是Alpaca数据集)上进行微调大约需要3小时(在A100 GPU上)。
在本文的后续部分,我将回答您可能存在的其他问题。
数据集可能非常关键。在我的实验中,我使用了包含50k个训练样本的Alpaca数据集。选择这个数据集是因为它广受欢迎,而且由于文章篇幅已经很长,尝试不同的数据集超出了本文的范围。
然而,值得注意的是,Alpaca是一个通过查询旧版本的ChatGPT生成的合成数据集,按照今天的标准可能不是最优选择。
数据质量可能非常重要。例如,在六月份,我讨论了LIMA数据集(领先于AI #9:LLM调整和数据集视角),这是一个精选的、只有1k个样本的数据集。
根据“LIMA:对准少即是多”的论文,在LIMA上微调的65B Llama模型明显优于在Alpaca上微调的65B Llama模型。
即使LIMA的数据集规模只有Alpaca的1/50,使用最佳配置(r=256,alpha=512)在LIMA上微调,我获得了与Alpaca数据集相似甚至更好的性能。
遗憾的是,对于这个问题,我并没有一个确切的答案。根据经验,知识通常是从预训练数据集中吸收的,而指令微调更多的是帮助或指导大型语言模型(LLM)遵循指令。
然而,值得注意的是,如果内存是一个问题,LoRA也可以用来在特定领域的数据集上进一步预训练现有的预训练LLM。
请注意,我的实验还包括两个算术基准测试(这些测试包含在我其他更具技术性的文章中),其中LoRA微调模型的性能明显不如预训练的基础模型。我的假设是模型取消了学习算术,因为Alpaca数据集不包含相应的示例。是模型完全丢失了知识,还是因为模型无法再处理指令,这需要进一步调查。但这里的一个要点是,在微调LLM时,包含您关心的每项任务的示例可能是一个好主意。
不幸的是,我没有任何好的启发式方法来选择一个好的r值,并认为它是一个超参数,需要针对每个LLM和每个数据集进行探索。我怀疑选择过大的r可能会导致更多的过拟合。另一方面,较小的r可能无法捕获数据集中的不同任务。换句话说,我怀疑数据集中的任务越多样化,r值应该越大。例如,如果我只想要一个执行基本2位运算的模型,那么一个小的r可能就足够了。然而,这只是一个假设,需要额外的调查。
我仅探索了两个设置:一是LoRA仅用于查询和值权重矩阵,二是LoRA用于所有层。在未来的实验中,探索其他组合可能会更有价值。例如,了解为投影层激活LoRA是否真正有益会是一个有趣的课题。如果我们考虑各种设置,包括lora_query、lora_key、lora_value、lora_projection、lora_mlp和lora_head,那么就需要探索2^6=64种组合,这将是未来研究的一个有趣方向。
通常,较大的r值会导致更多的过拟合,因为它决定了可训练参数的数量。如果模型存在过拟合问题,那么减小r值或增加数据集大小是首先要考虑的解决方案。此外,还可以尝试在AdamW或SGD优化器中增加权重衰减率,并考虑增加LoRA层的dropout值。我在实验中未探索过LoRA的dropout参数(固定使用了0.05的dropout率),这将是未来研究的一个有趣话题。
未来值得探索其他有趣的LLM(大型语言模型)优化器。例如,5月发布的Sophia:一种可扩展的随机二阶优化器,专为语言模型预训练而设计。
Sophia作为一种二阶优化算法,对Adam和AdamW这类在LLM中通常占据主导地位的优化器来说,具有特别的吸引力。据该论文所述,与Adam相比,Sophia的速度提高了2倍,且使用Sophia训练的模型能够实现更佳的建模性能。简而言之,Sophia通过梯度曲率(而非像Adam中的梯度方差)来标准化梯度。
除了精度和量化设置、模型大小、批量大小以及可训练的LoRA参数数量外,数据集本身也会影响内存使用情况。
请注意,Llama 2的区块大小为4048。这意味着,如果LLM的区块大小设置为4048个代币,那么它就能够一次性处理多达4048个代币的序列。然而,由于未来令牌的掩盖操作,使用较短的训练序列可以显著节省内存。
以Alpaca数据集为例,其规模相对较小,且最大长度仅为1304个标记。
当我试验其他长度高达 2048 个令牌的数据集时,我注意到内存使用量从 17.86 GB 增加到 26.96 GB。
我没有进行任何RLHF实验(对于那些好奇的人,我在这里介绍了RLHF),但我确实考虑了全参数微调。全参数微调至少需要2个GPU,每个GPU使用36.66 GB的内存,在3.5小时内完成。然而,基准测试结果并不理想,可能是由于过度拟合或次优的超参数选择。
是的,可以组合多组LoRA权重。在训练过程中,我们将LoRA权重与预训练权重分开,并在每次前向传递期间将它们相加。
但是,如果您有一个实际应用,其中包含多组LoRA权重,例如,每个应用客户一组,则最好单独存储这些权重以节省磁盘空间。不过,可以在训练后将预训练权重与LoRA权重合并,以创建单个模型。这样,我们就不必在每次前向传递中应用LoRA权重:
weight += (lora_B @ lora_A) * scaling
相反,我们按照上述方式应用权重更新并保存合并(相加的)权重。
同样,我们可以不断地添加多个LoRA权重集:
weight += (lora_B_set1 @ lora_A_set1) * scaling_set1
weight += (lora_B_set2 @ lora_A_set2) * scaling_set2
weight += (lora_B_set3 @ lora_A_set3) * scaling_set3
...
我还没有进行实验来评估这种方法的性能,但从Lit-GPT提供的scripts/merge_lora.py脚本来看,技术上已经可以实现这一点。
为了简单起见,我们通常为每一层训练具有相同学习率的深度神经网络,而学习率是我们需要优化的超参数。更进一步,我们还可以为每一层选择不同的学习率(在PyTorch中,这并不复杂)。然而,在实践中很少这样做,因为这会增加额外的开销,而在训练深度神经网络时,通常已经有许多参数需要调整。
类似于为不同层选择不同学习率的做法,我们也可以为不同层选择不同的LoRA等级。目前我还没有找到关于这一点的实验,但有相关文献详细介绍了这种方法,称为分层最优秩适应(Layer-wise Optimal Rank Adaptation,简称LORA)。从理论上讲,这在实践中听起来是个不错的主意。然而,在优化超参数时,这也会增加大量的选择和复杂性。
文章转载自:Practical Tips for Finetuning LLMs Using LoRA (Low-Rank Adaptation)