1. 程式人生 > >Python--Numpy基礎

Python--Numpy基礎

Numpy是Python的一個能快速處理矩陣運算的數學庫,如果你從事的是資料科學,或者機器學習領域的話,Numpy是一項最基本的技能。他不僅簡化了我們在處理矩陣運算時需要編寫的程式碼,而且,許多Numpy的底層函式用C編寫,我們能獲得在用普通Python自帶的列表結構時,所無法達到的運算速度。

下面,我將就Numpy的一些基本用法,做個簡單的介紹,當然,一來Numpy庫本身會不斷更新,二來,我本人的認知也會隨時間改變,所以,這部分內容也會隨時更新。當然一些我認為不太常用的方法、結構,不會出現在本文中,使得本文儘量簡潔明瞭。

因為Numpy並不是Python本身自帶的庫,所以,使用之前,當然需要先安裝,具體的方法,百度一下會有很多,我在此就略了。所以,我預設所有對這部分內容感興趣的人都已經安裝好了Numpy,並且有一定的Python語言基礎。需要注意的是,為了使得文中的程式碼儘量簡潔,所有出現的程式碼都預設已經引入了Numpy庫,並簡寫為np。也就是說,所有程式碼都預設在它們之前匯入了numpy庫:import numpy as np

建立陣列

1. np.array() 函式建立陣列

Numpy為我們提供的建立陣列的方法可以說非常豐富,針對某些特殊型別的陣列還有專門的函式,但是基本上講,有以下三種:

(1) Numpy中的陣列型別——ndarray

a = np.array([1, 2, 3]) # >>> [1 2 3]
b = np.array((1, 2, 3.5)) # >>> [1. 2. 3.5]
print(type(a), type(b)) # >>> <class 'numpy.ndarray'> <class
'numpy.ndarray'>

我們將一個序列傳遞給np.array() 函式(其實,這個序列不一定是列表,元組,甚至字串都可以),會生成一個新的 ndarray 型的物件,這就是Numpy中的陣列了,從形式上中也可以看出,這種物件不同於列表,中間是沒有 “,” 分隔的。

我們發現,當傳入的序列中的元素是不同型別時,生成ndarray物件後,元素的型別會受到影響,比如,上面的例子中,當整型和浮點型都存在時,生成的物件中原本是整型的元素也變成了一種特殊的浮點型(這種型別時ndarray物件所特有的),考慮到一般只是進行數學意義上的運算,所以儘量避免在傳入的序列中加入字串這種操作,以免型別錯亂,造成不必要的麻煩。

當然,np.array()函式也適用於生成多維陣列。

m = np.array([[1, 2], [3, 4]]) # >>> [[1 2]
                                      [3 4]]

需要注意的是,想要用序列生成陣列,傳入函式的一定是一個完整的序列,而不能只是元素的排布

a = np.array(1, 2, 3) # >>> 錯誤!
a = np.array([1, 2, 3]) # >>> 正確!

其實,在實際應用中,一般我們就把Numpy中的多維陣列當做矩陣,來執行相關運算。所以,正確地創立陣列,是開始所有代數運算的先決條件。

(2) ndarray型物件的屬性

此外,和其他資料型別一樣,我們可以檢視”ndarray”型物件的相關屬性:

  • ndarray.dtype:資料型別
a = np.array([1.5, 2, 3], dtype=int) #設定陣列中元素的資料型別為整型
print(a) # >>> [1 2 3]

b = np.array([[1, 2.3], [4.1, 5.0]])
print(b.dtype) # >>> float64,訪問陣列b的資料型別
  • ndarray.shape:陣列大小
a = np.array([1, 2, 3])
b = np.array([[1], [2], [3]])
print(a.shape) # >>> (3,)
print(b.shape) # >>> (3, 1) # shape實際上是一個元組

也可以通過設定shape來改變陣列的形狀:

