15年!NumPy論文終出爐,還登上了Nature

機器之心發表於2020-09-17

NumPy 團隊撰寫了一篇綜述文章,介紹 NumPy 的發展過程、主要特性和陣列程式設計等。這篇文章現已發表在 Nature 上。


NumPy 是什麼?它是大名鼎鼎的使用 Python 進行科學計算的基礎軟體包,是 Python 生態系統中資料分析機器學習、科學計算的主力軍,極大簡化了向量與矩陣的操作處理。除了計算外,它還包括了:

  • 功能強大的 N 維陣列物件。

  • 精密廣播功能函式。

  • 整合 C/C++ 和 Fortran 程式碼的工具。

  • 強大的線性代數、傅立葉變換和隨機數功能


今日,NumPy 核心開發團隊的論文終於在 Nature 上發表,詳細介紹了使用 NumPy 的陣列程式設計(Array programming)。這篇綜述論文的發表距離 NumPy 誕生已經過去了 15 年。

15年!NumPy論文終出爐,還登上了Nature

論文地址:https://www.nature.com/articles/s41586-020-2649-2

NumPy 官方團隊在 Twitter 上簡要概括了這篇論文的核心內容:

NumPy 為陣列程式設計提供了簡明易懂、表達力強的高階 API,同時還考慮了維持快速運算的底層機制。

NumPy 提供的陣列程式設計基礎和生態系統中的大量工具結合,形成了適合探索性資料分析的完美互動環境。NumPy 還包括增強與 PyTorch、Dask 和 JAX 等外部庫互操作性的協議。

基於這些特性,NumPy 為張量計算提供了標準的 API,成為 Python 中不同陣列技術之間的核心協調機制。

接下來,我們來看這篇 NumPy 綜述論文的詳細內容。

論文摘要

陣列程式設計為訪問、操縱和計算向量、矩陣和高維陣列中的資料提供了功能強大、緊湊且表達力強的語法。NumPy 是 Python 語言的主要陣列程式設計庫,它在物理、化學、天文學、地球科學、生物學、心理學、材料科學、工程學、金融和經濟學等領域的研究分析中都起著至關重要的作用。例如,在天文學中,NumPy 是發現引力波和黑洞首次成像的軟體棧中的重要部分。

這篇論文回顧了一些基本的陣列概念,以及它們如何形成一種簡單而強大的程式設計正規化,使其能夠用於組織、探索和分析科學資料。NumPy 是構建科學 Python 生態系統的基礎。它的應用十分普遍,一些面向特殊需求受眾的專案已經開發出自己的類 NumPy 介面和陣列物件。

由於其在 Python 生態系統中的核心地位,NumPy 越來越多地充當陣列計算庫之間的互操作層,並且和其 API 一起提供了靈活的框架,以支援未來十年的科學和工業分析。

NumPy 的演變史

在 NumPy 之前,已經出現了兩個 Python 陣列包。Numeric 包開發於 20 世紀 90 年代中期,它提供了 Python 中的陣列物件和 array-aware 函式。Numeric 是用 C 語言寫的,並連結到線性代數的標準快速實現。其最早的應用之一是美國勞倫斯利弗莫爾國家實驗室的慣性約束核聚變研究。

為了處理來自哈勃太空望遠鏡的大型天文影像,Numeric 被重實現為 Numarray,它新增了對結構化陣列、靈活 indexing、記憶體對映、位元組序變體、更高效的記憶體使用以及更好的型別轉換規則的支援。

儘管 Numarray 與 Numeric 高度相容,但這兩個包之間的差異足以將社群開發者分為兩類。而 2005 年,NumPy 的出現完美地統一了這兩個包,它將 Numarray 的功能和 Numeric 的 small-array 效能及其豐富的 C API 結合起來。

如今,15 年過去了,NumPy 幾乎支援所有進行科學和數值計算的 Python 庫(包括 SciPy、Matplotlib、pandas、scikit-learn 和 scikit-image)。NumPy 是一個社群開發的開源庫,它提供了多維 Python 陣列物件以及對其進行操作的 array-aware 函式。由於其固有的簡潔性,事實上 NumPy 陣列已經成為 Python 中陣列資料的交換格式。

