对抗训练浅谈:意义、方法和思考(附Keras实现)
?PaperWeekly 原創(chuàng) ·?作者|蘇劍林
單位|追一科技
研究方向|NLP、神經(jīng)網(wǎng)絡(luò)
當(dāng)前,說到深度學(xué)習(xí)中的對抗,一般會有兩個含義:一個是生成對抗網(wǎng)絡(luò)(Generative Adversarial Networks,GAN),代表著一大類先進(jìn)的生成模型;另一個則是跟對抗攻擊、對抗樣本相關(guān)的領(lǐng)域,它跟 GAN 相關(guān),但又很不一樣,它主要關(guān)心的是模型在小擾動下的穩(wěn)健性。
本人之前所涉及的對抗話題,都是前一種含義,而今天,我們來聊聊后一種含義中的“對抗訓(xùn)練”。
本文包括如下內(nèi)容:
對抗樣本、對抗訓(xùn)練等基本概念的介紹;
介紹基于快速梯度上升的對抗訓(xùn)練及其在 NLP 中的應(yīng)用;
給出了對抗訓(xùn)練的 Keras 實現(xiàn)(一行代碼調(diào)用);
討論了對抗訓(xùn)練與梯度懲罰的等價性;
基于梯度懲罰,給出了一種對抗訓(xùn)練的直觀的幾何理解。
方法介紹
近年來,隨著深度學(xué)習(xí)的日益發(fā)展和落地,對抗樣本也得到了越來越多的關(guān)注。
在 CV 領(lǐng)域,我們需要通過對模型的對抗攻擊和防御來增強(qiáng)模型的穩(wěn)健型,比如在自動駕駛系統(tǒng)中,要防止模型因為一些隨機(jī)噪聲就將紅燈識別為綠燈。
在 NLP 領(lǐng)域,類似的對抗訓(xùn)練也是存在的,不過 NLP 中的對抗訓(xùn)練更多是作為一種正則化手段來提高模型的泛化能力。
這使得對抗訓(xùn)練成為了 NLP 刷榜的“神器”之一,前有微軟通過 RoBERTa+ 對抗訓(xùn)練在 GLUE [1] 上超過了原生 RoBERTa,后有我司的同事通過對抗訓(xùn)練刷新了 CoQA [2] 榜單。這也成功引起了筆者對它的興趣,遂學(xué)習(xí)了一番,分享在此。
基本概念
要認(rèn)識對抗訓(xùn)練,首先要了解“對抗樣本”,它首先出現(xiàn)在論文 Intriguing properties of neural networks [3] 之中。
簡單來說,它是指對于人類來說“看起來”幾乎一樣、但對于模型來說預(yù)測結(jié)果卻完全不一樣的樣本,比如下面的經(jīng)典例子:
理解對抗樣本之后,也就不難理解各種相關(guān)概念了,比如“對抗攻擊”,其實就是想辦法造出更多的對抗樣本,而“對抗防御”,就是想辦法讓模型能正確識別更多的對抗樣本。
所謂對抗訓(xùn)練,則是屬于對抗防御的一種,它構(gòu)造了一些對抗樣本加入到原數(shù)據(jù)集中,希望增強(qiáng)模型對對抗樣本的魯棒性;同時,如本文開篇所提到的,在 NLP 中它通常還能提高模型的表現(xiàn)。
Min-Max
總的來說,對抗訓(xùn)練可以統(tǒng)一寫成如下格式:
其中?代表訓(xùn)練集,代表輸入,代表標(biāo)簽,是模型參數(shù),是單個樣本的 loss,是對抗擾動,是擾動空間。這個統(tǒng)一的格式首先由論文 Towards Deep Learning Models Resistant to Adversarial Attacks [4]?提出。
這個式子可以分步理解如下:
往屬于里邊注入擾動?,的目標(biāo)是讓?越大越好,也就是說盡可能讓現(xiàn)有模型的預(yù)測出錯;
當(dāng)然?也不是無約束的,它不能太大,否則達(dá)不到“看起來幾乎一樣”的效果,所以?要滿足一定的約束,常規(guī)的約束是?,其中?是一個常數(shù);
每個樣本都構(gòu)造出對抗樣本之后,用作為數(shù)據(jù)對去最小化loss來更新參數(shù)?(梯度下降);
反復(fù)交替執(zhí)行 1、2、3 步。
由此觀之,整個優(yōu)化過程是?和?交替執(zhí)行,這確實跟 GAN 很相似,不同的是,GAN 所?的自變量也是模型的參數(shù),而這里?的自變量則是輸入(的擾動量),也就是說要對每一個輸入都定制一步?。
快速梯度
現(xiàn)在的問題是如何計算?,它的目標(biāo)是增大?,而我們知道讓 loss 減少的方法是梯度下降,那反過來,讓 loss 增大的方法自然就是梯度上升,因此可以簡單地取:
當(dāng)然,為了防止?過大,通常要對?做些標(biāo)準(zhǔn)化,比較常見的方式是:
有了?之后,就可以代回式 (1) 進(jìn)行優(yōu)化:
這就構(gòu)成了一種對抗訓(xùn)練方法,被稱為?Fast Gradient Method(FGM),它由 GAN 之父 Goodfellow 在論文 Explaining and Harnessing Adversarial Examples [5] 首先提出。
此外,對抗訓(xùn)練還有一種方法,叫做?Projected Gradient Descent(PGD),其實就是通過多迭代幾步來達(dá)到讓?更大的?。
如果迭代過程中模長超過了?,就縮放回去,細(xì)節(jié)請參考Towards Deep Learning Models Resistant to Adversarial Attacks [6]。
但本文不旨在對對抗學(xué)習(xí)做完整介紹,而且筆者認(rèn)為它不如 FGM 漂亮有效,所以本文還是以 FGM 為重點。關(guān)于對抗訓(xùn)練的補(bǔ)充介紹,建議有興趣的讀者閱讀富邦同學(xué)寫的功守道:NLP中的對抗訓(xùn)練 + PyTorch實現(xiàn)。
回到NLP
對于 CV 領(lǐng)域的任務(wù),上述對抗訓(xùn)練的流程可以順利執(zhí)行下來,因為圖像可以視為普通的連續(xù)實數(shù)向量,也是一個實數(shù)向量,因此?依然可以是有意義的圖像。
但 NLP 不一樣,NLP 的輸入是文本,它本質(zhì)上是 one hot 向量(如果還沒認(rèn)識到這一點,歡迎閱讀詞向量與 Embedding 究竟是怎么回事?[7],而兩個不同的 one hot 向量,其歐氏距離恒為?,因此對于理論上不存在什么“小擾動”。
一個自然的想法是像論文 Adversarial Training Methods for Semi-Supervised Text Classification [8] 一樣,將擾動加到 Embedding 層。
這個思路在操作上沒有問題,但問題是,擾動后的 Embedding 向量不一定能匹配上原來的 Embedding 向量表,這樣一來對 Embedding 層的擾動就無法對應(yīng)上真實的文本輸入,這就不是真正意義上的對抗樣本了,因為對抗樣本依然能對應(yīng)一個合理的原始輸入。
那么,在 Embedding 層做對抗擾動還有沒有意義呢?有!實驗結(jié)果顯示,在很多任務(wù)中,在 Embedding 層進(jìn)行對抗擾動能有效提高模型的性能。
實驗結(jié)果
既然有效,那我們肯定就要親自做實驗驗證一下了。怎么通過代碼實現(xiàn)對抗訓(xùn)練呢?怎么才能做到用起來盡可能簡單呢?最后用起來的效果如何呢?
思路分析
對于 CV 任務(wù)來說,一般輸入張量的 shape 是?,這時候我們需要固定模型的 batch size(即),然后給原始輸入加上一個 shape 同樣為?、全零初始化的Variable。
比如就叫做?,那么我們可以直接求 loss 對?的梯度,然后根據(jù)梯度給?賦值,來實現(xiàn)對輸入的干擾,完成干擾之后再執(zhí)行常規(guī)的梯度下降。
對于 NLP 任務(wù)來說,原則上也要對 Embedding 層的輸出進(jìn)行同樣的操作,Embedding 層的輸出 shape 為?,所以也要在 Embedding 層的輸出加上一個 shape 為?的Variable,然后進(jìn)行上述步驟。但這樣一來,我們需要拆解、重構(gòu)模型,對使用者不夠友好。
不過,我們可以退而求其次。Embedding 層的輸出是直接取自于 Embedding 參數(shù)矩陣的,因此我們可以直接對 Embedding 參數(shù)矩陣進(jìn)行擾動。
這樣得到的對抗樣本的多樣性會少一些(因為不同樣本的同一個 token 共用了相同的擾動),但仍然能起到正則化的作用,而且這樣實現(xiàn)起來容易得多。
代碼參考
基于上述思路,這里給出 Keras 下基于 FGM 方式對 Embedding 層進(jìn)行對抗訓(xùn)練的參考實現(xiàn):
https://github.com/bojone/keras_adversarial_training
核心代碼如下:
def?adversarial_training(model,?embedding_name,?epsilon=1):"""給模型添加對抗訓(xùn)練其中model是需要添加對抗訓(xùn)練的keras模型,embedding_name則是model里邊Embedding層的名字。要在模型compile之后使用。"""if?model.train_function?is?None:??#?如果還沒有訓(xùn)練函數(shù)model._make_train_function()??#?手動makeold_train_function?=?model.train_function??#?備份舊的訓(xùn)練函數(shù)#?查找Embedding層for?output?in?model.outputs:embedding_layer?=?search_layer(output,?embedding_name)if?embedding_layer?is?not?None:breakif?embedding_layer?is?None:raise?Exception('Embedding?layer?not?found')#?求Embedding梯度embeddings?=?embedding_layer.embeddings??#?Embedding矩陣gradients?=?K.gradients(model.total_loss,?[embeddings])??#?Embedding梯度gradients?=?K.zeros_like(embeddings)?+?gradients[0]??#?轉(zhuǎn)為dense?tensor#?封裝為函數(shù)inputs?=?(model._feed_inputs?+model._feed_targets?+model._feed_sample_weights)??#?所有輸入層embedding_gradients?=?K.function(inputs=inputs,outputs=[gradients],name='embedding_gradients',)??#?封裝為函數(shù)def?train_function(inputs):??#?重新定義訓(xùn)練函數(shù)grads?=?embedding_gradients(inputs)[0]??#?Embedding梯度delta?=?epsilon?*?grads?/?(np.sqrt((grads**2).sum())?+?1e-8)??#?計算擾動K.set_value(embeddings,?K.eval(embeddings)?+?delta)??#?注入擾動outputs?=?old_train_function(inputs)??#?梯度下降K.set_value(embeddings,?K.eval(embeddings)?-?delta)??#?刪除擾動return?outputsmodel.train_function?=?train_function??#?覆蓋原訓(xùn)練函數(shù)?
定義好上述函數(shù)后,給 Keras 模型增加對抗訓(xùn)練就只需要一行代碼了:
?
需要指出的是,由于每一步算對抗擾動也需要計算梯度,因此每一步訓(xùn)練一共算了兩次梯度,因此每步的訓(xùn)練時間會翻倍。
效果比較
為了測試實際效果,筆者選了中文 CLUE 榜?[9] 的兩個分類任務(wù):IFLYTEK和TNEWS,模型選擇了中文 BERT base。
在 CLUE 榜單上,BERT base 模型在這兩個數(shù)據(jù)上的成績分別是 60.29% 和56.58%,經(jīng)過對抗訓(xùn)練后,成績?yōu)?62.46%、57.66%,分別提升了 2% 和 1%!
訓(xùn)練腳本請參考:
https://github.com/bojone/bert4keras/blob/master/examples/task_iflytek_adversarial_training.py
當(dāng)然,同所有正則化手段一樣,對抗訓(xùn)練也不能保證每一個任務(wù)都能有提升,但從目前大多數(shù)“戰(zhàn)果”來看,它是一種非常值得嘗試的技術(shù)手段。
此外,BERT 的 finetune 本身就是一個非常玄乎(靠人品)的過程,前些時間論文Fine-Tuning Pretrained Language Models: Weight Initializations, Data Orders, and Early Stopping [10] 換用不同的隨機(jī)種子跑了數(shù)百次 finetune 實驗,發(fā)現(xiàn)最好的結(jié)果能高出好幾個點,所以如果你跑了一次發(fā)現(xiàn)沒提升,不妨多跑幾次再下結(jié)論。
延伸思考
在這一節(jié)中,我們從另一個視角對上述結(jié)果進(jìn)行分析,從而推出對抗訓(xùn)練的另一種方法,并且得到一種關(guān)于對抗訓(xùn)練的更直觀的幾何理解。
梯度懲罰
假設(shè)已經(jīng)得到對抗擾動?,那么我們在更新?時,考慮對?的展開:
對應(yīng)的?的梯度為:
代入?,得到:
這個結(jié)果表示,對輸入樣本施加?的對抗擾動,一定程度上等價于往 loss 里邊加入梯度懲罰。
如果對抗擾動是?,那么對應(yīng)的梯度懲罰項則是?(少了個,也少了個2次方)。
事實上,這個結(jié)果不是新的,據(jù)筆者所知,它首先出現(xiàn)論文 Improving the Adversarial Robustness and Interpretability of Deep Neural Networks by Regularizing their Input Gradients?[11] 里。
只不過這篇文章不容易搜到,因為你一旦搜索“adversarial training gradient penalty”等關(guān)鍵詞,出來的結(jié)果幾乎都是 WGAN-GP 相關(guān)的東西。
幾何圖像
事實上,關(guān)于梯度懲罰,我們有一個非常直觀的幾何圖像。以常規(guī)的分類問題為例,假設(shè)有?個類別,那么模型相當(dāng)于挖了?個坑,然后讓同類的樣本放到同一個坑里邊去:
梯度懲罰則說同類樣本不僅要放在同一個坑內(nèi),還要放在坑底,這就要求每個坑的內(nèi)部要長這樣:
為什么要在坑底呢?因為物理學(xué)告訴我們,坑底最穩(wěn)定呀,所以就越不容易受干擾呀,這不就是對抗訓(xùn)練的目的么?
那坑底意味著什么呢?極小值點呀,導(dǎo)數(shù)(梯度)為零呀,所以不就是希望?越小越好么?
這便是梯度懲罰 (8) 的幾何意義了。類似的“挖坑”、“坑底”與梯度懲罰的幾何圖像,還可以參考能量視角下的GAN模型:GAN=“挖坑”+“跳坑”。
L約束
我們還可以從 L 約束(Lipschitz 約束)的角度來看梯度懲罰。所謂對抗樣本,就是輸入的小擾動導(dǎo)致輸出的大變化,而關(guān)于輸入輸出的控制問題,我們之前在文章深度學(xué)習(xí)中的Lipschitz約束:泛化與生成模型就已經(jīng)探討過。
一個好的模型,理論上應(yīng)該是“輸入的小擾動導(dǎo)致導(dǎo)致輸出的小變化”,而為了保證這一遍,一個很常用的方案是讓模型滿足 L 約束,即存在常數(shù)?,使得
這樣一來只要兩個輸出的差距?足夠小,那么就能保證輸出的差距也足夠小。
而深度學(xué)習(xí)中的Lipschitz約束:泛化與生成模型已經(jīng)討論了,實現(xiàn) L 約束的方案之一就是譜歸一化(Spectral Normalization),所以往神經(jīng)網(wǎng)絡(luò)里邊加入譜歸一化,就可以增強(qiáng)模型的對抗防御性能。
相關(guān)的工作已經(jīng)被發(fā)表在 Generalizable Adversarial Training via Spectral Normalization [12]。
美中不足的是,譜歸一化是對模型的每一層權(quán)重都進(jìn)行這樣的操作,結(jié)果就是神經(jīng)網(wǎng)絡(luò)的每一層都滿足 L 約束,這是不必要的(我們只希望整個模型滿足 L 約束,不必強(qiáng)求每一層都滿足),因此理論上來說 L 約束會降低模型表達(dá)能力,從而降低模型性能。
而在 WGAN 系列模型中,為了讓判別器滿足 L 約束,除了譜歸一化外,還有一種常見的方案,那就是梯度懲罰。因此,梯度懲罰也可以理解為一個促使模型滿足 L 約束的正則項,而滿足 L 約束則能有效地抵御對抗樣本的攻擊。
代碼實現(xiàn)
既然梯度懲罰號稱能有類似的效果,那必然也是要接受實驗驗證的了。相比前面的 FGM 式對抗訓(xùn)練,其實梯度懲罰實現(xiàn)起來還容易一些,因為它就是在 loss 里邊多加一項罷了,而且實現(xiàn)方式是通用的,不用區(qū)分 CV 還是 NLP。
Keras 參考實現(xiàn)如下:
def?sparse_categorical_crossentropy(y_true,?y_pred):"""自定義稀疏交叉熵這主要是因為keras自帶的sparse_categorical_crossentropy不支持求二階梯度。"""y_true?=?K.reshape(y_true,?K.shape(y_pred)[:-1])y_true?=?K.cast(y_true,?'int32')y_true?=?K.one_hot(y_true,?K.shape(y_pred)[-1])return?K.categorical_crossentropy(y_true,?y_pred)def?loss_with_gradient_penalty(y_true,?y_pred,?epsilon=1):"""帶梯度懲罰的loss"""loss?=?K.mean(sparse_categorical_crossentropy(y_true,?y_pred))embeddings?=?search_layer(y_pred,?'Embedding-Token').embeddingsgp?=?K.sum(K.gradients(loss,?[embeddings])[0].values**2)return?loss?+?0.5?*?epsilon?*?gpmodel.compile(loss=loss_with_gradient_penalty,optimizer=Adam(2e-5),metrics=['sparse_categorical_accuracy'], )?
可以看到,定義帶梯度懲罰的 loss 非常簡單,就兩行代碼而已。需要指出的是,梯度懲罰意味著參數(shù)更新的時候需要算二階導(dǎo)數(shù),但是 Tensorflow 和 Keras 自帶的 loss 函數(shù)不一定支持算二階導(dǎo)數(shù)。
比如K.categorical_crossentropy支持而K.sparse_categorical_crossentropy不支持,遇到這種情況時,需要自定重新定義 loss。
效果比較
還是前面兩個任務(wù),結(jié)果如下表??梢钥吹?#xff0c;梯度懲罰能取得跟 FGM 基本一致的結(jié)果。
完整的代碼請參考:
https://github.com/bojone/bert4keras/blob/master/examples/task_iflytek_gradient_penalty.py
本文小結(jié)
本文簡單介紹了對抗訓(xùn)練的基本概念和推導(dǎo),著重講了其中的 FGM 方法并給出了 Keras 實現(xiàn),實驗證明它能提高一些 NLP 模型的泛化性能。此外,本文還討論了對抗學(xué)習(xí)與梯度懲罰的聯(lián)系,并給出了梯度懲罰的一種直觀的幾何理解。
相關(guān)鏈接
[1] https://gluebenchmark.com/leaderboard
[2] https://stanfordnlp.github.io/coqa/
[3] http://https://arxiv.org/abs/1312.6199
[4] https://arxiv.org/abs/1706.06083
[5] https://arxiv.org/abs/1412.6572
[6] https://arxiv.org/abs/1706.06083
[7] https://kexue.fm/archives/4122
[8] https://arxiv.org/abs/1605.07725
[9] https://www.cluebenchmarks.com/
[10] https://arxiv.org/abs/2002.06305
[11] https://arxiv.org/abs/1711.09404
[12] https://arxiv.org/abs/1811.07457
點擊以下標(biāo)題查看更多往期內(nèi)容:?
圖自編碼器的起源和應(yīng)用
圖神經(jīng)網(wǎng)絡(luò)三劍客:GCN、GAT與GraphSAGE
如何快速理解馬爾科夫鏈蒙特卡洛法?
深度學(xué)習(xí)預(yù)訓(xùn)練模型可解釋性概覽
ICLR 2020 | 隱空間的圖神經(jīng)網(wǎng)絡(luò)
????
現(xiàn)在,在「知乎」也能找到我們了
進(jìn)入知乎首頁搜索「PaperWeekly」
點擊「關(guān)注」訂閱我們的專欄吧
關(guān)于PaperWeekly
PaperWeekly 是一個推薦、解讀、討論、報道人工智能前沿論文成果的學(xué)術(shù)平臺。如果你研究或從事 AI 領(lǐng)域,歡迎在公眾號后臺點擊「交流群」,小助手將把你帶入 PaperWeekly 的交流群里。
總結(jié)
以上是生活随笔為你收集整理的对抗训练浅谈:意义、方法和思考(附Keras实现)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微软 Xbox Game Pass 和
- 下一篇: Transformer的七十二变