训练技巧 | 功守道:NLP中的对抗训练 + PyTorch实现
作者丨Nicolas
單位丨追一科技AI Lab研究員
研究方向丨信息抽取、機器閱讀理解
最近,微軟的 FreeLB-Roberta [1] 靠著對抗訓練(Adversarial Training)在 GLUE 榜上超越了 Facebook 原生的 Roberta,追一科技也用到了這個方法僅憑單模型 [2] 就在 CoQA 榜單中超過了人類,似乎“對抗訓練”一下子變成了 NLP 任務的一把利器。剛好筆者最近也在看這方面的內容,所以開一篇文章,講一下。▲?GLUE Leaderboard
▲?CoQA Leaderboard
提到“對抗”,相信大多數人的第一反應都是 CV 中的對抗生成網絡(GAN),殊不知,其實對抗也可以作為一種防御機制,并且經過簡單的修改,便能用在 NLP 任務上,提高模型的泛化能力。關鍵是,對抗訓練可以寫成一個插件的形式,用幾行代碼就可以在訓練中自由地調用,簡單有效,使用成本低。不過網上的大多數博客對于 NLP 中的對抗訓練都介紹得比較零散且無代碼實現,筆者在這篇文章中,對 NLP 任務中的對抗訓練做了一個簡單的綜述,并提供了插件形式的 PyTorch 實現。?
本文專注于 NLP 對抗訓練的介紹,對對抗攻擊基礎感興趣的讀者,可以看這幾篇博客及論文 [3] [4] [5],這里就不贅述了。不想要理解理論細節的讀者也可以直接看最后的代碼實現。
對抗樣本
我們常常會聽到“對抗樣本”、“對抗攻擊”、“對抗訓練”等等這些令人頭禿的概念,為了讓大家對“對抗”有個更清晰的認識,我們先把這些概念捋捋清楚。▲?Taxonomy
Szegedy 在 14 年的 ICLR 中 [6] 提出了對抗樣本這個概念。如上圖,對抗樣本可以用來攻擊和防御,而對抗訓練其實是“對抗”家族中防御的一種方式,其基本的原理呢,就是通過添加擾動構造一些對抗樣本,放給模型去訓練,以攻為守,提高模型在遇到對抗樣本時的魯棒性,同時一定程度也能提高模型的表現和泛化能力。?
那么,什么樣的樣本才是好的對抗樣本呢?對抗樣本一般需要具有兩個特點:
1. 相對于原始輸入,所添加的擾動是微小的;2. 能使模型犯錯。
下面是一個對抗樣本的例子,決定就是你啦,胖達:
▲?一只胖達加了點擾動就被識別成了長臂猿
對抗訓練的基本概念
GAN 之父 Ian Goodfellow 在 15 年的 ICLR 中 [7] 第一次提出了對抗訓練這個概念,簡而言之,就是在原始輸入樣本 x 上加一個擾動,得到對抗樣本后,用其進行訓練。也就是說,問題可以被抽象成這么一個模型:
其中,y 為 gold label,θ 為模型參數。那擾動要如何計算呢?Goodfellow 認為,神經網絡由于其線性的特點,很容易受到線性擾動的攻擊。?
This linear behavior suggests that cheap, analytical perturbations of a linear model should also damage neural networks.
于是,他提出了 Fast Gradient Sign Method (FGSM) ,來計算輸入樣本的擾動。擾動可以被定義為:
其中,sgn 為符號函數,L 為損失函數。Goodfellow 發現,令 ?=0.25 ,用這個擾動能給一個單層分類器造成 99.9% 的錯誤率。看似這個擾動的發現有點拍腦門,但是仔細想想,其實這個擾動計算的思想可以理解為:將輸入樣本向著損失上升的方向再進一步,得到的對抗樣本就能造成更大的損失,提高模型的錯誤率。回想我們上一節提到的對抗樣本的兩個要求,FGSM 剛好可以完美地解決。?
在[7]中,Goodfellow 還總結了對抗訓練的兩個作用:?
1. 提高模型應對惡意對抗樣本時的魯棒性;?2. 作為一種 regularization,減少 overfitting,提高泛化能力。
Min-Max公式
在[7]中,對抗訓練的理論部分被闡述得還是比較 intuitive,Madry 在 2018 年的 ICLR 中[8]?總結了之前的工作,并從優化的視角,將問題重新定義成了一個找鞍點的問題,也就是大名鼎鼎的 Min-Max 公式:
該公式分為兩個部分,一個是內部損失函數的最大化,一個是外部經驗風險的最小化。?
1. 內部 max 是為了找到 worst-case 的擾動,也就是攻擊,其中,L 為損失函數,S 為擾動的范圍空間。?
2. 外部 min 是為了基于該攻擊方式,找到最魯棒的模型參數,也就是防御,其中 D 是輸入樣本的分布。?
Madry 認為,這個公式簡單清晰地定義了對抗樣本攻防“矛與盾”的兩個問題:如何構造足夠強的對抗樣本?以及,如何使模型變得刀槍不入?剩下的,就是如何求解的問題了。
從CV到NLP
以上提到的一些工作都還是停留在 CV 領域的,那么問題來了,可否將對抗訓練遷移到 NLP 上呢?答案是肯定的,但是,我們得考慮這么幾個問題:?首先,CV 任務的輸入是連續的 RGB 的值,而 NLP 問題中,輸入是離散的單詞序列,一般以 one-hot vector 的形式呈現,如果直接在 raw text 上進行擾動,那么擾動的大小和方向可能都沒什么意義。Goodfellow 在 17 年的 ICLR 中 [9]提出了可以在連續的 embedding 上做擾動:?
Because the set of high-dimensional one-hot vectors does not admit in?nitesimal perturbation, we de?ne the perturbation on continuous word embeddings instead of discrete word inputs.
乍一思考,覺得這個解決方案似乎特別完美。然而,對比圖像領域中直接在原始輸入加擾動的做法,在 embedding 上加擾動會帶來這么一個問題:這個被構造出來的“對抗樣本”并不能 map 到某個單詞,因此,反過來在 inference 的時候,對手也沒有辦法通過修改原始輸入得到這樣的對抗樣本。
我們在上面提到,對抗訓練有兩個作用,一是提高模型對惡意攻擊的魯棒性,二是提高模型的泛化能力。在 CV 任務,根據經驗性的結論,對抗訓練往往會使得模型在非對抗樣本上的表現變差,然而神奇的是,在 NLP 任務中,模型的泛化能力反而變強了,如[1]中所述:?
While adversarial training boosts the robustness, it is widely accepted by computer vision researchers that it is at odds with generalization, with classi?cation accuracy on non-corrupted images dropping as much as 10% on CIFAR-10, and 15% on Imagenet (Madry et al., 2018; Xie et al., 2019). Surprisingly, people observe the opposite result for language models (Miyato et al., 2017; Cheng et al., 2019), showing that adversarial training can improve both generalization and robustness.
因此,在 NLP 任務中,對抗訓練的角色不再是為了防御基于梯度的惡意攻擊,反而更多的是作為一種 regularization,提高模型的泛化能力。
有了這些“思想準備”,我們來看看 NLP 對抗訓練的常用的幾個方法和具體實現吧。
NLP中的兩種對抗訓練 + PyTorch實現
Fast Gradient Method(FGM)?
上面我們提到,Goodfellow 在 15 年的 ICLR [7] 中提出了 Fast Gradient Sign Method(FGSM),隨后,在 17 年的 ICLR [9] 中,Goodfellow 對 FGSM 中計算擾動的部分做了一點簡單的修改。假設輸入的文本序列的 embedding vectors ?為 x ,embedding 的擾動為:
實際上就是取消了符號函數,用二范式做了一個 scale,需要注意的是:這里的 norm 計算的是,每個樣本的輸入序列中出現過的詞組成的矩陣的梯度 norm。原作者提供了一個 TensorFlow 的實現 [10],在他的實現中,公式里的 x 是 embedding 后的中間結果(batch_size, timesteps, hidden_dim),對其梯度 g 的后面兩維計算 norm,得到的是一個 (batch_size, 1, 1) 的向量。
為了實現插件式的調用,筆者將一個 batch 抽象成一個樣本,一個 batch 統一用一個 norm,由于本來 norm 也只是一個 scale 的作用,影響不大。筆者的實現如下:
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()PyTorch 為了節約內存,在 backward 的時候并不保存中間變量的梯度。因此,如果需要完全照搬原作的實現,需要用 register_hook 接口 [11] 將 embedding 后的中間變量的梯度保存成全局變量,norm 后面兩維,計算出擾動后,在對抗訓練 forward 時傳入擾動,累加到 embedding 后的中間變量上,得到新的 loss,再進行梯度下降。不過這樣實現就與我們追求插件式簡單好用的初衷相悖,這里就不贅述了,感興趣的讀者可以自行實現。?
Projected Gradient Descent(PGD)
內部 max 的過程,本質上是一個非凹的約束優化問題,FGM 解決的思路其實就是梯度上升,那么 FGM 簡單粗暴的“一步到位”,是不是有可能并不能走到約束內的最優點呢?當然是有可能的。于是,一個很 intuitive 的改進誕生了:Madry 在 18 年的 ICLR 中 [8],提出了用 Projected Gradient Descent(PGD)的方法,簡單的說,就是“小步走,多走幾步”,如果走出了擾動半徑為 ? 的空間,就映射回“球面”上,以保證擾動不要過大:
其中為擾動的約束空間,α?為小步的步長。
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?param_data?+?rdef?backup_grad(self):for?name,?param?in?self.model.named_parameters():if?param.requires_grad:self.grad_backup[name]?=?param.graddef?restore_grad(self):for?name,?param?in?self.model.named_parameters():if?param.requires_grad:param.grad?=?self.grad_backup[name]使用的時候,要麻煩一點:
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()在 [8] 中,作者將這一類通過一階梯度得到的對抗樣本稱之為“一階對抗”,在實驗中,作者發現,經過 PGD 訓練過的模型,對于所有的一階對抗都能得到一個低且集中的損失值,如下圖所示:
我們可以看到,面對約束空間 S 內隨機采樣的十萬個擾動,PGD 模型能夠得到一個非常低且集中的 loss 分布,因此,在論文中,作者稱 PGD 為“一階最強對抗”。也就是說,只要能搞定 PGD 對抗,別的一階對抗就不在話下了。
實驗對照
為了說明對抗訓練的作用,筆者選了四個 GLUE 中的任務進行了對照試驗。實驗代碼是用的 Huggingface 的 transfomers/examples/run_glue.py [12],超參都是默認的,對抗訓練用的也是相同的超參。
我們可以看到,對抗訓練還是有效的,在 MRPC 和 RTE 任務上甚至可以提高三四個百分點。不過,根據我們使用的經驗來看,是否有效有時也取決于數據集。畢竟:緣,妙不可言~
總結
這篇博客梳理了 NLP 對抗訓練發展的來龍去脈,介紹了對抗訓練的數學定義,并對于兩種經典的對抗訓練方法,提供了插件式的實現,做了簡單的實驗對照。由于筆者接觸對抗訓練的時間也并不長,如果文中有理解偏差的地方,希望讀者不吝指出。
一個彩蛋:Virtual Adversarial Training
除了監督訓練,對抗訓練還可以用在半監督任務中,尤其對于 NLP 任務來說,很多時候輸入的無監督文本多的很,但是很難大規模地進行標注,那么就可以參考 [13] 中提到的 Virtual Adversarial Training 進行半監督訓練。?
首先,我們抽取一個隨機標準正態擾動(),加到 embedding 上,并用 KL 散度計算梯度:
然后,用得到的梯度,計算對抗擾動,并進行對抗訓練:
實現方法跟 FGM 差不多,這里就不給出了。
Reference
[1]?FreeLB: Enhanced Adversarial Training for Language Understanding?https://arxiv.org/abs/1909.11764?[2] Technical report on Conversational Question Answeringhttps://arxiv.org/abs/1909.10772?[3] EYD與機器學習:對抗攻擊基礎知識(一)https://zhuanlan.zhihu.com/p/37260275?[4] Towards a Robust Deep Neural Network in Text Domain A Surveyhttps://arxiv.org/abs/1902.07285?[5] Adversarial Attacks on Deep Learning Models in Natural Language Processing: A Surveyhttps://arxiv.org/abs/1901.06796?[6] Intriguing properties of neural networkshttps://arxiv.org/abs/1312.6199?[7] Explaining and Harnessing Adversarial Exampleshttps://arxiv.org/abs/1412.6572?[8] Towards Deep Learning Models Resistant to Adversarial Attackshttps://arxiv.org/abs/1706.06083?[9] Adversarial Training Methods for Semi-Supervised Text Classificationhttps://arxiv.org/abs/1605.07725?[10] Adversarial Text Classification原作實現https://github.com/tensorflow/models/blob/e97e22dfcde0805379ffa25526a53835f887a860/research/adversarial_text/adversarial_losses.py?[11] register_hook apihttps://www.cnblogs.com/SivilTaram/p/pytorch_intermediate_variable_gradient.html?[12] huggingface的transformershttps://github.com/huggingface/transformers/tree/master/examples?[13] Distributional Smoothing with Virtual Adversarial Traininghttps://arxiv.org/abs/1507.00677
點擊以下標題查看更多往期內容:?
#投 稿 通 道#
?讓你的論文被更多人看到?
如何才能讓更多的優質內容以更短路徑到達讀者群體,縮短讀者尋找優質內容的成本呢?答案就是:你不認識的人。
總有一些你不認識的人,知道你想知道的東西。PaperWeekly 或許可以成為一座橋梁,促使不同背景、不同方向的學者和學術靈感相互碰撞,迸發出更多的可能性。?
PaperWeekly 鼓勵高校實驗室或個人,在我們的平臺上分享各類優質內容,可以是最新論文解讀,也可以是學習心得或技術干貨。我們的目的只有一個,讓知識真正流動起來。
📝?來稿標準:
? 稿件確系個人原創作品,來稿需注明作者個人信息(姓名+學校/工作單位+學歷/職位+研究方向)?
? 如果文章并非首發,請在投稿時提醒并附上所有已發布鏈接?
? PaperWeekly 默認每篇文章都是首發,均會添加“原創”標志
📬 投稿郵箱:
? 投稿郵箱:hr@paperweekly.site?
? 所有文章配圖,請單獨在附件中發送?
? 請留下即時聯系方式(微信或手機),以便我們在編輯發布時和作者溝通
🔍
現在,在「知乎」也能找到我們了
進入知乎首頁搜索「PaperWeekly」
點擊「關注」訂閱我們的專欄吧
關于PaperWeekly
PaperWeekly 是一個推薦、解讀、討論、報道人工智能前沿論文成果的學術平臺。如果你研究或從事 AI 領域,歡迎在公眾號后臺點擊「交流群」,小助手將把你帶入 PaperWeekly 的交流群里。
▽ 點擊 |?閱讀原文?| 獲取最新論文推薦
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的训练技巧 | 功守道:NLP中的对抗训练 + PyTorch实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 保险属于什么行业类别
- 下一篇: 美联储不加息对黄金影响 美元与黄金价格多