NumPy

【NumPy】ユニバーサル関数(ufuncs)を用いた配列(ndarray)の計算

【NumPy】ユニバーサル関数(ufuncs)を用いた配列(ndarray)の計算

NumPyのユニバーサル関数(universal functions: ufuncs)の概要を紹介をします。

ユニバーサル関数(ufuncs)概要

NumPyのユニバーサル関数とは、NumPyの配列(ndarray)の演算のための関数です。universal functionsを省略してufuncsとも呼ばれます。

本記事ではユニバーサル関数の特徴とユニバーサル関数の例を紹介をします。

ユニバーサル関数(ufuncs)は演算が高速

ユニバーサル関数(ufuncs)は、Pythonのリストをでの演算と比べて高速であるという特徴があります。

では、具体的にPythonのリストでの数値演算処理とNumPyのユニバーサル関数(ufuncs)での数値演算処理がどの程度違うのかを比較していきたいと思います。

Python組み込みのリストを使ったループは遅い

Python組み込みのリストの各要素を指定した数値で割った数値を計算するという簡単な例を考えてみます。以下の簡単なプログラムを実行してみてください。

import numpy as np
import time


def measure_execution_time(func):
    def wrapper(*args, **kwargs):
        # 処理前の時刻を設定
        timer_start = time.time()
        # 対象関数の実行
        result = func(*args, **kwargs)
        # 処理後の時刻を設定
        timer_end = time.time()
        # 処理時間を計算
        elapsed_time = timer_end - timer_start
        print(f'処理実行時間: {elapsed_time} sec')
        return result
    return wrapper


@measure_execution_time
def calculate_div(values, div_num):
    """ 入力値を指定値で割る
    :param values: 対象配列
    :param div_num: 割る数
    :return: 対象配列の各要素をdiv_numで割った配列
    """
    output_array = np.empty(len(values))

    for i in range(len(values)):
        output_array[i] = values[i] / div_num

    return output_array


def main():
    np.random.seed(1)

    div_num = 2
    data_size = 10000
    values = np.random.randint(0, 10, data_size)
    print(f'values: {values}')

    result = calculate_div(values, div_num)
    print(result)


if __name__ == '__main__':
    main()
【実行結果例】data_size=10000
values: [5 8 9 ... 5 3 5]
処理実行時間: 0.003020763397216797 sec
[2.5 4.  4.5 ... 2.5 1.5 2.5]

上記はdata_size=10000のランダムなリストを生成し、各要素を2で割るという処理をしています。今回作成した関数calculate_divは内部でforの繰り返し処理で各要素の割り算を実行しています。

実行結果例は私のPCでの実行結果のため、パソコンのスペックにより時間は異なるかと思います。data_sizeについては皆さんお使いの環境にあわせて調整してみてください。

ここで試してもらいたいのは、data_sizeをどんどん大きな値にしてみてもらいたいという点です。以下はdata_sizeを100,000,000(1億)にして試してみた結果です。こちらも皆さんのPCのスペックにより時間は異なると思うので少しずつ件数を増やしながら確認してみてください。

【実行結果例】data_size=100000000
values: [5 8 9 ... 1 9 5]
処理実行時間: 22.246026754379272 sec
[2.5 4.  4.5 ... 0.5 4.5 2.5]

処理速度が非常に遅くなってしまっていることが分かります。Pythonは動的型付けのプログラミング言語で柔軟性が高く、listの各要素を別の型に設定するようなこともできます。

この動的型付けは各要素がどの型かを確認するようなオーバーヘッド処理が含まれることを意味しているため、どうしても処理が遅くなってしまいます。

話の流れ的にもうお気づきかと思いますが、NumPyを使用すると同様の処理を非常に高速に処理することができます。

NumPyのユニバーサル関数はベクトル計算で高速

NumPyの演算はユニバーサル関数(ufuncs)として定義されており、演算はベクトル演算として処理されます。

ベクトル演算では、例えば $\boldsymbol{x}$がベクトルであった時に$\boldsymbol{y}=2\times\boldsymbol{x}$とすると、$\boldsymbol{y}$は$\boldsymbol{x}$の各要素を2倍したベクトルになります。

NumPyのユニバーサル関数では例えば「y=2*x」というような自然なコードで上記のようなベクトル演算ができます。

では、先ほどの例のcalculate_divの部分を以下のように置き換えて実行してみてください。件数は、data_sizeを100,000,000(1億)にして実行してみます。

def calculate_div(values, div_num):
    """ 入力値を指定値で割る
    :param values: 対象配列
    :param div_num: 割る数
    :return: 対象配列の各要素をdiv_numで割った配列
    """
    output_array = values / div_num
    return output_array
【実行結果例】data_size=100000000
values: [5 8 9 ... 1 9 5]
処理実行時間: 0.15458941459655762 sec
[2.5 4.  4.5 ... 0.5 4.5 2.5]

Pythonの組み込みlistのループ処理では20秒以上かかっていた処理が、なんと0.15秒という速度で返ってきていることが分かります。

上記のような処理の背景にある関連内容として「ブロードキャスト(Broadcast)」というものがあります。これは異なるサイズのもののサイズをそろえて処理する機能のことでユニバーサル関数の動作の土台となる機能です。以下に基本的な考え方をまとめていますので参考にしてみてください。

ここでは、ユニバーサル関数が大きなサイズの演算にも高速に対応できるということを理解いただければと思います。

Note

今回は簡単なデコレーターで処理を確認してみましたが、以下のようにIPythonのマジックコマンドを用いると簡単に処理時間を調べることができます。

以下のような感じでIPythonコンソールを実行して、%timeitをつけた以下のような処理を実行してみてください。

import numpy as np
def calculate_div(values, div_num):
    output_array = values / div_num
    return output_array
np.random.seed(1)
div_num = 2
data_size = 100000000
values = np.random.randint(0, 10, data_size)
%timeit calculate_div(values, div_num)
【実行結果例】
197 ms ± 986 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)

IPythonでの実行では、該当箇所を繰り返し実行した平均と標準偏差を出してくれるのでより正確に処理時間が分かります。

各種ユニバーサル関数(ufuncs)の例

ユニバーサル関数の特徴について上記で見てきました。以降では、NumPyの具体的なユニバーサル関数の例を見ていきます。

数値計算

NumPyの数値計算のユニバーサル関数は、pythonでもともと用意されている数値計算の演算子と同じなのでとても直感的に使うことができます。

代表的な数値計算は以下の通りです。「x+1」等のように感覚的に使用することができます。また、それぞれの演算子と関数が紐づいているためnp.add(x, 1)のように使用しても構いません。

演算子ユニバーサル関数(ufuncs)説明
+np.add加算(例:1 + 1 = 2)
np.subtract減算(例:2 – 1 = 1)
*np.multiply乗算(例:2 * 5 = 10)
/np.divide除算(例:5 / 2 = 2.5)
**np.power累乗(例:2 ** 3 = 8)
//np.floor_divide切り捨て除算(例:5 // 2 = 2)
%np.mod剰余(例:5 % 2 = 1)
np.negativeマイナス(例:-2)

三角関数

他にもよく使用する関数として三角関数があります。代表的な三角関数は以下の通りです。

ユニバーサル関数(ufuncs)説明
np.sinsin関数
np.coscos関数
np.tantan関数
np.arcsinsinの逆関数
np.arccoscosの逆関数
np.arctantanの逆関数
Note

上記は数値計算や三角関数の一部を紹介したものなります。他にもNumPyにはたくさんのユニバーサル関数があります。

NumPyのユニバーサル関数については、公式ドキュメントのこちらを参照してみてください。