雑記 in hibernation

頭の整理と備忘録

2値分類の不均一データ対策って実際効果あるんかい

機械学習の2値分類問題において、不均一(=陰性・陽性のデータ比率に偏りがある)データを学習させる際に学習用のセットの陰性・陽性のデータ比率をある程度揃えてあげることでモデル精度が向上することが知られています。このアイデアをもとにオーバーサンプリング(マイノリティの陽性データを水増しする)やアンダーサンプリング(マイノリティの陽性データに合わせてマジョリティの陰性データを間引きする)といった手法が取られるわけですが、そもそも「データを均一にすることで本当に精度が向上するんかね」ってのが疑問だったので、原始的なアンダーサンプリングをちゃちゃっと実装して本当に精度が上がるのかを確認してみました。

1. 条件

クレジットカードのトランザクションデータから不正検知を行うタスクを題材とします(データはこちら)。そもそもクレカの不正利用はそうそう頻繁に起きるものではありません。したがって、トランザクションは陰性:陽性の比率、つまり「不正でないトランザクション」と「不正なトランザクション」の比率において前者が多数を占める不均一データであることが予想されます。

不均一データのラベル予測を行う2値分類モデルを構築するにあたり、不均一データを元データと同等の陰性・陽性比率のデータセットで学習を行った場合と、アンダーサンプリングで両者のサンプルサイズを揃えたデータセットで学習を行った場合とで、モデルの精度を比較します。

2. 実装

早速、モデルを実装して実験してみます。


2.1. 準備

必要なライブラリを読みこみます。

import pandas as pd
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn import metrics
import matplotlib.pyplot as plt


データセットも読みこみます

credit = pd.read_csv("/creditcard.csv")
credit
Time V1 V2 ... V27 V28 Amount Class
0 0 -1.359807 -0.072781 ... 0.133558 -0.021053 149.62 0
1 0 1.191857 0.266151 ... -0.008983 0.014724 2.69 0
2 1 -1.358354 -1.340163 ... -0.055353 -0.059752 378.66 0
3 1 -0.966272 -0.185226 ... 0.062723 0.061458 123.5 0
4 2 -1.158233 0.877737 ... 0.219422 0.215153 69.99 0
... ... ... ... ... ... ... ... ...
284802 172786 -11.881118 10.071785 ... 0.943651 0.823731 0.77 0
284803 172787 -0.732789 -0.05508 ... 0.068472 -0.053527 24.79 0
284804 172788 1.919565 -0.301254 ... 0.004455 -0.026561 67.88 0
284805 172788 -0.24044 0.530483 ... 0.108821 0.104533 10 0
284806 172792 -0.533413 -0.189733 ... -0.002415 0.013649 217 0


ここで、目的変数の0/1の比率も確認しておきます。

# 0/1比率を確認
num_0 = credit[credit["Class"] == 0 ].count()["Class"]
num_1 = credit[credit["Class"] == 1 ].count()["Class"]
rate = num_1 / num_0
print("0 : " , num_0, " / 1 : " , num_1 , " / rate :", rate )
0 :  284315  / 1 :  492  / rate : 0.0017304750013189597


陽性の割合は0.17%くらい。案の定、偏りまくってます。


2.2. アンダーサンプリング

テストデータと訓練データを分けたのち、訓練データからアンダーサンプリングして陰性・陽性のサンプルサイズを揃えた訓練データセットを作成します。

なお今回実施するアンダーサンプリングは、陽性データと同数の陰性データを無作為抽出するという至極単純な方法です。

# testデータを確保する
X = credit.drop(["Time", "Class"] ,axis=1)
y = credit["Class"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0 , stratify=y)

# アンダーサンプリングで均一データを作成する
train = pd.concat([X_train, y_train] , axis=1) # 一度特徴量と目的変数を一つのデータセットにまとめる
train_1 = train[train["Class"]==1] # 陽性のデータを抽出
num_1 = train_1.shape[0] # 陽性のデータ数を取得
train_0 = train[train["Class"]==0].sample(n=num_1, random_state=0) # 陽性のデータ数と同数だけ陰性データを無作為抽出
train_adjust = pd.concat([train_0, train_1]) # 陽性データと陰性データをマージ
X_train_adjust = train_adjust.drop(["Class"], axis=1) 
y_train_adjust = train_adjust["Class"]


