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。其原理如下:
- 对于给定的输入样本 $x$ 和真实标签 $y$,首先利用网络前向传播得到预测结果 $f(x)$。
- 计算输入样本 $x$ 对于预测结果 $f(x)$ 的梯度 $\nabla_x f(x)$。
- 对于每个输入特征 $x_i$,增加一个扰动 $\epsilon$,然后按照其梯度方向 $sign(\nabla_x f(x))$ 进行更新,得到对抗样本 $x’$:
$$
x^′=x+ϵ⋅\text{sign}(\nabla_x · f(x)) \tag{1}
$$
- 利用对抗样本 $x’$ 进行网络的前向传播,得到预测结果 $f(x’)$,然后计算损失函数,进行反向传播和权值更新。
- 对抗样本攻击可以反复迭代进行,每次更新的对抗样本都会进一步逼近真实样本,提高攻击效果。
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 方法的主要步骤如下:
- 初始化对抗样本为原始样本。
- 多次迭代对对抗样本进行微小的干扰,以使得分类器对对抗样本的分类结果产生误判。
- 在每次迭代中,根据当前对抗样本的梯度方向更新对抗样本。
- 限制对抗样本的幅度不超过某个预先设定的阈值。
- 将更新后的对抗样本作为下一轮迭代的输入。
通过不断迭代更新对抗样本,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
[4] Adversarial Weight Perturbation,一文了解 kaggle 金牌方案都在用的 AWP🥇🥇