表形式データ拡張手法 part12:MWMOTE
TL; DR
- MWMOTEはSMOTEで発生する多数クラスデータ領域の侵害を防ぐためのクラスターベースの手法である
-
-nnを3回実行することによって決定境界に近いデータを特定し,それを基にデータの拡張を行うk - 多数クラスデータにどれだけ近いかなどを考慮に入れて,各少数クラスサンプルの選択確率を重み付けし,更にクラスタリングも組み合わせることで境界付近でありながら安全なデータ生成を実現する
基本情報
SMOTEの記事で説明した通り,幅広い分野で使われる表形式データにはクラス不均衡という大きな課題があります.それを解決するために使用されるのが表形式データ拡張であり,その代表格がSMOTEです.
そのシンプルさと強力さから,SMOTEには多くの変種があります.実際,自分がこれまで説明してきた12手法のうち,ROSEを除いた11手法がSMOTEをベースにしたものです(何故かROSEがSMOTEベースの手法と紹介している論文がありますが,これは明らかに間違いです).それらは主にSMOTEの欠点を解消することを目的として設計されています.
各手法の工夫点は各手法ごとの記事を読んでいただくとして,それらの手法の改善の方向性としてよく挙げられるものとして多数クラス領域の侵害を回避し,分類器への悪影響を軽減すること があります.これはSMOTEが多数クラス分布を考慮に入れないために,多数クラス領域にデータを生成してしまう可能性があることに対する改善です.例えばSafe-Level-SMOTEでは周辺の多数クラスデータの個数に応じて,安全な領域にデータを生成するというアプローチをとっています.
本記事で紹介するMajority Weighted Minority Oversampling TEchnique (MWMOTE) もそのような目的で設計されている手法です.著者らはBorderline-SMOTEやSafe-Level-SMOTEのような
これからMWMOTEの説明を始めるわけですが,事前に注意喚起しておきます.それは本手法はかなり複雑なので,初めて表形式データ拡張を行うような人に対してはいきなりこの手法を理解しようとすることはおすすめしないということです.なるべくわかりやすく説明するように心がけますが,著作権の問題で論文の図を引用していいのかいまいちわからない以上,文章ベースの説明になってしまいます.もし原論文を読める環境であればそちらの図を見ながらこちらを読むことをおすすめします.
なお,書いていて流石に長過ぎるなと思ったのでTL; DRの章を入れておきます.全部読むのが面倒な人はそちらを見て下さい.
MWMOTEのアルゴリズムの紹介
アルゴリズムを紹介するわけですが,非常に長いため,一旦全体像をお見せしてから各ステップの内容について詳しく説明するような形にしたいと思います.その際,意識しておいてもらいたいことは,MWMOTEはBorderline-SMOTEと同様に決定境界に近い点ほど分類に重要であるため,そこを重点的にオーバーサンプリングする という考えのもとで設計されているという点です.この分野のみならず,手法の理解をするときには作成者がどのような思想のもと設計したのかを考慮したほうがわかりやすくなると思います.それでは始めます.
全体の流れ
初めに使用する用語について説明しておきます.
-
少数クラスデータに対する
-nnk
各少数クラスデータに対して,ユークリッド距離を用いてすべてのデータの中で 個の近傍点を探索する.この時,中心となる少数クラスデータをk_1 として,その近傍点をx_i とする.この時,NN(x_i) には少数クラスデータと多数クラスデータが含まれる.NN(x_i) -
少数クラスデータのフィルタリング
少数クラスデータ のうち,近傍点x_i に少数クラスデータが含まれないものをノイズデータとして削除した,フィルタリング後少数クラスデータ集合NN(x_i) を構築する.S_{minf} -
多数クラスデータ近傍点の探索
に属する各少数クラスデータS_{minf} に対して,多数クラスデータの中でx_i 個の近傍点を探索する.これをk_2 とする.この集合には多数クラスデータのみが含まれる.N_{maj}(x_i) -
ボーダーライン多数クラスデータ集合の構築
に含まれる多数クラスデータを結合し,ボーダーライン多数クラスデータ集合N_{maj}(x_i) を構築する.S_{bmaj} -
ボーダーライン少数クラスデータの探索
に含まれる多数クラスデータS_{bmaj} に対して,少数クラスデータの中でy_i 個の近傍点を探索する.これをk_3 とする.この集合には少数クラスデータのみが含まれる.N_{min}(y_i) -
ボーダーライン少数クラスデータ集合の構築
に含まれる少数クラスデータを結合し,ボーダーライン少数クラスデータ集合N_{min}(y_i) を構築する.S_{imin} -
少数クラスデータに対する情報重みの計算
に含まれる各多数クラスデータS_{bmaj} および,y_i に含まれる各少数クラスデータS_{imin} に対し,情報重みx_i を計算する.なお,この重みの定義は後ほど説明する.I_w(y_i, x_i) -
少数クラスデータに対する選択重みの計算
7で計算した情報重み を用いて,各少数クラスデータに対する選択重みI_w(y_i, x_i) をS_w (x_i)
S_w(x_i) = \sum_{y_i \in S_{bmaj}} I_w(y_i, x_i)
のように計算する. -
少数クラスデータに対する選択確率の計算
先ほど計算した選択重み を用いて選択確率S_w (x_i) をS_p(x_i)
S_p(x_i) = S_w(x_i) / \sum_{z_i \in S_{imin}} S_w(z_i)
のように計算する. -
少数クラスデータのクラスタリング
少数クラスデータ集合 (少数クラスデータ全てであって,フィルタリング後の集合S_{min} ではないことに注意)をクラスタリングする.S_{minf} -
拡張データの合成
少数クラスデータ選択確率 に基づいて少数クラスサンプルS_p(x_i) をx から選択する.その少数クラスサンプルが属するクラスターから自分以外のサンプルを選択し,SMOTEのように2点間でデータを生成する.なお,クラスターにデータが1つのみしかない場合は自分自身を複製する(ランダムオーバーサンプリング).S_{imin}
どうでしょうか.原論文に合わせてステップをかなり細切れにして説明しているため,若干わかりにくいかも知れません.また,これまでに説明してきた手法と比べるとそもそも操作がかなり多いのも特徴です.ここからは動作を大まかに3段階に分割し,各段階での意図を含めて詳しく説明していきます.
ステップ1~6:S_{imin} の構築
MWMOTEの前半部分では
さて,決定境界に近いサンプルというのはどの様に特定すればよいでしょうか.2次元ならばプロットして目視で特定できますが,高次元になると不可能です.決定境界に近いサンプルを用いた拡張データを生成するというMWMOTEと似たようなアプローチを取るBorderline-SMOTEでは,少数クラスデータの近傍点を探索し,そのうち多数クラスデータがいくつあるかということを基に特定します.このアプローチは妥当性があるように思います.周辺に多数クラスデータがある程度あるというのは,複数のクラスのデータが混在している状態であると考えられます.ではMWMOTEは何故このアプローチを採用しなかったのでしょうか.
答えはシンプルで,著者らは
ただし,どのようなデータでもいいわけではありません.例えば完全に多数クラスデータに囲まれている少数クラスデータは多数クラスデータから見た近傍点に選ばれる可能性が高いですが,実際にはこれはノイズデータである可能性があります.また,少数クラスデータから非常に離れた多数クラスデータから少数クラスデータ近傍点を探索したとしても,その多数クラスデータが分類に影響を及ぼす可能性は低いです.つまり,決定境界に近い多数クラスデータから見た,ノイズデータを除いた少数クラスデータ近傍点こそが決定境界に近い少数クラスデータである と著者らは主張しているわけです.
つまり,流れとしてはノイズデータの削除→決定境界に近い多数クラスデータの決定→決定境界に近い少数クラスデータの特定,と進みます.アルゴリズムのステップ1~6を眺めてもらえば確かにそのような流れになっています.まず多数クラスデータに囲まれている少数クラスデータをノイズデータとして削除し,残った少数クラスデータに近い多数クラスデータを決定境界に近い多数クラスデータとし,最後にそれらから近い少数クラスデータを決定境界に近い少数クラスデータであるとしています.
ステップ7~9:選択確率S_p(x_i) の計算
さて,前半部分で分類に重要であると考えられる決定境界に近い少数クラスデータを特定しました.しかし,筆者らはこれを用いて単純にオーバーサンプリングするのではなく,これらのデータの中でも重要度は異なるよね,と主張しています.その重要度は以下の3つの要素によって定められます.
- 決定境界からの近さ(近いほうが重要で,遠いほうが重要ではない)
- 少数クラスサンプルの属するクラスターの密度(疎のほうが重要で,密のほうが重要ではない)
- 少数クラスサンプルに近い多数クラスデータ周辺の密度(密のほうが重要で,疎のほうが重要ではない)
1はこの手法の根幹を担う決定境界に近い少数クラスデータこそが重要であるという思想に沿っています.2に関しては,元々密なクラスターでは十分な情報量があると判断し,疎なクラスターに多くサンプルを生成することで情報量を補填しようという考えです.3に関しては,近くの多数クラスの密度が高いと決定境界が押されてしまう可能性があるため,それを防ごうという考えです.これらの考えに基づいて各少数クラスサンプルに重みを付けていきます.
さて,具体的な式の説明に移るわけですが,正直この部分は理解しなくても良いと思います.あくまで上の3つの要素に基づいて少数クラスサンプルに重みが割り当てられるということさえ理解していれば何の問題もありません.本記事の目的は手法解説なので,式自体は説明しますが,あくまで思想の実現手段として設定されていると思えば十分です.
この重みに関してですが,まず初めに決定境界に近い多数クラスデータ集合
です.近接係数は上記の要素の内1の決定境界からの近さを反映し,密度係数は2の少数クラスサンプルクラスターの密度を反映します.まず,近接係数
ここでカットオフ関数
次に密度係数
これはどういうことかということを説明します.まず注意すべきことは,
分母の部分は多数クラスサンプルを
それに対し,
こうして計算された
この様に各サンプルに対する選択重み
ステップ11~12:拡張データの生成
選択確率をもとめたのでいよいよ拡張データを生成するわけですが,単純に選択して近くのサンプルとの間に生成するわけではありません.一番初めに少数クラスデータ全体(フィルタリング前)をクラスタリングします.ここでのクラスタリングにはaverage-linkage agglomerative clusteringというクラスタリング手法を使うようです.このクラスタリング手法はMWMOTEの本質とは関係がないため詳しい説明は省きますが,近くの距離にあるサンプルをどんどん結合していくようなクラスタリングだそうです.
クラスタリングが終わった後,先程計算した選択確率に基づいて
もしクラスターに属しているデータが1つのみの場合はそのデータの複製が行われます.この事によって多数クラスデータ領域への侵害を防いでいます.
Pythonによる使用例
以下にDBSMOTEを用いたPythonコードを示します.なお,このコードはGeminiを用いて生成したものです.同じクラスターベースの手法であるK-means SMOTEと並列に並べています.
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
import smote_variants
# 合計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)
print(len(X_train), len(X_test))
# SMOTEの適用
smote = SMOTE(random_state=42, k_neighbors=20)
X_train_smote, y_train_smote = smote.fit_resample(X_train, y_train)
print(len(X_train_smote))
# 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)
print(len(X_train_kmeans))
# MWMOTEの適用
mwmote = smote_variants.MWMOTE(random_state=42)
X_train_mwmote, y_train_mwmote = mwmote.sample(X_train, y_train)
print(len(X_train_mwmote))
# プロット領域の作成
# グラフを4つ並べるため幅を変更
plt.figure(figsize=(24, 6))
FONT_SIZE = 25
# オーバーサンプリング前のプロット
plt.subplot(1, 4, 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', fontsize=FONT_SIZE)
plt.legend()
# SMOTE適用後のプロット
plt.subplot(1, 4, 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', fontsize=FONT_SIZE)
plt.legend()
# KMeans SMOTE適用後のプロット
plt.subplot(1, 4, 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', fontsize=FONT_SIZE)
plt.legend()
# MWMOTE適用後のプロット
plt.subplot(1, 4, 4)
plt.scatter(X_train_mwmote[y_train_mwmote == 0][:, 0], X_train_mwmote[y_train_mwmote == 0][:, 1], label='Majority', alpha=0.5)
plt.scatter(X_train_mwmote[y_train_mwmote == 1][:, 0], X_train_mwmote[y_train_mwmote == 1][:, 1], label='Minority', alpha=0.7)
plt.title('After MWMOTE', fontsize=FONT_SIZE)
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)
# MWMOTE適用後の決定木モデルの学習と評価
clf_mwmote = DecisionTreeClassifier(random_state=42)
clf_mwmote.fit(X_train_mwmote, y_train_mwmote)
y_pred_mwmote = clf_mwmote.predict(X_test)
f1_mwmote = f1_score(y_test, y_pred_mwmote)
# 結果の出力
print(f"Original F1: {f1_original:.4f}")
print(f"SMOTE F1: {f1_smote:.4f}")
print(f"KMeans-SMOTE F1: {f1_kmeans:.4f}")
print(f"MWMOTE F1: {f1_mwmote:.4f}")
以下が実行結果の図になります.

各手法によるデータ分布の比較.左から元データ,SMOTE拡張後データ,K-means SMOTE後データ,MWMOTE後データ
図を見ると同じクラスターベースでもだいぶ違う位置にデータが生成されています.これはおそらくMWMOTEでのクラスタリングがK-means SMOTEによるクラスタリングと比べて非常に細かくなっていることが理由であると考えられます.なお,F1-Scoreの比較は
Original F1: 0.5455
SMOTE F1: 0.3200
KMeans-SMOTE F1: 0.6667
MWMOTE F1: 0.6667
となっています.
引用
Barua, Sukarna, et al. "MWMOTE--majority weighted minority oversampling technique for imbalanced data set learning." IEEE Transactions on knowledge and data engineering 26.2 (2012): 405-425.
Discussion