NXEP 4 — 默认随机接口#
- 作者:
Ross Barnowski (rossbar@berkeley.edu)
- 状态:
草稿
- 类型:
标准路径
- 创建日期:
2022-02-24
摘要#
伪随机数在 NetworkX 中的许多图和网络分析算法中起着重要作用。NetworkX 提供了一个随机数生成器的标准接口,其中包括对numpy.random
和 Python 内置的random
模块的支持。numpy.random
在 NetworkX 中广泛使用,并且在某些情况下是随机数生成的首选包。NumPy 在 NumPy 1.17 版本中的numpy.random
包中引入了一个新接口。根据NEP19,基于numpy.random.Generator
的新接口优先于旧版numpy.random.RandomState
,因为它具有更好的统计特性、更多功能和改进的性能。本 NXEP 提出了一种采用numpy.random.Generator
作为 NetworkX 中随机数生成的**默认**接口的策略。
动机与范围#
将numpy.random.Generator
作为 NetworkX 中默认的随机数生成引擎的主要动机是让用户受益于numpy.random.Generator
的改进,包括: - 现代伪随机数生成器统计质量的提升 - 性能改进 - 额外功能
numpy.random.Generator
API 与numpy.random.RandomState
API 非常相似,因此用户无需对其现有 NetworkX 代码进行任何额外更改[1]即可受益于这些改进。
原则上,这一变化将影响使用由np_random_state
或py_random_state
装饰的任何函数的 NetworkX 用户(当 random_state
参数涉及 numpy
时)。详见下一节。
用法与影响#
在 NetworkX 中,随机数生成器通常通过装饰器创建
from networkx.utils import np_random_state
@np_random_state("seed") # Or could be the arg position, i.e. 0
def foo(seed=None):
return seed
装饰器负责将各种不同的输入映射到函数内的随机数生成器实例。目前,返回的随机数生成器实例是一个numpy.random.RandomState
对象
>>> type(foo(None))
numpy.random.mtrand.RandomState
>>> type(foo(12345))
numpy.random.mtrand.RandomState
从随机状态装饰器中获取numpy.random.Generator
实例的唯一方法是直接传入该实例
>>> import numpy as np
>>> rng = np.random.default_rng()
>>> type(foo(rng))
numpy.random._generator.Generator
本 NXEP 建议更改行为,使得例如当为 seed
参数给定整数或None
时,取而代之返回一个numpy.random.Generator
实例,即
>>> type(foo(None))
numpy.random._generator.Generator
>>> type(foo(12345))
numpy.random._generator.Generator
numpy.random.RandomState
实例仍可用作 seed
,但必须显式传入
>>> rs = np.random.RandomState(12345)
>>> type(foo(rs))
numpy.random.mtrand.RandomState
向后兼容性#
主要有三个方面的担忧
Generator
接口与RandomState
不兼容流,因此Generator
方法的结果将与对应的RandomState
方法结果不完全相同。RandomState
和Generator
API 之间的方法名称和可用性存在一些细微差异。numpy.random
内部没有全局的Generator
实例,不像numpy.random.RandomState
那样。
numpy.random.Generator
接口打破了numpy.random.RandomState
维持的精确值可重现性的流兼容性保证。将默认随机数生成器从 RandomState
切换到 Generator
意味着由 np_random_state
装饰的函数在使用除已实例化 rng 之外的值作为 seed 时,将产生不同的结果。例如,以以下函数为例
@np_random_state("seed")
def bar(num, seed=None):
"""Return an array of `num` uniform random numbers."""
return seed.random(num)
在 np_random_state
的当前实现中,用户可以向 seed
传入一个整数值,该值将用于为一个新的 RandomState
实例设置种子。使用相同的 seed 值保证输出始终精确可重现
>>> bar(10, seed=12345)
array([0.92961609, 0.31637555, 0.18391881, 0.20456028, 0.56772503,
0.5955447 , 0.96451452, 0.6531771 , 0.74890664, 0.65356987])
>>> bar(10, seed=12345)
array([0.92961609, 0.31637555, 0.18391881, 0.20456028, 0.56772503,
0.5955447 , 0.96451452, 0.6531771 , 0.74890664, 0.65356987])
然而,在将由 np_random_state
返回的默认 rng 更改为 Generator
实例后,对于整数 seed,由装饰函数 bar
产生的数值将不再相同
>>> bar(10, seed=12345)
array([0.22733602, 0.31675834, 0.79736546, 0.67625467, 0.39110955,
0.33281393, 0.59830875, 0.18673419, 0.67275604, 0.94180287])
为了恢复原始结果的精确可重现性,需要显式创建并传入已设置种子的 RandomState
实例通过 seed
参数
>>> import numpy as np
>>> rng = np.random.RandomState(12345)
>>> bar(10, seed=rng)
array([0.92961609, 0.31637555, 0.18391881, 0.20456028, 0.56772503,
0.5955447 , 0.96451452, 0.6531771 , 0.74890664, 0.65356987])
由于流将不再兼容,本 NXEP 建议仅在主要版本发布时考虑切换默认随机数生成器,例如从 NetworkX 2.X 过渡到 NetworkX 3.0。
第二点担忧仅影响在其自己的库中使用create_random_state
以及对应装饰器np_random_state
的用户。例如,numpy.random.RandomState.randint
方法已被numpy.random.Generator.integers
替换。因此,任何使用 create_random_state
或 create_py_random_state
并依赖于返回的 rng 的 randint
方法的代码将导致 AttributeError
。这可以通过类似于 networkx.utils.misc.PythonRandomInterface
类的兼容类解决,它提供了random
和numpy.random.RandomState
之间的兼容层。
create_random_state
当前在输入为None
或 numpy.random
模块时返回全局 numpy.random.mtrand._rand
RandomState
实例。通过切换到numpy.random.Generator
,这将不再可能,因为在numpy.random
模块中没有全局的内部 Generator
实例。这对用户应该没有影响,因为当前的 seed=None
并不能保证可重现的结果。
详细描述#
本 NXEP 建议更改由create_random_state
函数(以及相关的装饰器np_random_state
)生成的默认随机数生成器,当输入为整数或None
时,从numpy.random.RandomState
实例更改为numpy.random.Generator
实例。
实现#
实现本身相当简单。决定输入如何映射到随机数生成器的逻辑封装在create_random_state
函数(以及相关的create_py_random_state
)中。目前(即 NetworkX <= 2.X),此函数将 None
、numpy.random
以及整数等输入映射到 RandomState
实例
def create_random_state(random_state=None):
if random_state is None or random_state is np.random:
return np.random.mtrand._rand
if isinstance(random_state, np.random.RandomState):
return random_state
if isinstance(random_state, int):
return np.random.RandomState(random_state)
if isinstance(random_state, np.random.Generator):
return random_state
msg = (
f"{random_state} cannot be used to create a numpy.random.RandomState or\n"
"numpy.random.Generator instance"
)
raise ValueError(msg)
本 NXEP 建议修改该函数,以便为这些输入生成 Generator
实例。一个示例实现可能如下所示
def create_random_state(random_state=None):
if random_state is None or random_state is np.random:
return np.random.default_rng()
if isinstance(random_state, (np.random.RandomState, np.random.Generator)):
return random_state
if isinstance(random_state, int):
return np.random.default_rng(random_state)
msg = (
f"{random_state} cannot be used to create a numpy.random.RandomState or\n"
"numpy.random.Generator instance"
)
raise ValueError(msg)
上述代码捕获了逻辑的核心变化,尽管实现细节可能有所不同。实施此更改的大部分工作将与改进/重组测试相关;包括添加 rng 流可重现性测试。
替代方案#
现状,即默认使用 RandomState
,是完全可接受的替代方案。RandomState
并未弃用,预计将永久保持其流兼容性保证。
另一种可能的替代方案是提供包级别的开关,用户可以使用它来切换 seed
关键字参数的行为,对于所有由 np_random_state
或 py_random_state
装饰的函数。举例说明(忽略实现细节)
>>> import networkx as nx
>>> from networkx.utils.misc import create_random_state
# NetworkX 2.X behavior: RandomState by default
>>> type(create_random_state(12345))
numpy.random.mtrand.RandomState
# Change random backend by setting pkg attr
>>> nx._random_backend = "Generator"
>>> type(create_random_state(12345))
numpy.random._generator.Generator
讨论#
本 NXEP 已在多次社区会议上讨论过,例如参见这些会议纪要。
这些讨论中出现的主要担忧是 NumPy Generator
接口不提供与旧版 RandomState
相同的严格流兼容性保证。因此,如果按照建议实施本 NXEP,依赖于设置种子的随机数的代码原则上可能会在未来的某个 NumPy 版本中由于默认的 BitGenerator
或 Generator
方法的更改而返回不同的结果。
许多 NetworkX 函数对随机种子相当敏感。例如,更改默认 spring_layout
函数的种子可以产生一个截然不同(但同样有效)的网络布局。流兼容性对于在这些场景中的可重现性非常重要。
因此,我们通过各种讨论得出结论:**不**实施本 NXEP 中提出的更改。RandomState
将继续是 random_state
装饰器的默认随机数生成器,旨在支持所有依赖于 random_state
的 NetworkX 用户代码的严格向后兼容性。Generator
接口在 random_state
装饰器中**受支持**,鼓励用户在流兼容性不是优先事项的新代码中使用 Generator
实例。