💡

表形式データ拡張手法:K-means SMOTE

に公開

基本情報

SMOTEの記事で説明した通り,幅広い分野で使われる表形式データにはクラス不均衡という大きな課題があります.それを解決するために使用されるのが表形式データ拡張であり,その代表格がSMOTEです.

しかし,SMOTEは選択した二点間の内分点を拡張データとするという非常に単純なアルゴリズムであり,多数クラス分布を考慮していないという欠点があります.これは多数クラス分布から見てより有用な領域にデータを生成できないだけでなく,拡張データが多数クラスデータ分布の中に生成されてしまい,結果として分類器の性能に悪影響を及ぼしてしまう可能性があります.
これは例えば2つの少数クラスクラスター間に多数クラスデータが多くあるような状況や,あるいは多数クラスデータ内に少数クラスデータがぽつんとあるような状況で起こります(このような孤立している少数クラスデータや多数クラスデータ内に生成された拡張データのことをよくノイズデータと呼びます.正しいデータである場合とラベル付けが誤っている場合が考えられます).

このような課題に対し,クラスタリングアルゴリズムを用いてノイズデータの生成を回避することを目的とした手法が本記事で紹介するK-means SMOTE です.名前のとおり,k-meansを用いてクラスタリングを行った後にSMOTEを行うという手法になります.

なお,この様にクラスタリング手法を用いる拡張手法のことをクラスターベースの拡張手法と呼ぶことがあります.正確に数えたことがあるわけではないですが,クラスターベースの拡張手法はそれなりにある印象です.その中の代表手法がK-meansSMOTEになります.

アルゴリズムの紹介

例のごとく,生成ステップはほとんどSMOTEと同じです.そのため,前段のk-meansを用いた処理を中心に説明を行います.

  1. k-meansによるクラスタリング
    まず初めに全てのデータをk-meansを用いてクラスタリングします.なお,本記事はあくまでK-means SMOTEの手法説明のためのものであるため,k-meansの詳しいアルゴリズムの説明は省きます.非常に単純化するならばデータをいくつかのクラスターに分割するアルゴリズムであり,それ以上の知識はK-means SMOTEの理解には必要ありません.

  2. クラス不均衡度の計算
    ステップ1で分離した各クラスターに対し,それぞれのデータのうち少数クラスデータがどの程度含まれているかを調べます.ここであるクラスターiにおける多数クラスデータの数をn_{i, maj}とし,少数クラスデータの数をn_{i, min}とすると不均衡度irtを以下のように定義します.
    irt = \dfrac{n_{i, min} + 1}{n_{i, maj} + 1}
    この値が高いほどそのクラスターには少数クラスデータの割合が高いということになります.なお,原論文ではirtの分母と分子が逆で記載されていますが,説明を読む限りでは分母が多数クラスデータで分子が少数クラスデータになる方が正しいと思われます.なお,論文内の図では自分が示した式が採用されています.ただし分母分子における+1は省かれています.この部分はおそらく0除算を考慮しているだけだと思われます.

  3. クラス不均衡度を用いたフィルタリング
    先ほど計算したクラス不均衡度を用いて,次以降の処理に使用するクラスターを決定します.気持ち的には不均衡度が高いほど生成が安全なクラスターで,低いほど危険なクラスターであると言えます.ここで事前に定義した閾値(デフォルトは1)を用いて,それ以上の不均衡度を持つクラスターのみを選択し,それ以外のクラスターは次のステップ以降では省かれます.閾値を高くすればより安全なクラスターのみを選択することになり,低くすると基準がゆるくなることになります.

  4. クラスターごとの生成数の決定
    3で選択された各クラスターは同数ずつのデータを生成するのではなく,密度を考慮してそれぞれに別々の生成数を設定します.ここでクラスターに含まれる少数クラスデータ間のユークリッド距離(多数クラスデータは考慮しないことに注意)を計算し,その平均をdとします.また,そのクラスター内に存在する少数クラスデータ数をn,データに含まれる特徴量の数をmとした時,そのクラスターの少数クラスデータの密度density
    density = \dfrac{n}{d^m}
    と計算されます.この密度の逆数に比例するように各クラスターの生成数を設定します.なお,m乗しているのは,平均距離dを一辺とするm次元空間での体積を表していると考えられます.つまりクラスターiにおける生成数は,密度をdensity_iとし,必要とする拡張データの合計数をNとすると
    \dfrac{1/density_i}{\sum_i 1/density_i} * N
    となります.これはそもそも密度が高い領域にはデータを生成する必要がなく,それに対し密度が低い領域はより多くのデータを生成したいという考えに基づいています.

  5. クラスター内でのデータの生成
    先ほど計算したクラスターごとの生成数に基づいて拡張データを生成します.このアルゴリズム自体はSMOTEと同様であるため,詳しくは省略しますが,クラスター内で近傍点を選択し,二点間でデータを生成します.

なお,本手法におけるユニークな点は,クラスタリングにおいて多数クラスデータも考慮することと,クラスターごとに生成数を設定することのようです.

K-means SMOTEの欠点

クラスタリングにおいてk-meansを使用しますが,このクラスター数kは非常に重要なハイパーパラメータであると考えられます.この値が適切でないと上手くクラスタリングできず,結果として性能が下がってしまう可能性が考えられます.

