什么是Numpy的ndarray


首先,Numpy的核心是ndarray。

然后,ndarray本質是數組,其不同於一般的數組,或者Python 的list的地方在於它可以有N 維(dimentions),也可簡單理解為數組里面嵌套數組。

最后,Numpy為ndarray提供了便利的操作函數,而且性能優越,完爆Python 的list,因此在數值計算,機器學習,人工智能,神經網絡等領域廣泛應用。

Numpy幾乎是Python 生態系統的數值計算的基石,例如Scipy,Pandas,Scikit-learn,Keras等出色的包都基於Numpy。



本文的主要目的在於理解numpy.ndarray的內存結構及其背后的設計哲學。

ndarray是什么

NumPy provides an N-dimensional array type, the ndarray, which describes a collection of “items” of the same type. The items can be indexed using for example N integers.

—— from https://docs.scipy.org/doc/numpy-1.17.0/reference/arrays.html

ndarray是numpy中的多維數組,數組中的元素具有相同的類型,且可以被索引

如下所示:

>>> import numpy as np >>> a = np.array([[0,1,2,3],[4,5,6,7],[8,9,10,11]]) >>> a array([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]]) >>> type(a) <class 'numpy.ndarray'> >>> a.dtype dtype('int32') >>> a[1,2] 6 >>> a[:,1:3] array([[ 1, 2], [ 5, 6], [ 9, 10]]) >>> a.ndim 2 >>> a.shape (3, 4) >>> a.strides (16, 4) 

注:np.array並不是類,而是用於創建np.ndarray對象的其中一個函數,numpy中多維數組的類為np.ndarray

ndarray的設計哲學

ndarray的設計哲學在於數據存儲與其解釋方式的分離,或者說copyview的分離,讓盡可能多的操作發生在解釋方式上(view上),而盡量少地操作實際存儲數據的內存區域。

如下所示,像reshape操作返回的新對象babshape不同,但是兩者共享同一個數據block,c=b.Tcb的轉置,但兩者仍共享同一個數據block,數據並沒有發生變化,發生變化的只是數據的解釋方式。

>>> a array([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]]) >>> b = a.reshape(4, 3) >>> b array([[ 0, 1, 2], [ 3, 4, 5], [ 6, 7, 8], [ 9, 10, 11]]) # reshape操作產生的是view視圖,只是對數據的解釋方式發生變化,數據物理地址相同 >>> a.ctypes.data 80831392 >>> b.ctypes.data 80831392 >>> id(a) == id(b) false # 數據在內存中連續存儲 >>> from ctypes import string_at >>> string_at(b.ctypes.data, b.nbytes).hex() '000000000100000002000000030000000400000005000000060000000700000008000000090000000a0000000b000000' # b的轉置c,c仍共享相同的數據block,只改變了數據的解釋方式,“以列優先的方式解釋行優先的存儲” >>> c = b.T >>> c array([[ 0, 3, 6, 9], [ 1, 4, 7, 10], [ 2, 4, 8, 11]]) >>> c.ctypes.data 80831392 >>> string_at(c.ctypes.data, c.nbytes).hex() '000000000100000002000000030000000400000005000000060000000700000008000000090000000a0000000b000000' >>> a array([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]]) # copy會復制一份新的數據,其物理地址位於不同的區域 >>> c = b.copy() >>> c array([[ 0, 1, 2], [ 3, 4, 5], [ 6, 7, 8], [ 9, 10, 11]]) >>> c.ctypes.data 80831456 >>> string_at(c.ctypes.data, c.nbytes).hex() '000000000100000002000000030000000400000005000000060000000700000008000000090000000a0000000b000000' # slice操作產生的也是view視圖,仍指向原來數據block中的物理地址 >>> d = b[1:3, :] >>> d array([[3, 4, 5], [6, 7, 8]]) >>> d.ctypes.data 80831404 >>> print('data buff address from {0} to {1}'.format(b.ctypes.data, b.ctypes.data + b.nbytes)) data buff address from 80831392 to 80831440 

