Rest Term

Blog

OpenCV 4.0のONNXサポートについて

前回の記事に引き続きOpenCV4.0の新機能を紹介します。今回は待望のONNXフォーマット対応を試してみます。

また、OpenCVのDeep Learning関連機能については昔の記事に書いてあります。基本的な使い方はこの頃からあんまり変わってないので参考までに。

ONNXとは

ONNX is a open format to represent deep learning models. With ONNX, AI developers can more easily move models between state-of-the-art tools and choose the combination that is best for them. ONNX is developed and supported by a community of partners.

ONNX(Open Neural Network Exchange)はニューラルネットワークのモデルを定義するためのオープンフォーマットの一つで、様々なDNNフレームワークで作成したモデルをONNXフォーマットで出力することで相互利用することができます。OpenCVでは既にCaffeやTensorFlow等のモデルを読み込みできましたが、OpenCV4.0ではONNXフォーマットもサポートされました。

環境

* CentOS 7.5 (x86_64/Xeon 8core CPU/16GB RAM)
* Python 3.6.5 (Anaconda custom)

使い方

これまでのOpenCV関連記事では基本的にC++から使っていましたけど、たまにはPythonだけでさくっと試してみます。まずは適当なONNXモデルをModel Zooから探します。

ここでは動作確認しやすい画像分類用モデル(ImageNet ILSVRC2012)であるResNet-50のCNNモデルで試してみます。必要なファイルは上記のリポジトリからダウンロードできます。

* 入力画像 (beer.jpg みなとみらいのオクトーバーフェストの時の写真)

* ONNXファイル

* 実装 (read_onnx.py)

* 出力結果

出力結果を見る限り特に問題は無さそうです。前処理としての入力画像の正規化や、カテゴリ定義ファイルの読み込み処理等を省いて見てみると、ONNXモデルの入力と推論処理は簡潔に書けることがわかります。ONNXファイルの読み込みはcv2.dnn.readNetFromONNXメソッドを使うだけです。以前のようにImporterオブジェクトを作る必要も無いので楽ですね。また、前処理部分のコードはModel Zooで紹介されているものをOpenCVのデータ形式と合うように微修正したものを利用しています。一応、cv2.dnn.blobFromImageメソッドでもスケーリング処理等の前処理用オプションが提供されていますが、ここでは手動でスケーリング/正規化処理をしています。

ついでにResNet-50だけではなくShuffleNetも試してみます。こちらもModel Zooからダウンロードできます。

ShuffleNetはエッジデバイスでの効率的な推論処理を実現できる軽量なモデルアーキテクチャの一つです。ONNXファイル比でResNet-50の約18分の1のサイズで収まっています。

ソースコードの変更は読み込むONNXファイル名を変えるだけです。

* 出力結果

分類精度はResNet-50と比べても遜色ないレベルですね。軽量で推論処理も高速なので、久しぶりにRaspberry Pi等のデバイスで動かしてみようかなという気になってきます。

おわりに

近年のOpenCVは機械学習関連の機能を充実させていますが、ONNXフォーマットに対応したことで様々なDNNフレームワークとの併用がより推進されそうです。以前の記事でも書きましたが、個人的にOpenCV3.xを使う機会に恵まれなかった分、OpenCV4.0はどんどん使い倒したいと思います。

前述のサンプルコードはGitHubにも上げてあります。今回は珍しくPython版のみでしたけどC++版も後ほど作って上げておこうと思います。

あと、あけましておめでとうございます。今年もほどほどにがんばろう。

 

Tags: , ,

OpenCV 4.0のQRコード検出、G-APIを試す

先日、OpenCV 4.0が正式リリースされました。リリースノートの内容を載せます。

