☰
影响函数实战:从公式推导到工程实现,定位模型异常样本
2026/10/17 3:56:32 网站建设 项目流程

1. 为什么还要啃这篇“老论文”

1.1 从一次调参事故说起

前阵子帮一个做风控模型的朋友排查问题,他训练了一个梯度提升树模型,离线AUC做到0.92,上线后却频繁出现某类样本评分异常。团队一开始怀疑是特征穿越,查了两天没结果。后来我建议他别只看全局指标,挑几条异常样本,反推“到底是训练集里哪些样本把这条预测带偏了”。他试了一圈现成工具,发现大部分只能给特征重要性,给不了“样本级”的归因。最后我们翻出了这篇2017年的老论文——Understanding Black-box Predictions via Influence Functions,用它的思路做了个简化版实现,半天就定位到是某批标注质量很差的样本在捣乱。

这件事让我意识到,影响函数(Influence Function)这个工具虽然年头不短,但在实际排查里依然好用。它解决的问题很具体:给定一个已经训练好的模型,我想知道“如果我把训练集里的某一条样本删掉,或者把它的标签改一下,模型的预测会怎么变”。这个问题听起来简单,但真要做,最笨的办法是“删一条、重训一次”,训练集有十万条就得重训十万次,成本高到离谱。影响函数的价值就在于,它用一次训练 + 一次求逆,近似估算出“删掉某条样本后模型参数会怎么变”,把重训成本压到几乎为零。

这篇内容适合谁看?如果你做过模型调试、数据清洗、标注质量审核,或者单纯对“黑盒模型到底怎么解释”感兴趣,那这套方法值得花时间搞懂。我会把论文里的公式一步步推给你看,不跳步,同时补上我实际复现时踩过的坑和工程上的取舍。公式推导部分我会尽量用“人话”解释每一步在干嘛,而不是甩一堆符号就完事。

1.2 影响函数到底在算什么

先把核心问题摆出来。假设我们有一个训练集,里面有 n 条样本,记作 z₁, z₂, …, zₙ。模型参数记作 θ,训练的目标是最小化经验风险:

R(θ) = (1/n) Σᵢ L(zᵢ, θ)

其中 L 是单条样本的损失函数。训练完成后得到最优参数 θ̂,满足 ∇R(θ̂) = 0。

现在我想知道:如果我把第 i 条样本的权重稍微调一下,或者干脆把它删掉,最优参数 θ̂ 会怎么变?论文里用了一个很巧妙的技巧——给每条样本加一个权重 ε,把经验风险改写成:

R(θ, ε) = (1/n) Σᵢ εᵢ L(zᵢ, θ)

当所有 εᵢ = 1 时,就是原始训练目标。如果我想“删掉第 i 条样本”,等价于把 εᵢ 从 1 降到 0。那么“删掉这条样本后参数的变化量”就可以写成:

dθ̂ / dεᵢ

这个导数就是影响函数的核心。论文证明了它有一个闭式近似解:

I(zᵢ) = -H⁻¹ ∇L(zᵢ, θ̂)

其中 H 是训练损失在 θ̂ 处的 Hessian 矩阵(二阶导矩阵),∇L(zᵢ, θ̂) 是第 i 条样本的损失梯度。这个公式的直觉是:H⁻¹ 描述了损失曲面的“形状”,梯度描述了这条样本“往哪个方向推参数”,两者一乘就得到这条样本对参数的净影响。

我第一次看到这个公式时觉得挺反直觉——为什么不用重训就能估出来?后来想明白了:这本质上是一阶泰勒展开。损失曲面在最优解附近近似是个二次碗,Hessian 就是这个碗的曲率。样本的梯度是它施加的“力”,曲率决定了“同样的力能把参数推多远”。所以只要碗的形状(H)和力(∇L)知道,位移就能算出来。

1.3 从参数影响到预测影响

光知道参数怎么变还不够,我们真正关心的是“预测怎么变”。假设有一条测试样本 z_test,模型对它的预测是 f(z_test, θ̂)。那么删掉训练样本 zᵢ 后,测试预测的变化量近似为:

I(zᵢ, z_test) = -∇f(z_test, θ̂)ᵀ H⁻¹ ∇L(zᵢ, θ̂)

这个式子就是论文里最常被引用的“影响函数”。它把“删掉某条训练样本对某条测试预测的影响”拆成了三部分:

  • ∇f(z_test, θ̂):测试样本对参数的敏感度,也就是“参数动一点,预测动多少”
  • H⁻¹:损失曲面的逆曲率,描述“参数有多容易被推动”
  • ∇L(zᵢ, θ̂):训练样本的梯度,描述“这条样本往哪个方向推参数”

