【问题标题】:Random number generator (RNG/PRNG) that returns updated value of seed返回种子更新值的随机数生成器 (RNG/PRNG)
【发布时间】:2018-08-15 13:16:54
【问题描述】:

我正在尝试编写一个 RNG,它还返回更新后的种子的值。这可能是显而易见的原因,以便以后可以将新的随机变量添加到程序中,而无需更改现有 RNG 的值。有关此问题的 python/numpy 版本,请参见例如:Difference between RandomState and seed in numpy

以下是(暂定)建议解决方案的使用示例:

program main

   ! note that I am assuming size=12 for the random 
   ! seed but this is implementation specific and
   ! could be different in your implementation
   ! (you can query this with random_seed(size) btw)

   integer :: iseed1(12) = 123456789
   integer :: iseed2(12) = 987654321

   do i = 1, 2
      x = ran(iseed1)
      y = ran(iseed2)
   end do

end program main

function ran( seed )

   integer, intent(inout) :: seed(12)
   real                   :: ran

   call random_seed( put=seed )
   call random_number( ran )
   call random_seed( get=seed )

end function ran

请注意,此解决方案(以及任何其他解决方案)的一个重要方面是,如果我们在上面添加 seed3x3x1x2。同样,x1x2 可以从代码中删除,而不会影响另一个的值。

如果有帮助,这就是ran()(扩展)函数在 Compaq/HP 和 Intel 编译器上的工作方式,我基本上是在尝试在 gfortran 上实现相同的 API。但是请注意,该函数的种子是一个标量,而它是一个使用 Fortran 90 子例程 random_seed 的一维数组(数组的大小/长度是特定于实现的)。

我将我当前的解决方案作为基准错误提供,但希望其他人可以批评该答案或提供更好的答案。

如果根据标准 PRNG 测试对基准测试结果进行任何分析,尤其是对种子设置方式的分析,我将不胜感激。在基准测试中,我只是对一个非常简单和简洁的数组使用标量广播,并且希望避免显式提供整个整数数组。

所以我想要一个稍微严格的确认这很好,或者是一种更简洁的设置种子的方法(以可重复的方式!)而不是写出一个完整的数组,比如 12 或 33 个整数。例如。也许有一些简单明了的方法可以从单个整数中生成一个漂亮的 12 个伪随机整数流(因为数组长度很短,这可能很容易?)。

编辑添加:关于在 fortran 中设置种子的后续问题:Correctly setting random seeds for repeatability

【问题讨论】:

  • 无论如何,如果想要的结果只是拥有单独的随机数序列,您可以使用为不同线程维护单独序列的并行 PRNG 来做到这一点。但是没有什么会强迫您将这些序列与线程一起使用,您可以以任何方式使用它们。示例:bitbucket.org/LadaF/elmm/src/…
  • @VladimirF 谢谢,这在概念上肯定是一个很好的建议,尽管比我的理想解决方案要复杂一些。我的意思是,我并没有真正提出一个我认为没有的问题,如果没有人认为我的基准答案有什么可怕的错误,那么我至少会在短期内使用它。
  • @JohnE,您应该使用子程序而不是函数。函数返回一个值。它所做的任何其他事情都是副作用,这可能会产生意想不到的后果。
  • @Steve 我想我看不出这与我通过带有intent(inout) 的子程序传递种子有什么不同?还是intent(inout) 现在也被不喜欢了?我想 f90 PRNG 的状态也会作为副作用发生变化,但不会比调用 PRNG 子例程本身时更多或更少。
  • @Steve, etc:Fortran 随机数函数本质上是不纯的,它们在幕后保持状态。正如 OP 所打算的那样,暴露该状态不会使它们更加不纯。事实上,一个接收一个状态并输出更新状态和序列中下一个数字的函数实际上可能是纯函数。

标签: fortran gfortran fortran90 intel-fortran


【解决方案1】:

在我看来,您提出的解决方案应该可行 - 您正在记录生成器的整个状态(通过get),并在必要时在流之间交换(通过put)。不过请注意,我还没有实际测试过您的代码。

这个答案的出现是因为之前的答案(现已删除)只保存了状态数组的第一个元素,并使用它来设置整个状态数组。它看起来像:

