Rest Term

Blog

PythonのWebアプリケーションフレームワーク Sanicを試す

PythonのWebアプリケーションフレームワークについて、Flaskからの移行先としてSanicが有力そうなので調べています。

Sanic is a Flask-like Python 3.5+ web server that's written to go fast.

Sanicは著名なイベントループライブラリであるuvloopを利用しており、Node.jsのように非同期I/Oによる高効率なHTTPリクエスト処理が可能です。また、SanicはFlaskとよく似たシンタックスを提供しているため、他のフレームワークよりも移行コストを抑えることができそうです。

環境

サーバ環境

  • Ubuntu 16.04 (x86_64)
  • CPU 3.30GHz 4core / 8GB RAM
  • Python 3.6.4
  • Sanic 0.7.0 / Flask 0.12

クライアント環境

  • macOS High Sierra 10.13.4 (x86_64)
  • CPU 1.70GHz 4core / 8GB RAM

ネットワーク環境

注意点としてはSanicは asyncio および async/await 機能を利用しているためPython3.5以降が必要です。Python3移行は2018年現在なら世間的にもけっこう進んでいるとは思いますが、3.5以降となると業務で使っている人はまだ少ないかもしれません。

使い方

公式でFlask-likeと謳っているとおりインタフェースはFlaskとよく似ているので移行作業自体は楽だと思います。

Flaskでhello, world

Sanicだと以下のようになります。まさにFlask-likeですね。

上記SanicのコードではPython3.5以降で正式に利用できるasyncキーワードが現れています。C#やNode.JSなど他言語におけるasync/awaitと同等の機能が言語標準で提供されるようになりました。Sanicを使う場合は、個人的にNode.js開発でよくやってしまうawaitの置き忘れも気にしなくて良いのは助かります。

JSON対応

JSONレスポンスを返すには以下のように書きます。FlaskもSanicどちらも使いやすいと思います。パッケージ構成はSanicの方が整理されてる気はしますがどうでもいいレベル。

ちなみにJSONパーサーは独自実装ではなく外部モジュールが使われています。Flaskを使う場合はsimplejsonをセットで入れておくと良いです。Sanicはujsonが必須となっています。

  • Flask: simplejson or json
  • Sanic: ujson

JSONモジュールのベンチマークに関しては以下のサイトが参考になります。

ujson優勢みたいなのでJSONレスポンスを含めたベンチマークだとFlaskが不利だと思うので、テキスト("hello,world")のみを返すシンプルなコードで計測します。ただ、有利不利とか言ってしまうとuvloopに乗っかっているSanicはズルいという話になってしまいますけども。。

ベンチマーク

前述のサンプルコード('hello, world'文字列を返却)で組み込みHTTPサーバのベンチマーク。同一LAN内でクライアント/サーバの2台用意して計測、ツールはwrk2を利用しました。

ネットワーク周りのカーネルパラメータは以下の通りです。TCPのsyn backlogをsomaxconnの値と合わせて増やす程度のチューニングはしてあります。TCP接続を使い回す設定にもしていますが、今回のベンチマークでは意味ないですね。

まずは4スレッド同時100コネクション、2000rpsで30秒間負荷を与えました。

レイテンシ周りの計測値を見てみるとわかりますが、これだけでもかなり性能差があることが読み取れますね。低スペックのサーバで動かしているのですがSanicの方はまだまだ余裕がありそうです。次は4スレッド同時200コネクション、4000rpsで30秒間負荷を与えました。

Flaskの方は限界が見えてますね。タイムアウトも発生しています。ネットワーク周りのカーネルパラメータが未調整だと他のソケット関連エラー等も併せて出てくるはずです。一方でSanicの方は安定した性能です。uvloopすごい。

(横軸の数値が見えてないですが、1/ (1 - Percentile) 刻みの対数表示となっています)

おわりに

Pythonのasyncioに対応したフレームワークはSanic以外にもいくつかありますが、既存のFlask製アプリケーションの移行コストを最小化するために、Flask-likeなSanicを採用するのは悪くない選択だと思います。2,3年前にFlaskの代替として、Falconというフレームワークに移行する作業もやったことがあるのですが、使い方がFlaskと結構違うので移行コストが高かったです。その点、Sanicを使えばその辺りの面倒を省くことができますね。Flaskプラグインの移植性も高く、ベストプラクティスの流用も行いやすいのではないでしょうか。

