Kaggle奮闘録(Titanic編2) -ニューラルネットワークによる分類-

# 1.はじめに

最初の投稿から随分空いてしまいましたが，2回目の投稿です．
[前回](https://qiita.com/bazz0907/items/c30c02e44193bafadb54)はロジスティック回帰を使った2値分類について書きましたが，今回は機械学習モデルをニューラルネットワークに変更して再度kaggleの"Titanic - Machine Learning from Disaster"に挑んだのでそのコードを残します．
ちなみに本記事で紹介するニューラルネットワークは，2層の中間層により構成される極めて単純なディープラーニングモデルです．



# 2.方法

今回もコードをバンバン貼っていきます．前回と被ってる部分が多いですがご了承ください．

### Titanic Tutorialに載っていたデータインストールのために必要なコード(コピペ)

[Titanic Tutorial](https://www.kaggle.com/code/alexisbcook/titanic-tutorial)

```python
# This Python 3 environment comes with many helpful analytics libraries installed
# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python
# For example, here's several helpful packages to load

import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)

# Input data files are available in the read-only "../input/" directory
# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory

import os
for dirname, _, filenames in os.walk('/kaggle/input'):
    for filename in filenames:
        print(os.path.join(dirname, filename))

# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using "Save & Run All" 
# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session
```


### パッケージインストール

```python
import matplotlib.pyplot as plt
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.model_selection import GridSearchCV

import keras
from keras.wrappers.scikit_learn import KerasClassifier
```
今回はkerasを使ってニューラルネットワークを作りました．
sklearnのGridSearchCV，kerasのKerasClassifierは，ニューラルネットワークのハイパーパラメータチューニングを行うためにインストールしています．


### モデルの定義（関数）

```python
def build_model(unit1=128, unit2=128, dropout=0):
  model = keras.models.Sequential()
  model.add(keras.layers.Dense(unit1, activation='relu', kernel_initializer=keras.initializers.he_normal(seed=0)))   # He Weight Initialization
  model.add(keras.layers.Dense(unit2, activation='relu', kernel_initializer=keras.initializers.he_normal(seed=0)))   # He Weight Initialization
  if dropout > 0:
    model.add(keras.layers.Dropout(dropout))
  model.add(keras.layers.Dense(1, activation='sigmoid', kernel_initializer=keras.initializers.glorot_uniform(seed=0)))   # Xavier Weight Initialization
  model.compile(optimizer='adam', loss='binary_crossentropy', metrics=["accuracy"])
  return model
```

僕は関数でモデルを定義するのが好きなので，関数化しています．この関数の中身だけを学習前に直接書いても大丈夫です．このモデル(コード)についてざっくり説明していきます．

Sequentialモデルというのは，直列に層を重ねるモデルのことをいいます．特に凝ったことをしない限りは"model = keras.models.Sequential()"でモデルの型を宣言しておいていいと思います．

その後は"model.add"でひたすら層を追加していきます．"keras.layers.Dense"というのは単純なノード(ニューロン)を有する層を追加することを意味し，最初の引数にはノード数，2つ目の"activation"には活性化関数の種類，3つ目の"kernel_initializer"には重み初期化手法の種類を入力します．僕は中間層には基本ReLU関数，出力層にはSigmoid関数(多クラス分類の時はSoftmax関数)を使うようにしているのですが，本当に適切かどうかは分かりません．

重みの初期化方法ですが，活性化関数としてReLU関数を使う時にはHeの初期値，Sigmoid関数(またはtanh関数)を使う時にはXaviarの初期値を設定するのが基本のようです．詳しくはオライリー・ジャパンが出版している「ゼロから作るDeep Learning -Pythonで学ぶディープラーニングの理論と実装」を参考にしていただけると幸いです．
別に初期化方法を宣言しなくてもコードは回りますが，予測能や再現性の向上のためにも設定しておくべきだと個人的には思います．

Dropoutというのはランダムに層のノードを不活性化させることを意味しており，これにより過学習(Overfitting)の抑制が期待できます．"keras.layers.Dropout()"により実装が可能で，層の全ノードのうち，何割のノードを不活性化させるかを表す割合をDropoutの引数として渡します．
すなわち4割のノードを不活性化させたければ引数として0.4を入力すれば大丈夫です．

また"model.compile"で，モデルの最適化手法，損失関数，評価関数を指定します．
最適化手法は多くの論文でAdamを使用しているので，僕もとりあえずAdamを使用しています．
損失関数も基本的に"binary_crossentropy"(多クラス分類の時は"categorical_crossentropy")を使用しており，変えるべきかどうかは分かりません．
評価関数は，単にモデルの評価基準としてなにを用いるかを意味しており，基本的にAccuracyでいいと思っていますが，ここにAUC(ROC曲線の曲線化面積)などを設定する人もいるようです．


### データのインストール

```python
train = pd.read_csv("/kaggle/input/titanic/train.csv")
test = pd.read_csv("/kaggle/input/titanic/test.csv")
```


### 欠損値の補間

```python
# for train
print(train.isnull().sum())   # Visualize the number of missing values
# fill Age column base on the sex and pclass of row
train['Age']= train.groupby(['Sex','Pclass'])['Age'].apply(lambda row : row.fillna(row.median()))

# for test
print(test.isnull().sum())
test['Age']= test.groupby(['Sex','Pclass'])['Age'].apply(lambda row : row.fillna(row.median()))
test['Fare']= test.groupby(['Sex','Pclass'])['Fare'].apply(lambda row : row.fillna(row.median()))

test.head(10)
```

対処方法は[前回](https://qiita.com/bazz0907/items/c30c02e44193bafadb54)と同じです．


### 特徴量の選択

```python
names = ['Pclass','Sex','Age','SibSp','Parch','Fare']
X_train = train[names]
X_test = test[names]
y_train = train['Survived']
X_train.head()
```


### カテゴリー変数の変換

```python
# Label encoding
sex_le = LabelEncoder()
X_train['Sex'] = sex_le.fit_transform(X_train['Sex'])
X_test['Sex'] = sex_le.fit_transform(X_test['Sex'])
```


### 標準化

```python
# Normalization (Mean=0, Standard division=1)
standard = StandardScaler()

standard.fit(X_train)
X_train = standard.transform(X_train)
X_test = standard.transform(X_test)
```


### ハイパーパラメータチューニング

```python
model = KerasClassifier(build_fn=build_model, verbose=0)
param_grid = dict(unit1=[64, 128, 256, 512],
             unit2=[64, 128, 256, 512],
             dropout=[0, 0.1, 0.3, 0.5],
             epochs=[20],
             batch_size=[16])
grid = GridSearchCV(estimator=model, param_grid=param_grid)
```

体感的にはディープラーニングでハイパーパラメータチューニングを半自動的に行なっているコードは少ない気がします(単に調べ不足かもしれませんが)．モデルを関数化しておくことで，KerasClasifierとGridSearchCVの組み合わせが出来るのでおすすめです．

今回は2層の中間層のノード数，およびドロップアウトの割合を，エポック数20，バッチサイズ16の条件下でチューニングしました．


### グリッドサーチの実行

```python
grid_result = grid.fit(X_train, y_train)

print (grid_result.best_score_)
print (grid_result.best_params_)
```

少し時間がかかります．


### ハイパーパラメータの決定

```python
model = build_model(unit1=grid_result.best_params_['unit1'], 
                   unit2=grid_result.best_params_['unit2'], 
                   dropout=grid_result.best_params_['dropout'])
```
グリッドサーチにより最も良いとされたパラメータの値を引数として代入し，分類を行うモデルを構築します．


### コールバックの設定

```python
early_stopping = keras.callbacks.EarlyStopping(
    monitor='val_loss',
    patience=10,
)
```

学習の際のオプション(コールバック)の設定です．設定しなくても大丈夫ですが，僕は過学習の抑制のために早期終了(Early stopping)を設定しました．

引数の意味としては，「検証データにおける損失関数が10エポックに渡り停滞したら，学習を打ち切ってください」ということです．

コールバックには他にも学習率減衰，最も学習精度が高かったモデルを採用する設定などもできますが，今回は特にやってません．


### 学習の実行

```python
history = model.fit(
X_train,
y_train,
validation_split=0.2,
epochs=50,
batch_size=16,
callbacks=early_stopping
)
```

訓練データのうち，2割を検証用データとしています．
またここでの"epochs"は最大エポック数を表しており，実際には早期終了によりもっと早く学習が打ち切られます．


### 学習結果の可視化

```python
# Plot training & validation accuracy values
plt.plot(history.history['accuracy'])
plt.plot(history.history['val_accuracy'])
plt.title('Model accuracy')
plt.ylabel('Accuracy')
plt.xlabel('Epoch')
plt.legend(['Train', 'Test'], loc='upper left')
plt.show()

# Plot training & validation loss values
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('Model loss')
plt.ylabel('Loss')
plt.xlabel('Epoch')
plt.legend(['Train', 'Test'], loc='upper left')
plt.show()

# Evaluate the model on the training data using `evaluate`
results = model.evaluate(X_train, y_train, batch_size=16, verbose=0)
print("train loss, train acc:", results)
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2350534/e39ea8c1-de02-b438-5f74-6d96718de3dd.png" width=45%>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2350534/6569d5b9-4526-fb2e-638b-2a05265b2906.png" width=45%>

検証用データでの精度の方が良いので，層を増やすなどしてもっとモデルを複雑にした方がいいかもしれません．ただ学習推移としては悪くなさそうです．

学習データ内での最終的は分類精度は84.6%程度でした．


### テストデータに対する予測結果の出力

```python
y_pred = model.predict(X_test)
y_pred = np.squeeze(y_pred)

y_pred[(y_pred >= 0.5)] = 1
y_pred[(y_pred < 0.5)] = 0
y_pred = np.array(y_pred, dtype='int')
```

少し原始的なコードです．"model.predict"によって出力された結果が要素数1の次元を含む2次元配列になっていたので，"np.squeeze"により削除しました(1次元にしておかないとsubmitできない)．

また予測結果が(ラベル1である)確率になっていたので，0.5以上の時にラベル1，0.5未満の時にラベル0としました．またそのあとにラベルをfloat型からint型に変換しています．


### 予測結果をCSVファイルに変換

```python
output = pd.DataFrame({'PassengerId': test.PassengerId, 'Survived': y_pred})
output.to_csv('submission.csv', index=False)
print("Your submission was successfully saved!")
```



# 3.結果
以上のモデルによる予測結果を提出したところ，予測精度は76.315%と，ロジスティック回帰の時より0.5%程度上がりました．
まだまだ80%越えへの道のりは長そうです．



# 4.おわりに
今回はニューラルネットワークを用いてタイタニックデータにおける生存者・死亡者の2値分類に挑みましたが，大幅な精度向上は見られませんでした．工夫点としては過学習の抑制のためにドロップアウト，早期終了を取り入れたこと，ハイパーパラメータチューニングを行ったことです．

モデルの変更だけでは8割越えは厳しそうなので，アンサンブル学習などを駆使してより工夫していきたいと思います．