コスト敏感学習
「サンプリングはデータを変える方法だった。今度はアルゴリズム側で不均衡に対処する方法を学ぼう。」
田中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 または ビジネスコスト比で設定 |
| XGBoost | scale_pos_weight で不均衡比率を指定 |
| LightGBM | is_unbalance=True で自動調整 |
| Focal Loss | 簡単なサンプルの損失を抑え、難しいサンプルに集中 |
| 推奨 | サンプリングより先にコスト敏感学習を試す |
チェックリスト
- class_weight=‘balanced’ の計算式を理解した
- XGBoost/LightGBMでの不均衡対策パラメータを使える
- Focal Lossの仕組みを説明できる
- コスト敏感学習とサンプリングの使い分けを判断できる
次のステップへ
コスト敏感学習を理解したところで、次は不均衡データに適した評価指標を体系的に学ぼう。
推定読了時間: 30分