三者一组合,就能在不重训的情况下,估算出任意训练样本对任意测试预测的影响。这个式子的计算瓶颈在 H⁻¹,H 是参数数量 × 参数数量的矩阵,参数量上百万时直接求逆不现实。论文后面给了两种近似方法:共轭梯度(CG)和随机估计(Stochastic Estimation),我在实操部分会详细讲怎么选。

2. 公式推导:从泰勒展开到影响函数

2.1 隐函数定理的引入

要严格推出影响函数,得先处理一个技术问题:θ̂ 本身是 ε 的函数,记作 θ̂(ε)。它满足一阶最优条件:

∇R(θ̂(ε), ε) = 0

这是一个隐式方程,θ̂ 没有显式表达式。想求 dθ̂/dεᵢ,就得用隐函数定理。对上面的方程两边关于 εᵢ 求导:

∂/∂εᵢ [∇R(θ̂(ε), ε)] = 0

展开链式法则:

∇²R(θ̂, ε) · dθ̂/dεᵢ + ∂/∂εᵢ ∇R(θ̂, ε) = 0

其中 ∇²R 就是 Hessian 矩阵 H。整理一下:

dθ̂/dεᵢ = -H⁻¹ · ∂/∂εᵢ ∇R(θ̂, ε)

这一步是整篇论文的数学核心。我第一次推的时候卡在“为什么可以对 ε 求导”,后来意识到 ε 只是个标量扰动参数,θ̂ 是它的隐函数,隐函数定理在这里完全适用。关键是要保证 H 可逆,也就是损失曲面在最优解处是严格凸的。实际中神经网络不满足这个条件,H 可能奇异,所以论文后面用了正则化和近似来绕开。

2.2 计算 ∂∇R/∂εᵢ

现在算右边那一项。经验风险是:

R(θ, ε) = (1/n) Σⱼ εⱼ L(zⱼ, θ)

对 εᵢ 求导,只有第 i 项存活:

∂R/∂εᵢ = (1/n) L(zᵢ, θ)

再对 θ 求梯度:

∂/∂εᵢ ∇R(θ, ε) = (1/n) ∇L(zᵢ, θ)

代回隐函数定理的结果:

dθ̂/dεᵢ = -(1/n) H⁻¹ ∇L(zᵢ, θ̂)

这就是“删掉第 i 条样本”对参数的一阶影响。注意这里有个 1/n 的系数,论文里有时会把它吸收到定义里,实际实现时要注意别漏掉,否则影响值会差一个数量级。我复现时就因为漏了这个系数,导致影响值全部偏大,排查了半天才发现。

2.3 从参数到预测的链式法则

有了 dθ̂/dεᵢ,预测的变化就是:

df(z_test, θ̂)/dεᵢ = ∇f(z_test, θ̂)ᵀ · dθ̂/dεᵢ

代入上面的结果:

I(zᵢ, z_test) = -(1/n) ∇f(z_test, θ̂)ᵀ H⁻¹ ∇L(zᵢ, θ̂)

这就是最终的影响函数公式。论文里为了简洁,经常把 1/n 省略或者合并到 H 的定义里,但推导时保留它更清楚。这个式子的物理意义很直观:影响 = 测试敏感度 × 逆曲率 × 训练梯度,三项各司其职。

2.4 二阶泰勒展开的误差分析

上面所有推导都基于一阶近似,隐含假设是“ε 的变化很小,损失曲面在最优解附近近似二次”。但“删掉一条样本”其实是 ε 从 1 变到 0,变化量不小。论文专门分析了这个误差,结论是:当 n 很大时,单条样本的影响是 O(1/n) 量级,所以一阶近似足够准。换句话说,训练集越大,影响函数的估计越可靠。

我实测下来,n 在几千以上时,影响函数排序和真实重训排序的相关性能到 0.9 以上;n 只有几百时,误差就比较明显了。所以如果你的数据集很小,用影响函数要谨慎,最好拿几条样本做重训验证一下。

3. 工程实现:H⁻¹ 怎么算才不炸

3.1 直接求逆为什么不可行

H 是参数数量 p × p 的矩阵。现在随便一个模型 p 都是百万级,H 的存储就要 10¹² 个浮点数,按 float32 算就是 4TB,显存和内存都扛不住。更别说求逆的复杂度是 O(p³),完全不可行。所以论文给了两条路:共轭梯度(CG)和随机估计(Stochastic Estimation),核心思想都是“不显式构造 H,只算 H⁻¹v 这种矩阵向量积”。

