LESSON

コスト敏感学習

「サンプリングはデータを変える方法だった。今度はアルゴリズム側で不均衡に対処する方法を学ぼう。」

田中VPoEが続ける。

「モデルに『不正を見逃すコストは高い』と教える。これがコスト敏感学習だ。」

クラス重み調整

最もシンプルなコスト敏感学習は、少数クラスに大きな重みを与えることである。

scikit-learnでの実装

from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier

# 方法1: class_weight='balanced' を使用
lr_balanced = LogisticRegression(class_weight='balanced', random_state=42)
lr_balanced.fit(X_train, y_train)

# balanced の内部計算:
# weight_class_i = n_samples / (n_classes * n_samples_class_i)
# 正常: 284807 / (2 * 284315) = 0.5009
# 不正: 284807 / (2 * 492)    = 289.44
# → 不正クラスに約578倍の重みが付く

カスタム重みの設定

# 方法2: ビジネスコストに基づくカスタム重み
# FNコスト:FPコスト = 50000:500 = 100:1
custom_weights = {0: 1, 1: 100}

rf_custom = RandomForestClassifier(
    class_weight=custom_weights,
    random_state=42,
    n_estimators=100
)
rf_custom.fit(X_train, y_train)

重み設定の効果

重みなし(デフォルト):
  → モデルは多数クラス(正常)に最適化
  → 不正の検知率(Recall)が低い

class_weight='balanced':
  → クラス比率の逆数で重み付け
  → Recallが向上するが、Precisionが低下する傾向

カスタム重み:
  → ビジネスコストに基づく設定が可能
  → コスト最小化に直結

XGBoost/LightGBMでのコスト敏感学習

XGBoostの scale_pos_weight

import xgboost as xgb

# 不均衡比率を直接指定
neg_count = (y_train == 0).sum()
pos_count = (y_train == 1).sum()
scale = neg_count / pos_count

xgb_model = xgb.XGBClassifier(
    scale_pos_weight=scale,  # ≈ 578
    max_depth=6,
    learning_rate=0.1,
    n_estimators=200,
    eval_metric='aucpr',     # PR-AUCで評価
    random_state=42
)
xgb_model.fit(
    X_train, y_train,
    eval_set=[(X_val, y_val)],
    verbose=False
)

LightGBMの is_unbalance

import lightgbm as lgb

lgb_model = lgb.LGBMClassifier(
    is_unbalance=True,         # 自動でクラス重みを調整
    # または scale_pos_weight=578,
    max_depth=6,
    learning_rate=0.1,
    n_estimators=200,
    metric='average_precision',
    random_state=42
)
lgb_model.fit(
    X_train, y_train,
    eval_set=[(X_val, y_val)],
    callbacks=[lgb.log_evaluation(50)]
)

非対称損失関数

Focal Loss

通常の交差エントロピーを改良し、「簡単なサンプル」の損失を抑え、「難しいサンプル」に集中する。

import torch
import torch.nn.functional as F

def focal_loss(y_pred, y_true, alpha=0.25, gamma=2.0):
    """
    Focal Loss: 不均衡データ向けの損失関数

    alpha: 正例クラスの重み(0〜1)
    gamma: 簡単なサンプルの損失を抑える度合い
           gamma=0 → 通常の交差エントロピー
           gamma=2 → 簡単なサンプルの損失を大幅に抑制
    """
    bce = F.binary_cross_entropy_with_logits(y_pred, y_true, reduction='none')
    p_t = torch.exp(-bce)

    alpha_t = alpha * y_true + (1 - alpha) * (1 - y_true)
    focal_weight = alpha_t * (1 - p_t) ** gamma

    loss = focal_weight * bce
    return loss.mean()

# 使用例
# loss = focal_loss(model_output, target, alpha=0.75, gamma=2.0)

Focal Lossの直感:

通常の交差エントロピー: -log(p_t)
Focal Loss:            -alpha * (1 - p_t)^gamma * log(p_t)

予測確率p_t=0.9(簡単なサンプル):
  CE:    -log(0.9) = 0.105
  Focal: -0.25 * (0.1)^2 * log(0.9) = 0.000263
  → 400倍も損失が抑制される

予測確率p_t=0.1(難しいサンプル):
  CE:    -log(0.1) = 2.303
  Focal: -0.25 * (0.9)^2 * log(0.1) = 0.466
  → 5倍程度の抑制

XGBoostカスタム目的関数

def weighted_binary_cross_entropy(y_pred, dtrain):
    """非対称重み付き二値交差エントロピー"""
    y_true = dtrain.get_label()
    weight_positive = 100  # 不正クラスの重み

    sigmoid = 1 / (1 + np.exp(-y_pred))

    # 重み付き勾配
    weights = np.where(y_true == 1, weight_positive, 1)
    grad = weights * (sigmoid - y_true)
    hess = weights * sigmoid * (1 - sigmoid)

    return grad, hess

# XGBoostで使用
# model = xgb.train(params, dtrain, obj=weighted_binary_cross_entropy)

コスト敏感学習 vs サンプリング

観点コスト敏感学習サンプリング
データ変更なしあり
実装の容易さパラメータ設定のみ前処理が必要
過学習リスク低いオーバーサンプリングで高い
情報損失なしアンダーサンプリングで発生
柔軟性損失関数レベルで調整可サンプル数レベルで調整
推奨場面まずこちらを試すコスト敏感学習で不十分な場合

実践的なガイドライン

不均衡データ対策のステップ:
1. まず class_weight='balanced' で学習してベースラインを確認
2. scale_pos_weight をビジネスコスト比に基づき調整
3. 不十分なら SMOTE + コスト敏感学習の組み合わせ
4. さらに追求するなら Focal Loss 等のカスタム損失関数
5. 閾値最適化で最終的なコスト最小化

まとめ

項目ポイント
クラス重みbalanced または ビジネスコスト比で設定
XGBoostscale_pos_weight で不均衡比率を指定
LightGBMis_unbalance=True で自動調整
Focal Loss簡単なサンプルの損失を抑え、難しいサンプルに集中
推奨サンプリングより先にコスト敏感学習を試す

チェックリスト

  • class_weight=‘balanced’ の計算式を理解した
  • XGBoost/LightGBMでの不均衡対策パラメータを使える
  • Focal Lossの仕組みを説明できる
  • コスト敏感学習とサンプリングの使い分けを判断できる

次のステップへ

コスト敏感学習を理解したところで、次は不均衡データに適した評価指標を体系的に学ぼう。

推定読了時間: 30分