Lesson2

NumPyの使い方(数値計算)

1. 学習の目標

これから各種パッケージライブラリを紹介しますが、まずは NumPy について学習します。

numpy_00.png

※画像ではSciPyと大きく書かれていますが、SciPyを構成する要素のひとつがNumPyです。

NumPyは 配列 を作成するためのライブラリです。リストのレッスンでも「配列」という言葉を出しましたが、リストと配列の違いから入り、NumPyの使い方を色々と見ていきましょう。

  • NumPyの配列とPython標準のリストの違い
  • NumPyの配列を作る
  • Numpyの配列を使う

2. NumPyの配列とPython標準のリストの違い

NumPyの配列もPython標準のリストも「複数の要素が格納できるロッカー」です。ただし、NumPyの配列は、Python標準のリストよりも制限があります。

  • ひとつの配列に2種類以上のデータ型の要素は格納できない(1種類のデータ型のみ)
  • 一度配列を作成すると伸縮できない(固定長)

このような制限を設けることで計算速度を速めています。また、数値計算に関する命令が多く搭載されており、数学でいう行列計算が非常に得意なライブラリです。そのため、機械学習や統計分析の分野ではNumPyの利用が事実上の標準状態になっています。

3. NumPyの配列を作る

NumPyを利用するためには numpy をモジュールとして import する必要があります。

まずは、JupyterLabで新規ノートブックを作成し、 Lesson2 という名前をつけてください。そうしたら、下記の1行をJupyterLabで実行しておきましょう。

import numpy as np

これだけで、JupyterLabを終了するまでは np.メソッド名() のように記述することでNumPyのさまざまな機能を利用できます。JupyterLabをいったん終了した後に学習を再開するときは、必ず上記の import 文を実行しましょう。

3.1 array() でNumPyの配列を作る

NumPyの配列を作るには np.array() メソッドを使います。引数にはリストかタプルを指定してください。別途作成したNumPyの配列も指定できます。

a = np.array([0, 1, 2])
print(type(a))
print(a)
print(a.dtype)

出力結果:

<class 'numpy.ndarray'>
[0 1 2]
int64

ここでは0, 1, 2という3つの要素をもつリストを np.array() に指定してNumPyの配列を作りました。最初にNumPyの配列のデータ型を表示しています。その結果 numpy.ndarray という名前が表示されました。「NumPyの配列」のデータ型は ndarray です。

以降のカリキュラムでは「NumPyの配列」は ndarrayと書いていきます。

次に、単純にすべての配列要素を表示しました。[0 1 2] というように [ ] の括弧で表示されているため、一瞬リストと見分けがつかない点は注意しましょう。これはタプルからndarrayを作成しても同じ表示内容になります。

最後に配列要素のデータ型を調べました。ndarrayには1種類のデータ型のみが格納できる仕様で、dtype は、ndarrayのオブジェクトが何の型のデータを保存できるかという情報を格納するプロパティです。ここでは int64 と表示されました。これは「64bitのサイズの整数値」という意味で、およそ922京までの大きさの整数値を扱えるデータ型です。NumPyにおいて整数値のデフォルトは int64 であると思ってください。

他にも色々なndarrayを作ってみましょう。

b = np.array([1, 1.5, 2])
print(b)
print(b.dtype)

c = np.array([1 + 2j, 3 + 4j, 5 + 6j])
print(c)
print(c.dtype)

d = np.array([True, False, True, False, False])
print(d)
print(d.dtype)

e = np.array(list("TellusxData"))
print(e)
print(e.dtype)

出力結果:

[1.  1.5 2. ]
float64
[1.+2.j 3.+4.j 5.+6.j]
complex128
[ True False  True False False]
bool
['T' 'e' 'l' 'l' 'u' 's' 'x' 'D' 'a' 't' 'a']
<U1

浮動小数は float64、複素数は complex128、真偽値は bool がデフォルトの配列のデータ型になります。ただし、文字列の場合は <U です。Uの後ろの数値は、文字列の最大文字数です。上記の例はすべて1文字の要素なので <U1 となりました。

