RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

機械学習モデルを組み込んだ Web アプリを Python 初心者が作ってみた

こんにちは。開発エンジニアの amdaba_sk(ペンネーム未定)です。

前回は「機械学習をコモディティ化する AutoML ツールの評価」、だいぶ間が空きましたが前々回は「機械学習のライブラリ・プラットフォームをいくつか試した所感まとめ」と、続けて機械学習をテーマとした記事を書きました。

これらの記事では機械学習モデルを作るまでのことしか言及していませんが、機械学習モデルは作ってそれで終わりのものでもありません。使ってなんぼのものなんです。かみせんプロジェクトとしての調査範囲からは外れますが、せっかくモデルを作ったならそれを使ったアプリも簡単なものでいいので作ってみたい。そう思うのは開発者として自然な感情ではないでしょうか。

というわけで今回は、「機械学習モデルを組み込んだ Web アプリを Python 初心者が作ってみた」という個人的な興味からやってみた系記事でございます。

なお後に述べるようにアプリの実装言語は Python を採用しますが、私はかみせんプロジェクトで初めて Python を触ったばかりの初心者です。Python らしくないコードを書いている可能性もありますので、その点ご了承ください。

また「かみせんってなんやねん」と思われた方は ↓ のリンクからかみせんカテゴリの記事をご覧ください。

tech-blog.rakus.co.jp

もくじ

どんなアプリを作る?

なるべくシンプルなものがよいですね。あまり時間はかけたくないので。

過去 2 記事で主に取り扱った機械学習のタスクは、分類でした。ということは、ユーザーが送信したデータを機械学習モデルで分類し、その結果を提示するだけというのが簡単で良さそうです。また画面を作るのは面倒なので JSON API ということにしましょう。

分類するデータの形式は、またもや過去 2 記事を見ると、ひとつはテキスト、ひとつはテーブルを使っています。テーブルデータだと複数のフィールドを埋めてやらないといけないので、作った後お試しで使ってみる時にめんどくさそうですね。テキストにしましょう。

結局「機械学習のライブラリ・プラットフォームをいくつか試した所感まとめ」の冒頭で構想したものと似たようなアプリになりました。

  1. 分類したいテキストを含む JSON を受け付ける
  2. 送られてきたテキストが既存のカテゴリのどれに相当するかを推測する
  3. 分類の結果を JSON に入れて返す

どんな分類をするのか?

テキストを何のカテゴリに分類してくれるのかもここで考えておきましょう。要するにどんな学習データを使うのかという話ですが、今回は 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 を作ろうとしているため、やっぱり OpenAPIJSON 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)

実にシンプルですね。シンプル過ぎてifforも使う余地がありませんでした。

動かしてみる①

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 を動かしてみましょう。

SwaggerUI にアクセスしたところ

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

やることが増えたので少しコードも複雑になりました。ポイントを説明します。

  1. 推論実行のメソッドをpredictからpredict_probaに変更したことで、カテゴリ毎の確率を得ることが出来ます。並び順は分類器のclasses_属性と一致しています。
  2. predict_probaから返された配列に対して負符号を付けることで、要素の正負をすべて反転しています。これは次のステップで降順ソートにするためです。
  3. 要素の正負を反転した結果に対してnumpy.argsortを行えば、値の大小に従ってソートした時の配列インデックスの変化を返してくれます。
  4. Python ではリストや配列に対して[]にインデックスのリストを入れると、各インデックスに対応する値を指定した順序で返してくれます。ここでは 3 で得たインデックスのリストを使って、カテゴリのリスト(self.clf.classes_)とカテゴリ毎の確率(result_proba)を並べ替えています。
  5. zip関数を使ってカテゴリと対応する確率をペアにしています。これで別々のリストになっていたものが一つにまとまって扱いやすくなりました。
  6. 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 アプリを作る時、何かの参考になれば幸いです。

参考


◆TECH PLAY
techplay.jp

◆connpass
rakus.connpass.com


  1. 実は今回大ハマりして一番苦労したのはこの部分だったりするのですが、話が逸れるので割愛します。

  2. 今回はWSL2+Dockerでやっています。

Copyright © RAKUS Co., Ltd. All rights reserved.