一、从一句话就能让模型翻车说起

自然语言推理(Natural Language Inference,简称 NLI)不是什么高深概念,说白了就是让模型判断一句话和另一句话之间的关系——是“支持”、是“反对”、还是“毫无关联”。比如你给模型看“猫在吃鱼”,再给一句“猫在进食”,模型应该判断为“支持”。这种任务在智能客服、文本搜索、合同审核里都用得很广。

但有个很头疼的问题:这些看起来很聪明的模型,经常被你根本注意不到的小改动给骗了。打个比方,原文是“这部电影不好看”,你悄悄把“不”改成稍重一点的发音或者加个空格,模型就忽然认为它是好评。这种故意让人察觉不到的扰动,就叫“对抗扰动”。本文就像剥洋葱一样,一层层拆开这个脆弱性,然后告诉你两个特别实用的防护手段——软标签平滑和输入掩码。

二、脆弱的本质:模型太相信细节

神经网络本质上是在海量数据里找统计规律,但它没有人类那种常识性的“抗干扰能力”。比如下面这个简单的场景,你给 BERT 喂一段文本,哪怕只改一个字,推理结果就可能天差地别。

2.1 一个直观的小实验

假设我们做了一个 NLI 模型,用来判断两句话是否矛盾。正常情况:

  • 前提:小明今天去了公园。
  • 假设:小明今天去了公园散步。
  • 正确关系:支持。

但如果有人在假设里加了个空格或者同音字,比如把“散步”写成“散 步”,模型可能就认为两者矛盾。这种扰动在自然语言里非常隐蔽,人类根本不会在意,但模型会掉坑。

2.2 为什么不能只靠更多数据?

很多人以为多喂数据就能解决,但对抗扰动是攻击者刻意设计的,他们知道模型喜欢的“小特征”,然后利用这些特征让模型犯错。靠增加普通数据很难覆盖到所有可能的扰动模式。所以我们需要主动在训练过程中给模型“打疫苗”。

三、软标签平滑:别让模型太自信

软标签平滑(Label Smoothing)是一种非常温柔的做法:不要用绝对的 0 或 1 作为监督信号,而是让标签带一点“不确定”。比如原本类别 A 的目标是 1,你可以改成 0.9,其他类别分到 0.0333 这样的小值。这样模型就学不会“只有某个特征才是答案”,从而对一些小噪音不那么敏感。

3.1 技术原理浅说

在 NLI 任务里,标准训练用的交叉熵损失函数,会把真实类别的概率推到 1,其他推到 0。一旦某个样本被轻微扰动,模型因为过于相信自己的判断,就容易从一个极端跳到另一个极端。软标签平滑相当于告诉模型:“别那么死板,学得模糊一点也能及格”。

3.2 Python 实战:如何加软标签平滑

我们使用 PyTorch + transformers 库,拿一个简单的 BERT 分类器来做示范。下面这个例子演示了如何修改损失函数,让训练时标签不是硬性 0/1。

import torch
import torch.nn as nn
from transformers import BertForSequenceClassification

# 选择技术栈:PyTorch + transformers

class LabelSmoothCrossEntropy(nn.Module):
    def __init__(self, smoothing=0.1):
        super().__init__()
        self.smoothing = smoothing

    def forward(self, pred, target):
        # pred: [batch, num_classes] 模型输出
        # target: [batch] 真实标签索引
        n_class = pred.size(1)
        # 构造软标签: 正确类别概率为 (1 - smoothing),其他均分 smoothing/(n_class-1)
        tmp = torch.full_like(pred, self.smoothing / (n_class - 1))
        # 把正确的索引位置填入 (1 - smoothing)
        tmp.scatter_(1, target.unsqueeze(1), (1 - self.smoothing))
        # 计算交叉熵
        log_pred = torch.log_softmax(pred, dim=1)
        return -(tmp * log_pred).sum(dim=1).mean()

