不均衡データに対するClassification

分類問題のなかには、ラベル0が90％、ラベル1が10％といったデータが不均衡のケースが存在します。特段の工夫をせずに分類モデルを生成すると少数派の分類精度の低いモデルになることが知られています。分類モデルの目的が多数派の識別であれば深刻な問題にならないのですが、こうした不均衡データを取り扱う場合は、少数派データの識別が目的のケースが多いので、工夫が必要とされます。

本論は、過去の研究によって提案されている方法のうち、1）アンダーサンプリング、2）オーバーサンプリング、3）コスト関数のカスタマイズについての実装結果とその効果について報告します。

#アンダーサンプリング

アンダーサンプリングとは、少数派のデータ件数に合うように多数派データからランダムに抽出する方法です。この方法の良いところは直感的でわかりやすいことでしょう。
多数派のデータからのリサンプリングは、DataFrameであればsampleというメソッドを使うことになります。これであれば、コードも1行程度でよく手間もかかりません。ただ、この方法だと元の多数派データの特徴量の分布が崩れてしまう懸念があります。リサンプリングする以上、元の分布が維持されているほうがモデルの精度にいい影響があると思われます。そこでリサンプリングする前に少し工夫をしたほうがいいと思われます。具体的には、多数派のデータを対象にクラスター分析を行い、特徴量が類似するデータ同士で群を作成します。そしてこの群単位にランダムにデータを抽出すれば、特徴量の分布を維持できることになります。

上記の考え方をコードで表現すると以下のようになります。

```python
# Under Samplingの関数（X: num:ターゲット件数 label:少数派のラベル）

def under_sampling_func(X,num,label) :
    
    # KMeansによるクラスタリング
    from sklearn.cluster import KMeans
    km = KMeans(random_state=201707)
    km.fit(X,Y)
    X['Cluster'] = km.predict(X)
    
    # 群別の構成比を少数派の件数に乗じて群別の抽出件数を計算
    count_sum = X.groupby('Cluster').count().iloc[0:,0].as_matrix()
    ratio = count_sum / count_sum.sum()
    samp_num = np.round(ratio * num,0).astype(np.int32)
    
    # 群別にサンプリング処理を実施
    for i in np.arange(8) :
        tmp = X[X['Cluster']==i]
        if i == 0 :
            tmp1 = X.sample(samp_num[i],replace=True)
        else :
            tmp2 = X.sample(samp_num[i],replace=True)
            tmp1 = pd.concat([tmp1,tmp2])
    tmp1['Class'] = label
    return tmp1
```

このコードを実際のデータで実験してみました。データはkaggleの「Credit Card Fraud Detection」を使いました。
https://www.kaggle.com/dalpozz/creditcardfraud

このデータは、ある期間のクレジットカード取引のデータで「不正取引かどうか」とともに取引の特徴を示す特徴量（主成分分析によって数値化したもの）が含まれていることから、この特徴量から不正取引を識別するモデルを作ることができます。不正取引の件数ですが、全体の0.172%と不均衡データとなっており、まさに不均衡データになっています。

分類モデルにはロジスティック回帰を採用しました。結果は以下のようになりました。

|Predict|    0|    1|
|:-----------------|------------------:|:------------------:|
|Class  |     |     |
|0      |  486|    6|
|1      |   35|  457|

Accuracy = 0.958333333333

全体の予測精度は95.8％とまずまずです。Class0（正当取引）の識別率も98.8％（486÷492）、またClass1（不正取引）の識別率も92.9％（457÷492）とまずまずの結果となりました。比較のため、不均衡な状態のデータを使った結果を以下に示します。


|Predict|    0|    1|
|:-----------------|------------------:|:------------------:|
|Class  |     |     |
|0      |  284273|   42|
|1      |   186 |  306

Accuracy = 0.999199457878

全体の精度は、不均衡のほうがいいことがわかります。また、正当データの識別率も不均衡のほうがよくなっています。一方、不正取引についてはアンダーサンプリングしたほうが良い結果となっていて今回のように不正取引の検出であれば、アンダーサンプリングしたデータをもとにチューニングをしたほうが望ましい結果になるように思われます。

