5个常见的交叉验证技术介绍和可视化

现在的训练可能很少用到交叉验证(cross-validate), 因为我现在处理的数据集规模庞大,如果使用交叉验证则会花费很长的时间。但是交叉验证的重要性有目共睹的,无论你是在使用小数据集做算法的改进,还是在Kaggle上打比赛,交叉验证都能够帮助我们防止过拟合,交叉验证的重要性已经不止一次的在kaggle的比赛中被证明了,所以请记住这句话:In CV we trust。

为什么要交叉验证?

如果不使用交叉验证,我们在训练时会将数据拆分为单个训练集和测试集。模型从训练数据中学习,然后通过预测测试集中所谓看不见的数据来测试其性能。如果对分数不满意,则可以使用相同的集合对模型进行调优,直到 GridSearch(或 Optuna)喊出“够了!”为止。

以下是此过程可能出现严重错误的多种方式中的两种:

  • 过拟合:这些集合不能很好地代表整体数据。作为一个极端的例子,在具有三个类别(a、b、c)的行中,所有 a 和 b 类别可能最终都在训练集中,而所有 c 都挂在测试集中。或者一个数值变量被拆分,使得某个阈值左侧和右侧的值在训练和集合中分布不均匀。或者接近于两个集合中变量的新分布与原始分布不同以至于模型从不正确的信息中学习。
  • 数据泄漏:在超参数调整期间,可能会将有测试集的信息泄漏到模型中。也就是说我们的使用了未知的数据进行了而训练,那么结果肯定会非常的好,但是在模型应用到真正的未知数据时就会变得很差,这也是过拟合的一种表现。

如果我们使用 CV ,所有这些问题都迎刃而解。这就是 CV 的神奇之处,如 Sklearn 用户指南中的介绍:

5个常见的交叉验证技术介绍和可视化

上面是一个 5 折交叉验证过程的例子,它需要五次训练过程才能完成。 模型使用4折进行训练,并在最后1折进行测试。 模型就可以在所有数据上进行训练和测试,而不会浪费任何数据。

接下来,用它们的标准偏差作为置信区间报告平均分。这样才能通过所选参数真正判断模型的性能,因为平均分数将代表模型有效地从数据中学习并准确预测未见样本的真正潜力。

现在,让我们开始介绍5种常用的交叉验证方法,在介绍之前,我们先编写一个快速函数来可视化 CV 的工作方式:

def visualize_cv(cv, X, y):
    fig, ax = plt.subplots(figsize=(10, 5))

    for ii, (tr, tt) in enumerate(cv.split(X, y)):
        p1 = ax.scatter(tr, [ii] * len(tr), c="#221f1f", marker="_", lw=8)
        p2 = ax.scatter(tt, [ii] * len(tt), c="#b20710", marker="_", lw=8)
        ax.set(
            title=cv.__class__.__name__,
            xlabel="Data Index",
            ylabel="CV Iteration",
            ylim=[cv.n_splits, -1],
        )
        ax.legend([p1, p2], ["Training", "Validation"])

    plt.show()

使用上面的函数我们可以轻松的可视化我们的分折策略

KFold

最简单的是 KFold,如上图所示。 它在 Sklearn 中以相同的名称实现。 让我们将一个具有七个分割的 KFold 传递给可视化函数:

from sklearn.datasets import make_regression
from sklearn.model_selection import KFold

X, y = make_regression(n_samples=100)

# Init the splitter
cv = KFold(n_splits=7)

visualize_cv(cv, X, y)

5个常见的交叉验证技术介绍和可视化

这就是经典的 KFold 的样子。

另一个经常使用方式是在执行拆分之前打乱数据。 这通过打乱样本的原始顺序进一步降低了过度拟合的风险:

cv = KFold(n_splits=7, shuffle=True)

visualize_cv(cv, X, y)

5个常见的交叉验证技术介绍和可视化

验证样本的索引是以随机的方式选择的。 但是样本总数仍然是整个数据的七分之一,因为我们在做 7 折的 CV。

KFold 是最常用的 CV ,它很容易理解而且非常有效。 但是根据数据集的特征,有时需要对要使用的 CV 程序有着不同的要求,下面让我们讨论替代方案。

StratifiedKFold

StratifiedKFold是为分类问题设计的 KFold 版本 。

在分类问题中,即使将数据拆分为多个集合,也必须保留目标分布。 简单的说就是分类目标的比例在进行分折后应该与原始数据相同,例如原始数据种A类占比30%,B类占比35%,C类占比35%,在我们分折以后,这个比例是不应该变化的。

在KFold 它不关心分类比例并且会进行打乱,所以无法保证这个比例与原始数据相同。所以我们使用 Sklearn 中的另一个分折器——StratifiedKFold:

from sklearn.datasets import make_classification
from sklearn.model_selection import StratifiedKFold

X, y = make_classification(n_samples=100, n_classes=2)

cv = StratifiedKFold(n_splits=7, shuffle=True, random_state=1121218)