小さい数値を使いたい場合

通常は int64float64 で問題ありませんが、小さいデータでもデフォルトは int64, float64 になるので、メモリやHDDの容量をムダに専有する可能性があります。使わないのにもったいない、という状態です。そこで、array() で配列を作る際に dtype = np.データ型 という引数を指定することで、もう少し小さいサイズの整数値・浮動小数値として扱えます。

f = np.array([100, 200, 300], dtype = np.int16)
print(f)
print(f.dtype)

出力結果:

[100 200 300]
int16

int16 と指定したので「16ビットの大きさの整数値」となりました。その範囲の最大値は約32,000です。int64(約922京)で使うよりも節約になります。

主なNumPyのデータ型をまとめます。

データ型 概要
int8 正負ありの整数値(-128から127)
int16 正負ありの整数値 (-32768から32767)
int32 正負ありの整数値 (およそマイナス21億から21億)
int64 正負ありの整数値 (およそマイナス922京から922京)
uint8 正の数のみの整数値 (0から255)
uint16 正の数のみの整数値 (0から65535)
uint32 正の数のみの整数値 (0からおよそ42億)
uint64 正の数のみの整数値 (0からおよそ1844京)
float16 半精度と呼ばれる小数値
float32 単精度と呼ばれる小数値
float64 倍精度と呼ばれる小数値

すでに存在するndarrayのデータ型を変換する

途中でデータ型を変更したい場合は astype() を使います。

たとえば、

g = np.array([1.5, 2.5, 3.5])
print(g)
print(g.dtype)

出力結果:

[1.5 2.5 3.5]
float64

このような float64 型の配列 g を作成した場合、以下のようにして int64 の整数型に変換できます。

g = g.astype(np.int64)
print(g)
print(g.dtype)

出力結果:

[1 2 3]
int64

3.2 配列の作成に関する便利なメソッド

np.arange()

range() のように連続する整数値でndarrayを作りたい場合は np.arange() メソッドを使います。引数の指定方法は range() と一緒です。引数が1つ(np.arange(n))なら「0以上n未満」、引数が2つ(np.arange(a, b))なら「a以上b未満」、引数が3つ(np.arange(x, y, z))なら「x以上y未満(増分:z)」です。

a = np.arange(20)
print(a)

b = np.arange(1, 21)
print(b)

c = np.arange(2, 41, 2)
print(c)

出力結果:

[ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19]
[ 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20]
[ 2  4  6  8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40]

np.linspace()

np.arange() に似たメソッドで np.linspace() というものがあります。これは np.linspace(a, b, c) と指定することで、「a以上b以下の数値をc分割」して作った配列を作れます。

d = np.linspace(0, 10, 5)
print(d)

出力結果:

[ 0.   2.5  5.   7.5 10. ]

0以上10以下までの数値を5分割にしたので、0, 2.5, 5, 7.5, 10という数のならびになりました。なお、NumPyでは整数値と小数値が混在した場合は、すべての要素が小数値となります。0., 5., 10. と表示されるのは、そのためです。

np.empty()

「とりあえず指定の要素数をもつ配列だけを用意しておいて、あとから各要素に代入したい」というときは np.empty() が使えます。引数には、作成したい配列の要素数を指定します。ただし、np.empty() は各要素を初期化しないので、要素の初期値は保証できません。すべて0のときもあれば、何だかよくわからないゴミのデータになる可能性もあります。つまり、そのまま何も代入せずに使おうとすると、不具合の原因になりますので気をつけてください。

e = np.empty(5)
print(e)

出力結果:

(下記の実行結果は一例です。このようにならない場合があります。)

[3.73603959e-262 6.02658058e-154 6.55490914e-260 5.30498948e-313 3.14673309e-307]

np.zeros()とnp.ones()

