クラス不均衡データにおける機械学習モデルの構築

クラス不均衡データとは、あるクラスのデータが圧倒的に多く、他のクラスのデータが非常に少ない状態のデータセットを指します。このようなデータで機械学習モデルを構築すると、多数派クラスに偏った予測をしてしまい、少数派クラスの予測精度が著しく低下してしまう、いわゆる過学習が起きてしまいます。

今回は、クラス不均衡データに対してどのようなアプローチを取ればよいのか解説したいと思います。

なぜクラス不均衡下で精度向上が難しいか

クラス不均衡データのモデル構築がなぜ困難であるか、以下で説明します。

モデルの過学習:

  • モデルは、学習データの多数派クラスの特徴を過度に学習してしまい、少数派クラスの特徴を捉えられなくなる。
  • 結果として、少数派クラスのデータを多数派クラスと誤分類してしまう可能性が高くなる。

評価指標の限界:

  • 一般的な評価指標である精度(Accuracy)は、多数派クラスの予測精度に大きく左右される。
  • 少数派クラスの予測精度を正確に評価するためには、Precision、Recall、F1-scoreなどの指標を併用する必要がある。

コストの非対称性:

  • 多くの場合、少数派クラスの誤分類の方が、多数派クラスの誤分類よりも大きなコストが伴う。
  • 例えば、病気の診断において、病気である人を健康と誤診する方が、健康な人を病気と誤診するよりも深刻な問題となる。

どのような対策があるか?

一般的に、クラス不均衡データに対するアプローチには、データレベルのアプローチ、アルゴリズムレベルのアプローチ、評価指標によるアプローチが考えられます。

データレベル:

  • オーバーサンプリング: 少数派クラスのデータを複製または生成して、データのバランスを改善する。
  • アンダーサンプリング: 多数派クラスのデータを削減して、データのバランスを改善する。
  • SMOTE (Synthetic Minority Over-sampling Technique): 少数派クラスのデータを合成的に生成することで、オーバーサンプリングによる情報量の損失を抑える。

アルゴリズムレベル:

  • コスト感度学習: 誤分類コストを考慮した学習を行う。
  • アンサンブル学習: 複数のモデルを組み合わせることで、予測の安定性を高める。
  • 異常検知: 少数派クラスを異常値として捉え、異常検知の手法を適用する。

評価指標:

  • Precision、Recall、F1-score: 少数派クラスの予測精度を評価するために、これらの指標を用いる。
  • ROC曲線: 異なる分類閾値におけるモデルの性能を評価する。
  • AUC: ROC曲線下の面積を表し、モデルの全体的な性能を評価する。

SMOTEについてもう少し詳しく

前述した通り、SMOTEは、不均衡なデータセットにおいて、少数派クラスのデータを合成的に生成することで、データのバランスを改善する手法です。

具体的には、少数派クラスのデータ点をいくつか選び、その点とk-最近傍にあるデータ点の間を線形に補間することで、新しいデータ点を合成します。これにより、少数派クラスのデータ数を増やし、データのバランスを改善します。

では、SMOTEのメリットとデメリットについても整理しておきます。

メリット:

  • 少数派クラスのデータ数を増やすことで、モデルの学習に貢献し、少数派クラスの予測精度を向上させることができる。
  • 過度に単純な複製ではなく、新しいデータ点を合成することで、データの多様性を維持できる。

デメリット:

  • 過度に合成データを増やしすぎると、ノイズが増え、過学習を引き起こす可能性があります。
  • 少数派クラス間のクラスターが密接している場合、新しいデータ点が既存のクラスターと重なり、クラス間の境界が曖昧になる可能性がある。

PythonによるSMOTE実装

クラス不均衡データに対するアルゴリズムをまとめたライブラリとしてimbalanced-learnというものがあります。このライブラリの中に、SMOTEが実装されているので、これの使い方をコードを交えて解説します。

まずは、pipコマンドからimbalanced-learnをインストールします。

$ pip install -U imbalanced-learn

今回、実験で使用するデータセットはimbalanced-learnに同梱されているものとします。データセットについてはこちらで確認してください。この中から、ecoliというデータセットを使用することにします。

import numpy as np
from imblearn.datasets import fetch_datasets

# データセットのダウンロード
datasets = fetch_datasets()
ecoli = datasets["ecoli"]
data_x = ecoli["data"]
data_y = ecoli["target"]

# 特徴量の確認
print(f"特徴量[0]: {data_x[0]}")
# => 特徴量[0]: [0.49 0.29 0.48 0.5  0.56 0.24 0.35]

# ターゲットクラスの個数
print(f"1: {np.sum(data_y == 1)}, -1: {np.sum(data_y == -1)}")
# => 1: 35, -1: 301

