一. 介绍
在开始进行监督学习的时候我们需要收集一个训练集
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)
∣Dtr?∣1?∑(xi?,yi?)∈Dtr??l(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所不知的挑战:
- Non-iid的输入数据:数据的连续性对于任何固定概率分布
P
(
X
,
T
,
Y
)
P(X,T,Y)
P(X,T,Y)都是不确定的,因为一旦任务切换,就可以观察到来自新任务的一系列例子。
- 灾难性遗忘:学习新任务可能会损害学习者之前已经解决的任务中的表现
- 迁移学习:当连续的任务互相关联时,存在迁移学习的机会。这将转化为更快地学习新任务,以提高旧任务的性能。
二. 持续学习的框架
依照(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?)~iid?Pti??(X,Y)。我们的目标为学习到一个模型
f
f
f,能够在任何时刻进行预测,这样的测试可以属于我们在过去观察到的任务,当前的任务或者我们将来在未来经历的任务。 任务描述符: 在顺序任务进行中,任务描述符至关重要。丰富的描述符为零学习提供机会,因此任务之间可以单独使用新的任务描述符推断出来。此外,任务描述符消除了相似学习任务的歧义。在本文中更加关注于减轻灾难性遗忘,从一个连续的数据学习并为未来的研究留下零学习机会。
训练协议以及评估指标
本文中,考虑的任务序列的设定如下:a.任务数量很大,b.每个任务的训练例子很少,c.学习者只观察每个任务的例子一次,d.报告了衡量迁移和遗忘的指标。 因此,在训练时候,我们每一次仅给一个例子(或一个mini-batch)给学习者。同时学习者不会经历同一个例子两次,而且热舞是按照顺序进行。不需要对任务强加任何顺序,因为未来的任务可能与过去的重合。 除了监控学习者跨任务的表现,评估学习者转移知识的能力也很重要,更具体的说,需要测量:
- 向后的迁移(BWT)。这个因子主要为学习任务t之后看t以前任务的表现。一方面,学习某些任务会提高前一个任务的性能,存在正向后迁移。而有些则会导致负的迁移。大量的负迁移就会导致灾难性遗忘。
- 向前的迁移(FWT)。这个因子则是学习任务t之后看模型在未来任务学习中的表现。
对于针对上面原则的评估,考虑对T个任务中每个任务的测试集的访问。当一个模型完成了任务
t
i
t_i
ti?之后,我们需要评估他的测试表现能力对所有(T)的任务。这样做之后,我们可以构造一个矩阵
R
∈
R
T
?
T
R\in \mathbb{R}^{T*T}
R∈RT?T,其中
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=T1?i=1∑T?RT,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=T?11?i=1∑T?1?RT,i??Ri,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=T?11?i=2∑T?1?Ri?1,i??bˉ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?)=∣Mk?∣1?(xi?,k,yi?)∈Mk?∑?l(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θt?1?,Mk?)?for?all?k<t,?(6) 接下来,作者描述了两个重要的特征去处理问题(6)。第一是,没有必要去存储旧的模型
f
θ
t
?
1
f_\theta^{t-1}
fθt?1?,只要我么保证每次更新后,之前的任务损失不会增加。第二,假设函数是局部线性的,记忆是代表例子过去的任务,我们可以诊断之前损失增加的任务,计算他们损失向量之间的夹角。因此(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~?min?21???∣∣g?g~?∣∣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??21?vTGGT?gTz+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)
bsz = y.data.size(0)
endcnt = min(self.mem_cnt + bsz, self.n_memories)
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()
past_task = self.observed_tasks[tt]
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()
if len(self.observed_tasks) > 1:
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))
if (dotp < 0).sum() != 0:
project2cone2(self.grads[:, t].unsqueeze(1),
self.grads.index_select(1, indx), self.margin)
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?,....,gt?1?))
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 = np.dot(memories_np, memories_np.transpose())
P = 0.5 * (P + P.transpose()) + np.eye(t) * eps
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算法。
|