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非常相似,因此用户可以在不进行任何额外更改的情况下受益于这些改进
[1] 。
原则上,这一变化将影响使用由 np_random_state
或 py_random_state
修饰的任何函数的NetworkX用户(当 random_state
参数涉及 numpy
时)。
有关详细信息,请参阅下一节。
使用和影响#
在NetworkX中,随机数生成器通常通过装饰器创建:
from networkx.utils import np_random_state
@np_random_state("seed") # 或者可以是参数位置,例如 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
实例的唯一方法是直接传递实例:
```plaintext
```
这个NXEP提议改变行为,使得当例如整数或 None
被用作 seed
参数时,将返回一个 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
将意味着使用值*而不是已实例化的rng*作为种子时,使用 np_random_state
装饰的函数会产生不同的结果。
例如,让我们看下面的函数:
@np_random_state("seed")
def bar(num, seed=None):
"""返回一个包含 `num` 个均匀随机数的数组。"""
return seed.random(num)
在当前的 np_random_state
实现中,用户可以向 seed
传递一个整数值,该值将用于生成一个新的 RandomState
实例。
使用相同的种子值保证输出始终完全可重现:
>>> 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
实例后,对于整数种子,装饰的 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
之间提供了兼容性层。
通过切换到` numpy.random.Generator ,这将不再可能,因为
numpy.random 模块中没有全局的内部
Generator`实例。
这对用户应该没有影响,因为 seed=None
当前不保证可重现的结果。
详细描述#
本NXEP建议将由 create_random_state
函数(及相关装饰器 np_random_state
)生成的默认随机数生成器从 numpy.random.RandomState
实例更改为 numpy.random.Generator
实例,当函数的输入为整数或 None
时。
相关工作#
Scikit-learn对依赖随机性的函数施加确定性有类似的模式。
例如, scikit-learn
中的许多函数都有一个 random_state
参数,其功能类似于许多NetworkX函数签名中 seed
的行为。
scikit-learn
和networkx
之间的一个区别是,scikit-learn仅通过random_state
关键字参数支持RandomState
,而NetworkX隐式支持内置的random
模块,以及numpy的RandomState
和Generator
实例(取决于seed
的类型)。
这反映在关键字参数的名称上,因为 random_state
(由scikit-learn使用)比 seed
(由NetworkX使用)更不含糊。
scikit-learn社区中有多个关于支持新的NumPy随机接口的潜在方法的相关讨论:
scikit-learn/scikit-learn#16988 讨论了启用用户使用基于
Generator
的随机数生成器的策略和相关问题。scikit-learn/scikit-learn#14042 是一个更高级别的讨论,包含了关于scikit-learn的
random_state
设计考虑和约束的额外信息。还有一个相关的 SLEP 。
实现#
实现本身非常简单。确定输入如何映射到随机数生成器的逻辑封装在 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} 无法用于创建 numpy.random.RandomState 或\n"
"numpy.random.Generator 实例"
)
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} 无法用于创建 numpy.random.RandomState 或\n"
"numpy.random.Generator 实例"
)
raise ValueError(msg)
上述内容捕捉了逻辑上的基本更改,尽管实现细节可能有所不同。 与实现此更改相关的大部分工作将涉及改进/重新组织测试;包括添加测试以实现rng流的可重现性。
替代方案#
- 现状,即默认情况下使用
RandomState
,是一个完全可接受的替代方案。 RandomState
没有被弃用,并且预计将永久保持其流兼容性保证。
另一个可能的替代方案是提供一个包级别的切换,用户可以使用该切换来切换由 np_random_state
或 py_random_state
修饰的所有函数的 seed
kwarg的行为。
为了说明(忽略实现细节):
```python >>> import networkx as nx >>> from networkx.utils.misc import create_random_state
# NetworkX 2.X 行为:默认情况下为 RandomState
>>> type(create_random_state(12345))
numpy.random.mtrand.RandomState
# 通过设置 pkg 属性更改随机后端
>>> 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
实例,其中流兼容性不是首要考虑因素。