Compacting, Picking and Growing for Unforgetting Continual Learning 论文及代码流程解读

本文介绍了在连续学习中避免遗忘、模型扩展与压缩的CPG方法。该方法结合深度模型压缩、关键权重选择和渐进网络扩展,允许模型在处理无限任务时保持紧凑性。通过挑选和重用旧任务权重,以及在必要时扩展架构,CPG方法在避免遗忘的同时提高了新任务性能。实验显示,此方法能利用过去任务的知识提高新任务的模型质量,且模型可以被压缩,减少了冗余。
摘要由CSDN通过智能技术生成

论文翻译

Abstract

我们的方法利用了深度模型压缩、关键权重选择和渐进网络扩展(deep model compression, critical weights selection,and progressive networks expansion)。
通过以迭代的方式加强它们的集成,我们引入了一种增量学习方法,该方法可扩展到连续学习过程中的连续任务数。
我们的方法有如下几个优点:

  1. 它可以避免遗忘(在记住以前所有任务的同时学习新任务)
  2. 它允许模型扩展,但可以在处理顺序任务时保持模型的紧凑性(compactness)
  3. 通过我们的压缩和选择/扩展机制,我们证明了通过学习以前的任务积累的知识有助于为新任务建立更好的模型,而不是单独训练模型。

1 Introduction

虽然学习的模型可以用作预训练的模型,但针对新任务对模型进行微调会迫使模型参数拟合新数据,从而导致对之前任务的灾难性遗忘。

  • 为了减轻灾难性遗忘的影响,Kirkpatrick et al. 和 Zenkeet al. 研究了训练期间利用梯度或权重的正则化的技术。该算法对网络权值进行了正则化,并希望能对当前任务和以前的任务搜索一个共同收敛的算法。
  • Schwarz et al.提出了一种用于正则化的网络蒸馏方法,该方法对自适应于教师网络的神经权值施加约束,并将弹性-权值巩固(elastic-weight-consolidation,EWC)[14]应用于增量式训练

然而,由于在学习过程中缺少以往任务的训练数据,且网络容量是固定的(且有限的),正则化方法往往会逐渐忘记学习到的技能。

