笔记:对抗训练及其在Bert中的应用
最近在打一個比賽,發現往年的優秀樣例都添加了對抗訓練和多模型融合,遂學習一下對抗訓練,并在實際比賽中檢驗效果
對抗樣本的基本概念
要認識對抗訓練,首先要了解 "對抗樣本",它首先出現在論文Intriguing properties of neural networks之中。簡單來說,它是指對于人類來說 "看起來" 幾乎一樣,但對于模型來說預測結果卻完全不一樣的樣本,比如下面的經典例子(一只熊貓加了點擾動就被識別成了長臂猿)
那么什么樣的樣本才是最好的對抗樣本呢?
對抗樣本一般需要具備兩個特點:
相對于原始輸入,所添加的擾動是微小的
能使模型犯錯
對抗訓練基本概念
GAN之父Goodfellow在15年的ICLR中第一次提出了對抗訓練這個概念,簡而言之,就是在原始輸入樣本 $x$ 上加上一個擾動 $\Delta x$ 得到對抗樣本,再用其進行訓練。
也就是說,這個問題可以抽象成這樣一個模型:
$$\max _{\theta} P(y \mid x+\Delta x ; \theta)$$
其中,$y$ 是 ground truth, $\theta$ 是模型參數。意思就是即使在擾動的情況下求使得預測出 $y$ 的概率最大的參數 $\theta$.
那擾動 $\Delta x$ 是如何確定的呢?
GoodFellow認為:神經網絡由于其線性的特點,很容易受到線性擾動的攻擊。于是他提出了 Fast Gradinet Sign Method (FGSM),來計算輸入樣本的擾動。擾動可以被定義為
$$\Delta x=\epsilon \cdot \operatorname{sgn}\left(\nabla_{x} L(x, y ; \theta)\right)$$
其中,$sgn$ 為符號函數,$L$ 為損失函數(很多地方也用 $J$ 來表示)。GoodFellow發現 $\epsilon = 0.25$ 時,這個擾動能給一個單層分類器造成99.9%的錯誤率。這個擾動其實就是沿著梯度反方向走了 $\Delta x$
最后,GoodFellow還總結了對抗訓練的兩個作用:
1. 提高模型應對惡意對抗樣本時的魯棒性
2. 作為一種regularization,減少overfitting,提高泛化能力
Min-Max公式
Madry在2018年的ICLR論文Towards Deep Learning Models Resistant to Adversarial Attacks中總結了之前的工作。總的來說,對抗訓練可以統一寫成如下格式:
$$\min _{\theta} \mathbb{E}_{(x, y) \sim \mathcal{D}}\left[\max _{\Delta x \in \Omega} L(x+\Delta x, y ; \theta)\right]$$
其中$\mathcal{D}$ 代表輸入樣本的分布,$x$ 代表輸入,$y$ 代表標簽,$\theta$ 是模型參數,$L(x+y; \theta)$ 是單個樣本的loss,$\Delta x$ 是擾動,$\Omega$ 是擾動空間。這個式子可以分布理解如下:
1. 內部max是指往 $x$ 中添加擾動 $\Delta x$,$\Delta x$ 的目的是讓 $L(x+\Delta x, y ; \theta)$ 越大越好,也就是說盡可能讓現有模型預測出錯。但是,$\Delta x$ 也是有約束的,要在 $\Omega$ 范圍內. 常規的約束是 $|| \Delta x|| \leq \epsilon$,其中 $\epsilon$ 是一個常數
2. 外部min是指找到最魯棒的參數 $\theta$ 是預測的分布符合原數據集的分布
這就解決了兩個問題:如何構建足夠強的對抗樣本、和如何使得分布仍然盡可能接近原始分布
從CV到NLP
對于CV領域,圖像可以認為是連續的,因此可以直接在原始圖像上添加擾動;
而對于NLP,它的輸入時文本,本質是one-hot,而兩個one-hot之間的歐式距離恒為 $\sqrt{2}$,理論上不存在“微小的擾動”,
而且,在Embedding向量上加上微小擾動可能就找不到與之對應的詞了,這就不是真正意義上的對抗樣本了,因為對抗樣本依舊能對應一個合理的原始輸入
既然不能對Embedding向量添加擾動,可以對Embedding層添加擾動,使其產生更魯棒的Embedding向量
Fast Gradient Method(FGM)
上面提到,Goodfellow 在 15 年的 ICLR 中提出了 Fast Gradient Sign Method(FGSM),隨后,在 17 年的 ICLR 中,Goodfellow 對 FGSM 中計算擾動的部分做了一點簡單的修改。假設輸入文本序列的 Embedding vectors 為 $x$,Embedding層的擾動為:
\begin{aligned}
\Delta x &=\epsilon \cdot \frac{g}{\|g\|_{2}} \\
g &=\nabla_{x} L(x, y ; \theta)
\end{aligned}
實際上就是取消了符號函數,用二范式做了一個 scale,需要注意的是這里norm計算是針對一個sample,對梯度 $g$ 的后兩維計算norm,為了方便,這里是對一個batch計算norm。其實除以norm本來就是一個放縮作用,影響不大。假設 $x$ 的維度是 $[batch\_size, len, embed_size]$,針對sample計算的norm是 $[batch\_size, 1, 1]$,針對整個batch計算的norm是 $[1, 1, 1]$。
class FGM():
def __init__(self, model):
self.model = model
self.backup = {}
def attack(self, epsilon=1., emb_name='emb'):
# emb_name這個參數要換成你模型中embedding的參數名
# 例如,self.emb = nn.Embedding(5000, 100)
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) # 默認為2范數
if norm != 0:
r_at = epsilon * param.grad / norm
param.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.backup
param.data = self.backup[name]
self.backup = {}
需要使用對抗訓練的時候,只需要添加5行代碼:
# 初始化 fgm = FGM(model) for batch_input, batch_label in data: # 正常訓練 loss = model(batch_input, batch_label) loss.backward() # 反向傳播,得到正常的grad # 對抗訓練 fgm.attack() # embedding被修改了 # optimizer.zero_grad() # 如果不想累加梯度,就把這里的注釋取消 loss_sum = model(batch_input, batch_label) loss_sum.backward() # 反向傳播,在正常的grad基礎上,累加對抗訓練的梯度 fgm.restore() # 恢復Embedding的參數 # 梯度下降,更新參數 optimizer.step() optimizer.zero_grad()
Note: 不是把上面的正常訓練換成對抗訓練,而是兩者都要,先正常訓練再對抗訓練
Projected Gradient Descent(PGD)
FGM 的思路是梯度上升,本質上來說沒有什么問題,但是FGM 簡單粗暴的 "一步到位" 是不是有可能并不能走到約束內的最優點呢?當然是有可能的。于是,一個新的想法誕生了,Madry 在 18 年的 ICLR 中提出了 Projected Gradient Descent(PGD)方法,簡單的說,就是"小步走,多走幾步",如果走出了擾動半徑為?的空間,就重新映射回 "球面" 上,以保證擾動不要過大:
\begin{aligned}
x_{t+1} &=\prod_{x+S}\left(x_{t}+\alpha \frac{g\left(x_{t}\right)}{\left\|g\left(x_{t}\right)\right\|_{2}}\right) \\
g\left(x_{t}\right) &=\nabla_{x} L\left(x_{t}, y ; \theta\right)
\end{aligned}
其中$S=\left\{r \in \mathbb{R}^ze8trgl8bvbq:\|r\|_{2} \leq \epsilon\right\}$ 為擾動的約束空間,$\alpha$ 是小步的步長
由于 PGD 理論和代碼比較復雜,因此下面先給出偽代碼方便理解,然后再給出代碼
對于每個x:
1.計算x的前向loss,反向傳播得到梯度并備份
對于每步t:
2.根據Embedding矩陣的梯度計算出r,并加到當前Embedding上,相當于x+r(超出范圍則投影回epsilon內)
3.t不是最后一步: 將梯度歸0,根據(1)的x+r計算前后向并得到梯度
4.t是最后一步: 恢復(1)的梯度,計算最后的x+r并將梯度累加到(1)上
5.將Embedding恢復為(1)時的值
6.根據(4)的梯度對參數進行更新
可以看到,在循環中r是逐漸累加的,要注意的是最后更新參數只使用最后一個 x+r 算出來的梯度
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這個參數要換成你模型中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:
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這個參數要換成你模型中embedding的參數名
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]
使用的時候要麻煩一點:
pgd = PGD(model)
K = 3
for batch_input, batch_label in data:
# 正常訓練
loss = model(batch_input, batch_label)
loss.backward() # 反向傳播,得到正常的grad
pgd.backup_grad() # 保存正常的grad
# 對抗訓練
for t in range(K):
pgd.attack(is_first_attack=(t==0)) # 在embedding上添加對抗擾動, first attack時備份param.data
if t != K-1:
optimizer.zero_grad()
else:
pgd.restore_grad() # 恢復正常的grad
loss_sum = model(batch_input, batch_label)
loss_sum.backward() # 反向傳播,并在正常的grad基礎上,累加對抗訓練的梯度
pgd.restore() # 恢復embedding參數
# 梯度下降,更新參數
optimizer.step()
optimizer.zero_grad()
Virtual Adversarial Training
除了監督任務,對抗訓練還可以用在半監督任務中,尤其對于 NLP 任務來說,很多時候我們擁有大量的未標注文本,那么就可以參考Distributional Smoothing with Virtual Adversarial Training進行半監督訓練
首先,抽取一個隨機標準正態擾動 $\left(d \sim \mathcal{N}(0,1) \in \mathbb{R}^ze8trgl8bvbq\right)$,加到Embedding上,并用KL散度計算梯度:
\begin{aligned}
g &=\nabla_{x^{\prime}} D_{K L}\left(p(\cdot \mid x ; \theta)|| p\left(\cdot \mid x^{\prime} ; \theta\right)\right) \\
x^{\prime} &=x+\xi d
\end{aligned}
然后,用得到的梯度,計算對抗擾動,并進行對抗訓練:
\begin{aligned}
&\min _{\theta} D_{K L}\left(p(\cdot \mid x ; \theta)|| p\left(\cdot \mid x^{*} ; \theta\right)\right) \\
&x^{*}=x+\epsilon \frac{g}{\|g\|_{2}}
\end{aligned}
實現起來有很多細節,并且筆者對于 NLP 的半監督任務了解并不多,因此這里就不給出實現了
實驗對照
為了說明對抗訓練的作用,網上有位大佬選了四個 GLUE 中的任務進行了對照試驗,實驗代碼使用的 Huggingface 的transformers/examples/run_glue.py,超參都是默認的,對抗訓練用的也是相同的超參
| 任務 | Metrics | BERT-Base | FGM | PGD |
|---|---|---|---|---|
| MRPC | Accuracy | 83.6 | 86.8 | 85.8 |
| CoLA | Matthew's corr | 56.0 | 56.0 | 56.8 |
| STS-B | Person/Spearmean corr | 89.3/88.8 | 89.3/88.8 | 89.3/88.8 |
| RTE | Accuracy | 64.3 | 66.8 | 64.6 |
可以看出,對抗訓練還是有效的,在 MRPC 和 RTE 任務上甚至可以提高三四個百分點。不過,根據我們使用的經驗來看,是否有效有時也取決于數據集
為什么對抗訓練有效
Adversarial Training 能夠提升 Word Embedding 質量的一個原因是:
有些詞與比如(good 和 bad),其在語句中 Grammatical Role 是相近的,我理解為詞性相同(都是形容詞),并且周圍一并出現的詞語也是相近的,比如我們經常用來修飾天氣或者一天的情況(The weather is good/bad; It's a good/bad day),這些詞的 Word Embedding 是非常相近的。文章中用 Good 和 Bad 作為例子,找出了其最接近的 10 個詞:
可以發現在 Baseline 和 Random 的情況下,good 和 bad 出現在了彼此的鄰近詞中,而喂給模型經過擾動之后的 X-adv 之后,也就是 Adversarial 這一列,這種現象就沒有出現,事實上, good 掉到了 bad 接近程度排第 36 的位置
我們可以猜測,在 Word Embedding 上添加的 Perturbation 很可能會導致原來的good變成bad,導致分類錯誤,計算的 Adversarial Loss 很大,而計算 Adversarial Loss 的部分是不參與梯度計算的,也就是說,模型(LSTM 和最后的 Dense Layer)的 Weight 和 Bias 的改變并不會影響 Adversarial Loss,模型只能通過改變 Word Embedding Weight 來努力降低它,進而如文章所說:
Adversarial training ensures that the meaning of a sentence cannot be inverted via a small change, so these words with similar grammatical role but different meaning become separated.
這些含義不同而語言結構角色類似的詞能夠通過這種 Adversarial Training 的方法而被分離開,從而提升了 Word Embedding 的質量,幫助模型取得了非常好的表現
梯度懲罰
這一部分,我們從另一個視角對上述結果進行分析,從而推出對抗訓練的另一種方法,并且得到一種關于對抗訓練更直觀的幾何理解
假設已經得到對抗擾動 $\Delta x$,更新 $\theta$ 時,對 $L$ 進行泰勒展開:
\begin{aligned}
\min _{\theta} \mathbb{E}_{(x, y) \sim D}[L(x+\Delta x, y ; \theta)] & \approx \min _{\theta} \mathbb{E}_{(x, y) \sim D}\left[L(x, y ; \theta)+<\nabla_{x} L(x, y ; \theta), \Delta x>\right] \\
&=\min _{\theta} \mathbb{E}_{(x, y) \sim D}\left[L(x, y ; \theta)+\nabla_{x} L(x, y ; \theta) \cdot \Delta x\right] \\
&=\min _{\theta} \mathbb{E}_{(x, y) \sim D}\left[L(x, y ; \theta)+\nabla_{x} L(x, y ; \theta)^{T} \Delta x\right]
\end{aligned}
對應的 $\theta$ 的梯度為:
$$\nabla_{\theta} L(x, y ; \theta)+\nabla_{\theta} \nabla_{x} L(x, y ; \theta)^{T} \Delta x$$
將 $\Delta x=\epsilon \nabla_{x} L(x, y ; \theta)$ 代入:
\begin{aligned}
&\nabla_{\theta} L(x, y ; \theta)+\epsilon \nabla_{\theta} \nabla_{x} L(x, y ; \theta)^{T} \nabla_{x} L(x, y ; \theta) \\
&=\nabla_{\theta}\left(L(x, y ; \theta)+\frac{1}{2} \epsilon\left\|\nabla_{x} L(x, y ; \theta)\right\|^{2}\right)
\end{aligned}
這個結果表示,對輸入樣本添加 $\epsilon \nabla x L(x, y ; \theta)$ 的對抗擾動,一定程度上等價于往loss中加“梯度懲罰”
$$\frac{1}{2} \epsilon\left\|\nabla_{x} L(x, y ; \theta)\right\|^{2}$$
如果對抗擾動是 $\epsilon\|\nabla x L(x, y ; \theta)\|$,那么對應的梯度懲罰項是 $\epsilon\|\nabla x L(x, y ; \theta)\|$ (少了個1/2,也少了個2次方)。
幾何解釋
事實上,關于梯度懲罰,我們有一個非常直觀的幾何圖像。以常規的分類問題為例,假設有n個類別,那么模型相當于挖了n個坑,然后讓同類的樣本放到同一個坑里邊去:
梯度懲罰則說“同類樣本不僅要放在同一個坑內,還要放在坑底”,這就要求每個坑的內部要長這樣:
為什么要在坑底呢?因為物理學告訴我們,坑底最穩定呀,所以就越不容易受干擾呀,這不就是對抗訓練的目的么?
那坑底意味著什么呢?極小值點呀,導數(梯度)為零呀,所以不就是希望 $‖?xL(x,y;θ)‖‖?xL(x,y;θ)‖$ 越小越好么?這便是梯度懲罰的幾何意義了。
驗證部分
將對抗訓練加到項目中, 出了點小問題,待修改
不知道為啥grad=None??
參考鏈接:
https://wmathor.com/index.php/archives/1537/
https://coding-zuo.github.io/2021/04/07/nlp中的對抗訓練-與bert結合/
https://zhuanlan.zhihu.com/p/91269728
論文:
Adversarial Training for Aspect-Based Sentiment Analysis with BERT
FGSM: Explaining and Harnessing Adversarial Examples
FGM: Adversarial Training Methods for Semi-Supervised Text Classification
FreeAT: Adversarial Training for Free!
YOPO: You Only Propagate Once: Accelerating Adversarial Training via Maximal Principle
FreeLB: Enhanced Adversarial Training for Language Understanding
SMART: Robust and Efficient Fine-Tuning for Pre-trained Natural
代碼
https://blog.csdn.net/weixin_42001089/article/details/115458615
https://github.com/bojone/keras_adversarial_training
https://github.com/bojone/bert4keras/blob/master/examples/task_iflytek_adversarial_training.py
個性簽名:時間會解決一切
總結
以上是生活随笔為你收集整理的笔记:对抗训练及其在Bert中的应用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 磁共振序列相关知识点记录
- 下一篇: RecyclerView复用item导致