在实际深度学习系统中,模型审计往往比模型训练更难。训练时只需要关注损失曲线和评估指标,而审计时需要回答一个更尖锐的问题:模型做出这个预测,到底依赖了哪些信息?如果模型把背景、水印、阴影或某个与任务无关的特征当成主要依据,准确率再高也不具备上线条件。ICON Decomposition 这类概念级解释方法,正是为了对深层网络的内部表示做“多变量概念级分解”,把黑盒表征拆成一组可理解的语义概念,再基于这些概念完成模型审计。本文会围绕这个概念展开,先讲清它解决的问题,再用一个最小可复现的 Python 示例展示实现思路,最后说明如何在真实项目里落地。
1. 理解 ICON Decomposition:从模型审计视角看深度表示
1.1 深度表示为什么不透明
深度神经网络内部不是一个清晰的规则库。以图像分类为例,网络在前几层可能学习到边缘、颜色、纹理等低级特征,在中间层会组合出更复杂的模式,比如“毛皮”“轮子”“圆形物体”,最后映射到类别概率。但问题是,这些模式不是以人类可读的方式存储的,而是分散在成千上万个中间特征图或向量维度里。
一个常见误解是:神经网络最后的全连接层权重可以解释模型决策。实际上,最后一层只是把倒数第二层的向量映射到类别,真正决定“这个概念是否出现”的信息,早就在中间层被编码了。如果只审计输出层的权重,无法发现模型是否偷偷使用了某些无关但容易区分的信号,例如照片背景里常见的水印、边框、拍摄设备噪声。
模型审计需要深入到这些表示内部。ICON Decomposition 的出发点就在这里:它不直接看单个神经元,而是把表示空间看成多个“概念方向”的组合,尝试用一个分解模型把这些方向分离出来。
1.2 概念级解释与显著性图的区别
早期可解释性大量使用显著性图,例如 Grad-CAM、SmoothGrad,它们回答的是“图像哪个区域对预测最重要”。这对定位问题很有用,但有一个明显短板:它只告诉你在哪里,不告诉你那个区域触发了什么概念。一张猫的图片中,模型可能关注了猫的脸,也可能关注了背景里的地毯。同样是高激活区域,这两者代表的决策逻辑完全不同。
概念级解释进一步回答“为什么这个区域重要”。它会把内部表示解释为若干个概念,例如“猫耳朵”“毛皮”“地毯纹理”“照片水印”,然后说明某张图片的预测主要被哪些概念驱动。这样审计者就能判断:模型是否依赖了不该依赖的概念。
ICON Decomposition 强调“多变量”概念,指的是一个概念不一定对应某个单独的神经元,而可能是多个维度的线性组合或非线性组合。这在真实网络里更常见:一个语义概念通常被分布式地编码在多个特征维度上,用单神经元解释会错失大量信息。
1.3 多变量概念分解的核心思路
可以把深度表示看成一张表格。假设从中间层抽取出 (N) 个样本的表示,每个表示有 (D) 个维度,组成矩阵 (R \in \mathbb{R}^{N \times D})。如果存在 (K) 个概念,我们希望找到:
- 概念定义矩阵 (H \in \mathbb{R}^{K \times D}),每一行是一个概念,表示这个概念在原始特征维度上的权重;
- 样本概念激活矩阵 (W \in \mathbb{R}^{N \times K}),每一行表示这个样本以多大强度包含每个概念。
这样就有近似关系:
[ R \approx W H ]
这就是常见的矩阵分解形式。ICON Decomposition 的核心不是简单的逼近,而是要求分解出的概念具有语义可解释性,并且能支撑后续审计。为了实现这一点,通常还要对 (H) 或 (W) 加入稀疏性、非负性、正交性等约束,让每个概念尽量由少量特征构成,同时避免多个概念重复表达同样的信息。
1.4 模型审计的应用场景
概念级分解最常用的四个审计场景如下:
| 审计目标 | 典型问题 | 概念级分解如何帮助 |
|---|---|---|
| 偏见识别 | 模型是否依赖性别、肤色、年龄等敏感属性 | 分解出与任务无关的敏感概念,并统计其对预测的影响 |
| 捷径学习 | 模型是否依赖水印、边框、背景色等低成本特征 | 发现训练数据中高频出现的干扰概念 |
| 数据泄漏 | 测试集和训练集是否存在隐藏标识 | 在表示中分离出与任务无关但能区分数据集来源的概念 |
| 失效模式分析 | 某些样本类别准确率低,模型在想什么 | 对比错误样本与正确样本的概念激活差异 |
这些场景的共同点是:不能只看准确率,必须进入模型内部看证据。概念级分解提供了这套证据链。
2. 理解多变量概念分解的数学基础
2.1 概念是方向,不是神经元
在深度网络中,“概念”可以用表征空间中的一个方向来定义。例如,假设某层特征有 512 个维度,一个“条纹”概念可能不是第 37 个神经元,而是第 12、45、200 个维度的一组组合权重。这个权重向量在输入空间中没有直观像素意义,但在表示空间中代表一个语义方向。
从几何角度看,给定一个概念向量 (v \in \mathbb{R}^D),样本表示 (r_i) 对概念 (v) 的激活分数可以定义为:
[ s_i = \frac{r_i \cdot v}{|r_i| |v|} ]
这个分数可以理解为余弦相似度。如果多个样本在某个概念方向上有持续的高激活,就能说明这些样本共享某个语义模式。
2.2 从单概念到多变量组合
单概念方法一次只找一个概念向量,例如 Concept Activation Vector(CAV)会用正负样本在表示空间训练一个线性分类器,把分类器权重当作概念方向。这个方法有效,但存在一个问题:多个概念可能同时出现在同一个表示里,单独找每个方向时容易互相干扰。
ICON Decomposition 采用多变量分解的思路,把表示矩阵一次分解成多个概念。这样做的好处是:
- 概念之间可以互相竞争,避免重复表达;
- 一个样本可以同时由多个概念解释,更贴近真实组合逻辑;
- 审计者能看到“概念组合”而不是“单一最强方向”。
2.3 稀疏性与非负性为什么重要
常见的矩阵分解包括 PCA、SVD、NMF 和稀疏编码。它们都能把矩阵拆成若干因子,但可解释性差别很大。
PCA/SVD 得到的因子可能有正有负,某些维度互相抵消,很难对应到“某个概念出现了”或“某个概念没出现”。非负矩阵分解要求 (W) 和 (H) 的所有元素不小于 0,因此概念激活不能是负的。这在语义解释上更自然:一个概念要么不出现,要么以一定强度出现,不会出现“负的条纹”这种说法。
稀疏性要求概念向量 (H) 中很多维度为零或接近零,这样每个概念只依赖少量原始特征,更容易归纳出语义。否则,一个概念向量在所有维度上都有权重,无法说清它到底代表什么。
注意:非负性不是所有特征都满足的前提。深度网络中间层特征常常包含负数,因此在做 NMF 前需要先做平移、ReLU 或归一化处理。
2.4 审计指标:概念贡献、覆盖度与稳定性
分解完成后,只有“概念”还不够,还需要量化指标来支撑审计结论。通常可以计算三类指标:
第一是概念贡献。对某样本 (i) 和概念 (k),可以用激活值 (W_{i,k}) 作为该概念对样本表示的贡献。如果要进一步评估对预测的贡献,可以移除该概念后观察概率变化。
第二是覆盖度。对某个类别,统计该类别的样本中哪些概念持续高激活。若一个概念只在极少数样本上激活,即使激活很强,也不是类别的主要解释。
第三是稳定性。同一个模型、同一批数据,如果每次重新分解得到的概念完全不同,审计结论就不可信。通常需要用多次初始化、交叉验证或其他聚类一致性指标来评估。
| 指标 | 计算方式 | 审计用途 |
|---|---|---|
| 概念贡献 | 概念激活值 / 样本表示范数 | 解释单样本预测 |
| 类别覆盖度 | 类别内高激活样本比例 | 判断模型是否依赖某个共享概念 |
| 稳定性 | 多次分解后概念方向的平均相似度 | 判断分解结论是否可信 |
| 重构误差 | (|R - WH|_F) | 判断概念数量是否足够 |
3. 环境准备:用 Python 搭一个概念分解实验
3.1 安装依赖
下面示例以 Python 为核心,使用 PyTorch 加载预训练模型,使用 scikit-learn 做矩阵分解。建议新开一个虚拟环境:
python -m venv icon-audit source icon-audit/bin/activate然后安装依赖:
pip install numpy pandas scikit-learn matplotlib torch torchvision opencv-python如果只需要跑通分解部分,不加载深度模型,也可以只安装:
pip install numpy scikit-learn matplotlib安装完成后,建议检查版本,避免出现兼容性差异:
python -c "import sklearn; print(sklearn.__version__)" python -c "import torch; print(torch.__version__)"3.2 准备深度表示数据集
为了演示完整链路过深,可以先使用 CIFAR-10 和 PyTorch 自带的 ResNet-18 预训练权重。这里要说明:下面的代码只是提取表示的通用模板,实际项目需要替换成自己的模型和数据集。
约定一个“表示提取模块”:从 ResNet-18 的 layer3 输出特征图,经过全局平均池化后得到一个 256 维向量。代码实现如下:
import torch import torch.nn as nn import torchvision import torchvision.transforms as transforms class FeatureExtractor(nn.Module): def __init__(self, backbone=None, layer_name='layer3'): super().__init__() if backbone is None: backbone = torchvision.models.resnet18(weights='IMAGENET1K_V1') # 截取到指定层为止 self.features = nn.Sequential( backbone.conv1, backbone.bn1, backbone.relu, backbone.maxpool, backbone.layer1, backbone.layer2, backbone.layer3, ) self.pool = nn.AdaptiveAvgPool2d((1, 1)) def forward(self, x): x = self.features(x) x = self.pool(x) return x.flatten(1)这里的features直接截取了 ResNet-18 的前三个残差阶段。如果把layer_name参数改为不同层,可以对比不同抽象级别的审计结果。
3.3 构造表示矩阵
为了演示,不一定要跑完整数据集。可以从 CIFAR-10 测试集中均匀抽取 2000 张图,每类 200 张,前向得到表示矩阵。
transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) dataset = torchvision.datasets.CIFAR10( root='./data', train=False, download=True, transform=transform ) # 每类抽 200 个样本 sampled_indices = [] for cls in range(10): cls_indices = [i for i, label in enumerate(dataset.targets) if label == cls][:200] sampled_indices.extend(cls_indices) subset = torch.utils.data.Subset(dataset, sampled_indices) loader = torch.utils.data.DataLoader(subset, batch_size=64, shuffle=False) extractor = FeatureExtractor().eval() features, labels = [], [] with torch.no_grad(): for x, y in loader: out = extractor(x) features.append(out) labels.append(y) R = torch.cat(features, dim=0).numpy() # shape (2000, 256) y = torch.cat(labels, dim=0).numpy() # shape (2000,)这一步是审计的“原材料准备”。后续所有分解都基于矩阵R,因此R的质量会影响审计结论。
3.4 环境检查清单
| 检查项 | 预期结果 | 说明 |
|---|---|---|
| Python 可用 | 版本 3.8 以上 | 兼容 PyTorch 和 scikit-learn |
| PyTorch 可加载预训练模型 | 能下载权重 | 如果网络受限,权重加载会失败 |
| 表示矩阵维度 | 2000 x 256 | 说明特征提取正常 |
| 样本标签分布 | 每类 200 个 | 审计结论必须覆盖所有类别 |
4. 核心实现:实现一个 ICON 风格的概念级分解
4.1 特征预处理
从神经网络中间层拿到的特征向量可能有负值,直接做 NMF 会报错或产生不合理结果。这里先做两个处理:
- 用
min-max把每个特征维度缩放到[0, 1]; - 或者使用
ReLU,将负值截断为 0。
第一种方式保留更多信息,第二种方式更接近“特征是否出现”的语义。这里以 min-max 为例:
from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler() R_scaled = scaler.fit_transform(R)注意:MinMaxScaler是在所有审计样本上拟合的。如果后续要审计新数据,新数据必须使用同一个 scaler 转换,不能重新拟合,否则概念定义会漂移。
4.2 用 NMF 做多变量概念分解
scikit-learn的NMF是现成可用的基线方法。它不仅支持非负约束,还允许调整稀疏性。
from sklearn.decomposition import NMF n_concepts = 16 model = NMF( n_components=n_concepts, init='nndsvda', beta_loss='frobenius', solver='cd', max_iter=500, random_state=42 ) W = model.fit_transform(R_scaled) # (2000, 16) H = model.components_ # (16, 256) reconstruction_error = model.reconstruction_err_参数解释:
| 参数 | 含义 | 调整建议 |
|---|---|---|
| n_components | 概念数量 | 最关键的参数,过少会欠拟合,过多会重复 |
| init | 初始矩阵方式 | nndsvda适合稀疏数据,random适合快速验证 |
| beta_loss | 损失函数类型 | frobenius是平方误差,kullback-leibler对计数型特征更合适 |
| solver | 优化方式 | cd适合中大型矩阵,mu更稳定但慢 |
| max_iter | 最大迭代次数 | 如果重构误差仍未收敛,需要调大 |
| random_state | 随机种子 | 审计必须固定,否则结果无法复现 |
4.3 增加稀疏约束让概念更易解释
直接用 NMF 得到的概念可能仍然稠密:每个概念在 256 个特征维度上都有明显权重,难以归纳语义。可以打开 L1 稀疏约束。
model_sparse = NMF( n_components=n_concepts, init='nndsvda', beta_loss='frobenius', solver='cd', max_iter=1000, alpha_W=0.1, alpha_H=0.1, l1_ratio=0.9, random_state=42 ) W_sparse = model_sparse.fit_transform(R_scaled) H_sparse = model_sparse.components_这里alpha_W和alpha_H控制 W 和 H 的 L1 正则强度,l1_ratio=0.9表示 90% 使用 L1 惩罚。增大alpha_H会让概念定义更稀疏,但也不能过大,否则重构误差显著上升。
4.4 把概念映射回输入空间
得到概念向量 (H_k) 后,还需要回答“这个概念的语义是什么”。可以把概念向量当作一组特征权重,对原始特征图做加权求和,得到概念激活图。
假设feature_map的形状是(C, H, W),概念向量concept_vec的形状是(C,),则概念激活图如下:
def concept_activation_map(feature_map, concept_vec): # feature_map: (C, H, W) # concept_vec: (C,) cam = torch.tensor(concept_vec) @ feature_map.view(feature_map.shape[0], -1) cam = cam.view(feature_map.shape[1], feature_map.shape[2]) cam = cam - cam.min() cam = cam / (cam.max() + 1e-8) return cam这个操作类似 Grad-CAM,但不需要梯度,也不需要类别预测。它反映的是“某个概念在空间上出现在哪里”。
实际使用前需要对feature_map做上采样到输入图像尺寸,可配合 OpenCV 的resize完成。
4.5 面向审计的概念报表
分解只是中间步骤,审计最终需要给出一份报表。以下函数统计每个概念在每个类别上的平均激活:
import pandas as pd df = pd.DataFrame(W_sparse, columns=[f'concept_{i}' for i in range(n_concepts)]) df['label'] = y report = df.groupby('label').mean().T print(report.round(3))输出是一张概念-类别交叉表,审计者可以通过这张表快速发现异常关联。比如某个概念在类别 5 上平均激活为 0.8,但在其他类别上接近 0,说明该概念几乎就是这个类别的“专属特征”。如果这个类别是人类标签,该概念可能合理;如果这是一个与任务无关的属性,就需要进一步探查。
5. 运行验证:从“能跑”到“审计结论可靠”
5.1 用合成数据验证分解流程
真实数据无法预知“正确”概念,因此先把流程跑在一份人工合成数据上,验证分解逻辑是否正确。
假设有 300 个样本,每个样本由 10 个特征构成,其中只有 3 个特征是有效概念,其余全是噪声:
rng = np.random.default_rng(0) n, d, k = 300, 50, 3 true_W = rng.uniform(0, 1, size=(n, k)) true_H = np.zeros((k, d)) # 概念1只用特征0-4 true_H[0, 0:5] = 1.0 # 概念2只用特征10-14 true_H[1, 10:15] = 1.0 # 概念3只用特征30-34 true_H[2, 30:35] = 1.0 R_synthetic = true_W @ true_H + 0.01 * rng.normal(size=(n, d))使用与上一节相同的 NMF 流程,但将n_concepts设为 4,观察能否恢复出三个有效概念和第四个噪声概念。这才是“验证方法有效”的第一步。
5.2 评估概念稳定性
审计不是跑一次就能下结论的。概念分解受随机初始化和样本抽样的影响很大。可以在多个随机种子下重复分解,然后计算两次概念矩阵的相似度。
一种简化方法是:固定样本,改变random_state,对每个新概念,在旧概念中找到最大余弦相似度,取平均作为稳定性分数。
from sklearn.metrics.pairwise import cosine_similarity def concept_stability(H1, H2): sim = cosine_similarity(H1, H2) return np.mean(np.max(sim, axis=1))如果多次运行后的平均稳定性低于 0.5,说明概念不稳定,要么增加样本量,要么增大稀疏约束,要么固定随机种子并明确说明审计版本。
5.3 审计发现示例:模型依赖背景
用一个典型场景说明审计输出如何帮助发现模型风险。
在 CIFAR-10 上,有两个类别:猫和狗。如果训练数据里猫的图片大量出现在沙发上,狗的图片大量出现在草地上,模型可能学习到“沙发”概念和“草地”概念,而不仅仅是猫和狗本身。
在概念分解报告中,如果看到某个概念在猫类别上的激活显著高于狗,并且这个概念的概念激活图主要落在背景区域,而不是动物主体区域,那么基本可以怀疑模型存在背景依赖。
验证方式可以是:构造一个“概念干预”实验。把测试图片中背景区域替换成更常见的公共背景,观察预测概率变化。变化越大,说明模型对该背景概念的依赖越强。
5.4 验证审计结论的可信边界
概念级分解是一种探索性工具,不是最终裁决。审计结论必须写明三个边界:
- 样本边界:只审计了哪批数据,是否覆盖真实数据分布;
- 模型边界:抽取了哪一层特征,不同层得到的概念可能不同;
- 分解边界:概念数量 K 的选取会影响结论,不能只看一组 K。
注意:概念解释不等于因果解释。某个概念与类别预测高度相关,不能直接认定模型“使用”了这个概念;只能说在表示空间中,这个概念对预测路径有较强的相关性。
6. 常见问题与排错路径
6.1 概念无法归纳成语义
现象:分解出的 16 个概念,每个都看不出明确含义,所有样本激活都接近 0.5。
可能原因:
- 特征没有归一化,尺度差异导致分解结果被少数维度主导;
- 概念数量过多或过少;
- 特征本身抽象程度过高,无法用简单直方图归纳。
处理方式:先减少概念数量(例如改为 8 个);再检查预处理是否生效;然后对每个概念画出高激活样本,人工查看共同点。如果依然无法归纳,可以考虑换更靠近输出的层,或改用稀疏约束更强的参数。
6.2 多次运行结果差别大
现象:概念稳定性分数低于 0.5。
可能原因:
- 样本量太小;
- NMF 随机初始化没有收敛;
- 特征维度存在高度相关性,导致多个局部最优解。
处理方式:
- 固定
random_state,把审计结果视为某一版本; - 提高
max_iter,检查reconstruction_err_是否下降; - 增加特征规范化与稀疏约束;
- 如果特征维度极高,先做 PCA 降维会损失一部分语义,需要权衡。
6.3 分解结果与审计问题脱节
现象:概念都很有语义,但没有一个概念对应用户关心的敏感属性或干扰特征。
可能原因:
- 审计样本中没有覆盖那些属性;
- 概念数量不足,目标概念被压进其他概念;
- 中间层选择不合适,目标属性只出现在更浅或更深的层。
处理方式:先做一次快速单概念检验,用 CAV 方式验证目标属性是否存在可分离的概念方向。如果 CAV 能分离,说明表示中有相关信息,再把概念数量调大或用其他分解算法继续挖。
6.4 排错清单
| 问题现象 | 检查方式 | 处理建议 |
|---|---|---|
| NMF 报“Negative values” | 检查 R 矩阵最小值 | 对特征做 min-max,或加非负平移 |
| 重构误差很大 | 打印reconstruction_err_ | 增大概念数量或增大迭代次数 |
| 概念大量重复 | 计算概念之间余弦相似度 | 增大稀疏约束或降低概念数量 |
| 审计报告无法解释 | 对齐高激活样本的原始图片 | 人工抽查,必要时用聚类摘要 |
| 结果不可复现 | 查看random_state是否设置 | 固定种子并记录所有超参 |
7. 最佳实践与扩展方向
7.1 学习环境与生产环境的差异
学习环境里,跑通概念分解可以很快,只要数据能加载、NMF 能收敛就行。生产环境做模型审计时,还需要额外的工程化要求:
| 维度 | 学习环境 | 生产环境审计 |
|---|---|---|
| 数据 | 任意抽样 | 必须覆盖真实分布和边缘场景 |
| 特征层 | 随意挑选 | 记录模型版本、层名、特征统计 |
| 分解参数 | 默认或少量调整 | 多次运行并比较稳定性 |
| 报告 | 打印表格 | 输出可追溯 JSON 或 PDF 报告 |
| 监控 | 无 | 记录模型变更前后的概念漂移 |
| 审批 | 无 | 审计结论需要复核人确认 |
7.2 概念级审计的落地流程
一个可复用的概念级审计流程可以按下面五步执行。
第一步,定义审计目标。先写清楚怀疑点:是否担心背景依赖、敏感属性、数据泄漏,还是类别失效模式。
第二步,确定表示层。从模型的多个中间层分别抽取特征,不要只选一层,因为不同语义可能在不同抽象级别出现。
第三步,进行多变量概念分解。使用固定随机种子,记录概念数量、稀疏参数和重构误差。
第四步,人工解释概念。抽取每个概念的高激活样本,生成概念激活图,由领域专家给概念打标签。
第五步,输出审计报告。包括概念定义、覆盖率、稳定性、风险判断、建议动作。
7.3 从 NMF 走向更完整的 ICON 方案
NMF 只是一个便于复现的基线。真实以 ICON Decomposition 为标题的方法,可能还会包含更多设计,例如:
- 用变分自编码器学习非线性概念表示;
- 通过稀疏自编码器把概念激活限制在稀疏码本上;
- 在概念层与预测层之间加入因果干预,估计概念对预测的因果效应;
- 对多个概念做交互效应分析,识别“概念组合”导致的错误。
实际项目中,可以根据数据规模选择:
| 方法 | 适用场景 | 代价 |
|---|---|---|
| NMF | 中型特征矩阵,快速基线 | 线性分解,表达能力有限 |
| 稀疏自编码器 | 大规模表示,能学到非线性概念 | 训练复杂,需要调参 |
| CAV / TCAV | 验证单一概念假设 | 需要正负样本标注 |
| ICON 风格分解 | 多概念同时出现,需要审计报告 | 实现成本高,需领域专家参与 |
7.4 审计报告应包含哪些内容
无论使用哪种方法,最终审计报告都应包含以下内容:
- 模型与数据版本:包括预训练权重、特征层名称、数据集切分;
- 分解配置:概念数量、随机种子、归一化方式、稀疏参数;
- 概念清单:每个概念的权重 Top 特征、高激活样本、人工命名;
- 审计发现:哪些概念与目标无关但激活显著,哪些类别存在风险;
- 证据图表:概念激活图、概念-类别热力图、稳定性评分;
- 建议动作:是否需要重新训练、收集数据或加入约束。
实际操作中,“审计”两个字容易让人直接联想到上线前检查。更合理的做法是把它放进模型迭代流程,每次数据更新或模型微调后都重新跑一次概念级审计,观察概念是否漂移、是否有新的捷径特征出现。这样,ICON Decomposition 的意义就不只是调试模型,更是让模型交付从“准确率高”升级为“理由可信”。对任何需要解释模型行为的团队来说,这都是值得长期投入的方向。