Gradient Episodic Memory for Continual Learning 论文阅读+代码解析

一. 介绍

在开始进行监督学习的时候我们需要收集一个训练集 D t r = { ( x i , y i ) } i = 1 n D_{tr}=\{(x_i,y_i)\}^n_{i=1} Dtr={(xi,yi)}i=1n。大部门的监督学习都假定每一个例子都是从一个固定的概率分布P中进行独立同分布的采样。监督学习的目标为构造一个模型 f f f去预测目标向量y通过x。为了实现这种假设,监督学习通常采用经验风险最小化原则(ERM), f f f在这种情况下为 1 ∣ D t r ∣ ∑ ( x i , y i ) ∈ D t r l ( f ( x i ) , y i ) \frac{1}{|D_{tr}|}\sum_{(x_i,y_i) \in D_{tr}}l(f(x_i),y_i) Dtr1(xi,yi)Dtrl(f(xi),yi)。实际上,ERM需要多次遍历训练集。
ERM是对我们认为的人类的学习的简化。然而现实中人们的观察是有序的,而且同一个例子智能记住少量的数据。因此,iid的假设和任何使用ERM原则的希望都破灭了。也就是说ERM会导致灾难性的遗忘。
本文通过缩小了ERM和人类学习的差距。值得注意的是,作者进行的学习是一个接一个,按顺序的到达。
( x 1 , t 1 , y 1 ) , . . . , ( x i , t i , y i ) , . . . , ( x n , t n . y n ) (1) (x_1,t_1,y_1),...,(x_i,t_i,y_i),...,(x_n,t_n.y_n) \tag{1} (x1,t1,y1),...,(xi,ti,yi),...,(xn,tn.yn)(1)
基于这种情况下,面临着ERM所不知的挑战:

  1. Non-iid的输入数据:数据的连续性对于任何固定概率分布 P ( X , T , Y ) P(X,T,Y) P(X,T,Y)都是不确定的,因为一旦任务切换,就可以观察到来自新任务的一系列例子。
  2. 灾难性遗忘:学习新任务可能会损害学习者之前已经解决的任务中的表现
  3. 迁移学习:当连续的任务互相关联时,存在迁移学习的机会。这将转化为更快地学习新任务,以提高旧任务的性能。

二. 持续学习的框架

依照(1)中所描述的数据,我们假设连续的任务分布中是局部的iid,也就是每一个 ( x i , t i , y i ) (x_i,t_i,y_i) (xi,ti,yi)都满足 ( x i , y i ) ~ i i d P t i ( X , Y ) (x_i,y_i)~_{iid}P_{t_i}(X,Y) (xi,yi)iidPti(X,Y)。我们的目标为学习到一个模型 f f f,能够在任何时刻进行预测,这样的测试可以属于我们在过去观察到的任务,当前的任务或者我们将来在未来经历的任务。
任务描述符: 在顺序任务进行中,任务描述符至关重要。丰富的描述符为零学习提供机会,因此任务之间可以单独使用新的任务描述符推断出来。此外,任务描述符消除了相似学习任务的歧义。在本文中更加关注于减轻灾难性遗忘,从一个连续的数据学习并为未来的研究留下零学习机会。

训练协议以及评估指标

本文中,考虑的任务序列的设定如下:a.任务数量很大,b.每个任务的训练例子很少,c.学习者只观察每个任务的例子一次,d.报告了衡量迁移和遗忘的指标。
因此,在训练时候,我们每一次仅给一个例子(或一个mini-batch)给学习者。同时学习者不会经历同一个例子两次,而且热舞是按照顺序进行。不需要对任务强加任何顺序,因为未来的任务可能与过去的重合。
除了监控学习者跨任务的表现,评估学习者转移知识的能力也很重要,更具体的说,需要测量:

  1. 向后的迁移(BWT)。这个因子主要为学习任务t之后看t以前任务的表现。一方面,学习某些任务会提高前一个任务的性能,存在正向后迁移。而有些则会导致负的迁移。大量的负迁移就会导致灾难性遗忘。
  2. 向前的迁移(FWT)。这个因子则是学习任务t之后看模型在未来任务学习中的表现。

对于针对上面原则的评估,考虑对T个任务中每个任务的测试集的访问。当一个模型完成了任务 t i t_i ti之后,我们需要评估他的测试表现能力对所有(T)的任务。这样做之后,我们可以构造一个矩阵 R ∈ R T ∗ T R\in \mathbb{R}^{T*T} RRTT,其中 R i , j R_{i,j} Ri,j表示为测试的准确率对于任务 t j t_j tj在观察了最后一个从 t i t_i ti的例子。让 b ˉ \bar{b} bˉ为每个任务的随机初始化时的测试精度向量,我们可以定义者三个矩阵:
A C C = 1 T ∑ i = 1 T R T , i (2) ACC = \frac{1}{T}\sum_{i=1}^TR_{T,i} \tag{2} ACC=T1i=1TRT,i(2)
B W T = 1 T − 1 ∑ i = 1 T − 1 R T , i − R i , i (3) BWT =\frac{1}{T-1}\sum_{i=1}^{T-1}R_{T,i}-R_{i,i} \tag{3} BWT=T11i=1T1RT,iRi,i(3)
B W T = 1 T − 1 ∑ i = 2 T − 1 R i − 1 , i − b ˉ i (4) BWT =\frac{1}{T-1}\sum_{i=2}^{T-1}R_{i-1,i}-\bar{b}_i \tag{4} BWT=T11i=2T1Ri1,ibˉi(4)