np.empty() と同じく配列を先に作っておきたいものの、確実に何らかの値で初期化しておきたいときは np.zeros() を使いましょう。引数は、np.empty() とまったく一緒で、作りたい配列の要素数です。すべての要素を0で初期化した配列ができあがります。

f = np.zeros(5)
print(f)

出力結果:

[0. 0. 0. 0. 0.]

サンプルコードは省略しますが、すべて1で初期化する np.ones() メソッドも存在します。

np.random.rand()

すべての要素の初期値をランダムな数値で作成するなら np.random.rand() を使ってください。こちらも引数は要素数です。0以上1未満のランダムな小数値を初期値とした配列ができあがります。

g = np.random.rand(5)
print(g)

出力結果:

(下記の結果は一例です)

[0.42000386 0.20898924 0.67091444 0.43957557 0.58572829]

1よりもっと大きな上限値にしたい場合は行列にかけ算をします。また、整数値に変換したい場合は astype() を使えばOKです。たとえば、以下の例は g を1から6までの整数値に変換しています。

g = g * 6 + 1
g = g.astype(np.int8)
print(g)

出力結果:

(下記の結果は一例です)

[3 2 5 3 4]

np.concatenate()

すでにある2つの配列を連結させたいときは np.concatenate() を使います。引数には、ndarrayを要素とするリストを指定します。

h = np.arange(1, 4)    # [1, 2, 3]
i = np.arange(4, 7)    # [4, 5, 6]
j = np.concatenate([h, i])
print(j)

出力結果:

[1 2 3 4 5 6]

補足:tolist()

np.array() でリストから配列を作成しますが、配列としてデータ処理を行ったあとでリストに戻してデータ管理したい、ということもあるかもしれません。配列をリストに変換したいときは tolist() というメソッドを利用してください。引数は要りません。

以下は rand() のところで作成した配列 g を使った例です。

gl = g.tolist()
gl.pop()
print(gl)

出力結果:

[3, 2, 5, 3]

3.3 多次元配列を作る

ndarrayでも2次元以上のものを作成できます。多次元のリストやタプルを np.array() メソッドの引数に指定してください。

a = np.array([[1, 2, 3], [4, 5, 6]])
print(a)

出力結果:

[[1 2 3]
 [4 5 6]]

empty() なども多次元配列の作成に使えます。たとえば empty() で2次元配列を作成する場合、以下のように1つ目の引数は (縦の要素数, 横の要素数) というタプル形式のデータを指定します。

a = np.empty((2, 3), dtype=np.int64)

a[0] = [1, 2, 3]
a[1] = [4, 5, 6]

print(a)

出力結果:

[[1 2 3]
 [4 5 6]]

1次元配列を多次元配列に変換するreshape()

1次元で作成された既存の配列から多次元の配列を新たに作る reshape() というメソッドが存在します。また、配列名.shape で、該当の配列の構造を知ることができます。

a = np.arange(1, 21)
print(a)
print(a.shape)

出力結果:

[ 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20]
(20,)

np.arange() で1から20までの「要素数20個の配列」を作成しました。この配列に対し、以下のコードを実行します。

b = a.reshape(4, 5)
print(b)
print(b.shape)

出力結果:

[[ 1  2  3  4  5]
 [ 6  7  8  9 10]
 [11 12 13 14 15]
 [16 17 18 19 20]]
(4, 5)

reshape(4, 5) を使って「要素数20個の配列」から「要素数5個の配列を4個もつ2次元配列」を新たに生成しました。表現がわかりにくいかもしれませんが、reshape(x, y) は、縦x行・横y列の2次元配列を作成します。print() によって表示された配列の内容を確認して、理解を深めてください。

reshape() による分割のルールは一定です。元の配列と要素数が同一であれば reshape() を使えます。元の配列が要素数20個なのに reshape(3, 6) はできません(3 x 6 = 18個となり要素数が合わないため)。そのあたりを注意しながら、色々な数値を引数に当てはめて実行確認し、動きを理解してください。

もちろん、引数を3つ以上に設定すれば3次元以上の配列が作成できます。

