「Scikit-learn」と「Chainer」で「分類」分析する

 Scikit-learnとChainerの勉強がてら、SIGNATEの「銀行の顧客ターゲティング」をやってみた。

・python3,pandas,numpy,scikit-learn,chainer
・JupyterNotebook
・Mac

[SIGNATE【練習問題】銀行の顧客ターゲティング](https://signate.jp/competitions/1)

#（１）環境構築

```.py
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
%matplotlib inline

#決定木の実行のためのライブラリをインポート
from sklearn.tree import DecisionTreeClassifier as DT

#クロスバリデートするためのライブラリをインポート
from sklearn.model_selection import cross_validate
#グリッドサーチするためのライブラリをインポート
from sklearn.model_selection import GridSearchCV

import chainer
import chainer.functions as F
import chainer.links as L
```

#（２）現状分析
###①SIGNATEからtrainデータとtestデータをダウンロード

```py
train = pd.read_csv("train.csv")
test = pd.read_csv("test.csv")
sample = pd.read_csv("submit_sample.csv",header=None)

train.shape
#(27128, 18)
test.shape
#(18083, 17)
```
###②trainXと、testXの設定

```.py
#idとy以外を設定
trainX = train.iloc[:,1:17]

#id以外を設定
testX = test.iloc[:,1:]

y = train["y"]

#全てをダミー変数化
trainX = pd.get_dummies(trainX)
testX = pd.get_dummies(testX)
```

#（３）Chainerによる「分類」分析
###①データセット

```.py
#pandasをnumpyにし、32bitのfloatにする
x = trainX.values.astype('f')
x.shape
#(27128, 51)

tx = testX.values.astype('f')
tx.shape
#(18083, 51)

#32bit int型。「回帰」では'f' として入力変数と合わせる必要がある。
t = y.values.astype('I')
```
###②ニューラルネットワークモデルの定義

```.py
class NN(chainer.Chain):
    def __init__(self,n_mid_units1=20,n_mid_units2=10,n_out=2):
        super().__init__()
        with self.init_scope():
            self.fc1 = L.Linear(None,n_mid_units1)
            self.fc2 = L.Linear(None,n_mid_units2)
            self.fc3 = L.Linear(None,n_out)
            
            #入力変数の数51に合わせる必要がある（ノーマリゼーション）
            self.bn = L.BatchNormalization(51)
            
    
    def __call__(self,x):
        h = self.bn(x)
        h = self.fc1(h)
        h= F.relu(h)
        h = self.fc2(h)
        h = F.relu(h)
        h = self.fc3(h)
        return h
```

###③インスタンス化する
モデルを計算していく（学習）際に、評価関数など進捗のレポートの機能を標準装備するL.Classifierを使う。加えてrandomシードを設定する。モデルを設定した際にランダムに初期化されるので、モデル定義の前にシードを固定する必要がある。この場合は１（なんでもいい）

```.py
np.random.seed(1)
nn = NN()
model = L.Classifier(nn)
```

###④chainerで使用するデータセット形式に変換
x（入力変数）と、t（出力変数（教師データ））をChainerで使えるようにするには、まず、タプルで囲い（zip関数を使う）、リスト化する必要がある。タプルで囲う際は（入力変数、出力変数（教師データ））の順で囲う。

```.py
dataset = list(zip(x,t))
```

###⑤入力データを訓練データと検証データに分割する

```.py
n_train = int(len(dataset)*0.7)
train,test = chainer.datasets.split_dataset_random(dataset,n_train,seed=1)
```

###⑥その他設定

```.py
#optimizer
optimizer = chainer.optimizers.Adam()
optimizer.setup(model)

#iteretor
batchsize = 2000
train_iter = chainer.iterators.SerialIterator(train,batchsize)
test_iter = chainer.iterators.SerialIterator(train,batchsize,repeat=False,shuffle=False)

#updator CPYは-1、GPYは0を設定
from chainer import training
updater = training.StandardUpdater(train_iter,optimizer,device=-1)

#TrainerとExtensionsの設定
from chainer.training import extensions
epoch = 100
trainer = training.Trainer(updater,(epoch,'epoch'),out='result/kekka')
trainer.extend(extensions.Evaluator(test_iter,model,device=-1))
trainer.extend(extensions.LogReport(trigger=(1,'epoch')))
trainer.extend(extensions.PrintReport(['epoch','main/accuracy','validation/main/accuracy','main/loss','validation/main/loss','elapsed_time']),trigger=(1,'epoch'))
```
###⑦学習の実行

```.py
trainer.run()
```
 ###⑧結果の可視化

```.py
import json
import matplotlib
%matplotlib inline

with open("result/kekka/log")as f:
    logs = json.load(f)
results = pd.DataFrame(logs)
results[["main/accuracy","validation/main/accuracy"]].plot()
```

###⑨testXの予測

データの予測にあたっては、txをそのまま使うことはできない。chainerでは、（バッジサイズ、入力変数の数）の形にして、model.predictorに代入する必要がある。そのため以下のようにnp.newaxisを実行。

```.py
tx[0].shape
#(51,)
tx[0][np.newaxis].shape
#(1, 51)
```

上記から以下のように記述する。

```.py
result=[]
for i in range(18083):
    x0 = tx[i].reshape(1,len(tx[i]))
    with chainer.using_config('train',False),chainer.using_config('enable_backprop',False):
        y0_predict = model.predictor(x0)
        y0_predict = F.softmax(y0_predict)
        y0_predict = y0_predict.array
        
        result.append(y0_predict[0][1])
```
SIGNATEに投稿した結果、0.9254847となった。まずまずの結果。 

#####(上記の補足メモ）※備忘
例えば、x[0]の予測値を出す

```.py
 x0 = x[0].reshape(1,len(x[0]))
with chainer.using_config('train',False),chainer.using_config('enable_backprop',False):
      y0_predict = model.predictor(x0)
```

```.py
y0_predict
#variable([[0.14841829, 0.03129183]])

type(y0_predict)
#chainer.variable.Variable
```
上記の結果となるが、足して１にならなければならないため以下の処理をしている。

```.py
y0_predict = F.softmax(y0_predict)
y0_predict
#variable([[0.5292482 , 0.47075182]])

#次にchainerで扱う形式からnumpyで扱う形式に戻す
y0_predict = y0_predict.array
y0_predict
#array([[0.5292482 , 0.47075182]], dtype=float32)

#１番目が今回求めるもの。なので、[[]]のうちの、0番目の1番目となる
y0_predict[0][1]
#0.47075182
```

#（４）Scikit-learnによる「分類」分析

###①決定木モデルを定義する
今回は、ハイパーパラメータであるmax_depthを4として、min_samples_leafを500とする。

```.py
clf1 = DT(max_depth=4,min_samples_leaf=500)
clf1.fit(trainX,y)
```

上記の決定木を可視化する。
以下を実行すると、tree.dotというファイルが生成されるので、これをhttp://www.webgraphviz.com に貼り付ける。

```.py
export_graphviz(clf1, out_file="tree.dot", feature_names=trainX.columns, class_names=["0","1"], filled=True, rounded=True)
```

testXを予測する。

```.py
pred = clf1.predict_proba(testX)
```

上記を実行すると以下のようになるが、左側がyが0となる確率、右側がyが1となる確率を意味するため、右側を取得する。
```
array([[0.41565704, 0.58434296],
       [0.41565704, 0.58434296],
       [0.94135287, 0.05864713],
       ...,
       [0.94135287, 0.05864713],
       [0.94135287, 0.05864713],
       [0.94135287, 0.05864713]])
```

```.py
pred = pred[:,1]
```

```.py
sample[1] = pred
sample.to_csv("submit_scikit.csv",index=None,header=None)
```

SIGNATEに投稿した結果、0.8651751となった。

###②決定木モデルをチューニングする
ハイパーパラメータである、max_depthとmin_samples_leafを調整する。
上述で作成したclf1の精度をクロスバリデーション法を使って確認する。クロスバリデーション法とはデータをK個に分割し、検証をK回繰り返す。K−１個をモデル構築データとし、残り1個をモデル検証データとする。最後にこれらの平均化することでパラメータに決定する手法。今回はclf1を５分割して検証する。

```.py
#cv=5とはtrainを5分割するという意味,scoring=roc_aucはroc_aucという評価方法をとるという意味（signateで定義されている評価方法）
cross_validate(clf1,trainX,y,cv=5,scoring="roc_auc",n_jobs=-1,return_train_score=True)

#５つに分割したデータのどれも0.86程度というのがわかる。
```

上記検証をハイパーパラメータの値を変更しながら繰り返すのだが、一方でグリッドサーチを使って最適なパラメータを探す方法もある。
max_depthを2から10の範囲、min_samples_leafを5,20,80,200,300,500の範囲で設定する。

```.py
clf4 = DT()
parameters = {"max_depth":list(range(2,11)),"min_samples_leaf":[5,20,80,200,300,500]}

gcv = GridSearchCV(clf4,parameters,cv=5,scoring="roc_auc",n_jobs=-1,return_train_score=True)
gcv.fit(trainX,y)
gcv.cv_results_
train_score = gcv.cv_results_["mean_train_score"]
test_score = gcv.cv_results_["mean_test_score"]
```

最適なパラメータを出す。
以下では、max_depthは10、min_sample_leafは200となった。

```.py
gcv.best_params_
#{'max_depth': 10, 'min_samples_leaf': 200}
```

```.py
pred = gcv.predict_proba(testX)
pred = pred[:,1]
sample[1]  = pred
sample.to_csv("submit_scikit2.csv",header=None,index=None)
```
SIGNATEに投稿した結果、0.8925017となった。
#（５）まとめ
Scikit-learnで回帰分析を行うと0.8925017、一方でChainerで回帰分析を行うと、0.9254847とわずかにChainerの方が良い結果となった。
恐らくもっと有効な手法があると思うが、これから掘り下げて学習していきたい。