integer :: seed_scalar, state_array(12)
...
! -- Generate a random number from a thread that is stored as seed_scalar
state_array(:) = seed_scalar
call random_seed( put=state_array )
call random_number( ran )
call random_seed( get=state_array )
seed_scalar = state_array(1)
! -- discard state_array, store seed_scalar

此答案旨在证明,对于某些编译器(gfortran 4.8 和 8.1,pgfortran 15.10),这种仅通过标量维护单独线程的方法会导致不良的 RNG 行为。

考虑以下代码以原始地测试随机数生成器。生成了许多随机数 - 本示例为 100M - 并以两种方式监控性能。首先,记录随机数增加或减少的次数。其次,生成一个 bin 宽度为 0.01 的直方图。 (这显然是一个原始的测试用例,但事实证明它足以证明这一点。)。最后,还生成了所有概率的估计一西格玛标准差。这使我们能够确定变化是随机的还是具有统计意义的。

program main
   implicit none
   integer, parameter :: wp = selected_real_kind(15,307)
   integer :: N_iterations, state_size, N_histogram
   integer :: count_incr, count_decr, i, loc, fid
   integer, allocatable, dimension(:) :: state1, histogram
   real(wp) :: ran, oldran, p, dp, re, rt

   ! -- Some parameters
   N_iterations = 100000000
   N_histogram = 100

   call random_seed(size = state_size)
   allocate(state1(state_size), histogram(N_histogram))
   write(*,'(a,i3)') '-- State size is: ', state_size

   ! -- Initialize RNG, taken as today's date (also tested with other initial seeds)
   state1(:) = 20180815
   call random_seed(put=state1)

   count_incr = 0
   count_decr = 0
   histogram = 0
   ran = 0.5_wp      ! -- To yield proprer oldran

   ! -- Loop to grab a batch of random numbers
   do i=1,N_iterations
      oldran = ran

      ! -- This preprocessor block modifies the RNG state in a manner
      ! -- consistent with storing only the first value of it
#ifdef ALTER_STATE
      call random_seed(get=state1)
      state1(:) = state1(1)
      call random_seed(put=state1)
#endif

      ! -- Get the random number
      call random_number(ran)

      ! -- Process Histogram
      loc = CEILING(ran*N_histogram)
      histogram(loc) = histogram(loc) + 1

      if (oldran<ran) then
         count_incr = count_incr + 1
      elseif (oldran>ran) then
         count_decr = count_decr + 1
      else
         ! -- This should be statistically impossible
         write(*,*) '** Error, same value?', ran, oldran
         stop 1
      endif
   enddo

   ! -- For this processing, we have:
   !     re    Number of times this event occurred
   !     rt    Total number
   ! -- Probability of event is just re/rt
   ! -- One-sigma standard deviation is sqrt( re*(rt-re)/rt**3 )

   ! -- Write histogram
   ! -- For each bin, compute probability and uncertainty in that probability
   open(newunit=fid, file='histogram.dat', action='write', status='replace')
   write(fid,'(a)') '# i, P(i), dP(i)'

   rt = real(N_iterations,wp)
   do i=1,N_histogram
      re = real(histogram(i),wp)
      p = re/rt
      dp = sqrt(re*(rt-1)/rt**3)

      write(fid,'(i4,2es26.18)') i, p, dp
   enddo
   close(fid)

   ! -- Probability of increase and decrease
   re = real(count_incr,wp)
   p = re/rt
   dp = sqrt(re*(rt-1)/rt**3)
   write(*,'(a,f20.15)') 'Probability of increasing: ', p
   write(*,'(a,f20.15)') '      one-sigma deviation: ', dp

   re = real(count_decr,wp)
   p = re/rt
   dp = sqrt(re*(rt-1)/rt**3)
   write(*,'(a,f20.15)') 'Probability of decreasing: ', p
   write(*,'(a,f20.15)') '      one-sigma deviation: ', dp

   write(*,'(a)') 'complete'

end program main

无需修改

没有预处理器指令ALTER_STATE,我们按预期使用gfortran的内置PRNG,结果符合预期:

enet-mach5% gfortran --version
GNU Fortran (SUSE Linux) 4.8.3 20140627 [gcc-4_8-branch revision 212064]
Copyright (C) 2013 Free Software Foundation, Inc.