NumPy 使用 CPU 對記憶體內(in-memory)陣列進行操作。為了利用現代的專用儲存和硬體,最近已經擴充套件出一系列 Python 陣列包。與 Numarray–Numeric 之間存在較大差異的情況不同,現在的這些新庫很難在社群開發者中引起分歧,因為它們都是建立在 NumPy 之上的。但是,為了使社群能夠使用新的探索性技術,NumPy 正在過渡為核心協調機制,該機制規劃了良好定義的陣列程式設計 API,並在合適的時候將其分發給專門的陣列實現。

NumPy 陣列

NumPy 陣列是一種能夠高效儲存和訪問多維陣列的資料結構,支援廣泛型別的科學計算。NumPy 陣列包括指標和用於解釋儲存資料的後設資料,即 data type(資料型別)、shape(形狀)和 strides(步幅),參見下圖 1a。

15年!NumPy論文終出爐,還登上了Nature

圖 1:NumPy 陣列包括多種基礎陣列概念。

資料型別描述了陣列中儲存元素的本質。一個陣列只有一個資料型別,陣列中的每個元素在記憶體中佔用的位元組數是一樣的。資料型別包括實數、複數、字串、timestamp 和指標等。

陣列的形狀決定了每個軸上的元素數量,軸的數量即為陣列的維數。例如,數字向量可儲存為形狀為 N 的一維陣列,而彩色影片是形狀為 (T, M, N, 3) 的四維陣列。

步幅是解釋計算機記憶體的必要元件,它可以線性地儲存元素。步幅描述了在記憶體中逐行逐列移動時所需的位元組數。例如,形狀為 (4, 3) 的二維浮點數陣列,它其中的每個元素均在記憶體中佔用 8 個位元組數。要想在連續列之間移動,我們需要在記憶體中前進 8 個位元組數,要想到達下一行,則需要前進 3 × 8 = 24 個位元組數。因此該陣列的步幅為 (24, 8)。NumPy 可以用 C 或 Fortran 的記憶體順序儲存陣列,沿著行或列遍歷。這使得使用這些語言寫的外部庫可以直接訪問記憶體中的 NumPy 陣列資料。

使用者使用「indexing」(訪問子陣列或單個元素)、「operators」(各種運算子)和「array-aware function」與 NumPy 陣列進行互動。它們為 NumPy 陣列程式設計提供了簡明易懂、表達力強的高階 API,同時還考慮了維持快速運算的底層機制。

對陣列執行 indexing 將返回單個元素、子陣列或滿足特定條件的元素(參見上圖 1b)。陣列甚至還可以用其他陣列進行 indexing(參加圖 1c)。返回子陣列的 indexing 還可以返回原始陣列的「view」,以便在兩個陣列之間共享資料。這就為記憶體有限的情況下基於陣列資料子集進行運算提供了一種強大的方式。

為了補充陣列語法,NumPy 還包括對陣列執行向量化計算的函式,包括 arithmetic、statistics 和 trigonometry(參見圖 1d)。向量化計算基於整個陣列執行而不是其中的單個元素,這對於陣列程式設計而言是必要的。這意味著,在 C 等語言中需要幾十行才能表達的運算在這裡只需一個清晰的 Python 表示式即可實現。這就帶來了簡潔的程式碼,並使得使用者不必關注分析細節,同時 NumPy 以接近最優的方式迴圈遍歷陣列元素。

對兩個形狀相同的陣列執行向量化計算(如加法)時,接下來會發生什麼是很明確的。而「broadcasting」機制允許 NumPy 處理維度不同的陣列之間的運算,例如向陣列新增一個標量值。broadcasting 還能泛化至更復雜的示例,如縮放陣列的每一列或生成座標網格。在 broadcasting 中,單個或兩個陣列可以重疊(沒有從記憶體中複製任何資料),使得 operands 的形狀匹配(參見圖 1d)。

其他 array-aware function(如加、求平均值、求最大值)都是執行逐元素的「reduction」,累積單個陣列的一個、多個或所有軸上的結果。例如,將一個 n 維陣列與 d 個軸進行累加,得到維度為 n − d 的陣列(參見圖 1f)。

NumPy 還包含可以建立、reshaping、concatenating 和 padding 陣列,執行資料排序和計數,讀取和寫入檔案的 array-aware function。這為生成偽隨機數提供了大量支援,它還可以使用 OpenBLAS 或 Intel MKL 等後端執行加速線性代數

