AI知识库

53AI知识库

学习大模型的前沿技术与行业应用场景


大模型(LLM)高效微调LISA原理及代码实现
发布日期:2024-04-26 07:59:18 浏览次数: 2026



1.概述

LoRA由于其允许适配器合并回基础模型参数的良好属性,成为最被广泛采用的PEFT技术之一。尽管如此,LoRA在微调任务上的优越性能尚未达到在所有设置下都能普遍超越全参数微调的程度(Ding等人,2022Dettmers等人,2023)。特别地,已观察到LoRA在连续预训练持续预训练是指在已有的预训练模型基础上,使用新的数据集继续进行训练,以此来进一步提升模型的性能期间对大型数据集的表现往往会有所下降(Lialin等人,2023),这引发了对其在这些情况下的有效性的质疑。我们认为,这是因为与基础模型相比,LoRA的可训练参数数量较少,限制了LoRA训练时的表示能力。

我们深入研究了LoRA在各层的训练统计信息,力求弥合LoRA与全参数微调之间的差距。令人惊讶的是,我们发现LoRA在各层的权重范数分布具有罕见的偏斜特征,即底层和/或顶层在更新过程中占据了大部分权重,而其他自注意力层仅占较小的比例,这意味着不同层在更新时的重要性不同。这一关键发现激发了我们按照各层重要性来“采样”的想法,这恰好符合重要性采样的概念。

因此,这一策略自然而然地催生了我们的分层重要性采样AdamLISA)算法。通过选择性地仅更新必要的LLM层,而保持其他层不变,LISA使得能够使用与LoRA相当或更少的内存消耗来训练包含≥650亿参数的大规模语言模型。此外,在下游任务的微调中,LISA大幅度超越了LoRA和传统的全参数微调方法,显示出LISA作为LoRA替代方案的巨大潜力。

我们总结了以下关键贡献:

• 我们发现在LoRA和全参数微调中都存在各层权重范数分布偏斜的现象,这暗示了大规模LLM训练中不同层的重要程度差异。

• 我们提出了分层重要性采样AdamWLISA)算法,这是一种简单的优化方法,能够在与LoRA相当或更少的内存成本下扩展到超过700亿参数的LLMs

• 我们展示了LISA在现代LLMs微调任务中的有效性,其中在MT-Bench上超越LoRA 8%-36%,并展现出更优的收敛行为。在某些设置下,LISA甚至超越了全参数训练。在不同大小的模型(70亿-700亿参数)和不同的任务(包括指令跟随、医疗问答和数学问题)上,我们都观察到了类似的性能提升。

 

1LLaMA-2-7B模型在Alpaca GPT-4数据集上的训练损失,分别展示了完全参数训练(FT)、LoRA以及LISA方法的结果。

2.方法

为了理解LoRA如何仅用一小部分参数就能实现有效的训练,我们在多个模型上进行了实证研究,特别关注了各个层间的权重范数变化。我们使用Alpaca-GPT4数据集(Peng等人,2023年)对该模型进行了微调。在训练过程中,我们详细记录了每次更新后每层ℓ在每步t时的平均权重范数,即

 

2展示了这些发现,其中x轴代表层ID,从嵌入权重到最后一层,y轴量化了权重范数。可视化揭示了一个关键趋势:

• 对于GPT2模型,嵌入层(wtewpe层)或语言模型(LM)头部层的权重范数相较于中间层显著更大,往往高出数百倍。然而,在全参数训练环境下,这一现象并不突出。

 

2:在使用LoRA和全参数训练时,GPT2LLaMA-2-7B模型训练过程中各层的权重范数分布。

在各层具有相同全局学习率的前提下,LoRA中权重范数较小的层在全参数设置下也应该有较小的概率解冻,从而确保迭代过程中预期的学习率保持一致。

由于除底层和顶层之外的所有层在LoRA中权重范数都很小,我们采用以下采样概率分布{p}NL=1 = {1.0, γ/NL, γ/NL, ..., γ/NL, 1.0},其中γ控制优化过程中预计解冻层数。直观地讲,γ作为一个补偿因子,用于弥合LoRA与全参数微调之间的差异,使LISA模仿出与LoRA相似的分层更新模式。为了进一步控制实际设置中的内存消耗,我们采取每次随机采样γ层的方式,以此来限制训练过程中解冻的最大层数上限。

LISA方法会为模型中的每一层分配一个采样概率,这些概率组成了一个分布{p}。在这个分布中,除了第一层(底层)和最后一层(顶层)之外,其他层的采样概率都是γ/NL,其中NL是层数,γ是一个补偿因子。这个补偿因子γ用来控制预计解冻的层数,也就是说,它决定了在优化过程中有多少层会被实际更新。

 

LISA 算法的核心思想是在每次迭代中,不是为每个层应用不同的学习率,而是通过采样的方式来决定哪些层应该被解冻(即更新其参数)。这样做可以保持全局学习率在各层之间的一致性,同时确保在迭代过程中预期的学习率保持不变。

算法的实现步骤如下:

