表形式データ拡張手法 part9:SMOTETomek
基本情報
SMOTEの記事で説明した通り,幅広い分野で使われる表形式データにはクラス不均衡という大きな課題があります.それを解決するために使用されるのが表形式データ拡張であり,その代表格がSMOTEです.
しかし,SMOTEENNの記事でも述べたとおり,SMOTEには多数クラスデータ分布を考慮しないという欠点があり,それゆえに拡張データが多数クラスデータ領域に入り込んでしまったり,逆に元のデータにおいて多数クラスデータが少数クラスデータ領域に存在する場合に対する対処が困難であるという課題があります.この様に各クラス分布が明確に分かれておらず,歪んでしまっている状態は分類器の性能低下を引き起こします.
このような状況を解決するために,SMOTE等でデータを拡張した後に他のクラス領域に入り込んでいるサンプルを特定し,削除することがよく行われます.このようなオーバーサンプリング手法とアンダーサンプリング手法を組み合わせた手法をハイブリッド手法と呼びます.ハイブリッド手法の代表格は以前に紹介したSMOTEENNと,本記事で紹介するSMOTETomekです.
SMOTETomekでは拡張後のデータセットからTomek linksと呼ばれるペアを探索し,それを削除することで他クラスデータ領域に入り込んでいるノイズサンプルを削除します.まずTomek linksについて説明を行い,その後にSMOTETomekのアルゴリズムの紹介,そして実装を示します.
Tomek linksとは
Tomek linksとは,簡単に言えば異なるクラスに属しているのに,互いにとって最も近い位置にあるサンプルのペアのことです.具体的には,2つのサンプル
となる
このTomek linksとはすなわち他のサンプルよりも近い位置にありながら異なるクラスに属する2つのサンプルのペアということです.異なるクラスのサンプル同士が近いという状況はすなわち,Tomek linksに属するサンプルは決定境界のボーダーライン近くにあるか,それとも他クラス領域の奥に入り込んでしまったサンプルだと言えます.これらを削除することで分類器の性能を向上させることを目的とします.
アルゴリズムの紹介
Tomek linksの概念さえ理解してしまえば,SMOTETomekの動作は非常に簡潔です.以下の3ステップで動作します.
-
SMOTEによるデータ拡張
少数クラスデータに対し,SMOTEを用いてデータ拡張を行います.SMOTEに関してはSMOTEの記事をご参照下さい. -
Tomek linksの特定
拡張されたデータに対し,Tomek linksを形成するペアを探索します. -
Tomek linksの削除
2で探索されたTomek linksに属するサンプルを削除します.ここではTomek linksを形成する多数クラスサンプルおよび少数クラスサンプルの両方を削除します.
なお,SMOTETomekの原論文ではSMOTEでデータの不均衡を解消した後でTomek linksを用いたアンダーサンプリングを実行するため,Tomek linksを形成する2つのサンプルを両方削除しています.これは両方削除しないとデータ数に偏りが出るためです.
しかし,Tomek linksを単体でアンダーサンプリング手法として実行する場合,Tomek linksを形成するペアのうち多数クラスサンプルのみを削除して不均衡を解消しようとする場合もあります.どちらが良いかは使用される段階でデータ不均衡がどうなっているかを考慮することが必要になります.
Pythonによる使用例
以下にimbalanced-learnを用いた実装例を示します.なお,コードはGeminiを用いて出力したものです.
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
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
from imblearn.combine import SMOTEENN, SMOTETomek
# 境界付近に重なりが生じるように不均衡データを生成
X, y = make_classification(
n_samples=5000,
n_features=2,
n_informative=2,
n_redundant=0,
n_clusters_per_class=1,
weights=[0.99, 0.01],
class_sep=0.5,
flip_y=0.0,
random_state=0
)
# 訓練データとテストデータに分割
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)
X_train_smote, y_train_smote = smote.fit_resample(X_train, y_train)
# SMOTEENNの適用
smote_enn = SMOTEENN(random_state=42)
X_train_sme, y_train_sme = smote_enn.fit_resample(X_train, y_train)
# SMOTETomekの適用
smote_tomek = SMOTETomek(random_state=42)
X_train_smt, y_train_smt = smote_tomek.fit_resample(X_train, y_train)
# プロット領域の作成
# グラフを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()
# SMOTEENN適用後のプロット
plt.subplot(1, 4, 3)
plt.scatter(X_train_sme[y_train_sme == 0][:, 0], X_train_sme[y_train_sme == 0][:, 1], label='Majority', alpha=0.5)
plt.scatter(X_train_sme[y_train_sme == 1][:, 0], X_train_sme[y_train_sme == 1][:, 1], label='Minority', alpha=0.7)
plt.title('After SMOTEENN', fontsize=FONT_SIZE)
plt.legend()
# SMOTETomek適用後のプロット
plt.subplot(1, 4, 4)
plt.scatter(X_train_smt[y_train_smt == 0][:, 0], X_train_smt[y_train_smt == 0][:, 1], label='Majority', alpha=0.5)
plt.scatter(X_train_smt[y_train_smt == 1][:, 0], X_train_smt[y_train_smt == 1][:, 1], label='Minority', alpha=0.7)
plt.title('After SMOTETomek', 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)
# SMOTEENN適用後の決定木モデルの学習と評価
clf_sme = DecisionTreeClassifier(random_state=42)
clf_sme.fit(X_train_sme, y_train_sme)
y_pred_sme = clf_sme.predict(X_test)
f1_sme = f1_score(y_test, y_pred_sme)
# SMOTETomek適用後の決定木モデルの学習と評価
clf_smt = DecisionTreeClassifier(random_state=42)
clf_smt.fit(X_train_smt, y_train_smt)
y_pred_smt = clf_smt.predict(X_test)
f1_smt = f1_score(y_test, y_pred_smt)
# 結果の出力
print(f"Original F1: {f1_original:.4f}")
print(f"SMOTE F1: {f1_smote:.4f}")
print(f"SMOTEENN F1: {f1_sme:.4f}")
print(f"SMOTETomek F1: {f1_smt:.4f}")
今回は拡張手法の代表格であるSMOTEとハイブリッド手法の代表格であるSMOTEENNを,本記事で紹介したSMOTETomekと比較します.得られた図は以下のとおりです.