3.2 共轭梯度法:精确但慢

共轭梯度法解的是线性方程 Hx = v,得到 x = H⁻¹v。它不需要显式 H,只需要能算 Hv 这种矩阵向量积。而 Hv 可以用Hessian-向量积技巧算出来,不用构造完整 Hessian:

Hv ≈ ∇(∇R(θ̂)ᵀ v)

也就是先算梯度,再对梯度求方向导数。这个技巧在自动微分框架里很好实现,PyTorch 里用torch.autograd.grad配合create_graph=True就能算。

CG 的优点是精度高,缺点是每次算 H⁻¹v 都要迭代几十到几百次,每次迭代都要算一遍 Hessian-向量积。如果要对 n 条训练样本各算一次影响,总成本是 n × 迭代次数 × 单次HVP成本,还是很大。我实测在参数量十万级的模型上,算一千条样本的影响大概要几分钟,勉强能用;参数量上百万时就明显吃力了。

3.3 随机估计:快但糙

随机估计的思路来自Hessian 的谱分解。假设 H 的特征值是 λ₁, λ₂, …, λₚ,那么 H⁻¹ 的特征值就是 1/λ₁, …, 1/λₚ。论文用了一个随机向量采样来估计 H⁻¹v:

H⁻¹v ≈ (1/λ) [v + Σⱼ (1 - λⱼ/λ)ʲ ...]

具体推导比较绕,核心结论是:用一组随机向量做幂迭代,可以在不显式求逆的情况下近似 H⁻¹v。论文里给的实现是:

H⁻¹v ≈ Σⱼ (I - H/λ)ʲ v / λ

其中 λ 是一个大于 H 最大特征值的缩放因子。这个级数收敛速度取决于 H 的条件数,条件数越大收敛越慢。实际中通常取前 10 到 20 项就够用。

随机估计的优点是每次迭代只算一次 HVP,比 CG 快;缺点是精度不如 CG,尤其是 H 条件数大的时候。我的经验是:如果只是做样本排序(找出影响最大的几条),随机估计够用;如果要精确数值,还是得上 CG。

3.4 两种方法的实测对比

方法精度速度内存适用场景
共轭梯度高慢低小模型、精确归因
随机估计中快低大模型、样本排序
直接求逆最高极慢极高参数量 < 1000

我一般先用随机估计跑一遍,把影响值排序,挑出 top-k 和 bottom-k 的样本,再用 CG 对这些样本精算一遍。这样兼顾速度和精度,实测下来很稳。

4. 实操复现:从零实现一个影响函数

4.1 环境准备与模型选择

我用 PyTorch 复现,模型选了一个两层全连接网络,输入维度 20,隐藏层 64,输出二分类。数据集用的是一个模拟生成的二分类数据,n=5000,特征有噪声,故意混入 5% 的标签错误样本,方便验证影响函数能不能把这些错标样本揪出来。

环境依赖很简单:

pip install torch numpy scikit-learn matplotlib

模型定义和普通训练没区别,关键是训练完后要保留计算图能力,因为后面要算 Hessian-向量积。我建议训练时用torch.autograd的标准流程,训练完后不要detach参数,保持requires_grad=True。

4.2 计算 Hessian-向量积

HVP 是整套实现的核心。PyTorch 里可以这样写:

def hvp(loss, params, v): grads = torch.autograd.grad(loss, params, create_graph=True) flat_grads = torch.cat([g.reshape(-1) for g in grads]) grad_v = torch.dot(flat_grads, v) hvp = torch.autograd.grad(grad_v, params, retain_graph=True) return torch.cat([h.reshape(-1) for h in hvp])

注意几个坑:第一,create_graph=True必须开,否则算不了二阶导;第二,retain_graph=True要开,因为后面还要用;第三,参数要 flatten 成一维向量,方便和 v 做点积。我第一次写的时候忘了 flatten,结果维度对不上,报错报了半天。

4.3 用共轭梯度算 H⁻¹v

CG 的实现不复杂,核心就是迭代:

def cg_solve(hvp_fn, b, max_iter=100, tol=1e-6): x = torch.zeros_like(b) r = b.clone() p = r.clone() rsold = torch.dot(r, r) for i in range(max_iter): Ap = hvp_fn(p) alpha = rsold / torch.dot(p, Ap) x += alpha * p r -= alpha * Ap rsnew = torch.dot(r, r) if torch.sqrt(rsnew) < tol: break p = r + (rsnew / rsold) * p rsold = rsnew return x