* OpenCV is now C++11 library and requires C++11-compliant compiler. Minimum required CMake version has been raised to 3.5.1.
* A lot of C API from OpenCV 1.x has been removed.
* Persistence (storing and loading structured data to/from XML, YAML or JSON) in the core module has been completely reimplemented in C++ and lost the C API as well.
* New module G-API has been added, it acts as an engine for very efficient graph-based image procesing pipelines.
* dnn module was updated with Deep Learning Deployment Toolkit from the OpenVINO™ toolkit R4. See the guide how to build and use OpenCV with DLDT support.
* dnn module now includes experimental Vulkan backend and supports networks in ONNX format.
* The popular Kinect Fusion algorithm has been implemented and optimized for CPU and GPU (OpenCL)
* QR code detector and decoder have been added to the objdetect module
* Very efficient and yet high-quality DIS dense optical flow algorithm has been moved from opencv_contrib to the video module.

まずは概要を確認すると、OpenCV4.0では実行環境がモダンになっていて(とはいえそれほど新しくはないのですが)、多くのC言語のインタフェースがついに削除されたようです。下位互換は保証されてないので業務で利用する際には注意が必要そうです。機能面ではGraph API (G-API)やDNNモジュールのIntel Deep Learning Deployment Toolkit (DLDT)連携、ONNXフォーマット対応など、近年のエッジデバイスにおける機械学習ブームを意識しているように見えますね。また、非機能面においても多くの性能改善が施されているようです。Intel CPUならAVX2命令をより活用するようになったりと、幅広い環境で高いパフォーマンスを発揮できるような細かな改善が見られます。さすが、メジャーバージョンが上がっただけのことはあります。

環境

* CentOS 7.5 (x86_64/Xeon 8core CPU/16GB RAM)
* GCC 4.8.5
* CMake 3.12.2
* Python 3.6.5 (Anaconda custom)

いつものようにLinuxサーバ環境上に構築しました。今回はGPU(CUDA)環境は無しで試します。

インストール

OpenCV3.x系と比べて環境に対する要求がいくらか上がっているようなので注意してください。特にC++11以降が必須なのが大きいかなと思います。まずは今回の環境で必要なパッケージ群を予めインストールしておきます。Python関連はAnaconda環境を構築した段階で揃うはずなのでここでは割愛。また、Linuxサーバ環境にインストールするので各種GUI環境も利用しません。

*-develパッケージの方を入れると依存パッケージも大体一緒に入れてくれるはず。UbuntuとかのDebian系なら*-devパッケージでOK。

僕の場合はpyenv環境上でPythonでの開発を普段行なっているので、OpenCVのPythonインタフェースもpyenv環境に適用します。それ以外だとIntel TBB利用を明示的に指定しているくらいです。各画像コーデック/デコーダ、Intel IPP や Protocol Buffer 等のサードパーティライブラリはCMake側でインストールチェック、デフォルトで有効化してくれますので、ごちゃごちゃとオプションを細かく指定する煩雑さは軽減されているようです。GPU(CUDA)環境を利用する場合はもう少し設定が増えて複雑になりますけど今回は利用しませんので。

ただし、2018/12現在、環境によってはLAPACKのリンクをオフにする必要があるようです。CMakeオプション WITH_LAPACK=0 にすれば良いです。GitHub issueにも挙がっています。

動作確認

せっかくなのでOpenCV4.0から追加された新機能を試してみます。目玉機能の一つとして注目度の高いG-APIを触る前に、まずは気軽に使えそうなQRコード検出機能を試してみました。

QRコード検出

