Scala製の機械学習サーバApache PredictionIOを使ってみよう

PredictionIOはSparkを中心としたJVMベースの機械学習アプリケーションを開発・運用するために必要なミドルウェアフレームワークなどを統合的に提供するもので、開発者はPredictionIOのフレームワークに従ってエンジンを作成することで、機械学習を使用したアプリケーションをWebサービスとしてデプロイすることができます。

まずはPredictionIOのWebサイトに掲載されているレコメンデーションのテンプレートを動かしてみます。

PredictionIOのインストール

ドキュメントを見るとソースからビルドするべしとなっており中々スパルタンな感じです。Dockerで動かすこともできるようですが、折角なのでソースからビルドしてみることにします(Javaは予めインストールしておく必要があります)。

まずはこちらから現時点で最新版の0.11.0-incubatingの配布物をダウンロードし、適当なディレクトリに展開します。ルートディレクトリがないようなのでディレクトリを掘ってから展開するのが良いです。

$ wget http://ftp.riken.jp/net/apache/incubator/predictionio/0.11.0-incubating/apache-predictionio-0.11.0-incubating.tar.gz
$ mkdir apache-predictionio-0.11.0-incubating
$ tar xvzfC apache-predictionio-0.11.0-incubating.tar.gz apache-predictionio-0.11.0-incubating

展開したディレクトリ内にあるmake-distribution.shでおもむろにビルドします。

$ cd apache-predictionio-0.11.0-incubating
$ ./make-distribution.sh

ビルドが成功するとPredictionIO-0.11.0-incubating.tar.gzというファイルができるので、これを適当な場所に展開します。

Sparkのインストール

PredictionIO-0.11.0-incubating/vendorsにSparkのディストリビューションを展開しておきます。が、PredictionIOがデフォルトでサポートしているのはSpark 1.6.3という古いのバージョンのようです。オプションでSpark 2系を使用するようビルドすることもできるのですが、デフォルトで最新のものが使われるようになって欲しいところです。

$ wget http://d3kbcqa49mib13.cloudfront.net/spark-1.6.3-bin-hadoop2.6.tgz
$ mkdir PredictionIO-0.11.0-incubating/vendors
$ tar zxvfC spark-1.6.3-bin-hadoop2.6.tgz PredictionIO-0.11.0-incubating/vendors

ストレージのインストール

PredictionIOの動作に必要なデータや、学習に使用するデータなどを格納するためのストレージを別途インストールする必要があります。PredictionIOは以下のストレージをサポートしています(ストレージに保存するデータにはメタデータ、イベントデータ、モデルデータの3種類があり、ストレージごとに保存できるデータが異なります)。

デフォルトではPostgreSQLを使用するようになっているのでこれを使ってみます。MacであればHomebrewでPostgreSQLをインストールできます。

$ brew install postgresql

ついでにbrew servicesで自動起動するようにしておきます(brew services stop postgresqlすると自動起動をやめることができます)。

$ brew services start postgresql

PredictionIOで使用するデータベースやユーザを作成します。

$ createdb pio
$ psql pio -c "create user pio with password 'pio'"

PostgreSQLのJDBCドライバをダウンロードしてPredictionIO-0.11.0-incubating/libに配置します。

$ cd PredictionIO-0.11.0-incubating/lib
$ wget https://jdbc.postgresql.org/download/postgresql-42.0.0.jar

PredictionIOを起動する

ここまでできたらPredictionIOを起動してみます。

$ cd PredictionIO-0.11.0-incubating/bin
$ ./pio eventserver &

すると、7070ポートでイベントサーバが起動します。この後の操作ではPredictionIO-0.11.0-incubating/binにあるpioコマンドを多用することになるのでこのディレクトリにPATHを通しておくとよいです。

学習データを登録する

PredictionIOのイベントサーバにアクセスするためのアクセスキーを発行します。

$ pio app new MyApp1

こんな感じでアクセスキーが発行されます。

...
[INFO] [App$] Initialized Event Store for this app ID: 1.
[INFO] [Pio$] Created a new app:
[INFO] [Pio$]       Name: MyApp1
[INFO] [Pio$]         ID: 1
[INFO] [Pio$] Access Key: i-zc4EleEM577EJhx3CzQhZZ0NnjBKKdSbp3MiR5JDb2zdTKKzH9nF6KLqjlMnvl