b2 = a.reshape(2, 2, 5)
print(b2)

出力結果:

[[[ 1  2  3  4  5]
  [ 6  7  8  9 10]]

 [[11 12 13 14 15]
  [16 17 18 19 20]]]

多次元配列を1次元配列に変換するflatten()とravel()

機械学習で利用する関数の中には、データを1次元配列の形式で引数に設定しないと動作しないものがあります。その場合、現状は多次元配列のデータだったとしても、多次元配列から1次元配列を生成する必要が生じます。多次元から1次元の配列を生成するには flatten()ravel() というメソッドを使います。

c = np.arange(1, 21).reshape(4, 5)
d = c.flatten()
print(c)
print(d)

e = c.ravel()

print(c)
print(e)

出力結果:

[[ 1  2  3  4  5]
 [ 6  7  8  9 10]
 [11 12 13 14 15]
 [16 17 18 19 20]]
[ 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20]
[[ 1  2  3  4  5]
 [ 6  7  8  9 10]
 [11 12 13 14 15]
 [16 17 18 19 20]]
[ 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20]

どちらも、得られる1次元配列の内容に違いはありません。ただし、内部動作が異なります。flatten() は必ず元の配列が残ります。ravel() は元の配列が無くなってしまう可能性があります。その代わり、ravel() の方が処理速度は高速です。用途に応じて使い分けると良いでしょう。

4. NumPyの配列を使う

配列の作り方を学んだところで、次は使い方を覚えましょう。

先に補足しておくと、ndarrayは一度サイズを決めてしまうと伸縮できない固定長であるという特徴はすでに説明したとおりです。それなのに要素の追加や削除が可能な理由は すでにある配列を基に新たな要素を追加した(or既存要素を削除した)配列を新規作成するからです。

4.1 要素の参照と更新

ndarrayでもリストやタプルと同様「スライス」による参照と更新、ならびに、配列の一部分を抽出して新しい配列を作成することが可能です。

下記は2次元配列での例です。2次元配列で指定する際の記述方法は 配列名[縦][横] でも 配列名[縦, 横] でも構いません。下記の例は後者の記述方法を採用しています。

a = np.arange(1, 101).reshape(10, 10)
print(a[0, 0])            # 0番目のみ
print(a[1, 1])            # 1番目のみ
print(a[1:3, 1:3])        # 1番目から2番目まで
print(a[1:5:3, 1:5:3])    # 1番目から4番目まで(増分3で)
print(a[:5, :5])          # 先頭から4番目まで
print(a[7:, 7:])          # 7番目から末尾まで
print(a[::3, ::3])        # 先頭から末尾まで(増分3で)
print(a[:, :])            # 全体(先頭から末尾まで)」

出力結果:

1
12
[[12 13]
 [22 23]]
[[12 15]
 [42 45]]
[[ 1  2  3  4  5]
 [11 12 13 14 15]
 [21 22 23 24 25]
 [31 32 33 34 35]
 [41 42 43 44 45]]
[[ 78  79  80]
 [ 88  89  90]
 [ 98  99 100]]
[[  1   4   7  10]
 [ 31  34  37  40]
 [ 61  64  67  70]
 [ 91  94  97 100]]
[[  1   2   3   4   5   6   7   8   9  10]
 [ 11  12  13  14  15  16  17  18  19  20]
 [ 21  22  23  24  25  26  27  28  29  30]
 [ 31  32  33  34  35  36  37  38  39  40]
 [ 41  42  43  44  45  46  47  48  49  50]
 [ 51  52  53  54  55  56  57  58  59  60]
 [ 61  62  63  64  65  66  67  68  69  70]
 [ 71  72  73  74  75  76  77  78  79  80]
 [ 81  82  83  84  85  86  87  88  89  90]
 [ 91  92  93  94  95  96  97  98  99 100]]

4.2 要素の新規追加

すでに作成した配列の末尾に要素を追加するには np.append() を利用します。引数は、最初が要素を追加したい配列、次が追加したいデータを単独のデータもしくはリスト・タプルの形式で指定してください。

