初探Numpy中的花式索引

前言

Numpy中對數組索引的方式有很多(為了方便介紹文中的數組如不加特殊說明指的都是Numpy中的ndarry數組),比如:

  • 基本索引:通過單個整數值來索引數組
import numpy as np    arr = np.arange(9) # 構造一維數組  print(arr) # array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])  # 通過整數值索引一維數組中的單個元素值  print(arr[2]) # 2  print(arr[8]) # 8

使用基本索引方式索引二維數組。

import numpy as np    arr2d = np.arange(9).reshape(3, 3) # 構建二維數組  print(arr2d) # [[0 1 2] [3 4 5] [6 7 8]]  # 通過整數值索引二維數組中的數組子集  print(arr2d[0]) # [0 1 2]  # 通過整數值索引二維數組中的單個元素值  print(arr2d[0, 2]) # 2
  • 切片索引:通過[start: end: step](起始位置為start,終止位置為end,步長為steps)的方式索引連續的數組子集
import numpy as np    arr2d = np.arange(9).reshape(3, 3)  print(arr2d) # [[0 1 2] [3 4 5] [6 7 8]]  print(arr2d[:, 0]) # [0 3 6]  print(arr2d[::2, :]) # [[0 1 2] [6 7 8]] 
  • 布爾索引:通過布爾類型的數組進行索引
import numpy as np    names = np.array(['Bob', 'Joe', 'Will'])  scores = np.random.randint(0, 100, (3, 4)) # 3名學生的4科成績    print(names == 'Bob')  print(scores[names == 'Bob']) # 獲取Bob的四科成績
  • 花式索引:通過整型數組進行索引

本文將重點介紹通過整型數組進行索引的花式索引。

a

什麼是花式索引?

花式索引(Fancy indexing)是指利用整數數組進行索引,這裡的整數數組可以是Numpy數組也可以是Python中列表、元組等可迭代類型。

花式索引根據索引整型數組的值作為目標數組的某個軸的下標來取值。這句話對於理解花式索引非常關鍵,而核心就是"軸"以及"下標",既然是整數數組作為下標,這就要求如果設置多個整數數組來索引的話,這些整數數組的元素個數要相等,這樣才能夠將整數數組映射成下標。比如對於[0, 1]和[1, 1]兩個整型數組,可以拼接成arr[0, 1]和arr[1, 1]的下標來取值,而對於[1, 2, 3]和[3, 4]兩個元素個數不等的情況下,是不能拼接成對應的下標的。

import numpy as np    arr3d = np.arange(12).reshape(2, 2, 3)    # 使用兩個整數數組來對axis= 0,1兩個軸進行花式索引  print(arr3d[[0, 1], [1, 1]])  print(arr3d[[0, 1], [0, 1, 2]]) #error    [[ 3  4  5]   [ 9 10 11]]  Traceback (most recent call last):    File "D:/code/PycharmProjects/Python_base/text01.py", line 147, in <module>      print(arr3d[[0, 1], [0, 1, 2]])  IndexError: shape mismatch: indexing arrays could not be broadcast together with shapes (2,) (3,) 

當然得益於Numpy中的廣播機制,如果其中的一個整型數組只有一個元素可以廣播到與之其它整型數組相同的元素個數,比如[0, 1]和[2]兩個整數數組,Numpy的廣播機制先將[2]變成[2, 2],然後再拼接成相應的下標arr[0, 2]和arr[1, 2]。當基本索引和緩釋索引組合的時候,基本索引會被廣播成整數數組,形成花式索引。

import numpy as np    arr3d = np.arange(12).reshape(3,4)    print(arr3d[[0, 1], [2]])  print(arr3d[[0, 1], [2, 2]])    # 花式索引和基本索引組合  print(arr3d[[0, 1], 2])  print(arr3d[[0, 1], [2, 2]])    print(arr3d[0, [0]])  print(arr3d[[0], [0]])    [2 6]  [2 6]  [2 6]  [2 6]  [0]  [0]

下面先來利用一維數組來舉例,花式索引利用整數數組來索引,那麼就先來一個整數數組,這裡的整數數組可以為Numpy數組以及Python中可迭代類型,這裡為了方便使用Python中的list列表。

