對抗訓練淺談:意義、方法和思考(附Keras實現)(轉載)

當前,說到深度學習中的對抗,一般會有兩個含義:一個是生成對抗網絡(Generative Adversarial Networks,GAN),代表着一大類先進的生成模型;另一個則是跟對抗攻擊、對抗樣本相關的領域,它跟GAN相關,但又很不一樣,它主要關心的是模型在小擾動下的穩健性。本博客裏以前所涉及的對抗話題,都是前一種含義,而今天,我們來聊聊後一種含義中的“對抗訓練”。

方法介紹

近年來,隨着深度學習的日益發展和落地,對抗樣本也得到了越來越多的關注。在CV領域,我們需要通過對模型的對抗攻擊和防禦來增強模型的穩健型,比如在自動駕駛系統中,要防止模型因爲一些隨機噪聲就將紅燈識別爲綠燈。在NLP領域,類似的對抗訓練也是存在的,不過NLP中的對抗訓練更多是作爲一種正則化手段來提高模型的泛化能力!

這使得對抗訓練成爲了NLP刷榜的“神器”之一,前有微軟通過RoBERTa+對抗訓練在GLUE上超過了原生RoBERTa,後有我司的同事通過對抗訓練刷新了CoQA榜單。這也成功引起了筆者對它的興趣,遂學習了一番,分享在此。

基本概念

要認識對抗訓練,首先要了解“對抗樣本”,它首先出現在論文《Intriguing properties of neural networks》之中。簡單來說,它是指對於人類來說“看起來”幾乎一樣、但對於模型來說預測結果卻完全不一樣的樣本,比如下面的經典例子:
在這裏插入圖片描述
理解對抗樣本之後,也就不難理解各種相關概念了,比如“對抗攻擊”,其實就是想辦法造出更多的對抗樣本,而“對抗防禦”,就是想辦法讓模型能正確識別更多的對抗樣本。所謂對抗訓練,則是屬於對抗防禦的一種,它構造了一些對抗樣本加入到原數據集中,希望增強模型對對抗樣本的魯棒性;同時,如本文開篇所提到的,在NLP中它通常還能提高模型的表現。

Min-Max

總的來說,對抗訓練可以統一寫成如下格式
在這裏插入圖片描述
在這裏插入圖片描述
這個式子可以分步理解如下:
在這裏插入圖片描述
在這裏插入圖片描述

快速梯度

在這裏插入圖片描述
在這裏插入圖片描述

回到NLP

在這裏插入圖片描述

實驗結果

既然有效,那我們肯定就要親自做實驗驗證一下了。怎麼通過代碼實現對抗訓練呢?怎麼才能做到用起來儘可能簡單呢?最後用起來的效果如何呢?

思路分析

在這裏插入圖片描述

代碼參考

基於上述思路,這裏給出Keras下基於FGM方式對Embedding層進行對抗訓練的參考實現:
核心代碼如下:

def adversarial_training(model, embedding_name, epsilon=1):
    """給模型添加對抗訓練
    其中model是需要添加對抗訓練的keras模型,embedding_name
    則是model裏邊Embedding層的名字。要在模型compile之後使用。
    """
    if model.train_function is None:  # 如果還沒有訓練函數
        model._make_train_function()  # 手動make
    old_train_function = model.train_function  # 備份舊的訓練函數

    # 查找Embedding層
    for output in model.outputs:
        embedding_layer = search_layer(output, embedding_name)
        if embedding_layer is not None:
            break
    if 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]  # 轉爲dense tensor

    # 封裝爲函數
    inputs = (model._feed_inputs +
              model._feed_targets +
              model._feed_sample_weights)  # 所有輸入層
    embedding_gradients = K.function(
        inputs=inputs,
        outputs=[gradients],
        name='embedding_gradients',
    )  # 封裝爲函數

    def train_function(inputs):  # 重新定義訓練函數
        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 outputs

    model.train_function = train_function  # 覆蓋原訓練函數

定義好上述函數後,給Keras模型增加對抗訓練就只需要一行代碼了:

# 寫好函數後,啓用對抗訓練只需要一行代碼
adversarial_training(model, 'Embedding-Token', 0.5)

需要指出的是,由於每一步算對抗擾動也需要計算梯度,因此每一步訓練一共算了兩次梯度,因此每步的訓練時間會翻倍。

效果比較

爲了測試實際效果,筆者選了中文CLUE榜的兩個分類任務:IFLYTEK和TNEWS,模型選擇了中文BERT base。在CLUE榜單上,BERT base模型在這兩個數據上的成績分別是60.29%和56.58%,經過對抗訓練後,成績爲62.46%、57.66%,分別提升了2%和1%!
在這裏插入圖片描述
當然,同所有正則化手段一樣,對抗訓練也不能保證每一個任務都能有提升,但從目前大多數“戰果”來看,它是一種非常值得嘗試的技術手段。此外,BERT的finetune本身就是一個非常玄乎(靠人品)的過程,前些時間論文《Fine-Tuning Pretrained Language Models: Weight Initializations, Data Orders, and Early Stopping》換用不同的隨機種子跑了數百次finetune實驗,發現最好的結果能高出好幾個點,所以如果你跑了一次發現沒提升,不妨多跑幾次再下結論。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章