總之,記憶體內的陣列表示、緊密貼近數學的語法和多種 array-aware function 共同構成了生產力強、表達力強的陣列程式語言。

科學 Python 生態系統

Python 是一個開源、通用的解釋型程式語言,非常適合資料清洗、與 web 資源互動和解析文字之類的標準程式設計任務。新增快速陣列操作和線性代數能夠讓科學家在一種程式語言中完成所有的工作。

儘管 NumPy 不是 Python 標準庫的一部分,但它也從與 Python 開發者的良好關係中受益。在過去這些年中,Python 語言已經加入了一些新的功能和特殊的語法,以便 NumPy 具備更加簡潔和易於閱讀的陣列表示法。但是,由於 NumPy 不是 Python 標準庫的一部分,所以它能夠規定自己的釋出策略和開發模式。

從發展史、開發和應用的角度來看,SciPy 和 Matplotlib 與 NumPy 聯絡緊密。SciPy 為科學計算提供了基礎演算法,包括數學、科學和工程程式。Matplotlib 生成可發表品質的圖表和視覺化檔案。NumPy、SciPy 和 Matplotlib 的結合,再加上 IPython、Jupyter 這類高階互動環境,為 Python 中的陣列程式設計提供了堅實的基礎。

如圖 2 所示,科學 Python 生態系統建立在上述基礎之上,它提供了多種廣泛應用的專有技術庫,而這又是眾多領域特定專案的基礎。NumPy 是這一 array-aware 庫生態系統的基礎,它設定了文件標準、提供了陣列測試基礎結構,並增加了對 Fortran 等編譯器的構建支援。

15年!NumPy論文終出爐,還登上了Nature

圖 2:NumPy 是科學 Python 生態系統的基礎。

很多研究團隊設計出大型、複雜的科學庫,這些庫為 Python 生態系統增添了特定於具體應用的功能。例如,由事件視界望遠鏡(Event Horizon Telescope, EHT)合作專案開發的 eht-imaging 庫依賴科學 Python 生態系統的很多低階元件。而 EHT 合作專案利用該庫捕獲了黑洞的首張影像。

在 eht-imaging 庫中,NumPy 陣列在流程鏈的每一步儲存和操縱數值資料。

基於陣列程式設計建立的互動式環境及其周邊的工具生態系統(IPython 或 Jupyter 內部)完美適用於探索性資料分析。使用者可以流暢地檢查、操縱和視覺化他們的資料,並快速迭代以改善程式設計語句。然後,將這些語句拼接入命令式或函式式程式,或包含計算和敘述的 notebook。

超出探索性研究的科學計算通常在文字編輯器或 Spyder 等整合開發環境(IDE)中完成。這一豐富和高產的環境使 Python 在科學研究界流行開來。

為了給探索性研究和快速原型提供補充支援,NumPy 形成了使用經過時間檢驗的軟體工程實踐來提升協作、減少誤差的文化。這種文化不僅獲得了專案領導者的採納,而且還被傳授給初學者。NumPy 團隊很早就採用分散式版本控制和程式碼審查機制來改善程式碼協同,並使用持續測試對 NumPy 的每個提議更改執行大量自動化測試。

這種使用最佳實踐來製作可信賴科學軟體的文化已經被基於 NumPy 構建的生態系統所採用。例如,在近期英國皇家天文學會授予 Astropy 的一項獎項中表示:「Astropy 專案為數百名初級科學家提供了專業水平的軟體開發實踐,包括版本控制使用、單元測試、程式碼審查和問題追蹤程式等。這對於現代研究人員而言是一項重要的技能組合,但物理或天文學專業的正規大學教育卻常常忽略這一點。」社群成員透過課程和研討會來彌補正規教育中的這一缺失。

近來資料科學機器學習人工智慧的快速發展進一步大幅提升了 Python 的科學使用。Python 的重要應用,如 eht-imaging 庫,現已存在於自然和社會科學的幾乎每個學科之中。這些工具已經成為很多領域主要的軟體環境。大學課程、新手培訓營和暑期班通常教授 NumPy 及其生態系統,它們也成為世界各地社群會議和研討會的焦點。NumPy 和它的 API 已經無處不在了。

陣列激增和互操作性

