试图了解linq / deferred执行的工作原理

我有以下方法,这是执行分层k折交叉validation的逻辑的一部分。

private static IEnumerable<IEnumerable> GenerateFolds( IClassificationProblemData problemData, int numberOfFolds) { IRandom random = new MersenneTwister(); IEnumerable values = problemData.Dataset.GetDoubleValues(problemData.TargetVariable, problemData.TrainingIndices); var valuesIndices = problemData.TrainingIndices.Zip(values, (i, v) => new { Index = i, Value = v }); IEnumerable<IEnumerable<IEnumerable>> foldsByClass = valuesIndices.GroupBy(x => x.Value, x => x.Index) .Select(g => GenerateFolds(g, g.Count(), numberOfFolds)); var enumerators = foldsByClass.Select(x => x.GetEnumerator()).ToList(); while (enumerators.All(e => e.MoveNext())) { var fold = enumerators.SelectMany(e => e.Current).OrderBy(x => random.Next()); yield return fold.ToList(); } } 

折叠代:

 private static IEnumerable<IEnumerable> GenerateFolds( IEnumerable values, int valuesCount, int numberOfFolds) { // number of folds rounded to integer and remainder int f = valuesCount / numberOfFolds, r = valuesCount % numberOfFolds; int start = 0, end = f; for (int i = 0; i  0) { ++end; --r; } yield return values.Skip(start).Take(end - start); start = end; end += f; } } 

通用的GenerateFolds<T方法根据指定的折叠次数简单地将IEnumerable拆分为IEnumerable的序列。 例如,如果我有101个训练样本,它将产生11倍大小和9倍大小10倍。

上面的方法基于类值对样本进行分组,将每个组拆分为指定的折叠数,然后将类别折叠连接到最终折叠中,确保类标签的分布相同。

我的问题是关于行yield return fold.ToList() 。 实际上,如果我删除ToList() ,该方法可以正常工作,但结果不再正确。 在我的测试用例中,我有641个训练样本和10个折叠,这意味着第一个折叠应该是65号,剩下的折叠大小为64.但是当我删除ToList() ,所有折叠的大小为64,类标签是没有正确分发。 有什么想法吗? 谢谢。

让我们想想什么是fold变量:

 var fold = enumerators.SelectMany(e => e.Current).OrderBy(x => random.Next()); 

它不是查询执行的结果。 这是一个查询定义 。 因为SelectManyOrderBy都是具有延迟执行方式的运算符。 因此,它只是保存了关于展平所有枚举器中的当前项并以随机顺序返回它们的知识。 我突出显示了单词current ,因为它是查询执行时的当前项。

现在让我们考虑一下这个查询的执行时间。 GenerateFolds方法执行的结果是IEnumerable 查询IEnumerable 。 以下代码不执行任何查询:

 var folds = GenerateFolds(indices, values, numberOfFolds); 

这又是一个查询。 您可以通过调用ToList()或枚举它来执行它:

 var f = folds.ToList(); 

但即使是现在内部查询也没有执行。 它们全部归还,但未执行。 即while在将查询保存到列表f执行了GenerateFolds中的循环。 并且e.MoveNext()已被多次调用,直到您退出循环:

 while (enumerators.All(e => e.MoveNext())) { var fold = enumerators.SelectMany(e => e.Current).OrderBy(x => random.Next()); yield return fold; } 

那么, f是什么? 它包含查询列表。 因此你得到了所有这些, 当前项是每个枚举器的最后一项(记住 – 我们在这个时间点完全循环迭代)。 但是这些查询都没有执行过! 在这里执行第一个:

 f[0].Count() 

您将获得第一个查询返回的项目数(在问题顶部定义)。 但是因此您已经枚举了所有查询,当前项目是最后一项。 你得到最后一项的索引数。

现在来看看

 folds.First().Count() 

在这里,您不会枚举所有查询以将其保存在列表中。 即while循环仅执行一次, 当前项目是第一项。 这就是为什么你在第一项中有索引计数的原因。 这就是为什么这些价值观不同的原因。

最后一个问题 – 为什么在while循环中添加ToList()时一切正常。 答案非常简单 – 执行每个查询。 并且您有索引列表而不是查询定义。 每次查询都在每次迭代时执行,因此当前项目总是不同的。 你的代码运行正常。