各手法によるデータ分布の比較.左から元データ,SMOTE拡張後データ,SMOTEENN後データ,SMOTETomek後データ
多数クラスデータの領域と少数クラスデータの領域が重なっている,非常に分類が困難なタスクです.実際,拡張なしの場合のF1-Scoreの値は0.0741とあまり分類できていません.
その中でSMOTEENNを見ると多数クラスデータと少数クラスデータが重なり合う領域で多数クラスサンプルが削り取られているような動作が見えます.それに対し,SMOTETomekの方ではあまりそのような効果が見られませんでした.これはTomek linksにおいて考慮されるのが最近傍の1点のみである事が要因であると考えられます.
SMOTEENNでは近傍の3点を用いて削除の判断を行います.そのため,ある程度ロバストに判断ができますが,SMOTETomekでは最近傍の1点のみを考慮するため,たまたま1番近くにあるのが同じクラスのデータの場合は削除が行われません.すなわちボーダーライン近くであっても,もし同じクラスのデータが近くに密集しているのであれば必然的に削除されないことになります.そのためボーダーラインデータやノイズデータを削除しきれていないという結果につながっていると考えられます.
実際,削除されたデータ数はSMOTEENNが198なのに対し,SMOTETomekでは36と非常に少なくなっていました.これらのことから,ハイブリッド手法を使用する際は基本的にはSMOTEENNを使うことが推奨されます.
引用
Batista, Gustavo EAPA, Ana LC Bazzan, and Maria Carolina Monard. "Balancing training data for automated annotation of keywords: a case study." Wob 3 (2003): 10-18.
Discussion