機械学習プログラマーのためのNumPy入門
PyData.Tokyo オーガナイザーの山本(@kaita)です。
昨今では、scikit-learn、pylearn2など、Pythonを利用してデータ分析をするためのツールが数多く提供されるようになってきました。一方で提供される機械学習ライブラリは汎用目的で実装されていることが多く、データ解析の内容によっては提供されるアルゴリズムに改変を加える必要が多々発生します。また、機械学習のアルゴリズムは日々進化しており、多くの新しいアルゴリズムが日々研究者によって提案されています。
既製のライブラリに実装されていない新しいアルゴリズムを利用する場合、自分自身で実装をしたり、既存の機械学習ライブラリに追加で実装を行う必要が生じます。また、ライブラリを改変しないまでも、ソースコードを読み込むことで、機械学習アルゴリズムの深い理解につながります。より効果的なデータ分析を行うには、アルゴリズムを実装できるスキルを身につけることや、ライブラリをソースコードレベルで理解することが求められます。
今回、Pythonを利用した機械学習のアルゴリズムの実装経験が豊富で、レコメンデーションシステムなどの開発されているシルバーエッグ・テクノロジー株式会社の加藤公一(@hamukazu)さんをお招きし、次の2点を紹介していただきました。
- (1)NumPyを利用した計算処理の高速化手法について
- (2)scikit-learnにおけるRidge Regression(リッジ回帰)やNon-negative Matrix Factorization(非負値行列因子分解)実装例
今回のレポートでは、特に(1)を中心にレポートさせていただきます。
(1)NumPyを利用した計算処理の高速化手法について
加藤さんの発表は「Pythonを利用した計算処理においてfor 文を記載するのは負けである」というコメントと共にプレゼンテーションが開始されました。実際にPythonのfor文は遅いことが有名です。加藤さんのプレゼンテーション資料を引用して説明させていただきますが、例えば、自然数の第n項までの和を求めるプログラムはC言語で書くときは、
#include <stdio.h> int main() { int i; double s=0; for (i=1; i<=100000000; i++) s+=i; printf("%.0f\n", s); }
と書くのが割と自然です。一方でこれをネイティブなPythonで実装してしまうと、以下のコードと書くのが自然です。ただしこのコードはパフォーマンス的に悪い結果をもたらします。
s = 0 for i in range(1, 100000001): s += i print(s)
これらを実行した場合、C言語の場合は約0.1秒程度で計算処理が終了するのに対して、Pythonの場合は8.7秒、すなわち80倍以上の実行時間が必要とされます。
このようにPythonの計算処理が遅くなってしまう理由としては、
- Pythonは動的型付けであること
- インタプリタ言語であること
- Pythonのメモリアクセスの仕様の問題
などが挙げられます。
同問題に対して、Cythonなどを利用する方法や、CやJavaなどでアルゴリズムを実装して高速化を図る手法も存在しますが、どちらのパターンも、Pythonという言語の簡潔性を損なうというデメリットも存在します。
加藤さんは、まずはPythonが提供する基本的な機能やライブラリを用いて高速化を図ることが重要であり、その中でも特にPythonプログラミング言語の拡張モジュールであるNumPyを利用する方法は簡潔性を損なわず、なおかつ数値計算の高速化に対して有効なアプローチであることから、まず同ライブラリの利用を検討する必要がある、と述べられていました。
NumPyを利用した計算処理について
ここでは、加藤さんが言及された「NumPy」について紹介します。NumPyはPythonの科学計算ソフトフェア群の基礎となるライブラリです。特に行列やベクトルの演算に最適化されたndarrayというデータ構造とそれらを処理するための各種メソッドが提供されており、また内部の処理はC言語やFortranで実装されています。このことから、目的の処理を多次元配列(ベクトル・行列・etc.)に対する演算として記述することができれば、計算処理をC言語によるネイティブコードで実行することが可能となります。その結果、高速な計算処理をPythonの簡潔な文法で記載することができるのです。
例えば、先ほどの処理をNumPyで記載した場合は以下のとおりとなります。
import numpy as np a = np.arange(1, 100000001) print(a.sum())
これらの処理を実行した場合、計算処理は約0.18秒とC言語と比較して1.8倍まで近づけることができ、なおかつ、言語自体の簡潔性も損なわれずに記載することができています。
例えば1次配列とスカラ値を積算するという処理を実施したい場合、NumPyのブロードキャスティングを用いれば以下のように簡潔に記載できます。
>>> import numpy as np >>> a = np.array([0, 1, 2, 3]) >>> a * 3 array([0, 3, 6, 9]) >>> np.exp(a) array([ 1. , 2.71828183, 7.3890561 , 20.08553692])
ブロードキャスティングとは、大きさの異なる多次元配列同士の算術演算に関する仕組みです。基本的な振る舞いとしては、次元が大きい配列に対して小さい配列を拡張し、次元数を揃えることによって演算結果を得ることができます。
なお、リスト4は配列が一次元の行列に対する積算処理を実施例ですが、配列が二次元の行列の場合は、リスト5のように記載できます。
>>> import numpy as np >>> a = np.arange(9).reshape((3, 3)) >>> b = np.array([1, 2, 3]) >>> a array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]) >>> b array([1, 2, 3]) >>> a * b array([[ 0, 2, 6], [ 3, 8, 15], [ 6, 14, 24]])
加藤さんの発表によるとブロードキャスティングを用いるとNative Pythonで実装して計算するより大抵の処理は高速化されると考えて問題はないが、一方でNumPyにおけるリファレンスでも記載されているように、メモリの使用量は若干増える可能性もあるとのことです。
NumPyにはその他にも配列要素の参照/代入手法や配列のスライス方法としてIndexingという手法が提供されたり、また基礎的な統計処理用の関数群が提供されたりと、数値演算を扱うために十分な機能が提供されています。加藤さんの講演資料やNumPyのリファレンスには多くの機能についての紹介があります。ぜひご覧ください。
最後に、加藤さんの講演の中で、PythonでNumPyを利用するためには以下のことに注意すべきとのコメントがありました。大変参考になりますので下記で紹介させていただきます。
- できるだけ多次元配列や疎行列のデータ型に入れてからライブラリ関数で計算する。計算中にPython側での要素へのアクセスはできるだけ避ける。
- そのために前処理が重くなっても、多少メモリを散らかしても気にしない。結局コストが安くつくことが多い。
- コードを書く前に代数的に同値な変形を考え、行列の積・和だけで表現できないか考える。そのとき、疎行列もうまく活用すること。