8.FGM、PGD、AWP 对抗训练技巧

1.FGM

GAN 之父 lan Goodfellow 在 Adversarial Training Methods for Semi-Supervised Text Classification 中提出 FGSM (Fast Gradient Sign Method): ICLR2015。接着在 Boosting Adversarial Attacks with Momentum 提出了 FGM。其原理如下:

  1. 对于给定的输入样本 $x$ 和真实标签 $y$,首先利用网络前向传播得到预测结果 $f(x)$。
  2. 计算输入样本 $x$ 对于预测结果 $f(x)$ 的梯度 $\nabla_x f(x)$。
  3. 对于每个输入特征 $x_i$,增加一个扰动 $\epsilon$,然后按照其梯度方向 $sign(\nabla_x f(x))$ 进行更新,得到对抗样本 $x’$:

$$
x^′=x+ϵ⋅\text{sign}(\nabla_x · f(x)) \tag{1}
$$

  1. 利用对抗样本 $x’$ 进行网络的前向传播,得到预测结果 $f(x’)$,然后计算损失函数,进行反向传播和权值更新。
  2. 对抗样本攻击可以反复迭代进行,每次更新的对抗样本都会进一步逼近真实样本,提高攻击效果。

FGM 的核心思想就是通过对输入样本的微小扰动,使得神经网络输出的预测结果发生错误,从而达到对抗训练的目的。Pytorch 实现代码如下,NLP 中主要是对 emb_name 参数的梯度乘以 epsilonh 后除以 norm 添加到原来的参数上进行扰动。r_at = self.epsilon * param.grad / norm

class FGM():
    '''
    Example
    # 初始化
    fgm = FGM(model,epsilon=1,emb_name='word_embeddings.')
    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()
    '''
    def __init__(self, model, emb_name, epsilon=1.0):
        # emb_name 这个参数要换成你模型中 embedding 的参数名
        self.model = model
        self.epsilon = epsilon
        self.emb_name = emb_name
        self.backup = {}

    def attack(self):
        for name, param in self.model.named_parameters():
            if param.requires_grad and self.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 = self.epsilon * param.grad / norm
                    param.data.add_(r_at)

    def restore(self):
        for name, param in self.model.named_parameters():
            if param.requires_grad and self.emb_name in name:
                assert name in self.backup
                param.data = self.backup[name]
        self.backup = {}

使用方法可以参考 something for improving,这个 notebook。

2.PGD

PGD 来自于论文 Towards Deep Learning Models Resistant to Adversarial Attacks。PGD 可以看作是 FGM 的多步迭代,因为当我们面对一个非线性模型,仅仅做一次迭代,方向是不一定完全正确的,所以在这种情况下我们显然不能认为 FGM 一步的操作就可以达到最优解。

PGD 方法的主要步骤如下:

  1. 初始化对抗样本为原始样本。
  2. 多次迭代对对抗样本进行微小的干扰,以使得分类器对对抗样本的分类结果产生误判。
  3. 在每次迭代中,根据当前对抗样本的梯度方向更新对抗样本。
  4. 限制对抗样本的幅度不超过某个预先设定的阈值。
  5. 将更新后的对抗样本作为下一轮迭代的输入。

通过不断迭代更新对抗样本,PGD 方法能够生成一组高度鲁棒的对抗样本,以提高模型的鲁棒性和泛化能力。相对 FGM,PGD 每次扰动会乘以步长 alpha,即代码中的 r_at = self.alpha * param.grad / norm 来和 k 步的扰动配合实现 小步走多步达到大步走 的效果。

具体来说,

对于每个 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) 的梯度对参数进行更新
                                                                                      -- 参考 1 
class PGD():
    '''
    Example
    pgd = PGD(model,emb_name='word_embeddings.',epsilon=1.0,alpha=0.3)
    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()
    '''
    def __init__(self, model, emb_name, epsilon=1., alpha=0.3):
        # emb_name 这个参数要换成你模型中 embedding 的参数名
        self.model = model
        self.emb_name = emb_name
        self.epsilon = epsilon
        self.alpha = alpha
        self.emb_backup = {}
        self.grad_backup = {}

    def attack(self, is_first_attack=False):
        for name, param in self.model.named_parameters():
            if param.requires_grad and self.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 = self.alpha * param.grad / norm
                    param.data.add_(r_at)
                    param.data = self.project(name, param.data, self.epsilon)

    def restore(self):
        for name, param in self.model.named_parameters():
            if param.requires_grad and self.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 and param.grad is not None:
                self.grad_backup[name] = param.grad.clone()

    def restore_grad(self):
        for name, param in self.model.named_parameters():
            if param.requires_grad and param.grad is not None:
                param.grad = self.grad_backup[name]

