空飛ぶロボットのつくりかた

ロボットをつくるために必要な技術をまとめます。ロボットの未来についても考えたりします。

機械学習のお勉強(Webアプリケーション)

教科書

GitHub - rasbt/python-machine-learning-book: The "Python Machine Learning (1st edition)" book code repository and info resource

robonchu.hatenablog.com の内容を実行している前提

例:http://raschkas.pythonanywhere.com/results

学習済みのscikit-learn推定器をシリアライズする

  • モデルの永続化のひとつの方法はpickleを使う

  • Numpyが含まれている場合はjoblibのほうが効率的らしい

import pickle
import os

dest = os.path.join('movieclassifier', 'pkl_objects')
if not os.path.exists(dest):
    os.makedirs(dest)

pickle.dump(stop, open(os.path.join(dest, 'stopwords.pkl'), 'wb'), protocol=2)  # 4)   
pickle.dump(clf, open(os.path.join(dest, 'classifier.pkl'), 'wb'), protocol=2)  # 4)

シリアライズ

import pickle
import re
import os
from vectorizer import vect

clf = pickle.load(open(os.path.join('pkl_objects', 'classifier.pkl'), 'rb'))

import numpy as np
label = {0:'negative', 1:'positive'}

example = ['I love this movie']
X = vect.transform(example)
print('Prediction: %s\nProbability: %.2f%%' %\
      (label[clf.predict(X)[0]], clf.predict_proba(X).max()*100))

上記スクリプトで使っている関数

from sklearn.feature_extraction.text import HashingVectorizer
import re
import os
import pickle

cur_dir = os.path.dirname(__file__)
stop = pickle.load(open(
                os.path.join(cur_dir, 
                'pkl_objects', 
                'stopwords.pkl'), 'rb'))

def tokenizer(text):
    text = re.sub('<[^>]*>', '', text)
    emoticons = re.findall('(?::|;|=)(?:-)?(?:\)|\(|D|P)',
                           text.lower())
    text = re.sub('[\W]+', ' ', text.lower()) \
                   + ' '.join(emoticons).replace('-', '')
    tokenized = [w for w in text.split() if w not in stop]
    return tokenized

vect = HashingVectorizer(decode_error='ignore',
                         n_features=2**21,
                         preprocessor=None,
                         tokenizer=tokenizer)

SQLiteデータベースをセットアップ

Webアプリケーションの予測に対するユーザーからのフィードバックを収集する

SQLite

f:id:robonchu:20171126101921p:plain

参考:

SQLite Home Page

SQLite入門

batteries included

pythonのbatteries includedの考え方: 標準ライブラリだけでも様々なことができる

参考: What “Batteries Included” Means | Musings of an Anonymous Geek

データベースの作成

batteries includedの考え方からsqlite3というSQLiteを操作できる標準APIがある

11.13. sqlite3 — SQLite データベースに対する DB-API 2.0 インタフェース — Python 2.7.14 ドキュメント

  • データベースの作成
import sqlite3
import os

if os.path.exists('reviews.sqlite'):
    os.remove('reviews.sqlite')

conn = sqlite3.connect('reviews.sqlite')
c = conn.cursor()
c.execute('CREATE TABLE review_db (review TEXT, sentiment INTEGER, date TEXT)')

example1 = 'I love this movie'
c.execute("INSERT INTO review_db (review, sentiment, date) VALUES (?, ?, DATETIME('now'))", (example1, 1))

example2 = 'I disliked this movie'
c.execute("INSERT INTO review_db (review, sentiment, date) VALUES (?, ?, DATETIME('now'))", (example2, 0))

conn.commit()
conn.close()
  • データベースの内容確認
conn = sqlite3.connect('reviews.sqlite')
c = conn.cursor()

c.execute("SELECT * FROM review_db WHERE date BETWEEN '2015-01-01 10:10:10' AND DATETIME('now')")
results = c.fetchall()

conn.close()

print(results)

結果

[(u'I love this movie', 1, u'2017-11-26 01:06:38'), (u'I disliked this movie', 0, u'2017-11-26 01:06:38')]

Firefoxのバージョン 57.0ではSQLite Managerのpluginがつかえず...

f:id:robonchu:20171126101144p:plain

FlaskでのWebアプリ開発

f:id:robonchu:20171126101934p:plain

参考:

Quickstart — Flask Documentation (0.12)

Flaskの簡単な使い方 - Qiita

[Python] 軽量WebフレームワークのFlaskに入門(準備、起動、HTML、静的ファイル、GET、POSTなど) - YoheiM .NET

なれるための単純な開発

1st_flask_app_1/
├── app.py  <- メインのコード
└── templates  
    └── first_app.html  <- webブラウザでレンダリングする静的なHTMLファイル

app.py

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('first_app.html')

if __name__ == '__main__':
    app.run(debug=True)

first_app.html

<!doctype html>
<html>
  <head>
    <title>First app</title>
  </head>
  <body>

  <div>
    Hi, this is my first Flask web app!
  </div>

  </body>