a = np.arange(1, 20)
a = np.append(a, 20)
a = np.append(a, (21, 22, 23, 24))
print(a)

出力結果:

[ 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24]

特定の場所に追加したいときは np.insert() を使います。引数は、最初が要素を追加したい配列、次が追加したい場所、最後が追加要素(単体データかリスト・タプル形式)です。

b = np.arange(1, 6)
b = np.insert(b, 1,6)
b = np.insert(b, 3, (7, 8))
print(b)

出力結果:

[1 6 2 7 8 3 4 5]

多次元配列の場合は、末尾に追加したい場合でも insert() を利用する方が扱いやすいので、オススメします。その際、引数が1つ増えます。4つ目の引数に0を入れると縦に1行追加され、1を入れた場合は横に1列追加されます。

a = np.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
b = np.array([1, 1, 1])

# 配列aの縦と横の要素数を取得
# (このように書くことで変数hには縦の要素数3、変数vには横の要素数3が格納される)
(h, v) = a.shape

# 上から2行目に1行追加
c = np.insert(a, 2, b, 0)
print(c)

# 縦の末尾に1行追加
d = np.insert(a, h, b, 0)
print(d)

# 左から2列目に1列追加
e = np.insert(a, 2, b, 1)
print(e)

# 横の末尾に1列追加
f = np.insert(a, v, b, 1)
print(f)

出力結果:

[[0 0 0]
 [0 0 0]
 [1 1 1]
 [0 0 0]]
[[0 0 0]
 [0 0 0]
 [0 0 0]
 [1 1 1]]
[[0 0 1 0]
 [0 0 1 0]
 [0 0 1 0]]
[[0 0 0 1]
 [0 0 0 1]
 [0 0 0 1]]

4.3 要素の削除

要素の削除には np.delete() を使います。引数には、最初が要素を削除したい配列、次に削除したい位置を指定します。

a = np.arange(5)
a = np.delete(a, 2)
print(a)

出力結果:

[0 1 3 4]

多次元配列の場合は、引数が1つ増えます。3つ目の引数に0を入れると1行丸々、1の場合は1列丸々削除されます。

# 1行目を削除
b = np.arange(1, 26).reshape(5, 5)
b = np.delete(b, 1, 0)
print(b)

# 1列目を削除
c = np.arange(1, 26).reshape(5, 5)
c = np.delete(c, 1, 1)
print(c)

出力結果:

[[ 1  2  3  4  5]
 [11 12 13 14 15]
 [16 17 18 19 20]
 [21 22 23 24 25]]
[[ 1  3  4  5]
 [ 6  8  9 10]
 [11 13 14 15]
 [16 18 19 20]
 [21 23 24 25]]

5. NumPyの配列の応用的な使い方

NumPyの配列では、便利に数値計算が行えます。

5.1 四則演算

配列名に単純な数値をたしたり、ひいたりする四則演算の指定をするだけで、すべての要素について加減乗除が可能です。四則演算の他、演算子のところで紹介した演算は、すべて配列に適用できます。

a = np.arange(1, 11)
print(a)
print(a + 1)
print(a - 1)
print(a * 2)
print(a / 2)
print(a % 2)

出力結果:

[ 1  2  3  4  5  6  7  8  9 10]
[ 2  3  4  5  6  7  8  9 10 11]
[0 1 2 3 4 5 6 7 8 9]
[ 2  4  6  8 10 12 14 16 18 20]
[0.5 1.  1.5 2.  2.5 3.  3.5 4.  4.5 5. ]
[1 0 1 0 1 0 1 0 1 0]

また、配列同士の演算も可能です。

a = np.arange(1, 7).reshape(2, 3)
b = np.arange(7, 13).reshape(2, 3)
print(b + a)
print(b - a)

出力結果:

[[ 8 10 12]
 [14 16 18]]
[[6 6 6]
 [6 6 6]]

もちろん、かけ算とわり算も可能です。