このアクセスキーを毎回指定するのは面倒なので以下のように環境変数に設定しておきます。

$ ACCESS_KEY=i-zc4EleEM577EJhx3CzQhZZ0NnjBKKdSbp3MiR5JDb2zdTKKzH9nF6KLqjlMnvl

学習データとして使用するデータをイベントサーバに登録してみます。

$ curl -i -X POST http://localhost:7070/events.json?accessKey=$ACCESS_KEY \
-H "Content-Type: application/json" \
-d '{
  "event" : "rate",
  "entityType" : "user",
  "entityId" : "u0",
  "targetEntityType" : "item",
  "targetEntityId" : "i0",
  "properties" : {
    "rating" : 5
  }
  "eventTime" : "2014-11-02T09:39:45.618-08:00"
}'
$ curl -i -X POST http://localhost:7070/events.json?accessKey=$ACCESS_KEY \
-H "Content-Type: application/json" \
-d '{
  "event" : "buy",
  "entityType" : "user",
  "entityId" : "u1",
  "targetEntityType" : "item",
  "targetEntityId" : "i2",
  "eventTime" : "2014-11-10T12:34:56.123-08:00"
}'

登録したイベントを検索してみます。

$ curl "http://localhost:7070/events.json?accessKey=$ACCESS_KEY"

以下のように、2つのイベントが登録されていることが確認できます。

[
  {
    "eventId":"774fdb3a572f48479e76e34681c7371b",
    "event":"rate",
    "entityType":"user",
    "entityId":"u0",
    "targetEntityType":"item",
    "targetEntityId":"i0",
    "properties": {
      "rating":5
    },
    "eventTime":"2014-11-02T09:39:45.618-08:00",
    "creationTime":"2017-05-05T09:35:54.604Z"
  },  
  {
    "eventId":"c1182fe0451d4744a0b695770c8098e9",
    "event":"buy",
    "entityType":"user",
    "entityId":"u1",
    "targetEntityType":"item",
    "targetEntityId":"i2",
    "properties":{},
    "eventTime":"2014-11-10T12:34:56.123-08:00",
    "creationTime":"2017-05-05T09:36:05.372Z"
  }
]

レコメンデーションエンジンを動かしてみる

本題のレコメンデーションエンジンです。PredictionIOのオフィシャルで提供されているエンジンテンプレートを使用してみます。まずはgitコマンドでテンプレートのリポジトリを適当な場所にクローンします。

$ git clone https://github.com/apache/incubator-predictionio-template-recommender.git MyRecommendation
$ cd MyRecommendation

準備として予め用意されているデータをまとめてインポートします。まずはPredictionIOのPythonSDKをインストールします。

$ pip install predictionio

以下のようにするとサンプルデータをまとめてイベントサーバにインポートできます。

$ curl https://raw.githubusercontent.com/apache/spark/master/data/mllib/sample_movielens_data.txt --create-dirs -o data/sample_movielens_data.txt
$ python data/import_eventserver.py --access_key $ACCESS_KEY

次にMyRecommendation/engine.jsonを編集してappNameに先ほどアクセスキーを発行したしたアプリケーション名を設定します。

...
"datasource": {
  "params" : {
    "appName": "MyApp1"
  }
},
...

レコメンデーションエンジンをビルドします。

$ pio build --verbose

登録したイベントデータを使用して学習を行います。

$ pio train

ここでスタックオーバーフローが発生してしまうことがあります。その場合はMyRecommender/src/main/scala/ALSAlgorithm.scalaの以下の行のコメントアウトを外してチェックポイントを有効にします。

sc.setCheckpointDir("checkpoint")

ここまででようやく準備完了です。レコメンデーションエンジンをWebサービスとしてデプロイします。

$ pio deploy

8000ポートでレコメンデーションエンジンのWebサービスが起動します。ブラウザでアクセスすると以下のような画面が表示されるはずです。

f:id:takezoe:20170511132241p:plain

実際にレコメンデーションエンジンを呼び出してみます。

$ curl -H "Content-Type: application/json" \
-d '{ "user": "1", "num": 4 }' http://localhost:8000/queries.json

すると以下のような結果が返ってくるはずです。指定したユーザに対してスコアが高い順に4つのアイテムが返却されていることがわかります。

