NumPyは、Pythonで多次元配列を扱う数値計算ライブラリです。

NumPyを使うことで

  • 研究のプロトタイプを簡単に実装できる
  • 数多くのライブラリを使い、高度な機能をシステムに組み込める
  • 高速な行列計算ができる

など様々なメリットがあります。

本記事では、NumPyに興味を持っている人や簡単にでも使えるようにしたい人向けに一通り読めばどのような機能があり、便利なのかを知ることができるように書いています。

本記事を参考にして、日々の業務や研究に活用してください。

NumPyを使ってみよう

まずはPythonを立ち上げます。

python3

まずはNumPyをインポートします。
慣例としてNumPyはnpとしてインポートすることが多いです。

>>> import numpy as np

次に簡単な配列を作っていきます。

>>> a = np.array([1, 2, 3])

はい、これで配列が1つ完成です。
配列同士で足し算や掛け算をしてみます。

>>> a * 3
array([3, 6, 9])
>>> a + 2
array([3, 4, 5])

このように数値を掛けたり足したりすると配列のすべての要素に対して演算が行われます。数値演算について詳しく知りたい方は以下の記事を参照してください。

NumPyの数学関数・定数まとめ /features/numpy-math.html

これをブロードキャストと言います。大事な概念なので別の項で取り上げます。もう1つ配列を作ってみます。

>>> b = np.array([2, 2, 0])

今度は配列同士で演算を行ってみましょう。

>>> a + b
array([3, 4, 3])
>>> a / b
__main__:1: RuntimeWarning: divide by zero encountered in true_divide
array([ 0.5,  1. ,  inf])
>>> a * b
array([2, 4, 0])

0で要素を割ってしまったので怒られてしまいました。

配列同士で*を使って演算をすると各要素同士の掛け合わせが出力となります。このような積をアダマール積と呼びます。

行列のような内積をしたかったらnp.dot()関数というのを使うと良いでしょう。

>>> np.dot(a,b)
6

この関数の詳しい使い方は以下の記事を参照してください。

ベクトルの内積や行列の積を求めるnumpy.dot関数の使い方 /features/numpy-dot.html

様々な配列を作る

次に関数を使っていろんな配列を作ってみましょう。
まずは決まった数までの連続した値を作ります。

>>> np.arange(10)
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

ちょっと細かく指定してみます。0~10までを2ごとに区切ります。

>>> np.arange(0, 10, 2) # (始点、終点、間隔)
array([0, 2, 4, 6, 8])

お気づきの人も多いかと思いますが、NumPyのarange関数では終点に指定した値は基本含みません。

次は決まった区間を15等分してみます。linspace関数を使います。

>>> np.linspace(0,10,15) # 0~10の区間を15等分
array([  0.        ,   0.71428571,   1.42857143,   2.14285714,
         2.85714286,   3.57142857,   4.28571429,   5.        ,
         5.71428571,   6.42857143,   7.14285714,   7.85714286,
         8.57142857,   9.28571429,  10.        ])

arange関数linspace関数についての詳しい記事はこちら。

連番や等差数列を生成するnumpy.arange関数の使い方 /features/numpy-arange.html

線形に等間隔な数列を生成するnumpy.linspace関数の使い方 /features/numpy-linspace.html

次は2次元配列を作ってみましょう。 以下のようにリストを並べたリストの形式で記述しています。

>>> c = np.array([[1, 2, 3], [4, 5, 6]])
>>> c
array([[1, 2, 3],
       [4, 5, 6]])

これが2次元配列です。

配列の形状の表し方は線形代数の行列と一緒で2×3となり、タプルで(2,3)と表されます。
これは配列.shapeを実行することで確認することができます。

>>> c.shape
(2, 3)

3次元配列も作ることができます。

>>> d = np.array([[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]],
...               [[13,14,15],[16,17,18],[19,20,21],[22,23,24]]])
>>> d
array([[[ 1,  2,  3],
        [ 4,  5,  6],
        [ 7,  8,  9],
        [10, 11, 12]],

       [[13, 14, 15],
        [16, 17, 18],
        [19, 20, 21],
        [22, 23, 24]]])

これの形状を見てみると、

>>> d.shape
(2, 4, 3)

と、なっています。いろんな見方があると思いますが、4×3の行列が2つ並んでいると見ることができます。shapeについての詳しい記事はこちら。

NumPyのndarrayのインスタンス変数shapeの意味 /features/numpy-shape.html

ここには軸(axis)の考えなども混ざってきます。
例えば和を求める関数はnp.sumで与えられるのですが、

>>> np.sum(c)
21

列ごとの和を知りたい時、以下の様に軸(axis)を指定するとうまくできます。

>>> np.sum(c, axis=1)
array([ 6, 15])

このような軸や次元の詳しい解説は以下の記事を参考にしてください。

NumPyの軸(axis)と次元数(ndim)とは何を意味するのか /features/numpy-axis.html

またこの配列の形状を変える事もできます。reshape関数を使って変えてみましょう。

>>> c.reshape(3,2)
array([[1, 2],
       [3, 4],
       [5, 6]])
>>> c.reshape(6,1)
array([[1],
       [2],
       [3],
       [4],
       [5],
       [6]])

