手作り3層ニューラルネットワークをフルスクラッチで実装して、 Kaggle Titanic コンペに Submit してみた (前編)

# **はじめに**
世間では今、ニューラルネットワークや機械学習といったカッコイイ言葉が飛び交っていますね。興味を持って色々と学習を進めている方も多いと思います。
一方で、書籍などを参考に基本知識はなんとなく理解したが、その次に何をしたら良いのか？と迷っている人も多いのではないでしょうか？(私がそうです)
本記事では、そんな感じで足踏みしている人に対して、自分の取り組みを通して次の一歩の参考になるような内容をご紹介できればと思います。
自分と同じような状態の方々の学習の一助になると幸いです。

今回初めてQiita記事を書いたのですが、書いていたらなぜか異様なほど長くなったので2回に分けます。
一応、前編と後編はある程度独立していて、前編を飛ばしても後編の内容には差し支えないです(たぶん)。


### **今回やってみたこと**
ほとんどタイトルの通りですが、今回やってみた内容は次のとおりです。
・3層ニューラルネットワークをPythonでフルスクラッチで実装してみた
・作成したニューラルネットワークを Kaggle Titanic コンペの学習用データセットで学習させてみた
・作ったモデルにテストデータを投入し、その結果を Submit してみた

### **想定読者**
本記事は次のような人を想定にしています。
・ニューラルネットワークはなんとなく理解し、なんとなくMNISTでほげほげしてみたけど、次に何をしたらいいかわからない
・データ分析って概念はなんとなくわかったけど、次に何をしたらいいかわからない
・手元にデータもないし良い目的も思いつかないので、実際的なデータ分析を学習するのに自分をモチベートするネタがほしい
・とりあえず Kaggle に登録してみたけど、始め方が良くわからず放置している

つまり私です(笑)

