赞
踩
2023年,大模型如雨后春笋般爆发,58同城TEG-AI Lab作为AI平台部门,紧跟大语言模型技术发展步伐,打造了大语言模型平台,支持大语言模型训练和推理部署,并基于大语言模型平台构建了58同城生活服务领域(房产、招聘、汽车、黄页)垂类大模型灵犀大语言模型( ChatLing),支撑了业务方大模型应用的探索落地。灵犀大语言模型在公开评测集和实际应用场景下,效果均优于开源通用大语言模型以及商用通用大语言模型。
在研发灵犀大模型过程中,我们在大模型参数高效微调PEFT(Parameter-Efficient-FineTuning)上进行了广泛实践。本文系统性地解析了几种常用的大模型参数高效微调(PEFT)方法,并对每种方法的算法原理和应用效果进行了详细介绍。首先本文将阐述参数高效微调的重要性和基本概念,随后简要介绍LoRA微调之前的两种PEFT方法Adapter Tuning和Prefix Tuning,然后本文会详细介绍LoRA(Low-Rank Adaptation)、QLoRA(Quantized LoRA)和AdaLoRA(Adaptive Low-Rank Adaptor)以及SoRA(Sparse low rank adaptation)四种参数高效方法。这些方法旨在减少训练参数量、降低微调成本,同时保持模型性能。此外,本文还将分享基于Unsloth的微调加速实践经验,并且展示了在不同模型和数据集上的训练加速效果和显存占用降低效果。综上,本文将从微调方法和训练加速两个角度分享相关的技术解析和实践经验。
自ChatGPT走红以来,众多厂商纷纷投身于人工智能大型语言模型的研发与应用。然而,高昂的计算成本使得从零开始训练一个大型语言模型对许多人而言是难以企及的。因此,自Transformer和BERT时代起,微调(fine-tuning)技术因其亲民性而受到青睐。对于业务应用而言,基于开源模型在大量语料上训练的语言模型,再针对特定业务领域数据进行微调,这种成本效益更高的方法在当前阶段似乎更符合大多数公司的需求。微调技术指的是在一个已经训练好的模型(预训练模型)的基础上,通过特定的下游任务数据对其进行进一步训练,从而使模型能够更好地适应特定任务的需求。预训练模型如同一个高效的特征提取器,基于先前训练数据中积累的经验,能够提取出有效的特征,从而显著提升下游任务的训练效果和收敛速度。全参数微调是指在下游任务的训练过程中,对预训练模型的所有参数进行更新。如下图所示在微调过程中,权重矩阵中的每个参数(d*d个参数)都要参与更新。然而对于大规模语言模型而言,传统的全参数微调方法所需的计算资源和时间成本极其高昂。例如,最近发布的开源模型如LLaMA 3-70B、Qwen 1.5-110B和DeepSeek-V2等,这些模型的参数规模之大使得即使是进行微调普通用户也难以承受。
但是,由于模型在预训练阶段已经见过足够多的数据,收获了足够的经验,因此我只要想办法给模型增加一个额外知识模块,让这个小模块去适配我的下游任务,模型主体保持不变(freeze)即可。这就称之为参数高效微调,这种方式显然可以大大降低训练参数量,也就降低了微调成本,而且大幅提高了微调效率。下面,本文将介绍几种广泛使用的高效微调方法。
2019 年,Adapter Tuning将 Adapter 引入 NLP 领域,作为全模型微调的一种替代方案。Adapter 主体架构如下图所示。图例中的左边是一层Transformer Layer结构,其中的Adapter是前文描述的“额外知识模块”;右边是Adatper的具体结构。在微调时,除了Adapter的部分,其余的参数都是被冻住的(freeze),这样能有效降低训练参数量。Adapter的内部架构不是本文所述的重点,这里就不再介绍了。但这样的结构设计存在一个显著劣势:添加了Adapter后,模型结构整体变的更深了,会增加推理时长。
前缀微调(prefix-tunning),用于生成任务的轻量微调。前缀微调将一个连续的特定于任务的向量序列添加到输入,称之为前缀。与提示(prompt)不同的是,前缀完全由自由参数组成,与真正的 token 不对应。相比于传统的微调,前缀微调只优化了前缀。因此,我们只需要存储一个大型 Transformer 和已知任务特定前缀的副本,对每个额外任务产生非常小的开销。Prefix Tuning通过在输入数据前增加prefix前缀来给模型提供一些先验知识的方式来提升微调效果,提供prefix的长度在训练中是一个可调整的超参,在微调中,同样需要冻结住模型其余模块,只训练prefix相关的超参即可。
如上论文图给出的是gpt2模型 ,prefix的作用是引导模型提取x相关的信息,进而更好地生成y。例如,我们要做一个summarization的任务,那么经过微调后,我们希望prefix能引导模型去提取输入x中的核心信息做总结。但是这样也会导致模型输入的增加,增加计算量和推理时间,并且增加prefix难以保证训练效果。
由于上面两种方法的劣势,我们希望找到一种方法既能像全参微调一样不增加额外输入或改变模型结构,又能大幅度减少训练参数量降低微调成本的方法。基于此LoRA(Low-Rank Adaptation,低秩适配器)第一个解决了这个问题。如下图是LoRA的整体架构,Lora通过在原始权重矩阵W的旁边新增一个旁路,这个旁路由低秩的两个矩阵 A 和 B 组成,这两个低秩矩阵组合用来近似模拟全参更新中的 ΔW 增量矩阵。在训练过程中,我们冻结住原始预训练模型的权重 W ,只更新LoRA的两个低秩参数矩阵 A 和 B 。为了在训练的初始时刻能保证加了LoRA Adapter之后不影响原始模型的能力,我们分别使用高斯初始化和零初始化来初始化 A 和 B 。
假设原始权重矩阵的维度是 d×d,LoRA低秩矩阵A的维度设置为 r×d,矩阵B的维度为 d×r,这里我们冻结原始权重,相当于只更新增量权重。可以理解为我们先通过A矩阵进行一个降维操作,然后再使用B矩阵进行升维操作。这样微调的参数就从原来的 d×d 降低到了 2×d×r。因为一般设置的参数 r 会远小于 d,所以这里能大大降低训练参数量。在训练过程中,由于预训练权重 W 被冻结,仅对低秩矩阵 A 和 B 进行训练。因此,在保存LoRA训练的权重时,仅需保存参数量相对较小的低秩部分即可。训练时,GPU显存通常存储以下内容:输入数据、模型权重、模型的中间结果、梯度以及优化器状态。相比全参数微调方法,LoRA训练中输入数据部分显存占用不变。而因为原始权重也需要参与计算,因此模型权重和中间结果的占用也不变(增加的LoRA部分权重几乎可以忽略不计)。关于梯度的显存占用分析则相对复杂,以反向传播时 B 的梯度计算为例进行具体分析如下:
h=Wx+BAx=Wmx∂B∂L=∂h∂L∂Wm∂h∂B∂Wm
考虑 B 梯度的前两项,梯度的维度和预训练权重的梯度相同,均为 d×d。然而,由于LoRA并不作用于模型的所有层,并且由于训练参数的减少,优化器状态的存储显著减少,因为通常像类似adam优化器需要存储一阶梯度和二阶动量,而且通常优化器状态中存储的都是fp32类型的值,所以这部分显存占用相比全参微调大大降低,总体上显著降低了显存的占用。在推理时,将LoRA权重与原始权重合并即 h=(W+BA)x 的方式得到与原始模型一样的结构。这意味着完全不需要改变模型的任何结构,微调后模型的推理参数量与原始模型参数量完全一致。且这种方式让我们可以根据不同的业务场景基于同一个基座模型训练不同的LoRA权重,然后在不同的应用上加载不同的LoRA权重,非常灵活,而且LoRA权重通常非常小,也很易于存储和加载。
LoRA特点总结:
• 对 A 采用高斯初始化,对 B 采用零初始化;
• 微调的参数就从原来的 d×d 降低到 2×d×r ; r << d。
• 推理时不增加任何计算量,与原始模型架构完全一致
• 可以基于同一个基座在不同的场景下训练不同的lora模型加载使用
LoRA现已在业界广泛应用于各个场景的微调任务,在诸多任务中都被验证是有效且可行的。我们也基于两个业务场景做了单一任务的微调实践。LoRA的原理在于增量矩阵 ΔW 满足低秩假设, h=(W+ΔW)×x 中的 ΔW 确实是一个低秩矩阵。如果不满足该假设,那么LoRA的分解就一定会有精度损失,而达不到最好的微调效果。因此需要在任务中需要选择合适的 r,理论上说,针对复杂的任务可能需要更大的 r,但是 r 的值越大,可训练的参数量就越大,训练时长和显存占用就会同时变得更大。一般来说增大 r 的值会取得更好的微调效果,但是也不是一定如此,对于简单任务的微调太大的训练参数反而会使模型训练容易过拟合而导致效果变差。因此在实验阶段我们在两个NLU业务数据集上对比了全参训练、以及不同大小的 r 的训练效果,同时为了证明微调的效果,我们还加入了使用GPT4 zero-shot做该任务的效果对比。实验基座模型采用qwen1.5-7b-base,我们分别对比全参微调、LoRA不同 r 的微调以及直接使用GPT4的效果对比(业务数据集2中“有效问题”指除了标签为“其它”的数据,“拒识”指标签为“其它”的数据):
NLU业务数据集1:
指标 | Precision | Recall | F1-Score |
---|---|---|---|
GPT4 | 63% | 58% | 59% |
全参 | 89.96% | 85.53% | 87.68% |
LoRA(r=2) | 89.42% | 85.85% | 86.23% |
LoRA(r=8) | 89.54% | 86.32% | 86.44% |
NLU业务数据集2:
由表中两个数据集的实验结果可见,在两个场景下,LoRA微调都达到了与全参微调媲美的效果,尤其在第二个场景,LoRA微调的效果实际比全参微调要更好。而且从第二个业务数据集的实验上看,LoRA微调的效果根据不同的秩的选取有较大关系,微调效果先随着秩的增大而变好,在秩为16时取得最好效果,随后继续增大则并没有提升。所以在用LoRA微调时,选取合适的秩的值非常重要。
QLoRA的主要工作是通过模型量化进一步降低显存占用,从上文对LoRA原理的介绍中可以看到,LoRA微调是通过可训练参数的大幅降低从而显著降低了优化器状态部分的显存,模型权重和中间变量的显存占用实际上没有太大变化。而QLoRA通过定义了NF4的精度单位和双重量化方式将原始半精度的模型参数大小降低了数倍,从而进一步降低了训练时的显存占用。量化本质上把一群大数据范围的数舍入到用一群小数据范围的数来表示。举个简单例子我们把0-9的数用0-4来表示,那么显然0对应0,9对应4,而4和5可能都对应2,这就造成了量化误差,因为第一种数据中的4和5在第二种数据中都映射到了2,这就造成了信息的丢失,而这也就是量化误差的由来。这种误差是不可逆的,而诸多的量化方法,都只是为了尽可能减少这种误差,但是因为数据范围的减少,误差其实不可避免。为了尽可能减少量化误差,QLoRA结合了分位数量化和分块量化的技术。
分位数量化的思想来源于概率分布。由于模型的权重通常符合正态分布,通过利用这种分布特性可以有效降低量化误差。例如,在分布的两端,由于数据出现的概率较低,可以扩大映射的空间;而在分布的中间,由于数据出现的概率较高,可以压缩映射的空间。举例来说,假设原始数据类型为0-9且符合正态分布,需要用0-4来表示这些数据。在这种情况下,可以采用如下映射方案:将0-3映射为0,4映射为1,5映射为2,6映射为3,7-9映射为4。可以看到,分布中间的几个数实际上是无损量化的,而量化误差集中在分布的两侧。由于正态分布的特性,位于两侧的数据出现频率较低,因此这种量化方式可以有效降低整体量化误差。上述例子主要用于帮助读者理解这种量化方法的基本原理。实际上,在具体应用中需要借助数学上的累积分布函数来确定合适的“分位数”点。以4bit量化为例,共有4个bit位可用于量化数据,因此4bit量化将数据映射成16个数。4bit量化的关键在于找到这16个合适的分位点,从而确保量化过程高效且精确。而0对于神经网络权重来说通常具有特殊意义,所以我们希望保留0的位置,使得0被量化完之后还能映射到0位置,而且由于标准正态分布的0和1对应的累积分布函数的反函数的解为 ∞ 到 −∞ ,因此我们需要提供一个offset偏移量,把范围从[0,1]缩减到[offset, 1-offset]。这就是分位数量化的主要思想,下面基于代码来简单介绍一下量化过程。
from scipy.stats import norm import torch def create_normal_map(offset=0.9677083, use_extra_value=True): if use_extra_value: # one more positive value, this is an asymmetric type v1 = norm.ppf(torch.linspace(offset, 0.5, 9)[:-1]).tolist() # 正数部分 v2 = [0]*(16-15) ## we have 15 non-zero values in this data type v3 = (-norm.ppf(torch.linspace(offset, 0.5, 8)[:-1])).tolist() #负数部分 v = v1 + v2 + v3 else: v1 = norm.ppf(torch.linspace(offset, 0.5, 8)[:-1]).tolist() v2 = [0]*(16-14) ## we have 14 non-zero values in this data type v3 = (-norm.ppf(torch.linspace(offset, 0.5, 8)[:-1])).tolist() v = v1 + v2 + v3 values = torch.Tensor(v) values = values.sort().values values /= values.max() assert values.numel() == 256 return values Q = create_normal_map()
注意代码中的 offset
可能与上文介绍的略有不同,但目的是为了避免取到 −∞ 和 +∞。use_extra_value
则是为了区分对称量化和非对称量化。如果 use_extra_value
为 true
则0左边将有7个值,0右边将有8个值,加上0共有16个值。而如果 use_extra_value
为 false
,则0的左右两边都有7个值,0占2个值。代码逻辑如下,先通过 torch.linspace
均匀取到 offset
到 0.5 的8个数。(为什么是0.5,因为标准正态分布,0位置对应的累积分布概率是0.5)。然后通过 scipy.stats
包的 norm.ppf
方法取到对应点的分位数值,再加上0位置对应的0,然后经过归一化操作之后,得到4bit量化的16个量化分位数。
分块量化,它的思想是把量化前的数据分成几个块,每个块的数据在每个块内量化,这样能尽可能降低整个数据中离群值对量化效果的影响。每个量化的块记录自己的量化常数,量化常数的计算方式是该块中数据的最大值,因为需要基于该量化常数计算归一化之后的值,然后根据最接近该值的分位数索引得到量化后的值。反量化时,只需基于该索引值找到量化分位数,再乘上对应的量化常数得到反量化结果。
在上面分位数量化和分块量化中提到,模型不仅需要保存量化后的结果,还需要保存每个块的量化常数,而量化常数通常是全精度或半精度的即32位或16位。QLoRA的双重量化就是对这个量化常数再做一次8bit的量化,在进行量化常数的量化时,QLoRA以每256个量化常数为一组再做一次量化。因为使用了双重量化,在进行反量化时我们也需要进行两次反量化才能把量化后的值还原。最后分页优化是针对梯度检查点做的进一步优化,以防止在显存使用峰值时发生显存OOM的问题。QLoRA分页优化其实就是当显存不足时,将保存的部分梯度检查点转移到CPU内存上,和计算机的内存数据转移到硬盘上的常规内存分页一个道理。关于梯度检查点的内容不是本文重点要介绍的内容,因此这里不展开讲解。结合分位数量化、分块量化、双重量化和分页优化,QLoRA实现了极低的显存占用微调,而且与原始LoRA相比可以做到几乎不损失微调精度。即便是与全参微调相比,许多工作也证明QLoRA的精度损失极小,在工业界也广泛应用于许多场景的微调中。
NLU业务数据集1: 在一张A800上对Qwen1.5-7B进行QLoRA训练,使用相同的数据集训练5400个step,开启gradient_checkpointing,并在在所有linear层均插入lora adapter。结果显示QLoRA显著降低了显存占用,而且相比原始LoRA微调几乎没有性能下降,但是QLoRA由于需要额外的量化和反量化时间,因此训练时间会比普通LoRA略长。
指标 | 训练显存占用 | 训练时间 |
---|---|---|
QLoRA | 39.1GB | 8760s |
LoRA | 55.7GB | 7192s |
指标 | Precision | Recall | F1-Score |
---|---|---|---|
GPT4 | 63% | 58% | 59% |
全参 | 89.96% | 85.53% | 86.68% |
QLoRA | 89.42% | 85.85% | 86.23% |
LoRA | 89.54% | 86.32% | 86.44% |
在LoRA(Low-Rank Adapted)的介绍中提到,通过添加一个低秩的适配器旁路,LoRA大幅度降低了微调参数量和显存占用。然而,LoRA中超参数 ( r ) 的设置是全局统一的,即在所有模块中采用相同的秩。这种做法显然无法满足不同模块的权重增量 ΔW 具有不同秩的实际情况。因此,从理论角度看,根据不同模块在模型中的重要性来设置不同的秩是更为合理的方法。为了解决这一问题,AdaLoRA(Adaptive Low-Rank Adapter)提出了一种对每个模块的秩进行自适应调整的方法。具体来说,AdaLoRA基于奇异值分解(SVD)的形式对 ΔW 进行参数化更新。通过这种基于SVD的方法,AdaLoRA可以在避免复杂SVD计算的情况下,高效地裁剪不重要的奇异值,从而降低计算量达到高效微调的目的。SVD(Singular Value Decomposition)是传统机器学习一个重要的算法,它可以用在矩阵分解中,svd分解一个最重要的性质就是可以通过前k个奇异值和它对应的左右奇艺向量来近似表示原始矩阵,所以它经常被用于降维算法中。SVD分解可以表示为
A=UΣVT
其中
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。