また、Sanicはasyncioのdrop-in replacement実装としてuvloopを組み込んでおり、uvloop自体はlibuvをベースにしているので実績も十分、性能も保証されています。Python3.5以降が必要なので実業務で採用するのはまだ少しネックになりそうですが、そこは時間が解決してくれる部分でしょう(スタートアップのサービスとかなら勢いで採用しても良いかも?)。他にuvloopをベースにしたフレームワークとしてはjaprontoというのもあります。Flask-likeでなくても良いならこちらも選択肢としてアリですね。

まずはプライベートワークで作っているPython製WebAPIはSanicに移行してみようと思いました。前回紹介した pyvips(libvips) を利用すれば高速な画像処理サーバもPythonで簡単に書けそうです。

参考

 

Tags: ,

libvipsで高速省メモリな画像処理

今回は高速省メモリな画像処理ライブラリである libvips を使ってみます。

libvips is a demand-driven, horizontally threaded image processing library. Compared to similar libraries, libvips runs quickly and uses little memory. It has around 300 operations covering arithmetic, histograms, convolution, morphological operations, frequency filtering, colour, resampling, statistics and others. - libvips: A fast image processing library with low memory needs.

libvips自体は著名な画像処理プロダクトですが、各スクリプト言語のバインディングライブラリや公式サイトが2017年に刷新されたようで、今後はさらに利用が普及することが予想されます。また、PythonバインディングのpyvipsはTop 10 Python libraries of 2017でも紹介されており、注目度も高くなっています。今回はこのpyvipsを使って簡単な使い方を学びます。

環境

  • Ubuntu 16.04.3 LTS (Bash on Ubuntu on Windows10 Pro)
  • Python 3.6.3 (conda 4.3)
  • libvips 8.6.1
  • pyvips 2.0.4

導入

* apt を利用する場合

aptでインストールするのは簡単ですが、バージョンがけっこう古いようなので注意してください。それに必須ではない依存パッケージもいろいろインストールされるようなのでミニマリストの方にはあまり印象は良くないかも。最新版かつ好きな構成で使いたいならgithubからソースをダウンロードしてきて手動でコンパイル・インストールします。

* ソースからインストールする場合

./configure時に対応ファイルフォーマットが標準出力に表示されるので、必要な依存ライブラリ(libjpeg、libpngなど)も併せてインストールしておいてください。

* 確認

libvips本体のインストールが完了したら続けてPythonバインディングのpyvipsをインストールします。こちらはpipを使えば簡単に入ります。

libvips本体が事前にインストールされていない場合、pyvipsのimport時にOSErrorが出ます。

使い方

今回は動作確認を兼ねて画像入出力や簡単な画像処理を試してみます。以降はJupyter Notebookで書いたドキュメントを貼り付けます。

上記のように、pivipsとnumpy間のデータ相互変換(pyvips.Image <-> numpy.ndarray)が可能なので、OpenCVとの連携も簡単に行うことができそうですね。

おわりに

libvipsは比較的簡単な画像処理用途に特化したプロダクトで、高速省メモリな処理が特徴となっています(Speed and memory use)。もし画像処理を手軽に行いたいだけならOpenCVを導入するのは大袈裟なので、要件に応じて適切なプロダクトを採用したいですね。今回は導入部分の紹介でしたが、次回はより深掘りして調査できればと思います。

ちなみに今回のエントリーからJupyter Notebookで書いたドキュメントを時々記事に貼ろうと考えているので、GitHubにNotebook用のリポジトリを作りました。こちらも興味があれば。

 

Tags: ,

JavaScriptで機械学習の実装 5 Gradient Boosting

少し間が空いてしまいましたけど、今回はGradient Boosting (勾配ブースティング)と呼ばれる機械学習アルゴリズムを試してみます。機械学習関連のコンペでも大人気の手法ですね。かなり昔(2011年)に決定木をJavaScriptで実装したことはあるので勾配ブースティングも併せて学んでおこうと思います。

Gradient Boosting (勾配ブースティング)

ブースティングについてWikipediaを引用すると、

ブースティング(英: Boosting)とは、教師あり学習を実行するための機械学習メタアルゴリズムの一種。ブースティングは、Michael Kearns の提示した「一連の弱い学習機をまとめることで強い学習機を生成できるか?」という疑問に基づいている[1]。弱い学習機は、真の分類と若干の相関のある分類器と定義される。- ブースティング - Wikipedia

