ストーリー
田
田中VPoE
ログ収集と品質メトリクスの基盤ができた。次はドリフト検出だ。NetShop社のレコメンドAIの精度が先月から徐々に低下しているが、原因がわからない状態だ
あなた
「ドリフト」というのは、時間とともに何かが変化していくことですか?
あ
田
田中VPoE
その通り。AIシステムでは主に2種類のドリフトがある。入力データの分布が変化する「データドリフト」と、入力と正解の関係が変化する「コンセプトドリフト」だ。ECサイトではセール時期や季節変動でこれらが頻繁に起きる
あなた
ドリフトを検知して早期に対処する仕組みを作りたいです
あ
ドリフトの種類
2種類のドリフト
| ドリフト | 定義 | 原因例 | 影響 |
|---|
| データドリフト | 入力データの分布が変化 | 新商品カテゴリの追加、季節変動、ユーザー層の変化 | モデルが未知のパターンに遭遇 |
| コンセプトドリフト | 入力と正解の関係が変化 | トレンドの変化、ユーザーの嗜好変化、競合の影響 | 学習時の知識が陳腐化 |
ドリフトの発生パターン
突発的ドリフト:
品質 ████████████▁▁▁▁▁▁▁▁
原因: モデルAPIの仕様変更、大規模障害
漸進的ドリフト:
品質 ████████████████▇▇▇▆▆▅▅▄▄
原因: 季節変動、ユーザー層の緩やかな変化
周期的ドリフト:
品質 ████▇▇████▇▇████
原因: セール期間、年末年始、曜日パターン
再帰的ドリフト:
品質 ████▇▇████▅▅████
原因: 不定期のトレンド変化
データドリフトの検出
統計的検定によるドリフト検出
import numpy as np
from scipy import stats
class DataDriftDetector:
"""データドリフトの検出"""
def __init__(self, reference_data: np.ndarray):
self.reference = reference_data
def detect_ks_test(self, current_data: np.ndarray, threshold: float = 0.05) -> dict:
"""Kolmogorov-Smirnov検定によるドリフト検出"""
statistic, p_value = stats.ks_2samp(self.reference, current_data)
return {
"test": "KS Test",
"statistic": round(statistic, 4),
"p_value": round(p_value, 4),
"drift_detected": p_value < threshold,
"severity": self._classify_severity(p_value)
}
def detect_psi(self, current_data: np.ndarray, bins: int = 10) -> dict:
"""Population Stability Index(PSI)によるドリフト検出"""
ref_hist, bin_edges = np.histogram(self.reference, bins=bins, density=True)
cur_hist, _ = np.histogram(current_data, bins=bin_edges, density=True)
# ゼロ除算回避
ref_hist = np.clip(ref_hist, 1e-6, None)
cur_hist = np.clip(cur_hist, 1e-6, None)
# PSI計算
psi = np.sum((cur_hist - ref_hist) * np.log(cur_hist / ref_hist))
return {
"test": "PSI",
"psi_value": round(float(psi), 4),
"drift_detected": psi > 0.2,
"interpretation": (
"変化なし" if psi < 0.1 else
"軽微な変化" if psi < 0.2 else
"重大な変化"
)
}
def _classify_severity(self, p_value: float) -> str:
if p_value < 0.001:
return "HIGH"
elif p_value < 0.05:
return "MEDIUM"
else:
return "LOW"
PSI(Population Stability Index)の解釈
| PSI値 | 解釈 | 対応 |
|---|
| < 0.1 | 有意な変化なし | 通常運用 |
| 0.1 - 0.2 | 軽微な変化あり | 監視強化 |
| > 0.2 | 重大な分布変化 | 原因調査、モデル再評価 |
コンセプトドリフトの検出
性能ベースの検出
class ConceptDriftDetector:
"""コンセプトドリフトの検出"""
def __init__(self, window_size: int = 1000):
self.window_size = window_size
self.performance_history: list[float] = []
def add_observation(self, performance_score: float):
"""性能スコアを追加"""
self.performance_history.append(performance_score)
def detect_adwin(self, delta: float = 0.002) -> dict:
"""ADWIN(Adaptive Windowing)による検出"""
if len(self.performance_history) < self.window_size * 2:
return {"drift_detected": False, "reason": "データ不足"}
# ウィンドウを分割して平均を比較
recent = self.performance_history[-self.window_size:]
previous = self.performance_history[-self.window_size*2:-self.window_size]
recent_mean = np.mean(recent)
previous_mean = np.mean(previous)
diff = abs(recent_mean - previous_mean)
# 統計的有意差の判定
combined_std = np.sqrt(
np.var(recent) / len(recent) + np.var(previous) / len(previous)
)
threshold = combined_std * 2 # 2シグマ基準
return {
"drift_detected": diff > threshold,
"previous_mean": round(previous_mean, 4),
"recent_mean": round(recent_mean, 4),
"difference": round(diff, 4),
"threshold": round(threshold, 4),
"direction": "低下" if recent_mean < previous_mean else "向上"
}
LLM特有のドリフト検出
プロンプト-レスポンスドリフト
| 検出対象 | 指標 | 検出方法 |
|---|
| 入力プロンプトの変化 | プロンプト長、トピック分布 | 埋め込みベクトルのクラスタリング |
| 出力品質の変化 | ROUGE、一貫性スコア | LLM-as-Judgeで自動評価 |
| レイテンシの変化 | 応答時間のP50/P95 | 統計的管理図 |
| コストの変化 | トークン単価、総コスト | 予算アラート |
埋め込みベースのドリフト検出
from openai import OpenAI
import numpy as np
client = OpenAI()
class EmbeddingDriftDetector:
"""埋め込みベクトルを使ったプロンプトドリフト検出"""
def __init__(self):
self.reference_embeddings: list[list[float]] = []
self.reference_centroid: np.ndarray | None = None
def set_reference(self, reference_prompts: list[str]):
"""基準期間のプロンプトを設定"""
embeddings = self._get_embeddings(reference_prompts)
self.reference_embeddings = embeddings
self.reference_centroid = np.mean(embeddings, axis=0)
def detect_drift(self, current_prompts: list[str], threshold: float = 0.15) -> dict:
"""現在のプロンプトのドリフトを検出"""
current_embeddings = self._get_embeddings(current_prompts)
current_centroid = np.mean(current_embeddings, axis=0)
# コサイン距離でドリフト量を計測
cosine_sim = np.dot(self.reference_centroid, current_centroid) / (
np.linalg.norm(self.reference_centroid) * np.linalg.norm(current_centroid)
)
drift_score = 1 - cosine_sim
return {
"drift_score": round(float(drift_score), 4),
"drift_detected": drift_score > threshold,
"interpretation": (
"安定" if drift_score < 0.05 else
"軽微な変化" if drift_score < 0.15 else
"重大な変化"
)
}
def _get_embeddings(self, texts: list[str]) -> np.ndarray:
response = client.embeddings.create(
model="text-embedding-3-small",
input=texts
)
return np.array([e.embedding for e in response.data])
まとめ
| ドリフト種類 | 検出手法 | 主要指標 |
|---|
| データドリフト | KS検定、PSI | 入力分布の変化量 |
| コンセプトドリフト | ADWIN、性能監視 | 予測性能の低下 |
| プロンプトドリフト | 埋め込み距離 | プロンプト分布の変化 |
チェックリスト
次のステップへ
次はアラート設計とダッシュボード構築を学びます。
推定読了時間: 30分