NumPy♪ファンシーインデックスが苦手だと感じたら

ファンシーインデックスの簡単な例はすぐに理解できますが、使い方の仕組みが分からず悩んでしまいました。キーワードは「ブロードキャスト」「インデックスとして渡す配列の数」「list指定とNumPy配列指定の違い」です。ルールがわかればスッキリします。

図を使って、徹底解説します。

0.「ゼロから作るDeep Learning」のポイント整理シリーズ

この記事は「ゼロから作るDeep Learning」のポイント整理シリーズの記事です。シリーズの名前のとおり私の自習用のメモです。書籍では説明されない基礎文法や補足・関連事項などを説明しており、書籍がなくてもわかる内容になっています。

1.ファンシーインデックスを理解する3つのポイント

ファンシーインデックスとはlistやNumPy配列を利用したインデックスであり、柔軟な要素指定が可能になります。

ファンシーインデックスの要素指定方法のルールを理解する3つのポイントは以下の通りです。

  • ファンシーインデックスとして配列を1つだけ渡した場合と各次元に配列を渡した場合で指定方法が全く違う。
  • 各次元に形状の異なる配列を渡すとブロードキャストで形状がそろう
  • ファンシーインデックスとしてlistを1つだけ渡した場合、listの次元が1つ減ることがある

なお、ファンシーインデックスのルールを理解するにはブロードキャストの理解が不可欠ですので、ブロードキャストの理解に自信がないかたは以下の記事を参考にしてください。

NumPy♪ブロードキャストを雰囲気で理解していませんか?

また、「次元数」「配列の形状」「i番目の次元」といった用語が分かりにくい場合には以下の記事を参考にしてください。

Python♪用語集:NumPyの配列に関する日本語表現

それでは順番に説明したいと思います。

2.配列を1つだけ渡すと形状が保たれる

ファンシーインデックスとしてNumPy配列を1つだけ渡した場合は、渡した配列の形状が保たれます。以下、1次元配列、2次元配列でファンシーインデックスを使用した例を紹介します。

(1) 1次元配列に配列を渡す

1次元配列にNumPy配列のファンシーインデックスを使用する例を説明します。例2-1は1次元配列のファンシーインデックスを使用した例であり、例2-2は2次元配列のファンシーインデックスを使用した例です。

例2-1(1次元配列に1次元配列を渡す)

コード01は1次元配列(d1)に1次元配列のファンシーインデックス(np3)を使ったサンプルコードです。

コード01では、渡したインデックスの形状がそのまま保持されます。[1, 0, 1]ように同じインデックスを複数回指定することも可能ですし、順番も自由です。

#コード01
import numpy as np
d1 = np.array([0., 1., 2., 3.])  #shape:(4, )
np3 = np.array([1, 0, 1])        #shape:(3, )
print(d1[np3])                   #shape:(3, )
#出力01
[1. 0. 1.]
d1np3とnp3は形状が同じ
d1np3の結果

例2-2(1次元配列に2次元配列を渡す)

コード02の[[1] [0]](np21)のように、2次元以上の配列をファインシーインデックスとして使うこともできます。

#コード02
import numpy as np
d1 = np.array([0., 1., 2., 3.])  #shape:(4, )
np21 = np.array([[1], [0]])      #shape:(2, 1)
print(d1[np21])                  #shape:(2, 1)
#出力02
[[1.]
 [0.]]
d1np21とnp21は形状が同じ
d1np21の結果

(2) 多次元配列に配列を渡す

多次元配列にファンシーインデックスとして配列を1つ使用する方法を紹介します。例2-3は2次元配列に1次元配列のファンシーインデックスを使用した例であり、例2-4は2次元配列に2次元配列のファンシーインデックスを使用した例です。

例2-3(2次元配列に1次元配列を渡す)

二次元配列の場合も基本的に同じ考え方です。ただし、インデックス0に対応する要素は[0 1 2]、インデックス1に対応する要素は[10 11 12]である点に注意する必要があります。

例えばコード03のように要素を指定される側の配列(d2)が多次元配列の場合は、[0. 1. 2.]や[10. 11. 12.]が「ひとまとまり」となり、それらが1つの要素のように扱われます。

したがって、形状を保つと言いましたが、正確には次元数が増加します。

なお、この方法では2次元配列の任意の要素を1つだけ取り出すことはできません。

