博客内容将首发在微信公众号"跟我一起读论文啦啦",上面会定期分享机器学习、深度学习、数据挖掘、自然语言处理等高质量论文,欢迎关注!
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/c12cf49b05b177d1f2d5e100b3e2ced8.png)
本次要总结和分享的是正在ICLR2020审稿的,关于NLU对抗学习的论文:FreeLB: Enhanced Adversarial Training for Language Understanding,论文链接 FreeLB,感觉该论文方法创新和实验效果均还不错,由于本人在对抗学习领域上水平有限,在此就对本篇论文进行一个浅显的解读,如有错误还望指正。
论文动机
- 对抗训练的初衷:目前人工智能技术在某些场景的应用已经非常强大,例如人脸识别、自动驾驶、刷脸支付、抓捕逃犯、美颜直播等等,人工智能与实体经济深度结合,彻底改变了我们的生活。神经网络和深度学习貌似强大无比,值得信赖。但是有些时候只要略施小计就能误导最先进的深度学习模型指鹿为马。可看这篇文章 人工智能秒变人工智障:误导神经网络指鹿为马。所以在训练模型时,要进行对抗学习,使得训练出的模型鲁棒性更强,模型的泛化能力更强,不容易被欺骗。
- 对抗训练的核心步骤是:用被对抗性样本污染过的训练样本来训练模型,直到模型能学习到如此类型的抵抗。从而保证模型的安全性,在自动驾驶和图像识别领域,保证模型的安全性尤为重要。
- 对抗学习已经在图像领域取得了不错的效果,可否将这种方式对抗训练迁移到NLP上呢?因为NLP中的输入raw_text是离散的,无法直接在raw_text上加上扰动,Goodfellow在17年提出了可以在连续的embedding上做扰动,但这样做有一个问题,训练模式时可以这样加入扰动(已知label,喂给模型是扰动后的训练样本),在模型预测时,如何加入扰动呢?(label未知,喂给模型的是正常训练样本),通常在图像领域,经过对抗训练后的模型在正常样本上表现很差,而在NLP中,由大量实验表明,对抗学习后的模型泛化能力变强了。因此在NLP任务中,对抗训练的目的不再是为了防御基于梯度的恶意攻击,反而更多的是作为一种regularization,提高模型的泛化能力。因此论文中也提到:We turn our focus away from the security benefits of adversarial training, and instead study its effects on generalization.
- 该论文方法同样也是在wordEmbedding空间内加入扰动,同时借鉴已有的PGD和free方法,相对于PGD方法,论文所提的FreeLB方法,鲁棒性和泛化能力更强。
FreeLB方法详述
在将FreeLB之前,需要粗略的说下FGSM与PGD方法。
不论何种方法,都是在word embedding空间上加入扰动,然后对扰动后的embedding进行look up,得到的词向量再喂给模型。
对抗训练目标函数均为:
min
θ
E
(
x
,
y
)
∼
D
[
max
r
a
d
v
∈
S
L
(
θ
,
x
+
r
a
d
v
,
y
)
]
\min_{\theta}E(x,y) \sim D[\max_{r_{adv} \in S} L(\theta, x+r_{adv}, y)]
θminE(x,y)∼D[radv∈SmaxL(θ,x+radv,y)]
这是一个典型的minmax问题,那么问题来了,这个
r
a
d
v
r_{adv}
radv,也就是扰动怎么设置呢?
FGSM方法
r
a
d
v
=
ϵ
∗
g
/
∣
∣
g
∣
∣
2
r_{adv} = \epsilon *g/||g||_2
radv=ϵ∗g/∣∣g∣∣2
g
=
▽
x
L
(
θ
,
x
,
y
)
g = \triangledown_xL(\theta, x, y)
g=▽xL(θ,x,y)
FGSM相当于在梯度方向上加线性扰动,我们认为这种基于梯度生成的对抗样本更能拟合神经网络,从而比较能迷惑模型。
代码:
import torch
class FGM():
def __init__(self, model):
self.model = model
self.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 / 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 = {}
需要使用对抗训练的时候,只需要添加五行代码:
# 初始化
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()
由代码可知,相当于做一次对抗训练更新一次网络参数。并且每次都是在原始的embedding空间上加扰动,而非在上一次加扰动的基础上再加扰动。
PGD
在论文Towards Deep Learning Models Resistant to Adversarial 提到虽然利用FGSM方法进行对抗训练取得了不错的效果,但是线性太简单,稍微老练的对手很容易找到破绽。
由此提出了PGD(projected Gradient Descent)方法,简单的说,就是“小步走,多走几步”,如果走出了扰动半径为
ϵ
\epsilon
ϵ的空间,就映射回“球面”上,以保证扰动不要过大:
x
t
+
1
=
∏
x
+
S
(
x
t
+
α
g
(
x
t
)
/
∣
∣
g
(
x
t
)
∣
∣
2
)
−
−
−
−
−
(
1
)
x_{t+1} = \prod_{x+S}(x_t+\alpha g(x_t)/||g(x_t)||_2)-----(1)
xt+1=x+S∏(xt+αg(xt)/∣∣g(xt)∣∣2)−−−−−(1)
g
(
x
t
)
=
▽
x
L
(
θ
,
x
t
,
y
)
−
−
−
(
2
)
g(x_t) = \triangledown_xL(\theta, x_t, y)---(2)
g(xt)=▽xL(θ,xt,y)−−−(2)
其中
S
=
r
∈
R
d
:
∣
∣
r
∣
∣
2
⩽
ϵ
S =r \in R^d:||r||_2 \leqslant \epsilon
S=r∈Rd:∣∣r∣∣2⩽ϵ
由上面公式(1)(2)可以看出,在一步更新网络内(公式2),在
S
S
S 范围内进行了多步小的对抗训练(公式1),在这多步小的对抗训练中,对wordEmbedding空间扰动是累加的。每次都是在上一次加扰动的基础上再加扰动,然后取最后一次的梯度来更新网络参数。
上图只是大概的流程图,细节不一定完全正确。
代码如下:
import torch
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 and not torch.isnan(norm):
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()
# 对抗训练
for t in range(K): ## 注意注意,对抗扰动是累加的,每次都是在上一次加扰动的基础上再加扰动
pgd.attack(is_first_attack=(t==0)) # 在embedding上添加对抗扰动, first attack时备份param.data
if 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()
相对于FGSM方法来说,PGD方法为了获取更高水平的鲁棒性,在训练过程中进行了多补对抗,对于计算时间和资源要求都要提高不少。
好了,关于FGSM与PGD的基础知识粗略的讲了下,需要说明的是该内容和代码部分参考自:功守道:NLP中的对抗训练 + PyTorch实现,这是一篇讲的不错的关于PGD的文章,建议读下。
FreeLB
回到本论文重点FreeLB部分,看看FreeLB到底改进了哪些,使得达到了他声称的相对PGD具有更高的鲁棒性和泛化能力。直接来看FreeLB算法流程。
上图只是大概的流程图,细节不一定完全正确。
min
θ
E
(
z
,
y
)
∼
D
[
1
K
∑
t
=
0
K
−
1
max
r
a
d
v
∈
S
L
(
f
θ
(
x
+
r
a
d
v
)
,
y
)
]
\min_{\theta}E(z,y) \sim D[\frac{1}{K}\sum_{t=0}^{K-1}\max_{r_{adv} \in S} L(f_{\theta}(x+r_{adv}), y)]
θminE(z,y)∼D[K1t=0∑K−1radv∈SmaxL(fθ(x+radv),y)]
显然,相对于PGD算法,FreeLB算法保留了每步对抗训练的梯度(并且获取这些每步的梯度是没有花销的,相对PGD而言),并且均参与了网络参数的更新。而每一步对抗训练的梯度是由当前步对抗训练样本计算而来,对从模型更新的角度来说,相当于将训练样本扩大了k倍。
同时论文中又说,相对于PGD做k步对抗训练,才做一次网络参数更新。在minmax角度来说,相当于进行了多次内部maximum,然后做一次minimize。而FreeLB对每一步的max都做了minmize。论文实验部分证明了这种做法,比起PGD鲁棒性和泛化能力更强。
论文还讲了下FreeLB如何添加dropout,这里就不讲了。
实验部分
略
个人总结
相对于PGD等前人的做法,FreeLB只是做了些许修改,改变的地方并不多,但是实验效果还不错,并且听说这篇论文在ICLR上的审稿得分高于8分,评价不错。