Python言語とNumPyで配列を扱う際の基礎的な部分をソースコード付きで解説します。
NumPyとは
本ページの内容は、以下動画で解説しています。
Pythonには、C言語の配列と似たような機能として「リスト」が標準で備わっています。
Pythonのリストは「要素数が可変」で「要素毎のデータ型が異なっていても良い」ため、使い勝手が良い反面、処理速度が遅いという欠点があります。
特に画像処理や機械学習など、大量のデータを処理する場合は処理速度の高速化が鍵となります。
そのため、Pythonで大量のデータを処理する場合は、リストではなくNumPyの配列(ndarray)を用います。
NumPyの中身はC言語とFortranで実装されているため、リストと比べて処理速度が非常に高速である利点があります。
また、算術演算子だけでベクトル演算が出来たり、短いコードで数値計算できる機能が数多く備わっているのも利点です。
活用例①画像の2値化処理
NumPyの活用例として、簡単な画像処理である2値化処理を例にします。
「C言語」「Python+リスト」「Python+NumPyの配列」の3パターンで2値化処理を実装した場合のアルゴリズム部分のコードを比較してみます。
C言語
for (y = 0; y < height ; y++){ for (x = 0; x < width; x++){ if (img[y][x] >= 160){ img[y][x] = 255; } else{ img[y][x] = 0; } } }
Python+リスト
for y in range(0, height): for x in range(0, width): if img[y][x] >= 160: img[y][x] = 255 else: img[y][x] = 0
Python+NumPy
img[img<160] = 0 img[img>=160] = 255
Python+NumPyだと直感的かつ短いコードでアルゴリズムを実装できることがわかります。
処理速度の比較
前述のコードの処理速度を比較してみます。
Core i3-2330M 2.20GHzのCPUを搭載したWindowsPCで入力画像(40×450[px]のグレースケール画像)に対して、2値化処理を行った結果が以下の通りです。
– | 処理速度[ms] |
---|---|
Python+リスト | 67.04449653625488 |
Python+NumPy | 0.99945068359375 |
リストが約67[ms]、NumPyが約1[ms]と約67倍も処理速度が高速化されています。
このように、PythonとNumPyを組み合わせると、画像処理や機械学習のアルゴリズムを短いコードで、かつ高速な処理速度で実装できます。
本章では、NumPyの配列の基本的な扱い方を紹介していきます。
なお、本ページ中に何度も登場する「配列」という言葉は「NumPyの配列」を示すものとします。
配列の生成(numpy.array、numpy.empty)
numpy.array(list, dtype)メソッドにPython標準のリストを渡すことで配列を生成できます。
1次元配列を生成する場合は引数listに1次元リスト、2次元配列を生成する場合は引数に2次元リストを渡します。
配列のデータ型はdtypeで指定します。データ型を指定しなくても自動的に決まりますが、バグの元になりやすいため、きちっとおこなった方が安全です。
配列の要素を確認したい場合は、リストと同じようにprint関数を用います。
print(ndarray)
配列のデータ型を確認した場合は、dtype属性を用います。
print(ndarray.ndarray)
サンプルコード
配列の生成・中身の確認
# -*- coding: utf-8 -*- import numpy as np # 1次元配列の生成 list1 = [1, 2, 3] x = np.array(list1, dtype='int32') print(x.dtype) # int32 print(x) # [1, 2, 3] # 2次元配列の生成 list2 = [[1, 2, 3], [4, 5, 6]] X = np.array(list2, dtype='float32') print(X.dtype) # float32 print(X) # [[1 2 3] # [4 5 6]]
種類 | データ型の一覧表 |
---|---|
bool | 論理値型 |
inti | OS依存の整数(64bitのOSならint型64ビット) |
int8 | 8ビットの整数型 |
int16 | 16ビットの整数型 |
int32 | 32ビットの整数型 |
int64 | 64ビットの整数型 |
unit8 | 8ビット符号なし整数型(画像処理などでよく使います) |
unit16 | 16ビット符号なし整数型 |
unit32 | 32ビット符号なし整数型 |
unit64 | 64ビット符号なし整数型 |
float16 | 16ビットの実数型 |
float32 | 32ビットの実数型 |
float64 | 64ビットの実数型 |
complex64 | 64ビットの複素数型 |
complex128 | 128ビットの複素数型 |
要素の値を初期化する必要がない場合は、numpy.empty(shape)を使ったほうが高速に配列を生成できます。
その場合、引数にはリストではなく配列の形状(2次元なら行と列のサイズ)を指定します。
# 2 × 3の空配列を生成 X = np.empty((2, 3), dtype='float32') print(X) # [[ 2.31948455e-310 2.31948455e-310 2.31948683e-310] # [ 2.31948683e-310 7.06652082e-096 7.05479222e-308]]
他にも、NumPyには配列を生成するのに便利なメソッドが数多く用意されています。
CSVファイルの読み込み(numpy.genfromtxt)
NumPyでは、外部ファイルからデータを読み込んで配列に格納するnumpy.genfromtxt(path, delimiter, dtype) メソッドが用意されています。
ここで、pathは読み込むファイルパス、delimiterは区切り文字、dtypeはデータ型を指定します。
このメソッドを使えば、CSVファイルなどのテキスト形式のファイルを読み込み、中身のデータを配列に格納するまで処理をたった1行で記述できます。後で紹介する応用例では、ネット上で公開されている日経平均株価の過去1年分のデータが記録されているCSVファイルを読み込んで統計処理してみます。
サンプルコード
CSVファイルを読み込みます。
import numpy as np # CSVファイルを読み込み(区切り文字はカンマ) X = np.genfromtxt('data.csv', delimiter=',') # 配列の中身を確認 print(X) #[[ 1. 2. 3. 4.] # [ 5. 6. 7. 8.] # [ 9. 10. 11. 12.]]
data.csv
1,2,3,4 5,6,7,8 9,10,11,12
CSVファイルに書き込み(numpy.savetxt)
配列のデータを外部ファイルに保存する場合は、numpy.savetxt(path, ndarray, delimiter, fmt)メソッドを使います。
pathはファイルパス、ndarrayは配列、delimiterは区切り文字、fmtはデータ型を指定します。
例えば、区切り文字をカンマにして、CSV形式でデータを保存すればExcelなど多くのソフトウェアで開くことができます。
サンプルコード
# -*- coding: utf-8 -*- import numpy as np # 2次元配列の生成 X = np.array([[1, 2, 3], [4, 5, 6]]) # 二乗の計算 Y = X ** 2 # CSVファイルに2次元配列Yのデータを出力 np.savetxt('data.csv', Y, delimiter=",", fmt='%d')
data.csv
1,4,9 16,25,36
統計処理
NumPyには、簡単な統計処理からフーリエ変換まで、様々なデータ処理用のメソッドが用意されています
NumPyは、これらのメソッドを活用して処理を実装していくことが重要です。
例えば、平均値を計算したい場合はnumpy.average(x)メソッドを用います。
引数に配列xを渡すだけで計算結果が返ってきます。
サンプルコード
# -*- coding: utf-8 -*- import numpy as np # 1次元配列の生成 x = np.array([1, 2, 3, 4, 5]) # 平均値の計算 ave = np.average(x) print(ave) # 3.0
他にもNumPyには様々な計算用のメソッドが用意されています。
配列の四則演算(算術演算子)
NumPyでは、算術演算子だけで配列の要素同士の四則演算ができます。
サンプルコード
# -*- coding: utf-8 -*- import numpy as np # 1次元配列の生成 x = np.array([4, 5, 6]) y = np.array([3, 2, 1]) # 加算 print(x + y) # [7 7 7] # 減算 print(x - y) # [1 3 5] # 乗算 print(x * y) # [12 10 6] # 除算 print(x / y) # [ 1.33333333 2.5 6. ] # 剰余 print(x % y) # [1 1 0] # 冪乗(3乗) print(x ** 3) # [ 64 125 216] # 符号反転 print(-x) # [-4 -5 -6]
算術演算子「+」「-」だけで2つのベクトル・行列の加減算をすることができます。
ただし、「*」は内積でなく要素同士の積算となるので注意しましょう。
内積を計算したい場合は、「x.dot(y)」を用います。
スライスで要素へアクセス
NumPyの配列は、リストと同じようにx[index]で要素へアクセスできます。
indexは0始まりで、x[0]なら先頭要素、x[1]ならその次の要素を示します。
負の値を指定すると末尾からアクセスできます。(x[-1]なら末尾要素)
勿論、リストでもお馴染みの便利なスライスも使えます。
ndarray[i:j]のようにコロン区切りでi~j-1番目までの要素を切り出します。
1次元配列で要素へアクセスする例を次のサンプルコードで見てみましょう。
サンプルコード
1次元配列の要素にアクセスします。
# -*- coding: utf-8 -*- import numpy as np # 1次元配列の生成 x = np.array([1, 2, 3, 4, 5]) # 先頭要素の参照 print(x[0]) # 1 # 末尾要素の参照 print(x[-1]) # 5 # スライスで参照(1~3番要素) print(x[1:4]) # [2 3 4]
2次元配列の要素にアクセスする場合は、 カンマ区切りで行・列の順にindexを指定します。
インデックスに数値でなくコロンを入れた場合は、指定した行もしくは列の全ての要素を指定します。
サンプルコード
2次元配列の要素にアクセスします。
# -*- coding: utf-8 -*- import numpy as np # 2次元配列の生成 X = np.array([[1, 2, 3], [4, 5, 6]]) # 0行目, 1列目にある要素の参照 print(X[0,1]) # 2 # 行の参照(0行目) print(X[0,:]) # [1 2 3] #列の参照(2列目) print(X[:,2]) # [3 6] # スライスで参照(0~1行目、1~2列目にある要素) print(X[0:2, 1:3]) # [[2 3] # [5 6]]
ブールインデックス(マスク処理)で要素へアクセス
要素にアクセスする他の手段として「ブールインデックス」があります。
ブールインデックスは、配列に対して同じサイズの論理値(True/False)を格納した配列を与えると、trueの要素についてのみ処理を行うことができる機能です。
これを使うことで、マスク処理ができます。
サンプルコードを見てみましょう。
# -*- coding: utf-8 -*- import numpy as np # NumPyのインポート # 1次元配列の生成 x = np.array([1, 2, 3, 4, 5]) # マスク用の配列を生成 mask = np.array([True, False, False, True, False]) # Trueの要素のみ取り出し y = x[mask] print(y) # [1 4] # Trueの要素のみ値を代入 x[mask] = 7 print(x) # [7 2 3 7 5]
「y = x[mask]」では、マスク配列のTrueとなっている部分の要素を配列xから取り出し、新たに生成された配列yに格納しています。
「x[mask] = 7」では、マスク配列のTrueとなっている部分の要素のみ代入しています。
このマスク処理は、比較演算子と組み合わせることで応用できます。
配列に対して比較演算子を与えると、比較結果(論理値)を格納した配列を取得できます。
これを元の配列に与えることで、特定の条件を満たす要素に対してのみ処理を行うことが可能です。
サンプルコードを見てみましょう。
# -*- coding: utf-8 -*- import numpy as np # NumPyのインポート # 1次元配列の生成 x = np.array([1, 2, 3, 4, 5]) # 比較演算子でマスク配列を生成 mask = x >= 3 print(mask) # [False False True True True] # マスク処理 y = x[mask] print(y) # [3 4 5]
このサンプルでは、配列xの要素が3以上ならTrue、そうでなければFalseとなるマスク配列を生成します。
そして、ブールインデックスを使って結果的に値が3以上の要素のみを取り出しています。
for文で要素へアクセス
リストと同様、配列でもfor文を使って順に要素へアクセスできます。
サンプルコード
# -*- coding: utf-8 -*- import numpy as np # 1次元配列の生成 x = np.array([1, 2, 3, 4, 5]) # for文+enumerate関数で配列から要素とインデックスを順に取り出す for index, a in enumerate(x): print('x[%d]=%d' % (index, a)) # x[0]=1 # x[1]=2 # x[2]=3 # x[3]=4 # x[4]=5
for文でループ処理を行う時にenumerate関数を使うと、要素aとそのインデックス(index)の両方を同時に取得できます。
ただし、for文で要素にアクセスしていくと処理速度が著しく低下していまします。
特に画像処理などで大量の要素をfor文で順に処理した場合は命取りとなります。
for文はできるだけ使わず、NumPyのメソッドで処理を実装するのがコツです。
例えば、配列から値が3である要素のインデックスを探索する場合の良い例とそうでない例を見てみましょう。
良い例
# -*- coding: utf-8 -*- import numpy as np # 1次元配列の生成 x = np.array([1, 2, 3, 4, 5]) # 値3をもつ要素のインデックスを探索(NumPyのメソッドで実装) index = np.where(x==3) print('値3のインデックス:', index[0]) # # 値3のインデックス [2]
良くない例
# -*- coding: utf-8 -*- import numpy as np # 1次元配列の生成 x = np.array([1, 2, 3, 4, 5]) # 値3をもつ要素のインデックスを探索(for文で実装) for i, a in enumerate(x): if(a == 3): print('値3のインデックス:', i) # # 値3のインデックス2
どうしてもfor文で大量のデータを扱わざる負えない場合は、以下の方法を取ると処理速度の低下をある程度抑えることができます。
NumPyの使い方自体は簡単ですが、どんなメソッドが用意されているのか把握しておくのが大切です。
コメント