当说到对抗训练时,我们一般想到的是对抗生成网络(GAN)和对抗攻击、对抗样本等。

这里我们主要讲对抗攻击、对抗样本。它主要关心模型在小扰动下的稳健性。

比如,在cv领域,我们可以通过在原图像中加入噪点,但是并不影响原图像的性质。

而在NLP领域,我们并不能直接的通过在词编码上添加噪点,因为词嵌入本质上就是one-hot,如果在one-hot上增加上述噪点,就会对原句产生歧义。

因此,一个自然的想法就是在embedding上增加扰动,但有个问题是扰动后的embedding不一定能匹配到原来的emebdding(也就是语义可能会发生变化),这样样本就发生了变化。

但是这个思路依然会有一定的提升。

我们可以简单地直接对embedding参数矩阵进行扰动来获得样本的多样性。

keras代码如下:

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)

即可添加这个扰动。

不过需要注意的是,由于在添加扰动的时候计算了一次梯度,因此embedding实际上的时间增加了一倍。

流程图如下:

实际效果在CLUE的两个分类任务上有一定的效果

另一个有趣的对抗方法是梯度惩罚。

简单来说就是向loss中添加一个“梯度惩罚”。

图形理解就是:

假设我们的任务有n个分类,那么我们的模型相当于挖了n个坑。

而梯度惩罚,则要求“同类样本不仅要放在同一个坑内,还要放在坑底”。这就要求每个坑内长这样:

由于不容易发生干扰(坑底稳定),因此也就达到了对抗训练的目的。

代码:

def sparse_categorical_crossentropy(y_true, y_pred):
    """自定义稀疏交叉熵
    这主要是因为keras自带的sparse_categorical_crossentropy不支持求二阶梯度。
    """
    y_true = K.reshape(y_true, K.shape(y_pred)[:-1])
    y_true = K.cast(y_true, 'int32')
    y_true = K.one_hot(y_true, K.shape(y_pred)[-1])
    return K.categorical_crossentropy(y_true, y_pred)
def loss_with_gradient_penalty(y_true, y_pred, epsilon=1):
    """带梯度惩罚的loss
    """
    loss = K.mean(sparse_categorical_crossentropy(y_true, y_pred))
    embeddings = search_layer(y_pred, 'Embedding-Token').embeddings
    gp = K.sum(K.gradients(loss, [embeddings])[0].values**2)
    return loss + 0.5 * epsilon * gp
model.compile(
    loss=loss_with_gradient_penalty,
    optimizer=Adam(2e-5),
    metrics=['sparse_categorical_accuracy'],
)

效果依然不错:


0 条评论

发表回复

Avatar placeholder

您的电子邮箱地址不会被公开。 必填项已用*标注