三. 情景记忆梯度(GEM)

GEM中主要特征为一个情景记忆 M t \mathcal{M}_t Mt,存储了从任务t中观察到的示例的子集。接下来,主要专注于通过情景记忆的有效使用来最小化负向的迁移(灾难性遗忘)。
实际汇总,学习者共有M个内存位置。如果任务数T是已知的,我们可以得出每个任务有m = M/T的内存空间。相反,如果任务数量未知,我们则需要不断减少m的值给新的任务。为了简单起见,我们假设内存是由每个任务的最后一个示例填充的,在这种情况下,可以定义从第k个任务的情景内存损失为:
l ( f θ , M k ) = 1 ∣ M k ∣ ∑ ( x i , k , y i ) ∈ M k l ( f θ ( x i , k ) , y i ) (5) l(f_\theta,\mathcal{M}_k)=\frac{1}{|\mathcal{M}_k|}\sum_{(x_i,k,y_i)\in \mathcal{M}_k}l(f_\theta(x_i,k),y_i) \tag{5} l(fθ,Mk)=Mk1(xi,k,yi)Mkl(fθ(xi,k),yi)(5)
显然,最小化当前的损失再加上(5)会导致在 M \mathcal{M} M中存在过拟合。作者将使用损失(5)作为不等式的约束,避免其增加但允许减少。也就是下面这个描述:
min ⁡ θ     l ( f θ ( x , t ) , y ) 其 中    l ( f θ , M k ) < = l ( f θ t − 1 , M k )   f o r   a l l   k < t , (6) \begin{aligned} \min_{\theta} \ \ \ &l(f_\theta(x,t),y) \\ 其中\ \ &l(f_\theta,\mathcal{M}_k)<=l(f_\theta^{t-1},\mathcal{M}_k) \ for\ all\ k<t,\tag{6} \end{aligned} θmin     l(fθ(x,t),y)l(fθ,Mk)<=l(fθt1,Mk) for all k<t,(6)
接下来,作者描述了两个重要的特征去处理问题(6)。第一是,没有必要去存储旧的模型 f θ t − 1 f_\theta^{t-1} fθt1,只要我么保证每次更新后,之前的任务损失不会增加。第二,假设函数是局部线性的,记忆是代表例子过去的任务,我们可以诊断之前损失增加的任务,计算他们损失向量之间的夹角。因此(6)可以表述为:
< g , g k > : = < ∂ l ( f θ ( x , t ) , y ) ∂ θ , ∂ l ( f θ , M k ) ∂ θ >   > = 0 , f o r   a l l   k < t (7) <g,g_k>:=<\frac{\partial l(f_\theta(x,t),y)}{\partial\theta},\frac{\partial l(f_\theta,\mathcal{M}_k)}{\partial\theta}>\ >=0,for\ all\ k<t \tag{7} <g,gk>:=<θl(fθ(x,t),y),θl(fθ,Mk)> >=0,for all k<t(7)
如果所有的都满足(7),那么参数更新g不可能增加之前任务的损失。另外,如果违反了一个或多个的话,那么至少有一个先前的任务在参数更新后损失会增加。那么可以将违反的梯度g投射到最接近的梯度 g ~ \tilde{g} g~(用平方的l2规范)。因此,到这里,关注点变为:
min ⁡ g ~ 1 2    ∣ ∣ g − g ~ ∣ ∣ 2 2 满 足 < g ~ , g k >    > = 0   f o r   a l l   k < t (8) \min_{\tilde{g}} \frac{1}{2}\ \ ||g-\tilde{g}||^2_2 \\满足<\tilde{g},g_k>\ \ >=0\ for\ all\ k<t \tag{8} g~min21  gg~22<g~,gk>  >=0 for all k<t(8)
到这一步可以发现我们只需要变换我们的 g ~ \tilde{g} g~使得其与每一个之前任务的向量夹角成锐角即可,而如何变化这个呢,作者将上述问题转换成2次规划的问题,然而转换后计算量过大,因此作者转换为2次规划的对偶问题求解。
min ⁡ z   1 2 v T G G T − g T z + g T G T v s u b j e c t   t o    v > = 0 (9) \min_z\ \frac{1}{2}v^TGG^T-g^Tz+g^TG^Tv \\ subject\ to \ \ v >=0 \tag{9} zmin 21vTGGTgTz+gTGTvsubject to  v>=0(9)
计算出v之后,我们可以获得我们新的 g ~ = G T v + g \tilde{g} = G^Tv+g g~=GTv+g
具体算法过程如下:
在这里插入图片描述
(图里的11对应我们的公式9)

