Machine Learning: FastAPIとDockerによるモデルデプロイ — MLモデルの本番サービングガイド
最終更新:2026-08-26
Jupyterの中で眠っている学習済みモデルには価値がありません。モデルの真価は、本番環境にデプロイされて初めて発揮されます。
1. 学習内容
- FastAPIによるモデルサービング:Pydanticバリデーション、非同期予測エンドポイント、Swaggerドキュメントの自動生成
- モデルのシリアライゼーション:sklearnモデルのjoblib/pickleによる保存と、PyTorchモデルのtorch.saveによる保存
- Dockerコンテナ化:Dockerfileの作成、マルチステージビルド、イメージの最適化
- docker-composeによるオーケストレーション:モデルサービス + Redisキャッシュ + Nginxロードバランシング
- Bobの予測API:POST /predictエンドポイントの設計と、1回の推論あたり50ms未満のレイテンシ実現
2. 現役MLエンジニアの実体験
(1) 課題:Notebookのモデルではビジネスに使えない
BobはR²=0.89のXGBoostモデルを学習させましたが、それはJupyter Notebookの中でしか動きませんでした。プロダクトマネージャーから「フロントエンドから呼び出せる?」と聞かれても、モデルをAPIにする方法がわからなかったのです。学習とデプロイの間のギャップこそ、MLプロジェクトにおける最大の「ラストワンマイル」問題です。
(2) FastAPI + Dockerによる解決策
FastAPIがモデルをREST APIでラップし、Dockerがそれをコンテナ化します。これにより、あらゆるサービスがHTTP経由で推論を呼び出せるようになります。
PYTHON
from fastapi import FastAPI
import joblib
app = FastAPI()
model = joblib.load("model.pkl")
@app.post("/predict")
def predict(features: PredictionInput):
result = model.predict([features.dict()])
return {"prediction": float(result[0])}
(3) 成果:リリース後に1日数百万リクエストを処理
BobがFastAPI + Dockerでモデルをデプロイしたところ、APIレイテンシは50ms未満を維持しながら、1日100万件以上のリクエストを処理できるようになりました。フロントエンド、CRM、レコメンデーションシステムのいずれからも呼び出し可能です。
3. モデルのシリアライゼーション
(1) モデルの保存と読み込み
▶ サンプル:sklearnモデルのシリアライゼーション
PYTHON
import joblib
import pickle
from sklearn.ensemble import RandomForestRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
import numpy as np
# Train and save model
rng = np.random.default_rng(42)
X = rng.uniform(0, 100, (1000, 5))
y = 50 + 0.8 * X[:, 0] + 1.2 * X[:, 1] + rng.normal(0, 5, 1000)
pipe = Pipeline([
("scaler", StandardScaler()),
("model", RandomForestRegressor(n_estimators=100, random_state=42)),
])
pipe.fit(X, y)
# Save with joblib (recommended for sklearn)
joblib.dump(pipe, "salespredict_model.joblib", compress=3)
# Save with pickle (alternative)
with open("salespredict_model.pkl", "wb") as f:
pickle.dump(pipe, f)
# Load and predict
loaded_model = joblib.load("salespredict_model.joblib")
sample = np.array([[50, 30, 20, 10, 5]])
prediction = loaded_model.predict(sample)
print(f"Prediction: {prediction[0]:.2f} thousand USD")
出力:
TEXT
📖 参照専用
# Executed successfully
| 方法 | 最適な用途 | メリット | デメリット |
|---|---|---|---|
| joblib | sklearn/numpy | 大規模配列に効率的 | Python専用 |
| pickle | 任意のPythonオブジェクト | 汎用的 | セキュリティリスク、バージョン互換性 |
| torch.save | PyTorch | 柔軟(state_dictの保存が可能) | PyTorch専用 |
| mlflow.sklearn | sklearn | バージョン管理 + メタデータ | MLflowが必要 |
| ONNX | フレームワーク間 | 言語/プラットフォーム横断 | 変換が複雑 |
▶ サンプル:PyTorchモデルの保存
PYTHON
import torch
import torch.nn as nn
# Save model state_dict (recommended)
class SimpleModel(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(nn.Linear(5, 32), nn.ReLU(), nn.Linear(32, 1))
def forward(self, x):
return self.net(x)
model = SimpleModel()
torch.save(model.state_dict(), "pytorch_model.pt")
# Load
loaded = SimpleModel()
loaded.load_state_dict(torch.load("pytorch_model.pt", weights_only=True))
loaded.eval()
出力:
TEXT
📖 参照専用
# Function defined successfully
4. FastAPIによるモデルサービング
(1) FastAPIの基本
▶ サンプル:完全な予測API
PYTHON
# File: app.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
import joblib
import numpy as np
import time
app = FastAPI(title="SalesPredict API", version="1.0.0")
# Load model at startup
model = joblib.load("salespredict_model.joblib")
class PredictionInput(BaseModel):
ad_spend_k: float = Field(..., ge=0, description="Ad spend in thousand USD")
traffic_k: float = Field(..., ge=0, description="Traffic in thousands")
category_electronics: float = Field(0, ge=0, le=1)
category_clothing: float = Field(0, ge=0, le=1)
is_promotion: float = Field(0, ge=0, le=1)
model_config = {"json_schema_extra": {
"example": {"ad_spend_k": 50, "traffic_k": 300,
"category_electronics": 1, "category_clothing": 0, "is_promotion": 1}
}}
class PredictionOutput(BaseModel):
predicted_revenue_k: float
latency_ms: float
@app.get("/health")
def health_check():
return {"status": "healthy", "model_loaded": model is not None}
@app.post("/predict", response_model=PredictionOutput)
def predict(input_data: PredictionInput):
start = time.time()
try:
features = np.array([[input_data.ad_spend_k, input_data.traffic_k,
input_data.category_electronics,
input_data.category_clothing, input_data.is_promotion]])
prediction = model.predict(features)[0]
latency = (time.time() - start) * 1000
return PredictionOutput(predicted_revenue_k=round(float(prediction), 2),
latency_ms=round(latency, 2))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/predict_batch")
def predict_batch(inputs: list[PredictionInput]):
features = np.array([[d.ad_spend_k, d.traffic_k, d.category_electronics,
d.category_clothing, d.is_promotion] for d in inputs])
predictions = model.predict(features)
return {"predictions": [round(float(p), 2) for p in predictions]}
出力:
TEXT
📖 参照専用
# Function defined successfully
(2) FastAPIサービスの実行
BASH
# Install dependencies
pip install fastapi uvicorn joblib scikit-learn
# Run the API server
uvicorn app:app --host 0.0.0.0 --port 8000 --reload
# Test with curl
curl -X POST http://localhost:8000/predict \
-H "Content-Type: application/json" \
-d '{"ad_spend_k": 50, "traffic_k": 300, "category_electronics": 1, "category_clothing": 0, "is_promotion": 1}'
# Access Swagger UI: http://localhost:8000/docs
| FastAPIの機能 | 説明 |
|---|---|
| Pydanticバリデーション | 入力値の型と範囲を自動で検証 |
| Swagger UI | 対話型ドキュメントを自動生成(/docs) |
| 型ヒント | レスポンスモデルを自動生成 |
| 非同期サポート | 非同期/awaitによる高並列処理 |
| 例外処理 | HTTPExceptionによる標準エラーコード |
5. Dockerコンテナ化
(1) Dockerfileの作成
▶ サンプル:SalesPredictのDockerイメージ
DOCKERFILE
# Stage 1: Build dependencies
FROM python:3.11-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# Stage 2: Runtime (smaller image)
FROM python:3.11-slim
WORKDIR /app
# Copy installed packages from builder
COPY --from=builder /install /usr/local
# Copy application code and model
COPY app.py .
COPY salespredict_model.joblib .
# Non-root user for security
RUN useradd -m appuser
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s \
CMD curl -f http://localhost:8000/health || exit 1
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
TEXT
📖 参照専用
# requirements.txt
fastapi==0.109.0
uvicorn==0.27.0
joblib==1.3.2
scikit-learn==1.4.0
numpy==1.26.4
pydantic==2.5.0
(2) Dockerコマンド
BASH
# Build image
docker build -t salespredict-api:latest .
# Run container
docker run -d -p 8000:8000 --name salespredict salespredict-api:latest
# Test
curl http://localhost:8000/health
# View logs
docker logs salespredict
# Stop and remove
docker stop salespredict && docker rm salespredict
| Dockerfileの最適化 | 効果 |
|---|---|
| マルチステージビルド | イメージを1.5GBから200MBに縮小 |
| slimベースイメージ | 不要なシステムパッケージを削除 |
| .dockerignore | .git/dataなどの大容量ファイルを除外 |
| 非rootユーザー | セキュリティの強化 |
| HEALTHCHECK | コンテナのヘルスチェック |
6. docker-composeによるオーケストレーション
本番環境のデプロイアーキテクチャでは、すべてのコンポーネントを連携させます。APIサービス、キャッシュ、ロードバランサー、モニタリングが一体となり、エンドツーエンドのパイプラインを構成します。
graph TB
CLIENT[Client / Frontend] --> NGINX[Nginx<br/>Rate Limit + LB]
NGINX --> API1[FastAPI Worker 1]
NGINX --> API2[FastAPI Worker 2]
API1 --> REDIS[(Redis Cache<br/>LRU 256MB)]
API2 --> REDIS
API1 --> MODEL[Model File<br/>.joblib]
API2 --> MODEL
PROM[Prometheus<br/>Metrics] --> API1
PROM --> API2
GRAF[Grafana<br/>Dashboard] --> PROM
▶ サンプル:完全な本番環境デプロイアーキテクチャ
YAML
# docker-compose.yml
version: "3.8"
services:
api:
build: .
ports:
- "8000:8000"
environment:
- REDIS_URL=redis://redis:6379
depends_on:
- redis
deploy:
replicas: 2
restart: unless-stopped
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- api
restart: unless-stopped
volumes:
redis_data:
TEXT
📖 参照専用
# nginx.conf (simplified load balancer)
upstream api_servers {
server api:8000;
}
server {
listen 80;
location / {
proxy_pass http://api_servers;
proxy_set_header Host $host;
}
}
▶ サンプル:API + Redisキャッシュ
PYTHON
# Enhanced app.py with Redis caching
from fastapi import FastAPI
from pydantic import BaseModel
import joblib
import numpy as np
import hashlib
import json
app = FastAPI(title="SalesPredict API with Cache")
model = joblib.load("salespredict_model.joblib")
# Redis cache (conceptual)
# import redis
# redis_client = redis.from_url(os.getenv("REDIS_URL", "redis://localhost:6379"))
class PredictionInput(BaseModel):
ad_spend_k: float
traffic_k: float
category_electronics: float = 0
category_clothing: float = 0
is_promotion: float = 0
def get_cache_key(input_data: PredictionInput) -> str:
data_str = json.dumps(input_data.model_dump(), sort_keys=True)
return f"pred:{hashlib.md5(data_str.encode()).hexdigest()}"
@app.post("/predict")
def predict(input_data: PredictionInput):
cache_key = get_cache_key(input_data)
# Check cache first
# cached = redis_client.get(cache_key)
# if cached:
# return json.loads(cached)
features = np.array([[input_data.ad_spend_k, input_data.traffic_k,
input_data.category_electronics,
input_data.category_clothing, input_data.is_promotion]])
prediction = float(model.predict(features)[0])
result = {"predicted_revenue_k": round(prediction, 2)}
# Cache for 5 minutes
# redis_client.setex(cache_key, 300, json.dumps(result))
return result
出力:
TEXT
📖 参照専用
# Function defined successfully
| コンポーネント | 役割 | 技術選定 |
|---|---|---|
| APIサービス | モデル推論 | FastAPI + Uvicorn |
| キャッシュ | 高頻度予測のキャッシュ | Redis(TTL 5分) |
| ロードバランサー | リクエストの分散 | Nginx |
| コンテナオーケストレーション | サービス管理 | docker-compose |
| ヘルスチェック | 障害検知 | /health + HEALTHCHECK |
❓ よくある質問
Q pickleとjoblibはどちらが良いですか?
A sklearnモデルにはjoblibを使用してください(大規模なnumpy配列をより効率的に圧縮できます)。汎用的なPythonオブジェクトにはpickleを使います。ただし、どちらもセキュリティリスクがあるため(信頼できないpickleファイルは悪意あるコードを実行する可能性があります)、本番環境ではMLflowやONNXの方が安全です。
Q FastAPIとFlaskのどちらを使うべきですか?
A 新規プロジェクトにはFastAPIを推奨します。ドキュメントの自動生成(Swagger)、型バリデーション(Pydantic)、非同期サポート、そしてより高いパフォーマンスが得られます。Flaskはより成熟していますが、API開発の体験ではFastAPIに及びません。
Q Dockerイメージが大きすぎる場合はどうすればいいですか?
A 3つのコツがあります。1)マルチステージビルド(ビルド段階は最終イメージに含まれません);2)slim/alpineベースイメージの使用;3).dockerignoreで.git/dataなどを除外する。
Q ダウンタイムなしでモデルを更新するには?
A 2つのアプローチがあります。1)ブルーグリーンデプロイ(旧バージョンと新バージョンの切り替え);2)ローリングアップデート(docker-compose rolling update)。MLflow Model Registryと組み合わせてバージョン管理を行うと効果的です。
Q APIレイテンシを最適化するには?
A 4つの層で最適化します。1)Redisで高頻度リクエストをキャッシュ;2)バッチ推論でオーバーヘッドを削減;3)複数ワーカーの並列実行(Uvicorn workers);4)モデルの量子化(サイズ縮小)。
Q APIリクエストのレート制限はどう設定しますか?
A slowapiライブラリでレート制限を設定します。
limiter = Limiter(key_func=get_remote_address) のように使い、例えば1分あたり100リクエストに制限できます。これにより不正利用や過負荷を防止できます。📖 まとめ
- モデルのシリアライゼーション:sklearnにはjoblib、PyTorchにはtorch.save(state_dict)を使用し、本番環境ではMLflowを推奨
- FastAPIによるREST API提供:Pydanticで入力を検証、Swaggerでドキュメントを自動生成、非同期で高並列処理に対応
- Dockerコンテナ化:マルチステージビルドでイメージを縮小、非rootユーザーでセキュリティを強化、HEALTHCHECKでヘルスモニタリング
- docker-composeによるオーケストレーション:API + Redisキャッシュ + Nginxロードバランシングによる本番グレードのデプロイ
- キャッシュ戦略:Redisで高頻度の推論結果をTTL 5分でキャッシュし、30〜50%のヒット率を達成
- APIレイテンシ目標:50ms未満(モデル推論を含む)、バッチ推論で平均レイテンシをさらに低減
📝 練習問題
- 基礎(難易度 ⭐):sklearnモデルを学習させ、joblibで保存した後、別のPythonスクリプトで読み込んで推論を実行してください。ヒント:joblib.dump/load。
- 中級(難易度 ⭐⭐):FastAPIで/predictエンドポイントを作成し、Pydanticによる入力バリデーションと/healthチェックを含めてください。uvicornで起動し、curlでテストしてください。ヒント:第4章の完全なAPIコードを参照。
- チャレンジ(難易度 ⭐⭐⭐):Dockerfile(マルチステージビルド)+ docker-compose.yml(API + Redis + Nginx)を作成し、イメージをビルドしてフルサービススタックを起動してください。ロードバランシングとキャッシュが動作することを確認してください。ヒント:第5〜6章の設定ファイルを参照。