浅谈NLP中的对抗训练方式
?作者 | 林遠平
單位 | QTrade AI研發中心
研究方向 | 自然語言處理
前言
什么是對抗訓練呢?說起“對抗”,我們就想起了計算機視覺領域的對抗生成網絡(GAN)。在計算機視覺中,對抗思想的基本配置是一個生成器,一個判別器。生成器希望能夠生成一些樣本能夠“欺騙”判別器,讓判別器無法正確判別樣本。而判別器當然需要不斷增強自己的判別能力,使得生成器不能夠欺騙自己啦。在這個相互作用下,判別器的能力不斷增強,從而得到一個很好的判別器。
下面,我們舉一個對抗樣本的例子,決定就是你啦,長臂猿~~不對,搞錯了,再來,決定就是你啦,胖達。
▲ 圖1 對抗樣本實例
從圖中我們可以看到,panda 在噪聲的干擾下被識別成了長臂猿。
對抗樣本一般需要具有兩個特點:
1、相對于原始輸入,所添加的擾動是微小的;?
2、能使模型犯錯。
大名鼎鼎的 Min-Max 公式:
該公式分為兩個部分,一個是內部損失函數的最大化,一個是外部經驗風險的最小化。用一句話形容對抗訓練的思路,就是在輸入上進行梯度上升(增大 loss)使得輸入盡可能跟原來不一樣,在參數上進行梯度下降(減小 loss)使得模型盡可能能夠正確識別。
眾所周知,在 CV 中我們可以通過在原圖像中加入噪點,但是并不影響原圖像的性質。而在 NLP 領域,我們并不能直接的通過在詞編碼上添加噪點,因為詞嵌入本質上就是 one-hot,如果在 one-hot 上增加上述噪點,就會對原句產生歧義。因此,一個自然的想法就是在 embedding 上增加擾動。
FGM
說起 FGM,那么就繞不開它的前世 FGSM,
我們先講講 FGSM 的公式:
Sign 是一個符號函數,如下所示:
▲ 圖2 Sign函數
FGSM 作者的想法很簡單,就是把擾動加到參數中,這樣就能使得損失值越來越大,模型將輸入圖像錯分類成正確類別以外的其他任何一個類別都算攻擊成功。其論文通過對損失函數做一個梯度,然后對梯度取 sign,當梯度大于 0 時恒為 1,當梯度小于 0 時恒為 -1,當梯度等于 0 時恒為 0。
但是這樣的方式會有一個問題,就是會改變梯度的方向,為了解決這個問題,FGM 從而被提出了。
FGM 取消了符號函數,用二范數做了一個 scale,這樣的話剛好避免了 FGM 改變梯度方向的問題。
筆者一直在強調 FGSM 改變了梯度的方向,但可能很多剛接觸對抗訓練的小伙伴并不懂為什么這么說。下面給個證明:
假設梯度為 ,那么 ,而對于 ,因此可以看出 FGSM 改變了梯度的方向,而 FGM 只是做了個縮放,并沒有改變梯度的方向。
接下來筆者把 FGM 的代碼放下來。僅供參考。
import?torch class?FGM():def?__init__(self,?model):self.model?=?modelself.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?/?normparam.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.backupparam.data?=?self.backup[name]self.backup?=?{}使用對抗訓練的時候的代碼如下:
#?初始化 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()接下來我們思考一個問題:
通過 min-max 公式我們可以知道,對抗訓練主要做的就是內部 max 的過程,而在內部 max 的過程,本質上是一個非凹的約束優化問題,FGM 解決的思路其實就是梯度上升,那么 FGM 簡單粗暴的“一步到位”,是不是有可能并不能走到約束內的最優點呢?
既然一步走到位不行,那么我們就分多步來走,這樣不就行了嗎?
PGD
Madry 在 18 年提出了用 Projected Gradient Descent(PGD)的方法,簡單的說,就是“小步走,多走幾步”,如果走出了擾動半徑為的空間,就映射回“球面”上,以保證擾動不要過大。公式如下:
其中 , 為擾動的約束空間, 為每步的步長。
可能公式對于初學者來說不太友好,那么我們直接上偽代碼吧。
對于每個輸入x,假設運行K步: 1、計算x的前向loss、反向傳播得到的梯度并保存,備份初始時的embedding,對于每一步t:2、根據embedding矩陣的梯度計算出擾動r,并加到當前embedding上,相當于x+r(超出范圍則投影回epsilon內)3、當t不是第K-1步(K從0開始)時:將梯度變回0,根據(2)中的x+r計算前向loss和反向梯度4、當t是第K-1步(K從0開始)時:恢復(1)的梯度,計算最后的x+r并將梯度累加到(1)上 5、將embedding恢復為(1)時的值 6、根據(4)的梯度對參數進行更新如果偽代碼不好理解,那么我們就結合著代碼來看吧。
import?torch class?PGD():def?__init__(self,?model):self.model?=?modelself.emb_backup?=?{}self.grad_backup?=?{}def?attack(self,?epsilon=1.,?alpha=0.3,?emb_name='emb.',?is_first_attack=False):#?emb_name這個參數要換成你模型中embedding的參數名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?/?normparam.data.add_(r_at)param.data?=?self.project(name,?param.data,?epsilon)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.emb_backupparam.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]?+?rdef?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]使用 PGD 的時候代碼如下:
pgd?=?PGD(model) K?=?3 for?batch_input,?batch_label?in?data:#?正常訓練loss?=?model(batch_input,?batch_label)loss.backward()?#?反向傳播,得到正常的gradpgd.backup_grad()#?對抗訓練for?t?in?range(K):pgd.attack(is_first_attack=(t==0))?#?在embedding上添加對抗擾動,?first?attack時備份param.dataif?t?!=?K-1:model.zero_grad()else:pgd.restore_grad()loss_adv?=?model(batch_input,?batch_label)loss_adv.backward()?#?反向傳播,并在正常的grad基礎上,累加對抗訓練的梯度pgd.restore()?#?恢復embedding參數#?梯度下降,更新參數optimizer.step()model.zero_grad()FreeLB
FreeLB 是在 PGD 的基礎上做了一些改進,我們首先對比一下 PGD 和 FreeLB 的公式
PGD 的公式:
FreeLB 的公式:
FreeLB 和 PGD 主要有兩點區別:?
1、PGD 是迭代 K 次 r 后取最后一次擾動的梯度更新參數,FreeLB 是取 K 次迭代中的平均梯度
2、PGD 的擾動范圍都在 epsilon 內,因為 PGD 偽代碼第 3 步將梯度歸 0 了,每次投影都會回到以第 1 步 x 為圓心,半徑是 epsilon 的圓內,而 FreeLB 每次的 x 都會迭代,所以 r 的范圍更加靈活,更可能接近局部最優
作者原文的偽代碼如下:
自己對于偽代碼的理解:
對于每個輸入x:1、通過均勻分布初始化擾動r,初始化梯度g為0,設置步數為K對于每步t=1...K:2、根據x+r計算前向loss和后向梯度g1,累計梯度g=g+g1/k3、更新擾動r,更新方式跟PGD一樣4、根據g更新梯度然后我們結合代碼來代碼來理解吧:
class?FreeLB():def?__init__(self,?model,?args,?optimizer,?base_model='xlm-roberta'):self.args?=?argsself.model?=?modelself.adv_K?=?self.args.adv_Kself.adv_lr?=?self.args.adv_lrself.adv_max_norm?=?self.args.adv_max_normself.adv_init_mag?=?self.args.adv_init_mag??#?adv-training?initialize?with?what?magnitude,?即我們用多大的數值初始化deltaself.adv_norm_type?=?self.args.adv_norm_typeself.base_model?=?base_modelself.optimizer?=?optimizerdef?attack(self,?model,?inputs):args?=?self.argsinput_ids?=?inputs['input_ids']#獲取初始化時的embeddingembeds_init?=?getattr(model,?self.base_model).embeddings.word_embeddings(input_ids.to(args.device))if?self.adv_init_mag?>?0:???#?影響attack首步是基于原始梯度(delta=0),還是對抗梯度(delta!=0)input_mask?=?inputs['attention_mask'].to(embeds_init)input_lengths?=?torch.sum(input_mask,?1)if?self.adv_norm_type?==?"l2":delta?=?torch.zeros_like(embeds_init).uniform_(-1,?1)?*?input_mask.unsqueeze(2)dims?=?input_lengths?*?embeds_init.size(-1)mag?=?self.adv_init_mag?/?torch.sqrt(dims)delta?=?(delta?*?mag.view(-1,?1,?1)).detach()else:delta?=?torch.zeros_like(embeds_init)??#?擾動初始化#?loss,?logits?=?None,?Nonefor?astep?in?range(self.adv_K):delta.requires_grad_()inputs['inputs_embeds']?=?delta?+?embeds_init??#?累積一次擾動delta#?inputs['input_ids']?=?Noneloss,?_?=?model(input_ids=None,attention_mask=inputs["attention_mask"].to(args.device),token_type_ids=inputs["token_type_ids"].to(args.device),labels=inputs["sl_labels"].to(args.device),inputs_embeds=inputs["inputs_embeds"].to(args.device))loss?=?loss?/?self.adv_Kloss.backward()if?astep?==?self.adv_K?-?1:#?further?updates?on?deltabreakdelta_grad?=?delta.grad.clone().detach()??#?備份擾動的gradif?self.adv_norm_type?==?"l2":denorm?=?torch.norm(delta_grad.view(delta_grad.size(0),?-1),?dim=1).view(-1,?1,?1)denorm?=?torch.clamp(denorm,?min=1e-8)delta?=?(delta?+?self.adv_lr?*?delta_grad?/?denorm).detach()if?self.adv_max_norm?>?0:delta_norm?=?torch.norm(delta.view(delta.size(0),?-1).float(),?p=2,?dim=1).detach()exceed_mask?=?(delta_norm?>?self.adv_max_norm).to(embeds_init)reweights?=?(self.adv_max_norm?/?delta_norm?*?exceed_mask?+?(1?-?exceed_mask)).view(-1,?1,?1)delta?=?(delta?*?reweights).detach()else:raise?ValueError("Norm?type?{}?not?specified.".format(self.adv_norm_type))embeds_init?=?getattr(model,?self.base_model).embeddings.word_embeddings(input_ids.to(args.device))return?loss使用 FreeLB 的時候代碼如下:
for?batch_input,?batch_label?in?data:#?正常訓練loss?=?model(batch_input,?batch_label)loss.backward()?#?反向傳播,得到正常的grad#?對抗訓練freelb?=?FreeLB(?model,?args,?optimizer,?base_model)loss_adv?=?freelb.attack(model,?batch_input)loss_adv.backward()?#?反向傳播,并在正常的grad基礎上,累加對抗訓練的梯度#?梯度下降,更新參數optimizer.step()model.zero_grad()總結
其實對抗訓練在 NLP 中的效果還是不錯的,筆者介紹的幾種對比算法都是在前人的基礎上不斷地進行改進,FGSM->FGM->PGD->FreeLB,雖然每個改動點都不是很大,但是確實比較有效果。當然,并不是說 FreeLB 就是最有效的。每個數據集都會有差異,從而會出現 PGD 在某數據集效果是最好的,但是在另外一個數據集反而不如 FGM 的情況也是有可能的。希望本文能對讀者們在對抗訓練方法方面有一定的幫助,能夠有進一步的認識。如果本文哪里您覺得不對的,也歡迎指出,謝謝!
參考文獻
[1] FGSM: Explaining and Harnessing Adversarial Examples
[2] FGM: Adversarial Training Methods for Semi-Supervised Text Classification
[3] FreeLB: Enhanced Adversarial Training for Language Understanding
[4]?訓練技巧 | 功守道:NLP中的對抗訓練 + PyTorch實現
特別鳴謝
感謝 TCCI 天橋腦科學研究院對于 PaperWeekly 的支持。TCCI 關注大腦探知、大腦功能和大腦健康。
更多閱讀
#投 稿?通 道#
?讓你的文字被更多人看到?
如何才能讓更多的優質內容以更短路徑到達讀者群體,縮短讀者尋找優質內容的成本呢?答案就是:你不認識的人。
總有一些你不認識的人,知道你想知道的東西。PaperWeekly 或許可以成為一座橋梁,促使不同背景、不同方向的學者和學術靈感相互碰撞,迸發出更多的可能性。?
PaperWeekly 鼓勵高校實驗室或個人,在我們的平臺上分享各類優質內容,可以是最新論文解讀,也可以是學術熱點剖析、科研心得或競賽經驗講解等。我們的目的只有一個,讓知識真正流動起來。
📝?稿件基本要求:
? 文章確系個人原創作品,未曾在公開渠道發表,如為其他平臺已發表或待發表的文章,請明確標注?
? 稿件建議以?markdown?格式撰寫,文中配圖以附件形式發送,要求圖片清晰,無版權問題
? PaperWeekly 尊重原作者署名權,并將為每篇被采納的原創首發稿件,提供業內具有競爭力稿酬,具體依據文章閱讀量和文章質量階梯制結算
📬?投稿通道:
? 投稿郵箱:hr@paperweekly.site?
? 來稿請備注即時聯系方式(微信),以便我們在稿件選用的第一時間聯系作者
? 您也可以直接添加小編微信(pwbot02)快速投稿,備注:姓名-投稿
△長按添加PaperWeekly小編
🔍
現在,在「知乎」也能找到我們了
進入知乎首頁搜索「PaperWeekly」
點擊「關注」訂閱我們的專欄吧
·
·
總結
以上是生活随笔為你收集整理的浅谈NLP中的对抗训练方式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 朋友共同買房房產證可以減名嗎
- 下一篇: 防臭地漏为什么不下水?