|公|众|号|包包算法笔记|
背景
事情的起因其实这样,实验室老同学的论文要冲分,问我有没有啥在NN上,基本都有用的刷点方法,最好是就是短小精悍,代码量不大,不需要怎么调参。
一般通用的trick都被写进论文和代码库里了,
像优秀的优化器,学习率调度方法,数据增强,dropout,初始化,BN,LN,确实是调参大师的宝贵经验,大家平常用的也很多。
除了这些,天底下还有这样的好事?
确实有一些这样的方法的,他们通用,简单。根据我的经验,在大多数的数据上都有效。
一、对抗训练
第一个,对抗训练。
对抗训练就是在输入的层次增加扰动,根据扰动产生的样本,来做一次反向传播。
以FGM为例,在NLP上,扰动作用于embedding层。
给个即插即用代码片段吧,引用了知乎id:Nicolas的代码,写的不错,带着看原理很容易就明白了。
import torch
class FGM():
def __init__(self, model):
self.model = model
self.backup = {}
def attack(self, epsilon=1., emb_name='emb.'):
# emb_name这个参数要换成你模型中embedding的参数名
for name, param in self.model.named_parameters():
if param.requires_grad and emb_name in name:
self.backup[name] = param.data.clone()
norm = torch.norm(param.grad)
if norm != 0 and not torch.isnan(norm):
r_at = epsilon * param.grad / norm
param.data.add_(r_at)
def restore(self, emb_name='emb.'):
# emb_name这个参数要换成你模型中embedding的参数名
for name, param in self.model.named_parameters():
if param.requires_grad and emb_name in name:
assert name in self.backup
param.data = self.backup[name]
self.backup = {}
具体FGM的实现
# 初始化
fgm = FGM(model)
for batch_input, batch_label in data:
# 正常训练
loss = model(batch_input, batch_label)
loss.backward() # 反向传播,得到正常的grad
# 对抗训练
fgm.attack() # 在embedding上添加对抗扰动
loss_adv = model(batch_input, batch_label)
loss_adv.backward() # 反向传播,并在正常的grad基础上,累加对抗训练的梯度
fgm.restore() # 恢复embedding参数
# 梯度下降,更新参数
optimizer.step()
model.zero_grad()
二、EMA
第二个,EMA(指数滑动平均)
移动平均,保存历史的一份参数,在一定训练阶段后,拿历史的参数给目前学习的参数做一次平滑。这个东西,我之前在earhian的祖传代码里看到的。他喜欢这东西+衰减学习率。确实每次都有用。
代码引用博客:https://fyubang.com/2019/06/01/ema/
# 初始化
ema = EMA(model, 0.999)ema.register()# 训练过程中,更新完参数后,同步update shadow weightsdef train(): optimizer.step() ema.update()# eval前,apply shadow weights;eval之后,恢复原来模型的参数def evaluate(): ema.apply_shadow() # evaluate ema.restore()
具体EMA实现,即插即用:
class EMA():
def __init__(self, model, decay):
self.model = model
self.decay = decay
self.shadow = {}
self.backup = {}
def register(self):
for name, param in self.model.named_parameters():
if param.requires_grad:
self.shadow[name] = param.data.clone()
def update(self):
for name, param in self.model.named_parameters():
if param.requires_grad:
assert name in self.shadow
new_average = (1.0 - self.decay) * param.data + self.decay * self.shadow[name]
self.shadow[name] = new_average.clone()
def apply_shadow(self):
for name, param in self.model.named_parameters():
if param.requires_grad:
assert name in self.shadow
self.backup[name] = param.data
param.data = self.shadow[name]
def restore(self):
for name, param in self.model.named_parameters():
if param.requires_grad:
assert name in self.backup
param.data = self.backup[name]
self.backup = {}
# 初始化
ema = EMA(model, 0.999)
ema.register()
# 训练过程中,更新完参数后,同步update shadow weights
def train():
optimizer.step()
ema.update()
# eval前,apply shadow weights;eval之后,恢复原来模型的参数
def evaluate():
ema.apply_shadow()
# evaluate
ema.restore()
三、TTA
第三个,TTA。
这个一句话说明白,测试时候构造靠谱的数据增强,简单一点的数据增强方式比较好,然后把预测结果加起来算个平均。
这个实现实在是比较简单,就不贴代码了。
四、伪标签
第四个,伪标签学习。
这个也一句话说明白,就是用训练的模型,把测试数据,或者没有标签的数据,推断一遍。构成伪标签,然后拿回去训练。注意不要leak。
下面那个老图,比较形象。
五、特定样本处理
第五个,特定样本处理。
说这个通用勉强一点,但确实在这类数据上基本都有效。
就是小样本,长尾样本,或者模型不太有把握的样本。把分类过程为根据特征检索的过程。
用向量表征去查找最近邻样本。
这块,有个ICLR2020的文章写的比较好,facebook的老哥把几种典型的方法整理了一下,具体可以参考:
https://arxiv.org/abs/1910.09217