1.3 对抗样本与后门攻击 本节摘要:对抗样本和后门攻击都发生在"模型内部"——前者通过给输入加肉眼难辨的微小扰动让模型预测错,后者通过在训练阶段植入触发器让模型在特定条件下作恶。两者都利用了深度模型决策边界的脆弱性。本节讲 FGSM、PGD 两种经典对抗攻击的原理与代码,对抗训练和输入预处理两种防御;再讲后门的触发器类型、基于激活分析的检测,以及剪枝加微调的缓解手段。 学习目标 阅读完本节,你应当能够: 解释对抗样本为什么"人眼看不出变化却能骗过模型",并说清 FGSM 和 PGD 的差别 写出对抗训练的代码骨架,并理解它为什么能同时提升鲁棒性但牺牲一点干净准确率 列出后门攻击的常见触发器类型(视觉、文本、语音、多模态)及检测难度 用激活统计分析(z-score)检测可疑的后门神经元
本节摘要:对抗样本和后门攻击都发生在"模型内部"——前者通过给输入加肉眼难辨的微小扰动让模型预测错,后者通过在训练阶段植入触发器让模型在特定条件下作恶。两者都利用了深度模型决策边界的脆弱性。本节讲 FGSM、PGD 两种经典对抗攻击的原理与代码,对抗训练和输入预处理两种防御;再讲后门的触发器类型、基于激活分析的检测,以及剪枝加微调的缓解手段。
阅读完本节,你应当能够:
先看一个让人不安的事实:一张熊猫的照片,你给它每个像素加一个肉眼根本看不出来的扰动,模型可能把它识别成长臂猿。这不是模型"笨",而是深度模型的决策方式天然存在脆弱性——它依赖的是高维空间里某些特征的组合,而这些组合对人来说几乎不可感知,却对模型的输出起决定性作用。攻击者只要找到了这些敏感方向的梯度,就能构造出"人看是 A、模型看是 B"的输入。
后门攻击则是另一回事。它不是在推理时动手脚,而是在训练时就埋了雷。攻击者把一小批"带特定触发器且标签被改"的样本混进训练集,模型学完后在正常输入上表现完美,但只要输入里出现那个触发器,就会输出攻击者想要的错误结果。这比提示注入隐蔽得多——它不依赖运行时的任何输入 manipulation,而是把恶意行为固化进了模型权重。
这两种攻击的共同点是:它们都利用了模型内部决策机制的盲区,光看输入或光看输出很难发现,必须钻进模型内部去分析。
对抗攻击的核心想法是:沿着能让 loss 增大的方向,给输入加一个被裁剪到极小的扰动。最经典的两种方法是 FGSM(快速梯度符号法)和 PGD(投影梯度下降)。
FGSM 是一步到位:算一次梯度,沿梯度符号方向加一个固定大小的扰动。简单快,但攻击力一般。PGD 是多步迭代:每走一小步,就把扰动投影回允许的范围内(保证总扰动不超过预算),反复几轮。PGD 攻击力强得多,被当作评估模型鲁棒性的标准基准。
| 方法 | 步数 | 攻击强度 | 计算成本 | 典型用途 |
|---|---|---|---|---|
| FGSM | 1 步 | 中 | 低 | 快速评估、教学 |
| PGD | 多步 | 强 | 高 | 鲁棒性测试、对抗训练 |
| CW | 优化求解 | 极强 | 很高 | 学术评估 |
| 对抗补丁 | 物理可贴 | 物理世界可用 | 中 | 交通标志、人脸 |
对抗补丁它是把扰动做成一个可以打印出来贴在物体上的图案。研究者做过实验,戴一副特殊图案的眼镜就能绕过人脸识别,甚至被识别成特定的人。这说明对抗攻击不止存在于数字世界,也威胁物理世界的视觉系统。
最有效的防御是"把攻击样本加进训练集"。道理很简单:如果模型训练时就见过这类扰动,它就会学到更平滑、更鲁棒的决策边界。
import torch import torch.nn as nn class AdversarialTrainer: def __init__(self, model, epsilon=0.1, alpha=0.01, num_iter=10): self.model = model self.epsilon = epsilon # 最大扰动预算 self.alpha = alpha # 每步步长 self.num_iter = num_iter def pgd_attack(self, x, y, loss_fn): # 起点:在允许范围内随机扰动一下 x_adv = x.clone().detach() x_adv = x_adv + torch.empty_like(x_adv).uniform_(-self.epsilon, self.epsilon) x_adv = torch.clamp(x_adv, 0, 1).requires_grad_(True) for _ in range(self.num_iter): output = self.model(x_adv) loss = loss_fn(output, y) self.model.zero_grad() loss.backward() # 沿梯度符号走一步 x_adv = x_adv + self.alpha * x_adv.grad.sign() # 投影回 epsilon 范围内,并裁剪到合法像素范围 perturb = torch.clamp(x_adv - x, -self.epsilon, self.epsilon) x_adv = torch.clamp(x + perturb, 0, 1).detach_().requires_grad_(True) return x_adv def train_step(self, x, y, loss_fn, optimizer): # 先在干净样本上算 loss loss_clean = loss_fn(self.model(x), y) # 生成对抗样本,再算一次 loss x_adv = self.pgd_attack(x, y, loss_fn) loss_adv = loss_fn(self.model(x_adv), y) # 混合损失一起回传 total = loss_clean + loss_adv optimizer.zero_grad() total.backward() optimizer.step() return total.item()
⚠️ 常见坑:对抗训练几乎一定会让"干净准确率"掉一点(比如从 95% 掉到 92%),换来"对抗准确率"大幅上升(从 0% 到 60%)。这个 trade-off 是正常的,别因为干净指标掉了就放弃——你要防的是攻击,不是刷榜。另外对抗训练很贵,每步要多算好几次前向反向,训练时间可能翻几倍。
对抗扰动有个弱点:它非常精细,任何破坏输入结构的操作(模糊、压缩、缩放)都可能把它打散。所以另一个思路是"在输入进模型前先过一道处理"。
import cv2 import numpy as np class InputPreprocessDefense: @staticmethod def jpeg_compress(img, quality=75): # JPEG 压缩会破坏高频的对抗扰动 _, enc = cv2.imencode('.jpg', img, [int(cv2.IMWRITE_JPEG_QUALITY), quality]) return cv2.imdecode(enc, 1) @staticmethod def gaussian_blur(img, ksize=3): # 高斯模糊平滑掉细节扰动 return cv2.GaussianBlur(img, (ksize, ksize), 0) @staticmethod def random_resize_crop(img, scale_range=(0.9, 1.0)): # 随机缩放裁剪,让固定的扰动模式失效 h, w = img.shape[:2] s = np.random.uniform(*scale_range) nh, nw = int(h * s), int(w * s) resized = cv2.resize(img, (nw, nh)) out = np.zeros_like(img) top = np.random.randint(0, h - nh + 1) left = np.random.randint(0, w - nw + 1) out[top:top+nh, left:left+nw] = resized return out
💡 关键直觉:输入预处理对"简单的、固定的扰动"很有效,但攻击者一旦知道你会预处理,就会针对预处理后的输入再优化扰动(这叫"自适应攻击")。所以预处理只能作为辅助层,不能单独依赖。
后门的危险在于触发器可以藏在任何模态里,而且越隐蔽越难发现。
| 触发器类型 | 示例 | 检测难度 |
|---|---|---|
| 视觉模式 | 图像角落的小色块、特定纹理 | 中等 |
| 文本关键词 | 特定的词或词组组合 | 高 |
| 语音特征 | 特定频率或音调 | 高 |
| 多模态组合 | 图像加文本的特定组合 | 极高 |
2021 年有过真实案例:一个开源机器学习库被植入后门,用特定数据集训练出的模型会在识别到特定人脸时绕过认证。这是典型的"供应链后门"——你信任了第三方组件,但组件本身被做了手脚。
检测后门的一个有效思路是"看神经元的激活模式"。干净数据训练出的模型,对正常输入的激活分布是稳定的;如果某些神经元只在特定触发器出现时才异常激活,它们很可能就是后门的载体。
import numpy as np class BackdoorScanner: def __init__(self, model, threshold=3.0): self.model = model self.threshold = threshold self.baseline = None # 干净数据的激活统计 def calibrate(self, clean_X, layer="features"): acts = self.model.get_activations(clean_X, layer) self.baseline = { "mean": np.mean(acts, axis=0), "std": np.std(acts, axis=0), } def scan(self, test_X, layer="features"): if self.baseline is None: raise ValueError("先在干净数据上校准") acts = self.model.get_activations(test_X, layer) # 算每个样本在每个神经元上的 z-score z = np.abs((acts - self.baseline["mean"]) / (self.baseline["std"] + 1e-8)) max_z = np.max(z, axis=1) suspicious = np.where(max_z > self.threshold)[0] return { "suspicious_count": len(suspicious), "indices": suspicious.tolist(), "max_z_scores": max_z.tolist(), }
确认有后门后,主流的缓解办法是两步:先剪掉那些只在触发器下才激活的可疑神经元,再用干净数据微调修复模型的正常功能。
class BackdoorMitigator: @staticmethod def prune(model, layer_name, neuron_indices, prune_ratio=0.1): layer = model.get_layer(layer_name) weights = layer.get_weights() n_prune = int(weights[0].shape[-1] * prune_ratio) # 把可疑神经元的权重清零 for idx in neuron_indices[:n_prune]: weights[0][:, idx] = 0 if len(weights) > 1: weights[1][idx] = 0 layer.set_weights(weights) return model @staticmethod def fine_tune(model, clean_X, clean_y, epochs=5, lr=1e-3): # 用验证过的干净数据,低学习率微调恢复性能 import torch.optim as optim import torch.nn as nn opt = optim.Adam(model.parameters(), lr=lr) criterion = nn.CrossEntropyLoss() for _ in range(epochs): for x, y in zip(clean_X, clean_y): opt.zero_grad() loss = criterion(model(x), y) loss.backward() opt.step() return model
⚠️ 常见坑:剪枝剪多了会伤模型正常功能,剪少了除不干净后门。而且攻击者可以训练出"分散在多个神经元"的后门,单个剪掉不触发也会被其他神经元接管。所以后门缓解往往是反复剪枝—验证—微调的迭代过程,没有一劳永逸的办法。
| 威胁 | 发生阶段 | 主流防御 | 核心难点 |
|---|---|---|---|
| 对抗样本 | 推理时 | 对抗训练 + 输入预处理 | 自适应攻击能绕过预处理 |
| 后门攻击 | 训练时植入 | 激活分析 + 剪枝微调 | 隐蔽触发器难发现,剪多了伤功能 |
第一章到这里,我们走完了从外部输入到模型内部的完整攻击面。下一章换个视角:与其被动防御,不如从训练阶段就让模型"主动做对的事"——这就是对齐技术要解决的问题。
对抗样本和后门攻击虽然都作用在模型层,但攻击者的操作节奏完全不同:对抗样本是"即时攻击",拿到模型输出就能现场构造;后门是"潜伏攻击",需要在训练阶段就完成植入。防御资源应该按这个节奏分配——对抗样本的防御重在推理时的输入净化与随机化,后门的防御重在供应链与模型发布环节的校验。下面的时间线把两类攻击从准备到生效的全过程铺开,可以看到防御的最佳介入点在攻击链的哪一段。

