【NumPy入門】配列の基礎的な扱い方(ndarray)

この記事では、Python言語とNumPyで配列を扱う際の基礎的な部分をソースコード付きで解説します。

【はじめに】NumPyの配列とは

Pythonには、C言語の配列と似たような機能として「リスト」が標準で備わっています。
Pythonのリストは「要素数が可変」で「要素毎のデータ型が異なっていても良い」ため、使い勝手が良い反面、処理速度が遅いという欠点があります。
特に画像処理や機械学習など、大量のデータを処理する場合は処理速度の高速化が鍵となります。
そのため、Pythonで大量のデータを処理する場合は、リストではなくNumPyの配列(ndarray)を用います。
NumPyのAPIは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は直感的で短いコードでアルゴリズムを実装できることがわかります。

続いて、前述のコードの処理速度を比較してみます。
実行環境と結果は次の通りです。

■実行環境

項目 説明
入力画像 グレースケール画像(440×450[px])
OS Windows10 Home Premium 64bit
メモリ容量 4GB
CPU Core i3-2330M 2.20GHz
ソースコード 【Python】リスト型とNumPy型の処理速度比較

■結果

処理速度[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次元配列Aを生成する場合は引数に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には配列を生成するのに便利なメソッドが数多く用意されています。

関連記事
1 【NumPy入門】よく使う機能一覧
2 【NumPy】データ型の種類一覧 dtype

【ファイル操作①】CSVファイルの読み込み

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(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入門】よく使う機能一覧

【算術演算子】配列の四則演算

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のメソッドで処理を実装するのが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の使い方自体は簡単ですが、どんなメソッドが用意されているのか把握しておくのが大切です。

速度低下を抑えるテクニック
1 SciPy」など他の数値計算ライブラリで処理を実装する
2 Numba」「Cython」といったライブラリを使用してコンパイルする
3 CやFortranで一部の処理を書いて、それをPythonで呼び出す
おすすめ記事
1 Python入門 サンプル集
2 NumPy入門 サンプル集
3 【NumPy入門】よく使う機能一覧

コメント