#コード03
import numpy as np
d2 = np.array([[0., 1., 2.], [10., 11., 12.]])
np3 = np.array([1, 0, 1])    #shape:(3,)
print(d2[np3])        #shape:(3, 3)
#出力03
[[10. 11. 12.]
 [ 0.  1.  2.]
 [10. 11. 12.]]

配列の形状は全く同じにはなりませんが、同様の形状になります。

d2np3とnp3は形状が同様
d2np3の結果

例2-4(2次元配列に2次元配列を渡す)

2次元配列に2次元配列を渡す場合も同様にインデックス0に対応する要素は[0 1 2]、インデックス1に対応する要素は[10 11 12]です。

#コード04
import numpy as np
d2 = np.array([[0., 1., 2.], [10., 11., 12.]])
np21 = np.array([[1], [0]])  #shape:(2, 1)
print(d2[np21])       #shape:(2, 1, 3)
#出力04
[[[10. 11. 12.]]

 [[ 0.  1.  2.]]]
d2np21とnp21は形状が同様
d2np21の結果

3.各次元毎に配列を渡すとブロードキャストで形状がそろう

多次元配列の場合は、コード01~04のように配列を1つだけ渡す方法だけではなく、各次元ごとに配列を渡す方法があります。

しかし、各次元ごとに配列を渡す方法は、1つだけ配列を渡す場合と要素指定のしくみがガラリと変わってしまうので、混同しないようにしましょう。

なお、各次元毎に配列を渡す方法は、配列の形状や要素を自由自在に設定できるので非常に自由度が高い方法です。

(1) 各次元に渡す配列の形状が同じ場合

各次元に渡す配列の形状が同じ場合には、ファンシーインデックスとして使用した配列の形状と出力される配列の形状は同じです。形状が違う配列を渡すこともできますが、その説明は後ほど行います。

さて、言葉だけではわかりにくいので、具体例を紹介します。

例3-1(2次元配列の各次元に1次元配列を渡す)

コード05は2次元配列の各次元に1次元配列を渡した例です。配列を1つだけ渡した場合とは全く違うことがわかります。d2は多次元配列ですが、コード03のときのように、[0. 1. 2.]や[10. 11. 12.]がひとまとまりで扱われることはなく、また、d2[np2a, np2b]は、np2a, np2bと全く同じ形状になります。

要素が指定されるしくみについてはコード05の後の図を参照してください。0番目の次元にnp3a=[0 1 1]、1番目の次元にnp3b[2 0 1]の配列を渡すことで、2次元配列d2のd2[0, 2]、d2[1, 0]、d2[1, 1]を指定することが可能です。

#コード05
import numpy as np
d2 = np.array([[0., 1., 2.], [10., 11., 12.]])
np3a = np.array([0, 1, 1])  #shape:(3, )
np3b = np.array([2, 0, 1])  #shape:(3, )
print(d2[np3a, np3b])       #shape:(3, )
#出力05
[ 2. 10. 11.]
09_d2の各次元にnp3
10_d2の各次元にnp3_形状決定
11_d2の各次元にnp3_要素決定

例3-2(2次元配列の各次元に2次元配列を渡す)

コード06は2次元配列の各次元に形状が同じ2次元配列を渡した例です。コード05と同様に、d2[np21a, np21b]の形状は、配列np21a, np21bと同じ形状になります。

このように、各次元に配列を渡せば、渡す配列の形状により、出力される配列の形状を自由に決められ、要素も自由に選択できます。

#コード06
import numpy as np
d2 = np.array([[0., 1., 2.], [10., 11., 12.]])
np21a = np.array([[0], [1]])  #shape:(2, 1)
np21b = np.array([[2], [0]])  #shape:(2, 1)
print(d2[np21a, np21b])       #shape:(2, 1)
#出力06
[[ 2.]
 [10.]]
12_d2の各次元にnp21
13_d2の各次元にnp21_形状決定
14_d2の各次元にnp21_要素決定

(2) ブロードキャスト可能な配列を渡す場合

コード05、コード06は各次元に渡す配列の形状が同じでしたが、形状が異なる配列を各次元に渡すこともできます。そして、この場合はブロードキャストによって配列の形状がそろえられます。

例3-3(2行1列の配列とスカラー)