为了解决数据丢失的问题(即在原有任务训练数据不足的情况下,引入了数据保存和记忆重放技术。Data-preserving:直接保存重要数据或潜在编码作为一种有效的形式
Memory-replay:引入额外的内存模型,如GANs,以间接的方式保持数据信息或分布。内存模型有能力重放以前的数据。基于过去的数据信息,我们可以训练一个性能在很大程度上能够满足旧任务要求的模型。
然而,Memory-replay的一个普遍问题是,它们需要使用积累的旧信息进行明确的再训练,这会导致工作记忆要么很大,要么在记忆和遗忘之间妥协。

本文介绍了一种学习可持续性而紧凑的深度模型的方法,该方法可以在避免遗忘的情况下处理无限数量的连续任务。由于一个有限的架构不能确保记住从无限的任务中逐步学到的技能,我们的方法允许架构在某种程度上成长。然而,在连续学习过程中,我们也消除了模型冗余,从而可以在有限的模型扩展下,不断地压缩多个任务。
此外,从一项起始任务开始进行预训练或逐步微调模型,只包含初始化时的先验知识;因此,知识库会随着过去的任务而减少。
由于人类有能力在一生中不断地获取、调整和传递知识和技能,因此,在终身学习中,我们希望从以前工作中积累的经验有助于学习新的任务。通过使用我们的方法,越来越多的学习到的模型可以作为一个紧凑的、不遗忘的基础,它通常会比独立训练任务产生更好的后续任务模型。实验结果表明,我们的终身学习方法可以利用过去积累的知识来提高新任务的表现。

方法设计的动机

ProgressiveNet 保持原有参数不动的情况下为新的任务训练新的参数,这样会形成严重的参数冗余,所以我们不断地对模型进行剪枝。
在我们不断发展的CPG方法中,提供了两种可能的选择。

  1. 为新任务使用之前释放的权重。如果当所有释放的权重都被使用时,性能目标还没有实现,那么我们接着进行第二种选择
  2. 扩展架构,释放的和扩展的权重都被用于新任务训练。

我们的方法的另一个区别是挑选步骤。这个想法的动机如下。在ProgressiveNet中,旧任务的权重都被保留下来,用于学习新任务。然而,随着任务数量的增加,旧任务的权重也会越来越大。当它们都与在成长阶段新增加的权重共同使用时,旧的权重(固定的)就像惯性一样,因为只有更少的新权重被允许调整,这往往会降低学习效果。为了解决这个问题,我们没有使用所有的旧任务权重,而是通过一个可区分的掩码从它们中选择一些关键的权重。
压缩任务权值的一个主要难点是缺少先验知识来确定剪枝率。为了解决这个问题,在我们的CPG方法的压缩(compacting)步骤中,我们使用了逐步修剪过程,删除了一小部分权重,并对剩余的权重重新进行训练,以迭代地恢复性能。当达到预先定义的精度目标时,该过程停止。请注意,只有新添加的权重(从生长步骤中释放和/或扩展的权重)才允许修剪,而旧的任务权重保持不变。

Method Overview

  1. 通过剪枝建立一个压缩模型。给定一个新任务,旧任务模型的权重也是固定的。
  2. 通过一个可区分的掩码来挑选和重用一些对新任务至关重要的旧任务权重,并使用之前释放的权重一起进行学习。
  3. 如果精度目标还没有达到,架构可以通过在模型中添加过滤器(filters)或节点并恢复过程来扩展。然后重复这个过程。
    在这里插入图片描述

新的任务权重由两部分组成:第一部分是通过对旧任务权重的可学习的掩码来选取,第二部分是通过对额外权重的逐步剪枝/再训练来学习。由于旧任务的权重是固定的,所以我们可以将所需的函数映射集成到一个紧凑的模型中,而不会影响其推理的准确性。我们的方法的主要特点概括如下。

  • 避免遗忘:我们的方法确保不遗忘。在增量地添加新任务时,将按照完全相同的方式维护以前构建的函数映射
  • 收缩的同时扩展:我们的方法允许扩展,但保持了架构的紧凑性,可以潜在地处理无限的顺序任务。实验结果表明,多任务可以被压缩到一个只有少量或没有架构增长的模型中。
  • 紧凑知识库:实验结果表明,将之前任务记录的压缩模型作为知识库,积累了经验,可用于选择权重,提高了学习新任务的性能。

2 Related Work

Continual lifelong learning可分为三大类:网络正则化、记忆或数据回放和动态架构。
另外,关于无任务和作为程序合成(programsynthesis)的工作也在最近被研究。在下面,我们简要回顾了主要类别的作品,并建议读者参考最近的调查论文[28]进行更多的研究。
网络正则化方法:核心思想是限制地更新已学习模型的权值。为了保留学习到的任务信息,对权值的变化进行了惩罚。EWC利用Fisher信息来评估旧任务权重的重要性,并根据重要性的程度更新权重。
费雪信息 (Fisher information) 的直观意义是什么?
[49]中的方法基于相似的思想,通过学习轨迹来计算重要性。在线EWC[40]和ewc++改善EWC的效率问题。[6]给出了一种信息保留惩罚。该方法建立了一个注意力图,并希望前一个模型和并发模型的注意力区域是一致的。这些工作在一定程度上缓解了非稳态遗忘,但不能保证前期工作的准确性。
记忆重放:记忆或数据重放方法[32,41,13,3,46,45,11,34,33,27]使用额外的模型来记住数据信息。生成重放[41]将GANs引入终身学习。它利用一个生成器对与之前数据分布相似的假数据进行采样。可以使用这些生成的数据训练新的任务。记忆重放GANs (MeRGANs)[45]表明,生成器中仍然存在遗忘现象,生成的数据在未来任务中性能会变差。他们使用重放数据来提高生成器的质量。动态生成记忆(Dynamic generate Memory, DGM)[27]利用神经掩蔽来学习条件生成模型中的连接可塑性,并在生成器中为顺序任务设置动态扩展机制。虽然这些方法可以利用数据信息,但仍然不能保证过去任务的准确执行。
动态结构:动态结构方法[38,20,36,29,48]用一系列的任务调整结构。ProgressiveNet[38]为新任务扩展了架构,并通过保留以前的权重来保持函数映射。LwF[20]将模型层划分为共享的和特定于任务的两个层次,其中前者由任务共同使用,后者通过进一步的分支扩展以用于新的任务。DAN[36]扩展了每个新任务的架构,而新任务模型中的每一层都是基模型对应层中原始过滤器的稀疏线性组合。最近在GANs上采用的记忆重放方法[27]也采用了架构扩展。这些方法通过结构扩展可以显著减少或避免灾难性遗忘,但模型是单调递增的,会产生冗余结构。
随着架构的不断增长,将保留模型冗余,一些方法在扩展[48]之前执行模型压缩,以便可以构建一个紧凑的模型。过去与我们相关的方法主要是动态扩展网(DEN)[48]。DEN通过稀疏正则化的方法减少了之前任务的权重。新添加的权值和旧的权值都能适应带有稀疏约束的新任务。然而,DEN并不能确保不遗忘。当旧任务权值和新任务权值联合训练时,会选择并修改部分旧任务权值。因此,我们引入了一个“分割和复制”的步骤来进一步恢复一些为减少遗忘效果而修改过的旧权值。Pack and Expand (PAE)[12]是我们之前利用PackNet[23]和ProgressiveNet[38]的方法。它可以避免遗忘,保持模型的紧凑性,允许动态的模型扩展。但是,由于它使用了之前任务的所有权重进行共享,所以在学习新任务时,性能会变得不太好。
我们的方法(CPG)是通过一个压缩挑选(增长)循环来完成的,它从旧任务中选择权重,而不修改它们,从而避免遗忘。此外,我们的方法不需要像DEN那样恢复旧任务的性能,因为性能已经保持,从而避免了繁琐的“分割和复制”过程,需要额外的时间调整模型,也会影响新任务的性能。因此,我们的方法简单且更容易实现。实验结果表明,该方法的性能也优于DEN和PAE。

3 The CPG approach for Continual Lifelong Learning

为了不失一般性,我们的工作遵循基于任务的顺序学习设置,这是持续学习中的常见设置。在下面,我们以顺序任务的方式展示我们的方法。

T a s k   1 Task ~1 Task 1
给定第一个任务( T a s k   1 Task ~1 Task 1)和通过其数据集训练的初始模型,我们对模型执行逐步剪枝[51],以去除冗余,同时保持性能。逐渐的修剪删除了部分权重,并训练模型迭代恢复性能,直到满足修剪标准,而不是一次修剪权重到修剪比率目标。因此,我们压缩当前模型,以便删除(或释放)模型权重中的冗余,然后将紧凑模型中的权重设置为不变且保持不变,以避免遗忘。逐步修剪后,模型权重可分为两部分:第一部分为任务1保留;另一部分被释放,可以被随后的任务所使用。

T a s k   k − k + 1 Task~ k-k+1 Task kk+1
假设在 T a s k   k Task~ k Task k 中,已经构建了一个可以处理 T a s k   1 − k Task~ 1-k Task 1k 的压缩模型。为 T a s k   1 − k Task~ 1-k Task 1k 保留的模型权重记为 W 1 : k P W^P_{1:k} W1:kP ,与任务k相关的释放(冗余)权重表示为 W k E W^E_{k} WkE 它们是可以用于后续任务的额外权重。给定 T a s k   k + 1 Task~ k+1 Task k+1 的数据集,我们应用一个可学习的掩码 M M M 来提取旧的权重 W 1 : k P W^P_{1:k} W1:kP M ∈ { 0 , 1 } D M∈\{0,1\}^D M{ 0,1}D 其中 D D D 的维数是 W 1 : k P W^P_{1:k} W1:kP 。然后表示被选择的权重 M ⨀ W 1 : k P M\bigodot W^P_{1:k} MW1:kP 在不丧失一般性的情况下,我们使用背驮式(piggyback)方法[22],该方法学习实值掩码并应用阈值进行二值化构造 M M M。因此,给定一个新任务,我们通过一个可学习的掩码从被压缩的模型中选择一组权重(已知的临界权重)。此外我们将在新任务中使用 W k E W^E_{k} WkE 。掩码 M M M 和额外的权重 W k E W^E_{k} WkE 是在 T a s k   k + 1 Task~ k+1 Task k+1 的训练数据上通过反向传播在 T a s k   k + 1 Task~ k+1 Task k+1 的损失函数上一起学习的。由于二值化的掩码不可微,在训练二值化的掩码 M M M 时,我们在后向过程中更新实值掩码 M ^ \hat{M} M^。然后 M M M 通过一个 M ^ \hat{M} M^ 上的阈值进行量化并应用到前向计算。如果性能还不满意,模型架构可以增加更多的训练权重,也就是说, W k E W^E_{k} WkE 可以被增加额外的权重(比如卷积层中的新过滤器和全连接层中的节点),然后恢复对 M M M W k E W^E_{k} WkE 的训练。注意,在训练期间,掩码 M M M 和新的权重 W k E W^E_{k} WkE 可以被调整,但原来的权重 W 1 : k P W^P_{1:k} W1:kP 只有被 pick 的权重参与训练,其他的被固定。这样,旧的任务就可以被准确地召回。

T a s k   k + 1 Task~ k+1 Task k+1的压缩:
经过 M M M W k E W^E_{k} WkE 的学习,得到了 T a s k   k + 1 Task~k+1 Task k+1 的初始模型。然后,我们固定掩码 M M M 并对 W k E W^E_{k} WkE 进行逐步剪,从而得到 T a s k   k + 1 Task~k+1 Task k+1 的压缩模型 W k + 1 P W^P_{k+1} Wk+1P 和冗余(被释放)的权重 W k + 1 E W^E_{k+1} Wk+1E 旧任务的压缩模型随后变成 W 1 : ( k + 1 ) P = W 1 : k P ∪ W k + 1 P W^P_{1:(k+1)}=W^P_{1:k} \cup W^P_{k+1} W1:(k+1)P=W1:kPWk+1P 从一个任务到另一个任务的压缩和选择/扩展循环是重复的。
在这里插入图片描述
实验部分就不翻译了。

实验1复现

下载数据集解压到/data目录下后

1.baseline:VGG16

分为baseline和finetune两个版本
baseline:独立对20个任务进行训练
finetune:每次随机从已训练的任务模型中选择一个进行微调。

    if [ "$task_id" != "1" ]
    then
        echo "Start training task " $task_id
        python TOOLS/random_generate_task_id.py --curr_task_id $task_id
        initial_from_task_id=$?  # 返回上一指令的返回值
        echo "Initial for curr task is " $initial_from_task_id
        CUDA_VISIBLE_DEVICES=$GPU_ID python packnet_cifar100_main_normal.py \
            --arch $arch \
            --dataset ${dataset[task_id]} --num_classes 5 \
            --lr 5e-3 \
            --weight_decay 4e-5 \
            --save_folder checkpoints/finetune/experiment1/$arch/${dataset[task_id]} \
            --epochs $finetune_epochs \
            --mode finetune \
            --logfile logs/finetune_cifar100_acc_normal_5e-3.txt \
            --initial_from_task checkpoints/finetune/experiment1/$arch/${dataset[initial_from_task_id]}

那么这部分最关键的也就是参数MASK的实现,看论文的时候就很好奇。
看到代码之后方才觉得自己的编程基础还不扎实啊。
思路很简单,对于模型需要更改参数的结构定义对应shape的可训练tensor备用即可。

    if not masks:
        for name, module in model.named_modules():
            if isinstance(module, nn.Conv2d) or isinstance(module, nn.Linear):
                if 'classifiers' in name:
                    continue
                mask = torch.ByteTensor(module.weight.data.size()).fill_(0)
                if 'cuda' in module.weight.data.type():
                    mask = mask.cuda()
                masks[name] = mask

在这里插入图片描述
以下部分的代码在跑baseline的时候也会运行,但效果应是和单纯跑VGG16是一样的。
我们第一次看主要是留个印象,后续还会再出现,届时再做延伸分析。

Train部分代码
第一次或者单独训练每个任务的时候看不出来,但这里也是独立出一部分classifiers参数备用

    for tuple_ in named_params.items():
        if 'classifiers' in tuple_[0]:
            if '.{}.'.format(model.module.datasets.index(args.dataset)) in tuple_[0]:
                params_to_optimize_via_SGD.append(tuple_[1])
                named_params_to_optimize_via_SGD.append(tuple_)                
            continue
        else:
            params_to_optimize_via_SGD.append(tuple_[1])
            named_params_to_optimize_via_SGD.append(tuple_)

不使用框架自带的正则化,因为我们不想改变所有参数

    # here we must set weight decay to 0.0, 
    # because the weight decay strategy in build-in step() function will change every weight elem in the tensor,
    # which will hurt previous tasks' accuracy. (Instead, we do weight decay ourself in the `prune.py`)
    optimizer_network = optim.SGD(params_to_optimize_via_SGD, lr=lr,
                          weight_decay=0.0, momentum=0.9, nesterov=True)  
    if args.mode == 'prune':
        print()
        print('Sparsity ratio: {}'.format(args.one_shot_prune_perc))
        print('Before pruning: ')
        baseline_acc = manager.validate(start_epoch-1)
        print('Execute one shot pruning ...')
        manager.one_shot_prune(args.one_shot_prune_perc)
    elif args.mode == 'finetune':
        manager.pruner.make_finetuning_mask()

finetuning的时候将剪枝的权重设为可训练(具体调整看后续)

    def make_finetuning_mask(self):
        """Turns previously pruned weights into trainable weights for
           current dataset.
        """
        assert self.masks
        self.current_dataset_idx += 1

        for name, module in self.model.named_modules():
            if isinstance(module, nn.Conv2d) or isinstance(module, nn.Linear):
                if 'classifiers' in name:
                    continue
                mask = self.masks[name]
                mask[mask.eq(0)] = self.current_dataset_idx

train的每个batch反向传播后

				# Set fixed param grads to 0.
                self.pruner.do_weight_decay_and_make_grads_zero()
    def do_weight_decay_and_make_grads_zero(self):
        """Sets grads of fixed weights to 0."""
        assert self.masks
        for name, module in self.model.named_modules():
            if isinstance(module, nn.Conv2d) or isinstance(module, nn.Linear):
                if 'classifiers' in name:
                    continue
                mask = self.masks[name]
                # Set grads of all weights not belonging to current dataset to 0.
                if module.weight.grad is not None:
                    / 手动正则化
                    module.weight.grad.data.add_(self.args.weight_decay, module.weight.data)
                    / 不是本数据集的参数不进行梯度更新0
                    module.weight.grad.data[mask.ne(
                        self.current_dataset_idx)] = 0
module.weight.grad.data.add_(self.args.weight_decay, module.weight.data)

这句相当于手动正则化 计算逻辑为
A . a d d _ ( B , C ) = A

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值