</html>

HTMLのわかりやすい参考: HTML | MDN

実行手順

  1. python app.py

  2. ブラウザでhttp://127.0.0.1:5000/にアクセス

  3. 「Hi, this is my first Flask web app! 」とでていることを確認

WTFormsでデータ収集

参考: WTForms Documentation

Flask にフォームがないから WTForms 使ってみた - present

WTFormsを学ぶ その1 - 学んだことをメモする日記

WTFormsを学ぶ その2 - 学んだことをメモする日記

WTFormsを学ぶ その3 - 学んだことをメモする日記

下記のような名前を入力して挨拶をするものをつくる

f:id:robonchu:20171126110945p:plain

f:id:robonchu:20171126110949p:plain

1st_flask_app_2/
├── app.py
├── static
│   └── style.css
└── templates
    ├── _formhelpers.html
    ├── first_app.html
    └── hello.html

app.py

from flask import Flask, render_template, request
from wtforms import Form, TextAreaField, validators

app = Flask(__name__)

class HelloForm(Form):
    sayhello = TextAreaField('',[validators.DataRequired()])

@app.route('/')
def index():
    form = HelloForm(request.form)
    return render_template('first_app.html', form=form)

@app.route('/hello', methods=['POST'])
def hello():
    form = HelloForm(request.form)
    if request.method == 'POST' and form.validate():
        name = request.form['sayhello']
        return render_template('hello.html', name=name)
    return render_template('first_app.html', form=form)

if __name__ == '__main__':
    app.run(debug=True)

_formhelpers.html

Jinja2というテンプレートエンジンを用いている

{% macro render_field(field) %}
  <dt>{{ field.label }}
  <dd>{{ field(**kwargs)|safe }}
  {% if field.errors %}
    <ul class=errors>
    {% for error in field.errors %}
      <li>{{ error }}</li>
    {% endfor %}
    </ul>
  {% endif %}
  </dd>
  </dt>
{% endmacro %}

style.css : CSScascading style sheets)ファイル

body {
    font-size: 2em;
}

first_app.html

<!doctype html>
<html>
  <head>
    <title>First app</title>
   <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
  </head>
  <body>

{% from "_formhelpers.html" import render_field %}

<div>What's your name?</div>
<form method=post action="/hello">

  <dl>
      {{ render_field(form.sayhello) }}
  </dl>

  <input type=submit value='Say Hello' name='submit_btn'>

</form>

  </body>
</html>

hello.html

<!doctype html>
<html>
  <head>
    <title>First app</title>
   <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
  </head>
  <body>

<div>Hello {{ name }}</div>


  </body>
</html>

Jinja2

テンプレートエンジン. pythonの結果からHTMLを作成したいときなどに便利.

Welcome to Jinja2 — Jinja2 Documentation (2.11)

jinja2 — it-note 1.0 ドキュメント

CSScascading style sheets)ファイル

HTMLドキュメントのルック&フィール(コンピュータの操作画面の見た目や操作感のこと)が指定できる

映画レビュー分類器をwebアプリとして実装

下記のようなレビューを書いて、その予測結果をフィードバックし、感謝を伝えるツールを作る

http://raschkas.pythonanywhere.com/results

f:id:robonchu:20171126110953p:plainf:id:robonchu:20171126111000p:plainf:id:robonchu:20171126111153p:plain

movieclassifier_with_update/
├── app.py
├── pkl_objects
│   ├── classifier.pkl
│   └── stopwords.pkl
├── reviews.sqlite
├── static
│   └── style.css
├── templates
│   ├── _formhelpers.html
│   ├── results.html
│   ├── reviewform.html
│   └── thanks.html
├── update.py
└── vectorizer.py

app.py

from flask import Flask, render_template, request
from wtforms import Form, TextAreaField, validators
import pickle
import sqlite3
import os
import numpy as np

# import HashingVectorizer from local dir
from vectorizer import vect

app = Flask(__name__)

######## Preparing the Classifier
cur_dir = os.path.dirname(__file__)
clf = pickle.load(open(os.path.join(cur_dir,
                 'pkl_objects',
                 'classifier.pkl'), 'rb'))
db = os.path.join(cur_dir, 'reviews.sqlite')

def classify(document):
    label = {0: 'negative', 1: 'positive'}
    X = vect.transform([document])
    y = clf.predict(X)[0]
    proba = np.max(clf.predict_proba(X))
    return label[y], proba

def train(document, y):
    X = vect.transform([document])
    clf.partial_fit(X, [y])

def sqlite_entry(path, document, y):
    conn = sqlite3.connect(path)
    c = conn.cursor()
    c.execute("INSERT INTO review_db (review, sentiment, date)"\
    " VALUES (?, ?, DATETIME('now'))", (document, y))
    conn.commit()
    conn.close()

######## Flask
class ReviewForm(Form):
    moviereview = TextAreaField('',
                                [validators.DataRequired(),
                                validators.length(min=15)])

