FC2ブログ

子供の落書き帳 Remix

15/4/13:ひと月に一度更新するブログになってしまっている

Numpyのスライスによる要素取り出し、booleanによるインデックス指定について
2018/07/21(土) 17:46:28

きっかけ


scikit-learnとTensorFlowによる実践機械学習の輪読会に参加して読み進めている。

その中によく分からないコードがあったので、調査をしてまとめた。


動作が分からなかったコードがこれだ



handson-ml/06_decision_trees.ipynb at master | ageron/handson-ml | GitHubの[7]である。
(このソースコード自体は決定木について扱っている。しかし分からなかった部分は決定木に直接関係するものではなく、単なるNumpyのコードである)


from sklearn.datasets import load_iris
iris = load_iris()
X = iris.data[:, 2:]
y = iris.target

X[(X[:, 1]==X[:, 1][y==1].max()) & (y==1)] # widest Iris-Versicolor flower

# => array([[4.8, 1.8]])


扱っているのは、機械学習でよく登場するiris(アヤメ)のデータセットだ。Xにはアヤメの「花弁の長さ」「花弁の幅」が2次元配列の形で入っている。yは各アヤメの品種を表す。
やっている処理は、本によれば「ある特定の品種(versicolor)のデータのうち、花弁の幅が最大のものを探し出すと、花弁の長さが4.8、花弁の幅が1.8である」らしい。しかし、どうしてこのコードでそれが出てくるのか分からなかった。

調べてみると、以下の3つの要素が組み合わさっている式だと分かった。順に説明する。

(A) X[:, 1] で特定行だけを抽出する



変数のあとに[]をつけて、その中で要素のインデックスを指定している。この方法の正式名称はスライス(スライシング)という。

2次元配列Xに対してX[:, 1]と書くと、行列の2列めだけを抽出する。

1つ目の次元(=行方向)に関しては何も条件を入れず、全ての要素を抽出している。「:」は全ての要素を表わす。
2つ目の次元(=列方向)については0始まりのインデックスで1の位置だけを抽出している。(つまり左から2番目の列である)

元の例はちょっとサイズが大きいので、小さい2次元配列の例でやってみよう。行列の2列めだけを抽出しているのが分かる。


import numpy as np
X1 = np.arange(12).reshape(3,4)

X1
# => array([[ 0, 1, 2, 3],
# [ 4, 5, 6, 7],
# [ 8, 9, 10, 11]])

X1[:,1]
# => array([1, 5, 9])


(B) 上記のスライシングで、末尾の, :は省略してもよい




NumPyの公式ページにはこう書いてある。


If the number of objects in the selection tuple is less than N , then : is assumed for any subsequent dimensions.
拙訳:
選択に用いたタプルの中のオブジェクト数がNより小さい場合、後に続く全て次元に関して「:」があるものと見なされる。
Indexing -- NumPy v1.14 Manual



例えば、2次元配列Xに対してX[1:3]と指定すると、X[1:3, :]と指定したと見なされる、ということだ。


X1[1:3]
# => array([[ 4, 5, 6, 7],
# [ 8, 9, 10, 11]])


実際には2次元の使用例が多いが、2次元配列に限らず、一般の多次元配列に対しても同様である。
例えば、Numpyの例では3次元配列に対して適用している。



# Numpy公式より引用:

x = np.array([[[1],[2],[3]], [[4],[5],[6]]])
x.shape
# => (2, 3, 1)
x[1:2]
# => array([[[4],
# [5],
# [6]]])


上の例で、x[1:2]はx[1:2, :, :]と書いたのと同じになる。

(C) X[条件式]と書くと、条件を満たすものだけを抽出できる



Numpy公式ページでは「Boolean array indexing」という名前で載っている。


X2 = np.arange(9)
np.random.shuffle(X2)

X2
# => array([5, 3, 0, 2, 1, 4, 8, 7, 6])

boolean_array = [True, False, True, False, True, False, True, False, True]
X2[boolean_array]
# => array([5, 0, 1, 8, 6])

X2[X2 < 5]
# => array([3, 0, 2, 1, 4])

X3 = X2.reshape(3,3)

X3
# => array([[5, 3, 0],
[2, 1, 4],
[8, 7, 6]])

X3[X3<3]
# => array([0, 2, 1])
# 注意:元の配列が2次元であっても、1次元配列に展開されることに注意!


参考文献



これらのページはとても参考になった。
(A),(B) NumPy配列ndarrayのスライスによる部分配列の選択と代入 | note.nkmk.me
(C) NumPy配列ndarrayから条件を満たす要素・行・列を抽出、削除 | note.nkmk.me



以上を踏まえて、もう一度読み解いてみると、こうなる



最後に、元のコードがどういう理屈で「ある特定の品種(versicolor)のデータのうち、花弁の幅が最大のもの」を抽出したのか、整理しておく。(元のコードに興味のない人は、これ以降は読まなくて良い。)

元々のコードを再度掲載しておく。

X[(X[:, 1]==X[:, 1][y==1].max()) & (y==1)]


少しずつ順番に見ていこう。
X[:, 1]は(A)で書いたとおり、「行列の2行目」だけを取り出す。
2次元配列Xの1行目には花弁の長さ、2行目には花弁の幅が入っているので、2行目の花弁の幅だけがが抽出される。

次にX[:, 1][y==1] だ。[y==1]はTrueとFalseの配列なので、それを指定すると(C)のBoolean array indexingによって条件を満たすものだけを取り出す。
日本語で書くと「y(品種)が1であるアヤメの花弁の幅」だけを抽出している。

X[:, 1][y==1].max()は上記のうちの最大値である。この値は1.8なので、分かりやすいように式を置き換えてしまおう。


X[(X[:, 1]==X[:, 1][y==1].max()) & (y==1)]

X[(X[:, 1]==1.8) & (y==1)]


ここまでくればあとはほぼ同じである。
X[:, 1]==1.8は「花弁の幅が」1.8かどうかのTrue/Falseの配列である。

(X[:, 1]==1.8) & (y==1) は
「y(品種)が1、かつXが1.8」のところだけがTrueであとがFalseの配列である。

最後に、これをXのインデックスに指定するとX[(X[:, 1]==1.8) & (y==1)] となり、これで
「ある特定の品種(1 = versicolor)のデータのうち、花弁の幅が最大のもの」が指定できた。

  1. 2018/07/21(土) 17:46:28|
  2. プログラミング
  3. | トラックバック:0
  4. | コメント:0

コメント


管理者にだけ表示を許可する

トラックバック

トラックバック URL
http://luvtome.blog5.fc2.com/tb.php/650-2923fb3a
この記事にトラックバックする(FC2ブログユーザー)

FC2Ad