Kei Minagawa's Blog

皆川圭(@keimina)のブログ、Pythonで試したことを書いていきます

Numpy 入門 (機械学習勉強会 in 新潟 2019/03/23)

Numpy 入門

Numpy 入門
(機械学習勉強会 in 新潟 2019/03/23)

1 はじめに

本記事は、機械学習勉強会 in 新潟 2019/03/23 の発表資料です。 numpy の入門者向けの記事になります。 numpy とは何か、何ができるかなどを説明します。numpy で簡単な数値計算を行えるようになることを目的とします。

※時間の都合上、 numpy の機能のほんの一部を紹介しています。 本記事は以下の参考文献を参考に書いています。 numpy についての詳細は以下の参考文献を参照してください。

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.16.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 配列を作成する関数

配列を作成する方法は、5.16.1 で紹介した方法が全てではありません。 以下に、配列を作成するための関数の一部を紹介します。

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 単位行列を作成する関数

No. 関数 説明
1 np.eye 単位行列を表す配列を作成します

コード18. 「np.eye 関数を使用し単位行列を表す配列を作成」:

import numpy as np
arr = np.eye(3)
arr
Out:
array([[1., 0., 0.],
       [0., 1., 0.],
       [0., 0., 1.]])

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 逆順の配列の取得

以下のようにすると、逆順の配列を取得できます。

コード29. 「逆順の配列の取得」:

import numpy as np
arr = np.arange(10)
arr[::-1]
Out:
array([9, 8, 7, 6, 5, 4, 3, 2, 1, 0])

8.1.28.1.3 で取得した arr[1:, 1:] や arr[::-1] は、あたかも配列のコピーのように思われるかもしれませんが、実際は配列のコピーではありません。これらはビューと呼ばれるものであり、実態は arr のデータを参照しているものです。扱いには注意しましょう。 8.2 でも説明します。

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 配列の制約

ここまで1次元配列、2次元配列の計算方法について説明しました。 numpy の配列は、標準Python のリストと違い、一度作成した配列に要素を追加したり、削除したりすることはできません。 一度作成した配列のデータタイプ(dtype)を変更することもできません。これらの操作を行いたい場合は、新たに配列を作成してください。 これらの制約を設けることにより高速(C言語並)な計算が可能になっています。

11 配列の長さや、形状が異なる場合に行われる計算について

形状の異なる配列同士の演算を行う場合、それ専用の規則(broadcasting rule)に従い処理が行われます。 その規則に従いエラーになることもあれば計算結果が求まることもあります。

11.1 エラーになるか計算結果が求まるかの判定方法

配列 arr1 と arr2 のそれぞれの配列の shape を一番右から左にかけて以下条件を満たす時、計算結果が求まります。 条件を満たさない場合はエラーとなります。また、エラーにならず、形状が異なる場合は、お互いに異なる形状を補うよう値が補完され、計算されます(詳細は割愛します)。

(以下は「Broadcasting」(https://docs.scipy.org/doc/numpy/user/basics.broadcasting.html) より引用)

  1. 両方の値が同じ、または、
  2. 片方の数値が1

11.2 Broadcasting rule の適用例

  1. OK

    配列 shape
    arr1 10
    arr2 1
    結果 10
  2. OK

    配列 shape  
    arr1 10 1
    arr2 1 10
    結果 10 10
  3. OK

    配列 shape    
    arr1 10 10 1
    arr2 1 1 3
    結果 10 10 3
  4. 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 ライフを!

著者: Kei Minagawa (http://keimina.hatenablog.jp)

Created: 2019-03-23 Sat 11:25

Validate