当报告的代理集的大小从 2 变为 3 时,为什么 n-of 显示不连续?

Why does n-of show a discontinuity when the size of the reported agentset goes from 2 to 3?

n-of 记者是随机选择的记者之一,所以我们知道,如果我们使用相同的 random-seed,我们将始终从 n-of 中获得相同的代理集。

n-of takes two arguments: size and agentset(它也可以使用列表,但稍后会对此进行说明)。我希望它通过抛出一个伪随机数来工作,使用这个数字从 agentset 中选择一个代理,然后重复这个过程 大小次.

如果这是真的,我们会期望,如果我们在相同的 agentset 上测试 n-of 并使用相同的random-seed,但每次将 size 增加 1,每个生成的 agentset 都将与之前的提取相同,再加上一个进一步的 agent .毕竟,用于挑选第一个(size - 1)个代理的伪随机数序列与之前相同

这似乎得到了普遍证实。下面的代码突出显示了相同的补丁加上每次 size 增加时的另一个补丁,如图所示:

to highlight-patches [n]
  clear-all
  random-seed 123
  
  resize-world -6 6 -6 6

  ask n-of n patches [
    set pcolor yellow
  ]
  
  ask patch 0 0 [
    set plabel word "n = " n
  ]
end

但有一个例外:当size从2变为3时,情况不会发生。如下图所示, n-of 似乎遵循从 1 的 size 开始时的通常行为,但是当 size 达到 3(成为上图的代理集 - 据我所知,它不再改变):

n-of 的幕后发生了什么,导致了这个看似无法解释的阈值的变化?

特别是,这似乎仅适用于 n-of。事实上,使用 repeatone-of 的组合并没有显示这种不连续性(或者至少据我所知):

to highlight-patches-with-repeat [n]
  clear-all
  random-seed 123

  resize-world -6 6 -6 6
  
  repeat n [
    ask one-of patches [
      set pcolor yellow
    ]
  ]
  
  ask patch 0 0 [
    set plabel word "n = " n
  ]
end

请注意,此比较不受 n-of 保证不存在重复而 repeat + one-of 可能有重复这一事实的影响(在我上面的示例中,第一次重复发生当 size 达到 13 时)。相关方面只是 size x 的报告代理集与 x 的报告代理集一致=77=]大小 x + 1.


关于在列表上使用 n-of 而不是代理集

对列表执行相同的操作会导致提取的数字总是不同,即额外提取的数字不等于之前提取的数字加上一个数字。如果提取总是基于相同的伪随机数序列,从期望总是从列表中提取相同的项目的角度来看,这在我看来是一种违反直觉的行为,至少它看起来会发生始终如一,因此在我看来,它不像在代理集的情况下那样模棱两可。

让我们看看它是如何协同工作的。让我们从检查原始实现本身开始。 It lives here。以下是为简洁起见删除了错误处理和注释的相关部分:

    if (obj instanceof LogoList) {
      LogoList list = (LogoList) obj;
      if (n == list.size()) {
        return list;
      }
      return list.randomSubset(n, context.job.random);
    } else if (obj instanceof AgentSet) {
      AgentSet agents = (AgentSet) obj;
      int count = agents.count();
      return agents.randomSubset(n, count, context.job.random);
    }

所以我们需要研究 randomSubset() 列表和代理集的实现。我将从代理集开始。

implementation lives here。以及相关位:

    val array: Array[Agent] =
      resultSize match {
        case 0 =>
          Array()
        case 1 =>
          Array(randomOne(precomputedCount, rng.nextInt(precomputedCount)))
        case 2 =>
          val (smallRan, bigRan) = {
            val r1 = rng.nextInt(precomputedCount)
            val r2 = rng.nextInt(precomputedCount - 1)
            if (r2 >= r1) (r1, r2 + 1) else (r2, r1)
          }
          randomTwo(precomputedCount, smallRan, bigRan)
        case _ =>
          randomSubsetGeneral(resultSize, precomputedCount, rng)
      }

好了。我们可以看到当resultSize2时有一个特例。它 auto-generates 2 个随机数,并翻转它们以确保它们不会“溢出”可能的选择。 the randomTwo() implementation 上的评论阐明这是作为优化完成的。 1 也有类似的特例,但那只是 one-of.

