模型提取攻击这个话题,这两年在做AI系统安全评估的时候被问到的频率明显高了起来。早几年大家聊AI安全,注意力基本都放在对抗样本、数据投毒这些方向,模型提取(Model Extraction)往往被当成一个偏学术的概念,觉得离生产环境很远。但只要你的业务里存在“把模型能力通过API暴露出去”这种形态——不管是做模型服务、开放推理接口,还是把AI能力嵌进产品对外提供——模型提取就是一条实打实需要评估的风险路径。这篇内容我想把Lab06这个模型提取攻击的实验完整拆一遍,讲清楚它到底在做什么、攻击者是怎么一步步把黑盒模型“偷”出来的、防御侧又该怎么看这件事。适合做AI应用安全、模型服务运维、算法工程的同学参考,哪怕你之前没接触过攻击侧视角,跟着走一遍也能建立起基本认知。
1. 先搞清楚模型提取到底在偷什么
1.1 模型提取攻击的本质:偷的是决策边界,不是权重文件
很多人第一次听到“模型提取”,下意识以为攻击者是把模型权重文件下载走了。这其实是两码事。权重泄露属于存储和权限层面的问题,而模型提取攻击针对的是模型对外暴露的推理接口——攻击者手里没有你的模型文件,也没有你的训练数据,他只有一件事能做:不停地发查询、拿返回结果,然后根据这些“输入-输出”对,去训练一个功能上近似你的替代模型。
打个生活化的比方。你有一台不透明的自动售货机,投币口能塞进各种面额组合,出货口会告诉你“够不够、找不找零”。你看不见里面的机械结构,但你可以反复投币测试,慢慢摸清它的定价规则和找零逻辑,最后自己造一台规则几乎一样的机器。模型提取干的就是这件事:通过大量查询,把目标模型的决策边界(decision boundary)近似复现出来。
所以这里有个关键认知要先立住:模型提取的成功标准,不是“字节级还原”,而是“功能等价”。攻击者提取出来的替代模型,只要在目标分布上的预测行为和原模型足够接近,这次攻击就算成功了。至于替代模型的内部结构、参数排布,完全可以和原模型毫无关系。
1.2 为什么这件事值得认真对待
站在防御方角度,模型提取带来的损失是多层的。第一层是知识产权层面:你花大量算力、数据、人力训出来的模型,其核心价值在于那份决策能力,一旦被功能等价地复制走,商业壁垒就被削掉一大块。第二层是安全放大层面:替代模型一旦成型,攻击者就可以在本地对它做白盒分析——找对抗样本、探测漏洞、研究绕过策略,这些在本地做成本极低,做完再拿回真实接口上打,威胁等级完全不一样。第三层是查询成本转嫁:攻击者的海量查询消耗的是你的推理资源,等于用你的算力帮他完成窃取。
我见过一些团队觉得“我的模型接口有频率限制,应该没事”。频率限制确实能抬高攻击成本,但它不是决定性防线,后面会讲到攻击者怎么在限流条件下依然能推进。真正要评估的是:在给定查询预算和限流策略下,攻击者能提取到什么程度的替代模型。这就是Lab06要回答的问题。
1.3 本次实验的目标设定与边界
Lab06的设定很明确:目标模型是一个部署在服务端的分类模型,只暴露推理接口,不暴露任何内部信息。攻击者侧只允许通过接口查询,拿到的是软标签(soft label,即各类别的概率输出)或硬标签(hard label,即仅返回top-1类别)。实验要验证两件事:一是在软标签和硬标签两种反馈条件下,提取效果差多少;二是在有限查询预算内,替代模型的保真度(fidelity)能到什么水平。
这里先约定几个后面反复用到的术语,避免混淆:
- 目标模型(victim model):被攻击的原始模型,黑盒。
- 替代模型(substitute model):攻击者训练出来的仿制品。
- 查询集(query set):攻击者用来向目标模型发问的输入样本集合。
- 保真度(fidelity):替代模型与目标模型在同一批测试样本上预测一致的比例,这是衡量提取成败的核心指标。
- 查询预算(query budget):攻击者允许发出的查询总数,实验中通常设成几千到几万这个量级。
注意:整个实验只在自建的模拟环境和自有模型上进行,目的是评估风险、验证防御,不涉及任何真实第三方系统。
2. 攻击方案的整体设计与选型考量
2.1 为什么选“查询-蒸馏”这条路线
模型提取的实现路线大致有几类:基于方程求解的精确提取(针对特定简单模型结构)、基于决策边界的探测、以及基于替代模型训练的近似提取。前两类对模型结构有较强假设,泛化性差;第三类也就是“查询-蒸馏”路线,不依赖目标模型内部结构,适用面最广,也是这次实验选的主路线。
它的逻辑链条很直白:先构造或采集一批查询样本,喂给目标模型拿到带标签的反馈,把这些“输入-反馈”对当作训练数据,去训练一个结构可以自选的替代模型。本质上就是把目标模型当成一个“标注器”,用它的输出来监督替代模型的训练。这个思路和知识蒸馏很像,区别在于蒸馏时教师模型是你自己的、可以拿到logits,而这里教师是黑盒,只能拿到接口返回的概率或类别。
选这条路线的另一个原因是它对攻击者的门槛低。不需要知道目标模型是CNN还是Transformer,不需要逆向推理框架,只要接口能批量查询,剩下的就是标准训练流程。这也正是它危险的地方——门槛低意味着潜在攻击者基数大。
2.2 软标签与硬标签:反馈信息量决定上限
实验里一个核心变量是目标接口返回什么。软标签返回完整概率分布,硬标签只返回top-1类别。这两者对提取效果的影响非常大,原因在于信息量差异。
软标签里包含了类别之间的相对关系。比如一张图,目标模型给出“猫0.7、狗0.2、狐狸0.1”,这个分布本身就携带了“猫和狗比较像、和狐狸没那么像”这种结构信息,替代模型能从中学到更细的决策边界。硬标签只告诉你“是猫”,把那些相对关系全丢了,替代模型只能学到粗糙的分界。
用一个类比:软标签像是老师批改作业时不仅告诉你对错,还告诉你“这题你思路对但算错了,那题方向就偏了”;硬标签只打个对错号。学生(替代模型)从前者学到的东西显然更多。所以实验预期是:软标签条件下保真度明显更高,硬标签条件下需要更多查询才能逼近。这个预期在后面的实测里得到了验证。
2.3 查询集的构造策略:分布覆盖比数量更重要
查询样本从哪来,直接决定替代模型学到的是目标模型的哪一部分能力。常见做法有三种:随机噪声、从公开数据集采样、以及基于目标模型反馈做主动采样。实验里主要用第二种,配合第三种做增强。
为什么优先用公开数据采样而不是纯随机噪声?因为目标模型是在真实数据分布上训练的,它的决策边界在真实分布附近才有意义。你用纯噪声去查询,大量样本落在模型“不关心”的区域,反馈信息密度低,浪费查询预算。从和目标任务同分布的公开数据里采样,等于把查询火力集中在决策边界真正复杂的区域。
这里有个实操细节值得说:查询集不需要很大,但覆盖要均匀。实验里我先用了一个类别均衡的采样策略,每个类别采相同数量,避免替代模型在类别上产生偏置。如果目标任务的类别本身不均衡,还要考虑按目标模型的输出分布做重加权,否则替代模型会学出一个“偏向多数类”的版本,保真度虚高但实际不可用。
2.4 替代模型的结构选择:够用就好,别贪大
替代模型选多大,是个权衡。选得太小,容量不够,学不到目标模型的复杂度,保真度上不去;选得太大,训练成本高,而且在查询数据有限的情况下容易过拟合到查询集上,泛化到测试集反而差。
实验里我用了和目标模型同量级但结构不同的一个中等规模网络。这里刻意让结构不同,是为了验证“功能等价不要求结构相同”这个判断。实测下来,中等规模、配合足够查询量,保真度就能到一个不错的水平。如果查询预算很紧,反而应该选小一点的替代模型,让它学目标模型的主要决策模式,放弃那些细枝末节——因为预算不够时,细枝末节根本学不准,硬学只会引入噪声。
3. 核心环节的实操拆解
3.1 环境与目标模型的搭建
先把靶子立起来。目标模型用一个在图像分类任务上训练好的卷积网络,部署成一个只接受推理请求的服务。关键点是只暴露推理接口,不返回logits、不返回中间层、不暴露模型版本细节。为了让实验可控,我在服务端加了一个查询计数器,方便统计预算消耗。
# 目标模型服务的简化封装(模拟环境) class VictimService: def __init__(self, model, mode="soft"): self.model = model self.mode = mode # "soft" 返回概率, "hard" 只返回类别 self.query_count = 0 def query(self, batch_inputs): self.query_count += len(batch_inputs) with torch.no_grad(): logits = self.model(batch_inputs) probs = torch.softmax(logits, dim=1) if self.mode == "soft": return probs else: return probs.argmax(dim=1)这段封装看着简单,但有两个点要注意。第一,torch.no_grad()必须加,否则显存会随着查询累积爆掉,实验跑到一半崩掉很常见。第二,查询计数要按样本数而不是请求数计,因为攻击者往往批量查询,按请求数统计会严重低估预算消耗。
3.2 查询集的生成与预处理
查询集我分了两部分:一部分从公开数据里按类别均衡采样,作为基础查询池;另一部分用简单的数据增强(翻转、裁剪、轻微色彩扰动)扩充,提升覆盖。增强这一步不是必须的,但在查询预算有限时能提高单次查询的信息利用率。
预处理要和目标模型的输入规范严格对齐——归一化参数、输入尺寸、通道顺序,任何一项对不上,查询反馈就是错的,替代模型学出来的东西全是垃圾。我踩过一次坑:归一化均值用错了,导致查询反馈整体偏移,替代模型保真度死活上不去,排查了半天才发现是预处理不一致。这种低级错误在实际操作里特别容易发生,因为攻击者往往拿不到目标服务的预处理文档,只能靠试探推断。
3.3 软标签条件下的提取流程
软标签是信息最丰富的场景,流程也最直接。核心就是把目标模型返回的概率分布当作软目标(soft target),用KL散度或交叉熵去监督替代模型。
# 软标签提取的核心训练循环 def extract_soft(victim, substitute, query_loader, epochs=10, lr=1e-3): optimizer = torch.optim.Adam(substitute.parameters(), lr=lr) criterion = torch.nn.KLDivLoss(reduction="batchmean") for epoch in range(epochs): for inputs, _ in query_loader: soft_targets = victim.query(inputs) # 目标模型概率 log_probs = torch.log_softmax(substitute(inputs), dim=1) loss = criterion(log_probs, soft_targets) optimizer.zero_grad() loss.backward() optimizer.step()这里用KL散度而不是普通交叉熵,是因为软标签场景下我们要匹配的是整个分布形状,不只是最大类别。KL散度对分布差异更敏感,实测收敛后保真度比交叉熵高几个百分点。温度参数这里没引入,因为目标接口返回的就是原始概率,没有logits可调温——如果接口返回的是logits,那可以加温度做平滑,效果会更好,但多数生产接口不会暴露logits。
3.4 硬标签条件下的提取流程
硬标签场景麻烦一些,因为反馈只有类别,没有概率。直接用交叉熵训练也能跑,但信息利用率低。实验里我用了两种增强手段。
第一种是查询集扩充:既然每个样本只给一个类别,那就多查一些样本,用数量换信息。第二种是利用替代模型的置信度做自训练:先用硬标签训一个初版替代模型,然后让替代模型对未标注样本打软标签,挑高置信度的样本加入训练集,迭代几轮。这个思路本质上是半监督学习里的自训练(self-training),在硬标签提取里效果不错。
# 硬标签 + 自训练增强的简化流程 def extract_hard_with_selftrain(victim, substitute, query_loader, unlabeled_loader, rounds=3): # 第一轮:纯硬标签训练 train_hard(victim, substitute, query_loader) for r in range(rounds): # 用当前替代模型给无标注数据打伪标签 pseudo_inputs, pseudo_labels, confidences = [], [], [] substitute.eval() with torch.no_grad(): for inputs, _ in unlabeled_loader: probs = torch.softmax(substitute(inputs), dim=1) conf, pred = probs.max(dim=1) mask = conf > 0.9 # 只保留高置信样本 pseudo_inputs.append(inputs[mask]) pseudo_labels.append(pred[mask]) # 合并硬标签数据和伪标签数据继续训练 train_with_pseudo(victim, substitute, query_loader, pseudo_inputs, pseudo_labels)置信度阈值0.9是个经验值,设太高伪标签太少、起不到增强作用,设太低噪声大、会把替代模型带偏。这个阈值要根据替代模型的实际置信度分布调,不能照搬。
3.5 保真度的评估方法
评估不能只看训练损失,要看替代模型和目标模型在独立测试集上的预测一致率。测试集必须和查询集不重叠,否则保真度会虚高。评估时同时统计两个指标:整体一致率,以及每个类别的一致率。整体一致率可能被多数类拉高,分类别看才能发现替代模型在哪些类别上没学到。
def evaluate_fidelity(victim, substitute, test_loader): total, agree = 0, 0 per_class_total, per_class_agree = {}, {} substitute.eval() with torch.no_grad(): for inputs, _ in test_loader: v_pred = victim.query(inputs).argmax(dim=1) s_pred = substitute(inputs).argmax(dim=1) agree += (v_pred == s_pred).sum().item() total += len(inputs) for vp, sp in zip(v_pred.tolist(), s_pred.tolist()): per_class_total[vp] = per_class_total.get(vp, 0) + 1 if vp == sp: per_class_agree[vp] = per_class_agree.get(vp, 0) + 1 overall = agree / total per_class = {c: per_class_agree.get(c, 0) / n for c, n in per_class_total.items()} return overall, per_class4. 实测结果与关键发现
4.1 软标签与硬标签的保真度对比
在相同查询预算下,软标签的保真度明显领先。实验里查询预算设成两万次,软标签条件下整体保真度能到0.9以上,硬标签条件下大概在0.75到0.8之间,差距相当可观。这个结果符合前面的信息量分析:软标签每次查询携带的监督信号更丰富,同样的预算能榨出更多信息。
| 反馈类型 | 查询预算 | 整体保真度 | 训练轮次 | 备注 |
|---|---|---|---|---|
| 软标签 | 20000 | 0.91 | 10 | 收敛稳定 |
| 硬标签 | 20000 | 0.78 | 10 | 需自训练增强 |
| 硬标签+自训练 | 20000 | 0.83 | 3轮增强 | 提升约5个点 |
| 软标签 | 5000 | 0.82 | 10 | 预算受限仍可用 |
从表里能看出两个趋势。一是软标签在低预算下依然能拿到不错的保真度,说明它的信息效率高;二是硬标签靠自训练能补回一部分差距,但补不满,因为自训练用的伪标签本身来自替代模型,存在误差累积的风险。
4.2 查询预算与保真度的关系曲线
预算和保真度不是线性关系,而是有明显的边际递减。实验里从五千次加到两万次,软标签保真度从0.82涨到0.91,涨了9个点;但从两万加到五万,只涨了不到3个点。这说明存在一个“够用”的预算区间,超过之后继续加查询,收益很小。
这个规律对防御方有直接意义:如果你的接口限流能把攻击者的有效查询压到某个阈值以下,就能把保真度压在一个可接受的水平。但要注意,攻击者可以通过多账号、分布式查询来绕过单账号限流,所以限流策略要结合账号维度和全局维度一起看。
4.3 哪些类别最容易被“偷准”,哪些最难
分类别看保真度,发现一个有意思的现象:样本量大、特征明显的类别,保真度高;样本少、类别间边界模糊的类别,保真度低。比如某些外观差异极大的类别,替代模型几乎不会错;而那些本身长得像、目标模型自己都容易混淆的类别,替代模型错得更多。
这背后的逻辑是:替代模型学的是目标模型的决策边界,边界清晰的地方好学,边界模糊的地方,目标模型自己的输出就不稳定,替代模型从这些不稳定反馈里学不到确定的东西。这也提示防御方,如果你的模型在某些类别上本身就不够鲁棒,那这些类别在模型提取攻击下会更脆弱。
5. 防御视角:怎么让提取变难
5.1 输出信息的裁剪:少给一点,攻击者就多花很多
最直接的防御是减少接口返回的信息量。把软标签降级成硬标签,或者只返回top-k类别、对概率做粗粒度量化(比如只返回“高/中/低”三档),都能显著抬高提取成本。实验数据已经说明,硬标签比软标签难提取得多。代价是可能影响正常业务对概率信息的使用,所以这是个需要权衡的产品决策,不是纯技术问题。
5.2 查询行为的异常检测
模型提取攻击在查询行为上有一些可识别的特征:查询量异常大、查询样本分布和正常用户差异明显、查询模式机械(比如批量、等间隔)、查询的输入多样性异常高。针对这些特征做检测,能在攻击早期就发现苗头。
实操上,可以统计每个账号的查询量分布、查询样本的特征分布,用简单的统计阈值或轻量异常检测模型就能筛出可疑账号。这里要注意别误伤正常的高频用户,阈值要结合业务实际调。我见过一些团队一上来就把阈值设得很严,结果正常业务被限流,投诉一堆,最后只能放宽,反而失去了防护意义。
5.3 水印与指纹:让偷走的东西能被追溯
在模型里嵌入水印(watermark)是另一条思路。具体做法是在训练时让模型对特定触发输入产生特定输出,这些触发模式正常使用中几乎不会出现,但一旦替代模型学到了这些行为,就能通过检测触发模式来判断它是不是从你的模型提取来的。这属于事后追溯手段,不能阻止提取,但能在发现侵权时提供证据。
水印设计的关键是触发模式要隐蔽且鲁棒。太明显的触发容易被攻击者在提取时过滤掉;太脆弱的触发在替代模型训练过程中会丢失。这块工程实现有一定门槛,实验里我只做了个简单验证,确认了基本可行性,生产级的水印方案需要更细致的设计。
5.4 限流策略的组合拳
单靠一种限流手段不够,要组合。账号维度限流防单点高频,IP维度限流防多账号轮换,全局维度限流防分布式查询。再叠加查询内容的多样性检测,对“输入分布异常分散”的查询做额外审查。这套组合拳的核心思路是:让攻击者在任何一个维度上都无法低成本地堆查询量。
不过要清醒认识到,限流只能抬高成本,不能根除风险。只要接口对外开放,理论上就存在被提取的可能。防御的目标是把攻击成本抬到高于模型本身价值,让攻击在经济上不划算,而不是追求绝对安全。
6. 实操中踩过的坑与排查经验
6.1 保真度上不去,先查预处理对齐
这是最高频的坑。替代模型训练半天保真度卡在0.6上不去,八成是预处理和目标服务不一致。归一化参数、输入尺寸、通道顺序、甚至图像解码方式(JPEG解码的细微差异)都可能造成偏差。排查方法很简单:拿一批样本,分别走目标服务和本地预处理,对比两边输入张量的统计量(均值、方差、范围),对不上就是这里的问题。
6.2 查询反馈的批量顺序错乱
批量查询时,如果服务端返回结果的顺序和请求顺序不一致(有些异步接口会这样),而你按顺序配对,那训练数据就全错位了。这个坑很隐蔽,因为损失曲线看起来正常,但保真度就是上不去。解决办法是给每个请求带唯一标识,返回结果按标识对齐,别依赖顺序。
6.3 替代模型过拟合查询集
查询集不够多样时,替代模型会把查询集里的噪声也学进去,表现为训练损失很低但测试保真度不高。判断方法是看训练集保真度和测试集保真度的差距,差距大就是过拟合。缓解手段是增加查询集多样性、加正则化、或者早停。我一般会留一部分查询样本不参与训练,专门用来监控过拟合。
6.4 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 保真度长期低位徘徊 | 预处理不一致 | 对比输入张量统计量 |
| 损失正常但保真度低 | 查询反馈错位 | 检查批量返回顺序对齐 |
| 训练损失低、测试保真度低 | 过拟合查询集 | 增加多样性、加正则、早停 |
| 硬标签提取效果差 | 信息量不足 | 加自训练、扩查询集 |
| 显存随查询增长爆掉 | 未禁用梯度 | 查询时加no_grad |
| 某些类别保真度特别低 | 类别边界模糊 | 分类别看,针对性补查询 |
6.5 一个容易被忽略的细节:查询预算的统计口径
前面提过,预算要按样本数统计。但还有个更细的点:如果目标接口对单次请求的批量大小有限制,攻击者会拆成多次请求,这时候按请求数统计会低估实际查询量。做防御评估时,一定要按“实际处理的样本数”来算预算,否则会高估自己的防护效果。这个细节在写评估报告时特别重要,数字口径错了,结论就全错了。
7. 这套实验方法还能怎么用
模型提取实验的价值不只在“验证能不能偷”,更在于它提供了一套量化评估模型服务暴露风险的方法。你可以把这套流程当成一个内部的红队工具:定期对自己的模型服务跑一遍提取实验,看在不同限流策略下保真度能到多少,据此调整防护强度。这比拍脑袋定限流阈值靠谱得多。
另外,这套方法稍作调整也能用于评估其他类型的模型服务风险,比如成员推断、属性推断这些,思路是相通的——都是通过接口查询反推模型内部信息。我在实际项目里会把这几类评估打包成一个“模型服务安全体检”流程,每次模型上线前跑一遍,形成基线,后续有改动再对比基线看风险变化。
最后分享一个我在实操里总结的小经验:做这类攻击评估,先固定一个简单的基线配置跑通全流程,再逐步加复杂度。很多人一上来就想搞最复杂的主动学习采样策略,结果流程里某个环节没对齐,排查起来牵一发动全身。先用最简单的随机采样+软标签跑出一个能复现的保真度数字,确认整条链路没问题,再去优化采样和训练策略,效率高得多。这个顺序上的取舍,比任何单个技巧都更能决定你实验做不做得成。