a = np.array([1, 2, 3, 4], [4, 2, 3, 1], [3, 4, 2, 1]) # 現在,a是3 x 4的矩陣
a.shape = 4, 3 # a的形狀被改變
print(a) # >>> [[1 2 3]
                [4 4 2]
                [3 1 3]
                [4 2 1]]
  • ndarray.ndim:陣列的維度
a = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]], [[9, 10], [11, 12]]])
b = np.array([[1], [2], [3]])
print(a.ndim) # >>> 3
print(b.ndim) # >>> 2
  • ndarray.size:陣列中元素的總個數
a = np.array([1, 2, 3])
b = np.array([[1], [2], [3]])
print(a.size) # >>> 3
print(b.size) # >>> 3

當然,ndarray物件還有很多其他屬性,這些屬性有助於我們更好地控制陣列物件,基本使用方法與上面類似,不再贅述。

2. 建立陣列過程中常用的其他函式

需要注意的一點是:上面的例子都是先建立一個Python序列,然後通過array()函式將其轉換為陣列,這樣做顯然效率不高。因此NumPy提供了很多專門用來建立陣列的函式。以方便使用者在已知陣列是具備的某種特殊性質下,快速建立陣列。

(1)np.arange()依據範圍特徵建立陣列

之前,我們知道Python基本語法裡的 range() 函式可以根據數值範圍生成列表,同樣的,我們在Numpy中通過 arange() 函式,按照一定規律生成陣列。

a = np.arange(5) # >>> [0 1 2 3 4]
b = np.arange(1, 7, 2) # >>> [1 3 5]