c = np.arange(1, 5).reshape(2, 2)
d = np.arange(5, 9).reshape(2, 2)
print(c * d)
print(c / d)

出力結果:

[[ 5 12]
 [21 32]]
[[0.2        0.33333333]
 [0.42857143 0.5       ]]

5.2 ユニバーサル関数

NumPyで用意されている関数のうち、一度に全要素へ計算処理を適用し、得られた計算結果で新しい配列を作れるものを総称して ユニバーサル関数 と呼んでいます。

簡単な例として、切り捨て・切り上げ・四捨五入の関数を紹介します。切り捨ては np.floor()、切り上げなら np.ceil()、四捨五入なら np.round() を使いましょう。引数はすべて1つで、その処理を行いたい配列を指定します。

a = np.random.rand(5)
print(a)

a = a * 6 + 1

print(np.array(np.floor(a), dtype=np.int64))
print(np.array(np.ceil(a), dtype=np.int64))
print(np.array(np.round(a), dtype=np.int64))

出力結果:

[0.47076593 0.35058192 0.04499632 0.8949763  0.44308899]
[3 3 1 6 3]
[4 4 2 7 4]
[4 3 1 6 4]

上記の例は np.random.rand() を使ってランダムな数値を取得しているので、実行結果が毎回変わる点に注意しましょう。

5.3 行列の反転と回転

NumPyは、行列を左右や上下に反転させたり、回転させたりする関数を持っています。一覧として、まとめます。

処理内容 関数
左右に反転させる fliplr(配列)
上下に反転させる flipud(配列)
反時計回りに90度回転させる rot90(配列, 1)
反時計回りに180度回転させる rot90(配列, 2)
反時計回りに270度回転させる rot90(配列, 3)

いったん、以下のような配列を作成します。

a = np.arange(1, 10).reshape(3, 3)
print(a)

出力結果:

[[1 2 3]
 [4 5 6]
 [7 8 9]]

反転

左右に反転させるには fliplr() を使います。

b = np.fliplr(a)
print(b)

出力結果:

[[3 2 1]
 [6 5 4]
 [9 8 7]]

また、上下に反転させたい場合は flipud() を使います。

c = np.flipud(a)
print(c)

出力結果:

[[7 8 9]
 [4 5 6]
 [1 2 3]]

回転

行列の回転には rot90() を使います。関数名の 90 は「反時計回りに90度」という意味です。

d = np.rot90(a, 1)
print(d)

出力結果:

[[3 6 9]
 [2 5 8]
 [1 4 7]]

rot90() の第2引数は回転数です。この引数に指定した数値分、反時計回りに90度回転させます。つまり、反時計回りに180度回転させるなら rot90(配列, 2) と記述します。

e = np.rot90(a, 2)
print(e)

出力結果:

[[9 8 7]
 [6 5 4]
 [3 2 1]]

同様に、rot90(配列, 3) と記述することで、反時計回りに270度(=時計回りに90度)回転させることができます。

f = np.rot90(a, 3)
print(f)

出力結果:

[[7 4 1]
 [8 5 2]
 [9 6 3]]

6. まとめ

このレッスンではNumPyについて学びました。NumPyは配列(ndarrayというデータ型のオブジェクト)を生成します。使い方はリストとほぼ変わりませんが、数値計算という側面ではリストよりも使いやすく、高速です。また、NumPyは他のさまざまなライブラリでも利用されているため、使い方を覚えると他の多くのライブラリを使ったプログラムが書けるようになります。

なお、NumPyの公式サイトを見るとわかるように、NumPyは「SciPy」というライブラリの一部です。SciPyには数学的な処理をする上で非常に便利な命令が多く用意されています。本カリキュラムでは、このあとのレッスンでSciPyについても扱います。

次のレッスンでは、グラフを描画する「Matplotlib」というライブラリの使い方を学びます。

Lesson3 Matplotlibの使い方(グラフ描画)へ進む
Copyright 2020 SAKURA Internet Inc. All rights reserved