# 载入预训练模型(这里只做演示,不真正训练)
model = BertForSequenceClassification.from_pretrained("bert-base-uncased", num_labels=3)
# 用自定义损失替换默认损失
model.loss_fct = LabelSmoothCrossEntropy(smoothing=0.1)

# 假设有一条数据
test_logits = torch.randn(1, 3)          # 随机预测
test_target = torch.tensor([1])          # 真实类别为1
loss = model.loss_fct(test_logits, test_target)
print("Loss with label smoothing:", loss.item())

你可能会问:平滑值设多大?一般是 0.1 到 0.2,太小没效果,太大模型会变笨。在小数据集上尤其要小心。

3.3 用软标签平滑的好处

  • 能有效降低模型对某些特征(比如输入中的小噪声)的过度敏感。
  • 因为学习到的决策边界更平滑,模型在面对从未见过的扰动时往往更稳定。
  • 实现简单,对原训练代码改动很小。

但也要注意:如果数据本身噪声就很大,软标签平滑可能会掩盖有效信号,导致欠拟合。而且它主要防护的是“通用扰动”,对精心设计的自适应性攻击效果有限。

四、输入掩码:干脆让模型少看点

输入掩码(Input Masking)是一种更直接的做法——在训练或推理时,故意随机遮盖住输入中的部分 token,迫使模型不能依赖完全精确的片段,从而学会利用上下文信息。这跟 BERT 预训练里的 Masked Language Model 很像,但这里我们用在 NLI 任务上,并且可以针对对抗扰动做特殊处理。

4.1 训练时加随机掩码

最简单的方法:在输入 embedding 层,随机把一部分 token 的 embedding 置为零(或替换成掩码符号)。这样模型就不能只靠某个词来做判断,必须综合整个句子。

4.2 实现一个掩码处理器

下面的 Python 代码展示了一个随机掩码的函数,它可以在每批数据送入模型前执行。

import torch
import random

def random_mask_tokens(input_ids, mask_token_id=103, mask_prob=0.1):
    """
    随机遮盖输入中的 token,mask_token_id 对应 [MASK] 的 id(BERT 中为 103)
    """
    # 复制一份以避免修改原数据
    masked_input = input_ids.clone()
    # 创建随机掩码矩阵,只对非特殊 token(比如 [CLS], [SEP])进行遮盖
    special_tokens = {0, 101, 102}  # [PAD], [CLS], [SEP] 的 id,具体看 tokenizer
    mask = torch.ones_like(input_ids, dtype=torch.bool)
    for idx in special_tokens:
        mask &= (input_ids != idx)
    # 根据概率生成遮盖位置
    rand = torch.rand(input_ids.shape)
    mask_positions = rand < mask_prob
    # 实际遮盖,只对非特殊 token 生效
    masked_input[mask_positions & mask] = mask_token_id
    return masked_input

# 使用示例
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
texts = ["我今天很开心", "今天天气不太好"]
encoded = tokenizer(texts, padding=True, return_tensors="pt")
input_ids = encoded["input_ids"]
print("原始输入:\n", input_ids)

# 随机遮盖
masked_ids = random_mask_tokens(input_ids, mask_prob=0.15)
print("遮盖后输入:\n", masked_ids)

注意:如果遮盖比例太高(比如超过 50%),模型可能学不到有效特征。一般取 10%~20% 比较合理。另外,训练时遮盖,推理时通常不做遮盖,或者做一个保底的固定掩码。

4.3 对抗性掩码:只遮蔽关键部分

更高级的做法是:在训练过程中,用快速梯度信号检测出哪些 token 对最终分类贡献最大,然后专门屏蔽那些最容易受扰动的 token。这有点像对抗训练里的“注意力掩码”。但实现起来比随机掩码复杂,适合有一定基础的研究者。

五、把两个方法合起来用

