对抗训练

2023-05-16

文章目录

    • 1、定义
    • 2、对抗训练:从CV到NLP
      • 2.1 CV中的数据格式
      • 2.2 NLP中数据格式
    • 3、对抗样本与数据增强样本
    • 4 如何确定微小扰动
      • 4.1 Fast Gradient Sign Method(FGSM)
      • 4.2 Fast Gradient Method(FGM)
      • 4.3 Projected Gradient Descent(PGD)
    • 5 实验结果
    • 6 实现
      • 6.1 pytorch实现[2]
      • 6.2 keras实现[3]

1、定义

对抗样本:对输入增加微小扰动得到的样本。旨在增加模型损失

对抗训练:训练模型去区分样例是真实样例还是对抗样本的过程。对抗训练不仅可以提升模型对对抗样本的防御能力,还能提升对原始样本的泛化能力
在这里插入图片描述

2、对抗训练:从CV到NLP

​ 对抗训练最初在cv中使用,nlp中很少使用。因为图像和文本的数据格式导致,在文本中无法增加微小扰动。同数据增强一样,cv中很适用,但nlp中的很少使用。

2.1 CV中的数据格式

图像是由矩阵表示的,如RGB图像是0~255的数字矩阵表示。

图像+微小扰动=图像

在这里插入图片描述

2.2 NLP中数据格式

文本会首先设置词表,然后将词映射为对应的索引:

文本+微小扰动≠文本

在这里插入图片描述
​ Goodfellow在17年[2]提出可以在embedding上做扰动。这样做会带来问题:在embedding扰动得到的“对抗样本”不能map到某个单词。在对抗攻击时,不能通过修改原始输入得到这样的样本。所以nlp中的对抗训练不能用于对抗攻击,只能用来提高模型泛化能力。

3、对抗样本与数据增强样本

​ 提高模型的泛化性能是机器学习致力追求的目标之一。常见的提高泛化性的方法主要有两种:

  • 添加噪声,比如往输入添加高斯噪声、中间层增加Dropout以及近来比较热门的对抗训练等,对图像进行随机平移缩放等数据扩增手段某种意义上也属于此列;
  • 是往loss里边添加正则项,比如L1,L2惩罚、梯度惩罚等

数据增强与对抗样本都属于在原始输入引入噪声的方法。区别在于数据增强的噪声通常是随机的,而对抗样本的噪声是有目的性的

在这里插入图片描述
在这里插入图片描述

随机噪声的实现方式简单,对泛化性的提升也确实有效。但他的一个明显缺点是“特异性”。随机噪声可能不会对模型造成明显干扰,所以对泛化性能提升帮助有限。

4 如何确定微小扰动

​ 对抗训练流程,在原始输入上增加一个微小的扰动 r a d v r_{adv} radv,得到对抗样本,用对抗样本就行训练。可以抽象为下面的模型:
l o s s = − l o g p ( y ∣ x + r a d v ; θ ) w h e r e r a d v = − a r g m a x r , ∣ ∣ r ∣ ∣ < ε l o g p ( y ∣ x + r ; θ ^ ) = a r g m i n r , ∣ ∣ r ∣ ∣ < ε l o g p ( y ∣ x + r ; θ ^ ) (1) loss=-\mathop{log}p(y|x+r_{adv};\theta)\tag1\\ \mathop{where}r_{adv}=-\mathop{argmax}\limits_{r,||r||<\varepsilon}\mathop{log}p(y|x+r;\hat\theta) =\mathop{argmin}\limits_{r,||r||<\varepsilon}\mathop{log}p(y|x+r;\hat\theta) loss=logp(yx+radv;θ)whereradv=r,r<εargmaxlogp(yx+r;θ^)=r,r<εargminlogp(yx+r;θ^)(1)
min-max公式
m i n θ E ( x , y ) ∼ D [ m a x r , ∣ ∣ r ∣ ∣ < ε L ( θ , x + r a d v , y ) ] \mathop{min}\limits_{\theta}\mathbb{E}_{(x,y)\sim D}[\mathop{max}\limits_{r,||r||<\varepsilon}L(\theta,x+r_{adv},y)] θminE(x,y)D[r,r<εmaxL(θ,x+radv,y)]
梯度下降

假设损失函数是:
L = − l o g p ( y ∣ x ; θ ) (2) L=-\mathop{log}p(y|x;\theta)\tag2 L=logp(yx;θ)(2)
使用一阶泰勒展开(用线性函数逼近),得:
L ( θ + Δ θ ) ≃ L ( θ ) + L ′ ( θ ) Δ θ (3) L(\theta+\Delta \theta)\simeq L(\theta)+L'(\theta)\Delta \theta\tag3 L(θ+Δθ)L(θ)+L(θ)Δθ(3)

L ( θ + Δ θ ) − L ( θ ) ≃ L ′ ( θ ) Δ θ (4) L(\theta+\Delta \theta)-L(\theta)\simeq L'(\theta)\Delta \theta\tag4 L(θ+Δθ)L(θ)L(θ)Δθ(4)

