NEP 19 — 隨機數生成器政策#

作者:

Robert Kern <robert.kern@gmail.com>

狀態:

最終

類型:

標準追蹤

建立於:

2018-05-24

更新於:

2019-05-21

決議:

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

摘要#

在過去十年中,NumPy 對其所有隨機數分佈的數字串流都採取了嚴格的向後相容性政策。與 numpy 中的其他數值組件不同,這些組件通常在修改後允許返回不同的結果(如果結果仍然正確),但我們有義務讓隨機數分佈始終在每個版本中產生完全相同的數字。我們的串流相容性保證的目標是為跨 NumPy 版本的模擬提供精確的可重複性,以促進可重複的研究。然而,此政策使得增強任何具有更快或更準確演算法的分佈變得非常困難。經過十年的經驗以及科學軟體周圍生態系統的改進,我們認為現在有更好的方法來實現這些目標。我們建議放寬我們嚴格的串流相容性政策,以消除接受對我們的隨機數生成能力做出貢獻的障礙。

現狀#

我們目前的政策,完整內容如下

固定的種子和使用相同參數對 RandomState 方法進行的一系列固定呼叫,將始終產生相同的結果,直到捨入誤差,除非這些值不正確。不正確的值將被修正,並且進行修正的 NumPy 版本將在相關的文檔字串中註明。允許擴展現有的參數範圍和新增參數,只要先前的行為保持不變。

此政策最初於 2008 年 11 月制定(本質上;全套的推託之詞隨著時間推移而增長),以回應使用者希望確保構成其科學出版物基礎的模擬可以在多年後使用當時最新的 numpy 版本完全重現。我們熱衷於支持可重複的研究,而且當時 numpy.random 仍處於早期階段。我們還沒有看到太多需要大幅更改分佈方法的原因。

我們也沒有非常徹底地思考我們可以真正承諾的限制(並且在本節中的「我們」實際上是指 Robert Kern,讓我們誠實一點)。儘管有所有的推託之詞,我們的政策仍然過度承諾了相容性。在不同平台上建置的相同版本 numpy,或僅僅以不同的方式建置,都可能導致串流發生變化,其罕見程度各不相同。最大的是 .multivariate_normal() 方法依賴於 numpy.linalg 函數。即使在相同的平台上,如果將 numpy 與不同的 LAPACK 連結,.multivariate_normal() 也可能返回完全不同的結果。更罕見的情況是,在不同的作業系統或 CPU 上建置可能會導致串流中的差異。我們在內部使用 C long 整數進行整數分佈(當時看起來是個好主意),並且這些整數的大小可能因平台而異。分佈方法可能會在不同的中斷點溢出其內部 C longs,具體取決於平台,並導致隨後的所有隨機變數抽取都不同。

即使所有這些都受到控制,我們的政策仍然無法提供跨版本的精確保證。當正確性受到威脅時,我們仍然會應用錯誤修復。即使我們沒有這樣做,任何重要的程式不僅僅是抽取隨機數。它們還對這些數字進行計算,使用來自 numpy 其餘部分的數值演算法轉換這些數字,而 numpy 其餘部分不受如此嚴格的政策約束。由於這些原因,為我們的隨機數分佈維護串流相容性並不能幫助可重複的研究。

現在,位元對位元可重複研究的標準做法是釘住您的軟體堆疊的所有程式碼版本,甚至可能細化到作業系統本身。如今,完成此操作的環境比 2008 年容易得多。我們現在有了 pip。我們現在有了虛擬機器。現在,那些需要精確重現模擬的人可以(並且應該)透過使用完全相同的 numpy 版本來做到這一點。我們不需要維護跨 numpy 版本的串流相容性來幫助他們。

我們的串流相容性保證阻礙了我們改進 numpy.random 的能力。幾位首次貢獻者提交了 PR,以改進分佈,通常是透過實作比目前存在的演算法更快或更準確的演算法。不幸的是,它們中的大多數都需要打破串流才能做到這一點。由於我們的政策以及我們無法繞過該政策,許多貢獻者只是離開了。

實作#

randomgen 專案中,擬議的新偽隨機數生成器 (PRNG) 子系統的工作已經在進行中。新設計的細節超出了本 NEP 的範圍,並且還有很多討論空間,但我們將討論將指導任何已採納程式碼發展的一般政策。我們還將概述這樣一個新系統必須具備的少數要求,以支援本 NEP 中提出的政策。

首先,我們將像對待 numpy 的其餘部分一樣,維護 API 原始碼相容性。如果我們必須做出破壞性變更,我們只會在適當的棄用期和警告下進行。