import numpy as np    arr = np.arange(9)  print(arr)    arr2 = arr[[0, 2]] # 使用花式索引  print(arr2)    print(arr2[0])  print(arr2[1])    [0 1 2 3 4 5 6 7 8]  [0 2]  0  2

前面提到對於理解花式索引非常關鍵的"軸"和"下標":

  1. 對於一維數組只有一個軸axis = 0,因此我們只能設置一個整型數組並且整型數組只能作用在axis = 0這個軸上;
  2. 下標其實也很好理解,對於整數數組為[0, 2],可以簡單理解0和2分別是arr數組的下標,即arr[0]和arr[2],花式索引arr[[0, 2]]結果中的元素值和單獨對arr[0]以及arr[2]進行索引的元素值是一致的。

一維數組還比較簡單,下面來看一個二維數組要如何理解?

import numpy as np    arr2d = np.arange(9).reshape(3, 3)  print(arr2d)    arr2d2 = arr2d[[0, 2]] # 使用花式索引  print(arr2d2)    print(arr2d[0])  print(arr2d[2])    [[0 1 2]   [3 4 5]   [6 7 8]]  [[0 1 2]   [6 7 8]]  [0 1 2]  [6 7 8]

繼續使用花式索引中的"軸"和"下標"來理解花式索引下的二維數組:

  1. 對於二維數組來說一共有兩個維度兩個軸axis = 0、axis = 1,由於此時整數數組只有一個,此時由於花式索引中只有一個數組,所以此時的索引數組只能作用在axis = 0的這個軸上;
  2. 由於這裡只有一個數組所以下標的理解和在一維數組中類似,對於[0, 2]來說,對應的下標索引為arr2d[0]、arr2d[2],對於二維數組相應的索引結果為二維數組arr2中的第一行和第三行;

一個整數數組能夠索引一個軸,那麼對於二維數組來說,如果有兩個整數數組的話肯定能夠索引兩個軸。接下來我們再為二維數組添加一個整數數組[1, 2]。

import numpy as np    arr2d = np.arange(9).reshape(3, 3)  print(arr2d)  arr2d2 = arr2d[[0, 2], [1, 2]] # 使用花式索引  print(arr2d2)    [[0 1 2] [3 4 5] [6 7 8]]  [1 8]
  1. 二維數組一共有兩個軸,此時的整數數組剛好有兩個,所以兩個整數數組會作用在二維數組中的兩個軸上;
  2. 由於二維數組的兩個軸都被索引了,所以此時的下標和上面的稍有不同,對於[0, 2]和[1, 2]兩個整數數組來說,相應的下標先在第一個整數數組中選擇0,然後再在第二個整數數組中選擇1,即為arr2d[0][0]等價arr2d[0, 0],同理對於第二個索引來說先在第一個整數數組中選擇2,然後再第二個整數數組中選擇2,即為arr2d[2][2]等價arr2d[2, 2]。這也從側面證明了為什麼花式索引會要求在給定軸上的整數數組元素個數要相等;

簡單總結一下,一個整數數組作用在待索引數組中的一個軸上,因此整數數組的個數要小於等於待索引數組的維度個數,對於下標來說,花式索引本質上可以轉換為基本索引,所以要求整數數組中的元素值不能超過對應待索引數組的最大索引。

b

花式索引的使用

通過上面的例子你可能會覺得花式索引完全可以被其它的索引方式所替代,並沒有存在的必要。花式索引擅長一些不規則的索引,這些不規則的索引使用其它的索引方式可能也可以實現,但是相比於花式索引實現會比較複雜。

比如現在有一個二維數組,二維數組的形狀為(3, 4),表示3名學生的4課成績。

import numpy as np    np.random.seed(666) # 設置隨機種子  scores = np.random.randint(0, 100, (3, 4))  print(scores)    [[ 2 45 30 62]   [70 73 30 36]   [61 91 94 51]]

現在比如想要獲取第1名學生以及第3名學生的成績。

import numpy as np    np.random.seed(666) # 設置隨機種子  scores = np.random.randint(0, 100, (3, 4))  print(scores)  print(scores[[0, 2]]) # 通過花式索引第1名學生以及第3名學生    [[ 2 45 30 62]   [70 73 30 36]   [61 91 94 51]]  [[ 2 45 30 62]   [61 91 94 51]]

