nlp常用的对抗训练有FGM、PGD以及FreeLB
对抗训练的公式
对抗训练的经典公式如下
m θ i n E ( Z , y ) ∼ D [ m a x L ∣ ∣ δ ∣ ∣ ≤ ϵ ( f θ ( X + δ ) , y ) ] \underset{\theta}minE_{(Z,y)\sim D}[max\underset{||\delta||\leq\epsilon}L(f_{\theta}(X+\delta),y)] θminE(Z,y)∼D[max∣∣δ∣∣≤ϵL(fθ(X+δ),y)]
内层(中括号内)是一个最大化,其中 X X X 表示样本的输入表示, ϵ \epsilon ϵ 表示叠加在输入上的扰动, f θ ( ) f_{\theta}() fθ() 是神经网络函数, y y y是样本的标签, L ( f θ ( X + ϵ ) , y ) L(f_{\theta}(X+\epsilon),y) L(fθ(X+ϵ),y) 则表示在样本 X X X上叠加一个扰动 ϵ \epsilon ϵ,再经过神经网络函数,与标签 y y y比较得到的损失。 m a x ( L ) max(L) max(L)是优化目标,即寻找使损失函数最大的扰动,简单来讲就是添加的扰动要尽量让神经网络迷惑。
这段比较重要,这里 m θ i n E ( Z , y ) ∼ D \underset{\theta}minE_{(Z,y)\sim D} θminE(Z,y)∼D表示扰动的最小,而 [ m a x L ∣ ∣ δ ∣ ∣ ≤ ϵ ( f θ ( X + δ ) , y ) ] [max\underset{||\delta||\leq\epsilon}L(f_{\theta}(X+\delta),y)] [max∣∣δ∣∣≤ϵL(fθ(X+δ),y)]我们可以理解为输出的标签不一样,损失最大。
也就是说在改变参数最小的情况下使标签发生变化!!!
注意!!!上面这个仅仅是对抗训练满足的条件,也就是说,下面提出来的扰动公式,比如FGM的 x = x + e p s i l o n ∗ p a r a m . g r a d / n o r m x = x+epsilon*param.grad/norm x=x+epsilon∗param.grad/norm,作者都认为它们满足上面这个对抗训练的条件,或者说无限接近于这个条件。之前我把上面理解为对抗训练的训练过程,这是完全错误的!!!
至于训练,自然是原先的参数先加上正常训练的梯度,再加上扰动之后训练的梯度,这样保证了在微小的扰动之后,预测的标签仍然是准确的!!!
1.首先介绍FGM对抗训练
设原先的embedding = x
1.计算原始的loss1以及对应的梯度step1,反向传播1
2. x = x + e p s i l o n ∗ p a r a m . g r a d / n o r m x = x+epsilon*param.grad/norm x=x+epsilon∗param.grad/norm
计算损失loss2以及对应梯度step2,反向传播
3.x恢复为原来的embedding之后,梯度下降,实际上更新参数
更新的新的损失为loss1+loss2,更新的新的梯度为step1+step2
如果我们理解了对抗训练之中的Min-Max公式之后,我们就更好理解了
对抗训练本质上是在loss损失最大的情况下期望最小
分析相应的步骤
1.计算原始的loss1以及对应的梯度step1,反向传播1
这里是使得loss的损失最大,因为在梯度方向上损失最大。
2.更新 x = x + e p s i l o n ∗ p a r a m . g r a d / n o r m x = x+epsilon*param.grad/norm x=x+epsilon∗param.grad/norm
这里我们认为使用这个对抗公式的时候,loss的期望最小
3.最后的loss叠加
loss = loss1+loss2
对应的FGM代码如下:
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 = {}
然后使用fgm进行对抗攻击
# 初始化
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()
1.先进行正常的计算loss损失以及反向传播,算出正常情况下的梯度
loss = model(batch_input,batch_label)
loss.backward()
然后进行扰动embedding之后,计算fgm的反向传播以及扰动后的梯度
loss_sum = model(batch_input,batch_label)
loss_sum.backward()
最后恢复embedding参数进行更新参数(此时这里的梯度为原先的梯度加上扰动之后的梯度
fgm.restore()
optimizer.step()
optimizer.zero_grad()
2.其次介绍PGD对抗训练
对应的代码如下:
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()
这里以k = 3为例演示正常的对抗生成的运行过程
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()
1.K = 0的之前,先正常地计算对应的损失以及保存相应的梯度
loss = model(batch_input,batch_label)
loss.backward()
pgd.backup_grad()
2.K = 0的时候,先利用之前的计算出的正常的梯度进行扰动embedding的权重内容
pgd.attack(is_first_attack=(t==0))
接着将之前求出的梯度清零(每一次的embedding是接着上次的embedding计算的,但是每一次的梯度都是会被清零的),下次的梯度是新的梯度进行的扰动
然后求出下一次对embedding进行扰动的梯度
loss_sum = model(batch_input,batch_label)
loss_sum.backward()
3.K = 1的时候与K = 0的时候求出梯度的过程类似,这里不再赘述
4.K = 2的时候求梯度的过程
首先利用之前的梯度进行攻击embedding的权重值
pgd.attack(is_first_attack=(t==0))
接着恢复之前没有扰动的时候计算出来的正常的grad
注意这里的embedding现在是扰动之后的embedding,只不过grad是正常的grad
这一步相当于包含了
optimizer.zero_grad()
现在的embedding是扰动之后的embedding,而grad是正常的grad,接下来计算对应的损失函数
loss_sum = model(batch_input,batch_label)
loss_sum.backward()
这里使用扰动之后的embedding计算出来了梯度,但是由于grad之中包含了原先正常的梯度,所以这里更新完成之后的梯度为正常embedding算出来的正常梯度和扰动embedding算出来的扰动梯度的和
最后更新的时候恢复为原先正常的embedding
pgd.restore()
然后对正常的embedding进行更新参数
optimizer.step()
optimizer.zero_grad()
这里的梯度为正常的梯度加上扰动的梯度,embedding为正常的embedding,更新完成之后构成了对抗训练之后的权重参数
从某种角度上来看,FGM有点像特殊的PGD,类似于K=1的时候的PGD。