今回のデータの場合、少数派のデータは492件と極めて少数でした。そのため、リサンプリングしたデータを加えても1000件弱しか生成できず、トレーニングデータとテストデータに分割して活用するには少しデータ不足です。こうした場合、次に示すオーバーサンプリングを併用する必要があります。

#オーバーサンプリング

オーバーサンプリングとは、少数派のデータをもとに不足分のデータを補完するというものです。新しいデータを生成するシンプルな方法は、数値データであれば少数派の平均と分散を使ってランダムに生成することやカテゴリーデータであれば、構成比をウェイトにnumpyのrandom.choiceを用いるというものが考えられます。この方法は、特徴量間の相関が考慮できません。例えば、身長と体重のように相関があるにも関わらず、身長100cm・体重80kgというデータが生成される可能性があり、モデルの精度に影響してしまうことも考えられます。

既にこうした問題などを考慮した方法としてSMOTE（synthetic minority over-sampling）というものがあり、pythonにもライブラリーが提供されています。
このSMOTEは、大まかにいうと以下のステップで新しいデータを生成します。
　1）タネとなるデータを抽出する。
　2）タネとなるデータの近傍にあるデータを探索する。（探索対象数は事前設定要）
　3）タネと探索したデータの間でランダムにデータを生成する。
こうすることで、特徴量間の相互関係を考慮したデータが生成できることになります。

実装は至って簡単です。

```python
from imblearn.over_sampling import SMOTE

sm = SMOTE(random_state=42)
X_res, Y_res = sm.fit_sample(X, Y)
```
Xは特徴量のデータセットです。Yはラベルのデータセットです。このfit_sampleを実行すると、Yのうち少数派のデータを多数派に合うようにデータを生成し、結果を返してくれます。

実際の適用を先のCredit Card取引のデータに対して行います。適用にあたり、500件弱のデータを12万件にまで増やすことも可能ですが、さすがにやりすぎと感じたので、多数派データを1000件にまでアンダーサンプリングした上で、少数派データを1000件まで増幅するという方法にしました。
結果は以下のようになりました。

|Predict|    0|    1|
|:-----------------|------------------:|:------------------:|
|Class  |     |     |
|0      |  978|   21|
|1      |   78 |  921

Accuracy = 0.95045045045

全体の予測精度は95.0%とアンダーサンプリングとほぼ同等です。Class0の識別率は97.9％、Class1の識別率は92.2％となり、アンダーサンプリングの98.8％および92.9%とやはり同等です。データ総数は1998件（四捨五入の関係でアンダーサンプリングした件数が999件だったため）とトレーニングデータとテストデータに分割して活用できる件数です。

#コスト関数のカスタマイズ

機械学習やニューラルネットのパラメータは、オプティマイザーがモデルの精度の悪さを表すコスト関数の値が最小となるパラメータを探索的に決定しています。

ロジスティック回帰の2値分類におけるコスト関数のコード(tensorflowにて実装する場合のもの）を以下に示します。

cost = -tf.reduce_sum(Y*tf.log(p0) + (1-Y)*tf.log(1-p0)) 

この計算式は、以下のステップによる精度の悪さの計算を1行のコードで表現しています。

　1)ラベル0を対象に1件ずつの精度の悪さを計算する
　2)ラベル1を対象に1件ずつの精度の悪さを計算する
　3)項番1)の合算値を算出する
　4)項番2)の合算値を算出する
　5)項番3)と4)を合算してトータルのコストを算出する

仮にラベル1のデータの構成比が極小とすると、1件当たりの精度が悪くても上記の項番4)の段階で全体のコストの押し上げにあまり寄与しません。そのため、全体のコストにより大きな影響を与えるラベル0の精度を高めるようにオプティマイザが機能し、結果として少数派の精度が上がらないことになります。
そこで、上記の1)または2)において少数派に対してウェイトをかけることで3)または4)の件数の差によるコスト全体への影響を抑制する方法が考えられます。
実際のpythonの場合、各機械学習にウェイトがオプションとして設定できるようになっているのでこれを活用することで実現できるようになっています。
ここでは、tensorflowを使って隠れ層なしのモデルを作成し、コスト関数に件数比を反映するしてみることにしました。
コードは以下の通りです。ウェイトは、件数比577.88として少数派である1のほうに乗算することにしました。