如果使用其它的索引方式會比較複雜,比如使用基本索引需要使用concat將arr[0]和arr[1]合併起來,而切片索引只能索引連續的位置。

還可以通過負值倒敘進行花式索引。比如現在想要索引最後一名學生以及第一名學生的4課成績。

import numpy as np    np.random.seed(666) # 設置隨機種子  scores = np.random.randint(0, 100, (3, 4))  print(scores)  score = scores[[-1, 0]]  print(score)    [[ 2 45 30 62]   [70 73 30 36]   [61 91 94 51]]  [[61 91 94 51]   [ 2 45 30 62]]

在機器學習中常通過使用花式索引來打亂數據集的樣本順序,避免機器學習模型學習到樣本的位置噪聲,對於監督學習的數據集如果打亂了樣本還需要打亂相對應的標籤值,樣本與標籤都是一一對應的關係,使用花式索引能夠輕鬆的解決。

import numpy as np  from sklearn import datasets    digits = datasets.load_digits()  X = digits.data  y = digits.target    index = np.random.permutation(X.shape[0])  print(type(index)) # <class 'numpy.ndarray'>  # 亂序後的數據集  X_random, y_random = X[index], y[index]

c

花式索引的維度問題?

到目前為止我們只關注索引的值,而忽視了最終索引後的維度變化。首先來看下面的例子,依然是上面的形狀為(3, 4)表示3名學生的4課成績的二維數組。這裡使用花式索引索引出第2名學生的4課全部成績。

import numpy as np    np.random.seed(666) # 設置隨機種子  scores = np.random.randint(0, 100, (3, 4))  print(scores)  score = scores[[1]] # 花式索引第2名學生的所有成績  print(score.shape)  print(score)    [[ 2 45 30 62]   [70 73 30 36]   [61 91 94 51]]  (1, 4)  [[70 73 30 36]]

通過前面的學習知道可以將花式索引中的整數數組轉換為數組下標的基本索引。通過前面的介紹將scores[[1]]轉換為scores[1]的基本索引方式。

import numpy as np    np.random.seed(666) # 設置隨機種子  scores = np.random.randint(0, 100, (3, 4))  print(scores)  score = scores[1] # 基本索引第2名學生的所有成績  print(score.shape)  print(score)    [[ 2 45 30 62]   [70 73 30 36]   [61 91 94 51]]  (4,)  [70 73 30 36]

雖然scores[[1]]的花式索引和score[1]的普通索引最後元素值相同,但是它們的維度卻有很大的差別。

如果一開始學習花式索引很容易被維度所搞亂。這裡我總結了一個小技巧,每一個整數數組作用一個維度,假設原始數組中有n個維度,使用花式索引,有第一個整數數組的時候結果維度為n,第二個整數數組後的索引結果維度為(n – 1),第三個整數數組後的索引結果維度為(n – 2),依次類推。

下面就來舉幾個小例子來實驗一下我們的小技巧。

import numpy as np    arr3d = np.arange(12).reshape(2, 2, 3)  print(arr3d.ndim) # 3  print(arr3d[[0]].ndim) # 3  print(arr3d[[0], [0]].ndim) # 2  print(arr3d[[0], [0], [0]].ndim) # 1

再來一個更加複雜一點的小例子。

import numpy as np    arr3d = np.arange(12).reshape(2, 2, 3)  print(arr3d.ndim) # 3  arr3d2 = arr3d[[0, -1]][:, [0, 1]]  print(arr3d2.ndim) # 3

arr3d[[0, -1]][:, [0, 1]]本身其實並不複雜,將arr3d[[0, -1]][:, [0, 1]]分成兩個部分,先是arr3d[[0, -1]],將結果再進行索引[:, [0, 1]],這裡只關注最後的維度,原始數組arr3d的ndim值為3,此時arr3d[[0, -1]]返回的是ndim = 3的索引結果,此時ndim = 3的數組進行[:, [0, 1]]的索引,其中只有一個整數數組,因此最終的維度還是3。