Python.Numpy極簡入門
Numpy庫一直在用,但從沒有去了解過numpy到底是個什麼東西,屬於知其然但不知其所以然的境界,雖然也沒什麼大礙,但今天看到某本書裡有介紹,看了一下,覺得還不錯,可以算是個簡單入門吧,所以依照書上的框架複述一遍,寫了這篇博文。
目錄
1. Numpy簡介
Numpy為Python帶來了多維陣列功能,並提供豐富的函式庫來處理這些陣列。他將常用的數學函式進行了陣列化,使得這些數學函式能夠直接對陣列進行操作,將本來需要在Python級別進行的迴圈,放到C語言的運算中,明顯的提高了程式的執行速度。這點是很重要的,因為Python最大的每種不足就是效能問題。
在Python中自帶有list(列表)物件,用來儲存一組“值”,我們可以將list近似的當作陣列來使用,但是列表中的元素可以是任何物件,比如['a',0.01,{"age":18}],因此列表中所儲存的是物件的指標,這樣為了儲存一個簡單的[1,2,3],需要3個指標和三個整數物件,在時間和空間上都造成了極大浪費。
為了克服list的弊端,就可以使用Numpy庫所帶來的陣列。
Numpy庫提供了兩種基本的物件,一是ndarray(N-dimensional array object)是儲存單一資料型別的多維陣列。二是ufunc(universal function object)是能夠對陣列進行處理的函式
2. ndarray物件
2.1 陣列的建立
通過給array函式傳遞Python的序列物件來建立陣列,如果傳遞的是多層巢狀序列,將建立多維陣列。示例如下:
import numpy as np a = np.array( [1,2,3,4] ) b = np.array( (5,6,7,8) ) c = np.array( [[1,2,3,4],[4,5,6,7],[7,8,9,10]] ) print(a) #[1 2 3 4] print(b) #[5 6 7 8] print(c) ''' [[ 1 2 3 4] [ 4 5 6 7] [ 7 8 9 10]] '''
2.2 陣列的形狀獲取和改變
陣列的大小可以通過陣列的shape屬性獲得。示例如下:
#緊接上面的程式碼
print(a.shape) #(4,)
print(c.shape) #(3, 4)
陣列a的shape只有一個元素,因此可以判定為一維陣列,而陣列c的第0軸長度為3,第1軸的長度為4,是3*4的陣列。
可以通過修改陣列的shape屬性,在保持元素個數不變的情況下,改變陣列每個軸的長度。相當於做了重排。
c.shape=4,3
print(c)
'''
[[ 1 2 3]
[ 4 4 5]
[ 6 7 7]
[ 8 9 10]]
'''
當某個軸的元素為-1時,將根據元素的個數自動計算此軸的長度,因此下面的程式將c的shape改為了(2,6)。
c.shape=2,-1
print(c)
'''
[[ 1 2 3 4 4 5]
[ 6 7 7 8 9 10]]
'''
使用陣列的reshape方法,可以建立一個改變了尺寸的新陣列,原陣列的shape保持不變。
此處要注意的是,reshape方法和shape屬性的區別。
d=a.reshape((2,2))
e=a.reshape(2,2) #兩種方式都可以
print(d)
print(e)
print(a)
'''
[[1 2]
[3 4]]
[[1 2]
[3 4]]
[1 2 3 4]
'''
陣列a和陣列b,共享資料儲存記憶體區域,因此修改其中任何一個數組的元素都會同時修改另一個。
print(a) #[1 2 3 4]
a[1]=100 #將陣列a的第二個元素給為100
print(a) #[ 1 100 3 4]
print(d)
'''
[[ 1 100] 陣列d中也被改變了
[ 3 4]]
'''
2.3 陣列元素型別的指定
陣列內部元素的型別可以通過dtype屬性獲得。上面例子中,引數序列的元素都是整數型別,因此陣列內元素型別也是整形。還可以在具體建立陣列時,顯示地指定元素型別,通過dtype引數。
temp01=np.array( [[1,2,3,4],[4,5,6,7],[7,8,9,10]],dtype=np.float)
print(temp01) #浮點數型別
'''
[[ 1. 2. 3. 4.]
[ 4. 5. 6. 7.]
[ 7. 8. 9. 10.]]
'''
temp02=np.array( [[1,2,3,4],[4,5,6,7],[7,8,9,10]],dtype=np.complex)
print(temp02) #複數型別
'''
[[ 1.+0.j 2.+0.j 3.+0.j 4.+0.j]
[ 4.+0.j 5.+0.j 6.+0.j 7.+0.j]
[ 7.+0.j 8.+0.j 9.+0.j 10.+0.j]]
'''
array方法裡面的引數是一個Python序列,也就是說,是先建立了一個Python序列,然後用array函式將其轉換為陣列。這樣效率不高,Numpy還提供了,許多特定的方法來建立陣列,例如linspace。
3. ufunc物件
Python裡面提供了許多universal function ,可以直接對陣列的每個元素都進行操作。並且由於Numpy內建的許多ufunc函式都是基於C語言編寫的,因此有較好的效能。
3.1 ufunc示例-sin()
import numpy as np
x=np.linspace(0,2*np.pi,10)
print(type(x)) #<class 'numpy.ndarray'> x是陣列型別
print(x)
'''
[0. 0.6981317 1.3962634 2.0943951 2.7925268 3.4906585
4.1887902 4.88692191 5.58505361 6.28318531]
'''
y=np.sin(x)
print(type(y)) #<class 'numpy.ndarray'> 結果y也是陣列型別
print(y)
'''
[ 0.00000000e+00 6.42787610e-01 9.84807753e-01 8.66025404e-01
3.42020143e-01 -3.42020143e-01 -8.66025404e-01 -9.84807753e-01
-6.42787610e-01 -2.44929360e-16]
'''
3.2 效能比較
用下面的程式比較numpy.sin和math.sin的計算速度。
import time
import math
import numpy as np
x = [i * 0.001 for i in range(10000000)]
start = time.clock()
for i,t in enumerate(x):
x[i] = math.sin(t)
print("math.sin: ",time.clock() - start)
#math.sin: 2.5702852179138964
x = [i * 0.001 for i in range(10000000)]
x=np.array(x)
start = time.clock()
t=np.sin(x)
print("numpy.sin: ",time.clock() - start)
#math.sin: 2.526883316363891
#numpy.sin: 0.10765761889063619
運行了10000000次正弦運算,numpy.sin速度比math.sin的速度快上一個數量級,這是因為numpy.sin可以一次性對一個數組進行計算。也就是說,numpy.sin是在C語言級別上進行迴圈的,而math.sin是在Python級別進行迴圈,眾所周知,Python的一大弊端就是效能不佳,因此numpy.sin的執行速度自然遠勝math.sin。
當然numpy.sin同樣也支援對單個數值求正弦。
3.3 ufunc示例-add()
import numpy as np
a=np.arange(0,4)
print(a,type(a)) #[0 1 2 3] <class 'numpy.ndarray'>
#arange([start,] stop[, step,], dtype=None)根據start與stop指定的範圍以及step設定的步長,生成一個 ndarray
b=np.arange(1,5)
print(b,type(b))
c=np.add(a,b)
print(c) #[1 3 5 7]
3.4 ufunc的廣播機制
使用ufunc函式對兩個陣列進行計算時,ufunc函式會對這兩個陣列的對應元素進行計算,因此要求著兩個函式的形狀相同。如果形狀不同,會進行如下廣播處理:
1).讓所有輸入陣列都向其中維數最多的陣列看齊,shape屬性中不足的部分通過在前面加1補齊。
2).輸出陣列的shape屬性是輸入陣列的shape屬性在各個軸上的最大值。
3).如果輸入陣列的某個軸長度為1或與輸出陣列對應軸的長度相等,這個陣列就能夠用來計算,否則出錯。
4).當輸入陣列的某個軸長度為1時,沿著這條軸運算時,都用此軸上的第一組值。
示例如下:
首先建立形狀為(6,1)的二維陣列a:
import numpy as np
a=np.arange(0,60,10).reshape(-1,1)
print(a)
"""
[[ 0]
[10]
[20]
[30]
[40]
[50]]
"""
print(a.shape) #(6, 1)
再建立形狀為(5,)的一維陣列b。
b=np.arange(0,5)
print(b) #[0 1 2 3 4]
print(b.shape) #(5,)
計算陣列a和b的和,得到一個加法表,相當於計算兩個陣列中所有元素組的和,得到形狀為(6,5)的陣列。
c=a+b
print(c.shape) #(6, 5)
print(c)
'''
[[ 0 1 2 3 4]
[10 11 12 13 14]
[20 21 22 23 24]
[30 31 32 33 34]
[40 41 42 43 44]
[50 51 52 53 54]]
'''
a是2維陣列,b是1維陣列,根據規則1),需要讓陣列b的shape屬性向陣列a對齊,在陣列b的shape屬性前面+1,補齊後為(1,5),相當於做如下運算。
b.shape=1,5
print(b) #[[0 1 2 3 4]]
這樣一來,做加法運算的兩個輸入陣列的shape屬性分別為(6,1)和(1,5),根據規則2),可知輸出陣列的shape屬性為(6,5)。由於陣列b的第0軸長度為1,而陣列a的第0軸長度為6,因此,為了能夠讓他們在第0軸上相加,需要將陣列b第0軸的長度拓展為6,這相當於:
b=b.repeat(6,axis=0)
print(b)
'''
[[0 1 2 3 4]
[0 1 2 3 4]
[0 1 2 3 4]
[0 1 2 3 4]
[0 1 2 3 4]
[0 1 2 3 4]]
'''
#同理把a的第1軸拓展為5
a = a.repeat(5,axis=1)
print(a)
"""
[[ 0 0 0 0 0]
[10 10 10 10 10]
[20 20 20 20 20]
[30 30 30 30 30]
[40 40 40 40 40]
[50 50 50 50 50]]
"""
經過上述處理後,陣列a和陣列b就可以按其對應元素進行相加運算。當然,在執行“a+b"時,Numpy內部並不會真正將長度為1的軸用repeat()進行拓展,這樣太浪費空間。由於這樣的廣播計算很常用,Numpy提供了ogrid物件,用以快速產生能進行廣播運算的陣列。
3.5 ogrid物件
x,y=np.ogrid[0:5,0:5]
print(x)
"""
[[0]
[1]
[2]
[3]
[4]]
"""
print(y)
"""
[[0]
[1]
[2]
[3]
[4]]
"""
ogrid物件和多維陣列一樣,用切片元組作為下標,返回的是一組可以用來廣播計算的陣列。其切片下標有兩種形式:
開始值,結束值,步長和“np.arange(開始值,結束值,步長)”類似。
開始值,結束值,長度j,當第三個引數為虛數時,他表示所返回陣列的長度,其和np.linspace(開始值,結束值,長度)”類似。
x,y=np.ogrid[0:1:4j,0:1:3j]
print(x)
"""
[[0. ]
[0.33333333]
[0.66666667]
[1. ]]
"""
print(y)
"""
[[0. 0.5 1. ]]
"""
本文完。如果錯誤,歡迎指出。如有想法,歡迎交流。但不接受批評,畢竟沒有錢拿。