nlp中的对抗训练操作

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+epsilonparam.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+epsilonparam.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+epsilonparam.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。

おすすめ

転載: blog.csdn.net/znevegiveup1/article/details/120304398
おすすめ