提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
文章链接
摘要
本博客介绍了论文《Unlocking Continual Learning Abilities in Language Models》提出了一种名为MIGU的无排练、无任务标签的方法,用于解决语言模型(LMs)在持续学习(CL)中灾难性遗忘的问题。MIGU利用LMs线性层输出的L1归一化幅度分布差异,在梯度更新过程中仅更新幅度大的参数,从而解锁LMs的固有CL能力。实验表明,MIGU适用于T5、RoBERTa和Llama2三种LM架构,在持续微调、持续预训练和四个CL基准测试中表现出色,还能与现有CL方法无缝集成以提升性能。不过,研究受计算限制,且在挖掘LMs固有特征方面还有待进一步探索。
Abstract
This blog introduces the paper "Unlocking Continual Learning Abilities in Language Models", which proposes a rehearsal-free, task-label-free approach called MIGU to solve the problem of catastrophic forgetting of language models (LMs) in continuous learning (CL). MIGU takes advantage of the L1 normalized amplitude distribution difference of the linear layer output of LMs, and only updates the parameters with large amplitude during the gradient update process, thereby unlocking the inherent CL capability of LMs. Experiments have shown that MIGU is suitable for T5, RoBERTa, and Llama2 LM architectures, and performs well in continuous fine-tuning, continuous pre-training, and four CL benchmarks, and can be seamlessly integrated with existing CL methods to improve performance. However, this study is limited by calculations, and further exploration is needed in mining the intrinsic characteristics of LMs.
一、MIGU(基于幅度的梯度更新以进行连续学习)
研究者提出的基于幅度和梯度更新的持续学习方法是一种无彩排,无任务的方法,它支持连续的预训练和持续填充,下图是MIGU与其他持续学习方法的比较:
研究者发现不同任务在语言模型线性层的L1归一化输出幅值分布存在显著差异,如下图所示,BooIQA、COPA和Yelp任务在同一层的输出幅值分布明显不同。基于上述差异,仅在反向传播时更新幅值最大的参数,减少梯度冲突,从而保留旧任务知识并学习新任务 。
研究者的方法采用两步过程来利用震级分布的固有差异,在下图中展示了这个过程。
1)缓存输出幅度和
2)通过基于幅度的掩码更新梯度。
实现步骤:
1.前向传播(缓存输出幅度)
对线性层输出进行L1归一化,计算每个权重向量的幅值 。给定权重矩阵W∈Rdin×Dout,将W的列解释为一组DOUT向量,每个都具有维度Din:
给定层x∈Rdin的输入向量,可以将层的操作视为x和每个权重向量wi之间的点乘积:
然后使用L1范数来计算归一化乘积幅度。
2.反向传播:通过基于幅度的掩码更新梯度
根据幅值排序,仅保留前T%的权重进行梯度更新 。在向后阶段计算梯度后,获得了重量W的梯度矩阵∇W。然后,研究者定义一个掩码矩阵M,以使用在前向阶段缓存的L1归一化乘积幅度来部分地掩码CNOW。形式上,按降序对乘积幅度进行排序,并屏蔽相应的梯度,如下所示:
其中,T是阈值与掩模梯度的比率,t是掩模的实际数量t,⌊.⌋是最低舍入。然后,模型根据以下式子进行更新:
其中η是学习率。该公式确保仅更新具有超过阈值T的L1归一化幅度的那些权重,避免全局参数更新,减少任务间干扰 。
在实践中,为了应用MIGU,研究者对一批中所有token的乘积幅度进行平均,以生成用于简单实现的掩码。
Transformer块中的MIGU,研究者将该方法应用于应用于MHA(Query/Key/Value/Output)和FFN的线性层。
研究者还实现了用于LM的参数有效微调(PEFT)的MIGU,特别是采用了低秩自适应(LoRA)。标准LoRA在数学上表示如下:
其中x表示层的输入表示,A ∈ Rdin×r和B ∈ Rr×dout是低秩矩阵,α是缩放常数,W是标准线性的原始权重矩阵,x0是应用LoRA变换后的输出。
二、实验
1.实验设计
研究者在三种主流LM架构T5、RoBERTa和Llama2上,针对持续微调和持续预训练两种场景,使用四个CL基准数据集进行实验。对比MIGU与多种基线方法的性能,通过准确率等指标评估MIGU的效果。
研究者将基线分为两类:没有旧数据或任务信息,以及在训练期间有旧数据或任务信息。对于第一类,包括vanilla FT,它在一系列任务上训练所有模型参数,以及vanilla LoRA,其中固定大小的LoRA参数在一系列任务上训练。对于第二类,研究者采用基于排练的方法:LoRAReplay在LoRA上训练新任务,混合2%的过去任务,LFPT5不断训练软提示,同时学习解决任务并生成用于经验重放的训练样本。
2.实验代码
完整的实验项目链接:https://github.com/wenyudu/MIGU
以下展示实验的关键代码:
MIGU 方法的构建:
python
import torch
import functools
import torch.distributed as dist
from accelerate.utils import DeepSpeedEngineWrapper
def get_module(root_module, module_name):
"""根据模块名称从根模块中检索子模块。
此函数用于定位模型的特定部分(例如线性层),以便在 MIGU 中应用选择性梯度更新,
支持论文第 3.2 节中描述的参数定位。
"""
attrs = module_name.split('.')
return functools.reduce(getattr, attrs, root_module)
def apply_importance_mask(name, module, importance_mask):
"""将重要性掩码应用于模块权重的梯度。
Args:
name (str): 模块名称(用于调试或日志记录)。
module: PyTorch 模块,其权重梯度将被掩码处理。
importance_mask (torch.Tensor): 二进制掩码张量,1 表示更新参数,0 表示掩码参数,基于输出的 L1 归一化幅度。
此函数实现 MIGU 的核心步骤,仅更新具有大 L1 归一化幅度的参数的梯度,
减少对先前任务的干扰,见论文第 3.2 节中的梯度更新公式。
"""
if hasattr(module, 'weight'):
assert module.weight.grad is not None, f"{module} 无梯度"
if module.weight.grad is not None:
module.weight.grad *= importance_mask.unsqueeze(dim=-1).to(module.weight.grad.device)
def compute_importance_mask(activation, ini_threshold):
"""基于 MIGU 方法计算梯度更新的重要性掩码。
Args:
activation (torch.Tensor): 线性层输出的 L1 归一化幅度,在前向传播中缓存,
对应论文第 3.2 节中的前向传播阶段。
ini_threshold (float): 分位数阈值(例如 0.7),用于选择顶部激活比例,
与论文中 \( T \)(更新参数的比例,例如 \( T = 0.3 \))相关,\( ini_threshold = 1 - T \)。
Returns:
torch.Tensor: 二进制掩码,1 表示更新参数,0 表示掩码参数。
此函数通过选择前 \( 1 - ini_threshold \) 的激活值实现 MIGU 的参数选择逻辑,
并在分布式训练中平均激活值以确保一致性
"""
device = activation[0].device
dist.all_reduce(activation, op=dist.ReduceOp.AVG) # 分布式进程间平均激活值
threshold = torch.quantile(activation, ini_threshold) # 计算分位数阈值
importance_mask = (activation >= threshold).float().to(device) # 生成二进制掩码
return importance_mask
def backward_full_tuning(self, loss, **kwargs):
"""为全调优设计的自定义反向传播方法,集成 MIGU 逻辑。
Args:
self: DeepSpeedEngineWrapper 实例。
loss (torch.Tensor): 用于计算梯度的损失张量。
**kwargs: 附加参数,包括:
- is_first_task (bool): 若为 True,则使用标准反向传播(默认:True)。
- method (str): 更新方法('migu' 或 'random_update')。
- activation (dict): 前向传播中缓存的线性层激活值。
- ini_threshold (float): 重要性掩码的分位数阈值。
此函数在非首任务中应用 MIGU,仅更新大幅度参数的梯度,减少灾难性遗忘,
支持论文第 3.2 节和第 4.1 节的实验验证。'random_update' 用于对照实验。
"""
self.engine.backward(loss) # 计算标准梯度
# 在非首任务且为梯度累积边界时应用 MIGU
if not kwargs.get('is_first_task', True) and (kwargs.get('method') in ["migu", "random_update"])\
and self.engine.is_gradient_accumulation_boundary():
method = kwargs['method']
activations = kwargs['activation']
ini_threshold = kwargs.get('ini_threshold')
if method == "migu":
# MIGU:基于激活幅度掩码梯度
for name, activation in activations.items():
importance_mask = compute_importance_mask(activation, ini_threshold)
module = get_module(self.engine, name)
apply_importance_mask(name, module, importance_mask)
elif method == "random_update":
# 随机掩码梯度,用于消融研究
for name, module in self.engine.named_modules():
if hasattr(module, 'default') and hasattr(module.default, 'weight'):
assert module.default.weight.grad is not None, f"{module} 无梯度"
cur_dim = module.default.weight.grad.shape[0]
importance_mask = torch.ones(cur_dim, dtype=torch.float)
ones_indices = torch.randperm(cur_dim)[:int(cur_dim * ini_threshold)]
importance_mask[ones_indices] = 0.0
module.default.weight.grad *= importance_mask.unsqueeze(dim=-1).to(module.default.weight.grad.device)
self.engine.step() # 更新模型参数
def backward_lora_tuning(self, loss, **kwargs):
"""为 LoRA 调优设计的自定义反向传播方法,集成 MIGU 逻辑。
Args:
self: DeepSpeedEngineWrapper 实例。
loss (torch.Tensor): 用于计算梯度的损失张量。
**kwargs: 附加参数,包括:
- is_first_task (bool): 若为 True,则使用标准反向传播(默认:True)。
- method (str): 更新方法('cluster_activate' 或 'random_update')。
- activation (dict): 前向传播中缓存的 LoRA 层激活值。
- ini_threshold (float): 重要性掩码的分位数阈值。
- n_clusters (int): 聚类数量(未完全实现)。
- cluster_indice (dict): 聚类索引(可选,未完全实现)。
- activation_combined (bool): LoRA B 矩阵激活处理标志。
- cluster_constructure_method (str): 聚类方法(例如 'sequential')。
此函数将 MIGU 适应于低秩适应(LoRA)调优,基于输出幅度掩码 LoRA 的 A 和 B 矩阵梯度,
"""
self.engine.backward(loss) # 计算标准梯度
# 在非首任务且为梯度累积边界时应用 MIGU 或随机更新
if not kwargs.get('is_first_task', True) and (kwargs.get('method') in ["cluster_activate", "random_update"])\
and self.engine.is_gradient_accumulation_boundary():
method = kwargs['method']
activations = kwargs['activation']
n_clusters = kwargs.get('n_clusters')
ini_threshold = kwargs.get('ini_threshold')
cluster_indice_dict = kwargs.get('cluster_indice', {})
activation_combined = kwargs.get('activation_combined', False)
cluster_constructure_method = kwargs['cluster_constructure_method']
if method == "cluster_activate":
# 为 LoRA 调优应用 MIGU
for name, activation in activations.items():
if "input" in name:
continue
if "lora_A" in name:
# 为 LoRA A 矩阵掩码梯度
importance_mask = compute_importance_mask(activation, ini_threshold)
module_name = name
elif ("lora_B" not in name) == activation_combined:
# 为 LoRA B 矩阵掩码梯度,考虑激活组合逻辑
module_name = name + ".lora_B.default" if activation_combined else name
if cluster_constructure_method == "sequential":
importance_mask = compute_importance_mask(activation, ini_threshold)
else:
if module_name not in cluster_indice_dict:
print(f"缺少 {module_name} 的聚类索引")
break
cluster_indice = cluster_indice_dict[module_name]
importance_mask = compute_importance_mask(activation, ini_threshold)
module = get_module(self.engine, module_name)
apply_importance_mask(name, module, importance_mask)
elif method == "random_update":
# 随机掩码 LoRA 参数梯度,用于对照实验
for name, module in self.engine.named_modules():
if hasattr(module, 'default') and hasattr(module.default, 'weight'):
assert module.default.weight.grad is not None, f"{module} 无梯度"
cur_dim = module.default.weight.grad.shape[0]
importance_mask = torch.ones(cur_dim, dtype=torch.float)
ones_indices = torch.randperm(cur_dim)[:int(cur_dim * ini_threshold)]
importance_mask[ones_indices] = 0.0
module.default.weight.grad *= importance_mask.unsqueeze(dim=-1).to(module.default.weight.grad.device)
self.engine.step() # 更新模型参数
def replace_accelerator_backward_with_own_backward_lora_tuning():
"""用自定义的 LoRA 调优反向传播方法替换 Accelerate 库的默认方法。
此函数将 MIGU 的反向传播逻辑集成到 LoRA 调优的训练流程中,
覆盖 DeepSpeedEngineWrapper 的默认行为
"""
DeepSpeedEngineWrapper.backward = backward_lora_tuning
def replace_accelerator_backward_with_own_backward_full_tuning():
"""用自定义的全调优反向传播方法替换 Accelerate 库的默认方法。
此函数将 MIGU 的反向传播逻辑集成到全调优的训练流程中,
覆盖 DeepSpeedEngineWrapper 的默认行为
"""
DeepSpeedEngineWrapper.backward = backward_full_tuning
get_module()
说明其作为工具函数,用于定位模型组件(如线性层),支持 MIGU 的参数选择性更新
apply_importance_mask()
描述其实现 MIGU 的梯度掩码逻辑,确保仅更新大幅度参数,引用论文第 3.2 节的梯度更新公式:
backward_full_tuning()
其为全调优设计的反向传播方法,如何在非首任务中应用 MIGU,减少灾难性遗忘。
3.实验结果