@app.route('/')
def index():
    form = ReviewForm(request.form)
    return render_template('reviewform.html', form=form)

@app.route('/results', methods=['POST'])
def results():
    form = ReviewForm(request.form)
    if request.method == 'POST' and form.validate():
        review = request.form['moviereview']
        y, proba = classify(review)
        return render_template('results.html',
                                content=review,
                                prediction=y,
                                probability=round(proba*100, 2))
    return render_template('reviewform.html', form=form)

@app.route('/thanks', methods=['POST'])
def feedback():
    feedback = request.form['feedback_button']
    review = request.form['review']
    prediction = request.form['prediction']

    inv_label = {'negative': 0, 'positive': 1}
    y = inv_label[prediction]
    if feedback == 'Incorrect':
        y = int(not(y))
    train(review, y)
    sqlite_entry(db, review, y)
    return render_template('thanks.html')

if __name__ == '__main__':
    app.run(debug=True)

reviewform.html

<!doctype html>
<html>
  <head>
    <title>Movie Classification</title>
   <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
  </head>
  <body>

<h2>Please enter your movie review:</h2>

{% from "_formhelpers.html" import render_field %}

<form method=post action="/results">
  <dl>
    {{ render_field(form.moviereview, cols='30', rows='10') }}
  </dl>
  <div>
      <input type=submit value='Submit review' name='submit_btn'>
  </div>
</form>

  </body>
</html>

results.html

<!doctype html>
<html>
  <head>
    <title>Movie Classification</title>
   <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
  </head>
  <body>

<h3>Your movie review:</h3>
<div>{{ content }}</div>

<h3>Prediction:</h3>
<div>This movie review is <strong>{{ prediction }}</strong>
     (probability: {{ probability }}%).</div>

<div id='button'>
      <form action="/thanks" method="post">
        <input type=submit value='Correct' name='feedback_button'>
        <input type=submit value='Incorrect' name='feedback_button'>
        <input type=hidden value='{{ prediction }}' name='prediction'>
        <input type=hidden value='{{ content }}' name='review'>
      </form>
</div>

<div id='button'>
      <form action="/">
        <input type=submit value='Submit another review'>
      </form>
</div>

  </body>
</html>

style.css

body{
    width:600px;
}

.button{
    padding-top: 20px;
}

thanks.html

<!doctype html>
<html>
  <head>
    <title>Movie Classification</title>
   <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
  </head>
  <body>

<h3>Thank you for your feedback!</h3>

<div id='button'>
      <form action="/">
        <input type=submit value='Submit another review'>
      </form>
</div>

  </body>
</html>

実行

python app.py

Webアプリケーションをパブ陸Webサーバーにデプロイ

これまでのアプリのローカルでのテストが完了すればパブリックでデプロイする

PythonAnywhere

Host, run, and code Python in the cloud: PythonAnywhere

  1. アカウントの登録

  2. Add a new web app

  3. File Upload or do something

そうすれば<ユーザー名>.pythonanywhere.comでアクセスできる

参考:

Flask Tutorial (part 5) - deploying to PythonAnywhere - YouTube

Simple Flask Hosting: PythonAnywhere

映画レビュー分類器の更新

サーバーがクラッシュした際にデータを消えないようにするには更新されるたびにclfオブジェクトをシリアライズすることが考えられる。

しかし、ユーザーの数が増えるに従い、計算効率が悪くなる、また、同時に更新した際にはファイルが壊れるかもしれない。

なので、もうひとつの方法として、 SQLiteデータベースに収集されたフィードバックデータを使った予測モデルの更新 がある。

実装

update.py *

import pickle
import sqlite3
import numpy as np
import os

# import HashingVectorizer from local dir
from vectorizer import vect

def update_model(db_path, model, batch_size=10000):

    conn = sqlite3.connect(db_path)
    c = conn.cursor()
    c.execute('SELECT * from review_db')

    results = c.fetchmany(batch_size)
    while results:
        data = np.array(results)
        X = data[:, 0]
        y = data[:, 1].astype(int)

        classes = np.array([0, 1])
        X_train = vect.transform(X)
        model.partial_fit(X_train, y, classes=classes)
        results = c.fetchmany(batch_size)

    conn.close()
    return model

cur_dir = os.path.dirname(__file__)

clf = pickle.load(open(os.path.join(cur_dir,
                  'pkl_objects',
                  'classifier.pkl'), 'rb'))
db = os.path.join(cur_dir, 'reviews.sqlite')

clf = update_model(db_path=db, model=clf, batch_size=10000)

# Uncomment the following lines if you are sure that
# you want to update your classifier.pkl file
# permanently.

# pickle.dump(clf, open(os.path.join(cur_dir,
#             'pkl_objects', 'classifier.pkl'), 'wb')
#             , protocol=4)

これをapp.pyで呼び出すようにする

以下の二箇所をapp.pyに追加

from update import update_model
if __name__ == '__main__' :
    clf = update_model(dp_path=db, model=clf, batch_size=10000)