NEP 19 - 随机数生成器策略#
- 作者:
罗伯特·科恩
- 状态:
最终
- 类型:
标准跟踪
- 创建:
2018-05-24
- 更新:
2019-05-21
- 解析:
https://mail.python.org/pipermail/numpy-discussion/2018-July/078380.html
摘要#
过去十年来,NumPy 对所有随机分布数字流都实施了严格的后向兼容性策略。与其他通常允许在修改后返回不同结果的数值组件(如果结果保持正确)不同,我们要求随机数分布始终在每个版本中生成完全相同的数字。我们流兼容性保证的目的是为了提供仿真中跨 numpy 版本的准确可重复性,以促进可重复研究。然而,这一策略使得利用更快速或更精确的算法来增强任何分布变得非常困难。在十年经验和科学软件周围生态系统的改进后,我们相信现在有更好的方法来实现这些目标。我们建议放松我们严格的流兼容性策略,以消除接受我们随机数生成功能的贡献的障碍。
现状#
我们的当前策略,具体如下
固定种子和使用相同参数的
RandomState
方法的固定调用系列始终会产生相同的结果,直到舍入误差,除非值不正确。不正确的值将被修复,并在相关文档字符串中记下进行修复的 NumPy 版本。只要之前的行为保持不变,则允许现有参数范围的扩展和新参数的添加。
该策略最初于 2008 年 11 月制定(实质上;完整的模棱两可措辞随着时间推移逐渐增加),以应对用户希望确保构成其科学出版物基础的模拟能够在几年后使用当时最新的 numpy
版本完全复制。我们热衷于支持可重复的研究,并且这在 numpy.random
的生命周期中还处于早期阶段。我们没有看到太多改变分布方法的理由。
我们也没有很深入地考虑我们真正可以承诺的限度(在本节的“我们”中,我们实际上是指罗伯特·科恩,老实说)。尽管有模棱两可的措辞,但我们的策略还是过度承诺了兼容性。在不同平台上构建的相同版本的 numpy
,或只是以不同的方式构建,会导致流中的变化,其罕见程度各不相同。最重要的是,.multivariate_normal()
方法依赖于 numpy.linalg
函数。即使在同一平台上,如果有人将 numpy
与不同的 LAPACK 链接,.multivariate_normal()
可能会返回完全不同的结果。更罕见的是,在不同的操作系统或 CPU 上构建可能会导致流中的差异。我们在内部使用 C long
整数进行整数分布(当时这似乎是个好主意),而这些整数可能会根据平台的不同而大小不同。分布方法可能会在其内部 C longs
中溢出,这取决于平台的不同,在不同的断点处溢出并导致随后进行的所有随机变量抽取均不同。
即使所有这些都受到控制,我们的策略仍然无法为不同版本提供确切的保证。当正确性受到影响时,我们仍会应用 Bug 修复。即使我们没有这样做,任何非平凡的程序所做的不仅仅是提取随机数。他们在这些数字上进行计算,使用 numpy
中的其他部分的数值算法对它们进行转换,而这不受如此严格的策略的约束。出于这些原因,尝试为我们的随机数分布维护流兼容性并不能帮助可重复的研究。
当前,对位可重复研究的标准做法是固定软件堆栈中所有代码版本,甚至可以固定到操作系统本身。与 2008 年相比,如今完成此任务的方法更容易得多。我们现在拥有 pip
。我们现在拥有虚拟机。那些现在需要精确再现模拟的人可以使用完全相同的 numpy
版本来(并且应该)再现模拟。我们无需维护 numpy
版本间的流兼容性来帮助它们。
我们的流兼容性保证阻碍了我们做出改进 numpy.random
的能力。一些首次贡献者提交了用于改进分布的 PR,通常是通过实现比现有算法更快或更精确的算法。不幸的是,其中大多数都需要破坏流才能实现。由于我们的政策和无法变通这一政策,许多贡献者就此放弃了。
实现#
在 randomgen 项目中,针对建议的新伪随机数生成器 (PRNG) 子系统的工作已经开展。新设计的具体内容超出了此 NEP 的范围,需要大量讨论,但我们将讨论指导采用任何代码的演变的一般性政策。我们还将概述此 NEP 中提出的政策必须满足的新系统的一些要求。
首先,我们将保持 API 源兼容性,就像我们对 numpy
的其他部分所做的那样。如果我们必须进行破坏性的更改,我们只会给予适当的弃用期限和警告后再进行更改。
其次,允许谨慎地破坏流兼容性以引入新功能或提高性能。此类更改将被视为功能,因此不会比功能的标准发布节奏更快(即 X.Y
发布中,绝不会是 X.Y.Z
)。这种情况下,速度慢不会被视为错误。破坏流兼容性的正确性错误修复可以在错误修复版本中进行,这很常见,但开发人员应考虑是否可以等到下一个功能版本再进行修复。我们建议开发人员权衡用户破坏流兼容性所受到的痛苦和改进带来的好处。值得改进的一个示例是更改算法以显著提升性能,例如,从 盒-穆勒变换 方法的高斯变种生成转向更快速的 之字形算法。不建议改进的一个示例是对之字形表格进行微调以获得微小的性能改进。
随机子系统的任何新设计都将提供不同的核心均匀 PRNG 算法选择。一个很有前途的设计选择是使这些核心均匀 PRNG 成为它们自己轻量级的对象,并有一组最少的方法(randomgen 称之为“BitGenerator”)。更广泛的非均匀分布将成为其自己的类,引用其中一个核心均匀 PRNG 对象,并在需要均匀随机数时简单地委托给核心均匀 PRNG 对象(randomgen 将其称为生成器)。借用 randomgen 的示例,MT19937
类是实现经典梅森旋转算法的 BitGenerator。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
) 应该具有与 RandomState
相同的所有分布方法,且函数签名非常接近,这样一来,当前几乎所有使用 RandomState
实例的代码都将能够使用 Generator
实例(忽略精确的流值)。整数分布允许发生一些差异:为了避免上述一些跨平台问题,应当将其重写为在所有平台上使用 uint64
数字。
支持单元测试#
由于我们在早期就做出了严格的流兼容性保证,因此对于流兼容性的依赖已经超出了可重复仿真。跨 numpy 版本的流兼容性的一个保留用例是在单元测试中使用伪随机流生成测试数据。如果小心,可以在小型单元测试的环境中避免许多跨平台不稳定性。
新的 PRNG 子系统必须提供第二个旧版分布类,它使用 numpy.random.RandomState
当前版本的分布方法的相同实现。此类的所有方法将具有严格的流兼容性保证,甚至比当前的策略更加严格。此类的目的是不再进行修改,但会在 numpy 内部发生变化时保持其正常运行。所有新开发都应放入主分布类。不得对 RandomState
进行更改流的错误修复;应让有 bug 的分布在有 bug 时发出警告。将把 RandomState
的目的是记录为仅为了单元测试这一有限目的向后兼容性提供某些固定功能和提供稳定数字,而不是让整个程序在 numpy 版本中都可重复。
出于向后兼容的考虑,此旧版分布类必须使用名称 numpy.random.RandomState
才能访问。当前所有使用给定状态实例化 numpy.random.RandomState
的方式都应使用相同状态实例化 Mersenne Twister BitGenerator。此旧版分布类必须能够接受其他 BitGenerator。其目的是确保用户可以使用混合库(这些库可能已从 RandomState
升级,或可能未升级)编写程序,始终保持 BitGenerator 状态一致。旧版分布类的实例必须对 isinstance(rg, numpy.random.RandomState)
响应 True
,因为当前有实用程序代码依赖于该检查。同样,numpy.random.RandomState
实例的旧 pickle 也必须能正确解 pickle。
numpy.random.*
#
获取可复现的伪随机数的最佳做法是实例化带种子的生成器对象,然后四周传递它。隐式全局 RandomState
位于 numpy.random.*
便捷功能的后面,可能会导致问题,尤其当涉及到线程或其他形式的并发时。全局状态始终成问题。在涉及可复现性时,我们绝对建议避免使用便捷功能。
话虽如此,人们还是会使用它们,并使用 numpy.random.seed()
来控制它们下面的状态。对一致且有用的 API 使用情况进行分类和计数可能很困难,但最常见的用法是单测,其中许多全局状态问题发生的可能性较小。
本 NEP 不建议删除这些函数或更改它们以使用不稳定的 Generator
分布实现。以后的 NEP 可能建议这样做。
具体来说,新 PRNG 子系统的初始版本应当把这些便捷函数留作全局 RandomState
上方法的别名,该全局由使用 Mersenne Twister BitGenerator 对象初始化。调用 numpy.random.seed()
会转发给那个 BitGenerator 对象。此外,全球 RandomState
实例必须通过 numpy.random.mtrand._rand
名称在这个初始版本中可访问:罗伯特·科恩很久以前就答应过 scikit-learn
这个名称将保持稳定。完了。
为了允许某些变通方法,必须有可能用任何其他 BitGenerator 对象替换全局 RandomState
下面的 BitGenerator(我们把精确的 API 详细信息留给新的子系统)。之后调用 numpy.random.seed()
应当只把给定的种子传递给当前 BitGenerator 对象,而不尝试将 BitGenerator 重置为 Mersenne Twister。一组 numpy.random.*
便捷函数应当保持目前的原样。它们应当是 RandomState
方法的别名,而不是新的稳定性较差的分布类(Generator
,在上面的示例中)。想要获得最快、最好的分布的用户可以遵循最佳做法并显式实例化生成器对象。
此 NEP 并未提议永久保留这些要求。在拥有新的 PRNG 子系统经验后,我们可以在未来的 NEP 中重新审视这些问题,而且应当这样做。
备选方案#
版本控制#
很长一段时间,我们都认为,在维护数据流的同时允许算法改进的方法是应用某种形式的版本控制。即,每次我们在某个分布中做出数据流更改时,我们都在某个地方递增一些版本号。 numpy.random
会保留所有过去版本的代码,而且会有办法获取旧版本。
我们不会这样做。如果人们需要从给定版本的 numpy
获取逐位精确的结果,无论是否使用随机数,人们都应当使用确切版本的 numpy
。
关于如何执行 RNG 版本控制的提议多种多样,我们不会在这里详尽列出。我们花了好几年时间反复讨论这些设计,但没有找到令人满意的结果。就让那段失去的时间,更重要的是,我们在犹豫不决时失去的贡献者,作为反对这一概念的证据吧。
具体来说,增加版本化使numpy.random
的维护变得困难。必然地,我们会保留很多相同代码的版本。安全地添加一个新算法仍然相当困难。
但最重要的是,版本化在根本上很难正确使用。我们希望让获取最新、最快、最好的分布算法版本变得容易且简单;否则,还有什么意义?实现这一目标的方法是将最新版设为默认值。但是默认值必然随着版本的发布而改变,因此无论如何用户代码都需要进行修改,以指定想要复制的具体版本。
通过增加版本化来保持流兼容性,仍然可以提供当前所做相同级别的流兼容性,但会存在前面描述的所有限制。鉴于对此类需求的标准做法是将numpy
作为一个整体固定下来,仅给RandomState
赋予版本化是没有必要的。
StableRandom
#
本NEP的早期版本建议在一段时间内完全停止使用RandomState
,并采用新名称构建一个新子系统。为了满足单元测试用例,建议引入一个名为StableRandom
的小型分布类。它将会提供一小部分在单元测试中被认为最有用的不同方法,但不是全部方法,因为在测试环境之外使用这些方法的可能性太大了。
在讨论此提案期间,很明显没有令人满意的子集。至少有些项目在单元测试中使用了相当广泛的RandomState
方法。
下游项目所有者将被迫修改他们的代码以适应新的PRNG子系统。有些修改可能只是机械性的,但大部分工作只是为了避免打破下游项目,而没有进行任何积极的改进,纯粹是繁琐的修改。
此外,根据此旧提案,我们将进行一段相当长的弃用期,在此期间,RandomState
将与新的 BitGenerator 和 Generator 类系统并存。保留RandomState
的实现,表示其无法使用新的 BitGenerator 状态对象。使用已升级和未升级库组合的开发程序需要管理两组 PRNG 状态。理论上,这会受到时间的限制,但是我们希望弃用期很长。
当前提案解决了所有这些问题。今后仍将继续使用所有当前 RandomState
用法,尽管一些用法可能会受到文档的阻止。单元测试可以继续使用 RandomState
方法的全部补充。混合的 RandomState/Generator
代码可以安全地共享常见的 BitGenerator 状态。未修改的 RandomState
代码可以使用可设置流等替代 BitGenerator 的新功能。
讨论#
版权#
本文件已置于公有领域。