Ruby勉強してるけど、やっぱ流行りのAI導入したいので、その辺調べました。
背景
Ruby on RailsでWEBアプリの作り方をいろんな方から教わった。
Pythonで機械学習もいろんな方から教わった。
そうなると当然、機械学習を利用したWEBアプリを作りたくなる。
でも言語が異なる2つのシステムをどうやって連携するのか?
素人の私が思いついた方法は、
Pythonで作った機械学習エンジンをAPI化させ、Ruby on RailsのWEBアプリからJSON経由で呼び出す
という結論に至った。
(多分他にいろいろ方法はあるんだろうが、私は現状これしか実現できない。。)
その学びの過程をこの記事に書いていこうと思う。
全体の流れ
少々長くなりそうなので、全体の流れを先に紹介です。
- 開発環境と事前準備
- Flaskを使ってみる
- FlaskでAPIを作ってみる
- APIなので認証機能をつけてみる
- 簡単な学習モデルを作る
- 学習モデルをAPIに乗せてみる
- HerokuでAPIを公開
- Ruby on Railsのシステムから呼び出す
ながいなぁ。
0. 私の状態
私の状態としては、
- 一応、Ruby on Railsで簡単なWEBアプリは公開できる
- Pythonをつかって、機械学習の簡単なチュートリアルくらいならなんとかなる
- APIとか認証とかサーバとかそういうのは、敬遠しがち
です。
なので、私と似ている方は参考になるかもしれません。
1. 開発環境と事前準備
事前準備としては、
- Herokuの登録 https://jp.heroku.com/
- VirtualBoxのインストール http://www.oracle.com/technetwork/server-storage/virtualbox/downloads/index.html?ssSourceSiteId=otnjp
- Vagrantのインストール https://www.vagrantup.com/
- Windowsな方はGitBashのインストール https://gitforwindows.org/
- PythonとRuby on Railsの開発環境
かなと思います。
一応開発環境を一致させるために、Vagranfileを載せておきます。
(Python開発環境のVagrantfile
)
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/xenial64"
config.vm.network :"forwarded_port", guest: 5000, host: 5000
config.vm.synced_folder "~/python_vagrant/workspace", "/home/ubuntu/workspace", :create => true, mount_options: ['dmode=777','fmode=755']
config.vm.provision "shell", privileged: false, inline: <<-SHELL
sudo apt-get -y upgrade
sudo apt-get -y update
# install essentials
sudo apt-get install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev libpng-dev
# get pyenv and set path
git clone https://github.com/yyuu/pyenv.git ~/.pyenv
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.profile
echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.profile
echo 'eval "$(pyenv init -)"' >> ~/.profile
. /home/ubuntu/.profile
# install anaconda
pyenv install anaconda3-5.0.1
pyenv rehash
pyenv global anaconda3-5.0.1
# install Heroku CLI
curl https://cli-assets.heroku.com/install-ubuntu.sh | sh
# pip install
pip install --upgrade pip
pip install flask-httpauth
pip install gunicorn
SHELL
end
(Ruby開発環境のVagrantfile
)
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/xenial64"
config.vm.network :"forwarded_port", guest: 3000, host: 3000
config.vm.synced_folder "~/workspace", "/home/ubuntu/workspace", :create => true, mount_options: ['dmode=777','fmode=755']
config.vm.provision "shell", privileged: false, inline: <<-SHELL
sudo apt-get -y upgrade
sudo apt-get -y update
# install essentials
sudo apt-get install git curl g++ make vim nodejs libreadline-dev libssl-dev zlib1g-dev imagemagick libmagickcore-dev libmagickwand-dev -y
sudo apt-get remove ruby -y
# get rbenv and set path
git clone git://github.com/rbenv/rbenv.git /home/ubuntu/.rbenv
echo 'export PATH="/home/ubuntu/.rbenv/bin:$PATH"' >> ~/.profile
echo 'eval "$(rbenv init -)"' >> ~/.profile
. /home/ubuntu/.profile
# install ruby-build
mkdir -p ~/.rbenv/plugins
git clone git://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
# install ruby
rbenv install 2.3.0
rbenv global 2.3.0
rbenv rehash
sudo apt-get install ruby-railties -y
# install bundle
gem install bundler --no-document
# install postgresql
sudo apt-get install postgresql postgresql-contrib python-psycopg2 libpq-dev -y
SHELL
end
2. Flaskを使ってみる
いろいろ調べているとどうやらFlaskというPythonのフレームワークで簡単にAPIを作ることができるらしい。
なのでPython開発環境でウェブアプリケーションフレームワーク Flask を使ってみるを参考にFlaskで遊んでみた。
とても分かりやすい記事で、スムーズに進めることができた。
Ruby on Railsを知っている人がFlaskで抑えるべき点は以下のような部分です。
-
@app.route()
でルーティングとHTTPメソッドを指定 -
@app.route()
直下にRailsのコントローラ的なものが続く -
return render_template()
とredirect()
で画面遷移をコントロール
これくらい知っておけば十分かなと思います。
3. FlaskでAPIを作ってみる
では目的であるAPIをFlaskで作っていきます。
以下の動画を参考にしました。
https://www.youtube.com/watch?v=CjYKrbq8BCw
https://www.youtube.com/watch?v=qH--M56OsUg
https://www.youtube.com/watch?v=2gunLuqHvc8
https://www.youtube.com/watch?v=I64c1L_Zl2Y
この動画の通りやれば、すんなりAPIが構築できますが、1点のみ注意が必要です。
動画内では最下部が以下のようになっていますが、
# 省略
if __name__ == '__main__':
app.run(debug = True, port = 8080)
以下のように修正してください。
どのIPからもcurlでリクエストを送れるようにしておくためです。
# 省略
if __name__ == '__main__':
app.debug = True
app.run(host = '0.0.0.0')
最終的に完成したコードを下記に載せます。
from flask import Flask, jsonify, request
app = Flask(__name__)
languages = [{'name' : 'java'}, {'name' : 'php'}, {'name' : 'ruby'}]
@app.route("/", methods = ['GET'])
def test():
return jsonify({'message' : 'it is works.'})
@app.route("/lang", methods = ['GET'])
def returnAll():
return jsonify({'langages' : languages})
@app.route("/lang/<string:name>", methods = ['GET'])
def returnOne(name):
langs = [language for language in languages if language['name'] == name]
return jsonify({'langages' : langs[0]})
@app.route("/lang", methods = ['POST'])
def addOne():
language = {'name' : request.json['name']}
languages.append(language)
return jsonify({'langages' : languages})
@app.route("/lang/<string:name>", methods = ['PUT'])
def editOne(name):
langs = [language for language in languages if language['name'] == name]
langs[0]['name'] = request.json['name']
return jsonify({'langages' : langs[0]})
@app.route("/lang/<string:name>", methods = ['DELETE'])
def removeOne(name):
langs = [language for language in languages if language['name'] == name]
languages.remove(langs[0])
return jsonify({'langages' : languages})
if __name__ == '__main__':
app.debug = True
app.run(host = '0.0.0.0')
4. APIなので認証機能をつけてみる
APIのサービスというと認証やTokenで制限されているのがほとんどです。
なので、認証機能をつけたくなります。
一番簡単そうなBasic認証をつけてみます。
Flask-HTTPAuthを参考にしました。
最終的には以下のようなコードになります。
from flask import Flask, jsonify, request
from flask_httpauth import HTTPBasicAuth # ←追記
app = Flask(__name__)
auth = HTTPBasicAuth() # ←追記
users = { # ←追記
"john": "hello", # ←追記
"susan": "bye" # ←追記
} # ←追記
@auth.get_password # ←追記
def get_pw(username): # ←追記
if username in users: # ←追記
return users.get(username) # ←追記
return None # ←追記
languages = [{'name' : 'java'}, {'name' : 'php'}, {'name' : 'ruby'}]
@app.route("/", methods = ['GET'])
@auth.login_required # ←追記
def test():
return jsonify({'message' : "Hello, %s!" % auth.username()})
@app.route("/lang", methods = ['GET'])
@auth.login_required # ←追記
def returnAll():
return jsonify({'langages' : languages})
@app.route("/lang/<string:name>", methods = ['GET'])
@auth.login_required # ←追記
def returnOne(name):
langs = [language for language in languages if language['name'] == name]
return jsonify({'langages' : langs[0]})
@app.route("/lang", methods = ['POST'])
@auth.login_required # ←追記
def addOne():
language = {'name' : request.json['name']}
languages.append(language)
return jsonify({'langages' : languages})
@app.route("/lang/<string:name>", methods = ['PUT'])
@auth.login_required # ←追記
def editOne(name):
langs = [language for language in languages if language['name'] == name]
langs[0]['name'] = request.json['name']
return jsonify({'langages' : langs[0]})
@app.route("/lang/<string:name>", methods = ['DELETE'])
@auth.login_required # ←追記
def removeOne(name):
langs = [language for language in languages if language['name'] == name]
languages.remove(langs[0])
return jsonify({'langages' : languages})
if __name__ == '__main__':
app.debug = True
app.run(host = '0.0.0.0')
すると、以下のcurlコマンドではレスポンスが返りますが、
$ curl --basic -u john:hello -X GET http://localhost:5000/
以下のcurlコマンドではレスポンスがエラーが返ります。
$ curl -X GET http://localhost:5000/
これでAPIへのBasic認証がつきました。
5. 簡単な学習モデルを作る
では次に、機械学習のモデルを作成しましょう。
機械学習で作成したモデルをREST APIとしてdeployする[python]を参考にしました。
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
import pandas as pd
iris = datasets.load_iris()
X = pd.DataFrame(iris.data, columns=["sepal_length", "sepal_width", "petal_length", "petal_width"])
y = iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=0)
clf = LogisticRegression()
clf.fit(X_train, y_train)
from sklearn.externals import joblib
joblib.dump(clf, 'iris_logreg.pkl')
joblib.dump(["sepal_length", "sepal_width", "petal_length", "petal_width"], 'iris_logreg_cols.pkl')
send_data = [{'petal_length': 4.5,
'petal_width': 1.5,
'sepal_length': 6.0,
'sepal_width': 2.8999999999999999}]
print(clf.predict(send_data))
python learning.py
を実行した時の出力は以下になります。
array([1])
learning.py
の最終行のjoblib.dump(["sepal_length", "sepal_width", "petal_length", "petal_width"], 'iris_logreg_cols.pkl')
はカラムを固定するための措置です。
APIを通じてJSONやpythonやRubyに変換しながらでデータをやり取りする際、順番が入れ替わらないように、カラム順を指定しておくのです。
またpython learning.py
を実行した時にiris_logreg.pkl
とiris_logreg_cols.pkl
が生成されます。
iris_logreg.pkl
がモデルです。
iris_logreg_cols.pkl
がカラム順指定ファイルです。
6. 学習モデルをAPIに乗せてみる
つぎは作成したモデルをAPIに乗せます!
引き続き機械学習で作成したモデルをREST APIとしてdeployする[python]を参考にします。
同じディレクトリ内に以下のファイルを作成します。
from sklearn.externals import joblib
from flask import Flask, jsonify, request
import pandas as pd
from sklearn import datasets
from flask_httpauth import HTTPBasicAuth # ←追記
app = Flask(__name__)
auth = HTTPBasicAuth()
users = {
"john": "hello",
"susan": "bye"
}
@auth.get_password
def get_pw(username):
if username in users:
return users.get(username)
return None
@app.route('/predict/<string:clf_file>', methods=['POST'])
@auth.login_required
def predict(clf_file):
clf = joblib.load("{}.pkl".format(clf_file))
data = request.json
query = pd.DataFrame(data)
cols = joblib.load("{}_cols.pkl".format(clf_file))
query = query[cols]
prediction = clf.predict(query)
return jsonify({'prediction':prediction.tolist()})
if __name__ == '__main__':
app.debug = True
app.run(host = '0.0.0.0')
main.py
の以下の部分は、iris_logreg_cols.pkl
で固定したカラム順にデータを成形しなおす処理に当たります。
# 省略
query = pd.DataFrame(data)
cols = joblib.load("{}_cols.pkl".format(clf_file))
query = query[cols]
# 省略
python main.py
を実行したのち、以下のコマンドを実行します。
$ curl --basic -u john:hello -X POST -H 'Content-Type:application/json' -d "{'petal_length': 4.5, 'petal_width': 1.5, 'sepal_length': 6.0, 'sepal_width': 2.8999999999999999}" http://localhost:5000/predict/iris_logreg
すると、以下のレスポンスが返ります。
{
"prediction": [
1
]
}
learning.py
の出力だったarray([1])
と一致していますね。
これが学習モデルのAPI化です。
7. HerokuでAPIを公開
ローカルでばっかりやってても面白くないですね。
では先ほどの学習APIをHerokuで公開します。
さらに以下のようにProcfile
とrequirements.txt
を作成します。
web: gunicorn main:app --log-file -
Procfile
はmain.py
をHeroku上で起動させる命令を行っています。
Flask==0.12.2
gunicorn==19.8.1
Flask-HTTPAuth==3.2.3
scikit-learn==0.18.1
pandas==0.19.2
requirements.txt
はHeroku上に追加でインストールさせるライブラリを指定しています。
これで最終的に同じディレクトリにlearning.py
とiris_logreg.pkl
とiris_logreg_cols.pkl
とProcfile
とrequirements.txt
がある状態になっているかと思います。
これらをまとめてHerokuにpushします。
すると以下のcurlコマンドで、
$ curl --basic -u john:hello -X POST -H 'Content-Type:application/json' -d "{'petal_length': 4.5, 'petal_width': 1.5, 'sepal_length': 6.0, 'sepal_width': 2.8999999999999999}" https://xxx.herokuapp.com/predict/iris_logreg
以下のレスポンスが返ってきます。
{
"prediction": [
1
]
}
これで学習APIの公開が完了です。
8. Ruby on Railsのシステムから呼び出す
最後は別に独立したRuby on Railsのシステムから公開されている学習APIにリクエストを送り、レスポンスを受け取るようにします。
$ curl --basic -u john:hello -X POST -H 'Content-Type:application/json' -d "{'petal_length': 4.5, 'petal_width': 1.5, 'sepal_length': 6.0, 'sepal_width': 2.8999999999999999}" https://xxx.herokuapp.com/predict/iris_logreg
上記のcurlコマンドはRubyでどのように表現するのでしょうか。
そんな時に役立つのがcurl-to-rubyです。
このサイトはcurlコマンドをRubyコードに変換してくれます。
では早速上記curlコマンドをRubyに変換してみましょう。
require 'net/http'
require 'uri'
uri = URI.parse("https://xxx.herokuapp.com/predict/iris_logreg")
request = Net::HTTP::Post.new(uri)
request.basic_auth("john", "hello")
request.content_type = "application/json"
request.body = "{'petal_length': 4.5, 'petal_width': 1.5, 'sepal_length': 6.0, 'sepal_width': 2.8999999999999999}"
req_options = {
use_ssl: uri.scheme == "https",
}
response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
http.request(request)
end
# response.code
# response.body
このresponse.body
で得られる値が、{"prediction": 1}
となります。
上記コードを既存のRuby on Railsソースに落とし込めば、Ruby on RailsとPython製学習APIの連携が完了となります。
最後に
API最強やん!って思いました。
あと、APIの認証は本当はTokenで行いたかったのですが、難しくてBasic認証にしました。
ひょっとして、機械学習より認証系や決済系の方が難しいかも!