コード07の5行目のd2[np21, 0]には、「2行1列の配列np21」と「スカラーである0」が渡されていますが、ブロードキャストにより2行2列の配列に形状がそろえられます。したがって、d2[np21, 0]は2行2列の配列になります。

#コード07
import numpy as np
d2 = np.array([[0., 1., 2.], [10., 11., 12.]])
np21 = np.array([[1], [0]])  #shape:(2, 1)
print(d2[np21, 0])           #shape:(2, 1)
#出力07
[[10.]
 [ 0.]]
15_d2の各次元にnp2と0
16_d2の各次元にnp2と0_形状決定
17_d2の各次元にnp2と0_要素決定

例3-4(要素数2の1次元配列と2行1列の2次元配列)

同様にd2[np2, np21]において、「要素数2の1次元配列np2」と「2行1列の2次元配列np21」ブロードキャストにより2行2列の配列に形状がそろえられます。

したがって、d2[np2, np21]の形状は2行2列の2次元配列になります。

#コード08
import numpy as np
d2 = np.array([[0., 1., 2.], [10., 11., 12.]])
np2 = np.array([0, 1])       #shape:(2,)
np21 = np.array([[1], [0]])  #shape:(2, 1)
print(d2[np2, np21])         #shape:(2, 2)
#出力08
[[ 1. 11.]
 [ 0. 10.]]
18_d2の各次元にnp2とnp21
19_d2の各次元にnp2とnp21_形状決定
20_d2の各次元にnp2とnp21_要素決定

(3) ブロードキャストできない配列は渡せない

互いにブロードキャストできない配列を渡すと出力09のようにエラーになります。形状(2, )と形状(3, )の配列はブロードキャストできません。

エラーメッセージの中に「broadcast」という記述があることからも、形状が異なる配列を使ったファンシーインデックスではブロードキャストの機能が用いられていることがわかります。

#コード09
import numpy as np
d2 = np.array([[0., 1., 2.], [10., 11., 12.]])
np2 = np.array([0, 1])       #shape:(2,)
np3 = np.array([0, 1, 1])    #shape:(3,)
print(d2[np2, np3])    #error
#出力09
IndexError: shape mismatch: indexing arrays could not be broadcast together with shapes (2,) (3,) 

4.listによるファンシーインデックス

listによるファンシーインデックスは、ファンシーインデックスとしてlistを1つだけ渡す場合には次元が1つ減る場合があるので要注意です。

(1) listを1つだけ渡す場合

出力10を見てください。8行目まではファンシーインデックスにNumPy配列を用いた場合もlistを用いた場合も結果が同じですが、10行目以降からはlistの方がNumPy配列よりも次元数が1つ減っています。

これがファンシーインデックスを分かりにくくしている最も大きな要因です。

ファンシーインデックスとして1つだけlistを渡した場合、その渡したlist全体を囲む一番外側の「[]」が消え、次元数が減ります。ただし、コード10の8行目のように「[]」を消すことでただの値(スカラー)になってしまう場合は次元数が減りません。

#コード10
import numpy as np
d1 = np.array([0., 1., 2., 3.])
print('d1:')
print(d1)
print('--------------------')
print(' int:   1    →',d1[            1    ])
print('--------------------')
print('list:  [1]   →',d1[           [1]   ])
print('  np:  [1]   →',d1[np.array(  [1]  )])
print('--------------------')
print('list: [[1]]  →',d1[          [[1]]  ])
print('  np: [[1]]  →',d1[np.array( [[1]] )])
print('--------------------')
print('list:[[[1]]] →',d1[         [[[1]]] ])
print('  np:[[[1]]] →',d1[np.array([[[1]]])])
#出力10
d1:
[0. 1. 2. 3.]
--------------------
 int:   1    → 1.0
--------------------
list:  [1]   → [1.]
  np:  [1]   → [1.]
--------------------
list: [[1]]  → [1.]
  np: [[1]]  → [[1.]]
--------------------
list:[[[1]]] → [[1.]]
  np:[[[1]]] → [[[1.]]]

(2) 次元数が減るlistの利便性

listの次元数が1つ減ってしまう機能は分かりにくく感じますが、コード11のように1つの変数(変数list21)を渡すだけで、各次元に異なるlistを渡すことができるので便利です。

