Skip to content

付録D. Flask による Web アプリ開発

本付録では、軽量な Web フレームワーク Flask を使って Web アプリと REST API を作る方法を学びます。第 13 回で Streamlit を使って作った感情分析アプリと同じ「LM Studio サーバ + 分類器」の構成を、今度は REST API・HTML フォームとして実装します。API を「使う側」の一般的なパターンは付録Cを参照してください。

D.1 Flask による Web アプリ

D.1.1 Flask とは

Flask は、Python で Web アプリケーションを構築するための軽量なフレームワークです。Streamlit よりも柔軟性が高く、より本格的な Web アプリや REST API を構築できます。

Flask の特徴

  • 軽量: 必要最小限の機能のみを提供
  • 柔軟: 拡張性が高く、カスタマイズしやすい
  • REST API: API サーバとして利用できる
  • 豊富なエコシステム: 多数の拡張ライブラリが存在

D.1.2 Flask のインストール

Flask は CHIKUWA Editor に標準では含まれていないため、ターミナルで以下のコマンドを実行してインストールします(自分で構築したローカル環境でも同様です)。

pip install flask

D.1.3 Flask の基本的な使い方

最も簡単な Flask アプリの例を見てみましょう。

hello_flask.py
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return "Hello, Flask!"

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

このスクリプトを実行します。

python hello_flask.py

ブラウザで http://localhost:5000 にアクセスすると、"Hello, Flask!" と表示されます。

D.1.4 Flask の基本構造

Flask アプリは、ルーティング(URL とハンドラ関数の対応付け)を基本としています。

from flask import Flask, request, render_template

app = Flask(__name__)

# ルート(トップページ)
@app.route('/')
def index():
    return "トップページ"

# 別のページ
@app.route('/about')
def about():
    return "About ページ"

# パラメータを受け取る
@app.route('/user/<name>')
def user(name):
    return f"Hello, {name}!"

# POST リクエストを処理
@app.route('/submit', methods=['POST'])
def submit():
    data = request.form['data']
    return f"受け取ったデータ: {data}"

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

D.1.5 テンプレートエンジン

Flask では、HTML テンプレートを使って動的なページを生成できます。テンプレートには Jinja2 というテンプレートエンジンを使用します。

ディレクトリ構造

project/
├── app.py
└── templates/
    └── index.html

テンプレート(templates/index.html)

<!DOCTYPE html>
<html>
  <head>
    <title>{{ title }}</title>
  </head>
  <body>
    <h1>{{ message }}</h1>
  </body>
</html>

Flask アプリ(app.py)

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html',
                           title="My App",
                           message="Hello, Flask!")

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

D.2 Flask で感情分析 API を作る

D.2.1 REST API とは

REST API は、HTTP リクエスト/レスポンスを使ってデータをやり取りする仕組みです。Web アプリや他のプログラムから機械学習モデルを利用できるようになります。

REST API の利点

  • プログラムから利用可能: 他のアプリケーションから呼び出せる
  • 言語非依存: どんなプログラミング言語からでも利用可能
  • スケーラブル: サーバを増やして処理能力を向上できる

D.2.2 感情分析 API の実装

Flask アプリ自身が「LM Studio サーバ + 分類器」をラップして、別の REST API として公開する形にします。クライアントから見ると Flask アプリだけが見え、内部でローカル LLM サーバを使っている事実は隠蔽されます。分類器には、第 13 回(13.3.2)で保存した sentiment_clf.pkl を使います。

graph LR
    A[クライアント<br/>curl, Python] -->|POST /predict<br/>text| B[Flask API]
    B -->|POST /v1/embeddings| C[LM Studio サーバ<br/>bge-m3]
    C -->|1024 次元ベクトル| B
    B --> D[分類器<br/>pickle]
    D --> B
    B -->|sentiment, confidence| A

    style A fill:#e6ccff
    style B fill:#ccffcc
    style C fill:#ffffcc
    style D fill:#ffcccc
api.py
from flask import Flask, request, jsonify
import requests
import numpy as np
import pickle

LLM_URL = "https://xxxxxxxx.trycloudflare.com"  # 授業でアナウンスされる URL に置き換える
EMBED_MODEL = "text-embedding-bge-m3"

app = Flask(__name__)

# 分類器の読み込み
print("分類器を読み込んでいます...")
with open('sentiment_clf.pkl', 'rb') as f:
    classifier = pickle.load(f)

print("分類器の読み込みが完了しました。")


# テキスト 1 文を LM Studio でベクトル化
def encode_one(text):
    r = requests.post(
        f"{LLM_URL}/v1/embeddings",
        json={"model": EMBED_MODEL, "input": text},
        timeout=30,
    )
    return np.array([r.json()["data"][0]["embedding"]])


@app.route('/')
def index():
    return """
    <h1>感情分析 API</h1>
    <p>使い方:POST /predict にテキストを送信してください。</p>
    <p>例:curl -X POST -H "Content-Type: application/json" -d '{"text":"この映画は面白かった"}' http://localhost:5000/predict</p>
    """


@app.route('/predict', methods=['POST'])
def predict():
    data = request.get_json()

    if 'text' not in data:
        return jsonify({'error': 'テキストが指定されていません'}), 400

    text = data['text']
    if not text.strip():
        return jsonify({'error': 'テキストが空です'}), 400

    # LM Studio でベクトル化
    embedding = encode_one(text)

    # 予測
    prediction = classifier.predict(embedding)[0]
    probability = classifier.predict_proba(embedding)[0]

    result = {
        'text': text,
        'sentiment': 'positive' if prediction == 1 else 'negative',
        'confidence': float(probability[prediction]),
        'probabilities': {
            'positive': float(probability[1]),
            'negative': float(probability[0]),
        },
    }

    return jsonify(result)