また,データが非常に偏っている場合,そもそもどのクラスターも不均衡度の閾値に達しない可能性があります.その場合閾値を変更すればよいですが,閾値の値の設定は困難であると考えられます.

その他,他手法に比べると若干保守的な手法になっています.なので,手法としてはSafe-Level-SMOTEと考え方が似ていると言えるでしょう.もちろんこの様に保守的な方が良いのか,あるいはもっと決定境界に近くなるように生成するべきかはデータによると考えられます.

Pythonを用いた使用例

以下にPythonを用いた実装を示します.

import matplotlib.pyplot as plt
import numpy as np
from sklearn.datasets import make_blobs
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import f1_score
from imblearn.over_sampling import SMOTE, KMeansSMOTE

# 合計3つのクラスタを生成
centers = [[0, 0], [3, 3], [-3, 3]]
n_samples = [2970, 15, 15]
cluster_std = [1.2, 0.4, 0.4]

X, y_temp = make_blobs(n_samples=n_samples, centers=centers, cluster_std=cluster_std, random_state=42)

# ラベルの再割り当て
y = np.where(y_temp == 0, 0, 1)

# 訓練データとテストデータに分割
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# SMOTEの適用
smote = SMOTE(random_state=42, k_neighbors=20)
X_train_smote, y_train_smote = smote.fit_resample(X_train, y_train)

# KMeans SMOTEの適用
kmeans_smote = KMeansSMOTE(random_state=42, k_neighbors=2, cluster_balance_threshold=0.01)
X_train_kmeans, y_train_kmeans = kmeans_smote.fit_resample(X_train, y_train)

# プロット領域の作成
plt.figure(figsize=(18, 6))

# オーバーサンプリング前のプロット
plt.subplot(1, 3, 1)
plt.scatter(X_train[y_train == 0][:, 0], X_train[y_train == 0][:, 1], label='Majority', alpha=0.5)
plt.scatter(X_train[y_train == 1][:, 0], X_train[y_train == 1][:, 1], label='Minority', alpha=0.7)
plt.title('Before Oversampling')
plt.legend()

# SMOTE適用後のプロット
plt.subplot(1, 3, 2)
plt.scatter(X_train_smote[y_train_smote == 0][:, 0], X_train_smote[y_train_smote == 0][:, 1], label='Majority', alpha=0.5)
plt.scatter(X_train_smote[y_train_smote == 1][:, 0], X_train_smote[y_train_smote == 1][:, 1], label='Minority', alpha=0.7)
plt.title('After SMOTE')
plt.legend()

# KMeans SMOTE適用後のプロット
plt.subplot(1, 3, 3)
plt.scatter(X_train_kmeans[y_train_kmeans == 0][:, 0], X_train_kmeans[y_train_kmeans == 0][:, 1], label='Majority', alpha=0.5)
plt.scatter(X_train_kmeans[y_train_kmeans == 1][:, 0], X_train_kmeans[y_train_kmeans == 1][:, 1], label='Minority', alpha=0.7)
plt.title('After KMeans-SMOTE')
plt.legend()
plt.tight_layout()

# 画像の保存
plt.savefig('oversampling_comparison.png')

# 適用前の決定木モデルの学習と評価
clf_original = DecisionTreeClassifier(random_state=42)
clf_original.fit(X_train, y_train)
y_pred_original = clf_original.predict(X_test)
f1_original = f1_score(y_test, y_pred_original)

# SMOTE適用後の決定木モデルの学習と評価
clf_smote = DecisionTreeClassifier(random_state=42)
clf_smote.fit(X_train_smote, y_train_smote)
y_pred_smote = clf_smote.predict(X_test)
f1_smote = f1_score(y_test, y_pred_smote)

# KMeans SMOTE適用後の決定木モデルの学習と評価
clf_kmeans = DecisionTreeClassifier(random_state=42)
clf_kmeans.fit(X_train_kmeans, y_train_kmeans)
y_pred_kmeans = clf_kmeans.predict(X_test)
f1_kmeans = f1_score(y_test, y_pred_kmeans)

# 結果の出力
print(f"Original F1: {f1_original:.4f}")
print(f"SMOTE F1:    {f1_smote:.4f}")
print(f"KMeans-SMOTE F1: {f1_kmeans:.4f}")

なお,いつもはmake_classificationを使用していますが,今回はクラスターを無理やり作り出すために別関数を使用しています.また,わかりやすさのためSMOTEの探索近傍点数を増やしています.以下が実行結果の図になります.

見ていただければ分かる通り,SMOTEではクラスター間にデータを生成することで決定境界の侵害を引き起こしているのに対し,K-means SMOTEではクラスター内にのみデータが生成されることで決定境界の侵害を防げていることがわかります.

また,F1-Scoreは拡張なしが0.5455,SMOTE後が0.3200,K-means SMOTE後が0.6667となり,決定境界を侵害しないことで性能を向上させていることが見て取れます.

引用

Douzas, Georgios, Fernando Bacao, and Felix Last. "Improving imbalanced learning through a heuristic oversampling method based on k-means and SMOTE." Information sciences 465 (2018): 1-20.

Discussion