{
  "itemScores":[
    {"item":"22","score":4.072304374729956},
    {"item":"62","score":4.058482414005789},
    {"item":"75","score":4.046063009943821},
    {"item":"68","score":3.8153661512945325}
  ]
}

まとめ

だいぶ手順が長くなってしまいましたが、PredictionIOは機械学習を使用したアプリケーションを開発・運用するために以下のような機能を提供していることがおわかりいただけたのではないかと思います。

今回は実際にPredictionIOでどのようにエンジンを開発するのかについては触れませんでしたが、興味のある方はエンジンテンプレートのソースコードを参照していただければと思います。また、PredictionIOのWebサイトには他にもいくつかのエンジンテンプレートが紹介されており、同様の手順で動かすことができますのでこちらも試してみていただければと思います。

Apache PredictionIOのコミッタになりました

f:id:takezoe:20170503224901p:plain

Apache PredictionIOはSpark MLlibを中心に、学習データやモデルデータを格納するためのストレージ、機械学習を利用したアプリケーションを定型的に開発するためのフレームワーク、作成したアプリケーションをWebサービスとしてデプロイする機能などを提供するもので、元々は2013年に創業されたPredictionIO社で開発されていたのですが、2016年2月にSalesforce社に買収され、Apache Software Foundationに寄贈されたものです。

github.com

弊社の機械学習チームでは機械学習を応用したアプリケーションを組織的に開発していくにあたり、開発・運用基盤としてApache PredictionIOに取り組んでいるのですが、このたび弊社から以下の4名がコミッタとしてPredictionIOの開発に参加させていただけることになりました(Elasticsearch周りなどで積極的に貢献されていた菅谷さんのプレゼンスが大きかったと思います)。

github.com

github.com

github.com

github.com

自分は以前Apache Clickのコミッタをやっていたのですが(ちなみに菅谷さんもApache Portalsのコミッタです!)、ClickがIncubatorに移行したのが2008年なのでもう9年前になるんですね…。その後2010年にトップレベルプロジェクトに昇格し、開発が停止してAtticに移ったのが2014年なので、約3年ぶりにコミット権のあるプロジェクトに復帰したことになります。まずはIncubatorを卒業するのが目標ということになるでしょうか。

自分は機械学習やSpark周りについては素人同然なのですが、PredictionIOはScalaで作られてはいるものの正直Scala的にはあまりよろしくないコードや、作り的に微妙な部分が多々ありますので(Java的にもどうなのかという部分もありますが)、引き続きScala警察として活動していきたいと思います。

MozillaのスクレイピングフレームワークFathomを試してみる

こちらのOSDNの記事で知ったのですが、MozillaでFathomというJavaScript用のスクレイピングフレームワークを開発しているようです。仕事でクローラーを作ったりしていたこともあり、面白そうだと思ったので軽く調べてみました。

mag.osdn.jp

以下のGitHubリポジトリで開発されています。GitHub Pagesに詳しいドキュメントもあります。

github.com

OSDNの記事で触れられている開発者のErik Rose氏のブログエントリはこちら。

hacks.mozilla.org

ドキュメントやErikさんのブログエントリなどをざっと眺めてみたところ、それほど複雑なものではなく、ある程度曖昧なルールとスコアリングを定義しておき、最もスコアの高い要素の選択するというのが基本的なコンセプトのようです。

なにはともあれ試してみます。まずは適当なディレクトリを掘り、npmでFathomとjsdomをインストールします。

$ npm install fathom-web
$ npm install jsdom

プログラムはこんな感じです。FathomのドキュメントのBasic Useに記載されているものをベースにしています。

const {dom, props, out, rule, ruleset, score, type} = require('fathom-web');
const jsdom = require('jsdom');
const { JSDOM } = jsdom;

function containsColonsOrDashes(element){
  // 要素にコロン(:)かダッシュ(-)が含まれてたらtrueを返す
}

const rules = ruleset(
  // titeタグ、スコアは1
  rule(dom('title'), type('titley')),
  // OpenGraphのmetaタグ、スコアは2
  rule(dom('meta[property="og:title"]'), type('titley').score(2)),
  // タイトルにコロン(:)かダッシュ(-)が含まれている場合は
  // ナビゲーション用っぽいのでペナルティとしてスコアを×0.5する
  rule(type('titley'), score(fnode => containsColonsOrDashes(fnode.element) ? .5 : 1)),
  // スコアが最も高いものをtitleキーとして提供する
  rule(type('titley').max(), out('title'))
);

