【问题标题】:Resetting R random number generator (rlecuyer) for inner loops using Snow/doSNOW使用 Snow/doSNOW 为内部循环重置 R 随机数生成器 (rlecuyer)
【发布时间】:2021-11-11 12:45:55
【问题描述】:

我有一个外部 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 向量),然后在所有内部实例中使用此常量数据。这不是最优的,因为随机数据总体上变得非常大,并且以较小的块动态(重新)生成它要好得多。

编辑

任何设置/重置都保持并行流独立是最重要的。据我目前了解这需要的机制:

在每个并行进程内:

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

【问题讨论】:

    标签: r random parallel-processing montecarlo snow


    【解决方案1】:

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

    .Random.seed 是一个整数向量,其第一个元素编码 RNG 和法线生成器的种类。

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

    其次,文档还指出:

    .Random.seed 保存统一随机数生成器的种子集,至少对于系统生成器。它不一定保存其他生成器的状态,特别是不保存 Box-Muller 正常生成器的状态。如果您想稍后重现工作,请调用 set.seed(最好使用明确的 kind 和 normal.kind 值)而不是 set .Random.seed。

    这意味着如果你要操纵更多的内部值,你肯定应该知道你使用的是什么 RNG。 如果您在致电clusterSetupRNGstream 之后致电clusterEvalQ(cl, RNGkind()), 你会看到它返回:

    [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 举行。

    【讨论】:

    • 感谢您花时间回答我的问题。指出我使用“set.seed(.Random.Seed)”的方式的问题非常有帮助。在您的解决方案中,您在全局环境中设置和重置一个种子。我不明白这如何适用于(两个)并行子流。最重要的是不要在子流之间创建任何相关性/依赖关系。我实际上查看了“parallel::RNGstreams”文档,但它根本没有解释这一点。他们的例子甚至没有使用并行循环。
    • 嗯,每个并行进程都有自己的(即本地)全局环境吗?
    • @gg 是的,我相信每个进程都有自己的,尽管我不确定在使用 FORKed 进程时是否也是这种情况。
    【解决方案2】:

    您可以尝试使用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)
    

    这是您所期待的吗?答案here 和here 帮助了我。

    【讨论】:

    • 对不起,但这不是我想要实现的。这为每个流提供了相同的随机种子。我想在每个随机流中重置种子。正如我在问题中所说:每行中的整数相等(!)。
    • 好的,误会了。您期望每行中的整数相等(即不包括第 1 列)和行之间的不同整数?
    • 我更新了我的答案。请让我知道你在想什么。谢谢!
    猜你喜欢
    • 2013-10-03
    • 2014-02-14
    • 1970-01-01
    • 1970-01-01
    • 2019-08-04
    • 1970-01-01
    • 2021-04-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多