サンプリング手法
「不均衡データへの最もシンプルな対策はサンプリングだ。だが、やり方を間違えると逆効果になる。」
田中VPoEがホワイトボードに図を描く。
「増やすか、減らすか、あるいは両方か。それぞれの特性と落とし穴を押さえておこう。」
オーバーサンプリング
少数クラス(不正取引)のデータを人工的に増やす手法である。
ランダムオーバーサンプリング
最も単純な方法。少数クラスのサンプルをランダムに複製する。
from imblearn.over_sampling import RandomOverSampler
ros = RandomOverSampler(random_state=42)
X_resampled, y_resampled = ros.fit_resample(X_train, y_train)
print(f"リサンプリング前: {dict(zip(*np.unique(y_train, return_counts=True)))}")
print(f"リサンプリング後: {dict(zip(*np.unique(y_resampled, return_counts=True)))}")
# リサンプリング前: {0: 199020, 1: 345}
# リサンプリング後: {0: 199020, 1: 199020}
問題点: 同じデータを複製するだけなので過学習しやすい。
SMOTE(Synthetic Minority Over-sampling Technique)
少数クラスのサンプル間を補間して、新しい合成サンプルを生成する。
from imblearn.over_sampling import SMOTE
smote = SMOTE(random_state=42, k_neighbors=5)
X_smote, y_smote = smote.fit_resample(X_train, y_train)
print(f"SMOTE後: {dict(zip(*np.unique(y_smote, return_counts=True)))}")
SMOTEのアルゴリズム:
1. 少数クラスのサンプル x_i を選択
2. x_i のk近傍(k=5がデフォルト)から1つをランダムに選択: x_nn
3. 合成サンプル = x_i + rand(0,1) * (x_nn - x_i)
4. 目標数に達するまで繰り返し
イメージ:
x_i ●──────────● x_nn
↑
この線分上にランダムに新サンプルを生成
注意点:
- ノイズサンプルの近傍にも合成サンプルが生成される(ノイズの増幅)
- 高次元データでは近傍の概念が曖昧になる
ADASYN(Adaptive Synthetic Sampling)
SMOTEの改良版。学習が難しい(境界付近の)サンプルに重点的に合成サンプルを生成する。
from imblearn.over_sampling import ADASYN
adasyn = ADASYN(random_state=42)
X_adasyn, y_adasyn = adasyn.fit_resample(X_train, y_train)
SMOTEとの違い:
SMOTE: 全ての少数サンプルに均等に合成
ADASYN: 多数クラスに囲まれた(学習困難な)サンプルに重点的に合成
結果: 決定境界付近のサンプルが増え、境界の学習が改善
アンダーサンプリング
多数クラス(正常取引)のデータを減らす手法である。
ランダムアンダーサンプリング
from imblearn.under_sampling import RandomUnderSampler
rus = RandomUnderSampler(random_state=42)
X_under, y_under = rus.fit_resample(X_train, y_train)
print(f"アンダーサンプリング後: {dict(zip(*np.unique(y_under, return_counts=True)))}")
# アンダーサンプリング後: {0: 345, 1: 345}
問題点: 多数クラスの情報を大量に捨てるため、モデルの汎化性能が低下する。
Tomek Links
クラス境界付近の「ノイジーな」多数クラスサンプルを除去する。
from imblearn.under_sampling import TomekLinks
tomek = TomekLinks()
X_tomek, y_tomek = tomek.fit_resample(X_train, y_train)
Tomek Linksの定義:
サンプル a(クラス0)と b(クラス1)が Tomek Link であるとは:
- aの最近傍が b
- bの最近傍が a
- aとbが異なるクラス
→ クラス境界のあいまいなサンプルを除去して境界を明確化
NearMiss
少数クラスに近い多数クラスサンプルを優先的に残す。
from imblearn.under_sampling import NearMiss
nm = NearMiss(version=1)
X_nm, y_nm = nm.fit_resample(X_train, y_train)
ハイブリッド手法
SMOTE + Tomek Links
from imblearn.combine import SMOTETomek
smt = SMOTETomek(random_state=42)
X_smt, y_smt = smt.fit_resample(X_train, y_train)
処理の流れ:
元データ → SMOTE(少数クラスを増やす)→ Tomek Links(境界のノイズを除去)
SMOTE + ENN(Edited Nearest Neighbours)
from imblearn.combine import SMOTEENN
smote_enn = SMOTEENN(random_state=42)
X_se, y_se = smote_enn.fit_resample(X_train, y_train)
手法の比較
| 手法 | メリット | デメリット | 推奨場面 |
|---|---|---|---|
| Random Over | 実装が簡単 | 過学習しやすい | ベースライン |
| SMOTE | 多様なサンプル生成 | ノイズ増幅のリスク | 一般的な不均衡 |
| ADASYN | 境界付近を重点的に学習 | 計算コスト高 | 境界が複雑な場合 |
| Random Under | 学習が高速 | 情報損失が大きい | データが大量の場合 |
| Tomek Links | 境界を明確化 | 不均衡解消は限定的 | 前処理として |
| SMOTE+Tomek | バランスの良い手法 | 計算コスト中 | 一般的な推奨 |
実装上の注意点
# 重要: サンプリングは学習データにのみ適用する
# テストデータには絶対に適用しない!
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# サンプリングは学習データにのみ
smote = SMOTE(random_state=42)
X_train_resampled, y_train_resampled = smote.fit_resample(X_train, y_train)
# テストデータはそのまま(元の分布を維持)
# model.predict(X_test) で評価
まとめ
| 項目 | ポイント |
|---|---|
| オーバーサンプリング | SMOTE/ADASYNで少数クラスの合成サンプル生成 |
| アンダーサンプリング | Tomek Links等で多数クラスのノイズ除去 |
| ハイブリッド | SMOTE+Tomekが実用的 |
| 鉄則 | サンプリングは学習データにのみ適用 |
チェックリスト
- SMOTEのアルゴリズムを説明できる
- ADASYNとSMOTEの違いを説明できる
- Tomek Linksの役割を理解した
- サンプリングを学習データにのみ適用する理由を説明できる
次のステップへ
サンプリング手法を理解したところで、次はコスト敏感学習によるアルゴリズムレベルの対処法を学ぼう。
推定読了時間: 30分