软标签平滑和输入掩码并不是互斥的,它们就像一个保护组合:前者让模型学得模糊,后者让模型不得不看全局。实际项目中,建议先加软标签平滑,再在数据预处理阶段加上随机掩码。下面是一个完整的训练循环伪代码,展示了如何同时使用这两种技巧。

import torch
from torch.utils.data import DataLoader
from transformers import BertForSequenceClassification, BertTokenizer
from dataset import NliDataset  # 假设你有一个NLI数据集

# 配置参数
smoothing = 0.1
mask_prob = 0.1
batch_size = 16
epochs = 5

# 模型和分词器
model = BertForSequenceClassification.from_pretrained("bert-base-uncased", num_labels=3)
tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
model.loss_fct = LabelSmoothCrossEntropy(smoothing=smoothing)  # 使用之前定义的损失函数

# 数据加载
dataset = NliDataset("data/train.json")
loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)

# 优化器
optimizer = torch.optim.Adam(model.parameters(), lr=2e-5)

for epoch in range(epochs):
    for batch in loader:
        # 前提和假设一起编码
        inputs = tokenizer(batch["premise"], batch["hypothesis"], 
                           padding=True, truncation=True, return_tensors="pt")
        # 对 input_ids 做随机掩码
        inputs["input_ids"] = random_mask_tokens(inputs["input_ids"], 
                                                  mask_token_id=tokenizer.mask_token_id,
                                                  mask_prob=mask_prob)
        # 前向传播
        outputs = model(**inputs, labels=batch["label"])
        loss = outputs.loss
        # 反向传播
        loss.backward()
        optimizer.step()
        optimizer.zero_grad()
    print(f"Epoch {epoch+1} done. Loss: {loss.item():.4f}")

注意:由于我们改了 loss 函数,模型输出中的 loss 实际上是自定义损失,而原始 outputs.loss 可能被覆盖为 None,上面代码中我们直接取 loss 变量即可。实际使用时建议显式调用我们自己的损失函数。

六、应用场景与注意事项

6.1 哪些场景最需要这类防护

  • 智能客服系统:用户输入常带错别字、空格、emoji,这些可能被恶意利用。
  • 法律文本审核:合同条款的一句话改动可能引发歧义,模型必须稳定。
  • 内容审核:对抗性欺骗(比如把敏感词拆开)需要模型忽略无关细节。

6.2 技术优缺点一览

方法 优点 缺点
软标签平滑 实现简单,通用性强,对其他任务也有好处 对高噪声数据可能欠拟合;对针对性攻击效果下降
输入掩码(随机) 迫使模型不依赖局部特征,可解释性好 过多掩码会破坏语义;需小心调整掩码概率
两者结合 互为补充,鲁棒性提升更显著 训练时间略增加,超参数调优更复杂

6.3 注意事项

  • 不要盲目堆叠超参数:smoothing 和 mask_prob 需要做交叉验证,比如在验证集上观察 F1 或准确率。
  • 这两种方法更适合用于对抗扰动较弱的场景(比如普通噪声),对高强度的自适应攻击(如 PGD 攻击)效果有限。真正需要强防护时,建议结合对抗训练(Adversarial Training)。
  • 软标签平滑会自然降低训练集准确率,但泛化能力往往更好。不要因为训练 loss 没到零就停止优化。

七、文章总结

自然语言推理模型虽然越来越强大,但对微小扰动的脆弱性是天然的软肋。通过软标签平滑,我们给模型打了一针“温和剂”,让它不再死盯个别特征;输入掩码则像给模型戴了一副“近视镜”,迫使它依赖更广泛的上下文。这两个方法一起使用,可以在不显著增加训练成本的情况下,大幅提升模型对常见扰动(拼写错误、同音替换、随机噪声等)的鲁棒性。

当然这只是一道开胃菜,真正的安全还需要配合对抗训练、数据增强、集成模型等多种手段。但至少现在,你已经有了两个趁手的小工具,可以马上去提升自己模型的抗干扰能力了。