コンテンツにスキップ

5.3.4 異常検知

異常検知の外れ値イメージ図

この節では、実用的なアラート実験を 1 つ作ります。

  • 正常点と合成異常点を作る;
  • Isolation Forest の contamination を調整する;
  • 異常スコアを確認する;
  • Isolation Forest と LOF を比較する;
  • precision、recall、偽陽性、偽陰性をプロダクト上のトレードオフとして読む。

まず図を見てください。異常検知の中心は、何をアラートにするか、そして各ミスがどれくらい高いかです。

異常検知の意思決定フローチャート

異常検知の警報しきい値マンガ

用語実用上の意味
anomaly通常パターンに合わないサンプル
outlier多くの点から離れている点
contamination期待される異常割合。しきい値の手がかりになる
score_samplesモデルスコア。Isolation Forest では低いほど異常
false positive正常サンプルを疑わしいと誤検知すること
false negative本当の異常を見逃すこと
IsolationForest木ベースの手法。異常点をすばやく隔離する
LOFLocal Outlier Factor。各点周辺の局所密度を比較する
Terminal window
python -m pip install -U scikit-learn numpy

この実験では、学習のために合成ラベルを使います。実際の異常検知では、ラベルがない、遅れて届く、不完全であることがよくあります。

anomaly_lab.py を作成します。

import numpy as np
from sklearn.datasets import make_blobs
from sklearn.ensemble import IsolationForest
from sklearn.metrics import confusion_matrix, f1_score, precision_score, recall_score
from sklearn.neighbors import LocalOutlierFactor
from sklearn.preprocessing import StandardScaler
normal, _ = make_blobs(n_samples=360, centers=2, cluster_std=0.75, random_state=42)
rng = np.random.default_rng(42)
outliers = rng.uniform(low=-8, high=8, size=(24, 2))
X = np.vstack([normal, outliers])
y_true = np.array([0] * len(normal) + [1] * len(outliers)) # 1 means anomaly
X_scaled = StandardScaler().fit_transform(X)
print("isolation_forest_contamination_lab")
for contamination in [0.03, 0.06, 0.12]:
model = IsolationForest(contamination=contamination, random_state=42)
pred = model.fit_predict(X_scaled)
y_pred = (pred == -1).astype(int)
tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()
print(
f"contamination={contamination:.2f} "
f"flagged={int(y_pred.sum())} "
f"precision={precision_score(y_true, y_pred):.3f} "
f"recall={recall_score(y_true, y_pred):.3f} "
f"f1={f1_score(y_true, y_pred):.3f} "
f"fp={fp} fn={fn}"
)
print("score_inspection")
best = IsolationForest(contamination=0.06, random_state=42)
best.fit(X_scaled)
scores = best.score_samples(X_scaled) # lower means more abnormal
order = np.argsort(scores)[:5]
for idx in order:
print(f"index={idx:<3} score={scores[idx]:.3f} true_anomaly={bool(y_true[idx])}")
print("lof_comparison")
lof = LocalOutlierFactor(n_neighbors=20, contamination=0.06)
y_pred = (lof.fit_predict(X_scaled) == -1).astype(int)
print(
f"flagged={int(y_pred.sum())} "
f"precision={precision_score(y_true, y_pred):.3f} "
f"recall={recall_score(y_true, y_pred):.3f} "
f"f1={f1_score(y_true, y_pred):.3f}"
)

実行します。

Terminal window
python anomaly_lab.py

期待される出力:

Terminal window
isolation_forest_contamination_lab
contamination=0.03 flagged=12 precision=1.000 recall=0.500 f1=0.667 fp=0 fn=12
contamination=0.06 flagged=23 precision=0.826 recall=0.792 f1=0.809 fp=4 fn=5
contamination=0.12 flagged=46 precision=0.478 recall=0.917 f1=0.629 fp=24 fn=2
score_inspection
index=371 score=-0.747 true_anomaly=True
index=368 score=-0.738 true_anomaly=True
index=373 score=-0.734 true_anomaly=True
index=364 score=-0.725 true_anomaly=True
index=378 score=-0.717 true_anomaly=True
lof_comparison
flagged=23 precision=0.870 recall=0.833 f1=0.851

異常検知 contamination 実験結果図

アラートのトレードオフを読む

Section titled “アラートのトレードオフを読む”

contamination は、モデルがどれくらいのサンプルをアラートにするかに影響します。

contamination=0.03 flagged=12 precision=1.000 recall=0.500
contamination=0.12 flagged=46 precision=0.478 recall=0.917

これは分類しきい値と同じトレードオフです。

  • contamination が低い:アラートは少なく、偽陽性も少ないが、見逃しが増える;
  • contamination が高い:アラートは増え、recall は上がるが、偽陽性も増える。