2.3. 学習

陰性・陽性の比率が不均一な訓練データと、アンダーサンプリングで均一にした訓練データのそれぞれでモデルを学習させます。 なおモデルはランダムフォレストを選びましたが、その理由は、なんとなくです。

print("不均一データ")
rfc_unevenness = RandomForestClassifier(max_depth=10, random_state=0)
rfc_unevenness.fit(X_train, y_train)
print("score = train : ", rfc_unevenness.score(X_train, y_train), " / test : ", rfc_unevenness.score(X_test, y_test))

print("均一データ")
rfc_adjust = RandomForestClassifier(max_depth=10, random_state=0)
rfc_adjust.fit(X_train_adjust, y_train_adjust)
print("score = train : ", rfc_adjust.score(X_train_adjust, y_train_adjust), " / test : ", rfc_adjust.score(X_test, y_test))
不均一データ
score = train :  0.999749202463835  / test :  0.9994382219725431
均一データ
score = train :  0.9927325581395349  / test :  0.9699097644043397


ちなみに今回のケースでは、ランダムフォレストをデフォルトパラメータで学習した場合に学習セットに対するスコアが1.0になります。つまり死ぬほど過学習します。そこで、少なくともスコアが1.0にはならんように適当な最大深度をパラメータとして与えています(それでも過学習気味ではありますが)。


2.4. 精度評価

正答率ベースのスコアは、今回の不均一データに対しての指標としては適切とは言えません(データの偏りと評価指標についてはこの記事でちょっと触れています)。

そこで、今回はAR値(=2AUC-1)によりモデル精度を評価します。AUCでなくARを基準とする理由は、今回の実験ではおそらくAUC=0.9近い精度同士の僅差の比較になることが予想され、より両者の絶対値の差分が大きく出る基準の方が直感的に比較しやすいかと思ったからです。

# 予測確率の取得
proba_unevenness = rfc_unevenness.predict_proba(X_test)[:,1]
proba_adjust = rfc_adjust.predict_proba(X_test)[:,1]

# FPR, TPR(, しきい値) を算出
fpr_unevenness, tpr_unevenness, thresholds = metrics.roc_curve(y_test, proba_unevenness)
fpr_adjust, tpr_adjust, thresholds = metrics.roc_curve(y_test, proba_adjust)

# AR値(=2AUC-1)の計算
print("AR value")
print(" unevenness : " , 2*metrics.auc(fpr_unevenness, tpr_unevenness)-1)
print(" adjust : " , 2*metrics.auc(fpr_adjust, tpr_adjust)-1)
AR value
 unevenness :  0.9455501019514152
 adjust :  0.958170134493483


案の定僅差ではありますが、均一化によりざっくり1.2%の精度向上が見られました。


一応、ROCを可視化しておきます。

plt.plot(fpr_unevenness, tpr_unevenness, label='unevenness')
plt.plot(fpr_adjust, tpr_adjust, label='adjust')
plt.legend()
plt.title('ROC curve')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.grid(True)

f:id:toeming:20210305143851p:plain


ROCにおいても若干の精度向上が確認できます。

3. 結論

「データを均一にすることで本当に精度が向上するんかね」という疑問に対しては、「向上する」が答えのようです。今回実施したアンダーサンプリングは手法としてはかなり原始的なので、他の手法であればより差が顕著に出るかもしれません。

また、サンプリングにより相当数の学習サンプルを削ってしまったわけですが、精度に対してはあまり影響ない(というか、均一化の効果が上回る)と言えそうな結果が得られました。データは量より分布なんだなあと小並感を抱いた次第です。

ちなみに補足ですが、デフォルトパラメータのランダムフォレストで激しく過学習が発生した状態では、テストセットに対する正答率は0.99台でありながらAR値は0.8台まで低下する現象が起きていました。この状態で正答率を信じてモデルをFIXしようものなら大事故必須ということで、不均一データの扱いは要注意、としみじみ感じました。


4. おわりに

ということで、めっちゃ適当に実装して試してみましたが、案外違いが出るもんですね。 仕事柄不均一データを扱うことが少なくないので、改めてデータの分布はちゃんと意識しながら進めなくちゃあいかんなと感じました。