【问题标题】:Is ScalaCheck's Gen.pick really random?ScalaCheck 的 Gen.pick 真的是随机的吗?
【发布时间】:2017-05-08 04:52:34
【问题描述】:

我在使用 ScalaCheck 的 Gen.pic 时观察到以下意外行为,这(对我而言)表明它的选择不是很随机,尽管 its documentation 这么说:

/** A generator that picks a given number of elements from a list, randomly */

设置后,我按顺序运行了以下三个小程序(在 2 天内,在不同的时间,可能很重要)

implicit override val generatorDrivenConfig = PropertyCheckConfig(
  maxSize = 1000, 
  minSize = 1000, 
  minSuccessful = 1000)

获得合适的样本量。

计划 #1

val set = Set(1,2,3,4,5,6,7,8,9,10,
      11,12,13,14,15,16,17,18,19,20,
      21,22,23,24,25,26,27,28,29,30,
      31,32,33,34,35,36,37,38,39,40,
      41,42,43,44,45,46,47,48,49,50)

// Thanks to @Jubobs for the solution
// See: http://stackoverflow.com/a/43825913/4169924
val g = Gen.pick(3, set).map { _.toList }
forAll (g) { s => println(s) }

在 2 次不同运行生成的 3000 个数字中,我得到了惊人的相似,并且非常非随机分布(数字是四舍五入的,仅列出前 5 个,至于从这里开始的所有列表):

  • 数字:运行 #1 中的频率,运行 #2 中的频率
  • 15:33%、33%
  • 47:22%、22%
  • 4:15%、16%
  • 19:10%、10%
  • 30:6%、6%

(免责声明:除了this way,我找不到如何在此处创建表格)

方案 2

val list: List[Int] = List.range(1, 50)
val g = Gen.pick(3, list)
forAll (g) { s => println(s) }

在使用List 的情况下,数字似乎会“卡在”范围的末尾(在两次运行的情况下为 3x1000 个数字):

  • 49:33%、33%
  • 48:22%、22%
  • 47:14%、14%
  • 46:10%、10%
  • 45:6%、6%

有趣的是,频率与程序 1 的情况几乎相同。

备注:我对列表重复运行多达 10 次,并且经历了完全相同的分布,差异为 +/- 1%,只是不想在此处列出所有数字奇怪的“表格”格式。

方案 3

为了增加趣味性,我运行了第三个小 sn-p,从 List(程序 2)创建了 Set(程序 1):

val set: Set[Int] = List.range(1, 50).toSet
val g = Gen.pick(3, set).map { _.toList }
forAll (g) { s => println(s) }

现在数字与程序 2 相同(List 获胜!),尽管频率(同样,2 次运行中的 3*1000 数字)在最后略有不同:

  • 49:33%、33%
  • 48:23%、22%
  • 47:16%、15%
  • 46:9%、10%
  • 45:7%、6%

问题

即使样本量不足以(因为它永远都不够)告诉真实随机性,我不禁质疑Gen.pick声称的随机性(就使用它而言开箱即用,我可能需要设置一些种子让它随机“更多”工作),因为数字“卡住”了,而且频率几乎相同。

在查看 Gen.pick's source code 时,在第 #672 行使用了某个 seed0