if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5000)

このアプリは「API を 使う側 (LM Studio を叩く)」と「API を 作る側 (/predict を公開する)」の両方を同時に体験できる構成になっています。API 呼び出しの一般的なパターンは付録Cで詳しく扱っています。

D.2.3 API の使い方

API の起動

python api.py

curl コマンドでテスト

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"text":"この映画は本当に素晴らしかった!"}' \
  http://localhost:5000/predict

レスポンス例

{
  "text": "この映画は本当に素晴らしかった!",
  "sentiment": "positive",
  "confidence": 0.92,
  "probabilities": {
    "positive": 0.92,
    "negative": 0.08
  }
}

Python からの利用

import requests

url = 'http://localhost:5000/predict'
data = {'text': 'この映画はつまらなかった'}

response = requests.post(url, json=data)
result = response.json()

print(result)

D.2.4 Flask アプリと HTML フォーム

Flask アプリに HTML フォームを追加して、ブラウザから直接利用できるようにしてみましょう。

ディレクトリ構造

project/
├── app.py
├── sentiment_clf.pkl
└── templates/
    ├── index.html
    └── result.html

app.py

from flask import Flask, render_template, request
import requests
import numpy as np
import pickle

LLM_URL = "https://xxxxxxxx.trycloudflare.com"  # 授業でアナウンスされる URL に置き換える
EMBED_MODEL = "text-embedding-bge-m3"

app = Flask(__name__)

# 分類器の読み込み
with open('sentiment_clf.pkl', 'rb') as f:
    classifier = pickle.load(f)


def encode_one(text):
    r = requests.post(
        f"{LLM_URL}/v1/embeddings",
        json={"model": EMBED_MODEL, "input": text},
        timeout=30,
    )
    return np.array([r.json()["data"][0]["embedding"]])


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


@app.route('/analyze', methods=['POST'])
def analyze():
    text = request.form['text']

    if not text.strip():
        return render_template('index.html', error="テキストを入力してください")

    # LM Studio でベクトル化と予測
    embedding = encode_one(text)
    prediction = classifier.predict(embedding)[0]
    probability = classifier.predict_proba(embedding)[0]

    sentiment = 'Positive' if prediction == 1 else 'Negative'
    confidence = probability[prediction]

    return render_template('result.html',
                           text=text,
                           sentiment=sentiment,
                           confidence=confidence,
                           prob_positive=probability[1],
                           prob_negative=probability[0])


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

templates/index.html

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>感情分析アプリ</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        max-width: 600px;
        margin: 50px auto;
        padding: 20px;
      }
      textarea {
        width: 100%;
        height: 150px;
        padding: 10px;
        font-size: 14px;
      }
      button {
        background-color: #4caf50;
        color: white;
        padding: 10px 20px;
        border: none;
        cursor: pointer;
        font-size: 16px;
      }
      button:hover {
        background-color: #45a049;
      }
      .error {
        color: red;
      }
    </style>
  </head>
  <body>
    <h1>😊 感情分析アプリ</h1>
    <p>テキストを入力すると、ポジティブかネガティブかを判定します。</p>

    {% if error %}
    <p class="error">{{ error }}</p>
    {% endif %}

    <form method="POST" action="/analyze">
      <textarea
        name="text"
        placeholder="テキストを入力してください..."
      ></textarea>
      <br /><br />
      <button type="submit">分析する</button>
    </form>
  </body>
</html>

templates/result.html

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>分析結果</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        max-width: 600px;
        margin: 50px auto;
        padding: 20px;
      }
      .result {
        padding: 20px;
        border-radius: 5px;
        margin: 20px 0;
      }
      .positive {
        background-color: #d4edda;
        border: 1px solid #c3e6cb;
      }
      .negative {
        background-color: #f8d7da;
        border: 1px solid #f5c6cb;
      }
      a {
        color: #007bff;
        text-decoration: none;
      }
    </style>
  </head>
  <body>
    <h1>分析結果</h1>

    <div
      class="result {% if sentiment == 'Positive' %}positive{% else %}negative{% endif %}"
    >
      <h2>{{ sentiment }}</h2>
      <p><strong>入力テキスト:</strong> {{ text }}</p>
      <p><strong>信頼度:</strong> {{ "%.2f"|format(confidence * 100) }}%</p>
      <hr />
      <p>Positive: {{ "%.2f"|format(prob_positive * 100) }}%</p>
      <p>Negative: {{ "%.2f"|format(prob_negative * 100) }}%</p>
    </div>

    <a href="/">← 戻る</a>
  </body>
</html>

D.3 まとめ

本付録では、Flask による Web アプリと REST API の作り方について学びました。

付録 D のまとめ

Flask

  • 軽量で柔軟な Web フレームワーク
  • ルーティングで URL とハンドラを対応付け
  • テンプレートエンジン(Jinja2)で HTML を生成
  • REST API の構築に適している

REST API 化

  • /predict エンドポイントで機械学習モデルをどんな言語からでも利用可能に
  • 内部で LM Studio を呼べば、「API を使う側と作る側」を同時に体験できる
  • リクエスト内容の検証とエラーレスポンス(400)を適切に返す

Streamlit との使い分け

  • デモやプロトタイプを素早く作る → Streamlit(第 13 回)
  • 他のプログラムから呼び出す API・画面を自由に設計したいアプリ → Flask