概要
文字で表現するとわかり難いが、要するに次のようなことを想定している。たとえば次のような1次元の配列があるとする。
1 2 3 4 5 6 7 8 9 10 |
import numpy as np np.random.seed(0) targets = np.random.randint(0, 5, 20) print(targets) # [4 0 3 3 3 1 3 2 4 0 0 4 2 1 0 1 1 0 1 4] print(np.bincount(targets)) # [5 5 2 4 4] |
この配列には20個の要素があり、0~4の数値がそれぞれ5個、5個、2個、2個、4個、4個、順不同で含まれている。
この配列において、各数値の数を最大でも3個以内となるように切り落としたい、というのが目標。
たとえば、機械学習の教師データの数がターゲットごとにばらついている場合、各ターゲットのデータ数をある程度の数以下に抑えたいときが想定される。
上の例で仮に早く出現した準から3つまでを残して後は捨てるとすれば、以下のような配列になる。
内容:4 0 3 3 3 1
3 2 4 0 0 4 2 1 0 1 1 0 1 4
個数:1 1 1 2 3 1
4 1 2 2 3 3 2 2 4 3 4 5 5 4
なお、単に1つの配列の要素を切り落とすだけでなく、これと対応する配列が別にあって、その要素についても同時に切り落とすことも想定する。これは、機械学習のターゲット配列でデータを制限するのに、これに紐づけられた画像データなどを格納した配列も同時に操作するイメージ。
手順
ターゲットごとのインデックスの取得
targets
の20個のデータのうちid=0について考える。targets
の要素のうち値が0のものは5個あり、それらのインデックスは(1, 9, 10, 14, 17)。同様にid=1についても5個あり、インデックスは(5, 13, 15, 16, 18)。
このようにしてid=0~4についてインデックスを書き出すと以下の通りになる。
0:1, 9, 10, 14, 17
1:5, 13, 15, 16, 18
2:7, 12
3:2, 3, 4, 6
4:0, 8, 11, 19
各idに対応する配列はnumpy.where()
関数を用いて以下のように得られる。
1 2 3 4 5 6 7 8 9 |
for id in range(5): array = np.where(targets == id)[0] print("{}:{}".format(id, array)) # 0:[ 1 9 10 14 17] # 1:[ 5 13 15 16 18] # 2:[ 7 12] # 3:[2 3 4 6] # 4:[ 0 8 11 19] |
上の例では、ループのidを0~4と変化させていくのにrange(5)
を使っている。ところが一般的には、番号が連続して存在しているとは限らず、またその上限もわからない。
そこで、targets
に出てくる要素を重なりなく、かつ全て使うためにnumpy.unique()
関数を使っている。unique()
関数は引数の配列の要素の重複を除き、昇順・辞書順に並べてくれる。この引数にtargets
を渡して、要素の重なりを除けば、targets
中の要素を重なりなく1つずつ参照できる。
1 2 3 4 5 6 7 8 9 10 11 12 |
print(np.unique(targets)) # [0 1 2 3 4] for id in np.unique(targets): array = np.where(targets == id)[0] print("{}:{}".format(id, array)) # [ 1 9 10 14 17] # [ 5 13 15 16 18] # [ 7 12] # [2 3 4 6] # [ 0 8 11 19] |
取り出す要素の制限
次に、すべてのターゲットのデータ数が3個以下になるようにすることを考える。
これらのデータで各idの個数を3個以下にするのに、出現順位の早いものから3個を選び出すことを考える。
0:1, 9, 10, 14, 17
1:5, 13, 15, 16, 18
2:7, 12
3:2, 3, 4, 6
4:0, 8, 11, 19
各配列の最初の3個を取り出すには、各idに対応する配列の先頭から3個目までをスライスで取り出せばよい。
1 2 3 4 5 6 7 8 9 |
for id in np.unique(targets): array = np.where(targets == id)[0][:3] print("{}:{}".format(id, array)) # 0:[ 1 9 10] # 1:[ 5 13 15] # 2:[ 7 12] # 3:[2 3 4] # 4:[ 0 8 11] |
これで値の最大3個までとするのに取り出すべきtargets
中のインデックスが得られた。
要素の抽出
targets
配列の要素の個数を制限するには、上で絞り込まれたインデックスに対応する要素を残し、それ以外の要素を切り捨てる。そのためには、残すべきインデックス位置の値がTrue
、その他のインデックス位置の値がFalse
であるbool
配列をつくり、これをtargetsの引数とすればよい。
この配列を例えばmask
という名前とすると、targets
と同じサイズですべての要素がFalse
である配列としてmask
を準備し、先ほどの切り落とすべきインデックスの位置のみTrue
にするとよい。
以下では、まず全要素がFalse
でtargets
と同じサイズのbool
配列を準備し、各idに対して3つ目までの要素の位置をTrue
(1)としている。
ループの1回目で1、9、10番目がTrue
になり、2回目で5、13、15番目がTrue
に代わっていき、ループを重ねるごとに、取り出すべき要素の位置がTrue
になっていることが確認できる。
なおbool
配列の初期化では、False
が数値の0と等価なため、numpy.zeros()
関数を使っている。同じ理由で、numpy.where()
でTrue
をセットするときに、数値の1をセットしている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
mask = np.zeros(targets.size, dtype=np.bool) print(mask) # [False False False False False False False False False False False False # False False False False False False False False] for target in np.unique(targets): mask[np.where(targets == target)[0][:3]] = 1 print(mask) # [False True False False False False False False False True True False # False False False False False False False False] # [False True False False False True False False False True True False # False True False True False False False False] # [False True False False False True False True False True True False # True True False True False False False False] # [False True True True True True False True False True True False # True True False True False False False False] # [ True True True True True True False True True True True True # True True False True False False False False] |
最後に、このbool
配列をtargets
に適用して、取り出すべき要素の配列を得る。
1 2 3 4 5 |
print(targets[mask]) # [4 0 3 3 3 1 2 4 0 0 4 2 1 1] print(np.bincount(targets[mask])) # [3 3 2 3 3] |
他の配列の同時操作
mask配列は、targetsと同じサイズを持つ次元の配列に繰り返し適用できるので、たとえば機械学習でtargets
の各要素に紐づけられた画像データなどを格納した配列などについても、targets
と整合させながら必要な分だけ切出すことができる。