四. 代码解析

作者的代码地址点这里
大家看完后肯定对二次规划那里比较疑惑,其实如果不是专门研究算法的,可以不用那么在意,因为我们只需要调用库即可。
再开始代码前,我们首先想一想这个论文和普通的DNN有什么区别。第一是,我们需要存储之前的任务(也就是开辟一个空间存储以前训练过的部分数据)。第二是,在进行求解损失时,需要对存储过的任务依次进行损失计算,并获得任务对应的梯度。第三就是,根据二次规划问题变换我们当前任务的梯度。
理清之后,我们就根据不同点一次进行代码解读。
首先是开辟空间存储数据,也就是拿一个列表(或数组)存一部分x和y
(关键代码对应的地址为 model/gem/observe)

## 当前训练的一个batch的数据(这里为10个)
bsz = y.data.size(0)
## 求出当前任务存储是否超过了内存限制(这里为每个任务最多存256个)
endcnt = min(self.mem_cnt + bsz, self.n_memories)
## 求出当前能存多少个(这里为<=10)
effbsz = endcnt - self.mem_cnt
## 存数据
self.memory_data[t, self.mem_cnt: endcnt].copy_(
    x.data[: effbsz])
if bsz == 1:
    self.memory_labs[t, self.mem_cnt] = y.data[0]
else:
    self.memory_labs[t, self.mem_cnt: endcnt].copy_(
        y.data[: effbsz])
## 更新内存指针方便下一次(指向下一次存储的开头)
self.mem_cnt += effbsz
if self.mem_cnt == self.n_memories:
    self.mem_cnt = 0

第一步做完之后,我们来看第二点不同,也就是求出之前任务的损失和梯度。

if len(self.observed_tasks) > 1:
    for tt in range(len(self.observed_tasks) - 1):
        self.zero_grad()
        # fwd/bwd on the examples in the memory
        past_task = self.observed_tasks[tt]
		## 这两个变量代表y的最小值和最大值
        offset1, offset2 = compute_offsets(past_task, self.nc_per_task,
                                           self.is_cifar)
        ptloss = self.ce(
            self.forward(
                self.memory_data[past_task],
                past_task)[:, offset1: offset2],
            self.memory_labs[past_task] - offset1)
        ptloss.backward()
        ## 存储
        store_grad(self.parameters, self.grads, self.grad_dims,
                   past_task)

最后也就是计算当前任务梯度以及找到是否有存在梯度夹角大于90度的情况:

self.zero_grad()
## 计算梯度
offset1, offset2 = compute_offsets(t, self.nc_per_task, self.is_cifar)
loss = self.ce(self.forward(x, t)[:, offset1: offset2], y - offset1)
loss.backward()

# check if gradient violates constraints
if len(self.observed_tasks) > 1:
    # copy gradient
    store_grad(self.parameters, self.grads, self.grad_dims, t)
    indx = torch.cuda.LongTensor(self.observed_tasks[:-1]) if self.gpu \
        else torch.LongTensor(self.observed_tasks[:-1])
    ## 计算梯度夹角
    dotp = torch.mm(self.grads[:, t].unsqueeze(0),
                    self.grads.index_select(1, indx))
    ## 存在夹角大于90则进行二次规划更新
    if (dotp < 0).sum() != 0:
        project2cone2(self.grads[:, t].unsqueeze(1),
                      self.grads.index_select(1, indx), self.margin)
        # copy gradients back
        overwrite_grad(self.parameters, self.grads[:, t],
                       self.grad_dims)
self.opt.step()

最后哦我们来看看二次规划中如何传参
这里大家对照着上面的(9)来看,因为求解v的过程有具体的库,我们只需要传入对应参数即可。
当前任务梯度:g
之前所有任务的梯度:G
(注意之前所有任务的梯度 G = − ( g 1 , . . . . , g t − 1 ) ) G=-(g_1,....,g_{t-1})) G=(g1,....,gt1))

def project2cone2(gradient, memories, margin=0.5, eps=1e-3):
    """
        Solves the GEM dual QP described in the paper given a proposed
        gradient "gradient", and a memory of task gradients "memories".
        Overwrites "gradient" with the final projected update.

        input:  gradient, p-vector
        input:  memories, (t * p)-vector
        output: x, p-vector
    """
    memories_np = memories.cpu().t().double().numpy()
    gradient_np = gradient.cpu().contiguous().view(-1).double().numpy()
    t = memories_np.shape[0]
    ## P = 1/2 *G *G^T
    P = np.dot(memories_np, memories_np.transpose())
    P = 0.5 * (P + P.transpose()) + np.eye(t) * eps
    ## q = g^T * G^T
    q = np.dot(memories_np, gradient_np) * -1
    G = np.eye(t)
    h = np.zeros(t) + margin
    v = quadprog.solve_qp(P, q, G, h)[0]
    x = np.dot(v, memories_np) + gradient_np
    gradient.copy_(torch.Tensor(x).view(-1, 1))

看完这篇文章,大家可以看一看我另一个解读改进GEM算法。

  • 5
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值