前回の第5回までは、認識の結果を画像で見て、「レモンがorangeになっている」などと目で確認してきました。しかし、システムを発注するときには「精度は何%か」を同じ物差しで測れる必要があります。
今回は、正解データを用意して、AIの結果と突き合わせ、適合率と再現率という2つの数字で精度を測ります。「精度○%」という言葉の中身を、実際の数字で理解する回です。
この回で作るもの
- 正解データ(何が・どこに写っているか)をJSONで持つ仕組み
- AIの結果と正解を突き合わせて、正解・誤検出・見逃しを数える処理
app/evaluate.py - 種類ごとの適合率・再現率を表示するコマンド
用語:TP・FP・FNと、適合率・再現率
AIの結果を、正解データと1つずつ照らし合わせると、次の3つに分けられます。
| 呼び方 | 言い換え | 意味 |
|---|---|---|
| TP | 正解 | 正解の枠と重なっていて、種類も合っている検出 |
| FP | 誤検出 | 正解にない(または種類が違う)のに見つけたもの |
| FN | 見逃し | 正解にあるのに、見つけられなかったもの |
この3つの数から、2つの割合を計算します。
- 適合率 = TP ÷(TP + FP):見つけたもののうち、正しかった割合。誤検出が多いと下がる
- 再現率 = TP ÷(TP + FN):正解のうち、見つけられた割合。見逃しが多いと下がる
「精度」という1つの言葉は、実際にはこの2つを指しているか、混ぜて使われていることが多くあります。どちらを重視するかは、第1回の「見逃しと誤検出のどちらが困るか」と同じ問いです。
「正しく見つけた」の判定:枠の重なり(IoU)
AIの枠と正解の枠が「同じ物体を指している」かどうかは、枠の重なり具合(IoU。共通部分の面積 ÷ 合わせた面積)で判断します。この連載では、0.5以上(半分以上重なる)を一致としました。
実装
枠の重なりを計算する
# app/evaluate.py(抜粋)
def iou(a: Box, b: Box) -> float:
"""2つの枠の重なり具合(0〜1)。共通部分の面積 ÷ 合わせた面積。"""
left, top = max(a[0], b[0]), max(a[1], b[1])
right, bottom = min(a[2], b[2]), min(a[3], b[3])
inter = max(0, right - left) * max(0, bottom - top)
union = (a[2] - a[0]) * (a[3] - a[1]) + (b[2] - b[0]) * (b[3] - b[1]) - inter
return inter / union if union else 0.0
突き合わせて数える
# app/evaluate.py(抜粋)
def evaluate(truths, detections, iou_threshold=0.5) -> dict[str, Score]:
"""種類ごとの TP / FP / FN を数える。信頼度の高い検出から順に、まだ使われていない正解と1対1で対応づける。"""
scores: dict[str, Score] = defaultdict(Score)
used: set[int] = set()
for det in sorted(detections, key=lambda d: -d.confidence):
best, best_iou = None, iou_threshold
for i, t in enumerate(truths):
if i in used or t.label != det.label:
continue
value = iou(det.box, t.box)
if value >= best_iou:
best, best_iou = i, value
if best is None:
scores[det.label].fp += 1
else:
used.add(best)
scores[det.label].tp += 1
for i, t in enumerate(truths):
if i not in used:
scores[t.label].fn += 1
return dict(scores)
ポイントは、1つの正解に対応できる検出は1つだけという点です。同じ物体に2つの枠を付けても、正解になるのは1つで、もう1つは誤検出として数えます。
正解データ
正解データは、JSONで、種類と枠を書きます。
{
"image": "fruits.jpg",
"objects": [
{"label": "orange", "box": [0, 0, 158, 105]},
{"label": "orange", "box": [69, 44, 348, 470]},
{"label": "banana", "box": [0, 95, 146, 294]},
{"label": "lime", "box": [322, 120, 512, 305]},
{"label": "lemon", "box": [318, 247, 512, 480]},
{"label": "kiwi", "box": [0, 277, 130, 480]}
]
}
このサンプルの正解データは、説明用にAI(この連載の執筆者)が写真を見て枠を決めたものです。 実際の業務では、現場の担当者が正解を作り、複数人で確認します(後述)。
動かして確かめる
python -m app.evaluate samples/ground_truth/fruits.json --conf 0.25
fruits.jpg: 正解 6 個 / 検出 4 件(しきい値 0.25、重なり 0.5)
種類 TP FP FN 適合率 再現率
banana 0 0 1 - 0%
kiwi 0 0 1 - 0%
lemon 0 0 1 - 0%
lime 0 0 1 - 0%
orange 1 3 1 25% 50%
全体 1 3 5 25% 17%
これまで見てきた問題が、数字で表れました。
- orange:見つけた4件のうち、本物のオレンジは1件だけ(適合率25%)。写真に2個あるオレンジのうち、見つけられたのは1個(再現率50%)
- banana・kiwi・lemon・lime:学習済みモデルが知らない、または見つけられなかったため、再現率0%
- 全体:適合率25%・再現率17%
しきい値を0.1に下げると、結果はこう変わります。
fruits.jpg: 正解 6 個 / 検出 8 件(しきい値 0.1、重なり 0.5)
種類 TP FP FN 適合率 再現率
banana 0 0 1 - 0%
dining table 0 1 0 0% -
kiwi 0 0 1 - 0%
lemon 0 0 1 - 0%
lime 0 0 1 - 0%
orange 2 5 0 29% 100%
全体 2 6 4 25% 33%
オレンジの再現率は100%になりました(見逃しなし)が、誤検出は5件に増え、適合率は29%のままです。しきい値を下げると再現率は上がるが、適合率は上がらないという、第1回で見た関係が数字で確かめられました。
数字をそのまま信じてはいけない
この結果を見て、「この学習済みモデルは精度25%」と結論づけるのは誤りです。理由は次のとおりです。
- たった1枚の写真で測っている:写真1枚では、偶然の影響が大きすぎます。実際の評価には、対象の現場で撮った数十〜数百枚が必要です
- 正解データの作り方で数字が変わる:バナナを1つの枠にするか、2つに分けるかで、結果が変わります
- AIが苦手な写真を、あえて選んでいる:この写真は、モデルが知らない果物ばかりを集めた、説明用の写真です
大切なのは、数字そのものより、「どの写真で、誰が作った正解で、どう測ったか」を説明できることです。
テスト
pytest -q # 25件成功
ruff check . # 問題なし
枠の重なりの計算、正解・誤検出・見逃しの数え方(1つの正解に対応できるのは1つだけという規則を含む)、適合率・再現率の計算を、テストで確認しています。
つまずきやすい点
- 種類の名前がそろわない:正解データの
orangeとモデルのorangeの綴りが違うと、全部が誤検出・見逃しになります。名前の一覧を最初に決めます - 重なりのしきい値:0.5が標準的に使われますが、小さな物体を数える場合は厳しすぎることがあります。目的に合わせて決めます
- 数えない種類がある:正解にもAIにも無い種類は、表に出ません(
-は「割り算できない」の意味)
発注者向けメモ:「テスト用の正解付き写真」は発注者が用意する
精度の目標を決めて、それを満たしたかを確かめるには、正解付きのテスト用写真が必要です。この写真は、業務の中身を知っている発注者側が用意するのが基本です。開発会社は、現場の「正解」を知らないためです。
発注者がやること チェックリスト
- ☐ 現場で実際に撮る写真を、状況を変えて(明るさ・角度・混み具合)数十〜数百枚集めた
- ☐ 集めた写真に、何が・いくつ写っているかの正解を付けた(複数人で確認するとよい)
- ☐ 見逃し(再現率)と誤検出(適合率)のどちらを優先するか、目標の数字と一緒に決めた
- ☐ テスト用の写真は、開発会社が学習に使う写真とは別に取り分けた
- ☐ 納品時と、稼働後の定期的な確認で、同じテスト用の写真を使うことを決めた
開発会社への質問例
- 「精度の目標は、適合率と再現率のどちらで、どの写真を使って測りますか」
- 「テスト用の写真は、何枚必要ですか。学習に使う写真と分けて管理されますか」
- 「精度が目標に届かなかった場合、追加の学習や調整は、どの範囲まで契約に含まれますか」
- 「稼働後も、同じ方法で精度を測り直せますか。その作業は保守に含まれますか」
まとめと次回予告
第6回では、次のことを行いました。
- 正解データとAIの結果を突き合わせ、正解・誤検出・見逃しを数えた
- 適合率と再現率を計算し、しきい値を変えたときの関係を数字で確認した
- 写真1枚の数字は結論にならないこと、正解の作り方が大切なことを確認した
次回の第7回では、学習済みモデルが知らない対象(レモンやキウイなど)を、自分たちの写真で追加学習して覚えさせる手順を扱います。
この連載の記事一覧
この記事は連載「YOLOで作る画像認識ツール」の1回です。連載のほかの回は次のとおりです(連載の一覧ページ)。
- 【YOLOで作る画像認識ツール 第0回】全体像と環境づくり:YOLOで最初の1枚を認識する
- 【YOLOで作る画像認識ツール 第1回】信頼度のしきい値とJSON出力:認識結果を「使える形」にする
- 【YOLOで作る画像認識ツール 第2回】認識結果を画像に描いて確認する:枠とラベルで「間違い」を見つけやすくする
- 【YOLOで作る画像認識ツール 第3回】種類ごとに何個あるか数えて、CSVに集計する
- 【YOLOで作る画像認識ツール 第4回】FastAPIで画像認識を「API」にして、ほかのシステムから使えるようにする
- 【YOLOで作る画像認識ツール 第5回】ブラウザから写真をアップロードして、認識結果をその場で見る
- 【YOLOで作る画像認識ツール 第6回】精度を数字で測る:正解データと、適合率・再現率(この記事)
- 【YOLOで作る画像認識ツール 第7回】学習済みモデルにない対象を覚えさせる:追加学習の手順と、結果の正しい読み方
- 【YOLOで作る画像認識ツール 第8回】自信の低い結果だけ人が確認する:AIと人の役割分担を設計する
- 【YOLOで作る画像認識ツール 第9回】速く・軽くする:ONNXに変換してCPUで動かし、変換前後を比べる
- 【YOLOで作る画像認識ツール 第10回】Dockerで動かす:設定・ログ・ヘルスチェックを運用に近い形にする
- 【YOLOで作る画像認識ツール 第11回(最終回)】運用の注意点とライセンス(AGPL-3.0):精度の低下に気づく仕組みと、連載のまとめ