```python
from sklearn import metrics

label = dat01['Class'].as_matrix().reshape(len(dat01),1)
features = dat01.drop(['Time','Amount','Class'],axis=1).as_matrix()

X = tf.placeholder(tf.float32,[None,28])
Y = tf.placeholder(tf.float32,[None, 1])

w0 = tf.Variable(tf.zeros([28,1]))
b0 = tf.Variable(tf.zeros([1]))

f0 = tf.matmul(X,w0) + b0
p0 = tf.sigmoid(f0)

cost = -tf.reduce_sum(Y*tf.log(p0)*577.88 + (1-Y)*tf.log(1-p0))  # 損失関数
train_step = tf.train.AdamOptimizer().minimize(cost)              # 最適化手法

# 初期設定
sess = tf.Session()
sess.run(tf.global_variables_initializer())


i = 0
for _ in range(280):
    i += 1
    sess.run(train_step, feed_dict={X : features , Y : label})
    if i % 40 == 0:
        loss_val = sess.run(cost, feed_dict={X : features , Y : label})
        predict = sess.run(p0 , feed_dict={X : features , Y : label})
        pre_label = [0 if x<0.5 else 1 for x in predict]
        acc_val = metrics.accuracy_score(pre_label,label)         # 予測結果の評価
        print ('Step: %d,  loss: %f,  acc; %f'
               % (i,  loss_val , acc_val))

```

結果は以下のようになりました。

|Predict|    0|    1|
|:-----------------|------------------:|:------------------:|
|Class  |     |     |
|0      |  255271|   29044|
|1      |   26 |  466

accuracy = 0.892060

全体の正解率は、90％を割り込んでしまいましたが、Class1をゼロとして誤る割合は最も小さくなりました。

#まとめ
不均衡データにおいても少数派の識別率を高める方法としてアンダーサンプリング・オーバーサンプリング・コスト関数のカスタマイズ（ウェイトの調整）は、有効であることが改めてわかりました。
直感的なわかりやすさでいうと、1)アンダーサンプリング、2)オーバーサンプリングの順番になろうかと思うので、まずはこの順序でデータの調整を行い、モデルのチューニング過程でウェイトの調整を取り入れるという手順が良いように感じました。

#おまけ
このクレジットカード取引のデータを使っているなかで2点ほど気が付いたことがありました。
第1点は、本件は二値分類問題よりも異常値検出問題としてアプローチしたほうがいいのではないかということです。不正取引(Class=1）は全体の0.172%しかありません。この割合は異常値ととらえたほう妥当な程度だと思われます。しかも異常値検出であれば、正常値（つまり正当取引）を対象に特徴量の閾値を設定することで、個々のデータが閾値の内外であることを判別することで識別できるようにできるので不均衡データの対応を必要としないはずです。
実装方法については、異常値識別の方法論から勉強する必要があるので、少し時間を必要としますが完成次第、発表したいと思います。
第2点は、コスト関数に経済的損失を反映したほうが、意味のあるモデルになるのではないかというものです。改めて、アンダーサンプリングによって作成したモデルの識別結果を以下に示します。

|Predict|    0|    1|
|:-----------------|------------------:|:------------------:|
|Class  |     |     |
|0      |  486|    6|
|1      |   35|  457|

これによると正当な取引を不正取引と誤識別するケースが6件、不正取引を正当取引をするケースが35件あります。どちらも単なる誤識別では済まず経済的損失は発生させます。不正取引の誤識別は、クレジット取引の回収ができず取引金額がそのまま損失となります。一方、正当取引の誤識別の結果、その顧客は他のカードを活用することになれば、本来であれば獲得できていた収益を失うという逸失利益を生じさせることになります。
現状のコスト関数は、こうした誤りがもたらす経済的価値が考慮されていません。実際には上記にしめしたように経済的損失を発生させているので、その値をコスト関数に反映したほうが、現実的な識別モデルになるように思われます。
実装方法は、コスト関数のカスタマイズに似たようなものになると思うので完成したら報告したいと思います。