ブースティングは逐次的(弱学習器を1つずつ順番に構築)に弱学習器を構築していく手法で、新しい弱学習器を構築する際にはそれまでに構築された全ての弱学習器の予測結果を利用するという特徴があります。Gradient Boosting (勾配ブースティング)では正解値と弱学習器の予測値との差を負の勾配と見なして、それを最小化するように逐次的に弱学習器を学習させていきます。弱学習器として決定木が用いられるため Gradient Boosting Decision Tree (GBDT) と呼ばれます。ちなみにOpenCVに顔検出機能はAdaBoostと呼ばれるブースティングアルゴリズムを利用していますね。AdaBoostは学生時代に特にお世話になりました。

今回は回帰問題を扱うので弱学習器には回帰木を用います。JavaScript(ES2015)での学習処理部分の実装を抜粋しますが、ここを見るだけでも勾配ブースティングの特徴がよく表れているのではないでしょうか。

検証

データセットはBoston housingを用い、人口 1 人当たりの犯罪発生数やNOxの濃度などの情報からボストン市の住宅価格を予測します。

今回もJavaScript(ES2015)で実装したのですが、いつものようにscikit-learn風なインタフェースにしていて、パラメータのデフォルト値はscikit-learnのGBDT実装(sklearn.ensemble.GradientBoostingRegressor)を参考にしました。もちろん外からパラメータ変更もできます。また、ファイルからのデータ読み込み処理は省略してデータセット自体も1つのモジュールとして埋め込んでいます。ソースコードもいつものようにGitHubに置いてあるので興味があれば。

クライアント側では以下のように使います。

* 出力結果例

モデル評価用にRMSEやR^2スコア(決定係数)計算用のモジュールも併せて作りました。まずは残差プロットでざっくりとモデルの性能を眺めてみます。最近はGoogleスプレッドシートでグラフを描くことが増えてきました。さすがにExcelよりは機能不足ですけど簡単なグラフならすぐ作成できるので便利に使っています。

良い感じに残差が均一に分散しています。それなりに上手く学習できているようです。

定量的な確認も少しだけ。データをシャッフルして3回計測したRMSEとR^2スコア(決定係数)の平均値を載せます。GBDTの学習率は0.1で固定して、回帰木の本数(ブースティングの繰り返し回数)と深さを変更しました。

RMSE R^2 score (決定係数)
回帰木の本数: 100, 深さ: 3 3.503 0.817
回帰木の本数: 200, 深さ: 4 3.156 0.851

ある程度想定した通りの結果が出ているので問題なさそうです。パラメータ調整についてはまだ理論を理解しきれていない部分もあるので適当なのですが、いろいろパラメータを変えて試してみた所、回帰木は深くしすぎてもダメみたいです。3 ~ 5くらいがちょうど良いんでしょうか。

おわりに

勾配ブースティングは以前からずっと作ってみたいと思っていたので、今回実装する機会を得ることができて良かったです。ところでロシアのYandexが7月18日にCatBoostというすばらしい名前の勾配ブースティング実装を公開しました。今はこちらを使っていろいろ勉強中です。まだ日本語のドキュメント類は無いみたいなので情報がまとまったらCatBoostに関するエントリーを書くかもしれません。またよろしくおねがいします。

参考

 

Tags: , ,

GPU対応の類似検索(最近傍探索)ライブラリ Faissの紹介 part2 GPUを利用した検索

Facebook AI Research (FAIR)が開発したGPU対応の類似検索ライブラリ Faiss に関する2回目の紹介エントリーとなります。前回は、FaissのインストールとC++チュートリアルの説明を行いました。今回はGPUを利用した検索処理を試してみます。

Faissのインストール方法については前回のエントリーを参照してください。

環境

Amazon Linux AMI release 2017.03
Intel Xeon CPU E5-2686 v4 @ 2.30GHz
NVIDIA GK210GL [Tesla K80]
Ubuntu 16.04 LTS (x86_64)
Intel Core i5-2500K CPU @ 3.30GHz
NVIDIA GM204 [GeForce GTX 970]
CUDA Toolkit 8.0
GCC 4.8.3 / 5.4.0
Python 3.5.2
NumPy 1.12