**もくじ**
[前編]
・[はじめに](#はじめに)
・[ニューラルネットワークの実装](#ニューラルネットワークの実装)
　- [必要な関数の実装](#必要な関数の実装)
　- [全結合3層ニューラルネットワークの実装](#全結合3層ニューラルネットワークの実装)

[後編]
・Kaggle Titanic 学習用データでモデル構築 
　- Kaggleとは?
　- データセットの取得
　- 学習用アルゴリズムの実装
　- 学習用データの前処理
　- モデル構築
・Kaggle Titanic テスト用データでデータ予測
　- モデルへのテスト用データの投入
　- 予測結果をSubmit

# **ニューラルネットワークの実装**
2値分類モデルを作成するためのニューラルネットワークを実装します。

ニューラルネットワークとは？ということをイチから説明するとなると、それだけで記事が何本もかけてしまいますし、そもそも私がわかりやすく噛み砕いて説明できるほど詳しくないので、今回はごく基本的な知識は前提とします。
入門書としては、例えば[オライリーのサカナ本](https://goo.gl/Pg4xuV)とか、[中井悦司さんのカモメ本](https://goo.gl/NG6SHp)が良書です。数学的記述に抵抗が無い場合は、[この記事](https://qiita.com/ta_to_co/items/c3a01b4fc8058030b1e2)が非常に綺麗にまとまっていてオススメです。

今回作成するのは全結合の３層ニューラルネットです
学習アルゴリズやハイパーパラメータは次の通りです。
・入力層の次元：7次元
・出力層の次元：2次元
・隠れ層の次元：120次元
・学習アルゴリズム：勾配降下法
・損失関数：交差エントロピー誤差
・学習率：0.005

このパラメータに特に根拠はなく、色々試したらこれがまぁまぁの結果だったというだけです。
この辺りのいじり方もちゃんと知りたいところですね。。。
ではまず必要な部品を揃えていきます。
Python初心者なので見苦しい所もあるかもしれませんがどうぞご容赦を。

### **必要な関数の実装**
必要となる関数を1つ1つ実装していきます。
なお、実装にあたっては、オライリーのサカナ本をフル活用(というかほぼ写経)していますので、こちらを既に読了済み方は次節に飛んで問題ありません。

今回使う関数は次の4つです。
・sigmoid関数
・softmax関数
・交差エントロピー誤差
・Gradient

それでは順に実装していきます。
基本的には定義式を読んだ通りにひたすらNumpyで実装すれば良いのですが、一部考慮が必要な物もあります。
またここでは、通常フォント(例: $x$ )はスカラーを表し、太字フォント(例: $\boldsymbol{x}$ )はベクトルを表すものとします。

**sigmoid関数の実装**
次のようにして表される$\mathbb{R}$ 上の実数値関数 $\sigma$ をシグモイド関数と呼びます。

```math
\sigma(x) = \frac{1}{1 + e^{-x}}
```
読んだ通り実装すると、$x$ が大きかったり小さかったりした時に桁あふれが発生するので、次のように実装しました。$x$ の値を上下から抑えています。範囲からはみ出た値は死んでしまいますが、とりあえず一旦これで進めます。
また、シグモイド関数自体は$\mathbb{R}$ 上の関数として定義されてますが、実際の実装ではベクトルを入力としてそれぞれの成分毎に作用させます。(その辺はNumpyが勝手にやってくれます)

```py:functions.py
import numpy as np
def sigmoid(x):
    return 1 / (1 + np.exp(-np.clip(x,-709,100000)))
```

**softmax関数**
次のようにして表される $\mathbb{R}^{m}$ から $\mathbb{R}^{m}$ への関数をsoftmax関数と呼びます。

```math
softmax( \boldsymbol{x} ) = \frac{1}{\sum_{i=1}^{m} e^{x_{i}}} ( e^{x_{1}}, e^{x_{2}}, ... , e^{x_{m}} )
```
これも読んだ通りの実装だと桁あふれが起こりやすいので、次のようにします。（分母分子を定数倍(ここでは $\boldsymbol{x}$ の最大の成分)して $e$ の肩に乗せてます）

```py:functions.py
def softmax(x):
    x = x - np.max(x)
    return np.exp(x) / np.sum(np.exp(x))
```

**交差エントロピー誤差**
次のようにして表される $\mathbb{R}^{n}\times \mathbb{R}^{n}$ 上の実数値関数 $L$ を交差エントロピー誤差と呼びます。

```math
L(\boldsymbol{y}, \boldsymbol{t}) = - \sum_{i=1}^{n} t_{i}
 \log{y_{i}}
```
これはそのまま実装します。

```py:functions.py
def cross_entropy(y, t):
    return (-1) * np.sum(t * np.log(y))
```

**Gradient**
損失関数を各重みパラメータで偏微分して並べたもの $\left( \frac{\partial L}{\partial {w}_{i,j}} \right) _{i,j}$ を求めます。こだわるならBackPropagationを実装すべきかもしれませんが、Python力の不足から今回は数値微分でいきます。

```py:gradient.py
import numpy as np
def numerical_gradient(f, x):
    h = 1e-4
    grad = np.zeros_like(x)
    
    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        tmp_val = x[idx]
        x[idx] = float(tmp_val) + h
        fxh1 = f(x)
        
        x[idx] = tmp_val - h 
        fxh2 = f(x)
        grad[idx] = (fxh1 - fxh2) / (2*h)
        
        x[idx] = tmp_val
        it.iternext()   
        
    return grad
```

これらの関数を、```functions.py```, 及び ```gradient.py``` というスクリプトファイルにまとめます。

### **全結合3層ニューラルネットワークの実装**
ThreeLayerNet というクラスを実装した下記のようなスクリプトファイルを```three_layer_neural_net.py```という名前で作成します。

```py:three_layer_neural_net.py
import sys, os
sys.path.append(os.pardir)
from functions import *
from gradient import numerical_gradient

class ThreeLayerNet:
    def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
        self.params = {}
        self.params["W1"] = weight_init_std * np.random.randn(input_size, hidden_size)
        self.params["b1"] = np.zeros(hidden_size)
        self.params["W2"] = weight_init_std * np.random.randn(hidden_size, output_size)
        self.params["b2"] = np.zeros(output_size)
        
    def predict(self, x0):
        W1, W2 = self.params["W1"], self.params["W2"]
        b1, b2 = self.params["b1"], self.params["b2"]
        
        a1 = np.dot(x0, W1) + b1
        x1 = sigmoid(a1)
        a2 = np.dot(x1, W2) + b2
        y = softmax(a2)
        
        return y
    
    def loss(self, x, t):
        y = self.predict(x)
        return cross_entropy(y, t)
    
    def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        t = np.argmax(t, axis=1)
        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy
    
    def numerical_gradient(self, x, t):
        loss_W = lambda W: self.loss(x, t)
        
        grads = {}
        grads["W1"] = numerical_gradient(loss_W, self.params["W1"])
        grads["b1"] = numerical_gradient(loss_W, self.params["b1"])
        grads["W2"] = numerical_gradient(loss_W, self.params["W2"])
        grads["b2"] = numerical_gradient(loss_W, self.params["b2"])
        
        return grads
```

メソッドの説明をします。
```__init__```：重みとバイアスの初期化を実施します。標準正規分布に従う乱数に、係数として"weight_init_std" を掛けたものを初期値としてます。 
```predict```：その時点での重みを持つニューラルネットに入力データを作用させた結果を返します。
```loss```：ニューラルネットの出力と学習用データを入力とし、その誤差を返します。誤差は先述の交差エントロピーで算出します。
```accuracy```：ニューラルネットの計算結果と学習用データラベルとの一致率（つまり正解率）を返します。
```numerical_gradient```：損失関数を重みパラメータに関して偏微分して、Gradientを計算します。

手作りニューラルネットワークの実装はここまでになります。

引き続き後編で、 Kaggleの Titanic データセットを用いたモデル構築の記事を書きますので、そちらも読んでもらえると嬉しいです。