また転置を行うこともできます。やり方は様々ですが、配列.Tの形やtranspose関数で求めることができます。

>>> c.T
array([[1, 4],
       [2, 5],
       [3, 6]])

>>> np.transpose(c)
array([[1, 4],
       [2, 5],
       [3, 6]])

reshapeや転置についての詳しい記事はこちら。

配列を形状変換するNumPyのreshapeの使い方 /features/numpy-reshape.html

配列の軸の順番を入れ替えるNumPyのtranspose関数の使い方 /features/numpy-transpose.html

次に乱数を発生させてみます。np.randomクラスにいろんな乱数を発生させる関数が実装されています。

randn関数が標準正規分布に従った値を返し、rand関数は0~1までの値の中で乱数を返します。

>>> np.random.randn()
-1.0597627440500497
>>> np.random.rand()
0.8549776402949295

()の中で形状を指定することで任意の形状の配列の要素を乱数で埋めたものを出力することができます。

>>> np.random.randn(2,3)
array([[ 0.53564291, -0.02883676, -0.25913376],
       [-2.04508435,  1.45158426,  0.35771574]])

乱数関数については以下の記事で詳しく解説しています。

NumPyのrandomを使った配列操作・乱数生成方法まとめ /features/numpy-random.html

インデックスとスライシング

配列の中の特定の値を抜き出したいときはインデックスとスライシングを使います。スライシングは少しクセがあるので注意してくださいね。

まずはインデックスを使ってみましょう。一次元配列から試してみます。

>>> a
array([1, 2, 3])
>>> a[0]
1
>>> a[2]
3
>>> a
array([1, 2, 3])
>>> a[1] = 3
>>> a
array([1, 3, 3])
>>> a[1] = 2
>>> a
array([1, 2, 3])

このように、先頭の要素のインデックスは0になっています。
またインデックス機能を使って配列の特定の値だけを変更することが可能です。
では2次元配列で見てみます。

>>> c
array([[1, 2, 3],
       [4, 5, 6]])
>>> c[0,0]
1
>>> c[0,2]
3
>>> c[1,2]
6

最初の数字で縦方向のインデックスを、次の数字で横方向のインデックスを指定しています。

次にスライシングです。これは複数の要素を抜き出すときに使うものです。新たに配列dを設定してスライシングを使ってみます。

>>> d = np.array([0, 5, 2, 7, 1, 9])
>>> d[1:5]
array([5, 2, 7, 1])
>>> d[1:3]
array([5, 2])

[始点:終点]の形で指定しますが、終点として指定されたインデックスは含まれないことに注意してください。
[始点:終点:間隔]の形で指定することができ、

>>> d[0:5:2]
array([0, 2, 1])
>>> d[::-1]
array([9, 1, 7, 2, 5, 0])

このように使うことができます。d[::-1]はとりあえず配列をひっくり返したいときに便利なものなので 覚えておくとよいですよ。スライシングについてもっとよく知りたい方は以下の記事を参考にしてください。

NumPy配列のスライシング機能の使い方 /features/numpy-slicing.html

ブロードキャスティング

これはNumPyの代表的な特徴の1つであり、使いこなせるようになるととても便利です。

配列の形状にあわせて適宜配列を拡張してくれる機能となっています。何言っているのかよくわからないですよね。

実際のコードを見てもらったほうが早いと思うので早速みていきましょう。

>>> a
array([1, 2, 3])
>>> c
array([[1, 2, 3],
       [4, 5, 6]])
>>> a + c
array([[2, 4, 6],
       [5, 7, 9]])
>>> a * c
array([[ 1,  4,  9],
       [ 4, 10, 18]])

見てわかるでしょうか。aの配列が2回繰り返されることでcへの足し算を行っていることが。 acの形状にあわせて拡張(この場合は行方向に要素が1回繰り返される)されたあと、通常の足し算や掛け算を行っていることになります。

ブロードキャストについて詳しく知りたい方はこちら。

NumPyのブロードキャストのメリットと解説 /features/numpy-broadcasting.html

NumPyの高速演算

NumPyがどれだけ早く計算できるのかを率直なPythonコードと比較して実際に確かめてみます。

>>> import numpy as np
>>> import time
>>> def calculate_time():
...     a = np.random.randn(100000)
...     b = list(a)
...     start_time = time.time()
...     for _ in range(1000):
...         sum_1 = np.sum(a)
...     print("Using NumPy\t %f sec" % (time.time()-start_time))
...     start_time = time.time()
...     for _ in range(1000):
...         sum_2 = sum(b)
...     print("Not using NumPy\t %f sec" % (time.time()-start_time))
...
>>> calculate_time()
Using NumPy	 0.049773 sec
Not using NumPy	 5.506544 sec

上記の場合では1.5倍ほど高速に演算できていることがわかりました。更に配列のサイズを大きくすると以下のようになります。

次はIPythonの%timeitを使って計測してみます。

In [1]: import numpy as np

In [2]: a = np.random.randn(10000000)


In [3]: %timeit np.sum(a)
6.32 ms ± 180 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [4]: %timeit sum(a)
1.1 s ± 15.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

明らかな速度差が見て取れます。NumPyは上手に使うと格段に高速化できることがあるので、アプリケーションが遅いときなどにも便利に使うことができます。