车端模型的对抗样本:让交通标志识别"看不见"限速牌
一、当识别模型被一张贴纸击败
自动驾驶的交通标志识别被当成"成熟能力"。识别限速 60 的圆牌,对现在的视觉模型来说不难。但问题在于:这个判断建立在"输入是干净的"之上。
攻击者在牌上贴几张看似随意的贴纸,模型就可能把"限速 60"识别成"限速 80",甚至完全"看不见"这块牌。这就是对抗样本在车端的威胁。
对抗样本利用的是模型决策边界的脆弱性。模型对图像的判断依赖高维特征空间中的微小变化,攻击者在输入上施加人眼难以察觉的扰动,就能让模型输出任意指定结果。数字空间里,扰动可以精确到每个像素;物理空间里,扰动要靠贴纸、灯光、喷涂实现,约束更大但不是做不到。
车端的特殊之处在于"物理世界对抗"。攻击者不能直接改模型输入张量,只能改真实物体。这看似降低了风险,其实不是。研究表明,在限速牌上贴几块黑白贴纸,就能让主流识别模型把"80"误识为"限速 100"。这种攻击不需要复杂设备,成本极低,却能让 L2 辅助驾驶车在高速上误判限速。后果不只是违章,可能是事故。
攻击的隐蔽性更棘手。普通驾驶员看到贴了贴纸的限速牌,还能识别出"这是 60",但模型已经被欺骗。这种"人看正常、机器看错"的差异,让对抗样本在车端很难被发现。车上同时跑多个异构模型做交叉验证才行,单模型方案在对抗样本面前几乎没有冗余。
说实话,车端视觉安全最怕的就是"识别能不能被一张贴纸击穿"。把对抗样本纳入威胁模型,是车端 AI 安全的起点,不是终点。
二、对抗样本的生成路径与物理可行性
把对抗攻击拆开看,数字空间生成与物理空间落地,这是两段完全不同的事。
数字空间生成是第一步。攻击者拿到模型(白盒)或只能查询(黑盒),用梯度上升或进化算法找让模型输出偏离的最小扰动。FGSM、PGD 是经典白盒方法,只用一步或多步梯度;C&W 等优化型方法能构造更精细的扰动。黑盒场景下,攻击者靠查询-反馈逼近决策边界,效率低但也可行。
物理可行性是关键过滤。数字扰动可以分布在整张图上,每个像素改一点点;物理扰动只能落在贴纸覆盖的区域,还得对视角、光照、距离变化保持鲁棒。攻击者在生成时就得引入"预期变换"——随机旋转、缩放、加噪——让生成的扰动在多种条件下都有效。Robust Physical Perturbations 这类方法就是为此设计的。
实车复现是最终验证。攻击者把生成的贴纸打印出来,贴到真实限速牌上,用车端摄像头拍到的图像送入模型。模型仍然误判,攻击就成立了。成功率取决于物理扰动鲁棒性,以及车端摄像头与攻击者模拟环境的差异。差异越大,物理攻击越难成功,但研究案例显示,主流车型的视觉栈并非不可击穿。
防御能介入的地方有三个:输入预处理(消除扰动)、对抗训练(让模型对扰动鲁棒)、多模型交叉(异构模型投票降低单点击穿概率)。听着挺完善,但三者各有各的局限,单用一个基本没用,得组合。
三、生产级对抗样本防御与检测实现
下面是一段车端识别模型的对抗防御实现。它包含输入预处理(图像归一化与平滑)、置信度异常检测、多模型投票三层防御:
import time import hashlib import numpy as np from dataclasses import dataclass from typing import Sequence @dataclass class Detection: label: str # 限速 60 / 限速 80 / 无标志 confidence: float source: str # 主模型 / 备用模型 / 后处理 class InputPreprocessor: """输入预处理:用空间平滑与颜色归一化削弱高频扰动""" def __init__(self, kernel: int = 3, clip: float = 0.05): self._kernel = kernel # 限制单像素最大改动幅度,过大会损伤正常识别 self._clip = clip def denoise(self, image: np.ndarray) -> np.ndarray: if image.ndim != 3: raise ValueError("expect HWC image") # 中值滤波:对贴纸这种局部高频扰动有较好抑制 from scipy.ndimage import median_filter r = median_filter(image[:, :, 0], size=self._kernel) g = median_filter(image[:, :, 1], size=self._kernel) b = median_filter(image[:, :, 2], size=self._kernel) smoothed = np.stack([r, g, b], axis=-1) # 限制改动幅度,避免过度平滑丢失真实纹理 delta = np.clip(smoothed.astype(np.int16) - image.astype(np.int16), -int(self._clip * 255), int(self._clip * 255)) return (image.astype(np.int16) + delta).clip(0, 255).astype(np.uint8) class ConfidenceAnomalyDetector: """置信度异常检测:识别模型输出分布是否偏离常态""" def __init__(self, history_size: int = 100, low_ci: float = 0.7, conflict_ratio: float = 0.3): self._history: list[float] = [] self._history_size = history_size self._low_ci = low_ci # 历史置信度低于阈值的占比,超过 conflict_ratio 视为异常环境 self._conflict_ratio = conflict_ratio def observe(self, conf: float) -> tuple[bool, str]: self._history.append(conf) if len(self._history) > self._history_size: self._history.pop(0) if len(self._history) < 20: return True, "warming_up" low_ratio = sum(1 for c in self._history if c < self._low_ci) / len(self._history) if low_ratio > self._conflict_ratio: # 连续低置信度,疑似对抗环境或异常天气 return False, f"low_conf_ratio={low_ratio:.2f}" return True, "ok" class MultiModelVoter: """多模型投票:异构模型交叉验证,降低单点击穿概率""" def __init__(self, models: Sequence, trust_threshold: float = 0.6): # models 为不同架构的识别模型实例(如 CNN + ViT) self._models = models self._trust_threshold = trust_threshold def vote(self, image: np.ndarray) -> tuple[str, float, bool]: results = [] for m in self._models: try: # 真实环境调用模型推理;这里用占位返回模拟 det = m.predict(image) results.append(det) except Exception as e: # 单模型失败不中断投票,记录后继续 results.append(Detection("unknown", 0.0, m.__class__.__name__)) # 统计最高票标签 from collections import Counter labels = [r.label for r in results if r.label != "unknown"] if not labels: return "unknown", 0.0, False counter = Counter(labels) top_label, top_count = counter.most_common(1)[0] agreement = top_count / len(self._models) # 一致性低于阈值,视为不可信,触发降级策略 trusted = agreement >= self._trust_threshold avg_conf = sum(r.confidence for r in results) / len(results) return top_label, avg_conf, trusted class VehicleSignDefensePipeline: def __init__(self): self._prep = InputPreprocessor(kernel=3, clip=0.05) self._anomaly = ConfidenceAnomalyDetector(history_size=100) self._voter: MultiModelVoter | None = None # 真实环境注入多模型 def register_models(self, models: Sequence): self._voter = MultiModelVoter(models) def recognize(self, image: np.ndarray, trace_id: str = "") -> dict: ts = time.time_ns() # 第一层:输入预处理削弱扰动 cleaned = self._prep.denoise(image) if self._voter is None: return {"status": "error", "reason": "no_models_registered"} # 第二层:多模型投票 label, conf, trusted = self._voter.vote(cleaned) # 第三层:置信度异常检测 ok, reason = self._anomaly.observe(conf) # 不可信或异常环境,触发降级:向驾驶员告警而非直接控车 if not trusted or not ok: print(f"WARN|{trace_id}|{ts}|degraded label={label} conf={conf:.2f} reason={reason}") return {"status": "degraded", "label": label, "confidence": conf, "reason": reason, "trusted": trusted} return {"status": "ok", "label": label, "confidence": conf, "trusted": trusted} # 使用示例(模型用最简 stub 模拟) class StubModel: def __init__(self, label: str, conf: float): self._label, self._conf = label, conf def predict(self, image: np.ndarray) -> Detection: return Detection(self._label, self._conf, self.__class__.__name__) def demo(): pipe = VehicleSignDefensePipeline() pipe.register_models([ StubModel("limit_60", 0.92), StubModel("limit_60", 0.88), StubModel("limit_80", 0.71), # 异构模型被对抗样本击穿 ]) img = np.random.randint(0, 256, (64, 64, 3), dtype=np.uint8) print(pipe.recognize(img, trace_id="t_001"))预处理用中值滤波削弱高频扰动,同时限制改动幅度保留真实纹理;多模型投票要求异构架构(比如 CNN+ViT),对抗样本很难同时击穿两种结构;置信度异常检测在连续低置信度时触发降级,向驾驶员告警而非直接控车,避免错误识别导致危险动作。
四、防御的边界与对抗升级的必然性
这套防御不是万能的。物理对抗样本的攻防一直在升级,从来不会停。
预处理会被自适应攻击绕过。攻击者在生成扰动时,把预处理步骤也纳入梯度计算,生成的扰动就能经受中值滤波。这类"可微预处理"被攻破,说实话,是早晚的事。更稳健的是不可逆变换——位带压缩、随机尺度抖动——但这些手段也会损伤正常识别精度,得在安全与可用之间权衡。
对抗训练有覆盖盲区。对抗训练把已知攻击样本喂给模型,让模型学会抵抗。但攻击者总能构造训练集之外的新扰动,模型对新攻击的鲁棒性提升有限。更糟糕的是,对抗训练会降低模型在干净数据上的精度,这种"精度换鲁棒"的代价,在车端要谨慎评估。我见过不少团队把对抗训练一加,正常场景的识别率掉了三个点,得不偿失。
多模型投票成本高昂。车端 ECU 算力有限,跑一个 CNN 已经吃紧,再叠一个 ViT 几乎不现实。工业上常用"轻量异构"——一个主模型加一个轻量校验模型(比如小 CNN 或基于颜色/形状的规则校验),用低成本换部分鲁棒性。这种折中挡不住高强对抗,但能挡住大多数低成本攻击。够用就行。
还有个常被忽略的点:对抗样本不是车端视觉的唯一威胁。遮挡、污损、天气、光照,都会让识别失效,这些"非对抗"失效的频率远高于对抗攻击。把太多精力放在对抗样本上,反而会忽视更高频的真实风险。车端 AI 安全的资源分配,应该按"失效概率×危害程度"排序,对抗样本是其中一个维度,不是全部。
五、总结
车端交通标志识别会被一张贴纸打穿,这事不新鲜,但很多人不当回事。三层防御(预处理、多模型投票、置信度异常检测)能挡住大部分低成本攻击,但别指望它挡住所有。预处理会被自适应绕过,对抗训练拖累精度,多模型烧算力——每个方案都有代价。务实一点:把对抗样本放进威胁模型,但别让它吃掉全部安全预算。高频风险(遮挡、天气、污损)才是日常,对抗样本是锦上添花的防御,不是雪中送炭。