// jsdomでDOMツリーを生成
const { document } = (new JSDOM('<html>...</html>')).window;
// 指定したDOMツリーに対する適用結果を取得
const facts = rules.against(document);

// 最もスコアの高いノードを取得してコンソールに表示
const bestTitleFnode = facts.get('title');
console.log(bestTitleFnode[0].element);

見ての通り、そんなに難しいことはしていません。上記のコードでは最もスコアの高い要素を取り出していますが、ルールにマッチした要素をすべて取得したり、要素からスコアを取得したりということもできます。

ルールの定義の仕方次第かもしれませんが、タイトルなどわかりやすいものはともかくデータとして使用する目的でスクレイピングするにはある程度きちんと論理的なマークアップがされていないとこのアプローチでは厳しいのではないかという気もします。ちゃんと使ってみないとなんとも言えない感じですね。実際は要素を取り出した後の切り出しや加工も必要だったりしますし…。

とはいえ、スコアリングの最適化機能など面白い機能もあるので時間があるときにもう少しちゃんとしたデータを使って試してみたいところです。

MacにHomebrewでPostgreSQLをインストールする

普通にHomebrewで入ります。

$ brew install postgresql

brew servicesで起動します。brew servicesというのは自動起動の設定をよろしくやってくれるものらしいです。

$ brew services start postgresql

サービスの状態を確認してみます。

$ brew services list
Name       Status  User          Plist
emacs      stopped
postgresql started naoki.takezoe /Users/naoki.takezoe/Library/LaunchAgents/homebrew.mxcl.postgresql.plist

自動起動をやめたくなった場合は以下のようにします。

$ brew services stop postgresql

PostgreSQLの操作に戻ります。データベースの作成。

$ createdb gitbucket

データベースに接続するためのユーザの作成。

$ psql gitbucket -c "create user gitbucket with password 'gitbucket'"

psqlでデータベースに接続してみます。

$ psql gitbucket
psql (9.6.2)
Type "help" for help.

gitbucket=#

まあ最近だとローカルに直接入れるよりDockerで立ち上げる方がいいのかもしれませんが…。

Macでpyenvを使ってPython開発環境を整える

Rubyのrbenvなどとほとんど同じ感じだと思いますが、メモとして残しておきます。

まずはHomebrewでpyenvをインストール

$ brew install pyenv

.bash_profileにpyenvを使うための設定を追加。

$ echo 'export PYENV_ROOT="${HOME}/.pyenv"' >> ~/.bash_profile
$ echo 'export PATH="${PYENV_ROOT}/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(pyenv init -)"' >> ~/.bash_profile

実際に追加されるのは以下の設定。

export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"

インストール可能なバージョンの一覧を表示。

$ pyenv install --list

バージョンを指定してPythonをインストール

$ pyenv install 2.7.13

使用するバージョンを指定。

$ pyenv global 2.7.13

バージョンを確認。

$ pyenv versions
  system
* 2.7.13 (set by /Users/naoki.takezoe/.pyenv/version)

$ python -V
Python 2.7.13

よさそうです。

Sierraでのキーボードの設定

SierraでKarabinar & Seilが使えなくなってしまうという致命的な問題によりキーボードの設定について研究が必要となりました。特に自分の場合、

  • Mac本体のキーボードはUS配列
  • ThinkPadキーボードはJIS配列

となっており、US配列とJIS配列の両方をカバーする設定が必要になります。やりたいことは単純で、スペースキーの両サイドのキーで英数/日本語の入力切替を行いたいということで、あとはこれを成立させるための設定や、より便利にするための細かい設定という感じです。

結論から言うと、Karabinar Elements英かなというツールを併用することで以前と同じキーマッピングを実現することができました。

Mac本体での設定

デフォルトでは日本語入力の切替がcontrol + spaceで行われるようになっているのですが、IDEでの入力補完やEmacsでの範囲選択などと被るので無効化しました。

f:id:takezoe:20170501202804p:plain

caps lockcontrolに入れ替えるのはOSの設定でもできるのですが、接続するキーボード毎に設定しなくてはいけないので後述するKarabinar Elementsで行っています。

また、トラックパッドの設定で、外部マウスが接続された場合にトラックパッドを無効化するようにしています。

f:id:takezoe:20170501202819p:plain