visualize_cv(cv, X, y)

5个常见的交叉验证技术介绍和可视化

它看起来和KFold一样,但现在在所有折中都保留了与原始数据相同的比例。

LeavePOut

有时数据非常有限,甚至无法将其划分为训练集和测试集。 在这种情况下也是可以执行 CV的,我们在每次拆分中只保留几行数据。 这称为 LeavePOut CV,其中 p 是您选择的参数,用于指定每个保持集中的行数。

最极端的情况是LeaveOneOut分割器,其中只使用单行作为测试集,迭代次数等于完整数据中的行数。我们甚至可以为一个100行数据集构建100个模型(当然效果不一定好)。

即使是更大的p,拆分次数也会随着数据集大小的增加而呈指数增长。想象一下,当p为5且数据只有50行时,将构建多少模型(提示—使用排列公式)。

所以,你很少在实践中看到这种情况,但它却经常出现,所以Sklearn将这些过程作为单独的类来实现:

from sklearn.model_selection import LeaveOneOut, LeavePOut

ShuffleSplit

我们根本不做 CV 而只是重复多次重 train/test split过程会是什么样的结果? 根据逻辑,使用不同的随机种子生成多个训练/测试集,如果进行足够多的迭代,应该类似于稳健的CV过程。这就是为什么在Sklearn中有一个分割器来执行这个过程:

from sklearn.model_selection import ShuffleSplit

cv = ShuffleSplit(n_splits=7, train_size=0.75, test_size=0.25)

visualize_cv(cv, X, y)

5个常见的交叉验证技术介绍和可视化

ShuffleSplit 的优点是可以完全控制每个折中训练和集合的大小。 集合的大小不必与拆分的数量成反比。

但是与其他拆分器相反,不能保证在每次随机拆分中生成不同的折。 因此,这是可以尝试交叉验证的另一种方式,但不建议这样做。

顺便说一下,ShuffleSplit 也有Stratified版本用于分类:

from sklearn.model_selection import StratifiedShuffleSplit

cv = StratifiedShuffleSplit(n_splits=7, test_size=0.5)

visualize_cv(cv, X, y)

5个常见的交叉验证技术介绍和可视化

TimeSeriesSplit

最后,时间序列数据是一个非常特殊的情况,因为其中样本的顺序很重要。

我们不能使用任何传统的 CV 类,因为它们会导致很多问题。最常见的就是很有可能会在未来的样本上进行训练并预测过去的样本。

为了解决这个问题,Sklearn 提供了另一个拆分器 — TimeSeriesSplit,它可以确保上述情况不会发生:

rom sklearn.model_selection import TimeSeriesSplit

cv = TimeSeriesSplit(n_splits=7)

visualize_cv(cv, X, y)

5个常见的交叉验证技术介绍和可视化

用于非 IID 数据的其他 CV 拆分器

到目前为止,我们一直在处理 IID(独立同分布)数据。换句话说,生成数据的过程没有过去样本的记忆。

但是,在某些情况下,数据不是 IID — 某些样本组相互依赖。例如,在 Kaggle 上的 Google Brain Ventilator Pressure 竞赛中,参赛者应该使用非 IID 数据。

数据记录了人工肺进行的数千次呼吸(吸气、呼气),并以几毫秒的间隔记录每次呼吸的气压。因此,每次呼吸的数据包含大约 80 行,并且这些行相互依赖。

在这里,传统的 CV 分路器无法按预期工作,因为拆分点很有可能产生在“呼吸的中间”。这是 Sklearn 用户指南中的另一个示例:

这种数据分组是特定于领域的。一个例子是从多个患者收集医疗数据,从每个患者采集多个样本。而这样的数据很可能取决于个体群体。在我们的示例中,每个样本的患者 ID 是其组标识符。

在那之后,它还说明了解决方案:

在这种情况下,我们想知道在特定组上训练的模型是否能很好地泛化到看不见的组。为了衡量这一点,我们需要确保验证折叠中的所有样本都来自配对训练折叠中根本没有代表的组。

Sklearn 列出了五个可以处理分组数据的不同CV类。如果您掌握了前几种拆分的思想并理解了非 IID 数据是什么,那么使用它们就不会遇到问题:

  • GroupKFold
  • StratifiedGroupKFold
  • LeaveOneGroupOut
  • LeavePGroupsOut
  • GroupShuffleSplit

这些拆分器中的每一个都有一个groups 参数,您应该在其中传递存储组 ID 的列。这告诉该拆分其如何区分每个组。

总结

在本篇文章中可能没有回答的一个问题是,“你应该总是使用交叉验证吗?”。 答案是应该是肯定的。 当您的数据集足够大时,任何随机拆分都可能与两组中的原始数据非常相似。 在这种情况下,CV起不到很好的作用。

但是无论数据大小,你都应该执行至少 2 或 3 倍的交叉验证。 这样才是最保险的。

作者:Bex T.

上一篇:图像的分割之基于边缘的分割


下一篇:C++ OpenCV(三):图像像素统计