Help us understand the problem. What is going on with this article?

Ruby on Railsで作ったWEBアプリとPythonで作った機械学習モデルを連携するには?

おつかれさまです、かきうち(@kakistuter)です。
Ruby勉強してるけど、やっぱ流行りのAI導入したいので、その辺調べました。

背景

Ruby on RailsでWEBアプリの作り方をいろんな方から教わった。
Pythonで機械学習もいろんな方から教わった。
そうなると当然、機械学習を利用したWEBアプリを作りたくなる。

でも言語が異なる2つのシステムをどうやって連携するのか?
素人の私が思いついた方法は、

Pythonで作った機械学習エンジンをAPI化させ、Ruby on RailsのWEBアプリからJSON経由で呼び出す

という結論に至った。
(多分他にいろいろ方法はあるんだろうが、私は現状これしか実現できない。。)

その学びの過程をこの記事に書いていこうと思う。

全体の流れ

少々長くなりそうなので、全体の流れを先に紹介です。

  1. 開発環境と事前準備
  2. Flaskを使ってみる
  3. FlaskでAPIを作ってみる
  4. APIなので認証機能をつけてみる
  5. 簡単な学習モデルを作る
  6. 学習モデルをAPIに乗せてみる
  7. HerokuでAPIを公開
  8. Ruby on Railsのシステムから呼び出す

ながいなぁ。

0. 私の状態

私の状態としては、

  • 一応、Ruby on Railsで簡単なWEBアプリは公開できる
  • Pythonをつかって、機械学習の簡単なチュートリアルくらいならなんとかなる
  • APIとか認証とかサーバとかそういうのは、敬遠しがち

です。
なので、私と似ている方は参考になるかもしれません。

1. 開発環境と事前準備

事前準備としては、

かなと思います。
一応開発環境を一致させるために、Vagranfileを載せておきます。

(Python開発環境のVagrantfile)

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)

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を作ってみる

では目的であるAPIFlaskで作っていきます。
以下の動画を参考にしました。

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')

最終的に完成したコードを下記に載せます。

crud.py
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を参考にしました。

最終的には以下のようなコードになります。

crud.py
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]を参考にしました。

learning.py
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を通じてJSONpythonRubyに変換しながらでデータをやり取りする際、順番が入れ替わらないように、カラム順を指定しておくのです。

またpython learning.pyを実行した時にiris_logreg.pkliris_logreg_cols.pklが生成されます。
iris_logreg.pklがモデルです。
iris_logreg_cols.pklがカラム順指定ファイルです。

6. 学習モデルをAPIに乗せてみる

つぎは作成したモデルをAPIに乗せます!
引き続き機械学習で作成したモデルをREST APIとしてdeployする[python]を参考にします。
同じディレクトリ内に以下のファイルを作成します。

main.py
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で固定したカラム順にデータを成形しなおす処理に当たります。

main.py
# 省略
    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で公開します。

さらに以下のようにProcfilerequirements.txtを作成します。

web: gunicorn main:app --log-file -

Procfilemain.pyをHeroku上で起動させる命令を行っています。

requirements.txt
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.pyiris_logreg.pkliris_logreg_cols.pklProcfilerequirements.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 RailsPython製学習APIの連携が完了となります。

最後に

API最強やん!って思いました。
あと、APIの認証は本当はTokenで行いたかったのですが、難しくてBasic認証にしました。
ひょっとして、機械学習より認証系や決済系の方が難しいかも!

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away