作为算法工程师,我经常需要快速粗略地估计一个预测模型在给定的数据集上的表现。很长一段时间,我都是通过
交叉验证来这样做的,然而我意识到我完全走偏了。事实上,对于现实世界中的问题,交叉验证是完全不可信
的。
我猜许多算法工程师仍然依赖于这种方法,我认为深入研究这一主题非常重要。
在这篇文章中,我将探讨为什么在处理现实世界的问题时,交叉验证从来都不是一个好的选择,并给出了更好的替代方法。
交叉验证是一种模型验证算法,用于评估在已知数据集上训练的模型在未知数据集的表现。
注意:交叉验证方法包含多种方法,在本文中,为了简单起见,当我们说“交叉验证”时,指的是随机K折叠交
叉验证,这是迄今为止最常见的交叉验证类型。
那么,交叉验证在实践中是如何实现的呢?
假设我们有一个为机器学习模型准备好的数据集,即预测因子矩阵(我们称之为X)和包含目标变量的向量(我们称为y)。
每一行代表一个数据集,交叉验证将数据集的每一行分配给K个折叠中的一个(通常K=5)。请注意,通过确保所有折叠的行数大致相同,可以完全随机地进行分配。
如下图有10行数据集,并设置K=5 ,每行的数字表示分配到第几个折叠数据,如Fold=3,表示分配到第3个折叠的数据集,Fold=2,表示分配到第2个折叠的数据集,绿框表示每行所属的K折,每个折叠的数据集个数相等。
image-20240510214557924K折交叉验证的原理是在K-1个折叠的数据集上训练模型,并测量其在剩余折叠的数据集性能。
在我们的案例中:
感兴趣的度量(例如精度);
模型B在折叠1、3、4和5上训练,并在折叠2上测量;
模型C在折叠1、2、4和5上训练,并在折叠3上测量;
模型D在折叠1、2、3和5上训练,并在折叠4上测量;
模型E在折叠1、2、3和4上进行训练,并在折叠5上进行测量。
因此我们有5个精度测量值,于是我们取这5个值的平均值,平均值是我们对未知数据集的准确性评估。
你可能会认为:这似乎是一个非常聪明的方法:我们使用所有可用的观测值来验证模型,但从未使用相同的观测值同时训练和验证模型。
那么,可能出了什么问题呢?
您可能没有注意到交叉验证背后有一个非常强大的假设,在真实的案例中从未匹配这种假设!
通过完全随机地拆分固定的数据集,每个折叠将具有相同的分布。因此,我们用于训练的K-1折叠的分布将与性能测试中遗漏的折叠非常相似。这会导致我们的交叉验证率非常高,然而评估新数据集的准确率非常低,有相同经历的童鞋么。
原则上,这没有错。如果我们知道新数据集的分布与我们在训练数据上的分布相同,这将很好。问题是在实际应用中,这种情况永远不会发生。
因此我觉得在实践项目开发中,交叉验证就像作弊!
事实上,在现实生活中,总有一些维度(我们称之为分层变量)使新数据与训练数据不同,例如:
让我们试着通过一个例子让这种解释变得更加清晰。
假设在不同的城市销售相同的产品,我们想建立一个模型来预测我们的产品在特定城市以特定的销售价格是否
会被购买。
如下图,我们有两个特征:城市和售价
image-20240510220829584San Francisco(旧金山)的售价比 Cincinnati(辛辛那提)贵,通过上图的数据,我们设置旧金山的售价阈值是95美元,辛辛那提的售价阈值是85美元,意味着当旧金山产品的价格低于95美元,消费者才会购买,辛辛那提低于85美元时,消费者才会购买。
现在让我们使用交叉验证来计算模型的准确性。出于简单起见,让我们使用2折叠(K=2)。
image-20240512180052989如上图的随机抽样,折叠1和折叠2的分布完全相同。毕竟,这是交叉验证的重要假设。由于拆分是完全随机的,我们期待每次的折叠都遵循相同的分布。
现在,让我们看看在训练/测试阶段会发生什么。
模型A在折叠1数据进行训练,由于折叠2和折叠1完全相等,当我们使用模型A对折叠2进行预测时,我们得到了完美的预测,准确率100%。
同样地,我们在折叠2数据训练模型B,并在折叠1的数据进行测试时,准确率100%。
image-20240512181320476当然,这个例子的拆分是故意的。但主要观点是,交叉验证的不同fold之间总会存在一些“信息泄露”。
请注意,在实际情况中,即使我们没有在训练数据集中包含分层变量,数据集中可能会有许多其他特征,因此模型可能会找到其他捷径来“作弊”,即通过识别数据集中的其他特征来识别城市(不可观察)。
那么在实际预测时会发生什么?
根据交叉验证方法的最后一个步骤,我们在整个数据集(所有12行)上训练一个新模型,并使用它对一些新数据进行预测。
例如,假设我们想要在一个新的城市纽约使用这个模型。纽约的价格更类似于旧金山而不是辛辛那提,所以我们假设“阈值价格”为100美元。但模型无法知道这一点,因为纽约没有出现在训练数据集中。
因此,在纽约的预测结果:
image-20240512181914838预测准确率只有75%,远远低于交叉验证的100%准确率。
由于交叉验证存在过度乐观的问题,我们需要一种更为真实的数据拆分策略,这种真实体现在更符合真实的数据集分布。
如上述例子,我们希望将模型应用于一个新的城市,我们优化数据集划分策略,每个城市只出现在一个fold中。这被称为组K折(group K-fold)交叉验证。通过这样做,我们确保不同fold之间没有信息泄漏:每个城市要么在训练集中,要么在测试集中。通俗地讲,不同组的数据集分布是完全不一样的,不存在信息泄漏。
因此2折交叉验证就有如下图的划分:
image-20240512182959862模型A在折叠1数据集训练,得到售价阈值是95,因此训练准确率100%。将模型A应用在折叠2数据集,得到验证准确率是67%。
模型B在折叠2数据集训练,得到售价阈值是85,因此训练准确率100%。将模型B应用在折叠1数据集,得到验证准确率是67%。
这种组K折交叉验证更接近于实际情况(75%),而不是100%(交叉验证),因此组K折交叉验证是一种比交叉验证更好的估计方法。
这只是一个很简单的举例,让我们看看同样的情况是否适用于真实数据集。
我们通过Pycaret库获取“银行”数据集。
from pycaret.datasets import get_data
data = get_data('bank')
通过该数据集比较组K交叉验证和K折交叉验证。
该数据集包含银行客户的信息,具体包括:
可视化前5个数据集的特征信息:
image-20240512185106913我们预测模型的目标是预测产品(银行定期存款)是否会被订阅(1)或不会被订阅(0)。
我们使用月份作为分层变量,如下图每个月累积的客户数(如1月1403,2月2649):
image-20240512185734758为了模拟真实环境中的情况,让我们选择一个特定的月份(例如二月),并将其用作“未见过的月份”。
换句话说,假设我们处于一月底,我们想要在过去的11个月(三月到一月)上构建一个模型,并将其用于仍未观察到的二月数据。
我们对比交叉验证和组 K 折交叉验证。
在交叉验证中,拆分是随机的,因此在每个折叠数据中,我们将拥有属于所有 11 个月的行。Scikit-learn 使用这种拆分策略进行交叉验证:
from sklearn.model_selection import cross_val_score
cross_val_score(estimator=model, X=X, y=y)
在组 K 折交叉验证中,每个月只允许出现在一个折叠中。在 Scikit-learn 中,我们可以使用以下代码进行这种类型的验证
from sklearn.moddel_selection import cross_val_score, GroupKFold
cross_val_score(estimator=model, X=X, y=y, cv=GroupKFold(), groups=X["month"])
因此,每个月所属不同的折叠数据集。
image-20240512191725820由于每个月的行数非常不平衡,而我们选择了 K=5,因此组 K 折交叉验证尽最大努力使 5 个折叠的大小尽可能相等。例如,拥有最多行数的五月份,会被分在一个独立的折叠中。
根据交叉验证模型优化步骤:
在数据集(除了二月)上运行了 5 折交叉验证;
在数据集(除了二月)上运行了 5 折组 K 折交叉验证;
在 11 个月(三月到一月)上训练了一个模型,并在二月的数据集上测量其性能 - 这是我们的基准
为了使结果更加健壮,我已经针对不同的模型重复了这个过程:随机森林(random forest)、XGBoost 和 LightGBM。如下图,灰色圆圈表示交叉验证,蓝色圆圈表示组K折交叉验证,红色倒三角形是在新数据集上的测试。
结果对比:
解释上述图表:
通过上述图表,我们很容易得到蓝色条更接近倒红色三角形,因此组K折交叉验证的准确率更客观些。
通过上述例子,表明为什么在现实生活中使用交叉验证来估计模型性能可能会完全误导人。
表现在:
组K交叉验证的评价更具客观性且预测的方差较小。
欢迎扫码关注: