AIに異常を検知させると、警告が並びます。たいてい重大度の高い順です。
重大度順のリストは正しく見えます。ほかの並べ方を思いつきにくい。ところが上から順に開けていくと、外れが混ざっています。しかも外れほど上にいることがある。
並べ方が間違っているのではありません。並べるときに見ている値が1つ足りません。足りないのは重大度の細かさではなく、その警告がどれくらい確かかです。
この記事では、ビル管理の業務システム(Python / FastAPI)で警告の順位をどう出しているかを、最小実装つきで書きます。
順位はこの式で出している
スコア = 重大度の重み × 確信度 + 種類の加点
- 重大度の重み:critical / high / medium / low の4段階
- 確信度:0〜1。その検知ロジックがどれだけ確実か
- 種類の加点:検知の種類ごとの加点
掛け算と足し算を組み合わせた形です。出てくるのは数値で、降順に並べたものが順位になります。
確信度は、検知ロジックごとに決まる
確信度は警告1件ごとの気分ではなく、検知の仕組みで決まります。実際の値を2つ挙げます。
| 検知 | 確信度 | 理由 |
|---|---|---|
| 点検期限の超過 | 0.98 | 期限の日付と今日の日付を比べるだけ。外しようがない |
| 提出期限の接近 | 0.75 | 前回の提出履歴を見ていないので、提出済みのものにも警告が出る |
同じ「期限」の話でも、片方は確実で、片方は誤発火します。重大度だけで並べると、この差が順位に出ません。
最小実装
重みと境目の値、2件以外の重大度と確信度は、この記事用の例です。
from dataclasses import dataclass
# 重みの値はこの記事用の例
SEVERITY_WEIGHT = {"critical": 100, "high": 60, "medium": 30, "low": 10}
ORDER = list(SEVERITY_WEIGHT)
@dataclass(frozen=True)
class Signal:
kind: str # 検知の種類
severity: str # critical / high / medium / low
confidence: float # 0〜1。その検知ロジックがどれだけ確実か
kind_weight: float = 0.0 # 種類ごとの加点
def score(s: Signal) -> float:
return SEVERITY_WEIGHT[s.severity] * s.confidence + s.kind_weight
def label(x: float) -> str:
# 内部は数値で並べ、人には段階で見せる(境目の値も例)
for name, floor in (("critical", 80), ("high", 50), ("medium", 25)):
if x >= floor:
return name
return "low"
signals = [
Signal("故障の予測", "critical", 0.50),
Signal("提出期限の接近", "high", 0.75), # 提出履歴を見ていないので、提出済みでも鳴る
Signal("点検期限の超過", "high", 0.98), # 日付を比べるだけ。外しようがない
Signal("不具合の放置(14日以上)", "medium", 0.95, kind_weight=5),
]
print("重大度だけで並べた場合")
for s in sorted(signals, key=lambda s: ORDER.index(s.severity)):
print(f" {s.severity:<8} {s.kind}")
print("重大度の重み × 確信度 + 種類の加点 で並べた場合")
for s in sorted(signals, key=score, reverse=True):
print(f" {score(s):5.1f} {label(score(s)):<8} {s.kind}")
実行結果です。
重大度だけで並べた場合
critical 故障の予測
high 提出期限の接近
high 点検期限の超過
medium 不具合の放置(14日以上)
重大度の重み × 確信度 + 種類の加点 で並べた場合
58.8 high 点検期限の超過
50.0 high 故障の予測
45.0 medium 提出期限の接近
33.5 medium 不具合の放置(14日以上)
重大度だけだと、確信度0.5の予測が先頭に来ます。確信度を掛けると、確実に分かっている点検期限の超過が先に出ます。
これは副作用ではなく、意図して選んだ設計です。「重大かもしれない」より「確実に問題」を先に処理する、と決めたということです。
重みの差が、どこまでの不確かさを許すかを決める
この式では、重大度の重みの差がそのまま「確信度でどこまで逆転を許すか」になります。
上の例の重みだと、critical の警告が high・確信度0.98 の警告より上に来るのは、確信度が 58.8 ÷ 100 = 0.588 を超えたときだけです。0.588 を下回る critical は、確実な high の下に回ります。
重みを決めることは、どれくらい不確かな警告まで上に置くかを決めることです。重みを「なんとなく」で置くと、この境目も「なんとなく」になります。設計レビューでは、重みの値より先にこの境目を書き出して確認すると話が早くなります。
放置日数は、軸に入れずに引き金にする
「放置されている日数」もスコアに効きそうに見えますが、式には入れていません。代わりに、14日以上未対応なら、そのこと自体を1つの検知として立てるようにしています(上のコードの「不具合の放置(14日以上)」)。
日数を毎日少しずつ加点する形だと、順位がじわじわ上がるだけで、いつ手をつけるべきかの線が出ません。閾値で別の警告として出すと、「今日から別扱い」という線が引けます。
内部は数値、人には段階で見せる
画面に出すのはスコアの数値ではなく、critical / high / medium / low のラベルです。
数値のままだと、58.8 と 50.0 の差に意味があるのかを、見る人が毎回考えることになります。並べる計算は数値で行い、人が判断するときは段階で見る、という二段構えにしています。
順位を決めるのは式で、AIには説明だけを書かせる
AIに書かせているのは「なぜこの順位なのか」の説明です。何位にするかは式が決めます。
順位そのものをAIに出させると、同じ入力でも並びが揺れたときに理由を追えません。式で決めておけば、順位が変わったときに、どの値が変わったのかを追えます。
確実に鳴る火災報知器と、たまに誤作動する火災報知器が、同じ音量で鳴っている。重大度だけで並べると、これが起きます。
この順位は提案です。担当者は下位の案件でも先に対応して完了にでき、その操作は誰が・いつ・何を変えたかとして残ります。では、どの操作をAIの提案止まりにして、どれを人が押すのか。私はこれを「取り返しがつくか」「誰に届くか」の2つの質問だけで割り振っています。2つの軸の定義と、操作を動詞で書いて粒度をそろえる決め方を Zenn に書いています(この章は無料で読めます)。