NumPy PRNG进阶:非均匀分布下advance()失效问题及解决咨询
我想借助NumPy的随机数生成功能从不同分布中抽取随机数,要求在给定生成器、种子和分布的前提下,直接获取序列中的第i个随机数,无需先生成前i-1个数。PCG64、PCG64DXSM和Philox这三种比特生成器都提供advance()方法,看起来能满足需求。
以PCG64DXSM为例,均匀分布下的测试代码运行正常:
seed = 42 bit_generator = np.random.PCG64DXSM(seed) generator = np.random.Generator(bit_generator) # 生成20个0到1之间的均匀分布随机数 n = 20 nums = [generator.uniform(0, 1) for _ in range(n)] # 直接获取最后一个数 bit_generator = np.random.PCG64DXSM(seed) generator = np.random.Generator(bit_generator) bit_generator.advance(n - 1) num = generator.uniform(0, 1) assert num == nums[-1]
但在正态分布、瑞利分布等非均匀分布中,advance()方法普遍失效。以下是展示失效情况的测试代码:
import numpy as np import matplotlib.pyplot as plt def plot(ax, rand, n=20): BitGenerator = [np.random.PCG64DXSM, np.random.PCG64, np.random.Philox][0] seed = 42 bit_generator = BitGenerator(seed) generator = np.random.Generator(bit_generator) x = [rand(generator) for _ in range(n)] ax.plot(x, 'C0-') for i in range(n): bit_generator = BitGenerator(seed) generator = np.random.Generator(bit_generator) bit_generator.advance(i) x = rand(generator) ax.plot(i, x, 'C1.') fig, axes = plt.subplots(3) plot(axes[0], lambda generator: generator.uniform(0, 1)) plot(axes[1], lambda generator: generator.normal(0, 1)) plot(axes[2], lambda generator: generator.rayleigh(1)) plt.show()
生成的图表中,三个面板分别对应均匀、正态和瑞利分布:蓝色线条是单个生成器连续生成的随机序列,橙色点是通过全新生成器调用advance()单独获取的随机数。advance()方法在正态和瑞利分布中运行一段时间后失效,失效时机取决于种子和使用的比特生成器。
现提出以下问题:
- 为何
advance()方法在均匀分布中表现完美,但在其他分布中普遍失效?- 为何该方法在每个序列的前几个随机数中可以正常工作?
- 这是否是预期/已知行为?
- 如何解决该问题?
1. 失效原因分析
均匀与非均匀分布的核心差异
均匀分布的uniform()方法每次只消耗1个原始随机数(比特生成器输出的无符号整数),所以调用advance(i)后,正好跳到第i+1个原始随机数的位置,生成结果和连续调用i次uniform()完全一致。
而非均匀分布(如正态分布的normal()、瑞利分布的rayleigh())依赖转换算法生成样本:
- 比如生成正态分布的Box-Muller变换,需要2个均匀随机数来生成2个正态数,第一次调用会消耗2个原始随机数,生成1个样本并缓存另一个;第二次调用直接用缓存的结果,不消耗新的原始随机数。
- 前几次调用时,缓存机制还未启动,或消耗的原始随机数步数刚好和
advance(i)的步数匹配,所以结果一致;但缓存形成后,advance(i)的步数和实际消耗的原始随机数步数不再对应,就会出现结果不匹配的情况。
2. 是否为预期行为?
这是预期且有文档说明的行为。比特生成器的advance()仅控制原始随机数的生成位置,而高层分布函数会根据算法需求消耗不同数量的原始随机数,甚至缓存中间结果,所以advance()无法直接对应到高层分布的第i个样本。
3. 解决方案
方案1:使用Generator.jumped()方法(推荐)
NumPy的Generator类提供的jumped()方法,是基于高层分布样本计数的跳转,不管该分布消耗多少原始随机数,都能精准定位到第i个样本:
seed = 42 # 连续生成正态分布序列 bit_gen = np.random.PCG64DXSM(seed) gen = np.random.Generator(bit_gen) normal_seq = [gen.normal() for _ in range(20)] # 直接获取第10个样本(索引从0开始) bit_gen2 = np.random.PCG64DXSM(seed) gen2 = np.random.Generator(bit_gen2) gen2 = gen2.jumped(10) assert gen2.normal() == normal_seq[10]
方案2:预生成缓存样本
如果样本规模不大,可以预先生成所有需要的序列并缓存,后续直接通过索引访问,这种方法简单直观,适合小规模场景。
方案3:手动跟踪原始随机数消耗(不推荐)
若必须使用advance(),需要深入对应分布的底层实现,明确每个样本消耗的原始随机数数量(含缓存逻辑),再计算总步数调用advance()。但这种方法维护成本极高,仅适用于特殊场景。
内容的提问来源于stack exchange,提问作者jmd_dk

