|公|眾|號|包包算法筆記|
背景
事情的起因其實這樣,實驗室老同學(xué)的論文要沖分,問我有沒有啥在NN上,基本都有用的刷點方法,最好是就是短小精悍,代碼量不大,不需要怎么調(diào)參。
一般通用的trick都被寫進(jìn)論文和代碼庫里了,
像優(yōu)秀的優(yōu)化器,學(xué)習(xí)率調(diào)度方法,數(shù)據(jù)增強,dropout,初始化,BN,LN,確實是調(diào)參大師的寶貴經(jīng)驗,大家平常用的也很多。
除了這些,天底下還有這樣的好事?
確實有一些這樣的方法的,他們通用,簡單。根據(jù)我的經(jīng)驗,在大多數(shù)的數(shù)據(jù)上都有效。
一、對抗訓(xùn)練
第一個,對抗訓(xùn)練。
對抗訓(xùn)練就是在輸入的層次增加擾動,根據(jù)擾動產(chǎn)生的樣本,來做一次反向傳播。
以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這個參數(shù)要換成你模型中embedding的參數(shù)名
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這個參數(shù)要換成你模型中embedding的參數(shù)名
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的實現(xiàn)
# 初始化
fgm = FGM(model)
for batch_input, batch_label in data:
# 正常訓(xùn)練
loss = model(batch_input, batch_label)
loss.backward() # 反向傳播,得到正常的grad
# 對抗訓(xùn)練
fgm.attack() # 在embedding上添加對抗擾動
loss_adv = model(batch_input, batch_label)
loss_adv.backward() # 反向傳播,并在正常的grad基礎(chǔ)上,累加對抗訓(xùn)練的梯度
fgm.restore() # 恢復(fù)embedding參數(shù)
# 梯度下降,更新參數(shù)
optimizer.step()
model.zero_grad()
二、EMA
第二個,EMA(指數(shù)滑動平均)
移動平均,保存歷史的一份參數(shù),在一定訓(xùn)練階段后,拿歷史的參數(shù)給目前學(xué)習(xí)的參數(shù)做一次平滑。這個東西,我之前在earhian的祖?zhèn)鞔a里看到的。他喜歡這東西+衰減學(xué)習(xí)率。確實每次都有用。
代碼引用博客:https://fyubang.com/2019/06/01/ema/
# 初始化
ema = EMA(model, 0.999)ema.register()# 訓(xùn)練過程中,更新完參數(shù)后,同步update shadow weightsdef train(): optimizer.step() ema.update()# eval前,apply shadow weights;eval之后,恢復(fù)原來模型的參數(shù)def evaluate(): ema.apply_shadow() # evaluate ema.restore()
具體EMA實現(xiàn),即插即用:
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()
# 訓(xùn)練過程中,更新完參數(shù)后,同步update shadow weights
def train():
optimizer.step()
ema.update()
# eval前,apply shadow weights;eval之后,恢復(fù)原來模型的參數(shù)
def evaluate():
ema.apply_shadow()
# evaluate
ema.restore()
三、TTA
第三個,TTA。
這個一句話說明白,測試時候構(gòu)造靠譜的數(shù)據(jù)增強,簡單一點的數(shù)據(jù)增強方式比較好,然后把預(yù)測結(jié)果加起來算個平均。
這個實現(xiàn)實在是比較簡單,就不貼代碼了。
四、偽標(biāo)簽
第四個,偽標(biāo)簽學(xué)習(xí)。
這個也一句話說明白,就是用訓(xùn)練的模型,把測試數(shù)據(jù),或者沒有標(biāo)簽的數(shù)據(jù),推斷一遍。構(gòu)成偽標(biāo)簽,然后拿回去訓(xùn)練。注意不要leak。
下面那個老圖,比較形象。

五、特定樣本處理
第五個,特定樣本處理。
說這個通用勉強一點,但確實在這類數(shù)據(jù)上基本都有效。
就是小樣本,長尾樣本,或者模型不太有把握的樣本。把分類過程為根據(jù)特征檢索的過程。
用向量表征去查找最近鄰樣本。
這塊,有個ICLR2020的文章寫的比較好,facebook的老哥把幾種典型的方法整理了一下,具體可以參考: