深層学習における不均衡データ対策:Focal LossとGHMの理論と実装

不均衡データ問題とFocal Loss

物体検出や自然言語処理などのタスクにおいて、ポジティブサンプルとネガティブサンプルの極端な不均衡、および「容易なサンプル」と「困難なサンプル」の比率の偏りは、モデルの学習を阻害する主要な要因となります。単段(Single-stage)の物体検出器を例に取ると、数万から数十万の候補領域が生成されますが、その绝大部分は背景(ネガティブ)であり、正解ラベル(ポジティブ)はごくわずかです。

標準的な交差エントロピーと課題

二値分類における標準的な交差エントロピー(Cross Entropy, CE)損失は以下の式で表されます。

CE(p, y) = -log(p) if y == 1 else -log(1 - p)

ここで、p はモデルがポジティブクラスを予測する確率です。正負サンプルの不均衡を緩和するために、クラスごとの重み α を導入したバランス付き交差エントロピーが一般的に使用されます。

CE_balanced = -α_t * log(p_t)

ただし、p_t は正解クラスの予測確率、α_t は対応するクラスの重みです。このアプローチは正負サンプルの数量不均衡には有効ですが、「分類が容易なサンプル」と「分類が困難なサンプル」の不均衡には対処できません。実際、大量の容易なサンプルの損失値は小さいものの、その絶対数が圧倒的に多いため、合計損失を支配し、モデルの学習を妨害します。

Focal Lossの導入

Focal Lossは、分類が容易なサンプル(予測確率 p_t が高いサンプル)の損失寄与を意図的に減衰させることで、困難なサンプルに学習の焦点を合わせる手法です。損失関数にモジュレーティング因子(調整項)を追加します。

FL(p_t) = - (1 - p_t)^γ * log(p_t)

焦点パラメータ γ (gamma) は、容易なサンプルの重みをどの程度減衰させるかを制御します。例えば γ = 2 の場合、p_t = 0.9 の容易なサンプルの損失は標準の交差エントロピーに比べて約100分の1にまで減衰します。これにクラスバランス用の重み α を組み合わせることで、正負の不均衡と難易度の不均衡を同時に解決します。

FL(p_t) = - α_t * (1 - p_t)^γ * log(p_t)

経験的に、α = 0.25、γ = 2.0 が良好なパフォーマンスを示すことが知られています。

Focal LossのPyTorch実装

以下は、PyTorchを用いたFocal Lossのカスタム実装です。シグモイド活性化と二値交差エントロピーを組み合わせて計算します。

import torch
import torch.nn.functional as F

def calculate_focal_loss(logits, targets, alpha=0.25, gamma=2.0, reduction='mean'):
    """
    Focal Lossを計算する関数
    :param logits: モデルの未活性化出力 (BCEWithLogitsLoss用)
    :param targets: 正解ラベル (0 or 1)
    :param alpha: クラスバランス重み
    :param gamma: 焦点パラメータ
    :param reduction: 損失の集約方法 ('none', 'mean', 'sum')
    """
    # 確率の計算
    probs = torch.sigmoid(logits)
    
    # p_t (正解クラスの予測確率) の計算
    p_t = (probs * targets) + ((1 - probs) * (1 - targets))
    
    # alpha_t (正解クラスの重み) の計算
    alpha_t = (alpha * targets) + ((1 - alpha) * (1 - targets))
    
    # モジュレーティング因子 (1 - p_t)^gamma
    modulator = torch.pow(1.0 - p_t, gamma)
    
    # 基礎となるBCE損失 (reduction='none'で要素ごとの損失を取得)
    base_loss = F.binary_cross_entropy_with_logits(logits, targets, reduction='none')
    
    # Focal Lossの適用
    focal_loss = alpha_t * modulator * base_loss
    
    if reduction == 'mean':
        return focal_loss.mean()
    elif reduction == 'sum':
        return focal_loss.sum()
    return focal_loss

Focal Lossの限界とGHMの提案

Focal Lossは画期的な手法ですが、いくつかの構造的な課題を抱えています。第一に、極端に分類が困難なサンプル(外れ値やアノテーションミスを含むノイズ)に対して過剰に注意を払ってしまう点です。これにより、モデルがノイズに過適合し、全体の汎化性能が低下するリスクがあります。第二に、ハイパーパラメータである α と γ の最適値がタスクに強く依存し、これらを同時に調整するコストが高いという問題があります。

これらの課題を解決するために提案されたのが、Gradient Harmonizing Mechanism(GHM)です。GHMは、予測確率そのものではなく、「勾配の大きさ(Gradient Norm)」と「サンプル密度(Gradient Density)」に基づいて損失の重みを動的に調整します。

勾配ノルムとサンプル密度

交差エントロピー損失における勾配の大きさは、予測確率 p と正解ラベル y* の差の絶対値として近似できます。

g = |p - y*|

この g はサンプルの困難さを表します。g が0に近いサンプルは容易、1に近いサンプルは困難(または外れ値)です。データセット全体の g の分布を観察すると、g が0付近の容易なサンプルと、g が1付近の極端に困難なサンプルの両方にピークを持つU字型の分布になることが 많습니다。

GHMでは、勾配ノルムの範囲(0から1)を複数のビン(区間)に分割し、各ビンに属するサンプルの密度(単位勾配あたりのサンプル数)を計算します。密度が高いビン(大量に存在する容易なサンプルや多数存在する外れ値)の重みを下げ、密度が低いビン(適度に困難で有益なサンプル)の重みを相対的に高めることで、調和の取れた学習を実現します。

GHM分類損失(GHM-C)の実装

以下は、勾配密度の逆数を重みとして適用するGHM-CのPyTorch実装です。

import torch
import torch.nn as nn
import torch.nn.functional as F

class GHMCLoss(nn.Module):
    def __init__(self, num_bins=10):
        super(GHMCLoss, self).__init__()
        self.num_bins = num_bins
        # 0.0から1.0までの区切り点を生成
        self.edges = torch.linspace(0.0, 1.0, num_bins + 1)
        
    def forward(self, logits, targets):
        # 確率の計算と勾配計算のためのデタッチ
        probs = torch.sigmoid(logits).detach()
        
        # 勾配ノルム (g) の計算
        grad_norms = torch.abs(probs - targets)
        
        weights = torch.zeros_like(targets, dtype=torch.float32)
        valid_bin_count = 0
        total_samples = targets.numel()
        
        # 各ビンごとにサンプル数をカウントし、密度の逆数を重みとして設定
        for i in range(self.num_bins):
            lower_bound = self.edges[i]
            upper_bound = self.edges[i+1]
            
            # 現在のビンに属するサンプルのマスク
            if i == self.num_bins - 1:
                mask = (grad_norms >= lower_bound) & (grad_norms <= upper_bound)
            else:
                mask = (grad_norms >= lower_bound) & (grad_norms < upper_bound)
                
            bin_sample_count = mask.sum().item()
            
            if bin_sample_count > 0:
                # 密度の逆数 (全サンプル数 / ビン内のサンプル数) を重みに設定
                weights[mask] = total_samples / bin_sample_count
                valid_bin_count += 1
                
        # 有効なビン数で正規化
        if valid_bin_count > 0:
            weights = weights / valid_bin_count
            
        # 重み付きBCE損失の計算
        loss = F.binary_cross_entropy_with_logits(
            logits, targets, weight=weights, reduction='sum'
        ) / total_samples
        
        return loss

回帰タスクへの拡張(GHM-R)

GHMの概念は分類タスクだけでなく、境界箱回帰(Bounding Box Regression)などの回帰タスクにも拡張可能です。GHM-Rでは、Smooth L1 Lossの勾配ノルムを利用して同様の密度ベースの重み付けを行い、簡単すぎるサンプルや極端な外れ値による回帰学習の劣化を防ぎます。これにより、単段検出器における分類と回帰の両方の不均衡問題を統一的な枠組みで解決することができます。

タグ: FocalLoss GHM PyTorch ObjectDetection LossFunction

10月20日 02:06 投稿