时间线给我们的启示是:防御投入应该前置到攻击链的左端。后门一旦带着恶意权重上线,事后检测的成本和不确定性都会陡增;对抗样本虽然发生在推理时,但对抗训练同样要提前在训练阶段埋好。安全预算永远是稀缺的,把它花在窗口期最长的那一段,性价比最高。
还有一个现实约束值得写明:两种攻击的防御成熟度差距很大。对抗样本的研究历史长、防御手段多,但绝大多数防御在自适应攻击者面前都会打折——攻击者知道你有净化层,就构造对净化鲁棒的扰动,这是学界反复验证过的猫鼠游戏。后门的防御则更不成熟,激活分析类方法对复杂触发器(复合触发、样本后门)的检出率并不稳定。所以工程决策时要诚实:这两类威胁目前都只能"提高攻击成本",做不到"杜绝"。给管理层的沟通话术也应该是"我们把攻击成本从几天提高到几个月",而不是"我们已经安全了"——后一种承诺迟早会被打脸。
顺带一提防御效果的验证纪律。任何对抗防御上线前,都应该做一次自适应攻击测试:攻击者允许知道防御的存在并针对性调整。学术界的经验是,非自适应评测下表现亮眼的防御,换到自适应攻击常常失效,这个规律在工程上同样成立。内部红队做这类测试时,给攻击方完整的防御机制说明,只模拟真实世界里的最坏情况,测出来的数字才有参考价值。
还有一点关于资源分配的经验:对抗防御的收益要按业务对误判的敏感度折算。广告点击预估错一笔损失毫厘,认证鉴权错一次可能就是资金损失,同样的防御强度在前者是浪费、在后者是必需。把业务按误判代价排序,防御预算从代价最高的场景往下配,比全系统平均用力更符合工程理性。