GNU Fortran comes with NO WARRANTY, to the extent permitted by law.
You may redistribute copies of GNU Fortran
under the terms of the GNU General Public License.
For more information about these matters, see the file named COPYING

enet-mach5% gfortran -cpp -fcheck=all main.f90 && time ./a.out
-- State size is:  12
Probability of increasing:    0.499970710000000
      one-sigma deviation:    0.000070708606619
Probability of decreasing:    0.500029290000000
      one-sigma deviation:    0.000070712748851
complete

real    0m2.414s
user    0m2.408s
sys     0m0.004s

增加/减少的预期概率是 0.5,两者都具有估计的不确定性(0.49997 小于 0.5 的 0.00007)。用误差线绘制的直方图是

对于每个 bin,与预期概率 (0.01) 的变化很小,通常在估计的不确定性范围内。因为我们生成了很多数字,所以所有的变化都很小(0.1%)。本次测试基本上没有发现任何可疑行为。

有修改

如果我们启用ALTER_STATE 中的块,我们会在每次生成数字时修改随机数生成器的内部状态。这是为了模仿现在已删除的解决方案,该解决方案仅存储状态的第一个值。结果是:

enet-mach5% gfortran -cpp -DALTER_STATE -fcheck=all main.f90 && time ./a.out
-- State size is:  12
Probability of increasing:    0.501831930000000
      one-sigma deviation:    0.000070840096343
Probability of decreasing:    0.498168070000000
      one-sigma deviation:    0.000070581021884
complete

real    0m16.489s
user    0m16.492s
sys     0m0.000s

观察到的增加概率超出预期变化(26 sigma!)。这已经表明出了点问题。直方图为:

请注意,y 的范围发生了很大变化。在这里,我们的变化比前一种情况大约大两个数量级,远远超出了预期的变化。在这里很难看到误差线,因为 y 范围要大得多。如果我的随机数生成器是这样运行的,我会觉得用它做任何事情都不会舒服,甚至抛硬币也不行。

结束

random_seedputget 选项访问随机数生成器的处理器相关内部状态。这通常比单个数字具有更多的熵,并且其形式取决于处理器。不能保证第一个数字完全代表整个州。

如果您要初始化一个随机种子并生成多次,那么使用单个标量就可以了。但是,如果您打算使用该状态来生成每个数字,显然需要存储多个数字。

坦率地说,我有点惊讶这个原始测试能够证明不良行为。 RNG的有效性是一个复杂的主题,我绝不是专家。结果也取决于编译器:

  • 显示的结果和直方图适用于 gfortran 4.8,其状态大小为 12。
  • 英特尔 16.0 使用状态大小为 2,此测试未显示任何不良行为。
  • gfortran 8.1.0 的状态大小为 33,而 PGI 15.10 的状态大小为 34。它们的行为相同,而且是最差的。将整个状态设置为单个值时,后续随机数总是相同。当从单个种子初始化时,这些生成器需要大约 30 代的“启动”才能使数字看起来合理。当在状态中只保存一个种子时,这种启动永远不会发生。

当仅保存单个值时,较大的状态大小可能会导致更多的熵损失,因此它与较差的行为相关。这当然符合我的观察。但是,如果不知道每个生成器的内部结构,就无法分辨。

【讨论】:

  • 您的测试存在缺陷。并非所有种子都是平等的。 state1(1) 可能不参与 prng 的内部声明。初始化生成器,抽取 1 个或多个随机数,然后将旧种子与新状态进行比较。
  • 我认为你所说的正是我想要表达的观点。只处理 state1(1) 是没有意义的。当我有机会时,我会编辑问题,以更清楚地说明是什么促使了答案,因为旧答案已被删除。
  • 谢谢罗斯,这太棒了!如果您愿意,请随时删除关注旧答案的内容,而只关注新答案。在这一点上,旧的答案可能只是令人困惑。如果您认为英特尔 16.0 有趣,也可以随意扩展您的 cmets。
  • 仅供参考,我将示例答案移至问题本身。
  • @Ross,是的,您的结论使我们明白,如果一个人(部分)操纵了 prng 的内部状态,则需要进行护理和测试
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-25
  • 1970-01-01
  • 2013-08-23
  • 1970-01-01
  • 2011-07-02
相关资源
最近更新 更多