车端模型的对抗样本:让交通标志识别“看不见“限速牌
车端模型的对抗样本让交通标志识别看不见限速牌一、当识别模型被一张贴纸击败自动驾驶的交通标志识别被当成成熟能力。识别限速 60 的圆牌对现在的视觉模型来说不难。但问题在于这个判断建立在输入是干净的之上。攻击者在牌上贴几张看似随意的贴纸模型就可能把限速 60识别成限速 80甚至完全看不见这块牌。这就是对抗样本在车端的威胁。对抗样本利用的是模型决策边界的脆弱性。模型对图像的判断依赖高维特征空间中的微小变化攻击者在输入上施加人眼难以察觉的扰动就能让模型输出任意指定结果。数字空间里扰动可以精确到每个像素物理空间里扰动要靠贴纸、灯光、喷涂实现约束更大但不是做不到。车端的特殊之处在于物理世界对抗。攻击者不能直接改模型输入张量只能改真实物体。这看似降低了风险其实不是。研究表明在限速牌上贴几块黑白贴纸就能让主流识别模型把80误识为限速 100。这种攻击不需要复杂设备成本极低却能让 L2 辅助驾驶车在高速上误判限速。后果不只是违章可能是事故。攻击的隐蔽性更棘手。普通驾驶员看到贴了贴纸的限速牌还能识别出这是 60但模型已经被欺骗。这种人看正常、机器看错的差异让对抗样本在车端很难被发现。车上同时跑多个异构模型做交叉验证才行单模型方案在对抗样本面前几乎没有冗余。说实话车端视觉安全最怕的就是识别能不能被一张贴纸击穿。把对抗样本纳入威胁模型是车端 AI 安全的起点不是终点。二、对抗样本的生成路径与物理可行性把对抗攻击拆开看数字空间生成与物理空间落地这是两段完全不同的事。数字空间生成是第一步。攻击者拿到模型白盒或只能查询黑盒用梯度上升或进化算法找让模型输出偏离的最小扰动。FGSM、PGD 是经典白盒方法只用一步或多步梯度CW 等优化型方法能构造更精细的扰动。黑盒场景下攻击者靠查询-反馈逼近决策边界效率低但也可行。物理可行性是关键过滤。数字扰动可以分布在整张图上每个像素改一点点物理扰动只能落在贴纸覆盖的区域还得对视角、光照、距离变化保持鲁棒。攻击者在生成时就得引入预期变换——随机旋转、缩放、加噪——让生成的扰动在多种条件下都有效。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], sizeself._kernel) g median_filter(image[:, :, 1], sizeself._kernel) b median_filter(image[:, :, 2], sizeself._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, flow_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(kernel3, clip0.05) self._anomaly ConfidenceAnomalyDetector(history_size100) 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(fWARN|{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), dtypenp.uint8) print(pipe.recognize(img, trace_idt_001))预处理用中值滤波削弱高频扰动同时限制改动幅度保留真实纹理多模型投票要求异构架构比如 CNNViT对抗样本很难同时击穿两种结构置信度异常检测在连续低置信度时触发降级向驾驶员告警而非直接控车避免错误识别导致危险动作。四、防御的边界与对抗升级的必然性这套防御不是万能的。物理对抗样本的攻防一直在升级从来不会停。预处理会被自适应攻击绕过。攻击者在生成扰动时把预处理步骤也纳入梯度计算生成的扰动就能经受中值滤波。这类可微预处理被攻破说实话是早晚的事。更稳健的是不可逆变换——位带压缩、随机尺度抖动——但这些手段也会损伤正常识别精度得在安全与可用之间权衡。对抗训练有覆盖盲区。对抗训练把已知攻击样本喂给模型让模型学会抵抗。但攻击者总能构造训练集之外的新扰动模型对新攻击的鲁棒性提升有限。更糟糕的是对抗训练会降低模型在干净数据上的精度这种精度换鲁棒的代价在车端要谨慎评估。我见过不少团队把对抗训练一加正常场景的识别率掉了三个点得不偿失。多模型投票成本高昂。车端 ECU 算力有限跑一个 CNN 已经吃紧再叠一个 ViT 几乎不现实。工业上常用轻量异构——一个主模型加一个轻量校验模型比如小 CNN 或基于颜色/形状的规则校验用低成本换部分鲁棒性。这种折中挡不住高强对抗但能挡住大多数低成本攻击。够用就行。还有个常被忽略的点对抗样本不是车端视觉的唯一威胁。遮挡、污损、天气、光照都会让识别失效这些非对抗失效的频率远高于对抗攻击。把太多精力放在对抗样本上反而会忽视更高频的真实风险。车端 AI 安全的资源分配应该按失效概率×危害程度排序对抗样本是其中一个维度不是全部。五、总结车端交通标志识别会被一张贴纸打穿这事不新鲜但很多人不当回事。三层防御预处理、多模型投票、置信度异常检测能挡住大部分低成本攻击但别指望它挡住所有。预处理会被自适应绕过对抗训练拖累精度多模型烧算力——每个方案都有代价。务实一点把对抗样本放进威胁模型但别让它吃掉全部安全预算。高频风险遮挡、天气、污损才是日常对抗样本是锦上添花的防御不是雪中送炭。