副本是一個數據的完整的拷貝,如果我們對副本進行修改,它不會影響到原始數據,物理內存不在同一位置。

視圖是數據的一個別稱或引用,通過該別稱或引用亦便可訪問、操作原有數據,但原有數據不會產生拷貝。如果我們對視圖進行修改,它會影響到原始數據,物理內存在同一位置。

視圖一般發生在:

  • 1、numpy 的切片操作返回原數據的視圖。
  • 2、調用 ndarray 的 view() 函數產生一個視圖。

副本一般發生在:

  • Python 序列的切片操作,調用deepCopy()函數。
  • 調用 ndarray 的 copy() 函數產生一個副本。

—— from NumPy 副本和視圖

view機制的好處顯而易見,省內存,同時速度快

ndarray的內存布局

NumPy arrays consist of two major components, the raw array data (from now on, referred to as the data buffer), and the information about the raw array data. The data buffer is typically what people think of as arrays in C or Fortran, a contiguous (and fixed) block of memory containing fixed sized data items. NumPy also contains a significant set of data that describes how to interpret the data in the data buffer.

—— from NumPy internals

ndarray的內存布局示意圖如下:

https://stackoverflow.com/questions/57262885/how-is-the-memory-allocated-for-numpy-arrays-in-python

可大致划分成2部分——對應設計哲學中的數據部分和解釋方式:

  • raw array data:為一個連續的memory block,存儲着原始數據,類似C或Fortran中的數組,連續存儲
  • metadata:是對上面內存塊的解釋方式

metadata都包含哪些信息呢?

  • dtype數據類型,指示了每個數據占用多少個字節,這幾個字節怎么解釋,比如int32float32等;
  • ndim:有多少維;
  • shape:每維上的數量;
  • strides維間距,即到達當前維下一個相鄰數據需要前進的字節數,因考慮內存對齊,不一定為每個數據占用字節數的整數倍;

上面4個信息構成了ndarrayindexing schema,即如何索引到指定位置的數據,以及這個數據該怎么解釋

除此之外的信息還有:字節序(大端小端)、讀寫權限、C-order(行優先存儲) or Fortran-order(列優先存儲)等,如下所示,

>>> a.flags C_CONTIGUOUS : True F_CONTIGUOUS : False OWNDATA : True WRITEABLE : True ALIGNED : True WRITEBACKIFCOPY : False UPDATEIFCOPY : False 

ndarray的底層是C和Fortran實現,上面的屬性可以在其源碼中找到對應,具體可見PyArrayObjectPyArray_Descr等結構體。

為什么可以這樣設計

為什么ndarray可以這樣設計?

因為ndarray是為矩陣運算服務的,ndarray中的所有數據都是同一種類型,比如int32float64等,每個數據占用的字節數相同、解釋方式也相同,所以可以稠密地排列在一起,在取出時根據dtype現copy一份數據組裝成scalar對象輸出。這樣極大地節省了空間,scalar對象中除了數據之外的域沒必要重復存儲,同時因為連續內存的原因,可以按秩訪問,速度也要快得多。

https://docs.scipy.org/doc/numpy-1.17.0/reference/arrays.html

>>> a array([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]]) >>> a[1,1] 5 >>> i,j = a[1,1], a[1,1] # i和j為不同的對象,訪問一次就“組裝一個”對象 >>> id(i) 102575536 >>> id(j) 102575584 >>> a[1,1] = 4 >>> i 5 >>> j 5 >>> a array([[ 0, 1, 2, 3], [ 4, 4, 6, 7], [ 8, 9, 10, 11]]) # isinstance(val, np.generic) will return True if val is an array scalar object. Alternatively, what kind of array scalar is present can be determined using other members of the data type hierarchy. >> isinstance(i, np.generic) True 

這里,可以將ndarray與python中的list對比一下,list可以容納不同類型的對象,像stringinttuple等都可以放在一個list里,所以list中存放的是對象的引用,再通過引用找到具體的對象,這些對象所在的物理地址並不是連續的,如下所示

