【關(guān)于 數(shù)據(jù)增強 之 對抗訓(xùn)練】 那些你不知道的事
作者:楊夕
面筋地址:https://github.com/km1994/NLP-Interview-Notes
個人筆記:https://github.com/km1994/nlp_paper_study
個人介紹:大佬們好,我叫楊夕,該項目主要是本人在研讀頂會論文和復(fù)現(xiàn)經(jīng)典論文過程中,所見、所思、所想、所聞,可能存在一些理解錯誤,希望大佬們多多指正。
【注:手機閱讀可能圖片打不開?。?!】

一、介紹篇
1.1 什么是 對抗訓(xùn)練 ?
對抗訓(xùn)練 從 CV 引入到 NLP 領(lǐng)域,作為一種防御機制,能夠在修改部分信息的情況下,提高模型的泛化能力。
1.2 為什么 對抗訓(xùn)練 能夠 提高模型效果?
對抗樣本可以用來攻擊和防御,而對抗訓(xùn)練其實是“對抗”家族中防御的一種方式,其基本的原理呢,就是通過添加擾動構(gòu)造一些對抗樣本,放給模型去訓(xùn)練,以攻為守,提高模型在遇到對抗樣本時的魯棒性,同時一定程度也能提高模型的表現(xiàn)和泛化能力。
1.3 對抗訓(xùn)練 有什么特點?
對抗樣本一般需要具有兩個特點:
相對于原始輸入,所添加的擾動是微小的;
能使模型犯錯
1.4 對抗訓(xùn)練 的作用?
提高模型應(yīng)對惡意對抗樣本時的魯棒性;
作為一種regularization,減少overfitting,提高泛化能力。
二、概念篇
2.1 對抗訓(xùn)練的基本概念?
在原始輸入樣本 x 上加一個擾動 $r_adv$ ,得到對抗樣本后,用其進行訓(xùn)練。也就是說,問題可以被抽象成這么一個模型:

2.2 如何計算擾動?
動機:神經(jīng)網(wǎng)絡(luò)由于其線性的特點,很容易受到線性擾動的攻擊
方法:FGSM

注:sgn 為符號函數(shù), L 為損失函數(shù)。Goodfellow發(fā)現(xiàn),令 ε=0.25 ,用這個擾動能給一個單層分類器造成99.9%的錯誤率。
2.3 如何優(yōu)化?
動機:將問題重新定義成了一個找鞍點的問題
方法:Min-Max公式

注:公式由兩部分構(gòu)成:一個是內(nèi)部損失函數(shù)的最大化,一個是外部經(jīng)驗風(fēng)險的最小化 內(nèi)部max是為了找到worst-case的擾動,也就是攻擊,其中, L 為損失函數(shù), S 為擾動的范圍空間。外部min是為了基于該攻擊方式,找到最魯棒的模型參數(shù),也就是防御,其中 D 是輸入樣本的分布。
三、實戰(zhàn)篇
3.1 NLP 中經(jīng)典對抗訓(xùn)練 之 Fast Gradient Method(FGM)
方法:假設(shè)輸入的文本序列的embedding vectors [v1,v2,...,vT] 為 x ,embedding的擾動為:

注:實際上就是取消了符號函數(shù),用二范式做了一個scale,需要注意的是:這里的norm計算的是,每個樣本的輸入序列中出現(xiàn)過的詞組成的矩陣的梯度norm。原作者提供了一個TensorFlow的實現(xiàn) [10],在他的實現(xiàn)中,公式里的 x 是embedding后的中間結(jié)果(batch_size, timesteps, hidden_dim),對其梯度 g 的后面兩維計算norm,得到的是一個(batch_size, 1, 1)的向量 $||g||_2$ 。為了實現(xiàn)插件式的調(diào)用,筆者將一個batch抽象成一個樣本,一個batch統(tǒng)一用一個norm,由于本來norm也只是一個scale的作用,影響不大。
代碼實現(xiàn):
FGM 類實現(xiàn)
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 類調(diào)用
# 初始化
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()
注:PyTorch為了節(jié)約內(nèi)存,在backward的時候并不保存中間變量的梯度。因此,如果需要完全照搬原作的實現(xiàn),需要用register_hook接口[11]將embedding后的中間變量的梯度保存成全局變量,norm后面兩維,計算出擾動后,在對抗訓(xùn)練forward時傳入擾動,累加到embedding后的中間變量上,得到新的loss,再進行梯度下降。
3.2 NLP 中經(jīng)典對抗訓(xùn)練 之 Projected Gradient Descent(PGD)
動機:內(nèi)部max的過程,本質(zhì)上是一個非凹的約束優(yōu)化問題,F(xiàn)GM解決的思路其實就是梯度上升,那么FGM簡單粗暴的“一步到位”,是不是有可能并不能走到約束內(nèi)的最優(yōu)點呢?
方法:用Projected Gradient Descent(PGD)的方法,簡單的說,就是“小步走,多走幾步”,如果走出了擾動半徑為 ε 的空間,就映射回“球面”上,以保證擾動不要過大:

代碼實現(xiàn):
PGD 類實現(xiàn)
import torch
class PGD():
def __init__(self, model):
self.model = model
self.emb_backup = {}
self.grad_backup = {}
def attack(self, epsilon=1., alpha=0.3, emb_name='emb.', is_first_attack=False):
# emb_name這個參數(shù)要換成你模型中embedding的參數(shù)名
for name, param in self.model.named_parameters():
if param.requires_grad and emb_name in name:
if is_first_attack:
self.emb_backup[name] = param.data.clone()
norm = torch.norm(param.grad)
if norm != 0 and not torch.isnan(norm):
r_at = alpha * param.grad / norm
param.data.add_(r_at)
param.data = self.project(name, param.data, epsilon)
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.emb_backup
param.data = self.emb_backup[name]
self.emb_backup = {}
def project(self, param_name, param_data, epsilon):
r = param_data - self.emb_backup[param_name]
if torch.norm(r) > epsilon:
r = epsilon * r / torch.norm(r)
return self.emb_backup[param_name] + r
def backup_grad(self):
for name, param in self.model.named_parameters():
if param.requires_grad:
self.grad_backup[name] = param.grad.clone()
def restore_grad(self):
for name, param in self.model.named_parameters():
if param.requires_grad:
param.grad = self.grad_backup[name]
FGM 類調(diào)用
pgd = PGD(model)
K = 3
for batch_input, batch_label in data:
# 正常訓(xùn)練
loss = model(batch_input, batch_label)
loss.backward() # 反向傳播,得到正常的grad
pgd.backup_grad()
# 對抗訓(xùn)練
for t in range(K):
pgd.attack(is_first_attack=(t==0)) # 在embedding上添加對抗擾動, first attack時備份param.data
if t != K-1:
model.zero_grad()
else:
pgd.restore_grad()
loss_adv = model(batch_input, batch_label)
loss_adv.backward() # 反向傳播,并在正常的grad基礎(chǔ)上,累加對抗訓(xùn)練的梯度
pgd.restore() # 恢復(fù)embedding參數(shù)
# 梯度下降,更新參數(shù)
optimizer.step()
model.zero_grad()
參考
【煉丹技巧】功守道:NLP中的對抗訓(xùn)練 + PyTorch實現(xiàn)
