如果我们想比较不同的算法,我们是否必须在 10 倍交叉验证之前修复拆分?

机器算法验证 r 交叉验证 插入符号 e1071
2022-04-02 16:49:34

我与 R 一起工作,假设我有一个训练集和一个测试集。我想测试不同的算法(例如神经网络和 svm)。

我将在我的训练集上执行前 10 次交叉验证以调整神经网络。

然后我将在我的训练集上执行 10 倍交叉验证以调整 svm。

我将在我的测试集上比较每个最佳模型的性能。

我想知道这在理论上是否是一个问题,即 10 倍(随机构建)在两种算法的调整中并不相同。

我认为这应该不是问题,因为调整的结果应该对折叠的选择是稳健的。但显然情况并非如此(我已经读过关于 knn 的内容,在 R 中使用了 e1071 包的 tune.knn)。

如果我们必须在调整之前修复拆分,您知道如何在 R 中这样做吗?我还没有找到 e1071 包的 tune 功能的正确选项。

相对于这一点而言,插入符号是一个更好的包吗?由于在调优时似乎可以重复 10 次交叉验证,我认为这可能会使调优的结果更加稳健,并且不同模型的比较更加合理。

感谢您的洞察力

4个回答

它当然有帮助,但不是绝对必要的。

交叉验证拆分的选择引入了(无趣的)可变性的来源。使用同一组拆分可以消除这种差异来源,这可能会提高您检测不同 分类器(如果存在)性能变化的能力,这通常更有趣。

如果你能控制分裂,那么你真的应该这样做——这是一种无需做太多额外工作即可增加实验能力的简单方法。另一方面,如果您已经有一些难以复制的结果而忘记存储拆分,那么您当然可以使用该数据;请注意,基于这些结果的比较不会像它们可能的那样强大。人们经常假设分区相关的方差比跨分类器的方差小很多,尽管这可能不是真的,尤其是在你的类非常不平衡的情况下。

e1071具体来说文档tune

交叉验证在构建拆分之前对数据集进行随机化,这些拆分(一旦创建)在训练过程中保持不变。可以通过返回对象的 train.ind 组件恢复拆分。

您可以让tune第一个算法自己生成折叠,然后通过tune.control(采样=固定)使用它们来评估后续的。或者,看起来 tunesample在函数的最开始使用全局 prng(通过调用)生成其分区(源代码中 Tune.R 的第 71 行),因此您可以通过重置来生成相同的折叠每次调用tune. 不过,这似乎有点脆弱。最后,这很容易自己编程。

结果对拆分很敏感,因此您应该在数据的相同分区上比较模型。比较这两种方法:

  1. 方法 1 将比较两个模型,但使用相同的 CV 分区。
  2. 方法 2 将比较两个模型,但第一个模型的 CV 分区与第二个不同。

我们想选择最好的模型。方法 2 的问题在于两个模型之间的性能差异将来自两个不同的来源:(a)两个折叠之间的差异和(b)算法本身之间的差异(例如,随机森林和逻辑回归) . 如果一个模型优于另一个模型,我们将不知道这种性能差异是完全、部分还是根本不是由于两个 CV 分区的差异。另一方面,使用方法 1 的任何性能差异都不能归因于数据分区方式的差异,因为分区是相同的。

要修复分区,请使用cvTools创建(重复的)CV 分区并存储结果。

除了@Matt Krause 的回答:

我会从两个不同的方面来处理这个问题:

  • 交叉验证的基本假设之一是建立在不同拆分上的模型是相等的(或至少是相等的)。这允许汇集所有这些拆分的结果。如果满足该假设,则拆分无关紧要。
    然而,在实践中,分裂确实会引入不可忽略的方差,即模型是不稳定的。训练数据的细微变化,所以无论如何都应该检查这种变化(即使没有优化任何东西)。

  • 在相同的拆分上评估不同的分类器意味着您可以在配对测试中评估比较,这比相应的非配对测试更强大:这就是为什么您可以通过保持拆分不变来检测较小的性能变化。

虽然这已经得到解答,但如果您想要一些代码允许您在插入符号中使用相同的 CV 拆分进行多个模型训练,您可以使用以下代码:

tune_control <- trainControl(
  method = "repeatedcv", 
  repeats = 2,
  number = 5,
  index = createMultiFolds(df$y, k=5, times=2) # assuming your object is df and you are modeling y
)

您可以通过训练两个模型并比较以下输出来手动检查它是否有效:

model$control$index # replace model w/ name of your model

应该打印出如下内容:

List of 10
Fold1.Rep1: int [1:2400] 1 2 3 4 5 7 9 10 11 12 ...
Fold2.Rep1: int [1:2400] 1 2 3 4 5 6 7 8 9 10 ...
Fold3.Rep1: int [1:2400] 2 3 4 6 8 9 10 11 13 14 ...
Fold4.Rep1: int [1:2400] 1 2 5 6 7 8 11 12 16 18 ...
Fold5.Rep1: int [1:2400] 1 3 4 5 6 7 8 9 10 11 ...
Fold1.Rep2: int [1:2400] 1 3 4 5 6 8 10 11 12 14 ...
Fold2.Rep2: int [1:2400] 1 2 3 4 5 6 7 8 9 10 ...
Fold3.Rep2: int [1:2400] 2 3 4 5 7 8 9 10 11 12 ...
Fold4.Rep2: int [1:2400] 1 2 3 5 6 7 8 9 11 12 ...
Fold5.Rep2: int [1:2400] 1 2 4 6 7 9 10 13 16 17 ...