語法上講,和 range() 函式是一致的,都是由三個引數 `(start, stop, step)組成

(2)np.linspace()

作用與arange()類似,三個必要的引數:”start, stop, num”,不過預設狀態下包含終值。

a = np.arange(1, 7, 3) # >>> [1.  4.  7.]

(3)np.logspace()

函式和linspace類似,不過它建立等比數列,引數設定為:(start, stop, num=50, endpoint=True, base=10.0, dtype=None),一般我們只設置”start, stop, num”3個引數,表示等比數列的起始點是basestart,終止點為basestop。當然,有時候也需要改變base值。比如產生2024之間的5個數構成陣列:

a = np.logspace(0, 4, 5, base=2) # >>>[1.   2.   4.   8.  16.]

(4)np.fromfunction()

fromstring, frombuffer, fromfile, fromfunction這些函式從其他物件中抽取序列,構建陣列。其中,我個人認為比較有用的是fromfunction,其引數設定為np.fromfunction(function, shape),由一個函式和陣列形狀作為輸入。需要注意的是,陣列中每個位置的值為這個位置的座標輸入function的計算結果,當然,函式有n個引數時,就只能生成n維陣列了

# 定義函式,這裡的函式只有1個引數
def foo1(i):
    return i % 2

# 注意fromfunction()中,第2個引數shape的輸入形式是元組
a = np.fromfunction(foo1, (2,)) # >>> [ 0.  2.]

b = np.fromfunction(lambda x, y: x + y, (2, 3)) # >>> [[ 0.  1.  2.]
                                                        [ 1.  2.  3.]]

(5)np.zeros()np.ones()np.eye()等方法可以構造特定的矩陣

上面這3個函式分別用來構造零矩陣;元素全為1的矩陣,以及單位矩陣(主對角線為1,其餘元素為0)。他們的作用是幫助我們對這些特殊的矩陣實現快速構造。

# 引數為陣列形狀
a = np.zeros((2, 3)) # >>> [[ 0.  0.  0.]
                            [ 0.  0.  0.]]
b = np.ones((2, 3)) # >>> [[ 1.  1.  1.]
                           [ 1.  1.  1.]]

# 引數為方陣的行數(列數)
c = np.eye(3) # >>> [[ 1.  0.  0.]
                     [ 0.  1.  0.]
                     [ 0.  0.  1.]]

3. 給矩陣新增行或列

有時,需要我們在已有的矩陣中,新增一行,或者一列。這裡主要介紹2種實現方法。

(1)np.r_[]np.c_[]分別新增行和列,生成新的矩陣:

如下程式碼所示,現在要在矩陣a中新增一行

a1 = np.array([[1, 2, 3], [4, 5, 6]])
b1 = np.array([[0, 0, 0]])

print(np.r_[a1, b1]) # >>>[[1 2 3]
                           [4 5 6]
                           [0 0 0]]

注意此時b1的寫法,也是一個二維矩陣,且列數與a相等。再比如,新增一列:

a1 = np.array([[1, 2], [3, 4], [5, 6]])
b1 = np.array([[0], [0], [0]])

print(np.c_[a1, b1]) # >>>[[1 2 0]
                           [3 4 0]
                           [5 6 0]]

同樣,需要注意此時b1的寫法。

(2)np.row_stack()np.column_stack()分別新增行和列,生成新的矩陣:

還有兩個與np.r_[]np.c_[]類似的函式:np.row_stack()np.column_stack()。但是它們的用法更加自由,所新增的行或列,要求只要是序列(也就是說列表,元組皆可)就行,不需要像np.r_[]np.c_[]那樣嚴格要求新增的東西必須是陣列了。具體用法網上容易查到,此處省略。

此外,還有一些別的建立陣列的方法,限於篇幅,不再介紹了,若遺漏了重要的方法,會隨時補充。

訪問陣列

1. 切片

與列表的性質類似,通過切片的形式,可以訪問已經構建好的陣列。

比如對於一維陣列:

a = np.arange(5) # >>> [0 1 2 3 4]
print(a[2:3]) # >>> [2]
print(a[::2]) # >>> [0 2 4]

語法規則與列表中的切片一致,包含起始點,且不包含終止點。需要注意的是,這種陣列切片返回的是原始陣列的一個檢視,與原始陣列共享相同的記憶體空間,而並沒有建立新的陣列。

a = np.arange(5) # >>> [0 1 2 3 4]
print(id(a[2])) # >>> 4366848312
print(id(a[2:3][0])) # >>> 4366848312

2. 訪問矩陣的行與列

在切片的方法中,我要特別強調一個細節,就是對矩陣行、列的訪問。因為這個知識點經常用到,所以特地列一小節出來。但是基本方法就是切片了。

  • 訪問矩陣的行
a = np.array([[1, 2, 3], [4, 5, 6]])

# 訪問矩陣第0行
print(a[0]) # >>> [1 2 3]

# 訪問矩陣第0行的0,1兩列
print(a[0][:2]) # >>> [1 2]
  • 訪問矩陣的列
a = np.array([[1, 2, 3], [4, 5, 6]])

# 取矩陣a第2print(a[:, 2]) # >>> [3 6]

當然,這是取全列,你也可以根據切片控制某一列中,行的範圍,具體做法跟上面的取一行時一致。

3. for迴圈遍歷矩陣

如果要遍歷整個矩陣,for迴圈是非常合適的選擇。

a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

# for迴圈遍歷行
for row in a:
    print(row) # >>> [1 2 3]
               # >>> [4 5 6]
               # >>> [7 8 9]

# for迴圈遍歷列
for col in np.transpose(a):
    print(col) # >>> [1 4 7]
               # >>> [2 5 8]
               # >>> [3 6 9]

np.transpose()是計算矩陣轉置的函式,這個後面會說。

4. 使用整數序列訪問

其實最簡單的訪問陣列單個元素的做法是通過元素的下標(包括多維陣列也是一樣)。這一點我在上面雖然沒有強調,但是早開始這麼用了。但是當現在的任務變成訪問陣列中個別幾個位置的元素,而這些位置又是毫無規律的(不太方便使用切片),那也可以方便地使用整數序列作為下標解決這個問題。

a = np.arange(0, 20, 2) # >>> [ 0  2  4  6  8 10 12 14 16 18]

# 直接通過陣列`np.array([1, 4, 5, 7])`,訪問1, 4, 5, 7位的元素
print(a[np.array([1, 4, 5, 7])]) # >>> [ 2  8 10 14]

也可以通過列表建立要訪問的下標序列,但是需要先通過一個名稱引用這個列表

b = np.array([[1, 2, 3], [4, 5, 6]]) # 二維陣列

# 訪問第一行的第0列,第1列;訪問第2行的第1列,第2列
index = [[0, 1], [1, 2]]
print(b[index]) # >>> [2 6]

然而需要注意一下,這裡不能用ndarray型物件的引用作為下標,因為直譯器會認為應該在陣列的第一行取值

b = np.array([[1, 2, 3], [4, 5, 6]])

index1 = np.array([[0, 1], [1, 2]])
index2 = np.array([[0, 1], [1, 0]])
index3 = [[0, 1], [1, 0]]

print(b[index1]) # >>> wrong!因為範圍超了,index1中的元素2會讓直譯器尋找b的第2行,找不到

print(b[index2]) # >>> [[[1 2 3]
                         [4 5 6]]

                        [[4 5 6]
                         [1 2 3]]]

print(index3) # >>> [2 4],第0行第1列,以及第1行,第0

上面的東西看上去“規矩”很多,其實用的時候最好直接引用建立好的列表就行,這樣最方便。再者,就是臨時寫個小指令碼試一下,最穩妥。

5. 通過布林陣列訪問

這是一種非常特殊的方法,舉個例子來說,如下:

a = np.arange(12).reshape(3, 4) # 建立了一個3 x 4的矩陣

# b是一個布林陣列,這個布林陣列與a形狀相同,其中每個元素表示a中對應位置的元素是否比4大
b = a > 4 # >>> b = [[False False False False]
                     [False  True  True  True]
                     [ True  True  True  True]]
print(a[b]) # >>> [ 5  6  7  8  9 10 11]

也可以通過布林陣列來選擇一個數組的行和列

a = np.arange(12).reshape(3, 4)
b1 = np.array([False, True, True])
b2 = np.array([False, True, True, False])

# 按bool屬性取行
print(a[b1,:]) # >>> [[ 4  5  6  7]
                      [ 8  9 10 11]]

# 按bool屬性取列
print(a[,:b2]) # >>> [[ 1  2]
                      [ 5  6]
                      [ 9 10]]

線性代數運算

這是Numpy最吸引人的地方了。我們常見的矩陣、向量的運算基本在Numpy中都能找到現成的函式,為快速開發提供了可能。操作中,”ndarray”型物件既然可以表示多維陣列,那麼當然也可以表示我們最常用的矩陣。實際上,Numpy中,我們對矩陣操作時,用”ndarray”型物件的頻率甚至高過了標準的矩陣物件”np.matrix()”.

1. 矩陣轉置

ndarray.transpose()函式直接生成轉置矩陣

a = np.array([[1, 2],[3, 4]])

print(a.transpose()) # [[1 3]
                        [2 4]]

2. 矩陣的跡

np.trace()函式輸出矩陣的跡,即方陣主對角線上元素之和。若所要計算跡的矩陣非方陣,則計算行列序相等的元素之和。

a = np.array([[1, 2],[3, 4]])
b = np.array([[1, 2, 3],[4, 5, 6]])

print(np.trace(a)) # >>> 5
print(np.trace(b)) # >>> 6(由1 + 5生成的)

3. 矩陣乘法

np.dot(a, b)表示兩個矩陣之間的乘法,在Python3中,一個更簡單的符號@表示矩陣之間的乘運算。注意:符號*不能表示ndarray物件之間相乘。

a = np.array([[1, 2, 3], [4, 5, 6]])
b = np.array([[1, 2], [3, 4], [5, 6]])
print(np.dot(a, b)) # >>> [[22 28]
                       [49 64]]
print(np.transpose(b) @ np.transpose(a)) # >>> [[22 49]
                                                [28 64]]

4. 矩陣的加減、數乘

+, -, *, /這些運算子能直接執行矩陣的基本運算,當然,都是按元素進行的。其中乘法特指數乘,跟我們用的最多的矩陣乘法不同。

a = np.array([[1, 2, 3],[4, 5, 6]])
b = np.array([[6, 5, 4],[3, 2, 1]])

# 矩陣對應元素做加減
print(a + b) # >>> [[7 7 7]
                    [7 7 7]]

print(a - b) # >>> [[-5 -3 -1]
                    [ 1  3  5]]

# 矩陣所有元素與某數相乘
print(a * 2) # >>> [[ 2  4  6]
                    [ 8 10 12]]

# 矩陣所有元素的次冪
print(a ** 2) # >>> [[ 1  4  9]
                     [16 25 36]]

5. 隨機矩陣

子庫random用於生成隨機的陣列、矩陣等等。這裡我簡略舉2個例子,用法跟標準庫中的random模組類似,具體可以查閱相關文件。

  • np.random.rand(a, b)函式用來生成a行b列的矩陣,矩陣中每個元素都是隨機的浮點數,範圍在[0,1)
a = np.random.rand(2, 3) # >>> [[ 0.23600569  0.62347282  0.84519854]
                                [ 0.85702323  0.10537731  0.59344112]]
  • np.random.randint(low, high, size)函式用來生成形狀為size的隨機矩陣,其中,矩陣的每一個元素都是整數,且範圍在[low,high)之間。
a = np.random.randint.rand(0, 10, (2, 3)) # >>> [[7 0 9]
                                                 [5 4 7]]

5. 矩陣的逆與計算行列式

Numpy的線性代數子庫linalg也是需要了解一下的,它有兩個特別重要的應用:求取矩陣的逆;計算行列式

  • 矩陣求逆:np.linalg.inv(ndarray)
a = np.random.randint(0, 10, (3, 3)) # >>> [[7 1 5]
                                            [7 8 1]
                                            [0 3 0]]

print(np.linalg.inv(a)) # >>> [[-0.03571429  0.17857143 -0.46428571]
                               [ 0.          0.          0.33333333]
                               [ 0.25       -0.25        0.58333333]]
  • 計算行列式:np.linalg.det(ndarray)
a = np.random.randint(0, 10, (3, 3)) # >>> [[2 6 3]
                                            [8 5 4]
                                            [2 8 0]]

print(np.linalg.det(a)) # >>> 146

6. 向量的最大最小值、加和、平均值、範數

實現這些對向量的運算程式碼如下:

a = np.random.randint(0, 10, (1,)) # >>> [6 5 1 4 5 9 8 9 0 0]

print(np.sum(a)) # >>> 47
print(np.max(a)) # >>> 9
print(np.min(a)) # >>> 0
print(np.average(a)) # >>> 4.7
print(np.linalg.norm(a)) # >>> 18.1383571472

其實,最後一項範數的計算,我們當然可以令這個向量與自身做內積,再開根號。當然,上面程式碼中的寫法卻是更加簡潔方便。

有些時候我們想要一次性求得矩陣每一行或者每一列的最大最小值,加和,平均值等等,這個時候,“軸”引數的設定就顯現出作用了。

a = np.random.randint(0, 10, (3, 3)) # >>> [[3 7 1]
                                            [1 0 3]
                                            [3 6 2]]

print(np.sum(a, axis=0)) # >>> [ 7 13  6]
print(np.sum(a, axis=1)) # >>> [11  4 11]

# 如果直接求sum,得到的會是全部元素的加和
print(np.sum(a)) # >>> 26

軸(axis)的含義其實表示的是矩陣的維度,比如拿一個二維矩陣來說,其軸的表示如下圖:

這裡寫圖片描述

8. 向量內積

其實,這個東西可以用矩陣乘法解決,但是對於一維陣列來說,還有個專門的函式np.inner(v1, v2),因為我在工作中經常會用到這種一維陣列計算內積的情況,故在此列出:

v1 = np.array([1, 2, 3])
v2 = np.array([2, 3, 4])

print(np.inner(v1, v2)) # >>> 20

關於Numpy基礎知識,我暫時想到的就是以上這些了。因為Numpy的內容實在太多,所以我寫的一定不全。還希望讀者們有什麼好的提議儘可提出,我再新增。這篇部落格將在未來,不定時更新。