1. 程式人生 > >Python.Numpy極簡入門

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. ]]
"""

本文完。如果錯誤,歡迎指出。如有想法,歡迎交流。但不接受批評,畢竟沒有錢拿。