入力画像として以下のQRコード画像を使います。このブログのURL(https://rest-term.com)情報を埋め込んで作りました。

まずはC++インタフェースから試してみます。

* qrcode_sample.cpp

OpenCVのAPIは名前空間を省略せずに書いたので、cv::が付いている箇所を中心に見てみると少ないコードでQRコード検出できることがわかります。検出器のインスタンスを作って入力画像や出力結果を入れるコンテナを渡すだけです。C++11以降で使える型推論等の機構は使ってませんが、この規模のコードだとそれほど効果的ではないと思います。

* CMakeLists.txt

CMakeの最小バージョンは一応OpenCV4.0本体と合わせておきました。あとはC++11以降でコンパイルすることに注意しておけば大丈夫そうです。

* 実行結果

* 出力画像

正しくQRコードを検出できていますし、URL情報のデコードにも成功しています。QR決済のシステムやアプリに組み込むならデコード処理だけで良さそうです。また、QRコードの規格にはバージョンというものがありまして、QRコードの大きさはバージョン1の21×21セルからバージョン40の177×177セルまで4セル刻みで決められています。cv::QRCodeDetector::detectAndDecodeの3つめの引数にバージョンに合わせた解像度(セル数)でQRコードを書き出してくれるのでバージョン番号も計算できます。今回作成したQRコードだとバージョン3(29x29セル)だということがわかります。

次はPythonから使ってみます。PYTHONPATHは予め通しておきます。

* qrcode_sample.py

* 実行結果 (出力画像はC++版と同じなので省略)

Pythonインタフェースも動作に問題なさそうです。パッケージが cv2 なのはそろそろ変えて欲しいところですけども。

Graph API (G-API)

次はG-APIを試してみます。一連の処理を計算グラフとして構築・適用する機構となっています。DNNフレームワークだと一般的に備わっている仕組みで、Web系バックエンドでもよく利用されるApache Sparkのような分散処理環境においても同様の考え方で処理を記述することができます。膨大なロジックであってもグラフ上での局所的な計算の繋がり(組み合わせ)として考えるので、途中計算結果を共有できる(システム的にはキャッシュとして扱える)のはスループット向上に繋がりそうです。

公式のG-APIのチュートリアルはビデオ入力を伴っているので、ここでは少し修正して入力・出力共に静止画像を扱うようにしました。

* 入力画像

* 出力画像

個々の画像処理を繋げてパイプラインを作っていることが読み取れます。このサンプルでは一連の処理をすぐに適用していますが、適用自体は結果が必要になる時まで遅らせることも出来ますし、実行時に途中の処理を他の処理に差し替えることもできます。途中の処理結果はconstにしていることからもわかるようにイミュータブルなオブジェクトとして扱うことができるのも嬉しいですね。適切な粒度でグラフを構築しておけば、データの不変性・局所性により、どこかのノードで計算処理に失敗した場合でも、処理が成功しているノードまで遡って続きから再実行することもできます。

定義した計算グラフを遅延評価したい場合は、cv::GComputation::applyの代わりにcv::GComputation::compileでグラフを事前構築してから、任意の場所で実行できます。

また、2018/12現在、G-APIのPythonインタフェースは提供されていないようです。G-APIのC++インタフェースは、例えばApache SparkのRDD/DataFrame/Datasetを活用したコードと比較するとまだ洗練されていないように見えますし、そのままPython用にバインディングしてもスクリプト言語らしい手軽さで使えるようにするのは難しいかもしれません。

一応、グラフ定義部分はラムダ式を使うと以下のようにも書けます。一時オブジェクトのスコープが限定されるところは良いですけど、あんまり変わらない気も。

また、G-APIの重要な機構のひとつに実行環境を透過的に選択できる点があります。2018/12現在、リファレンスを見る限りCPU/GPU(via OpenCL)/Fluidがサポート予定のようです。今回はCPU環境で試しましたが、次はGPU(CUDA)環境にインストールしていろいろ試す予定なので、G-APIの実行環境についてはまた別記事にしようと思います。

なんだかんだ書きましたけど、G-API、とても面白いです。OpenCV4.0時点ではまだ開発段階とのことなので今後の改善に期待したいです(まぁ僕自身がOpenCV開発に貢献できればもっと良いのですが)。

おわりに

OpenCV2.x系は仕事でもよく使ってたのですが、OpenCV3.x系は結局ほとんど使うことなく4.0を試すことになってしまいました。3.x系からの機能差分や旧バージョンとの非互換性等の整理をしておきたいところです。メジャーバージョンが上がっただけあって、多くの新機能追加、性能改善などが施されているようですので引き続きいろいろ試してみたいと思います。

今回作成したサンプルコードはGitHubにも上げてありますので興味があれば。

先月まで仕事が激務だったんですが、やっと余裕ができたのでブログ更新の頻度も上げられるといいなぁ。

 

Tags: , ,

GitHubのユーザープロフィールを表示するWordPressプラグインを作った

WordPressの勉強目的にプラグインを1つ作ってみました。

WP GitHub Card

GitHubのユーザープロフィールをカード形式でWordPressのページに貼り付けるだけのプラグインです。以下のように使います。

上記のコードをWordPress上の任意の場所に貼り付けます。WordPressのショートコード形式となっているのでウィジェットに限定しません。どこでもOKです。

* 表示例

上記スクリーンショットの通り、以下の情報が表示されます。

  • GitHubアカウント名とアバター画像
  • 最近コミットしたリポジトリ
  • よく使うプログラミング言語トップ3(リポジトリのスター数でソート)
  • パブリックなリポジトリとgist数
  • フォロワー数
  • 全てのリポジトリのスター数の合計

データ取得にはGitHubの公式APIを利用しており、取得した各種データはWordPressのDB内に一定時間キャッシュして非機能面も考慮していまう(Transientsのキャッシュ機構を利用)。

公式プラグインページとソースコードは以下にあります。

目的

元々はGitHub Badgeというサービスの機能を自分のサーバでホスティングしたいという動機からプラグイン開発を始めました。GitHub Badgeのように外部コンテンツをiframeで自分のサイトに埋め込む方式だと性能面やセキュリティ面で問題を感じており、また、サービスとして提供すると不特定多数の人が利用するためGitHub APIのリクエスト数制限にもひっかかる頻度が高くなります。それらの問題を解決するために、自分のサーバでコンテンツ配信しつつ適切にデータをキャッシュすることにしたという経緯です。

WordPressについて

このサイトはWordPressで数年運用していますけど、これまでWordPress本体についての知識はほとんどなかったです。まぁプラグインを作った程度ではWordPressのほんの一部しか理解できないと思いますが、良い勉強になりました。

ところで2018年現在、インターネット全体(厳密にはtop10 millionのサイトが集計対象)の約30%のWebサイトがWordPress製サイトであるという統計結果があります。

2015年時点では25%程度だったそうなので、そこから3年弱で5%も上がったということですか。大昔にみんながMovable TypeからWordPressに移転してた頃に現在のこの状況を想像できたでしょうか。

また、GoogleもWordPressエキスパートの求人をしているようです。それほどWordPressは現在のWebにおいて重要な位置に鎮座しているということでしょうか。最近まで転職活動をしてた知人のWebデザイナーも面接でWordPressが使えるかどうか聞かれたことがあると言っていました。

長年WordPressを触っていたにもかかわらず、これまで全くと言っていいほどWordPressについて無知でした。遅いスタートになりますが今後もプラグインやテーマを作ってみる機会を増やしてWordPressの学びを増やしていきたいと思います。

 

Tags: , ,

常時SSL化とHTTP/2対応しました

ドメイン rest-term.com の常時SSL化とHTTP/2に対応しました。

本サイトのコンテンツはCloudflareで配信しているので常時SSL化自体は簡単でした。後はコンテンツ内でのhttp/https混在(mixed content)を解消したり、パフォーマンス改善のためにWordPressプラグインを整理したり、一部は自作したものに置き換えたりとか細かい作業をしていました。SSL化関連の作業メモについてはWikiの方に書いてますので興味があれば。CloudflareだけでなくLet's Encryptの証明書発行作業等についても触れています。

現在、RSSフィードは旧URLと新URL(https://rest-term.com/feed/atom/)の両方を配信してます。いずれは新URLの方だけにする予定です。

HTTPS化したのでHTTP/2対応も併せて行いました。早くHTTP/2が当たり前の世界になって欲しいですね。

HTTPS化するとはてなブックマークなどのSNSへのシェア数がリセットされる問題は有名かと思いますが、SNS Count Cacheプラグインを利用すればHTTP時代の数値を合算表示させるのは簡単に実装できました。あとはGitHubバッジのウィジェットが重いのでセルフホスティングできるタイプのプラグインを自作しようかと思っています。

近い内に各Webブラウザが非HTTPSサイトを警告表示する取り組みが実施されるかと思いますが、常時SSL化が間に合って良かったです。まだ一部不具合が残っているページもあるかもしれませんが確認次第対応していきます。

 

Tags: ,

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: , ,