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