def pick[T](n: Int, l: Iterable[T]): Gen[Seq[T]] = {
    if (n > l.size || n < 0) throw new IllegalArgumentException(s"invalid choice: $n")
    else if (n == 0) Gen.const(Nil)
    else gen { (p, seed0) =>
    // ...

我在其他任何地方都找不到定义(在Gen.scala source codescala.util.Random 文档中),但我有预感它可能与观察到的行为有关。 这是Gen.pick 的预期行为吗?如果是这样,我怎样才能获得“更多”的随机挑选?

【问题讨论】:

  • Bugfoot,不确定你是否还在乎,但我认为@ashawley 的诊断是错误的,这实际上只是一个错误。有关详细信息,请参阅我的答案
  • 您的回复现在被接受为答案,感谢您加倍努力。

标签: scala random scalacheck


【解决方案1】:

虽然@ashawley 的答案已被接受,但我认为它不正确。我认为这实际上是一个错误,它是由erik-stripe's commit on Sep 1, 2016 引入的,并且该错误实际上是在行

      val i = (x & 0x7fffffff).toInt % n

应该是这样的

      val i = (x & 0x7fffffff).toInt % count

这仍然不太正确。

我还希望最后一个值的 33% 实际上是 100%,而且您没有考虑选择 3 个元素的事实,因此您的所有统计信息都应乘以 3。因此,对于 3 元素选择,最后一个元素的选择率 100%,前一个 - 66.6% 等等,这比您预期的还要糟糕。

这里再次摘录一段代码:

else gen { (p, seed0) =>
  val buf = ArrayBuffer.empty[T]
  val it = l.iterator
  var seed = seed0
  var count = 0
  while (it.hasNext) {
    val t = it.next
    count += 1
    if (count <= n) {
      buf += t
    } else {
      val (x, s) = seed.long
      val i = (x & 0x7fffffff).toInt % n
      if (i < n) buf(i) = t
      seed = s
    }
  }
  r(Some(buf), seed)
}

那么这段代码应该做什么以及它实际上做了什么? if (count &lt;= n) 分支用第一个 n 元素填充输出 buf,之后总是 else 分支工作。为了更清楚,我将while 移动到外部if 更改为以下代码:

  for (i <- 0 until  n) {
    val t = it.next
    buf += t
  }
  while (it.hasNext) {
    val t = it.next
    val (x, s) = seed.long
    val i = (x & 0x7fffffff).toInt % n
    if (i < n) buf(i) = t
    seed = s
  }

所以现在很明显else 分支应该同时决定是否应该将当前元素添加到输出buf 以及应该替换哪个元素。显然,当前代码总是选择每个元素,因为if (i &lt; n) 始终为真,因为i 被计算为something % n。这就是为什么你会看到最后一个元素如此巨大的偏差。

显然计划是使用 Fisher–Yates shuffle 的修改版本,它仅选择随机播放的第一个 n 元素,并且要正确执行此操作,您需要选择 [0, count) 范围内的随机数这可能就是为什么代码以它的编写方式编写的原因,即在while 循环中保留counter

使用% count 仍然不太正确,因为当count 不是2 的幂时,这种简单的方法不会产生均匀分布。更公平地说,类似

    val c0 = choose(0, count-1)
    val rt: R[Int] = c0.doApply(p, seed)        
    seed = rt.seed      
    val i = rt.retrieve.get // index to swap current element with. Should be fair random number in range [0, count-1], see Fisher–Yates shuffle
    if (i < n) buf(i) = t

或者应该使用其他方式来创建i 作为在这样一个范围内的公平均匀分布的随机数。

更新(为什么只是 % count 是错误的)

您可以查看java.util.Random.nextInt(int) 实现或org.scalacheck.Choose.chLng 以了解应该如何完成的示例。它比% count 更复杂,这是有充分理由的。为了说明它,请考虑以下示例。让我们假设您的源随机生成器生成均匀随机的 3 位值,即在 [0, 7] 范围内,并且您希望在范围 [0, 2] 内获得随机数,您只需执行

srcGenerator.nextInt() % 3

现在考虑将[0, 7] 范围内的值映射到[0, 2] 范围内:

  • 0, 3, 6 将映射到 0(即映射 3 个值)
  • 1, 4, 7 将映射到 1(即映射 3 个值)
  • 2, 5 将映射到 2(即仅映射 2 个值)

因此,如果您只使用% 3,您的分布将是 0 - 3/8、1 - 3/8、2 - 2/8,这显然是不均匀的。这就是为什么我之前引用的那些实现使用某种循环并丢弃源生成器生成的一些值的原因。需要生成 unifrom 分布。

【讨论】:

  • 不,事实证明(尽管不是很明显)您不需要知道长度。这称为油藏采样,请参阅 gregable.com/2007/10/reservoir-sampling.htmlen.wikipedia.org/wiki/Reservoir_sampling
  • @ashawley,但这(来自 wiki 的算法 R)正是我所期望的该代码的原始意图以及我最后一段代码所暗示的。
  • @ashawley,好吧,我的最后一段代码是你应该放入 while 循环的 else 分支而不是包含我放入第一段的错误的行的代码。我不确定您还期望什么。
  • @ashawley,它复杂的唯一原因是很难以“Scala Check way”生成random(1, i)(或者更确切地说是random(0, i-1),因为Scala数组是从0开始的)。这就是代码 5 行中的前 4 行所做的(我的 i 是来自 wki 的 j,我的 count 是来自 wiki 的 i,我的 n 是来自 wiki 的 k)!你看过 ScalaCheck gen 实际需要什么吗?
  • @ashawley, no % count 是一种改进,但不是 正确如果你想获得均匀的随机分布,解决方案。请参阅我的更新以了解为什么这是错误的示例。
【解决方案2】:

我认为这与种子无关。它与 Scalacheck 的启发式方法有关。

有一个微妙的错误。考虑它在做什么。它强迫自己在开始时选择值,然后随机覆盖它们:

while (it.hasNext) {
  val t = it.next
  count += 1
  if (count <= n) {
    buf += t
  } else {
    val (x, s) = seed.long
    val i = (x & 0x7fffffff).toInt % n
    if (i < n) buf(i) = t
    seed = s
  }
  ...

它将这些元素随机分配给else-block 中的结果,这就是它优先考虑尾部值的原因。

所以,pick 是从一组中随机选择值。但是,它牺牲了在值之间进行平均选择,并倾向于列表的末尾,因为代码试图懒惰地遍历列表。

要尝试使选取的元素均匀分布,您需要知道集合的长度,但正如我的回答所暗示的,如果不使用两次迭代,这是不可能的。

也许如果你在你的名单上运行reverse,或者shuffle,你会通过pick获得更好的选择分布。

因为 Scalacheck 是一个通用的属性测试库,我预测它不能在不牺牲任意大小集合的性能的情况下完成这两项工作。

更新

但是正如Alexey Romanov 指出的那样,这应该实现reservoir sampling 算法,避免知道长度并且可以在O(n) 时间内运行。代码中只是有一个缺陷。修复只是更正随机数生成的行。它应该得到一个从 1 到列表中访问的第 k 个 (count) 元素的随机数。

val i = (x & 0x7fffffff).toInt % n

应该是:

val i = (x & 0x7fffffff).toInt % count

我已经向 Scalacheck 提交了 PR:

https://github.com/rickynils/scalacheck/pull/333

【讨论】:

  • 您是绝对正确的,这是因为将拣选“推迟”到列表末尾。仍然我不能称最终结果为随机选择...我在 ScalaCheck 的 GitHub (github.com/rickynils/scalacheck/issues/332) 上提出了一个问题,如果你允许的话,引用并归功于你。
  • 你提出了 b-word,你在正确的轨道上,并且给予了信用,但 @SergGr 得到了完整的解释,所以我不得不接受他作为答案。
  • 好吧,我从没想过我应该得到答案。我认为你过早地选择了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-03-08
  • 2015-01-05
  • 1970-01-01
  • 2016-11-07
  • 2017-05-29
相关资源
最近更新 更多