ScalaCheck的Gen.pick真的是随机的吗?

时间:2017-05-08 04:52:35

标签: scala random scalacheck

我在使用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个数字中,我得到了一个惊人的相似,非常随机的分布(数字是四舍五入的,只列出了前五名,从这里开始的所有列表):

  • 数字:运行#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

为了稍微调整一下,我运行了第三个小片段,从Set(程序2)创建了List(程序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的预期行为吗?如果是这样,我怎样才能“更多”随机挑选?

1 个答案:

答案 0 :(得分:5)

虽然@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 <= n)分支使用第一个buf元素填充输出n,之后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 < n)始终为真,因为i计算为something % n。这就是为什么你看到最后一个元素如此巨大的倾斜。

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

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

count

或使用其他方式创建 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错误)

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

[0, 2]

现在考虑将范围srcGenerator.nextInt() % 3 中的值映射到您的范围[0, 7]

  • [0, 2]将映射到0, 3, 6(即映射了3个值)
  • 0将映射到1, 4, 7(即映射了3个值)
  • 1将映射到2, 5(即只映射了2个值)

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