https://jakevdp.github.io/PythonDataScienceHandbook/02.01-understanding-data-types.html

所以相對ndarraylist訪問到數據需要多跳轉1次,list只能做到對對象引用的按秩訪問,對具體的數據並不是按秩訪問,所以效率上ndarraylist要快得多,空間上,因為ndarray只把數據緊密存儲,而list需要把每個對象的所有域值都存下來,所以ndarraylist要更省空間。

小結

下面小結一下:

  • ndarray的設計哲學在於數據與其解釋方式的分離,讓絕大部分多維數組操作只發生在解釋方式上
  • ndarray中的數據在物理內存上連續存儲,在讀取時根據dtype現組裝成對象輸出,可以按秩訪問,效率高省空間
  • 之所以能這樣實現,在於ndarray是為矩陣運算服務的,所有數據單元都是同種類型

參考



numpy數組和矩陣之間有什么區別?我該用哪一個?

Numpy矩陣嚴格是二維的,而numpy陣列(Ndarray)是N維的.矩陣對象是ndarray的子類,因此它們繼承了ndarray的所有屬性和方法。

numpy矩陣的主要優點是它們為矩陣乘法提供了一種方便的表示法:如果a和b是矩陣,那么a*b就是它們的矩陣乘積。

import numpy as np

a=np.mat('4 3; 2 1')b=np.mat('1 2; 3 4')print(a)# [[4 3]#  [2 1]]print(b)# [[1 2]#  [3 4]]print(a*b)# [[13 20]#  [ 5  8]]

另一方面,從Python3.5開始,NumPy支持使用@運算符,因此您可以使用Python>=3.5中的ndarray實現矩陣乘法的同樣方便。

import numpy as np

a=np.array([[4, 3], [2, 1]])b=np.array([[1, 2], [3, 4]])print(a@b)# [[13 20]#  [ 5  8]]

矩陣對象和ndarray都有.T若要返回轉置,但矩陣對象也具有.H對於共軛轉置,和.I反之亦然。

相反,numpy數組始終遵循按元素應用操作的規則(新的除外)。@(操作員)因此,如果ab是numpy數組,那么a*b是將組件元素按順序相乘形成的數組:

c=np.array([[4, 3], [2, 1]])d=np.array([[1, 2], [3, 4]])print(c*d)# [[4 6]#  [6 4]]

若要獲得矩陣乘法的結果,請使用np.dot(或@在Python>=3.5中,如上文所示):

print(np.dot(c,d))# [[13 20]#  [ 5  8]]

這個**運算符的行為也不同:

print(a**2)# [[22 15]#  [10  7]]print(c**2)# [[16  9]#  [ 4  1]]

a是矩陣,a**2返回矩陣積a*a..自c是一條警鍾,c**2返回一個包含每個組件平方元素的ndarray。

矩陣對象和ndarray之間還有其他技術差異(與np.ravel、項選擇和序列行為有關)。

Numpy陣列的主要優點是它們比二維矩陣更通用.當你想要一個三維數組時會發生什么?然后你必須使用ndarray,而不是矩陣對象。因此,學習使用矩陣對象是更多的工作-你必須學習矩陣對象操作和ndarray操作。

編寫一個同時使用矩陣和數組的程序會使你的生活變得困難,因為你必須跟蹤你的變量是哪種類型的對象,以免乘法返回你不想要的東西。

相反,如果您只使用ndarray,那么您可以完成矩陣對象所能做的所有事情,甚至更多,除非函數/表示法稍有不同。

如果您願意放棄NumPy矩陣乘積表示法的視覺吸引力(在Python>=3.5中使用ndarray幾乎可以很好地實現它),那么我認為NumPy數組絕對是可行的。

PS。當然,你真的不必犧牲另一個而選擇一個,因為np.asmatrixnp.asarray允許您將其中一個轉換為另一個(只要數組是二維的)。


NumPy之間的區別有一個概要arraysVS NumPymatrix埃斯這里.

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM