敢问多任务学习优化算法路在何方?|附代码
本文包括以下几部分内容:
神经网络参数优化的基本背景
“时间域”里的优化算法(SGD/SGDM/AdamGrad/Adam等)进化史
“空间域“里的多任务学习优化算法改进PCGrad
“时空“里的MTL优化算法改进狂想曲
1. 神经网络参数优化的基本背景
假设输入为 (特征), (标签);神经网络的参数为 ;那么神经网络的输出 可以简单表示为:
如何优化?
我们定义一个优化目标 简写为 ,来表示预测与标签之间的差异,比如我们可以简单使用 来表示标签和预测结果之间的差异。我们希望可以调整 来让 和 尽可能的接近。于是就有了基本的优化公式:
其中 为学习率Learning Rate; 为目标函数对 的梯度(Gradient) 。 的具体求解方法(包括链式法则)不在本文范畴,现在假设求得 。
代表第 个任务的权重,一般来说可以直接让 ;并且当 的时候,退化为单任务学习。
注意上面式子的 严格来说是多任务学习里共享的参数 而不是所有参数。
为了方便讲述,下文统一将 或者 称为 梯度 各类优化算法所重点研究的便是:如何优化 。
4. 更新梯度: ,意思是:不使用当前时刻的梯度,而是假设参数先更新一步,再算出来一个未来的梯度,然后再进行步骤2,并结合以前的历史梯度信息来更新梯度。这也是步骤2里面有一个 的原因。
优于别人,并不高贵, 真正的高贵应该是 优于过去的自己,更要优于可见未来的自己!
多多笔记
AdaGrad
优化点在步骤1,不仅考虑参数的历史的梯度信息(SGD Momentum),还考虑历史梯度出现频繁程度(二阶动量) ,一个参数的梯度在历史上出现的次数越多 越大。最终 。在 时刻以前出现次数约多,则梯度小,出现次数少的参数,在 时刻的梯度就大一点。
AdaDelta/RMSProp
优化点在步骤2,把时间窗口,滑动平均的概念融合到 上, ,避免二阶动量一直相加越来越大。
Adam
既考虑一阶动量,也考虑二阶动量:
Nadam
把SGD Nesterov Accerleration再结合Adam就是Nadam了。
到目前为止不难看出,整个经典深度学习优化算法的进化历史,都是围绕“时间域”这个维度在进行的:
那么对于多任务学习,怎么样结合当前时刻 的梯度和历史梯度、假象的未来梯度,甚至是当前时刻多任务学习特有的梯度特点,让 时刻的梯度更优秀?下一章先谈谈如何结合多任务学习的特点对当前时刻的梯度进行改善。
3. '空间域“里的多任务学习优化算法改进PCGrad
有了第1部分多任务学习的参数(重点指共享参数)优化公式 ,第2部分在 时刻的参数优化步骤和,我们开始谈一谈PCGrad是如何在“空间域”上对优化算法进行改进的。
将第2部分的优化流程结合上PCGrad:
对于每一个btach,也就是在第 个epoch:
1. 计算目标函数在 时刻对神经网络参数 的梯度:
1.1 先分别计算每个目标函数对参数 的梯度
1.2 然后两两梯度之间看是否存在冲突
1.2.1如果存在冲突,则通过向量正交 映射减缓冲突
1.2.2 如果不存在冲突,不变
1.3 多个任务梯度求和得到 时刻参数的梯度
2. 根据 时刻前后的梯度 来优化 时刻的梯度 得到
3. 将优化后的 乘上学习率 得到
PCGrad特点:
PCGrad的改进位置:步骤1。从“空间”上优化对于当前时刻的梯度 计算。这里的“空间”广泛的指多任务学习对共享参数会有不同方向的梯度(多维梯度向量的方向不一样)。
改进的出发点是:在 时刻,多任务学习里多个目标对于共享参数 的梯度可能存在冲突。因为存在多个监督信息,那么多个监督信号可能想让共享参数往不同的方向调整。
具体改进是:将冲突减小,不存在冲突的梯度保持不变。当然论文中还有很多理论分析和证明,这里就不一一描述了。
为了让冲突变小,我们首先需要知道什么是冲突(这个冲突是作者自己定义的),然后想办法将冲突减小。简单来讲,不同任务对于参数的梯度冲突和不冲突的梯度是这个样子的(a和d):
图1 梯度冲突示意图
如图1a,任务 和任务 的梯度存在冲突。在 时刻,这两个任务的梯度在多维空间里指向了不同的方向(且角度大于90度,相当于两个向量合成的新向量的时候会相互抵消一部分,如果一个任务的梯度幅度远大于另一个任务的梯度幅度,那合成向量的方向将会直接变成了大梯度那个任务所要调整的方向了)。那么解决冲突的方式如图1b,c所示,把 往 的正交向量上映射,把 往 上的正交向量上映射。简单理解为:把朝向两个不同方向的向量往各自正交向量上折中一下,大家互相抵消也就小一些了。
结合前面的流程和图示,看到整个流程是不是觉得其实并不难?而且也很符合直觉。这也是笔者分享该方法的原因,但该算法还有改进的地方吗?有的,把该算法结合上“时间域”的改进,也就是下一章:“时空“里的MTL优化算法改进狂想曲,具体说说咱还能做的地方。
另外,看完原作者开源的tensorflow的写法:
https://github.com/tianheyu927/PCGrad/blob/master/PCGrad_tf.py
感觉不太习惯tensorflow的调试,因此本文在这里也分享一种比较容易看懂的pytorch写法方便大家上手:改一改深度学习参数优化过程的命脉--梯度:
def PCGrad_backward(net, optimizer,
X, y,
loss_layer=nn.CrossEntropyLoss()):
# net:训练的神经网络
# optimizer 优化器比如Adam
# X:神经网络d的输入
# y:标签
# loss_layer 目标函数
num_tasks = len(y) # T 任务数量
grads_task = [] #用来保存多个任务的梯度
losses = []
grad_shapes = [p.shape if p.requires_grad is True else None
for group in optimizer.param_groups
for p in group['params']] #获取梯度的shape
grad_numel = [p.numel() if p.requires_grad is True else 0
for group in optimizer.param_groups
for p in group['params']] #获取梯度的参数数量
optimizer.zero_grad() #t时刻优化前将梯度清零
# 针对每个任务先分别计算梯度
for i in range(num_tasks):
result = net(X)
loss = loss_layer(result[i], y[i])
losses.append(loss)
loss.backward()
devices = [p.device for group in optimizer.param_groups
for p in group['params']]
grad = [p.grad.detach().clone().flatten()
if (p.requires_grad is True
and p.grad is not None)
else None
for group in optimizer.param_groups
for p in group['params']]
# 对于当前任务该参数没有梯度,任务specific的参数,梯度设置为0
grads_task.append(torch.cat(
[g if g is not None else torch.zeros(
grad_numel[i], device=devices[i])
for i, g in enumerate(grad)]))
optimizer.zero_grad()
# 由于我们要在任务间两两配对看梯度是否冲突,先整体shuffle一下
random.shuffle(grads_task)
# 开始梯度冲突消除
grads_task = torch.stack(grads_task, dim=0) # (T, # of params)
proj_grad = grads_task.clone()
def _proj_grad(grad_task):
for k in range(num_tasks):
inner_product = torch.sum(grad_task*grads_task[k])
proj_direction = inner_product / (torch.sum(
grads_task[k]*grads_task[k])+1e-12)
grad_task = grad_task -
torch.min(
proj_direction, torch.zeros_like(proj_direction))
* grads_task[k]
return grad_task
# 减缓多个任务对应的梯度冲突并最后合并多个任务对应的梯度。
proj_grad = torch.sum(torch.stack(
list(map(_proj_grad, list(proj_grad)))), dim=0) # (of params, )
# 根据梯度的shape还原梯度。
indices = [0, ] + [v for v in accumulate(grad_numel)]
params = [p for group in optimizer.param_groups
for p in group['params']]
assert len(params) == len(grad_shapes) == len(indices[:-1])
#把减缓了冲突d的梯度放回参数中,之后便可以进行进行梯度下降了
for param, grad_shape, start_idx, end_idx in zip(params, grad_shapes, indices[:-1], indices[1:]):
if grad_shape is not None:
param.grad[...] = proj_grad[start_idx:end_idx].view(grad_shape) # copy proj grad
#输出losses 方便查看
return losses
完整的例子参考:
https://github.com/wgchang/PCGrad-pytorch-example/blob/master/pcgrad-example.py
4. “时空“里的MTL优化算法改进狂想曲
再次放上基本优化流程+PCGrad:
对于每一个btach,也就是第 个epoch:
1. 计算目标函数在 时刻对神经网络参数 的梯度:
1.1 先分别计算每个目标函数对参数 的梯度
1.2 然后两两梯度之间看是否存在冲突
1.2.1如果存在冲突,则通过向量正交 映射减缓冲突
1.2.2 如果不存在冲突,不变
1.3 多个任务梯度求和得到 时刻参数的梯度
2. 根据 时刻前后的梯度 来优化 时刻的梯度 得到
3. 将优化后的 乘上学习率 得到
咱再看一下这个PCGrad流程,发现仅仅在第1步计算梯度的时候对冲突进行改善。那结合咱们的SGD-》Adam的改进历史来看看PCGrad还有哪些地方没有可以探讨探讨的(下文为笔者脑爆时间,如果存在不合理不正确指出望读者见谅,也希望读者能一起思考):
咱们结合“时间”“空间“的改进来一波排列组合:
PCGrad Momentum
引入“梯度冲突”的一阶动量,也就是考虑了最近一段时间的历史梯度信息,看看多任务学习里不同任务对于共享参数在历史梯度里是否一直在发生冲突。如果是?咱们的PCGrad虽然在每个 时刻进行了修正,但“梯度冲突“是否也会累积呢?冲突累积多了是不是还是存在使用PCGrad之前的问题?
PCGrad Nesterov Accerleration
提前预测下一个时刻是否存在“梯度冲突”来帮助一下当前时刻?
AdaPCGrad
不仅仅考虑 时刻以前以及当前的“梯度冲突“,还要考虑“梯度冲突”发生的频繁程度(二阶动量)。如果两个任务之间“梯度冲突”的频率很高,应该如何是好?发生“梯度冲突”的频率低又应当如何?
AdaDelta/RMSProp PCGrad
把时间窗口,滑动平均的概念融合到“梯度冲突”二阶动量中去,看看最近一段时间的“梯度冲突”。
Adam PCGrad
既考虑“梯度冲突”的一阶动量,也要考虑二阶动量。
最后,“梯度冲突”反应的是两个任务之间的不相关性,那么“梯度契合”是不是可以反应两个任务之间的相关性?也可以把上面的流程走一遍,思考一遍。
以上便是笔者对于多任务学习优化算法的一点脑爆想法,由于笔者水平有限,不免有错误之处。欢迎读者朋友们批评指证!如果对您的研究思考有一丝启发,欢迎点赞哦~谢谢
参考文献:
1. https://ruder.io/optimizing-gradient-descent
2. https://arxiv.org/abs/2001.06782
3. https://zhuanlan.zhihu.com/p/32230623?utm_source=wechat_session&utm_medium=social&utm_oi=758092967545171968&utm_content=first