データセットの中身を確認すると、特徴量は7次元の数値データ、ターゲットクラスのラベルはおおよそ10:1であることが分かりました。

まずは、ベースラインを確認するために、データをそのまま使用して分類モデルを構築します。

from sklearn.linear_model import LogisticRegression
from sklearn.metrics import f1_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

# 訓練データとテストデータに分割
train_x, test_x, train_y, test_y = train_test_split(
    data_x,
    data_y,
    test_size=0.1,
    shuffle=True,
    random_state=22394
)
print(f"訓練データ: {len(train_x)}件, テストデータ: {len(test_x)}件")

# 分類モデルにはロジスティック回帰を使用
pipeline = Pipeline([
    ("preprocessing", StandardScaler()),
    ("classification", LogisticRegression()),
])
pipeline.fit(train_x, train_y)

# 訓練データとテストデータそれぞれで推論
pred_train_y = pipeline.predict(train_x)
pred_test_y = pipeline.predict(test_x)

# F1スコアで評価
f1_train = f1_score(train_y, pred_train_y, average='micro')
f1_test = f1_score(test_y, pred_test_y, average='micro')
print(f"訓練データ: {f1_train}, テストデータ: {f1_test}")
# => 訓練データ: 0.9304635761589404, テストデータ: 0.8529411764705882

F1スコアの結果を確認すると、テストデータに対する性能は、訓練データより 約0.08程度 悪化してるのが分かります。思ったほど悪くないですね…

次に、SMOTEを利用して改善するか確認していきます。そこで、imbalanced-learnに実装されてるSMOTEを確認すると、次のようなクラスがあります。今回は、特徴量がすべて数値データなので、もっとも基本的なSMOTEを使用していきます。

クラス概要得意分野
SMOTEもっとも基本的なSMOTEアルゴリズム全般
SMOTENCカテゴリカル変数と連続変数を両方扱う SMOTEカテゴリカル変数と連続変数の混合データ
SMOTENカテゴリカル変数専用の SMOTEカテゴリカル変数のみのデータ
ADASYN少数派クラスの密度を考慮した SMOTEクラス間の境界が曖昧なデータ
BorderlineSMOTEクラス境界付近の少数派データを重点的に生成クラス間の境界付近の少数派データの強化
KMeansSMOTEk-meansクラスタリングで少数派クラス内の分布を考慮した SMOTE少数派クラス内のクラスター構造を維持
SVMSMOTEサポートベクターマシン (SVM) を用いてクラス境界付近の少数派データを重点的に生成クラス境界付近の少数派データの強化 (特に高次元データ向け)

使い方は非常に簡単で、次のように実装すればOKです。

from imblearn.over_sampling import SMOTE

# SMOTEを使用して、少数ラベルのデータを増やす
smote = SMOTE(sampling_strategy="minority")
data_x, data_y = smote.fit_resample(data_x, data_y)

# 訓練データとテストデータに分割
train_x, test_x, train_y, test_y = train_test_split(
    data_x,
    data_y,
    test_size=0.1,
    shuffle=True
)
print(f"訓練データ: {len(train_x)}件, テストデータ: {len(test_x)}件")

# 分類モデルにはロジスティック回帰を使用
pipeline = Pipeline([
    ("preprocessing", StandardScaler()),
    ("classification", LogisticRegression()),
])
pipeline.fit(train_x, train_y)

# 訓練データとテストデータそれぞれで推論
pred_train_y = pipeline.predict(train_x)
pred_test_y = pipeline.predict(test_x)

# F1スコアで評価
f1_train = f1_score(train_y, pred_train_y, average='micro')
f1_test = f1_score(test_y, pred_test_y, average='micro')
print(f"訓練データ: {f1_train}, テストデータ: {f1_test}")
# => 訓練データ: 0.9094269870609981, テストデータ: 0.9180327868852459

結果を見ると分かりますが、テストデータに対する性能に改善が見られています(0.8529411764705882→0.9180327868852459)。過学習をうまく抑制できることが分かりました。

クラス不均衡データに対する機械学習モデルは、少数派クラスの予測精度が著しく低下してしまうか過学習対策が必須になりますが、上で見たようにimbalanced-learnを利用することで、比較的簡単に対策を打てます。今回紹介したSMOTE以外にも、機械学習モデル側で改善する手法など様々なアルゴリズムが実装されているので、APIリファレンスを参考に色々と試してみるのがいいかと思います。

More Information: arXiv:1106.1813, N. V. Chawla, K. W. Bowyer, L. O. Hall, W. P. Kegelmeyer, 「SMOTE: Synthetic Minority Over-sampling Technique」, https://arxiv.org/abs/1106.1813