其次,為了引入新功能或提高效能而打破串流相容性將被允許,但需謹慎。此類變更將被視為功能,因此它們的速度不會快於功能的標準發布節奏(即在 X.Y 版本上,絕不在 X.Y.Z 上)。為此目的,速度慢不會被視為錯誤。通常,修復錯誤的版本可以修復錯誤,但開發人員應考慮是否可以等到下一個功能版本。我們鼓勵開發人員權衡使用者因串流相容性中斷而產生的痛苦與改進之間的關係。一個值得的改進範例是更改演算法以顯著提高效能,例如,將高斯變數生成的 Box-Muller 轉換方法更改為更快的 Ziggurat 演算法。一個不鼓勵的改進範例是稍微調整 Ziggurat 表格以獲得微小的效能提升。

隨機子系統的任何新設計都將提供不同核心均勻 PRNG 演算法的選擇。一個有希望的設計選擇是使這些核心均勻 PRNG 成為它們自己輕量級的物件,並具有最少的方法集(randomgen 稱它們為「BitGenerators」)。更廣泛的非均勻分佈集將是它自己的類別,它保存對這些核心均勻 PRNG 物件之一的引用,並且當它需要均勻隨機數時,只需委託給核心均勻 PRNG 物件(randomgen 稱此為 Generator)。為了借用 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) 應該具有與 RandomState 相同的所有分佈方法,並且函數簽章非常接近,以至於幾乎所有目前與 RandomState 實例一起使用的程式碼都將與 Generator 實例一起使用(忽略精確的串流值)。對於整數分佈,將允許一些差異:為了避免上面描述的一些跨平台問題,應該重寫這些分佈以在所有平台上使用 uint64 數字。

支援單元測試#

由於我們在 NumPy 生命週期的早期就做出了強大的串流相容性保證,因此對串流相容性的依賴性已超出可重複的模擬範圍。跨 NumPy 版本串流相容性仍然存在的一個用例是在單元測試中使用偽隨機串流來生成測試資料。謹慎地說,在小型單元測試的上下文中可以避免許多跨平台的不穩定性。

新的 PRNG 子系統必須提供第二個舊版分佈類別,該類別使用與目前版本的 numpy.random.RandomState 相同的分佈方法實作。此類別的方法將具有嚴格的串流相容性保證,甚至比目前的政策更嚴格。目的是這個類別將不再被修改,除非在 NumPy 內部結構發生變化時保持其工作狀態。所有新的開發都應該進入主要分佈類別。更改串流的錯誤修復不應對 RandomState 進行;相反,有錯誤的分佈應該在它們有錯誤時發出警告。RandomState 的目的將被記錄為提供某些固定的功能,以實現向後相容性和用於單元測試有限目的的穩定數字,而不是使整個程式在跨 NumPy 版本中可重複。

這個舊版分佈類別必須可以透過名稱 numpy.random.RandomState 訪問,以實現向後相容性。目前所有使用給定狀態實例化 numpy.random.RandomState 的方式都應該使用相同的狀態實例化 Mersenne Twister BitGenerator。舊版分佈類別必須能夠接受其他 BitGenerator。此處的目的是確保可以使用一致的 BitGenerator 狀態編寫程式,其中混合了可能已升級或未從 RandomState 升級的程式庫。舊版分佈類別的實例必須對 isinstance(rg, numpy.random.RandomState) 回應 True,因為目前有實用程式碼依賴於該檢查。同樣,numpy.random.RandomState 實例的舊 pickle 必須正確地解 pickle。

numpy.random.*#

獲得可重複偽隨機數的首選最佳實務是實例化具有種子的生成器物件並將其傳遞。隱式全域 RandomStatenumpy.random.* 便利函數背後可能會導致問題,尤其是在涉及執行緒或其他形式的並發時。全域狀態始終存在問題。當涉及可重複性時,我們明確建議避免使用便利函數。

也就是說,人們確實會使用它們,並使用 numpy.random.seed() 來控制它們底下的狀態。可能難以一致且有效地分類和計算 API 使用情況,但一個非常常見的用法是在單元測試中,在單元測試中,全域狀態的許多問題不太可能發生。

本 NEP 並未提議刪除這些函數或將它們更改為使用不太穩定的 Generator 分佈實作。未來的 NEP 可能會。

具體而言,新的 PRNG 子系統的初始版本應將這些便利函數保留為全域 RandomState 上方法的別名,該全域 RandomState 使用 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)。想要獲得最快、最佳分佈的使用者可以遵循最佳實務並顯式實例化生成器物件。

本 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 類型的可設定串流的新功能。

討論#