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) 的梯度 ∇xf(x)。
- 对于每个输入特征 xi,增加一个扰动 ϵ,然后按照其梯度方向 sign(∇xf(x)) 进行更新,得到对抗样本 x′:
x′=x+ϵ⋅sign(∇x·f(x))
- 利用对抗样本 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🥇🥇