#コード11
import numpy as np
d2 = np.array([[0., 1., 2.], [10., 11., 12.]])
list21 = [[0, 1, 1], [2, 0, 1]]  #shape:(2, 3)
print(d2[list21])                #shape:(3, )
#出力11
[ 2. 10. 11.]

同じ事をNumPy配列で行うにはコード12のように配列を2つ使用するしかありません。

#コード12
import numpy as np
d2 = np.array([[0., 1., 2.], [10., 11., 12.]])
np3a = np.array([0, 1, 1])  #shape:(3, )
np3b = np.array([2, 0, 1])  #shape:(3, )
print(d2[np3a, np3b])       #shape:(3, )
#出力12
[ 2. 10. 11.]

コード13のようにlistと同じようなことをしようとしてもエラーになります。

d2にはインデックス0の[0., 1., 2.]やインデックス1の[10., 11., 12.]はありますが、インデックス2は存在しないので、4行目の[[0, 1, 1], [2, 0, 1]]の2が存在しません。

#コード13
import numpy as np
d2 = np.array([[0., 1., 2.], [10., 11., 12.]])
np23 = np.array([[0, 1, 1], [2, 0, 1]])  #shape:(2, 3)
print(d2[np23])
#出力13
IndexError: index 2 is out of bounds for axis 0 with size 2

なお、インデックスとして使用するlistの次元数が減ること以外はNumPy配列のときと全く同じです。

(3) 各次元にlistを渡す場合

一方、各次元にlistを渡す場合にはlistとNumPy配列の出力結果は同じです。

したがって、ファンシーインデックスとして使用したlist、NumPy配列の形状がそのまま出力結果の配列の形状になります。

#コード14
import numpy as np
d2 = np.array([[0., 1., 2.],[10., 11., 12.]])

print('ファンシーインデックス[1]')
np1 = np.array([1])
list1 = [1]
print(d2[np1, np1])
print(d2[list1, list1])

print('ファンシーインデックス[[1]]')
np11 = np.array([[1]])
list11 = [[1]]
print(d2[np11, np11])
print(d2[list11, list11])

print('ファンシーインデックス[[[1]]]')
np111 = np.array([[[1]]])
list111 = [[[1]]]
print(d2[np111, np111])
print(d2[list111, list111])
#出力13
ファンシーインデックス[1]
[11.]
[11.]
ファンシーインデックス[[1]]
[[11.]]
[[11.]]
ファンシーインデックス[[[1]]]
[[[11.]]]
[[[11.]]]

5.まとめ

いかがでしょうか。ファンシーインデックスはルールが分からないうちは「あれっ?なんでこうなるの!」と悩むことが多いと思いますが、機能を順番に整理して覚えれば難しくありません。

  • ファンシーインデックスでは、まず、NumPy配列を使用した場合を理解する。
  • ファンシーインデックスとして配列を1つだけ渡した場合と各次元に配列を渡した場合では要素の指定方法が全く異なる。
  • 各次元に形状が異なる配列を渡した場合には、ブロードキャストの機能によって形状がそろえられる。(そろえられない場合はエラー)
  • ファンシーインデックスにNumPy配列を使用した場合は次元数が減らないが、listを使用した場合は次元数が減ることがある。
  • ファンシーインデックスとして1つだけlistを渡した場合、そのlist全体を囲む一番外側の「[]」が消え、次元数が減る。しかし、「[]」を消すことでただの値(スカラー)になってしまう場合は次元数が減らない。
  • ファンシーインデックスにlistを用いても、各次元にlistを渡した場合には次元数は減らない。

私が実際に購入した教材のご紹介

以下、私が実際に購入したPythonの教材をまとめてみました。 Pythonを学習する上で、少しでもお役に立つことができればうれしいです。

Python♪私が購入したPythonの書籍のレビュー
UdemyのPythonの動画講座を書籍を買う感覚で購入してみた

その他

Twitterへのリンクです。SNSもはじめました♪

以下、私が光回線を導入した時の記事一覧です。
 (1) 2020年「光回線は値段で選ぶ」では後悔する ←宅内工事の状況も説明しています。
 (2) NURO光の開通までWiFiルーターを格安レンタルできる
 (3) NURO光の屋外工事の状況をご紹介。その日に開通!
 (4) 光回線開通!実測するとNURO光はやっぱり速かった
 (5) ネット上のNURO光紹介特典は個人情報がもれないの?