正しい選択は数学だけでは決まりません。詐欺を 1 件見逃すコストが高ければ、偽陽性を多めに受け入れることがあります。人手レビューが高ければ、少数の高確信アラートを選ぶことがあります。

異常検知手法の比較図

Isolation Forest はランダムな分割木を作ります。異常点は少ない分割で孤立しやすいため、より異常なスコアを受け取ります。

実験では:

scores = best.score_samples(X_scaled)

Isolation Forest では、スコアが低いほど異常です。最も疑わしいサンプルは、合成した本当の異常でした。

index=371 score=-0.747 true_anomaly=True

単なる yes/no 判定ではなくレビューキューを作りたい場合、スコア順に見るほうが役立ちます。

LOF は、ある点の周辺密度と、その近傍点の周辺密度を比べます。全体からは遠くなくても、局所的に変な点を見つけるのに向いています。

この合成実験では:

lof_comparison
flagged=23 precision=0.870 recall=0.833 f1=0.851

LOF はここでは Isolation Forest より少し良い結果でした。ただし、常に優れているわけではありません。このデータでは局所密度という仮定が合っていた、という意味です。

状況最初に試すもの理由
一般的な表形式異常検知Isolation Forest速く、頑健で、調整しやすい
局所密度の異常LOF近傍と比べて不自然な点を見つけられる
単一数値列のチェックZ-score または IQR透明で安い
高次元 embeddingIsolation Forest + 近傍チェックスコアと近傍を合わせて見る
アラート運用が必要任意のモデル + しきい値/レビュー設計運用はスコアと同じくらい重要

経験者向け:異常検知は、遅れて届くラベル、レビュー可能件数、アラート疲れ、ドリフト監視を含めて評価します。オフライン F1 が最大のモデルでも、レビュー担当者を圧迫するなら実務では弱い設計です。

このページを終えたら、この evidence card を残します。

タスク
clustering、dimensionality reduction、または anomaly detection の目標
データ表示
スケーリング済み特徴量、射影、クラスタ、または異常スコア
解釈
このシナリオでグループ、軸、またはアラートが何を意味するか
失敗確認
任意のクラスタ数、スケーリングの問題、ノイズの多い次元、または誤検知
期待される成果
解釈と不確実性メモを含む教師なし結果
症状よくある原因修正
アラートが多すぎるcontamination またはしきい値が高いcontamination を下げ、レビュー段階を分ける
異常を多く見逃すしきい値が厳しすぎるcontamination を上げ、弱いルールも足し、recall を監視する
新データでスコア分布が変わるデータ分布のドリフトスコア分布を継続監視する
尺度の違いだけを異常扱いする特徴量をスケーリングしていない数値特徴量を先にスケーリングする
評価ラベルがない異常検知ではよくあるサンプルレビュー、フィードバック収集、遅延結果の追跡を行う
  1. 合成異常点の数を 24 から 1248 に変えてください。contamination はどう調整すべきですか?
  2. 外れ値を近づけるために low=-5, high=5 に変えてください。どの手法がより影響を受けますか?
  3. 尺度が非常に大きい 4 つ目の特徴量を追加してください。スケーリング前後で何が変わりますか?
  4. 固定しきい値ではなく、score_samples() で並べ替えて上位 20 件を確認してください。
  5. 3 段階のアラートキューを設計してください:今すぐレビュー、あとでレビュー、無視。
参考実装と解説
  1. contamination は想定される異常率に近づけます。たとえば 12 / total_samples48 / total_samples です。実務では運用上の仮定なので、レビュー結果で調整します。
  2. 外れ値が正常クラスタに近づくと、どの手法も難しくなります。局所密度や境界分離に強く依存する方法は偽陰性が増えやすいため、スコアだけでなく上位サンプルを確認します。
  3. 尺度の大きい特徴量は、スケーリング前に距離や異常スコアを支配します。スケーリング後は単位の大きさではなくパターン差を見やすくなります。
  4. score_samples() で並べる方法は運用向きです。しきい値が確定していなくても、分析者は最も怪しい候補から確認できます。
  5. たとえば最上位の少数を「今すぐレビュー」、次の層を「あとでレビュー」、残りを「無視」にします。割合はレビュー能力と誤報コストで決めます。

次を説明できれば、この節はクリアです。

  • 異常検知はアラートワークフローであり、モデルだけではない;
  • contamination は偽陽性/偽陰性のトレードオフを変える;
  • Isolation Forest は不自然な点をすばやく隔離する;
  • LOF は局所密度の異常を見つける;
  • 単一の yes/no ラベルより、スコア順位を見るほうが役立つことが多い。