NEP 19 — 随机数生成器策略#

作者

罗伯特·科恩<罗伯特.克恩@gmail ​ com >

地位

最终的

类型

标准轨道

创建

2018-05-24

更新

2019-05-21

解决

https://mail.python.org/pipermail/numpy-discussion/2018-July/078380.html

抽象的

在过go的十年中,NumPy 对其所有随机数分布的数字流都有严格的向后兼容性政策。与 中的其他数值组件不同numpy,如果结果保持正确,修改时通常允许返回不同的结果,我们要求随机数分布在每个版本中始终产生完全相同的数字。我们的流兼容性保证的目标是为跨 numpy 版本的模拟提供精确的可重复性,以促进可重复的研究。然而,这一政策使得用更快或更准确的算法增强任何分布变得非常困难。经过十年的经验和周围科学软件生态系统的改进,我们相信现在有更好的方法来实现这些目标。我们建议放宽严格的流兼容性政策,以消除接受对随机数生成功能的贡献的障碍。

现状#

我们当前的完整政策:

固定种子和对使用相同参数的方法的一系列固定调用RandomState将始终产生相同的结果(直到舍入误差),除非值不正确。不正确的值将被修复,并且修复后的 NumPy 版本将在相关文档字符串中注明。只要先前的行为保持不变,就允许扩展现有参数范围并添加新参数。

这项政策于 2008 年 11 月首次实施(实质上;随着时间的推移,全套狡猾的词汇不断增长),以回应用户希望确保构成其科学出版物基础的模拟可以在几年后准确地再现,无论numpy当时的版本如何。我们热衷于支持可重复的研究,而且它还处于早期阶段numpy.random。我们没有看到太多理由go改变分配方法。

我们也没有非常彻底地考虑过我们真正可以承诺的限制(老实说,本节中的“我们”实际上是指罗伯特·科恩)。尽管有这么多狡猾的言论,我们的政策还是过度承诺了兼容性。在不同平台上构建的相同版本,或者只是以不同的方式构建的相同版本numpy可能会导致流发生变化,并且具有不同程度的稀有性。最大的就是.multivariate_normal()方法依赖于 numpy.linalg函数。即使在同一个平台上,如果链接numpy 不同的LAPACK,.multivariate_normal()很可能会返回完全不同的结果。更罕见的是,在不同的操作系统或 CPU 上进行构建可能会导致流出现差异。我们在内部使用 Clong整数进行整数分配(当时这似乎是一个好主意),并且这些整数的大小可能因平台而异。分发方法可能会在不同的断点处溢出其内部 C,longs具体取决于平台,并导致随后的所有随机变量绘制都不同。

即使所有这些都受到控制,我们的政策仍然无法跨版本提供确切的保证。当正确性受到威胁时,我们仍然会应用错误修复。即使我们不这样做,任何重要的程序都不仅仅是绘制随机数。他们对这些数字进行计算,用数值算法将那些数字从其他数字中转换出来numpy,而其他数字则不受如此严格的政策约束。由于这些原因,尝试保持随机数分布的流兼容性无助于可重复的研究。

现在,逐位可重复研究的标准做法是固定软件堆栈的所有代码版本,可能具体到操作系统本身。今天实现这一目标比 2008 年要容易得多。我们现在拥有了pip。我们现在有了虚拟机。那些现在需要准确重现模拟的人可以(并且应该)使用完全相同的numpy.我们不需要维护跨numpy版本的流兼容性来帮助他们。

我们的流兼容性保证阻碍了我们改进numpy.random.一些首次贡献者已提交 PR 来改进分发,通常是通过实施比当前算法更快或更准确的算法。不幸的是,他们中的大多数人都需要中断流才能做到这一点。由于我们的政策的阻碍,以及我们无法解决该政策,许多贡献者干脆走开了。

执行

randomgen项目已经在进行拟议的新伪随机数生成器 (PRNG) 子系统的工作 。新设计的具体细节超出了本 NEP 的范围,需要进行大量讨论,但我们将讨论指导所采用的任何代码演变的一般政策。我们还将概述这样一个新系统必须满足的一些要求,以支持本新经济政策中提出的政策。

首先,我们将保持 API 源兼容性,就像我们对 numpy.如果我们必须进行重大更改,我们只会在适当的弃用期和警告的情况下进行。