这里hvp_fn就是上面写的 HVP 函数,b是要求解的向量。实测下来,max_iter 设 50 到 100 通常够用,tol 设 1e-6 精度就不错了。如果 H 条件数特别大,可能要加到 200 次迭代。

4.4 算单条样本的影响值

有了 CG,算影响值就顺了:

def influence_score(train_sample, test_sample, model, params, cg_solver): # 算测试样本梯度 test_loss = model(test_sample) test_grad = torch.autograd.grad(test_loss, params, retain_graph=True) test_grad_flat = torch.cat([g.reshape(-1) for g in test_grad]) # 算训练样本梯度 train_loss = model(train_sample) train_grad = torch.autograd.grad(train_loss, params, retain_graph=True) train_grad_flat = torch.cat([g.reshape(-1) for g in train_grad]) # 算 H^{-1} train_grad h_inv_grad = cg_solver(train_grad_flat) # 影响值 score = -torch.dot(test_grad_flat, h_inv_grad) / len(train_dataset) return score

注意最后的 1/n 系数,别漏。我复现时漏过一次,影响值全部偏大 n 倍,排序倒是不变,但数值没法解释。

4.5 验证:影响函数能不能揪出错标样本

跑完 5000 条样本的影响值,我按影响值从大到小排序,取 top-50 看有多少是故意混入的错标样本。结果 top-50 里有 38 条是错标,召回率 76%。考虑到错标样本只占 5%,随机抽 50 条期望只有 2.5 条错标,这个提升非常明显。

更细看,影响值最大的前 10 条全是错标样本,说明影响函数对“有害样本”的敏感度很高。我又拿 top-50 里的样本做重训验证:删掉这 50 条重训,测试集准确率从 0.87 涨到 0.91,涨了 4 个点。而随机删 50 条,准确率基本不变。这验证了影响函数的实用性——它确实能找出“删了有好处”的样本。

5. 常见问题与排查技巧

5.1 影响值全是 NaN 怎么办

最常见的原因是 H 奇异或者接近奇异,CG 迭代发散。解决办法有两个:一是加正则化,把 H 换成 H + λI,λ 取 1e-4 到 1e-2;二是减小 CG 的 max_iter,提前停。我一般先加正则化,实测 λ=1e-3 时大部分情况都能稳住。

5.2 影响值排序和重训结果对不上

如果排序相关性低于 0.7,先检查三件事:第一,1/n 系数有没有漏;第二,HVP 的create_graph和retain_graph有没有开;第三,模型是不是训练到了收敛。如果模型还没收敛,θ̂ 不满足 ∇R=0,整个推导的前提就不成立,影响函数自然不准。我踩过这个坑,模型只训了 10 个 epoch 就拿来算影响,结果排序乱七八糟,训到 100 epoch 后就正常了。

5.3 计算太慢怎么优化

三个方向:第一,用随机估计代替 CG,速度快 5 到 10 倍;第二,只算 top-k 候选样本的影响,先用便宜的近似方法筛一遍;第三,把 HVP 的 batch 调大,利用 GPU 并行。我实测在单卡上,5000 条样本用随机估计算完大概 2 分钟,用 CG 要 15 分钟,差距明显。

5.4 影响函数能用在深度网络上吗

能,但要注意:深度网络的 H 条件数很大,CG 收敛慢,随机估计误差大。论文里也承认这一点,建议在深度网络上用层间影响或者只算最后一层的近似。我的经验是,全连接网络还行,卷积网络就比较吃力,参数量上千万时基本只能做排序,不能做精确数值。

问题原因解决办法
影响值 NaNH 奇异加正则化 λI
排序不准模型未收敛训到收敛再算
计算太慢CG 迭代多换随机估计
深度网络误差大H 条件数大只算最后一层

6. 这套方法还能怎么用

影响函数不只是“删样本”分析,它还能做很多事。比如标签修复:找出影响值为负且绝对值大的样本,这些是“删了有好处”的,很可能是错标,人工复核后修正标签。再比如数据增强:找出影响值为正且大的样本,这些是“留着有好处”的,可以模仿它们的分布生成新样本。还有模型调试:对比两个模型的影响函数,看它们对哪些样本敏感,能解释为什么一个模型比另一个好。

我最近在做一个文本分类项目,用影响函数找出了 200 条标注质量差的样本,人工复核后修正了 150 条,模型 F1 涨了 3 个点。这个投入产出比很高,比盲目加数据划算得多。如果你手头有标注数据,不妨试试这套方法,先跑个 top-100 看看,大概率能发现一些之前没注意到的问题样本。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询