英かなの設定

日本語入力を左右のコマンドキーで行うように設定します。

f:id:takezoe:20170501201850p:plain

Karabinar Elementsでも左右のcommandキーを英数/日本語の切替に変更することができるのですが、そうするとcommandキーが使えなくなってしまうので、commandキーと組み合わせるショートカットが使用できなくなってしまいます。

Karabinarではcommandキーを単独で押した場合は入力切替、他のキーと組み合わせて押した場合はコマンドキーとして動作するという設定ができたのですが、Karabinar Elementsでは今のところできないようです。ただ、以下のプルリクエストが取り込まれればできるようになるみたいなので将来的にはKarabinar Elements単体で済むようになるかもしれません。

github.com

Karabinar Elementsの設定

JIS配列のキーボードの変換、無変換キーで入力切替を行うように設定します。また、caps lockcontrolに置き換える設定もKarabinar Elementsで行なっています。

f:id:takezoe:20170501202029p:plain

あとはファンクションキーを押した場合に通常のファンクションキーとして動作するようにするのと、外付けキーボード接続時は本体のキーボードを無効化する設定を行なっています。

f:id:takezoe:20170501202855p:plain

f:id:takezoe:20170501202905p:plain

残課題

これで一応以前と同じ動きをするように設定できたのですが、外付けのJISキーボードがUS配列に認識されたり、一度JISキーボードを接続すると本体のキーボードもJIS配列と認識されてしまったりすることがあります。OSの設定でもう一度キーボードの配列を指定すれば直るのですが、ちょっと面倒です。

面倒といえばこれは以前からなのですが、ThinkPadキーボードのトラックポイントのスクロール方向と、本体のトラックパッドの設定が連動しており、本体のトラックパッドと外付けのトラックポイントを切り替える際に毎回設定を変更しないといけません。外付けマウスの場合のみスクロール方向を逆転させるツールもあって以前試してみたのですが、うまくスクロールができなくなってしまことがあるなど問題があり、常用は断念しました。

まとめ

  • US配列なら英かなだけでよさそう(ただし外付けキーボードのファンクションキーをデフォルトでF1〜F12として使うにはKarabinar Elementsが必要かもしれない)
  • JIS配列ならKarabiner Elementsだけでよさそう
  • 使用するキーボードはUS配列ならUS配列、JIS配列ならJIS配列に統一するべき

GitBucket 4.12をリリースしました

4.12にはリダイレクト時の処理に問題があり、これを修正した4.12.1をリリース済みです。こちらをご利用ください。 https://github.com/gitbucket/gitbucket/releases/tag/4.12.1

Scalaで実装されたオープンソースのGitサーバ、GitBucket 4.12をリリースしました。

https://github.com/takezoe/gitbucket/releases/tag/4.12

Gist埋め込み用のJavaScript

GistプラグインがGitBucket 4.12にあわせてバージョンアップし、スニペット埋め込み用のJavaScriptを提供するようになりました。サイドバーからJavaScriptを取得することができます。

f:id:takezoe:20170428124144p:plain

このJavaScriptをHTML中にペーストするとスニペットの内容が以下のように表示されます。

f:id:takezoe:20170428124152p:plain

ブランチ比較時のドロップダウンにフィルタを追加

ブランチ比較時にリポジトリやブランチを選択するドロップダウンメニューにフィルタリング用のボックスを追加しました。フォークしたリポジトリやブランチの数が多い場合でも目的のブランチを簡単に選択できるようになります。

f:id:takezoe:20170428124317p:plain

組み込みH2データベース利用時に警告

GitBucketはデフォルトでは組み込みモードのH2データベースを使用します。しかし、組み込みモードのH2はGitBucketと同じVM内で動作するため、GitBucketがクラッシュした場合にH2のデータが破損してしまう可能性があります。このためGitBucketを重要な用途に使用する場合はMySQLもしくはPostgreSQLの使用を推奨しています。

今回のバージョンから、GitBucketが組み込みH2で動作している場合は以下のように管理画面に警告メッセージを表示するようにしました。データが消えたら困るという場合はリンク先のインストラクションに従ってMySQLもしくはPostgreSQLへの移行を行っていただければと思います。

f:id:takezoe:20170428124820p:plain

今回のバージョンではこの他にも様々な改善やバグフィックスを行っています。詳細についてはIssueの一覧をご覧ください。