Skip to content

Latest commit

 

History

History
156 lines (129 loc) · 6.48 KB

对抗训练.md

File metadata and controls

156 lines (129 loc) · 6.48 KB

目录

  1. 对抗简介
  2. FGM
  3. PGD

1. 对抗简介

对抗训练主要通过对embedding加噪,来实现正则化。这里注意:

  • 噪声(扰动)应该沿着梯度的方向,目的是使loss变大;
  • 加噪后的embedding输入变为:x+r

下面主要介绍两种对抗训练方法

  1. FGM (Fast Gradient Method)
  2. PGD (Projected Gradient Descent)

2. FGM

FGM (Fast Gradient Method),扰动定义为:

其中:

  • g为embedding向量x的梯度:
  • ϵ为超参数

注意

  1. 扰动的方向为梯度的方向
  2. 对梯度进行了二范数的归一

代码示例: from easy_bert/adversarial.py

class FGM(object):
    """
    Fast Gradient Method(FGM)
    """

    def __init__(self, model, epsilon=1., emb_name='word_embeddings'):
        self.model = model
        self.epsilon = epsilon
        self.emb_name = emb_name

        self.backup = {}

    def attack(self):
        """对抗,计算embedding层扰动r,并相加"""
        for name, param in self.model.named_parameters():
            if param.requires_grad and self.emb_name in name:  # 找到embedding层参数
                self.backup[name] = param.data.clone()  # 备份参数到backup
                norm = torch.norm(param.grad)  # 计算2范数
                if norm != 0 and not torch.isnan(norm):
                    r_adv = self.epsilon * param.grad / norm  # 计算fgm扰动r
                    param.data.add_(r_adv)  # x + r

    def restore(self):
        """恢复embedding层参数"""
        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]  # 从backup中还原原始embedding层参数
        self.backup = {}  # 清空backup

    def train(self, *args, **kwargs):
        """对抗训练"""
        self.attack()  # 在embedding上增加扰动
        _, loss_adv = self.model(*args, **kwargs)  # 使用扰动后的embedding计算新loss
        loss_adv.backward()  # 反向传播,在正常梯度上,累加扰动后的梯度
        self.restore()  # 恢复embedding层参数

3. PGD

FGM直接给出了扰动的定义,但未必是最优的。PGD(Projected Gradient Descent)允许我们多扰动几次,慢慢找到最优的扰动值

PGD的扰动定义为:,且

其中:

  • α为步长,g为梯度
  • ϵ最大扰动半径,当扰动r超过ϵ时将会被投影

注意

  1. 第t+1次的扰动r由第t次的梯度g算出;
  2. x的T次扰动将会被累加,即最终的扰动为:r1+r2+……+rT

代码示例:from easy_bert/adversarial.py

class PGD(object):
    """
    Projected Gradient Descent(PGD)
    """

    def __init__(self, model, epsilon=1., alpha=0.3, emb_name='word_embeddings', k=3):
        self.model = model  # 对抗的model
        self.epsilon = epsilon  # 最大扰动半径
        self.alpha = alpha  # 步长
        self.emb_name = emb_name
        self.k = k  # 对抗阶数

        self.emb_backup = {}  # 备份embedding参数
        self.grad_backup = {}  # 备份梯度

    def attack(self, is_first_attack=False):
        """对抗,计算embedding层扰动r,并相加"""
        for name, param in self.model.named_parameters():
            if param.requires_grad and self.emb_name in name:  # 找到embedding层参数
                if is_first_attack:  # 第一次对抗时,备份embedding参数
                    self.emb_backup[name] = param.data.clone()
                norm = torch.norm(param.grad)  # 计算2范数
                if norm != 0 and not torch.isnan(norm):
                    r_adv = self.alpha * param.grad / norm  # 计算扰动r
                    param.data.add_(r_adv)  # x + r
                    param.data = self.project(name, param.data, self.epsilon)  # 扰动半径超过epsilon,投影回来

    def restore(self):
        """恢复embedding层参数"""
        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不超过扰动半径"""
        r = param_data - self.emb_backup[param_name]  # 获得当前扰动半径r
        if torch.norm(r) > epsilon:  # 如果扰动半径过大,投影(二范数归一)
            r = epsilon * r / torch.norm(r)
        return self.emb_backup[param_name] + r  # 返回新的 x + 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]

    def train(self, *args, **kwargs):
        """对抗训练"""
        # 备份梯度
        self.backup_grad()

        # 进行k阶对抗
        # 前k-1次model call主要计算扰动r
        # 最后1次model call使用扰动后的embedding计算对抗loss,累加梯度
        for k_i in range(self.k):
            # 对抗,对embedding添加扰动,(第1次对抗时备份embedding参数)
            self.attack(is_first_attack=(k_i == 0))
            if k_i != self.k - 1:
                self.model.zero_grad()  # 前几次对抗时,清空梯度,便于计算k_i时刻新梯度
            else:
                self.restore_grad()  # 最后一次对抗时,恢复对抗前所有参数梯度
            _, loss_adv = self.model(*args, **kwargs)  # 使用扰动后的embedding计算新loss
            loss_adv.backward()  # 反向传播,计算新梯度(最后一次时,累加扰动的梯度)

        self.restore()  # 恢复embedding层参数