其次,为了引入新功能或提高性能而破坏流兼容性的行为将谨慎允许。此类更改将被视为功能,因此不会比功能的标准发布节奏更快(即在发布时,永远不会)。为此目的,缓慢不会被视为错误。像往常一样,在错误修复版本中可能会发生破坏流兼容性的正确性错误修复,但开发人员应该考虑是否可以等到下一个功能版本。我们鼓励开发人员强烈权衡用户因流兼容性中断而带来的痛苦与改进。值得改进的一个例子是改变算法以显着提高性能,例如,从高斯变量生成的Box-Muller 变换方法转向更快的Ziggurat 算法。不鼓励改进的一个例子是稍微调整 Ziggurat 表以实现小幅性能改进。X.YX.Y.Z

随机子系统的任何新设计都将提供不同核心统一 PRNG 算法的选择。一个有前途的设计选择是使用最少的方法集使这些核心统一的 PRNG 成为自己的轻量级对象(randomgen将其称为“BitGenerators”)。更广泛的非均匀分布集将是它自己的类,该类保存对这些核心均匀 PRNG 对象之一的引用,并在需要均匀随机数时简单地委托给核心均匀 PRNG 对象(randomgen将其称为生成器)。借用randomgen的一个例子,该类MT19937是一个 BitGenerator,它实现了经典的 Mersenne Twister 算法。该类Generator包装 BitGenerator 以提供所有非均匀分布方法:

# This is not the only way to instantiate this object.
# This is just handy for demonstrating the delegation.
>>> bg = MT19937(seed)
>>> rg = Generator(bg)
>>> x = rg.standard_normal(10)

