MLOpsで本番地獄を見た話。半年運用して学んだ3つの落とし穴
モデル開発チームが半年のMLOps本番運用で痛感した失敗をぶっちゃけます。精度監視だけでは足りない理由、デプロイで起きた想定外、チームで動く仕組みの作り方まで。
MLOpsの本番運用で学んだ失敗と実装パターン|2026年の実践知見
うちのチームがモデル開発環境をMLOpsで整備したのが去年の秋。半年運用してみて、正直いうと最初の3ヶ月は地獄だった。データパイプラインが勝手に死ぬ、デプロイしたモデルが本番で精度が出ない、アラートは鳴り続ける…という状況から、今ようやく「まあ回ってるな」という状態に落ち着いてきた。
この記事は、本当に運用して気づいた落とし穴と、チームで実装したパターンについて書く。教科書的な解説ではなく、実務で踏んだ地雷の話だ。
最初の罠:モデル監視を「精度チェック」だけだと思ってた
MLOpsを導入する時、多くのチーム(うちも含める)が監視の話になると「モデルの精度を監視しよう」という話になる。でも実際に本番に流してみたら、精度はそこまで悪くないのに、ユーザーからは「なんか変な結果が増えた」という報告が来た。
原因は入力データのドリフト。本番環境のユーザー行動が季節変動で変わって、訓練データとの分布がズレてた。でも既存の精度メトリクスだけ監視してたら、その変化は引っかからない。
そこから気づいたのが、モデル監視って実は3層あるということなんだ。
| 監視レイヤー | 対象 | 見るべき指標 |
|---|---|---|
| 入力データの品質と分布監視 | 本番データ | 欠損値、値域外れ、クラス不均衡 |
| モデル出力の分布監視 | 予測結果 | 予測値の平均、確率分布 |
| 実ビジネスメトリクスの監視 | ビジネス成果 | CTR、コンバージョン、ユーザー満足度 |
これを全部セットで見ないと、本当の問題を見落とす。特に2番目のモデル出力分布は、精度メトリクスと別に監視する必要がある。うちはデータ品質管理2026年版の記事の考え方を応用して、Prometheus + Grafanaで毎時間のヒストグラムを追跡することにした。
# モデル出力分布の監視例
from prometheus_client import Histogram, Counter
import numpy as np
# 予測確率の分布を記録
pred_confidence = Histogram(
'model_prediction_confidence',
'Prediction confidence distribution',
buckets=(0.1, 0.3, 0.5, 0.7, 0.9, 0.95, 0.99)
)
# 予測クラスの分布
pred_class_dist = Counter(
'model_predicted_class',
'Predicted class distribution',
['class']
)
def monitor_predictions(predictions, confidences):
for conf in confidences:
pred_confidence.observe(conf)
for pred in predictions:
pred_class_dist.labels(class=str(pred)).inc()
これで入力と出力の両面を監視できるようになった。地味だけど、これがないと本番で勝手に品質低下に気づかない。
デプロイの地獄:「ローカルでは動く」がMLでも起きる
最初、モデルデプロイのCI/CDを設計した時、Dockerize → ECRにプッシュ → ECSにデプロイ という流れで「完成」だと思ってた。
でも実際に運用してみたら、ローカルで学習したモデルを本番環境に乗せると、精度が明らかに落ちてた。原因は3つ重なってたんだ。
- NumPyやSciPyのバージョン違い
- 前処理ロジックの微妙な差分
- ランダムシード由来の数値誤差
それからうちが実装したのが「バリデーション環境」のステップ。デプロイ前に、本番環境と全く同じDockerコンテナで、同じテストデータセットを使ってモデルを実行して、精度が既知の値とズレてないかを確認する。こうすることで、環境差による想定外の精度低下は完全に消えた。
# GitHub Actionsの例(デプロイ前検証)
name: Model Validation
on:
push:
branches: [main]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t model-inference:${{ github.sha }} .
- name: Run validation
run: |
docker run --rm \
-v ${{ github.workspace }}/test_data:/data \
model-inference:${{ github.sha }} \
python validate_model.py
- name: Check accuracy
run: |
ACCURACY=$(cat validation_output.json | jq '.accuracy')
if (( $(echo "$ACCURACY < 0.85" | bc -l) )); then
echo "Model accuracy $ACCURACY below threshold"
exit 1
fi
このステップを入れたことで、本番デプロイされるモデルは、常に同じ環境で同じ精度が出ることが保証されるようになった。
リトレーニングのスケジュール設計:「定期実行」だけじゃ危ない
最初、モデルのリトレーニングを「毎週月曜朝6時に自動実行」という単純なスケジュールで運用してた。でも数ヶ月運用すると、いろいろ問題が出てきた。
新しいデータが急に増える時期(キャンペーン期間中とか)に、古いスケジュールのままだと対応が遅い。逆に、データが少ない時期にリトレーニングすると精度が落ちる。さらには、リトレーニング中にスケジュール競合が発生して、前回の学習がまだ走ってるのに新しいジョブが始まるみたいなことも起きた。
そこから改善したのが、トリガーベースのリトレーニング。データドリフトを検知したら、そのタイミングでリトレーニングを始めるという方式だ。
# データドリフト検知とリトレーニングトリガー
import boto3
from scipy import stats
import numpy as np
lambda_client = boto3.client('lambda')
def detect_data_drift(current_data, reference_data):
"""
Kolmogorov-Smirnov テストでドリフト検知
"""
for feature in current_data.columns:
statistic, p_value = stats.ks_2samp(
reference_data[feature],
current_data[feature]
)
# p_value < 0.05 なら有意なドリフト
if p_value < 0.05:
return True, feature, p_value
return False, None, None
def lambda_handler(event, context):
# 最新のデータを取得
current_batch = get_recent_data(hours=24)
reference_batch = get_reference_data() # 直近1ヶ月の平均
has_drift, drift_feature, p_val = detect_data_drift(
current_batch, reference_batch
)
if has_drift:
print(f"Data drift detected in {drift_feature} (p={p_val:.4f})")
# リトレーニングジョブをトリガー
response = lambda_client.invoke(
FunctionName='retrain-model',
InvocationType='Event',
Payload=json.dumps({
'drift_feature': drift_feature,
'timestamp': datetime.utcnow().isoformat()
})
)
return {'statusCode': 202, 'message': 'Retraining triggered'}
return {'statusCode': 200, 'message': 'No drift detected'}
これを毎6時間実行させることで、必要な時だけリトレーニングが走る。無駄なコンピュート費用も減るし、データが充分にある時だけリトレーニングするから精度も安定するんだ。
本番デプロイ:カナリアデプロイとロールバック戦略
モデルをいきなり100%本番に流すと、もし精度が悪かったら一瞬にしてビジネスに影響する。うちが実装したのは、Canary deploymentだ。
新しいモデルを、まず全体の5%のユーザーに流す。その5%で1日回して、ビジネスメトリクス(CTR、コンバージョン、ユーザーエラーレート)が既存モデルと比べて「明らかに悪くない」かを確認する。OKなら25%、75%と段階的に増やしていく。この仕組みで、「モデルAはいいけどモデルBは駄目」という判断を自動でできるようになった。
# Canary deployment制御ロジック
class CanaryDeploymentController:
def __init__(self, s3_client, cloudwatch):
self.s3 = s3_client
self.cw = cloudwatch
def get_routing_weight(self, model_version):
"""
モデルバージョンに対するトラフィック割合を取得
"""
config = self.s3.get_object(
Bucket='model-deployment',
Key=f'routing/{model_version}.json'
)
return json.loads(config['Body'].read())
def check_metrics_health(self, model_version, hours=24):
"""
過去24時間のメトリクスが許容範囲か確認
"""
metrics_to_check = {
'ctr_change': 0.05, # 5%以内の変動
'error_rate_increase': 0.01, # エラー率が1%以上増加してないか
'latency_p99_increase': 50 # レイテンシ99パーセンタイルが50ms以上増加してないか
}
for metric, threshold in metrics_to_check.items():
metric_data = self.cw.get_metric_statistics(
Namespace='MLModel',
MetricName=metric,
Dimensions=[{'Name': 'ModelVersion', 'Value': model_version}],
StartTime=datetime.utcnow() - timedelta(hours=hours),
EndTime=datetime.utcnow(),
Period=3600,
Statistics=['Average']
)
if not self._is_within_threshold(metric_data, threshold):
return False, metric
return True, None
def promote_canary(self, model_version, next_weight):
"""
カナリアバージョンへのトラフィックを段階的に増加
"""
is_healthy, failed_metric = self.check_metrics_health(model_version)
if not is_healthy:
print(f"Model {model_version} failed health check: {failed_metric}")
# 自動ロールバック
self.rollback(model_version)
return False
# トラフィック割合を更新
self.s3.put_object(
Bucket='model-deployment',
Key=f'routing/{model_version}.json',
Body=json.dumps({'weight': next_weight})
)
return True
うちの場合、SLI/SLO設計の記事で定義したSLIを、ここでもそのまま使ってる。
チームで運用するための仕組み
MLOpsって、技術的な課題もあるけど、実は組織的な課題の方が大きいと気づいた。誰がモデルを作ったのか、どのデータで学習したのか、本番でいつぶっ壊れるのか…みたいなことが属人化してると、チーム全体の負荷が一人に集中する。
うちが実装した仕組みが、Model Manifest。モデルごとに、メタデータをJSONで定義して、GitHubで管理する。
{
"model_id": "recommendation_v2.1",
"created_by": "alice@example.com",
"created_at": "2026-07-01T10:30:00Z",
"framework": "scikit-learn",
"training_data": {
"source": "s3://data-lake/training/recommendation/2026-06",
"rows": 2500000,
"features_count": 147,
"time_range": "2026-04-01 to 2026-06-30"
},
"performance_metrics": {
"accuracy": 0.892,
"precision": 0.845,
"recall": 0.798,
"auc": 0.923
},
"deployment": {
"status": "CANARY",
"canary_weight": 0.25,
"started_at": "2026-07-05T06:00:00Z",
"target_weight": 1.0,
"estimated_full_rollout": "2026-07-08T06:00:00Z"
},
"monitoring": {
"data_drift_check": "enabled",
"drift_detection_method": "ks_test",
"alert_thresholds": {
"p_value": 0.05,
"error_rate_increase": 0.01
}
},
"rollback_policy": {
"auto_rollback_conditions": [
"error_rate > 2%",
"p95_latency > 500ms",
"data_drift_detected"
]
}
}
このManifestをPRの時に更新して、誰がレビューして承認したのかも記録する。Manifestがあれば、新しい人が引き継ぐ時も「このモデルは誰が作った」「どんなデータで学習した」「今の本番精度は」みたいなことが一瞬で分かるんだ。
結果として見えてきたこと
MLOpsを半年運用して思うのは、結局のところ「普通のソフトウェア開発と同じ」だということなんだ。入力データのドリフトは、外部APIの仕様変更に近い。モデルの精度低下は、バグと同じ扱いで監視する必要がある。デプロイ戦略もCanaryが最強だし、ロールバック計画も重要。
ただ、MLっていう領域固有の難しさがあるのも事実だ。決定的な正解がない。「このモデルの精度は十分か」という判断は、単なる技術的なメトリクスじゃなく、ビジネス要件とのバランスで決まる。
だからこそ、MLOpsの仕組みって「自動化」と「可視化」の組み合わせが大事なんだと、今は思ってる。データドリフトは自動検知して、その上で「どうするか」の判断は人間がやる。本番デプロイはCanaryで自動化するけど、「全体の100%に流してOK」という判断は人間がやる。このバランスが取れると、チーム全体の負荷も減るし、本番環境も安定する。
まとめ
MLOpsの本番運用で本当に大事なのは、以下の3つだ。
1. 多層的な監視 — 精度だけじゃなく、入力データの品質と分布、モデル出力の分布、ビジネスメトリクスの3つを同時に監視する。データ品質管理の考え方が役立つ。
2. デプロイ前の厳密なバリデーション — 本番環境と同じDockerコンテナで、同じテストデータで動かすステップを必ず入れる。「ローカルでは動く」を防げる。
3. Canary deploymentと自動ロールバック — 新しいモデルは段階的に流して、メトリクス低下があれば自動でロールバック。人間の判断が間に合わないタイミングってあるから、ここは自動化が命だ。
そして組織的には、Model Manifestみたいなメタデータ管理で、属人化を防ぐことが長期的には一番効く。知識が一人の頭の中に閉じ込まると、その人が休んだ時に立ち直れなくなるんだ。
うちのチームも、あと3ヶ月もすれば、もっと安定した運用が見えてくると思う。その時また改めて書きたいな。