使用 Snow/doSNOW 为内部循环重置 R 随机数生成器 (rlecuyer)

Resetting R random number generator (rlecuyer) for inner loops using Snow/doSNOW

我有一个包含内部循环的外部 foreach/dopar 并行循环。内循环的每个实例都应该在同一组随机数上工作。其余部分,即外部主体的其余部分和并行实例应该照常工作,即具有独立的随机数。

我可以在非并行实现中实现这一点,方法是在内部循环开始之前保存 RNG 的状态,并在执行每个内部循环实例后恢复该状态。请参阅以下示例:

library(doSNOW)

seed = 4711

cl = makeCluster(2)
registerDoSNOW(cl)
clusterSetupRNGstream (cl, seed=rep(seed,6))

erg = foreach(irun = 1:3,.combine = rbind) %dopar% {

  #do some random stuff in outer loop
  smp = runif(1)

  # save current state of RNG
  s = .Random.seed

  # inner loop, does some more random stuff
  idx = numeric(5)
  for(ii in seq.int(5)) {
    idx[ii] = sample.int(10, 1)
    # reset RNG for next loop iteration
    set.seed(s)
  }

  c(smp,idx)
}

> print(erg)
              [,1] [,2] [,3] [,4] [,5] [,6]
result.1 0.5749162    7    6    2    3    7
result.2 0.1208910    4    3    6    8    9
result.3 0.3491315    7    2    7    6   10

我想要的输出是每行的常数整数,每行都不同。所以这不能并行工作。原因很明显:snow 使用不同的随机生成器 (rlecuyer) 并且必须处理并行流。

问题是:如何使用随机数生成器 rlecuyer 实现雪中种子的重置?对我来说,挑战之一是确定应该重置种子的正确子流。

我目前的解决方法是为内部循环预先计算所有随机内容(在示例中为 idx 向量)一次,然后在所有内部实例中使用此常量数据。这不是最佳选择,因为随机数据总量变得非常大,最好以较小的块动态(重新)生成它。

编辑

最重要的是 setting/resetting 保持并行流的独立性。据我目前了解的机制,这需要:

对于每个并行进程以及在每个并行进程中:

  1. 检测进程正在处理的相关子流。
  2. 在方便的时候:找到并存储这个子流的随机种子。
  3. 在方便的时候:使用 2) 的种子从 1) 重置子流,并保持所有其他流不受干扰。

您可以尝试使用 set.seed(seed=NULL) 来重置种子,我认为这与选择随机种子基本相同。

library(doSNOW)
library(rlecuyer)
library(doParallel)

cl = makeCluster(2)
registerDoSNOW(cl)

seeds <- c(1,88,99)

erg = foreach(i = 1:3,.combine = rbind) %dopar% {
  set.seed(NULL, kind = "L'Ecuyer-CMRG")
  #do some random stuff in outer loop
  smp = runif(1)
  
  # inner loop, does some more random stuff
  idx = numeric(5)
  for(ii in seq.int(5)) {
    set.seed(seeds[i], kind = "L'Ecuyer-CMRG")
    idx[ii] = sample.int(10, 1)
  }
  
  c(smp,idx)
}
stopCluster(cl)
erg
#>               [,1] [,2] [,3] [,4] [,5] [,6]
#> result.1 0.6653043    3    3    3    3    3
#> result.2 0.8019645    9    9    9    9    9
#> result.3 0.6653043   10   10   10   10   10
Created on 2021-09-20 by the reprex package (v2.0.1)

这是您期待的吗?答案 and here 对我有帮助。

这里有几件事值得一提。 首先,您要保存 .Random.seed,然后将其直接传递给 set.seed, 即使您没有遇到 RNG 问题,这也不会给您想要的结果。 这样做的原因是,正如所记录的那样, set.seedseed 参数只关心一个整数, 所以如果你传递一个向量, 它仍然只会使用第一个值, .Random.seed 的文档指出:

.Random.seed is an integer vector whose first element codes the kind of RNG and normal generator.

所以你所有的并行流都会以相同的整数结束,因为对于相同的 RNG 类型,.Random.seed 的第一个值总是相同的。

其次,文档还指出:

.Random.seed saves the seed set for the uniform random-number generator, at least for the system generators. It does not necessarily save the state of other generators, and in particular does not save the state of the Box–Muller normal generator. If you want to reproduce work later, call set.seed (preferably with explicit values for kind and normal.kind) rather than set .Random.seed.

这意味着如果您要操纵更多的内部值,您绝对应该知道您使用的是什么 RNG。 如果您在 呼叫 clusterSetupRNGstream 之后呼叫 clusterEvalQ(cl, RNGkind()) , 你会看到它 returns:

[1] "user-supplied" "Inversion"     "Rejection"

所以你不能假设 .Random.seed 就足以保存 RNG 的状态。 事实上,我什至不确定它与 doSNOW 是否兼容, 看到这个差异:

clusterSetupRNGstream(cl, seed = rep(seed, 6))
foo = foreach(irun = 1:2, .combine = list) %dopar% {
  list(
    .Random.seed,
    get(".Random.seed", .GlobalEnv)
  )
}
> str(foo)
List of 2
 $ :List of 2
  ..$ : int [1:626] 10403 1 -921191862 -372998484 563067311 -15494811 985677596 1278354290 1696669677 -1382461401 ...
  ..$ : int 10405
 $ :List of 2
  ..$ : int [1:626] 10403 1 -921191862 -372998484 563067311 -15494811 985677596 1278354290 1696669677 -1382461401 ...
  ..$ : int 10405

最后,从您的示例中可以清楚地看出 set.seed 在这种情况下并没有真正发挥作用。 clusterSetupRNGstream 的文档说明使用了 rlecuyer 包, 而且我不知道关于那个包的足够细节来说明它是否支持 set.seed 或不, 但我想它不会。

我能给你的唯一选择是我以前用过的那个有点冗长:

RNGkind("L'Ecuyer-CMRG")

cl = makeCluster(2)
registerDoSNOW(cl)

seed = 4711L
set.seed(seed)
# x is a vector of length irun - 1
seeds = Reduce(x = 1:2, init = .Random.seed, accumulate = TRUE, f = function(x, ignored) {
  parallel::nextRNGStream(x)
})

erg = foreach(pseed = seeds, .combine = rbind) %dopar% {
  RNGkind("L'Ecuyer-CMRG")
  assign(".Random.seed", pseed, .GlobalEnv)

  # do some random stuff in outer loop
  smp = runif(1)
  
  # save current state of RNG
  s = get(".Random.seed", .GlobalEnv)
  
  # inner loop, does some more random stuff
  idx = numeric(5)
  for(ii in seq.int(5)) {
    idx[ii] = sample.int(10, 1)
    # reset RNG for next loop iteration
    assign(".Random.seed", s, .GlobalEnv)
  }
  
  c(smp, idx)
}

> print(erg)
              [,1] [,2] [,3] [,4] [,5] [,6]
result.1 0.9482966    2    2    2    2    2
result.2 0.1749918    3    3    3    3    3
result.3 0.3263343    1    1    1    1    1

AFAIK,R 自己的 L'Ecuyer-CMRG 的状态确实保存在 .Random.seed