こんにちは。開発エンジニアの amdaba_sk(ペンネーム未定)です。
前回は「機械学習をコモディティ化する AutoML ツールの評価」、だいぶ間が空きましたが前々回は「機械学習のライブラリ・プラットフォームをいくつか試した所感まとめ」と、続けて機械学習をテーマとした記事を書きました。
これらの記事では機械学習モデルを作るまでのことしか言及していませんが、機械学習モデルは作ってそれで終わりのものでもありません。使ってなんぼのものなんです。かみせんプロジェクトとしての調査範囲からは外れますが、せっかくモデルを作ったならそれを使ったアプリも簡単なものでいいので作ってみたい。そう思うのは開発者として自然な感情ではないでしょうか。
というわけで今回は、「機械学習モデルを組み込んだ Web アプリを Python 初心者が作ってみた」という個人的な興味からやってみた系記事でございます。
なお後に述べるようにアプリの実装言語は Python を採用しますが、私はかみせんプロジェクトで初めて Python を触ったばかりの初心者です。Python らしくないコードを書いている可能性もありますので、その点ご了承ください。
また「かみせんってなんやねん」と思われた方は ↓ のリンクからかみせんカテゴリの記事をご覧ください。
もくじ
どんなアプリを作る?
なるべくシンプルなものがよいですね。あまり時間はかけたくないので。
過去 2 記事で主に取り扱った機械学習のタスクは、分類でした。ということは、ユーザーが送信したデータを機械学習モデルで分類し、その結果を提示するだけというのが簡単で良さそうです。また画面を作るのは面倒なので JSON API ということにしましょう。
分類するデータの形式は、またもや過去 2 記事を見ると、ひとつはテキスト、ひとつはテーブルを使っています。テーブルデータだと複数のフィールドを埋めてやらないといけないので、作った後お試しで使ってみる時にめんどくさそうですね。テキストにしましょう。
結局「機械学習のライブラリ・プラットフォームをいくつか試した所感まとめ」の冒頭で構想したものと似たようなアプリになりました。
どんな分類をするのか?
テキストを何のカテゴリに分類してくれるのかもここで考えておきましょう。要するにどんな学習データを使うのかという話ですが、今回は livedoor ニュースコーパスを使わせていただくことにします。
livedoor ニュースコーパスは「livedoor ニュース」のうち、クリエイティブ・コモンズライセンスが適用されるニュース記事を集めたもので、株式会社ロンウイットさんによって配布されています。ここからダウンロードすることができます。
ニュースコーパスには以下の 9 カテゴリのニュース記事が格納されています。
これを学習することで、与えられたテキストが 9 カテゴリのうちどれに当てはまりそうかを推測することが出来るでしょう。
どうやってアプリを作る?
機械学習フレームワークは何にする?
機械学習モデルを作る際のフレームワークはなんだかんだ言って scikit-learn が使いやすいです。今回の主目的は機械学習モデルを作る部分ではないので、ここにあまり力を入れません。凝ったことは考えず、scikit-learn を使うことにします。
実装言語と Web アプリのフレームワークは何にする?
機械学習モデルが Python のフレームワークで作られるとなると、それを使うアプリの方も実装言語は Python を使うのがやりやすいです。本記事の冒頭でも述べたように Python で Web アプリなんて作ったことはありませんが、まあ、簡単なものを試作するくらいなら何とかなるでしょう。
Python で Web アプリのフレームワークといえば、少し調べると以下の二つが代表的なようです。
今回の用途では Flask の方が手軽で良さそうです。が、ここではそのどちらでもなく、FastAPI を選択します。
Web アプリのフレームワークを調べていた際に「Python 製 Web フレームワークを Flask から FastAPI に変えた話」という記事を見つけました。それによれば、
しかし、どちらのフレームワークを使う場合でも下記のような機能を使おうとするとプラグインやサードパーティの助けを借りる必要があります。
- OpenAPI
- JSON Schema
- GraphQL
- WebSocket
- タイプヒントを使ったバリデーション
- 非同期処理
- CORS の設定
- リバースプロキシとの連携サポート
Django も Flask も近年登場したサーバサイドの技術や Python 3 の新機能に対するネイティブサポートがちょっと弱いです。
とのことなのです。Django も Flask についても私自身はあまり詳しく調査していませんが、この記事を信ずるのであればどちらを選んでもプラグインの選定作業が加わることになってあまり楽できそうにないです。特に JSON API を作ろうとしているため、やっぱり OpenAPI
や JSON Schema
は入れたいです。
また FastAPI のドキュメントを見ていると、なんだかこれで作れそうな気がしてきました。それにタイプヒントを使ったバリデーション
も、とっても好みです。
というわけで Web アプリのフレームワークは FastAPI を使うことにします。
プロジェクト構成はどんな感じにする?
今回の Web アプリのプロジェクト構成は下のようなものにしました。これは初めに完全に決めたというよりも、作りながら試して結果こうなったという感じです。
my_ml_app ├── Pipfile ├── Pipfile.lock ├── text ... 学習データ置き場 ├── models ... 機械学習モデル置き場 ├── tokenizer.py ... 学習・アプリ共通依存モジュール ├── training.py ... 学習スクリプト └── my_ml_app.py ... Web アプリ本体スクリプト
「ゼロから学ぶ Python」というオンライン学習サイトによれば、「The Hitchhiker’s Guide to Python」というサイトで解説されている推奨構成に従うのがよいらしいです。今回の構成はあまり推奨構成に則っていないかもしれませんが、参考にはさせていただきました。
「Cookiecutter」等を使うことでテンプレートからディレクトリ構成などひな型は作れるのですが、どのテンプレートがいいのかよくわからず結局上の構成は手で作っています。
機械学習モデルはどう作る?
機械学習モデルの作成スクリプトは training.py
です。
詳細な内容は今回の主眼ではないので省略しますが、Web アプリで使うために出来上がったモデルをシリアライズしてファイルに保存しておく必要があります1。
モデル作成の概略は以下の通りです。
学習の際データセットを分割し、一部を性能測定に使っています。結果は以下の通りでした。モデルの性能は今回重要ではないのですが、まあ、そこそこなモデルになっているのではないでしょうか。
precision recall f1-score support dokujo-tsushin 0.68 0.91 0.78 218 it-life-hack 0.92 0.89 0.90 218 kaden-channel 0.89 0.93 0.91 216 livedoor-homme 1.00 0.25 0.40 128 movie-enter 0.84 0.97 0.90 218 peachy 0.80 0.71 0.75 210 smax 0.86 1.00 0.93 217 sports-watch 0.89 1.00 0.94 225 topic-news 0.98 0.74 0.84 192 accuracy 0.85 1842 macro avg 0.87 0.82 0.82 1842 weighted avg 0.87 0.85 0.84 1842
Web アプリ本体はどう作る?
Web アプリ本体のスクリプトは my_ml_app.py
です。とりあえず全文を見てみましょう。
from enum import Enum from fastapi import FastAPI from pydantic import BaseModel, Field import joblib from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.naive_bayes import MultinomialNB class CategoryName(str, Enum): topic_news = 'topic-news' # トピックニュース sports_watch = 'sports-watch' # Sports Watch it_life_hack = 'it-life-hack' # ITライフハック kaden_channel = 'kaden-channel' # 家電チャンネル movie_enter = 'movie-enter' # MOVIE ENTER dokujo_tsushin = 'dokujo-tsushin' # 独女通信 smax = 'smax' # エスマックス livedoor_homme = 'livedoor-homme' # livedoor HOMME peachy = 'peachy' # Peachy class MyClassifier(): clf: MultinomialNB vec: TfidfVectorizer def __init__( self, classifier: MultinomialNB, vectrizer: TfidfVectorizer ): self.clf = classifier self.vec = vectrizer def classify(self, targetText: str): v = self.vec.transform([targetText]) return self.clf.predict(v)[0] class ClassifyRequest(BaseModel): text: str = Field(..., max_length=10000) app = FastAPI() my_classifier = MyClassifier( joblib.load('models/livedoor_tfidf_mnb.model'), joblib.load('models/livedoor_tfidf.model') ) @app.post('/classify', response_model=CategoryName) async def classify(req: ClassifyRequest): return my_classifier.classify(req.text)
実にシンプルですね。シンプル過ぎてif
もfor
も使う余地がありませんでした。
動かしてみる①
Python 3 が使える環境にプロジェクトをデプロイ2し、プロジェクトのディレクトリに移動。その後起動コマンドを実行します。
# pipenv run uvicorn my_ml_app:app --port 80 --host 0.0.0.0 --reload INFO: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit) INFO: Started reloader process [521] using statreload INFO: Started server process [528] INFO: Waiting for application startup. INFO: Application startup complete.
サーバーが起動しました!
FastAPI は SwaggerUI をホストしていますので、そこから API を動かしてみましょう。
「Try it out」で本記事の冒頭部分を分類してみます。
{ "text": "機械学習モデルを組み込んだ Web アプリを Python 初心者が作ってみた\nこんにちは。開発エンジニアの amdaba_sk(ペンネーム未定)です。\n前回は「機械学習をコモディティ化する AutoML ツールの評価」、だいぶ間が空きましたが前々回は「機械学習のライブラリ・プラットフォームをいくつか試した所感まとめ」と、続けて機械学習をテーマとした記事を書きました。\nこれらの記事では機械学習モデルを作るまでのことしか言及していませんが、機械学習モデルは作ってそれで終わりのものでもありません。使ってなんぼのものなんです。かみせんプロジェクトとしての調査範囲からは外れますが、せっかくモデルを作ったならそれを使ったアプリも簡単なものでいいので作ってみたい。そう思うのは開発者として自然な感情ではないでしょうか。\nというわけで今回は、「機械学習モデルを組み込んだ Web アプリを Python 初心者が作ってみた」という個人的な興味からやってみた系記事でございます。" }
これを送信すると、結果が以下のように返ってきます。
"it-life-hack"
どうやら本記事は「ITライフハック」に入ってそうな記事だそうです。
仕様をちょっと変えてみる
ところで、送信したテキストがどのカテゴリに相当するのかをひとつだけ返してくるだけでは、そっけなく感じますね。
実際出来上がったものを見ると、欲ができてます。以下のようにちょっと仕様を変えてみましょうか。
- どのカテゴリにどの程度の確率で分類されるのかのリストを返す
- その際、分類される確率の高い順にカテゴリを並べる
この仕様変更は幸い Web アプリ側だけで対応できます。my_ml_app.py
を以下のように修正しましょう。
@@ -1,4 +1,5 @@ from enum import Enum +from typing import List from fastapi import FastAPI from pydantic import BaseModel, Field @@ -30,17 +31,30 @@ def classify(self, targetText: str): v = self.vec.transform([targetText]) - return self.clf.predict(v)[0] + result_proba = self.clf.predict_proba(v)[0] # <1> + order = (-result_proba).argsort() # <2>, <3> + ordered_cats = self.clf.classes_[order] # <4> + ordered_probas = result_proba[order] # <4> + return [ + { + 'category': cat, + 'probability': proba + } for cat, proba in zip(ordered_cats, ordered_probas) # <5> + ] # <6> class ClassifyRequest(BaseModel): text: str = Field(..., max_length=10000) +class ClassifyResponse(BaseModel): + category: CategoryName + probability: float + app = FastAPI() my_classifier = MyClassifier( joblib.load('models/livedoor_tfidf_mnb.model'), joblib.load('models/livedoor_tfidf.model') ) -@app.post('/classify', response_model=CategoryName) +@app.post('/classify', response_model=List[ClassifyResponse]) async def classify(req: ClassifyRequest): return my_classifier.classify(req.text)
やることが増えたので少しコードも複雑になりました。ポイントを説明します。
- 推論実行のメソッドを
predict
からpredict_proba
に変更したことで、カテゴリ毎の確率を得ることが出来ます。並び順は分類器のclasses_
属性と一致しています。 predict_proba
から返された配列に対して負符号を付けることで、要素の正負をすべて反転しています。これは次のステップで降順ソートにするためです。- 要素の正負を反転した結果に対して
numpy.argsort
を行えば、値の大小に従ってソートした時の配列インデックスの変化を返してくれます。 - Python ではリストや配列に対して
[]
にインデックスのリストを入れると、各インデックスに対応する値を指定した順序で返してくれます。ここでは 3 で得たインデックスのリストを使って、カテゴリのリスト(self.clf.classes_
)とカテゴリ毎の確率(result_proba
)を並べ替えています。 zip
関数を使ってカテゴリと対応する確率をペアにしています。これで別々のリストになっていたものが一つにまとまって扱いやすくなりました。return
以降はリスト内包表記と言われる構文です。5 で作成したリストのそれぞれの要素を、dict に詰め替えて新しいリストを作っています。この部分はfor
文を使って下のように書くこともできます。
ret = [] for cat, proba in zip(ordered_cats, ordered_probas): ret.append({ 'category': cat, 'probability': proba }) return ret
リスト内包表記とfor
文、どっちが読みやすいかは人によるでしょう。私自身は、リスト内包表記は文ではなく式であるという点でリスト内包表記の方が好みです。また実行速度はリスト内包表記の方が若干速いらしいですね(Pythonのリスト内包の速度)。
動かしてみる②
サーバーを再起動して、SwaggerUI の画面を再度開きます。先ほどと同様にして「Try it out」で本記事の冒頭部分を分類してみましょう。結果は以下のようになりました。
[ { "category": "it-life-hack", "probability": 0.3086756411902118 }, { "category": "smax", "probability": 0.15842430053076112 }, { "category": "dokujo-tsushin", "probability": 0.13005882520629944 }, { "category": "kaden-channel", "probability": 0.1221002790185913 }, { "category": "peachy", "probability": 0.11371989611168741 }, { "category": "movie-enter", "probability": 0.04808770370861974 }, { "category": "sports-watch", "probability": 0.04605206014172084 }, { "category": "topic-news", "probability": 0.03650002186757273 }, { "category": "livedoor-homme", "probability": 0.036381272224538394 } ]
どうやら本記事は、9 カテゴリ内では「ITライフハック」に入ってそうではあるけれども、確率は 3 割程度でそんなに高くないようです。仕様変更によってより詳しい結果を知ることができるようになりました。
まとめ
以上、機械学習モデルを組み込んだ Web アプリを Python 初心者が作ってみました。意図してシンプルな仕様にしたこともあり、またフレームワークの助けもありとっても簡単に動くものが作れました。
今回は機械学習モデルを組み込んだ Web アプリ を作りましたが、機械学習モデルを使わない場合も基本的な作り方はあまり変わらないのではないかと思います。これを読んでくださった方が Python で Web アプリを作る時、何かの参考になれば幸いです。
参考
- livedoor ニュースコーパス - https://www.rondhuit.com/download.html
- scikit-learn - https://scikit-learn.org/stable/index.html
- Django - https://djangoproject.jp
- Flask - https://flask.palletsprojects.com/en/1.1.x/
- FastAPI - https://fastapi.tiangolo.com/ja/
- Python 製 Web フレームワークを Flask から FastAPI に変えた話 - https://note.com/navitime_tech/n/nc0381517d067
- ゼロから学ぶ Python - https://rinatz.github.io/python-book/
- The Hitchhiker’s Guide to Python - https://docs.python-guide.org/
- Cookiecutter - https://cookiecutter.readthedocs.io/en/1.7.2/
- Pythonのリスト内包の速度 - https://qiita.com/intermezzo-fr/items/43f90e07e4cebe63aeb6
エンジニア中途採用サイト
ラクスでは、エンジニア・デザイナーの中途採用を積極的に行っております!
ご興味ありましたら是非ご確認をお願いします。
https://career-recruit.rakus.co.jp/career_engineer/カジュアル面談お申込みフォーム
どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。
以下フォームよりお申込みください。
rakus.hubspotpagebuilder.comラクスDevelopers登録フォーム
https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/イベント情報
会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください!
◆TECH PLAY
techplay.jp
◆connpass
rakus.connpass.com