Numpy 入門
(機械学習勉強会 in 新潟 2019/03/23)
目次
1 はじめに
2 numpy とは何か
Python の数値計算のためのライブラリです。 numpy によってできることとメリットは以下になります
No. | ライブラリ | 説明 | できること、メリット |
---|---|---|---|
1 | numpy | 数値計算ライブラリ | 1. 行列の計算を行うコードを短く書ける |
2. 行列の計算、ベクトルの計算の高速化( 速度比較 参照) |
3 numpy モジュールのインポート方法について
numpy を使用する場合、numpy モジュールのインポートは以下のように行います。
コード1. 「モジュールのインポート」:
import numpy as np
これにより、'np' の2文字で numpy を使用することができます。
コード2. 「モジュールの使用方法」:
import numpy as np arr = np.array([1,2,3]) arr
Out: array([1, 2, 3])
使用する名前は numpy など np 以外の名前にもできますが、 np と書くのが一般的になっているためこちらを使用しましょう。
4 数値を作成する
はじめに、numpy で数値を作成しましょう。数値は、スカラ(Scalar)と呼ばれることがあります。 これらは、後述する配列の要素( 5.1.1 参照)となりますので、しっかり覚えましょう。 np.int32 を使用すると numpy の整数を作成できます。
コード3. 「numpy で数値の作成」:
import numpy as np answer = np.int32(100) answer
Out: 100
4.1 np.int32 とは
np.int32 はデータタイプ(dtype)として定義されています。 np.int32(100) を実行すると np.int32 データタイプのオブジェクトが作成されます。 np.int32 以外にも数値を表わすデータタイプが用意されています。
データタイプ | 数値の種類 | 用途 |
---|---|---|
np.uint8 | 8bit 符号無し整数 | グレースケール画像 |
np.int32 | 32bit 整数 | 画像処理 |
np.float32 | 32bit 浮動少数 | 画像処理、平均値、確率 |
np.float64 | 64bit 浮動少数 | 数値計算(精度が求められるもの) |
… |
真偽値(True/False)を現す場合は np.bool データタイプが用意されています。
5 「1次元配列」の数値計算をする
配列の基本となる1次元配について学習しましょう。
5.1 「1次元配列」を作成する
1次元の配列を作成しましょう。1次元配列とは以下のようなものです。配列は'要素'と呼ばれるもので構成されています。
値1 | 値2 | 値3 | ,,, |
5.1.1 配列の'要素'とは
一般的に、上記 5.1 のような配列の中にある値1や値2などのことを配列の”要素”(Element)と呼びます。 1次元配列だけでなく、2次元配列や3次元配列も同様です。配列は要素によって構成されています。
※ numpy では配列を作成する関数の引数に dtype を指定することで、その配列の要素のデータタイプを指定できます。 引数 dtype を指定しない場合は、自動的に配列のデータタイプが決定されます。 numpy では配列は同一のデータタイプの要素から構成されます。
5.1.2 np.array 関数
np.array 関数を使用すると、標準 Python のリストを配列に変換することができます.
コード4. 「標準 Python のリストを配列に変換する」:
import numpy as np arr = np.array([1, 2, 3]) arr
Out: array([1, 2, 3])
5.1.3 np.arange 関数
np.arange 関数を使用すると、数値の1次元配列を作成することができます。
コード5. 「np.arange 関数を使用し、数値の1次元配列を作成」:
import numpy as np arr = np.arange(3) arr
Out: array([0, 1, 2])
5.2 「1次元配列」の形状 shape について
配列には shape と呼ばれる、配列の形状を表す情報があります。 配列の形状 shape を調べるには以下のようにします。 この例の場合、配列は長さ3の1次元配列であることがわかります。
コード6. 「配列の形状 shape を調べる」:
import numpy as np arr = np.arange(3) answer = arr.shape answer
Out: (3,)
5.3 「1次元配列」 + 「1次元配列」
1次元配列と1次元配列の四則演算の基本を学習します。 ここでは例として、1次元配列と1次元配列の足し算を行います。 以下のように、1次元配列を二つ作成し、足し算記号(+) を使用して求めます。 計算は、要素ごと(element wise)に行われます。
コード7. 「1次元配列と1次元配列の足し算」:
import numpy as np arr_1 = np.array([1, 2, 3]) arr_2 = np.array([0, 0, 1]) answer = arr_1 + arr_2 answer
Out: array([1, 2, 4])
その他の演算子(-*/など)についても、同じように、配列の要素ごとに計算されます。 形状の異なる配列同士の計算は、エラーになるか、ならない場合があります。 形状の異なる配列同士の計算については 11 を参照してください。
5.4 配列の要素に数学関数を適用する
1次元配列の要素ごと(element wise)に数学関数を適用してみましょう。 ここでは例として平方根を適用してみましょう。
コード8. 「配列の各要素に平方根を適用」:
import numpy as np arr = np.array([1, 2, 3]) answer = np.sqrt(arr) answer
Out: array([1. , 1.41421356, 1.73205081])
np.sqrt 関数以外にも様々な数学関数が用意されています。 それらの関数も、上記のように使用することで、要素ごとに適用することができます。 以下に数学関数の一部を記載します。
No. | 関数 | 説明 |
1 | np.exp | 指数関数 |
2 | np.log | 対数関数 |
3 | np.sin | sin関数 |
4 | np.arcsin | arcsin関数 |
5.5 1次元配列の要素の合計を求める
合計を求める場合は、 np.sum 関数を使用します。
コード9. 「np.sum 関数を使用して1次元配列の要素の合計を求める」:
import numpy as np arr = np.arange(11) answer = np.sum(arr) answer
Out: 55
6 「2次元配列」の数値計算をする
2次元配列について学習しましょう。
6.1 「2次元配列」を作成する
2次元の配列を作成しましょう。2次元配列とは以下のようなものです。
値11 | 値12 | 値13 | ,,, | |
値21 | 値22 | 値23 | ,,, | |
値31 | 値32 | 値33 | ,,, | |
,,, |
6.1.1 np.array 関数
np.array 関数を使用すると、2次元配列を作成することができます。(1次元配列の時と同様)
コード10. 「np.array 関数を使用して2次元配列を作成する」:
import numpy as np arr = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) arr
Out: array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
6.1.2 reshape メソッド
配列の reshape メソッドを使用すると配列の形状を変えることができます。 ここでは、 1次元配列の形状を変えて、2次元配列を作成してみましょう。
コード11. 「reshape メソッドを使用して1次元配列の形状を変えて、2次元配列を作成する」:
import numpy as np arr = np.arange(9) answer = arr.reshape((3, 3)) answer
Out: array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
6.2 「2次元配列」 + 「2次元配列」
2次元配列同士の四則演算、関数を適用した時の動作は、1次元配列の時と同様、要素ごとに計算が行われます。 (そのため2次元配列の四則演算については説明を省略します。)2次元配列を使用すると、行列の掛け算を行うことができます。
行列の掛け算とは、具体的には以下のような計算のことです。 行列 A と行列 B の掛け算(積)は A・B と書き、A と B と A・B は以下のような関係になります。 ※説明の簡略化のため行列の大きさは2x2としています。
行列 A
a b c d 行列 B
e f g h 行列 A・B
a*e+b*g a*f+b*h c*e+d*g c*f+d*h
行列の掛け算を行うには np.dot 関数を使用します。
コード12. 「np.dot 関数を使用して行列の掛け算を行う」:
import numpy as np arr_1 = np.arange(9).reshape((3, 3)) arr_2 = np.arange(9).reshape((3, 3)) answer = np.dot(arr_1, arr_2) answer
Out: array([[ 15, 18, 21], [ 42, 54, 66], [ 69, 90, 111]])
配列オブジェクトの dot メソッドを使用して以下のように書くこともできます。
コード13. 「dot メソッドを使用して行列の掛け算を行う」:
import numpy as np arr_1 = np.arange(9).reshape((3, 3)) arr_2 = np.arange(9).reshape((3, 3)) answer = arr_1.dot(arr_2) answer
Out: array([[ 15, 18, 21], [ 42, 54, 66], [ 69, 90, 111]])
注意: np.dot は '∗'による掛け算と同じではありません。 '∗' による掛け算は要素ごと(element wise)に掛け算が行わます。 一方 np.dot は行列の掛け算ですので、積和演算(掛け算と足し算)が行われます。
6.3 「2次元配列」の要素の合計を求める
'すべての要素の合計'を求める場合は、1次元配列の時と変わりませんので省略しますが、 2次元配列では、さらに、'列の合計'と'行の合計'を求めることができます。
6.3.1 列の合計
np.sum 関数の引数で axis=0 とすることで列の合計を求めることができます。
コード14. 「列の合計を求める」:
import numpy as np arr = np.arange(9).reshape((3, 3)) answer = np.sum(arr, axis=0) answer
Out: array([ 9, 12, 15])
6.3.2 行の合計
np.sum 関数の引数で axis=1 とすることで行の合計を求めることができます。
コード15. 「行の合計を求める」:
import numpy as np arr = np.arange(9).reshape((3, 3)) answer = np.sum(arr, axis=1) answer
Out: array([ 3, 12, 21])
6.3.3 配列の次元数を保つには
6.3.1 、 6.3.2 で np.sum を使用して求めた配列を見ると、2次元配列の入力に対して出力が1次元配列となっており、次元数が変わってしまうことに気付くかと思います。 np.sum 関数の引数で keepdims=True とすることで、配列の次元数を保つことができます。
コード16. 「np.sum 関数の引数で keepdims=True とし配列の構造をなるべく保つ」:
import numpy as np arr = np.arange(9).reshape((3, 3)) answer = np.sum(arr, axis=1, keepdims=True) answer
Out: array([[ 3], [12], [21]])
6.4 行列計算用の関数
2次元配列には行列計算用の関数を使用することができます。 行列計算用の関数は np.linalg にあります。 その一部を紹介します。
関数 | 説明 |
---|---|
np.linalg.inv | 逆行列を求めるための関数 |
np.linalg.norm | 行列のノルムを求めるための関数 |
7 配列を作成する関数
7.1 固定された値を要素にもつ配列を作成する関数
No. | 関数 | 説明 |
---|---|---|
1 | np.zeros | 全ての要素が 0 の配列を作成します |
2 | np.ones | 全ての要素が 1 の配列を作成します |
3 | np.full | 全ての要素が x の配列を作成します(x は引数で指定) |
コード17. 「np.zeros 関数を使用し全ての要素が 0 の配列を作成」:
import numpy as np arr = np.zeros((3, 3)) arr
Out: array([[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]])
7.2 単位行列を作成する関数
7.3 ランダムな値を要素にもつ配列を作成する関数
No. | 関数 | 説明 |
---|---|---|
1 | np.random.randint | ランダムな整数の配列を作成します |
2 | np.random.random | ランダムな少数の配列を作成します |
コード19. 「np.random.randint 関数を使用しランダムな整数の配列を作成」:
import numpy as np arr = np.random.randint(0, 10, (3, 3)) arr
Out: array([[4, 0, 7], [2, 3, 4], [3, 0, 5]])
コード20. 「np.random.random 関数を使用しランダムな少数の配列を作成」:
import numpy as np arr = np.random.random((3, 3)) arr
Out: array([[0.23963676, 0.14041946, 0.67624721], [0.75209081, 0.3182057 , 0.18579186], [0.0888395 , 0.87384273, 0.24740674]])
7.4 似たような配列を作成する関数
配列の計算をプログラミングしているときに、ある配列と同じ形状をもつ配列だが、値は違うという配列を作成したい時があります。そのような場合は、以下の関数を使用します。
No. | 関数 | 説明 |
---|---|---|
1 | np.zeros_like | 引数で指定した配列と同じ形状 |
& | ||
全ての要素が 0 の配列を作成します | ||
2 | np.ones_like | 引数で指定した配列と同じ形状 |
& | ||
全ての要素が 1 の配列を作成します |
コード21. 「np.zeros_like 関数を使用し形状が同じ配列を作成」:
import numpy as np arr = np.random.randint(0, 10, (3, 3)) arr_2 = np.zeros_like(arr) arr_2
Out[4]: array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
7.5 ある範囲の数値を要素にもつ配列を作成する関数
No. | 関数 | 説明 |
---|---|---|
1 | np.linspace | 数直線の範囲を等幅で分割した1次元配列を作成します |
1変数関数を調べる時などに使用できます | ||
2 | np.meshgrid | 2次元座標などの座標を配列を使い表現するときに使用します。 |
2変数関数を調べる時などに使用できます |
コード22. 「np.linspace 関数の使用例」:
import numpy as np arr = np.linspace(0.0, 0.9, 10) arr
Out: array([0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9])
コード23. 「np.meshgrid 関数の使用例」:
import numpy as np x = np.linspace(0, 0.4, 5) y = np.linspace(0, 0.4, 5) x_mesh, y_mesh = np.meshgrid(x, y) answer = x_mesh, y_mesh answer
Out: (array([[0. , 0.1, 0.2, 0.3, 0.4], [0. , 0.1, 0.2, 0.3, 0.4], [0. , 0.1, 0.2, 0.3, 0.4], [0. , 0.1, 0.2, 0.3, 0.4], [0. , 0.1, 0.2, 0.3, 0.4]]), array([[0. , 0. , 0. , 0. , 0. ], [0.1, 0.1, 0.1, 0.1, 0.1], [0.2, 0.2, 0.2, 0.2, 0.2], [0.3, 0.3, 0.3, 0.3, 0.3], [0.4, 0.4, 0.4, 0.4, 0.4]]))
8 配列の要素へのアクセス
8.1 配列の要素の取得
添字(インデックス)を使用することで、配列の要素を取得することができます。
8.1.1 要素の取得の基本
以下のようにすると、配列の要素の値を取得することができます。
コード24. 「1次元配列の配列の要素の取得の基本」:
import numpy as np arr = np.array([1, 3, 5]) arr[2]
Out: 5
コード25. 「2次元配列の配列の要素の取得の基本(1)」:
import numpy as np arr = np.array([[1, 3, 5], [7, 11, 13], [17, 21, 23]]) arr[2][2]
Out: 23
上記コードは以下のようにも書くことができます。
コード26. 「2次元配列の配列の要素の取得の基本(2)」:
import numpy as np arr = np.array([[1, 3, 5], [7, 11, 13], [17, 21, 23]]) arr[2, 2]
Out: 23
8.1.2 要素を範囲で指定して取得
以下のようにすると、指定した範囲にある配列の要素を取得することができます。
コード27. 「1次元配列の要素を範囲で指定して取得」:
import numpy as np arr = np.array([1, 3, 5]) arr[1:]
Out: array([3, 5])
コード28. 「2次元配列の要素を範囲で指定して取得」:
import numpy as np arr = np.array([[1, 3, 5], [7, 11, 13], [17, 21, 23]]) arr[1:, 1:]
Out: array([[11, 13], [21, 23]])
8.1.3 逆順の配列の取得
8.2 配列の要素の書き換え
8.2.1 要素の書き換えの基本
配列の要素を添字(インデックス)で指定して別の値に書き換えることができます。 以下のようにすると、配列の要素の値を書き換えることができます。
コード30. 「1次元配列の配列の要素の書き換えの基本」:
import numpy as np arr = np.array([1, 3, 5]) arr[2] = 100 arr
Out: array([ 1, 3, 100])
コード31. 「2次元配列の配列の要素の書き換えの基本(1)」:
import numpy as np arr = np.array([[1, 3, 5], [7, 11, 13], [17, 21, 23]]) arr[2][2] = 100 arr
Out: array([[ 1, 3, 5], [ 7, 11, 13], [ 17, 21, 100]])
上記コードは以下のようにも書くことができます。
コード32. 「2次元配列の配列の要素の書き換えの基本(2)」:
import numpy as np arr = np.array([[1, 3, 5], [7, 11, 13], [17, 21, 23]]) arr[2, 2] = 100 arr
Out: array([[ 1, 3, 5], [ 7, 11, 13], [ 17, 21, 100]])
8.2.2 要素を範囲で指定して書き換え
以下のようにすると、指定した範囲にある配列の要素を書き換えることができます。
コード33. 「1次元配列の要素を範囲で指定して書き換え」:
import numpy as np arr = np.array([1, 3, 5]) arr[1:] = 100 arr
Out: array([ 1, 100, 100])
コード34. 「2次元配列の要素を範囲で指定して書き換え」:
import numpy as np arr = np.array([[1, 3, 5], [7, 11, 13], [17, 21, 23]]) arr[1:, 1:] = 0 arr
Out: array([[ 1, 3, 5], [ 7, 0, 0], [17, 0, 0]])
上記コードの arr[1:] や arr[1:, 1:] は配列のコピーではないことに注意しましょう。 このようなものを numpy ではビュー(View)と呼びます。実態は arr のデータを参照しているものです。 このような ビュー arr[1:] や arr[1:, 1:] に対する値の代入は、ビューの参照先である arr に対する値の代入であることを意味します。
よくあるのが、値を保持しておきたい配列のビューに代入を行なってしまいその配列を破壊してしまうことです。 値を代入したい場合は、以下のように、配列のコピーを作成して、そのコピーに対して代入しましょう。
コード35. 「配列のコピーを作成して、そのコピーに対して代入する」:
import numpy as np arr_orig = np.array([[1, 3, 5], [7, 11, 13], [17, 21, 23]]) arr = arr_orig.copy() arr[1:, 1:] = 0 arr_orig
Out: array([[ 1, 3, 5], [ 7, 11, 13], [17, 21, 23]])
9 真偽値(True/False)について
'==' や '<' などの比較演算子を使用すると配列の要素ごとに比較が行われます。
コード36. 「比較演算子による配列の比較」:
import numpy as np arr_1 = np.arange(9).reshape((3, 3))**2 answer = arr_1 >= 10 answer
Out: array([[False, False, False], [False, True, True], [ True, True, True]])
この真偽値を配列のインデックスに指定すると、 真偽値のTrue の箇所の要素だけ集めた1次元配列を求めることができます。
コード37. 「真偽値の配列による要素の収集」:
import numpy as np arr_1 = np.arange(9).reshape((3, 3))**2 answer = arr_1[arr_1 >= 10] answer
Out: array([16, 25, 36, 49, 64])
以下のように、 np.where を使用すると値が True のインデックスを求めることができます。 np.where の戻り値は、「インデックスを要素にもつ配列」を要素にもつタプル(tuple)です。
コード38. 「np.where による条件を満たす配列のインデックスの取得」:
import numpy as np arr_1 = np.arange(9).reshape((3, 3))**2 answer = np.where(arr_1 >= 10) answer
Out: (array([1, 1, 2, 2, 2]), array([1, 2, 0, 1, 2]))
10 配列の制約
11 配列の長さや、形状が異なる場合に行われる計算について
形状の異なる配列同士の演算を行う場合、それ専用の規則(broadcasting rule)に従い処理が行われます。 その規則に従いエラーになることもあれば計算結果が求まることもあります。
11.1 エラーになるか計算結果が求まるかの判定方法
配列 arr1 と arr2 のそれぞれの配列の shape を一番右から左にかけて以下条件を満たす時、計算結果が求まります。 条件を満たさない場合はエラーとなります。また、エラーにならず、形状が異なる場合は、お互いに異なる形状を補うよう値が補完され、計算されます(詳細は割愛します)。
(以下は「Broadcasting」(https://docs.scipy.org/doc/numpy/user/basics.broadcasting.html) より引用)
- 両方の値が同じ、または、
- 片方の数値が1
11.2 Broadcasting rule の適用例
OK
配列 shape arr1 10 arr2 1 結果 10 OK
配列 shape arr1 10 1 arr2 1 10 結果 10 10 OK
配列 shape arr1 10 10 1 arr2 1 1 3 結果 10 10 3 NG
配列 shape arr1 10 10 3 arr2 1 1 2 結果 エラー
12 速度比較
numpy を使用すると、どれくらい速くなるのでしょうか。 100x100の要素からなる2次元配列の各要素を +1 する処理を numpy を使用する前と後で処理時間を比較してみました。 処理時間の計測には、マジックコマンド %timeit を使用しています。 その結果、numpy の使用により処理速度は 100倍 になりました。また可読性も向上したと思います。
コード39. 「速度比較 numpy 使用前」:
def func(x): y = x.copy() for i in range(100): for j in range(100): y[i][j] += 1 return y # リストの初期化 x = [[0 for i in range(100)] for j in range(100)] # 処理時間計測 %timeit func(x)
785 µs ± 24.2 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
コード40. 「速度比較 numpy 使用後」:
import numpy as np def func(x): y = x.copy() y += 1 return y # 配列の初期化 x = np.zeros((100, 100)) # 処理時間計測 %timeit func(x)
7.11 µs ± 284 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
13 まとめ
配列の作成方法、配列同士の計算や、配列への関数の適用方法について学習しました。
numpy を使って得られる恩恵は以下の2つです。
- 行列計算のコード量が少なくなる
- 処理速度が早くなる
14 最後に
既存の行列の計算のソースコードの可読性の向上や、処理速度向上などを目的に、 numpy を使用したいと思われるかもしれません。 まず計算の対象が 10 の制約の範囲内で処理が可能であることを確認しましょう。 次に、以下の表のように可読性と、処理速度を天秤にかけ、 numpy で実装するか標準 Python で実装するか考えると良いのではないかと思います。 (numpy を使ったからといって常に「可読性が高い&処理速度が早い」コードになる訳ではありません。)
可読性 | 処理速度 | numpy を使用する? |
---|---|---|
高くなる | 高くなる | YES |
高くなる | 低くなる | YES or NO |
低くなる | 高くなる | YES or NO |
低くなる | 低くなる | NO |
numpy を使用してコーディングするとテクニカルで難解なコードになってしまうことがあります。 可読性重視であればテクニカルなコーディングはしないほうが良いでしょう。 numpy を使用してコーディングする際は、numpy を使用する当初の目的を見失わないようにしましょう。
それでは、よい numpy ライフを!
Created: 2019-03-23 Sat 11:25