今回からはプログラムのベンチマークも行っていくので自宅のGPUサーバをメインに使って検証します。EC2のp2.xlargeをがっつり使うと請求が怖いし、そもそもレイテンシが大きくてストレスが溜まるので。

前回の導入部分の補足としてCUDAコード部分のコンパイルにはGCCではなくNVCC(CUDAコンパイラ)を利用するので事前にインストール済みであることを確認しておきます。また、GPU製品のCompute Capabilityは3.5以降が必要となっています。

GPUでk-meansクラスタリング

まずは、公式ドキュメントにサンプルとして掲載されているGPUを利用したk-meansクラスタリングプログラムの動作確認をします。日本語での補足も簡単に入れておきます。

faiss::gpuに属するクラスについては後述しますが、faiss::gpu::GpuIndexFlatL2 は前回のエントリーで紹介した faiss::IndexFlatL2 のGPU版となっています。faiss::Clustering::train 実装内部ではイテレーション毎に現在のセントロイドをインデックスしておき、全データをクエリとして検索しています。

* コンパイル

* 実行結果例

GPUを利用したk-meansクラスタリングは過去にもいろいろライブラリ等が出ていますし、サンプルとしてはよく見るものです。ここでの動作確認では5,000,000個のデータを20,000クラスタに分けるのに4分弱で処理できました。いまいちよく分からないのでCPU版と比較してみます。

上記プログラムをCPUでのクラスタリング処理に変更するには、インデックスオブジェクトである faiss::gpu::GpuIndexFlatL2faiss::IndexFlatL2 に変えるだけです。ただし上記プログラムの設定だとCPU版の処理が一向に終わらなかったので以下のようにテストの規模を縮小することで調整しました。

  • データ(128次元): 500,000
  • クラス(分割数): 2,000
  • イテレーション: 20

500,000個のデータを2,000クラスタに分けます。それぞれ5回計測した平均処理時間は以下の通りです。また、参考までに各製品のリリース時期も併記しています。

CPU
Intel Core i5-2500K
(2011/1)
CPU
Intel Xeon E5-2686
(2016/6)
GPU
NVIDIA GM204 [GeForce GTX 970]
(2014/9)
GPU
NVIDIA GK210GL [Tesla K80]
(2014/11)
511.15s 114.10s 5.70s 4.85s

前回のエントリーにも書いた通り、CPU版はOpenMPやSIMDを駆使して実装されており十分に高速です。しかしGPU版はそれと比較しても遙かに高速に動作します。また、CPUからGPUへのデータ転送は必要に応じて都度行われるため(計算に必要なデータだけGPU上に載せる)、VRAM容量を超えるような巨大なデータセットでもクラスタリング可能となっています。

さらに、FP16(半精度浮動小数点値での演算)を有効にしてGTX 970上で比較してみました。こちらも同様に5回計測した平均処理時間を載せます。

  • データ(128次元): 5,000,000
  • クラス(分割数): 20,000
  • イテレーション: 20
FP16無効 FP16有効
228.47s 191.76s

なかなか興味深い結果となりました。確かPascal世代からFP16の性能が格段に上がっていたかと思うので(1SPでFP16二つを同時演算したり)、PascalのGPUでベンチマークしてみたいところです。Facebook ResearchのレポートだとTesla P100でだいたい100秒ほどで完走するらしいです。
* GPU k means example

GPUを利用した検索

次は前回紹介したCPU版チュートリアルにある各種検索プログラムをGPU対応していきます。

GPUリソースの確保・初期化

GPUで処理するために予めGPUリソースの事前確保作業が必要ですが、生のCUDAプログラムと比較すれば十分抽象化されているので特に難しいところは無いかと思います。GPU版のヘッダファイルは faiss/gpu 以下にあります。

  • faiss::gpu::StandardGpuResources
  • faiss::gpu::GpuIndexFlatConfig

前述のk-meansのサンプルコードと同じ設定でだいたい大丈夫です。内部ではcuBLASのハンドル初期化やCUDAのストリーム生成処理等を行っています。デフォルトだとストリーム(cudaStream_t)は2本準備しているようです。

GPU版インデックスクラス

チュートリアルで紹介した各種CPU版インデックス用クラスのGPU対応は簡単なのでまとめて紹介します。