上土表明,研究者提出的方法MIGU提高了所有五种CL方法的性能。LoRA+MIGU方法比普通LoRA方法高出15.2%,显著减轻了LoRA在长序列CL设置中的缺点。
下图中的结果也显示了研究者的方法优于或等同于具有任务标签或旧数据的复杂CL方法的结果。例如,FT+MIGU在MF1和ACC中分别实现了0.37%和0.42%的改进。结果表明,虽然DAS模型在较早学习的域中表现出较少的遗忘,但它在最后的域中也学习较少,这可能是由于在长序列的CL过程中用于约束其参数更新的强正则化。相比之下,MIGU展示了一种更可持续的方法,在早期和最近学习的领域表现出强大的性能。
研究者在要求更高的LLM连续指令调优设置上进一步评估了MIGU方法。在Magicoder-Evol-Instruct-110 K上微调了一个基地Llama 2 - 7 B,共32个纪元。该数据集包含7297万个编程问答令牌。然为了评估代码学习性能,研究者使用Humaneval基准,其中包含164个问题,这些问题生成了一个带有文档字符串和函数签名的Python程序。如果某个生成通过了所有提供的单元测试,则认为该生成是正确的。实验如下图所示:
与基线FT相比,MIGU方法学习了类似水平的新代码知识,但表现出显著较少的遗忘先前的知识。这表明,MIGU方法在学习可塑性和记忆稳定性之间的帕累托边界上实现了更好的平衡点。例如,在32个训练时期之后,研究者的方法的三个基准的平均准确度是59.4,而基线模型仅达到58.4。
总结
1.作者提出了"MIGU"方法,这是一种无排练、无任务标签的持续学习方法,仅更新大输出幅度的模型参数。实验表明,MIGU普遍适用于T5、RoBERTa和Llama2三种语言模型架构,在持续微调与持续预训练场景及四个基准测试中表现出色,能与现有持续学习方法无缝集成以提升性能,有效解锁语言模型的持续学习能力。
2.创新性:利用语言模型线性层输出的L1归一化幅度分布差异,无需任务标签和旧任务数据,缓解任务间梯度冲突。
- 提出MIGU方法,在正向传播缓存输出幅度,反向传播通过幅度掩码更新梯度,可应用于多种模型架构和持续学习场景。
- 3.不足:受计算限制,虽用LoRA微调Llama2 - 7B,但无法扩展到语言模型持续预训练或全量微调,不过在RoBERTa上的实验显示该方法有可扩展性潜力。