はじめに:mAP 90%でも現場で使えないモデルの正体
「学習済みモデルのmAP(mean Average Precision)が0.9を超えました!これでリリースできます!」
開発現場でこのような報告を受けたものの、いざ実データで検証してみると、肝心の「稀に発生する重大な欠陥」を完全に見逃している——実際のプロジェクトでは、こうした状況がしばしば発生します。
数値上は優秀なモデルが、なぜ現場では役に立たないのか。その原因の多くは、学習データにおける「クラス不均衡(Class Imbalance)」と、それを隠蔽してしまう「mAPという指標の性質」にあります。
多くのエンジニアは、データが偏っていることを知りつつも、「とりあえずデータを増やそう」あるいは「ライブラリのデフォルト設定で学習させよう」としがちです。しかし、根本的なメカニズムを理解せずに表面的な対策を行っても、モデルは「多数派クラス」に最適化されるだけで、本当に検出すべきレアケースは無視され続けます。AI導入の目的はPoCの成功ではなく、ビジネス課題の解決です。
今回は、この根深い問題に対して、以下の4つのステップで論理的かつ実践的に切り込みます。
- バイアスの可視化:なぜmAPが実態を隠すのか、Pythonコードで数理的に暴く
- データレベルの対策:単純な増量ではない、物体検出特有の拡張手法
- アルゴリズムレベルの対策:Focal Lossの実装による「難易度」へのフォーカス
- 評価指標の再設計:ビジネス価値に基づいた「Weighted mAP」の提案
ブラックボックスになりがちな評価指標を解体し、コードレベルで納得感を持ちながら、現場で本当に使えるAIモデルを構築していきましょう。
1. mAPの罠:不均衡データが評価指標を歪めるメカニズム
まず、根本的な原因を知ることから始めましょう。なぜクラス不均衡があると、物体検出の標準的な評価指標であるmAP(mean Average Precision)は、モデルの性能について実態と異なる数値を示すのでしょうか。AI駆動型プロジェクトマネジメントの観点から、その数理的な背景を紐解いていきます。
多数派クラスがmAPを支配する数理的理由
mAPは、定義上、各クラスのAverage Precision(AP)の算術平均です。つまり、すべてのクラスは平等に扱われます。しかし、不均衡データにおいては、この「平等」こそが落とし穴となります。
例えば、工場の製造ラインで「良品(990個)」と「欠陥(10個)」のデータセットがあると仮定してください。
- 良品クラス(多数派)のAP: 1.0(完璧に検出)
- 欠陥クラス(少数派)のAP: 0.2(ほとんど見逃し)
この場合、mAPは $(1.0 + 0.2) / 2 = 0.6$ と算出されます。一見すると「0.6」というスコアは「改善の余地はあるが悪くはない」ように見えますが、欠陥検出システムとしては致命的な性能です。さらに、もしクラス数が多く、多数派クラスがいくつもある場合、少数のレアクラスの低いスコアは平均化の波に飲まれ、全体のmAPは実態よりも遥かに高い値を示してしまいます。
【検証コード】不均衡データセットでのベースライン評価
この現象をPythonで再現してみましょう。scikit-learnとmatplotlibを使って、不均衡データ下でのPR曲線(Precision-Recall Curve)とAPの乖離を可視化します。
以下のコードでは、意図的に偏ったダミーデータを生成し、多数派クラスが高いスコアを出す一方で、少数派クラスが低いスコアにとどまる状況をシミュレーションします。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import precision_recall_curve, average_precision_score
# シミュレーション用データの生成
# クラス0: 多数派 (良品) - 1000サンプル
# クラス1: 少数派 (欠陥) - 50サンプル
def generate_mock_predictions(n_samples, base_accuracy):
# 正解ラベル (1)
y_true = np.ones(n_samples)
# 予測スコア: ノイズを含ませる
noise = np.random.normal(0, 0.2, n_samples)
y_score = base_accuracy + noise
# スコアを0-1の範囲に収める(確率値として扱うため)
y_score = np.clip(y_score, 0, 1)
return y_true, y_score
# 多数派クラス: 高精度な予測をシミュレート
y_true_majority, y_score_majority = generate_mock_predictions(1000, 0.9)
# 少数派クラス: 低精度な予測をシミュレート (見逃しが多い)
y_true_minority, y_score_minority = generate_mock_predictions(50, 0.4)
# 偽陽性(FP)用のデータを追加 (背景を誤検出)
y_false_majority = np.zeros(100)
y_score_false_majority = np.random.uniform(0, 0.6, 100)
# データ結合
y_true_class0 = np.concatenate([y_true_majority, y_false_majority])
y_score_class0 = np.concatenate([y_score_majority, y_score_false_majority])
y_true_class1 = np.concatenate([y_true_minority, np.zeros(10)]) # FPは少ないとする
y_score_class1 = np.concatenate([y_score_minority, np.random.uniform(0, 0.4, 10)])
# AP計算
ap_class0 = average_precision_score(y_true_class0, y_score_class0)
ap_class1 = average_precision_score(y_true_class1, y_score_class1)
mAP = (ap_class0 + ap_class1) / 2
print(f"Majority Class AP: {ap_class0:.4f}")
print(f"Minority Class AP: {ap_class1:.4f}")
print(f"mAP: {mAP:.4f}")
# 可視化
plt.figure(figsize=(10, 5))
precision0, recall0, _ = precision_recall_curve(y_true_class0, y_score_class0)
precision1, recall1, _ = precision_recall_curve(y_true_class1, y_score_class1)
plt.plot(recall0, precision0, label=f'Class 0 (Majority) AP={ap_class0:.2f}')
plt.plot(recall1, precision1, label=f'Class 1 (Minority) AP={ap_class1:.2f}', linestyle='--')
plt.title(f'Precision-Recall Curve (mAP={mAP:.2f})')
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.legend()
plt.grid(True)
plt.show()
実行結果の考察(Why)
このコードを実行すると、Majority Class APはおそらく0.9以上という高い値を示す一方、Minority Class APは0.4程度にとどまるでしょう。しかし、mAPは約0.65〜0.7と計算されます。
もしレポートに「mAP 0.7」とだけ記載されていたら、プロジェクトの意思決定者はどう判断するでしょうか。「実用化に向けて順調だ」と誤解するリスクが高いと言えます。実際には、最も重要なClass 1(欠陥)の検出能力は実用レベルに達していません。
このように、mAPという単一の指標は、データの偏りを「平均化」という操作によって隠蔽してしまう性質を持っています。不均衡データを扱う際は、全体のmAPだけでなく、クラスごとのAPを必ず確認し、プロジェクトのROI(投資対効果)に直結する指標を見極める必要があります。
2. データレベルの解消アプローチ:サンプリングと拡張の実装
評価の罠を理解したところで、次は具体的な対策に入ります。まずはデータ入力(Input)段階でのアプローチです。
単に少数派クラスの画像をコピーして増やす「単純オーバーサンプリング」は、物体検出では推奨されません。背景情報ごとコピーするため、モデルが背景と物体を誤って関連付けて学習(過学習)してしまうリスクが高いからです。
Mosaic Augmentationによる背景コンテキストの強化
物体検出モデルのYOLOシリーズで標準的に採用されているMosaic Augmentationは、4枚の画像をランダムにクロップして1枚に結合する手法です。
この手法はYOLOv4で導入され広く知られるようになりましたが、2026年現在、YOLOv4のアクティブな開発は終了しています。現在は、より少ないパラメータで同等以上の精度を実現するYOLO11(またはそれ以降の最新モデル)での利用が標準となっています。
Mosaic Augmentationを適用することで、以下の効果が得られます:
- 見かけ上のバッチサイズ増加: 1回の更新で多様な背景コンテキストを学習できる。
- 小物体の検出精度向上: 縮小配置されることで、相対的に小さい物体の学習機会が増える。
移行のポイント:
新規プロジェクトでは、YOLOv4ではなくYOLO11以降の使用が推奨されます。Ultralyticsなどの最新フレームワークでは、Mosaic Augmentationがデフォルトで最適化されており、YOLOv4時代の手動実装よりも容易に導入可能です。
Copy-Paste Augmentationの実装パターン
さらに強力なのが、インスタンスセグメンテーションのマスク情報を利用して、物体部分だけを切り抜き、別の背景画像に貼り付けるCopy-Paste手法です。これにより、レアな物体を多様な背景に出現させることができます。
ここでは、画像処理ライブラリAlbumentationsを用いた実装イメージを紹介します。
import albumentations as A
import cv2
# Copy-Pasteのような高度な拡張を含むパイプライン定義
transform = A.Compose([
A.HorizontalFlip(p=0.5),
A.RandomBrightnessContrast(p=0.2),
# 実際にはCopy-Pasteはカスタム関数や専用ライブラリで行うことが多いが、
# ここではRandomSizedBBoxSafeCropなどで構図を変える例を示す
A.RandomSizedBBoxSafeCrop(height=512, width=512, p=0.5),
A.ShiftScaleRotate(p=0.5),
], bbox_params=A.BboxParams(format='yolo', label_fields=['class_labels']))
# 実装のポイント
# 少数派クラスの画像に対してのみ、より強いAugmentationを適用する戦略も有効です。
# 例えば、データローダー内でクラスIDを確認し、レアクラスの場合のみ
# Copy-Paste処理を走らせるロジックを組み込みます。
PyTorchでのWeightedRandomSampler適用
データ拡張だけでなく、バッチ作成時に少数派クラスが選ばれやすくするサンプリング戦略も必須です。PyTorchの標準機能であるWeightedRandomSamplerを使えば、Epoch内で各クラスが均等に出現するように調整できます。
これはPyTorchのバージョンに依存せず広く使える手法であり、不均衡データ対策の基本と言えます。
import torch
from torch.utils.data import DataLoader, WeightedRandomSampler
# データセット内の各サンプルのクラスIDリスト(例)
# 0: 多数派, 1: 少数派
dataset_targets = [0, 0, 0, 0, 0, 1, 0, 0, 1, 0]
# クラスごとの出現回数
class_counts = [8, 2]
# サンプルごとの重み計算(出現回数の逆数)
weights = [1.0 / class_counts[t] for t in dataset_targets]
sampler = WeightedRandomSampler(
weights=weights,
num_samples=len(weights),
replacement=True # 重複サンプリングを許可
)
# DataLoaderにsamplerを渡す(shuffle=Trueとは排他利用)
dataloader = DataLoader(
dataset,
batch_size=4,
sampler=sampler
)
# これにより、バッチ内ではクラス0とクラス1が概ね1:1の比率で出現するようになります。
このサンプリングを行うだけで、モデルが見る世界の「偏り」は強制的に是正されます。
3. アルゴリズムレベルの解消アプローチ:損失関数のカスタム実装
データ側での対策に加え、モデルの学習ロジック(損失関数)自体を修正するアプローチも非常に効果的です。ここで登場するのがFocal Lossです。
Cross EntropyからFocal Lossへの移行
通常のCross Entropy Lossは、簡単に分類できる「易しいサンプル(Easy Examples)」からの損失も加算し続けます。不均衡データでは、大量の多数派クラス(易しいサンプル)が損失全体の大部分を占めてしまい、モデルの更新方向を支配してしまいます。
Focal Lossは、予測確率 $p_t$ が高い(正解に近い)サンプルの損失を重み係数 $(1-p_t)^\gamma$ で減衰させます。これにより、モデルは「まだ上手く分類できていない難しいサンプル(Hard Examples)」、つまり少数派クラスや微妙な境界ケースに集中して学習するようになります。
PyTorchによるFocal Lossのカスタム実装
ライブラリに頼らず、PyTorchでゼロから実装してみましょう。数式をコードに落とし込むことで理解が深まります。
import torch
import torch.nn as nn
import torch.nn.functional as F
class FocalLoss(nn.Module):
def __init__(self, alpha=0.25, gamma=2.0, reduction='mean'):
super(FocalLoss, self).__init__()
self.alpha = alpha
self.gamma = gamma
self.reduction = reduction
def forward(self, inputs, targets):
# inputs: モデルの出力(ロジットなど)。BCEWithLogitsLossを使う前提で処理
# targets: 正解ラベル(0 or 1)
# まずは標準的なBCE Lossを計算(reductionなしで各要素のlossを取得)
bce_loss = F.binary_cross_entropy_with_logits(inputs, targets, reduction='none')
# 確率ptを計算(sigmoidを通す)
pt = torch.exp(-bce_loss)
# Focal Lossの係数を計算: (1 - pt) ^ gamma
focal_weight = (1 - pt) ** self.gamma
# Alpha係数の適用(正例と負例のバランス調整)
if self.alpha is not None:
alpha_t = self.alpha * targets + (1 - self.alpha) * (1 - targets)
focal_loss = alpha_t * focal_weight * bce_loss
else:
focal_loss = focal_weight * bce_loss
if self.reduction == 'mean':
return focal_loss.mean()
elif self.reduction == 'sum':
return focal_loss.sum()
else:
return focal_loss
# 使用例
criterion = FocalLoss(alpha=0.25, gamma=2.0)
# outputs = model(images)
# loss = criterion(outputs, targets)
α(バランス係数)とγ(フォーカシングパラメータ)の調整
- $\gamma$ (Gamma): 易しいサンプルの損失をどれだけ抑制するか。通常は
2.0が推奨されます。値を大きくするほど、難しいサンプルへの集中度が高まります。 - $\alpha$ (Alpha): 正例(Positive)と負例(Negative)の重みバランス。物体検出では背景(負例)が圧倒的に多いため、正例の重みを大きくするのではなく、逆に背景の影響を下げる調整が必要です。RetinaNetの論文では
0.25が使用されています。
この損失関数に切り替えるだけで、mAPの数値以上に、レアケースの検出能力(Recall)が向上すると考えられます。
4. 評価指標の再設計:クラス別重み付き評価の実装
最後に、モデルの評価方法そのものを見直します。冒頭で述べた「mAPの罠」を回避し、ビジネスゴールに合致した指標を定義します。
Confusion Matrix(混同行列)による詳細分析
mAPはスカラー値(単一の数値)ですが、分析にはマトリクスが必要です。特に「どのクラスをどのクラスと間違えているか」を知ることは、改善の第一歩です。
from sklearn.metrics import confusion_matrix
import seaborn as sns
def plot_confusion_matrix(y_true, y_pred, classes):
cm = confusion_matrix(y_true, y_pred)
# 正規化(Recallの確認用)
cm_normalized = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]
plt.figure(figsize=(10, 8))
sns.heatmap(cm_normalized, annot=True, fmt='.2f',
xticklabels=classes, yticklabels=classes, cmap='Blues')
plt.ylabel('True Label')
plt.xlabel('Predicted Label')
plt.title('Normalized Confusion Matrix')
plt.show()
このマトリクスを見れば、「クラスA(レア)が背景(Background)として見逃されている」のか、「クラスB(類似品)と誤認されている」のかが一目瞭然です。
ビジネスインパクトに基づいたWeighted mAPの計算
全てのクラスが等しく重要であるケースは稀です。例えば、半導体の欠陥検査において、「埃(除去可能)」の見逃しと、「スクラッチ(致命的欠陥)」の見逃しでは、損失コストが桁違いです。
そこで、ビジネス上の重要度(コストやリスク)を重みとして定義し、独自の評価指標 Weighted mAP を計算しましょう。
def calculate_weighted_map(ap_per_class, class_weights):
"""
ap_per_class: {class_id: ap_score} の辞書
class_weights: {class_id: weight} の辞書
"""
total_weight = sum(class_weights.values())
weighted_sum = 0
for cid, ap in ap_per_class.items():
weight = class_weights.get(cid, 1.0) # デフォルト重みは1.0
weighted_sum += ap * weight
return weighted_sum / total_weight
# 使用例
ap_scores = {0: 0.95, 1: 0.40} # 0:良品, 1:致命的欠陥
# 通常のmAP
normal_map = (0.95 + 0.40) / 2
print(f"Normal mAP: {normal_map:.3f}") # -> 0.675
# ビジネス重み付きmAP
# 致命的欠陥(Class 1)は良品(Class 0)の10倍重要とする
business_weights = {0: 1.0, 1: 10.0}
weighted_map = calculate_weighted_map(ap_scores, business_weights)
print(f"Weighted mAP: {weighted_map:.3f}") # -> 0.450
結果の解釈
通常のmAPは 0.675 ですが、重要度を加味したWeighted mAPは 0.450 と算出されました。これが「ビジネス視点での真の実力値」です。この指標をKPIに設定することで、開発チームは「多数派の良品判定を0.1%上げる」ことよりも「致命的欠陥の検出を改善する」ことに注力せざるを得なくなります。
まとめ:AIは「数値」ではなく「価値」で評価せよ
物体検出におけるクラス不均衡問題は、単なるデータサイエンスの課題ではなく、ビジネス課題そのものです。
- mAPを過信しない: 多数派クラスによる隠蔽バイアスを常に疑う。
- データを戦略的に扱う: WeightedRandomSamplerやMosaic Augmentationで入力の偏りを補正する。
- モデルに「難問」を教える: Focal Lossで学習の焦点をハードネガティブに合わせる。
- 指標をビジネスに合わせる: Weighted mAPで、本当に守るべき価値を数値化する。
これらを実装することで、開発するモデルは「コンペで勝てるモデル」から「現場で稼げるモデル」へと進化します。
しかし、実際のプロジェクトでは、データの特性や運用コストとのトレードオフなど、より複雑な判断が求められる場面が多々あります。「自社のデータセットでFocal Lossのパラメータをどう調整すべきか?」「Weighted mAPの重みはどう決めるのが妥当か?」といった課題に直面することもあるでしょう。
次のステップへ
AIはあくまでビジネス課題を解決するための手段です。プロジェクトの成功には、理論と実装の両輪を回し、ビジネス価値に直結する実用的なAI開発を推進していくことが重要です。現場のデータに基づいた論理的なアプローチで、ROIの最大化を目指していきましょう。
コメント