NumPy 在 CPU 上提供了記憶體內、多維和均勻鍵入(即單一指向和跨步的)的陣列。NumPy 可以在嵌入式裝置和世界上最大的超級計算機等機器上執行,其效能接近編譯語言。在大多數情況下,NumPy 解決了絕大部分的陣列計算用例。

但是現在,科學資料集通常超出單個機器的儲存容量,並且可以在多個機器或雲上儲存。此外,近來深度學習人工智慧應用的加速需求已經促生了專用加速器硬體,包括 GPU、TPU 和 FPGA。目前,由於 NumPy 具有的記憶體內資料模型,它無法直接使用這類儲存和專用硬體。

然而,GPU、TPU 和 FPGA 的分散式資料和並行執行能夠很好地對映到陣列程式設計正規化,所以可用的現代硬體架構與利用它們的計算能力所必需的工具之間存在著差距。

社群為彌補這一差距做出的努力使得新的陣列實現激增。例如,每個深度學習框架都建立了自己的陣列。PyTorch、TensorFlow、Apache MXNet 和 JAX 陣列都有能力以分散式方式在 CPU 和 GPU 上執行,其中使用惰性計算(lazy evaluation)實現額外效能最佳化。SciPy 和 PyData/Sparse 都提供有稀疏陣列,這些陣列通常包含很少的非零值,並只在記憶體中儲存以提升效率。

此外,還有一些專案在 NumPy 陣列上構建作為資料容器,並擴充套件相應功能。Dask 透過這種方式使分散式陣列成為可能,而標記陣列是透過 xarray 實現的。

這類庫常常模仿 NumPy API,以降低初學者准入門檻,併為更廣泛的社群提供穩定的陣列程式設計介面。這反過來也會阻止一些破壞性分立(disruptive schism),如 Numeric 和 Numarray 之間的差異。

但是探索使用陣列的新方法從本質上講是試驗性的,事實上,Theano 和 Caffe 等一些有前途的庫已經停止了開發。每當使用者決定嘗試一項新技術時,他們必須更改 import 語句,並確保新庫能夠實現他們當前使用的所有 NumPy API 部件。

在理想狀態下,使用者可以透過 NumPy 函式或語義在專用陣列上進行操作,這樣他們可以編寫一次程式碼,然後從 NumPy 陣列、GPU 陣列、分散式陣列以及其他陣列之間的切換中獲益。為了支援外部陣列物件之間的陣列操作,NumPy 增加了一項充當核心協調機制的功能,並提供指定的 API,具體如上圖 2 所示。

為了促進這種互操作性,NumPy 提供了允許專用陣列傳遞給 NumPy 函式的「協議」,具體如下圖 3 所示。反過來,NumPy 根據需要將操作分派給原始庫。超過 400 個最流行的 NumPy 函式得到了支援。該協議透過 Dask、CuPy、xarray 和 PyData/Sparse 等廣泛使用的庫來實現。

得益於這些進展,使用者現在可以使用 Dask 將自己的計算從單個機器擴充套件至多個系統。該協議允許使用者透過 Dask 陣列中嵌入的 CuPy 陣列等,在分散式多 GPU 系統上大規模地重新部署 NumPy 程式碼。

使用 NumPy 的高階 API,使用者可以在具有數百萬個核的多系統上利用高度並行化的程式碼執行,並且需要的程式碼更改最少。

如下圖 3 所示,NumPy 的 API 和陣列協議向生態系統提供了新的陣列:

15年!NumPy論文終出爐,還登上了Nature

現在,這些陣列協議是 NumPy 的主要特徵,它們的重要性預計也會越來越大。NumPy 開發者(很多也是這篇文章的作者)迭代地改善和增加協議設計,以改進實用性和簡化應用方式。

論文最後對 NumPy 的現狀和未來進行了總結和展望:

在未來十年中,NumPy 開發者將面臨多項挑戰。新裝置將出現,現有的專用硬體將面臨摩爾定律的收益遞減,資料科學從業者將越來越多,型別也更加廣泛。而他們中的大部分將使用 NumPy。

隨著光片顯微鏡和大型綜合巡天望遠鏡(LSST)等裝置和儀器的採用,科學資料的規模將持續擴大。新一代語言、直譯器和編譯器,如 Rust、Julia 和 LLVM,將創造出新的概念和資料結構。

相關文章