简介
数据质量的高低是决定使用机器学习算法获得预测结果质量高低的重要因素,在很多常见任务中,数据质量的作用远大于模型的作用,本文讨论数据预处理时会遇到的一个常见问题:训练集与测试集数据分布不一致。
什么是训练集与测试集数据分布不一致?
一个具体的例子,比如我现在要预测泰坦尼克号乘客存活率(Kaggle 上的经典题,已经被各路选手将准确率刷爆了),如果训练集的输入特征中,“性别” 这一特征多数是男性,而在测试集里,“性别” 这一特征多数是女性,这便是训练集与测试集上,某特征其数据分布不均。
训练集和测试集分布不一致也被称作数据集偏移 (Dataset Shift),导致这种问题有两个常见原因:
- 样本选择偏差 (Sample Selection Bias): 训练集是通过有偏方法得到的,例如非均匀选择 (Non-uniform Selection),导致训练集无法很好表征的真实样本空间。
- 环境不平稳 (Non-stationary Environments): 当训练集数据的采集环境跟测试集不一致时会出现该问题,一般是由于时间或空间的改变引起的。
先讨论样本选择偏差,在有监督学习里,样本会分为特征数据 (feature) 与目标变量 (label),样本选择偏差也会分分为两种情况:
- 没有考虑数据中不同特征的分布问题,如前面举例的预测泰坦尼克号乘客存活率问题,训练集的性别特征中,男性比例大,而测试集的性别特征中,女性比例大。
- 没有考虑数据中目标变量分布问题,从而会出现:训练集类别 A 数据量远多于类别 B,而测试集相反的情况。
样本选择偏差会导致训练好的模型在测试集上鲁棒性很差,因为训练集没有很好覆盖整个样本空间。
接着讨论环境不平稳带来的数据偏移,最典型的就是在时序数据中,用历史时序数据预测未来时序,未来突发事件很可能带来时序的不稳定表现,这便带来了分布差异。
环境因素不仅限于时间和空间,还有数据采集设备、标注人员等。
校验数据分布
如何判断训练集与测试集数据分布是否不一致呢?
通常使用核密度估计 (kernel density estimation, KDE) 分布图和 KS 校验这两种方法来判断。
KDE 分布图
在讨论 KDE 分布图之前,先考虑一下使用概率密度直方图来判断数据分布的问题。
概率密度直方图是用数据集中不同数据出现的次数来表示其概率,需注意这种假设不一定成立。
要对比训练集和测试集数据的分布,我们可以通过绘制相应的概率密度直方图,然后直观的判断直方图的差异,但通过直方图判断数据分布的会有两个缺陷:
- 1. 受 bin 宽度影响大
- 2. 不平滑
而 KDE 分布图相比于直方图,它受 bin 影响更小,绘图呈现更平滑,易于对比数据分布,下图便是直方图和核密度估计的一个对比:
在进一步讨论 KDE 前,先讨论一下核函数,核函数定义一个用于生成 PDF (概率分布函数,Probability Distribution Function) 的曲线,不同于将值放入离散 bins 中,核函数对每个样本值都创建一个独立的概率密度曲线,然后加和这些平滑曲线,最终得到一个平滑连续的概率分布曲线。
“核” 在不同的语境下的含义是不同的,在 “非参数估计”(即不知道数据分布情况) 的语境下,“核” 是一个函数,用来提供权重。例如高斯函数 (Gaussian) 就是一个常用的核函数。
KDE 在数学上还有挺多细节,但在实现上,通过 seaborn 库便可以轻松实现,代码如下:
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
# 创建样例特征
train_mean, train_cov = [0, 2], [(1, .5), (.5, 1)]
test_mean, test_cov = [0, .5], [(1, 1), (.6, 1)]
# np.random.multivariate_normal 从多变量正态分布中随机抽取样本
# 多正态分布是一维正态分布向高维的推广。这样的分布是由它的平均值和协方差矩阵来确定的。
# 这些参数类似于一维正态分布的均值(平均或“中心”)和方差(标准差或“宽度”的平方)。
train_feat, _ = np.random.multivariate_normal(train_mean, train_cov, size=50).T
test_feat, _ = np.random.multivariate_normal(test_mean, test_cov, size=50).T
# 绘KDE对比分布
sns.kdeplot(train_feat, shade = True, color='r', label = 'train')
sns.kdeplot(test_feat, shade = True, color='b', label = 'test')
plt.xlabel('Feature')
plt.legend()
plt.show()
注意,上述代码中,train_feat 参数是一维的(即单独某个特征的分布,多个多特征,需要绘制多个KDE分布图)。效果如图所示:
从上图可知,训练集与测试集分布差异不大,可以继续模型训练等操作,如果分布差异较大,比如下图这种,就需要对原始数据进行处理了。
KS 检验
KDE 是使用 PDF 来对比,而 KS 检验是基于 CDF (累计分布函数 Cumulative Distribution Function) 来检验两个数据分布是否一致,它也是非参数检验方法。
KS 检验是基于累计分布函数,用于检验一个分布是否符合某种理论分布或比较两个经验分布是否有显著差异。
KS 检验一般返回两个值:
- 第一个值表示两个分布之间的最大距离,值越小即这两个分布的差距越小,分布也就越一致。
- 第二个值是 p 值,用来判定假设检验结果的一个参数,p 值越大,越不能拒绝原假设(待检验的两个分布是同分布),即两个分布越是同分布。
通过 scipy 库可以快速实现 KS 检验,代码如下:
from traceback import print_tb
import numpy as np
from scipy import stats
train_mean, train_cov = [0, 2], [(1, .5), (.5, 1)]
test_mean, test_cov = [0, .5], [(1, 1), (.6, 1)]
train_feat, _ = np.random.multivariate_normal(train_mean, train_cov, size=50).T
test_feat, _ = np.random.multivariate_normal(test_mean, test_cov, size=50).T
result = stats.ks_2samp(train_feat, test_feat)
print(result)
# 打印结果:
# KstestResult(statistic=0.18, pvalue=0.3959398631708505)
若 KS 统计值小且 p 值大,则可以接受 KS 检验的原假设,即两个数据分布一致。
上面样例数据的统计值较低,p 值大于 10% 但不是很高,因此反映分布略微不一致。如果p 值 < 0.01,建议拒绝原假设,p 值越大,越倾向于原假设成立。
分类器对抗验证
所谓对抗验证,就是构建一个分类模型去分类训练集和测试集,如果分类模型可以清楚的分类,则说明训练集和测试集的分布有明显差异,反之分布差异不大。
分类模型可以直接使用 sklearn 中提供了几种常见分类器来实现,比如 SVM。
具体步骤如下:
- 训练集和测试集合并,同时新增标签Is_Test去标记训练集样本为 0,测试集样本为 1。
- 构建分类器 (例如 SVM、LGB、XGB 等) 去训练混合后的数据集 (可采用交叉验证的方式),拟合目标标签Is_Test。
- 输出交叉验证中最优的 AUC 分数。AUC 越大 (越接近 1),越说明训练集和测试集分布不一致。
结尾
本文的方法虽然基于训练数据与测试数据进行讨论,但同样可以用于训练数据与预测数据的分布检测上,在模型训练测试阶段,我们会将已有的数据划分为训练数据与测试数据,当模型通过测试后,通常会合并训练数据与测试数据,用所有数据进行训练,获得最终的模型,然后上线使用。
如果上线后效果不好,数据分布问题依旧要考虑,通常,我们会收集线上的待预测数据,将待预测数据的特征分与训练数据的特征分布进行比较,依旧使用本文提及的方法,如果分布差异大,则说明,训练数据无法代表待预测数据,当前模型是没有实用价值的。
数据预处理是多数机器学习任务的核心,反倒是模型,因为很多成熟的实现,反而不是啥大问题。