1. 初始化学习率 0`

2. 对于每个训练步骤 `i` 0 `T/K - 1`(这里的 `T` 是总的训练步数,`K` 是某个特定的步数,用于确定采样频率):

- 对于每个层 `l` 1 `NL``NL` 是总层数):

- 如果随机变量 `u(0, 1)` 大于某个阈值 `pl`,则冻结该层(即不更新该层的参数)。

K次迭代采样一次,因此总共采样次数为T/K次,每一次的采样中才会随机选择γ层进行解冻更新。

3. 除了底层和顶层之外,LoRA 中的所有层都有较小的权重范数。因此,我们采用一个学习率采样概率的序列 `{p}`,其中 `γ` 是一个控制优化过程中预期解冻层数的因素。`γ` 用于弥补 LoRA 和全参数调优之间的差异,使得 LISA 能够模拟与 LoRA 相似的层级更新模式。

4. 为了进一步控制实际设置中的内存消耗,我们每次随机采样 `γ` 个层,以限制训练过程中最大解冻层数的上限。

通过这种方法,LISA 算法能够在保持与 LoRA 相似的层级更新模式的同时,有效地控制内存消耗,并且避免了 LoRA 的低秩表示能力的限制。这使得 LISA 成为一种适用于大型语言模型微调的有效方法。

Layerwise Importance Sampling AdamW (LISA) 算法中,随机变量 `u(0, 1)` 代表的是一个在每次迭代中独立生成的、范围在 0 1 之间的均匀分布的随机数。这个随机数用于决定在训练过程中哪些层将被选中进行参数更新(即解冻),哪些层将保持不变(即冻结)。

这里的 `pl` 是一个预设的阈值,用于控制冻结层的概率。当 `u(0, 1)` 大于这个阈值 `pl` 时,意味着当前层将不会被选中进行参数更新,因此该层的参数将被冻结。换句话说,只有当 `u(0, 1)` 小于或等于 `pl` 时,该层的参数才会被更新。

为什么要这样做呢?这其实是一种基于重要性采样的策略。在 LoRALow-Rank Adaptation of Large Language Models)中,权重范数较小的层通常被认为重要性较低,因此在全参数微调设置中,这些层的学习率也应该较小。通过使用随机变量 `u(0, 1)` 和阈值 `pl`LISA 算法能够模拟这种层级更新模式,同时保持全局学习率的一致性。这样,LISA 就能够在不牺牲性能的前提下,有效地减少内存消耗和计算成本。

简而言之,`u(0, 1)` `pl` 的机制允许 LISA 算法智能地选择哪些层应该参与到参数更新中,从而在保持模型性能的同时,提高训练效率和减少资源消耗。

如果前面还没理解,再看看这幅图

 

核心思想:在T次迭代中,周期K(总共采样了T/K次),每次随机抽取γ层进行参数更新(图示中γ=1),总共进行T/K次,然后就完事了。

3.代码

代码库为:https://github.com/OptimalScale/LMFlow

 

LISA代码实现:

# 检查是否使用LISA微调方法if training_args.use_lisa:    # 定义动态层激活回调类    class DynamicLayerActivationCallback(TrainerCallback):        def __init__(self, n_layers, interval_steps, model):            # 调用父类构造函数            super().__init__()            # 初始化属性            self.n_layers = n_layers  # 激活的层数            self.interval_steps = interval_steps  # 激活层更新间隔步数            self.model = model  # 模型对象            # 根据模型类型确定层的访问路径            if self.model.__class__.__name__ == 'LlamaForCausalLM':                self.layers_attribute = 'model.model.layers'  # LlamaForCausalLM模型的层访问路径            else:                self.layers_attribute = 'model.transformer.h'  # 其他模型的通用层访问路径            # 动态获取层的数量            self.total_layers = len(eval('self.' + self.layers_attribute))            # 初始化时冻结所有层            self.freeze_all_layers()  # 调用冻结层的方法            self.active_layers_indices = []  # 初始化激活层索引列表
def freeze_all_layers(self): # 动态获取所有层 layers = eval('self.' + self.layers_attribute) # 遍历层并冻结参数 for layer in layers: for param in layer.parameters(): param.requires_grad = False
def on_step_begin(self, args, state, control, **kwargs): # 检查是否到了更新激活层的时间 if state.global_step % self.interval_steps == 0 or state.global_step == 1: self.switch_active_layers() # 调用更新激活层的方法
def switch_active_layers(self): # 首先冻结所有层 self.freeze_all_layers() # 随机选择n_layers个层来激活 layers = eval('self.' + self.layers_attribute) # 重新获取层的引用 self.active_layers_indices = np.random.choice(range(self.total_layers), self.n_layers, replace=False) print(f"在接下来的步骤中激活索引为:{self.active_layers_indices}的层。") # 仅对选中的层启用梯度 for idx in self.active_layers_indices: for param in layers[idx].parameters(): param.requires_grad = True
# 实例化回调类 dynamic_layer_activation_callback = DynamicLayerActivationCallback( n_layers=training_args.lisa_activated_layers, interval_steps=training_args.lisa_interval_steps, model=model.get_backend_model() ) # 将回调类实例添加到训练器的回调列表中 trainer_callbacks.append(dynamic_layer_activation_callback)



53AI,企业落地应用大模型首选服务商

产品:大模型应用平台+智能体定制开发+落地咨询服务

承诺:先做场景POC验证,看到效果再签署服务协议。零风险落地应用大模型,已交付160+中大型企业

联系我们

售前咨询
186 6662 7370
预约演示
185 8882 0121

微信扫码

与创始人交个朋友

回到顶部

 
扫码咨询