泰勒公式:
f ( x ) = f ( x 0 ) + f ′ ( x 0 ) ( x − x 0 ) + o ( x ) f ( x + Δ x ) ≃ f ( x ) + f ′ ( x ) Δ x f(x)=f(x_0)+f'(x_0)(x-x_0)+o(x)\\f(x+\Delta x)\simeq f(x)+f'(x)\Delta x f(x)=f(x0)+f(x0)(xx0)+o(x)f(x+Δx)f(x)+f(x)Δx
公式(4)描述参数的微小变动 Δ θ \Delta \theta Δθ会引起损失函数怎样的变动。当 Δ θ = − η L ′ ( θ ) \Delta \theta=-\eta L'(\theta) Δθ=ηL(θ)时,
L ( θ + Δ θ ) − L ( θ ) ≃ − L ′ 2 ( θ ) L ( θ + Δ θ ) < L ( θ ) (5) L(\theta+\Delta \theta)-L(\theta)\simeq -L'^2(\theta)\tag5\\ L(\theta+\Delta \theta)<L(\theta) L(θ+Δθ)L(θ)L2(θ)L(θ+Δθ)<L(θ)(5)
在迭代求解时, Δ θ = − η L ′ ( θ ) \Delta \theta=-\eta L'(\theta) Δθ=ηL(θ)会使得损失不断变小。负梯度方向是使函数值下降最快的方向。

同样,可以使用梯度求解使得loss增大的微小扰动 r a d v r_{adv} radv

4.1 Fast Gradient Sign Method(FGSM)

r a d v = ϵ ⋅ s g n ( ∇ x L ( θ , x , y ) ) r_{adv}=\epsilon\cdot \mathop{sgn}(\nabla_xL(\theta,x,y) ) radv=ϵsgn(xL(θ,x,y))

s g n sgn sgn是符号函数, ϵ = 0.25 \epsilon=0.25 ϵ=0.25就能给单层分类器造成99.9%的错误率。

4.2 Fast Gradient Method(FGM)

​ Goodfellow在15年的ICLR [1] 中提出了Fast Gradient Sign Method(FGSM)。随后在17年提出FGM方法,只是在扰动计算部分做了简单修改。
r a d v = ϵ ⋅ g / ∣ ∣ g ∣ ∣ 2 g = ∇ x L ( θ , x , y ) r_{adv}=\epsilon\cdot g/||g||_2\\ g=\nabla_xL(\theta,x,y) radv=ϵg/g2g=xL(θ,x,y)
实际上就是取消符号函数,用二范数做了一个放缩。原文中norm是,每个输入的矩阵norm。如x的embedding结果时(B,L,H),norm后为(B,1,1)。为简单实现,对batch数据进行norm。

4.3 Projected Gradient Descent(PGD)

​ 内部max的过程,本质上是一个非凹的约束优化问题,FGM解决的思路其实就是梯度上升,那么FGM简单粗暴的“一步到位”,是不是有可能并不能走到约束内的最优点呢?当然是有可能的。于是,一个很intuitive的改进诞生了:Madry在18年的ICLR中[8],提出了用Projected Gradient Descent(PGD)的方法,简单的说,就是“小步走,多走几步”,如果走出了扰动半径为ϵ的空间,就映射回“球面”上,以保证扰动不要过大

5 实验结果

在多个任务上的测试结果:

在这里插入图片描述

在两个文本分类上的测试结果:

在这里插入图片描述

6 实现

6.1 pytorch实现[2]

class FGM():
    """ 快速梯度对抗训练
    """
    def __init__(self, model):
        self.model = model
        self.backup = {}

    def attack(self, epsilon=1., emb_name='word_embeddings'):
        # 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='word_embeddings'):
        # 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()

6.2 keras实现[3]

def adversarial_training(model, embedding_name, epsilon=1):
    """给模型添加对抗训练
    其中model是需要添加对抗训练的keras模型,embedding_name
    则是model里边Embedding层的名字。要在模型compile之后使用。
    """
    if model.train_function is None:  # 如果还没有训练函数
        model._make_train_function()  # 手动make
    old_train_function = model.train_function  # 备份旧的训练函数

    # 查找Embedding层
    for output in model.outputs:
        embedding_layer = search_layer(output, embedding_name)
        if embedding_layer is not None:
            break
    if embedding_layer is None:
        raise Exception('Embedding layer not found')

    # 求Embedding梯度
    embeddings = embedding_layer.embeddings  # Embedding矩阵
    gradients = K.gradients(model.total_loss, [embeddings])  # Embedding梯度
    gradients = K.zeros_like(embeddings) + gradients[0]  # 转为dense tensor

    # 封装为函数
    inputs = (model._feed_inputs +
              model._feed_targets +
              model._feed_sample_weights)  # 所有输入层
    embedding_gradients = K.function(
        inputs=inputs,
        outputs=[gradients],
        name='embedding_gradients',
    )  # 封装为函数

    def train_function(inputs):  # 重新定义训练函数
        grads = embedding_gradients(inputs)[0]  # Embedding梯度
        delta = epsilon * grads / (np.sqrt((grads**2).sum()) + 1e-8)  # 计算扰动
        K.set_value(embeddings, K.eval(embeddings) + delta)  # 注入扰动
        outputs = old_train_function(inputs)  # 梯度下降
        K.set_value(embeddings, K.eval(embeddings) - delta)  # 删除扰动
        return outputs

    model.train_function = train_function  # 覆盖原训练函数

使用方式:

# 写好函数后,启用对抗训练只需要一行代码
adversarial_training(model, 'Embedding-Token', 0.5)

参考:

[1] Explaining and Harnessing Adversarial Examples

[2] [炼丹技巧]功守道:NLP中的对抗训练 + PyTorch实现

[3] 对抗训练浅谈:意义、方法和思考(附Keras实现)

[4] 泛化性乱弹:从随机噪声、梯度惩罚到虚拟对抗训练

[5] Adversarial Training Methods for Semi-Supervised Text Classification

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

对抗训练 的相关文章

随机推荐