NumpyでPythonの関数をベクトル化する方法

スポンサーリンク

今回は、PythonのNumpy Vectorizationを学びます。

NumpyはPythonにおける配列のC言語実装で、同じPythonインタプリタでありながら比較的高速に処理することができます

今回は、numpy.vectorize()を使って、配列に関数を要素ごとに適用する方法を探ります。

スポンサーリンク

なぜforループはNumpyのベクタライゼーションに最適ではないのか?

リストやタプル、NumPyの配列の要素に関数を適用するには、Pythonでは簡単にforループを使うことができます。

しかし、Pythonはインタプリタ型言語であり、CやC++と比較すると、ほとんどの実装が遅いのです。

この遅い計算の主な理由は、Pythonの動的な性質と、メモリのオーバーヘッドを発生させるコンパイラレベルの最適化がないことに起因しています。

これは、Pythonを巨大な計算のために使用する人々にとって理想的な状況とは言えません。

NumPyはより高速な実装を提供していますが、forループはNumPyが提供するスピードの一部を奪っています。

このボトルネックに対処するために、NumPy はベクトル化機能を提供し、関数が効率的に配列にマッピングされるようにした。

numpy.vectorize() vs Python for loop – ベクトル化速度の比較

それでは、Pythonのfor loopとベクトル化したバージョンの速度を比較してみましょう。

timeit関数を使用して、正確な速度テストを行います。

# We use a large array for benchmarking our method
a = np.random.rand(10000)
b = 5
 
print("Benchmark for the for loop implementation: ")
%timeit [foo(i, b) for i in a]
print()
print("Benchmark for the vecfoo implementation: ")
%timeit vecfoo(a, b)
numpy.vectorize(pyfunc, otypes=None, doc=None, excluded=None, cache=False, signature=None)

ベクトル化したものは、for loopの実装に比べて3倍以上速いことがわかります。

numpy.vectorize()関数によるNumpyベクトル化

Numpyのvectorize関数は、pythonの関数(pyfunc)を受け取り、その関数のベクトル化されたバージョンを返します。

ベクトル化されたバージョンの関数は、オブジェクトのシーケンスまたはNumPyの配列を入力として取り、入力シーケンスの各要素に対してPython関数を評価します。

Numpyのベクトル化は基本的にPythonのmap()と同じように機能しますが、NumPyのブロードキャスト機構という付加機能がついています。

それでは、numpy.vectorize()関数をより詳細に理解しましょう。

def foo(a, b):
    """
    If a > b return a + b,
    else return a - b.
    """
    if a >= b:
       return a + b
    else:
       return a - b


必要なパラメータ

pyfunc: オブジェクトのシーケンスに適用したい関数です。

オプションのパラメータです。

  • otypes: 関数の出力タイプは、文字列またはデータ型のリストとして指定できます。otypes が指定されず、キャッシュが True に設定されている場合、出力タイプは入力の最初の要素を呼び出すことによって決定されます。
  • doc: 作成されたdocstringを指定します。doc: 作成された関数のdocstringを指定します。
  • cache: otypes が提供されない場合、True ならば、出力数を決定する最初の関数呼び出しをキャッシュします。

Vectorizing a function

Fake tag
Fake tag
Fake tag
Fake tag

ベクトル化された関数の出力型

Numpyはotypesパラメータがfalseに設定されている場合、関数の出力型を自動的に評価します。

以下はそれを紹介する例です。

# Create a vectorized version of foo
vecfoo = np.vectorize(foo)
vecfoo(np.arange(5), 5)
array([-5, -4, -3, -2, -1])

また、戻り値のデータ型を強制することで、ベクトル化関数の出力を制御することができます

以下はその例です。

a = np.array([1, 2, 3, 4])
b = 2
 
vecfoo =  np.vectorize(foo)
res = vecfoo(a, b)
print(type(res[0]))
<class 'numpy.int64'>

Numpyのベクトル化におけるキャッシュ

optypesが指定されていない場合、関数は入力の第1引数を呼び出して入力数を決定することは既に見ました。

この結果はキャッシュすることができるので、関数が同じ処理を何度も実行するのを防ぐことができます。

Final Remarks

np.vectorize() だけでなく、NumPy の日常的な操作でベクトル化はずっと使われてきました。

例えば、以下のように追加します。

a = np.array([1, 2, 3, 4])
b = 2
 
vecfoo = np.vectorize(foo, otypes=[float])
res = vecfoo(a, b)
print(type(res[0]))
<class 'numpy.float64'>

同じルールが、引き算、掛け算、sin、cosなどの異なるプリミティブ関数にも当てはまります。

これらの関数は、ビルトインのベクトル化のサポートを持っています。

しかし、私たちが使っているPythonは一般的にこのようなベクトル化をサポートしていないので、高速かつ効率的にベクトル化された操作を行うにはnumpy.vectorize()が必要です。

参考文献

  • Numpy ドキュメント
タイトルとURLをコピーしました