好的,现在让我们检查列表。看起来像 it's implementation of randomSubset() lives over here。这是代码片段:

  def randomSubset(n: Int, rng: Random): LogoList = {
    val builder = new VectorBuilder[AnyRef]
    var i = 0
    var j = 0
    while (j < n && i < size) {
      if (rng.nextInt(size - i) < n - j) {
        builder += this(i)
        j += 1
      }
      i += 1
    }
    LogoList.fromVector(builder.result)
  }

代码有点迟钝,但对于列表中的每个元素,它会随机将其添加​​到结果子集中或不添加。如果不添加早期项目,则后期项目的几率会上升(如果需要,可达到 100%)。因此,更改列表的整体大小会更改将在序列中生成的数字:rng.nextInt(size - i)。这可以解释为什么在使用相同种子但列表更大时看不到按顺序选择的相同项目。

详细说明

好的,让我们详细介绍一下代理集的n = 2优化。为了解释这一点,我们必须知道一些事情:

  1. non-optimized 代码有什么作用?

non-optimized agentset code 看起来很像我已经讨论过的列表代码——它迭代代理集中的每个项目并随机决定是否将其添加到结果中:

val iter = iterator
var i, j = 0
while (j < resultSize) {
  val next = iter.next()
  if (random.nextInt(precomputedCount - i) < resultSize - j) {
    result(j) = next
    j += 1
  }
  i += 1
}

请注意,对于代理集中的每个项目,此代码将执行几个算术运算,precomputedCount - iresultSize - j 以及最终的 < 比较和增量ji 以及 j < resultSize 检查 while 循环。它还为每个检查的元素生成一个随机数(一个昂贵的操作)并调用 next() 来向前移动我们的代理迭代器。如果它在处理代理集中的所有元素之前填充结果集,它将“提前”终止并保存一些工作,但在最坏的情况下,它可能会在风时对代理集中的每个元素执行所有这些操作需要最后一个代理来完全“填充”结果。

  1. 优化后的代码有什么作用,为什么更好?

所以现在让我们检查一下 the optimized code n = 2 code:

if (!kind.mortal)
  Array(
    array(smallRandom),
    array(bigRandom))
else {
  val it = iterator
  var i = 0
  // skip to the first random place
  while(i < smallRandom) {
    it.next()
    i += 1
  }
  val first = it.next()
  i += 1
  while (i < bigRandom) {
    it.next()
    i += 1
  }
  val second = it.next()
  Array(first, second)
}

首先,开始时对kind.mortal的检查基本上是检查这是否是补丁代理集。补丁永远不会 die,因此可以安全地假设代理集中的所有代理都还活着,并且您可以 return 在提供的两个随机数的支持数组中找到的代理作为结果。

继续第二位。这里我们必须使用 iterator 从集合中获取代理,因为其中一些可能已经死了(海龟或链接)。当我们调用 next() 获取下一个代理时,迭代器将为我们跳过这些。您会看到此处的操作正在执行 while 检查,因为它将 i 增加到所需的随机数。所以这里的工作是索引器 i 的增量,以及 while() 循环的检查。我们还必须调用 next() 来向前移动迭代器。这是有效的,因为我们知道 smallRandom 小于 bigRandom - 我们只是跳过代理并挑选出我们想要的。

与 non-optimized 版本相比,我们避免了生成许多随机数,我们避免了使用额外的变量来跟踪结果集计数,我们避免了数学和 less-than 检查确定结果集中的成员身份。这还不错(尤其是 RNG 操作)。

会有什么影响?好吧,如果你有一个很大的代理集,比如 1000 个代理,而你选择了其中的 2 个,那么选择任何一个代理的几率都很小(实际上从 1/1000 开始)。这意味着您将 运行 编写所有代码很长时间才能获得 2 个生成的代理。

那么为什么不针对 n-of 3、4、5 等进行优化呢?好吧,让我们回过头来看看运行优化版的代码:

case 2 =>
  val (smallRan, bigRan) = {
    val r1 = rng.nextInt(precomputedCount)
    val r2 = rng.nextInt(precomputedCount - 1)
    if (r2 >= r1) (r1, r2 + 1) else (r2, r1)
  }
  randomTwo(precomputedCount, smallRan, bigRan)

末尾的小逻辑 if (r2 >= r1) (r1, r2 + 1) else (r2, r1) 确保 smallRan < bigRan;即严格小于,不等于。当您需要生成 3、4 或 5 个以上的随机数时,该逻辑会变得更加复杂。 None个可以相同,而且都要有顺序。有一些方法可以快速对可能有效的数字列表进行排序,但是生成不重复的随机数要困难得多。