4、缺失模态填补技术:均值/零值填补、最近邻填补、基于矩阵分解的填补
模态缺失了怎么办?直接扔掉?太浪费了。强行训练?模型会学歪。
我个人习惯的做法是——先试试填补。说白了,就是把缺失的数据想办法补上。补得好,模型照样跑得稳。补得糙,反而引入噪声。
这一章咱们就聊聊三种最常用的填补方法。从最简单的到稍微复杂一点的,我都踩过坑,咱们一个一个说。
核心观点:填补不是万能药,但它是处理模态缺失最直接、最工程友好的手段。选对方法,能省下大量重新采集数据的成本。
4.1 均值/零值填补:简单粗暴,但有用
先讲最简单的。均值填补,就是用该特征在所有样本上的平均值,去填缺失的位置。零值填补更直接,缺失的地方直接写0。
你可能会问:这么简单的方法,真的有用吗?
我在一个工业质检项目里遇到过这种情况。传感器偶尔掉线,缺失率不到5%。当时团队想上复杂的生成模型,我拦住了。我说先试试均值填补。结果呢?精度只掉了0.3%,训练时间几乎没增加。有时候,简单就是美。
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 零值填补 | 缺失率极低(<1%),特征本身非负 | 实现简单,计算零成本 | 引入偏差,破坏分布 |
| 均值填补 | 缺失率较低(<10%),特征分布较集中 | 保持均值不变,方差略缩小 | 忽略特征间相关性 |
避坑指南:我曾经在一个多模态情感分析任务里,对文本特征用了零值填补。结果模型学到的全是「缺失即负面」的假关联。后来换了均值填补,才把模型拉回来。记住:零值填补会引入「缺失即零」的强先验,不是所有场景都适用。
import numpy as np
def mean_imputation(data, missing_mask):
"""
均值填补
data: (n_samples, n_features)
missing_mask: 布尔矩阵,True表示缺失
"""
imputed = data.copy()
for j in range(data.shape[1]):
col = data[:, j]
mask = missing_mask[:, j]
if np.any(mask):
# 用非缺失部分的均值填补
mean_val = np.mean(col[~mask])
imputed[mask, j] = mean_val
return imputed
# 使用示例
X = np.array([[1.0, 2.0, np.nan],
[4.0, np.nan, 6.0],
[7.0, 8.0, 9.0]])
mask = np.isnan(X)
X_filled = mean_imputation(X, mask)
print(X_filled)
4.2 最近邻填补:让相似样本帮你补
均值填补有个硬伤——它不管样本之间的差异。比如用户A和用户B的喜好完全不同,你用全局均值去填,结果两边都不讨好。
最近邻填补的思路就聪明多了:找一个跟当前样本最相似的完整样本,把它的值抄过来。你想想看,这不就是「物以类聚」吗?
具体做法分三步:
- 找邻居:用非缺失维度计算距离,找到k个最近邻
- 选值:从邻居的对应维度取值
- 聚合:取均值或加权平均
我记得有一次做医疗影像+结构化数据的多模态任务。患者的某些化验指标经常缺失。用均值填补,模型完全分不清重症和轻症。换成最近邻填补后,AUC直接涨了4个点。为什么?因为最近邻保留了患者之间的相似性结构。
小技巧:距离度量别死磕欧氏距离。高维数据试试余弦相似度,混合类型数据试试Gower距离。我一般会先跑个t-SNE看看数据分布,再选距离度量。
from sklearn.impute import KNNImputer
# 直接用sklearn,省心
imputer = KNNImputer(n_neighbors=5, weights='distance')
X_filled = imputer.fit_transform(X)
# 自己实现核心逻辑(更灵活)
def knn_impute(data, missing_mask, k=5):
imputed = data.copy()
n_samples = data.shape[0]
for i in range(n_samples):
missing_dims = np.where(missing_mask[i])[0]
if len(missing_dims) == 0:
continue
# 只使用非缺失维度计算距离
present_dims = np.where(~missing_mask[i])[0]
if len(present_dims) == 0:
continue
# 找最近邻
dists = []
for j in range(n_samples):
if i == j or np.any(missing_mask[j, present_dims]):
continue
d = np.linalg.norm(data[i, present_dims] - data[j, present_dims])
dists.append((d, j))
dists.sort(key=lambda x: x[0])
neighbors = dists[:k]
# 用邻居的均值填补
for dim in missing_dims:
values = [data[n[1], dim] for n in neighbors
if not missing_mask[n[1], dim]]
if values:
imputed[i, dim] = np.mean(values)
return imputed
4.3 基于矩阵分解的填补:挖掘潜在结构
前面两种方法都是「局部修补」。如果缺失模式很复杂,比如某个模态整体缺失了30%以上,局部修补就不够用了。
这时候我会上矩阵分解。核心思想很简单:把数据矩阵X分解成两个低秩矩阵的乘积。缺失值?没关系,只对观测到的部分做优化,缺失的部分自然就被低秩结构「推断」出来了。
数学上就是解这个优化问题:
minimize || (X - UV^T) ⊙ M ||_F^2 + λ(||U||_F^2 + ||V||_F^2)
其中:
- X: 原始数据矩阵 (n_samples, n_features)
- U: 样本隐因子矩阵 (n_samples, k)
- V: 特征隐因子矩阵 (n_features, k)
- M: 观测掩码矩阵 (0表示缺失)
- k: 隐因子数量(超参数)
说白了,就是让模型自己学出数据背后的「潜规则」。比如在推荐系统里,用户-物品矩阵的缺失值,通过矩阵分解就能预测出用户可能喜欢什么。
| 方法 | 缺失容忍度 | 计算开销 | 可解释性 |
|---|---|---|---|
| 均值/零值填补 | 低(<10%) | O(1) | 高 |
| 最近邻填补 | 中(<30%) | O(n²) | 中 |
| 矩阵分解填补 | 高(<60%) | O(n·k·iter) | 低(隐因子含义不明确) |
注意:矩阵分解的隐因子数量k需要调参。k太小,欠拟合,补出来的值太平滑;k太大,过拟合,把噪声也学进去了。我一般用交叉验证,或者观察重构误差的下降曲线——当误差下降变缓时,就是合适的k。
import numpy as np
def matrix_factorization_impute(X, mask, k=10, lr=0.01, reg=0.1, epochs=100):
"""
基于矩阵分解的缺失值填补
X: (n, m) 数据矩阵,缺失处为0
mask: (n, m) 布尔矩阵,True表示观测到
k: 隐因子数量
"""
n, m = X.shape
U = np.random.randn(n, k) * 0.01
V = np.random.randn(m, k) * 0.01
for epoch in range(epochs):
# 只对观测到的位置计算梯度
pred = U @ V.T
error = (X - pred) * mask
# 梯度下降
U_grad = -2 * error @ V + 2 * reg * U
V_grad = -2 * error.T @ U + 2 * reg * V
U -= lr * U_grad
V -= lr * V_grad
# 监控损失
loss = np.sum(error**2) + reg * (np.sum(U**2) + np.sum(V**2))
if epoch % 20 == 0:
print(f"Epoch {epoch}, Loss: {loss:.4f}")
# 用分解结果填补缺失值
X_filled = X.copy()
X_filled[~mask] = (U @ V.T)[~mask]
return X_filled
4.4 三种方法对比与选型指南
方法讲完了,怎么选?我总结了一张决策表,你直接对着用:
| 场景特征 | 推荐方法 | 理由 |
|---|---|---|
| 缺失率 < 5%,特征独立 | 均值填补 | 够用,快,不折腾 |
| 缺失率 5%-20%,有相似样本 | 最近邻填补 | 保留局部结构,效果好 |
| 缺失率 > 20%,特征相关性强 | 矩阵分解 | 挖掘全局结构,鲁棒性高 |
| 实时推理,算力受限 | 均值/零值填补 | 零延迟,无额外计算 |
| 多模态融合前预处理 | 最近邻或矩阵分解 | 保留模态内结构,融合更稳 |
我的经验:别一上来就上矩阵分解。先跑个简单的均值填补,看看模型掉点多少。如果掉点超过5%,再升级方法。工程上,够用就好,不要过度设计。
4.5 本章小结:填补方法全景图
下面这张图把三种方法的关系和适用边界画清楚了。你可以把它当作选型时的速查表。
三种方法各有各的脾气。均值填补快但糙,最近邻填补稳但慢,矩阵分解强但调参麻烦。我的建议是:从最简单的开始,让数据告诉你什么时候该升级方法。