機械学習のお勉強(Webアプリケーション)
教科書
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
参考:
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がつかえず...
FlaskでのWebアプリ開発
参考:
Quickstart — Flask Documentation (0.12)
[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
実行手順
python app.py
ブラウザでhttp://127.0.0.1:5000/にアクセス
「Hi, this is my first Flask web app! 」とでていることを確認
WTFormsでデータ収集
Flask にフォームがないから WTForms 使ってみた - present
下記のような名前を入力して挨拶をするものをつくる
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 : CSS(cascading 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)
CSS(cascading style sheets)ファイル
HTMLドキュメントのルック&フィール(コンピュータの操作画面の見た目や操作感のこと)が指定できる
映画レビュー分類器をwebアプリとして実装
下記のようなレビューを書いて、その予測結果をフィードバックし、感謝を伝えるツールを作る
http://raschkas.pythonanywhere.com/results
- ディレクトリ構成
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
アカウントの登録
Add a new web app
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)