一般来说,由于 PGD 设置 k 步,一般会是 FGM 的 k 倍。训练开销太大,我一般只用 FGM,实践中 PGD, 不会好于 FGM。如果要提升比较明显,还是 AWP 结合 EMA 或者 SWA 比较好。那么来看看 AWP 实践吧!

3.AWP

来自于论文Adversarial Weight Perturbation Helps Robust Generalization。AWP(Adversarial Weight Perturbation)是一种对抗性训练方法,其基本思想是通过对神经网络权重的扰动来增强模型的鲁棒性。相对于对抗性样本中输入的扰动,AWP 对权重的扰动相对较小,从而减少了对模型的性能影响。

具体地,AWP 在训练时,将一部分正常的样本添加随机扰动来构造对抗性样本,然后通过梯度反向传播调整神经网络的权重,使得网络的输出对于对抗性样本的分类更具有鲁棒性。在这个过程中,AWP 不会将对抗性样本加入到训练集中,而是直接使用加上随机扰动后的样本进行训练。

在 AWB 的原始论文中,作者使用了一种自适应的权重扰动策略,即根据权重的梯度大小调整扰动的大小。这种方法能够有效地提高模型的鲁棒性,并且不需要手动设置扰动的大小。

class AWP:
    def __init__(
        self,
        model,
        optimizer,
        adv_param="weight",
        adv_lr=1,
        adv_eps=0.2,
        start_epoch=0,
        adv_step=1,
        scaler=None
    ):
        self.model = model
        self.optimizer = optimizer
        self.adv_param = adv_param
        self.adv_lr = adv_lr
        self.adv_eps = adv_eps
        self.start_epoch = start_epoch
        self.adv_step = adv_step
        self.backup = {}
        self.backup_eps = {}
        self.scaler = scaler

    def attack_backward(self, x, y, attention_mask,epoch):
        if (self.adv_lr == 0) or (epoch < self.start_epoch):
            return None

        self._save() 
        for i in range(self.adv_step):
            self._attack_step() 
            with torch.cuda.amp.autocast():
                adv_loss, tr_logits = self.model(input_ids=x, attention_mask=attention_mask, labels=y)
                adv_loss = adv_loss.mean()
            self.optimizer.zero_grad()
            self.scaler.scale(adv_loss).backward()

        self._restore()

    def _attack_step(self):
        e = 1e-6
        for name, param in self.model.named_parameters():
            if param.requires_grad and param.grad is not None and self.adv_param in name:
                norm1 = torch.norm(param.grad)
                norm2 = torch.norm(param.data.detach())
                if norm1 != 0 and not torch.isnan(norm1):
                    r_at = self.adv_lr * param.grad / (norm1 + e) * (norm2 + e)
                    param.data.add_(r_at)
                    param.data = torch.min(torch.max(param.data, self.backup_eps[name][0]), self.backup_eps[name][1]
                    )
                # param.data.clamp_(*self.backup_eps[name])

    def _save(self):
        for name, param in self.model.named_parameters():
            if param.requires_grad and param.grad is not None and self.adv_param in name:
                if name not in self.backup:
                    self.backup[name] = param.data.clone()
                    grad_eps = self.adv_eps * param.abs().detach()
                    self.backup_eps[name] = (self.backup[name] - grad_eps,
                        self.backup[name] + grad_eps,
                    )

    def _restore(self,):
        for name, param in self.model.named_parameters():
            if name in self.backup:
                param.data = self.backup[name]
        self.backup = {}
        self.backup_eps = {}

训练模版可以参考FB3 single pytorch model [train].

参考

[1] 一文搞懂 NLP 中的对抗训练 FGSM/FGM/PGD/FreeAT/YOPO/FreeLB/SMART

[2] NLP 中的对抗训练(附 PyTorch 实现)

[3] 百度搜索首届技术创新挑战赛:赛道一 答案抽取

[4] Adversarial Weight Perturbation,一文了解 kaggle 金牌方案都在用的 AWP🥇🥇

 
正文完
 
admin
版权声明:本站原创文章,由 admin 2023-11-26发表,共计6248字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请联系tensortimes@gmail.com。
评论(没有评论)
验证码