CPU GPU
faiss::IndexFlatL2 faiss::gpu::GpuIndexFlatL2
faiss::IndexIVFFlat faiss::gpu::GpuIndexIVFFlat
faiss::IndexIVFPQ faiss::gpu::GpuIndexIVFPQ

2017/05現在、faiss::gpu::GpuIndexIVFPQ にはGPU版インデックスクラスのポインタを直接受け取るコンストラクタは未実装のようですので別のコンストラクタを利用しています。この場合は引数で渡す faiss::MetricType に応じたGPU版インデックスクラスのインスタンスを内部で生成/保持してくれます。前回紹介していませんでしたが距離尺度は faiss::MetricType 定数で指定するようになっており、チュートリアルではL2ノルムの紹介のみでしたが、もちろん内積(Inner Product)にも対応しています。

また、faiss::gpu::IndicesOptions は以下のように定義されています。これはCPU<->GPU間でのインデックス情報の転送・保存をどのような形式で行うかを指定します。

CPU版とは異なりGPU版の各種インデックスクラスのメンバ変数はなぜかカプセル化されているみたいなので、提供されているgetter/setter経由でデータ操作を行います。GPU対応する際には忘れないようにしましょう。

ベンチマーク

次は最近傍探索処理の測定を行います。チュートリアルのプログラムをベースにパラメータ等を調整して比較しました。論文中ではDeep1BYFCC100Mなどの巨大なデータセットでベンチマークをしていて真似したかったんですけど、自宅のGPU開発環境だと空冷が適当なので kidle_inject が発生し、温度上昇防止のためCPU使用に制限をかけてしまいます。。ここではANN_SIFT1Mと同じ小規模データでベンチマークを行いました。

  • 次元数: 128
  • データ数: 1,000,000 (100万データ)
  • 同時実行クエリ数: 10,000 (1万クエリ)
  • インデックス: faiss::gpu::IndexFlatL2 (全探索) / faiss::gpu::IndexIVFPQ (PQ + 転置インデックス)
  • 転置インデックスのリストサイズ 100、サブベクトル数 8、量子化ビット数 8、FP16有効
  • 検索時のkの値: 1 ~ 1024 の範囲で変更

ベンチマーク用プログラムはここに記載するには少し長くなるのでGistにアップロードしておきます。

僕の個人開発環境だと1万クエリの上位20件を検索するのに、全探索(IndexFlatL2)の場合は約1.1秒、PQ + 転置インデックス(IVFPQ)の場合は約20ミリ秒でした。ただし、Faissの真価はbillion-scaleでの検索処理において発揮されるので、ここでのベンチマークは参考程度ということで。

また、Facebook AI Research公式の詳細なベンチマーク結果は以下を参照してください。

注意点など

GPUでの検索処理にはいくつか制限が設けられています。2017/06現在の仕様なので変更される可能性があります。

GPUテンポラリメモリ

cudaMalloc/cudaFree はコストが高い処理なので事前にある程度のメモリ領域が自動的に確保されるのですが、そのサイズをユーザー側が指定することができます。StandardGpuResources のAPIで指定できます。

テンポラリメモリが足りないと以下のような警告が出力されます。cudaMalloc が新たに必要になってしまうから遅くなるよというヒントであって処理が継続できないわけではありません。

PQ(Product Quantization)のために内部ルックアップテーブルを準備する際にはそこそこ十分な容量を確保しておいた方が良いかと思います。

kおよびnprobe値の上限

kとnprobe値は1024が上限となっていますが、1024もあれば実用上は十分かなと思います。

Product Quantizationのパラメータ

PQの実装上、量子化ビット数(サブベクトル当たりのデータサイズ)は8ビット以下、データの次元数をサブベクトル数で割った余りが0になるように調整する必要があります。今回紹介したベンチマークプログラムではデータは128次元、サブベクトル数は8、量子化ビット数は8で設定しています。

おわりに

今回はFaiss GPU版のインタフェースを紹介しました。CPU版から移行修正しやすいように工夫されているので特に難しいところはなかったかなと思います。GPU版の実装は論文と併せて読み進めていますが、k-selection(選択アルゴリズム)のGPU実装の仕組みが難しくて難航しています。。理論面を調べつつ、そろそろPythonインタフェースを用いたアプリケーション寄りの開発もやってみたいと思っています。

 

Tags: , ,