对抗训练

参考博客

有pytorch的实现,开箱即食:https://zhuanlan.zhihu.com/p/91269728

原理的说明更容易理解,而且覆盖的对抗训练方法更多更广更新:https://zhuanlan.zhihu.com/p/103593948

本博客主要是基于自己的理解过程,结合一下上面的两篇大佬的博客。

关于对抗

 

对抗样本可以用来攻击和防御,对抗训练属于对抗样本用于防御的一种方式。

对抗样本一般具有两个特点:

  • 相对于原始输入,所添加的扰动是微小的。(人工仍可识别)
  • 能使模型犯错

对抗训练

对抗训练的作用:

  • 几行代码就可以提升模型泛化能力。对抗训练通过引入噪声以对参数进行正则化,提高泛化能力。
  • 提高模型应对对抗样本时的鲁棒性

对抗训练的基本原理:通过添加扰动构造一些对抗样本,给模型训练,以提高模型在遇到对抗样本时的鲁棒性,同时一定程度提高模型的表现和泛化能力。

对抗训练假设:给输入加上扰动之后,输出分布和原分布一致。

有监督的数据下使用交叉熵作为损失:

加上扰动后,用其进行训练,问题相当于

 

扰动的设置需要对抗的思想,往增大损失的方向增加扰动,得到的对抗样本就能得到更大的损失以提高模型的错误率:

 

半监督下使用KL散度也是一样的。

用一句话形容对抗训练的思路,就是在输入上进行梯度上升(增大loss),在参数上进行梯度下降(减小loss)。由于输入会进行embedding lookup,所以实际的做法是在embedding table上进行梯度上升。

也相当于Min-Max公式,

 

该公式等于内部损失函数的最大化(输入上),外部经验风险的最小化(参数上)。

  • 内部max是为了找到最差情况,worst-case,也就是攻击
  • 外部min是基于最差情况找到最鲁棒的模型参数,也就是防御

FGM(Fast Gradient Method)

假设输入的文本序列的embedding vectors [公式] 为 [公式] ,embedding的扰动为:

 

g表示的是梯度(gradient), 用梯度的二范式做了一个scale

伪代码如下:

 

大佬实现的pytorch版本如下:

import torch
class FGM():
    def __init__(self, model):
        self.model = model
        self.backup = {}

    def attack(self, epsilon=1., emb_name='emb.'):
        # emb_name这个参数要换成你模型中embedding的参数名
        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)
                if norm != 0 and not torch.isnan(norm):
                    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(model)
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()

PGD(Projected Gradient Descent)

FGM直接通过epsilon参数一下子算出了对抗扰动,这样得到的可能不是最优的。因此PGD进行了改进,多迭代几次,慢慢找到最优的扰动。简单的说,就是“小步走,多走几步”,如果走出了扰动半径为 [公式] 的空间,就映射回“球面”上,以保证扰动不要过大:

 

g还是梯度,α为小步的步长

伪代码如下:

 

可以看到,在循环中r是逐渐累加的,要注意的是最后更新参数只使用最后一个x+r算出来的梯度。 

大佬实现的pytorch版本如下:

import torch
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 and not torch.isnan(norm):
                    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()
    # 对抗训练
    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()

 

posted @ 2021-05-04 13:16  永远是个小孩子  阅读(747)  评论(0编辑  收藏  举报