我们将对这些 BitGenerator 对象上的方法的选择子集更加严格。它们必须保证一组指定方法的流兼容性,这些方法的选择是为了更容易地组合它们以构建其他发行版,并且需要抽象各种 BitGenerator 算法的实现细节。即,

  • .bytes()

  • integers()(以前.random_integers()

  • random()(以前.random_sample()

分布类 ( ) 应该具有与足够接近的函数签名Generator相同的所有分布方法,以便当前与实例一起使用的几乎所有代码都将与实例一起使用(忽略精确的流值)。整数分布允许存在一些差异:为了避免上述的一些跨平台问题,应该重写这些分布以处理所有平台上的数字。RandomStateRandomStateGeneratoruint64

支持单元测试#

因为我们在 numpy 诞生之初就做出了强有力的流兼容性保证,所以对流兼容性的依赖已经超出了可重现的模拟范围。跨 numpy 版本的流兼容性的一种用例是使用伪随机流在单元测试中生成测试数据。只要小心,许多跨平台的不稳定性就可以在小型单元测试中避免。

新的 PRNG 子系统必须提供第二个遗留分发类,该类使用与当前版本的numpy.random.RandomState.此类的方法将具有严格的流兼容性保证,甚至比当前策略更严格。此类将不再被修改,除非在 numpy 内部发生变化时保持其工作状态。所有新的开发都应该进入初级发行版类别。不得进行更改流的错误修复RandomState;相反,有缺陷的发行版应该在有缺陷时发出警告。其目的RandomState将被记录为提供某些固定功能以实现向后兼容性和稳定数字以用于单元测试的有限目的,并且不使整个程序可在 numpy 版本之间重现。

numpy.random.RandomState为了向后兼容,这个遗留发行版类必须可以在名称下访问 。当前使用给定状态进行实例化的所有方法都numpy.random.RandomState应该使用相同的状态来实例化 Mersenne Twister BitGenerator。遗留发行版类必须能够接受其他 BitGenerator。这里的目的是确保人们可以编写一个具有一致 BitGenerator 状态的程序,其中混合了可能或可能没有从 RandomState.遗留发行版类的实例必须响应 True,因为当前的实用程序代码依赖于该检查。类似地,实例的旧pickle 必须正确unpickle。isinstance(rg, numpy.random.RandomState)numpy.random.RandomState

numpy.random.*#

获取可重现的伪随机数的首选最佳实践是使用种子实例化生成器对象并将其传递。便利函数RandomState背后的隐式全局numpy.random.*可能会导致问题,特别是当涉及线程或其他形式的并发时。全局状态总是有问题的。我们明确建议在涉及重现性时避免使用便利功能。

也就是说,人们确实使用它们并用来numpy.random.seed()控制它们之下的状态。一致且有用地对 API 使用情况进行分类和计数可能很困难,但一种非常常见的用法是在单元测试中,其中许多全局状态问题不太可能出现。

本 NEP 不建议删除这些函数或更改它们以使用不太稳定的Generator发行版实现。未来的新经济政策可能会。

RandomState 具体来说,新 PRNG 子系统的初始版本应将这些便利函数保留为使用 Mersenne Twister BitGenerator 对象初始化的全局方法的别名。对的调用 numpy.random.seed()将被转发到该 BitGenerator 对象。此外,RandomState在此初始版本中必须可以通过名称访问全局实例numpy.random.mtrand._rand:Robert Kern 很久以前就承诺scikit-learn该名称将是稳定的。哎呀。

为了允许某些解决方法,必须可以用RandomState任何其他 BitGenerator 对象替换全局下的 BitGenerator(我们将精确的 API 详细信息留给新子系统)。此后的调用 numpy.random.seed()应该只是将给定的种子传递给当前的 BitGenerator 对象,而不是尝试将 BitGenerator 重置为 Mersenne Twister。便利功能集numpy.random.*应与当前保持相同。它们应该是 RandomState方法的别名,而不是新的不太稳定的发行版类(Generator在上面的示例中)。想要获得最快、最好的发行版的用户可以遵循最佳实践并显式实例化生成器对象。

本新经济政策并不建议永久保留这些要求。在我们获得了新的 PRNG 子系统的经验后,我们可以而且应该在未来的 NEP 中重新审视这些问题。

备择方案

版本控制#

长期以来,我们认为在维护流的同时允许算法改进的方法是应用某种形式的版本控制。也就是说,每次我们在其中一个发行版中进行流更改时,我们都会在某处增加一些版本号。 numpy.random将保留所有过go版本的代码,并且有办法获取旧版本。

我们不会这样做。如果需要从给定版本的 获得精确的逐位结果numpy,无论是否使用随机数,都应该使用精确版本的numpy

关于如何进行 RNG 版本控制的建议多种多样,我们不会在这里详尽地列出它们。我们花了数年时间反复研究这些设计,但未能找到一个足够的。让我们失go的时间,更重要的是,我们在犹豫不决时失go的贡献者,成为反对这个想法的证据。

具体来说,添加版本控制会使维护变得numpy.random 困难。我们必然会保留相同代码的多个版本。安全地添加新算法仍然相当困难。

但最重要的是,版本控制从根本上来说很难正确使用。我们希望能够轻松、直接地获取最新、最快、最佳版本的分发算法;否则,还有什么意义呢?简单的方法是将最新的设置为默认值。但默认值必然会随着版本的不同而改变,因此用户的代码无论如何都需要更改以指定想要复制的特定版本。

添加版本控制以维护流兼容性仍然只能提供与我们当前相同级别的流兼容性,并且具有前面描述的所有限制。鉴于此类需求的标准做法是将发布numpy作为一个整体进行固定,RandomState单独的版本控制是多余的。

StableRandom#

该 NEP 的先前版本建议在RandomState弃用期内完全保持独立,并使用新名称构建新子系统。为了满足单元测试用例,它建议引入一个名义上称为 的小型发行版类StableRandom。它将提供被认为在单元测试中最有用的分发方法的一小部分子集,但不是完整的集,因此它很可能在测试上下文之外使用。

在讨论该提案时,很明显没有令人满意的子集。至少有些项目RandomState在单元测试中使用了相当广泛的方法选择。

下游项目所有者将被迫修改其代码以适应新的 PRNG 子系统。有些修改可能只是机械性的,但大部分工作都是乏味的,对下游项目没有积极的改进,只是避免被破坏。

此外,根据这个旧提案,我们将有一个相当长的弃用期,该期RandomState与 BitGenerator 和 Generator 类的新系统一起存在。保留固定的实现 RandomState意味着它无法使用新的 BitGenerator 状态对象。开发混合使用已升级和未升级库的程序需要管理两组 PRNG 状态。理论上,这是有时间限制的,但我们希望弃用的时间很长。

当前的提案解决了所有这些问题。当前的所有用法 RandomState将继续永久有效,尽管某些用法可能会通过文档进行阻止。单元测试可以继续使用完整的RandomState方法补充。混合RandomState/Generator 代码可以安全地共享公共 BitGenerator 状态。未经修改的RandomState 代码可以利用类似 BitGenerator 的替代可设置流的新功能。

讨论