ディープラーニングを基本から学ぶ　Part2 ニューラルネットワークの学習

◆前章：[Part1 ニューラルネットワークの基本](http://qiita.com/nvtomo1029/items/a4c37d57c8d322ce9dd2)
◆次章：[Part3 誤差逆伝播法でニューラルネットワークの学習を高速化する](http://qiita.com/nvtomo1029/items/95b82c2ab4fc0a1b5aad)

---

##**■2.ニューラルネットワークの学習**

###**▼2.1.学習とは？**
ニューラルネットワークの特徴は、データから学習できる点にある。
ニューラルネットワークで言う学習とは、

**訓練データから最適な重みパラメータの値を自動で獲得すること**

である。
ニューラルネットワークで学習を行うのに損失関数と言う指標が使われる。
この損失関数を基準にして、その値が最も小さくなる重みパラメータを探し出すというのが学習の目的になる。

###**▼2.2.学習でデータを取り扱う際の注意点？**
**(1)訓練データとテストデータを分ける**
一般的な機械学習では、訓練データ(教師データとも呼ばれる)を使って学習を行い、最適なパラメータを探索し、テストデータを使って、その訓練したモデルの汎化能力を評価する。
汎化能力とは、訓練データに含まれないデータに対しての能力であり、この汎化能力を獲得することが機械学習の目標となる。

**(2)ひとつのデータセットだけで学習と評価を行わない**
そして、汎化能力を獲得するにはひとつの訓練データとテストデータの組み合わせ(データセット)だけでは正しい評価が行えなくなる。
そのひとつのデータセットだけに過度に対応した状態となるためで、その状態を過学習と呼ぶ。

###**▼2.3.損失関数**
損失関数は、ニューラルネットワークの性能の**”悪さ”**を示す指標である。
損失関数は任意の関数を用いることができるが、一般的には2乗和誤差や交差エントロピー誤差などが用いられる。

####**▽2.3.1.2乗和誤差**

```math
E = \frac{1}{2} \sum_k (y_k - t_k)^2
```

ここで、$y_k:ニューラルネットワークの出力、t_k:訓練データ(教師データ)、k:データの数$を表す。

```py3
# coding: utf-8
import numpy as np

# 2乗和誤差
def mean_squared_error(y, t):
    return 0.5 * np.sum((y - t)**2)
```

この関数を使って計算すると、教師データが下記の場合に

```py3
# 教師データ：2つ目を正解にする
t = [0, 1, 0, 0, 0, 0, 0, 0, 0]
```

ニューラルネットワークの出力を下記の二例にすると、教師データと同じ2つ目の確率が最も高い場合の方が2乗和誤差の値が小さくなる。
”誤差”なので値が小さい方がより正しい結果となる。

```py3
# ニューラルネットワークの出力例1：2つ目の確率が最も高い→正解
y1 = [0.1, 0.7, 0.05, 0.0, 0.15, 0.0, 0.1, 0.2, 0.0]
print(mean_squared_error(np.array(y1), np.array(t))) # 0.0875

# ニューラルネットワークの出力例2：一番最後の確率が最も高い→不正解
y2 = [0.1, 0.0, 0.05, 0.0, 0.15, 0.0, 0.1, 0.2, 0.7]
print(mean_squared_error(np.array(y2), np.array(t))) # 0.7875
```

####**▽2.3.2.交差エントロピー誤差**

```math
E = -\sum_k t_k \log y_k
```

ここで、$y_k:ニューラルネットワークの出力、t_k:訓練データ(教師データ)、k:データの数$を表す。

```py3
# coding: utf-8
import numpy as np

# 交差エントロピー誤差
def cross_entropy_error(y, t):
    delta = 1e-7 # 微小な値
    return -np.sum(t * np.log( y + delta))
```

np.logの計算時に微小な値 deltaを足して計算しているのは、np.log(0)のような収束しない値になることを防ぐためである。

先ほどと同じ教師データ、ニューラルネットワークの出力データを、この関数に使ってみる。

```py3
# 教師データ：2つ目を正解にする
t = [0, 1, 0, 0, 0, 0, 0, 0, 0]

# ニューラルネットワークの出力例1：2つ目の確率が最も高い→正解
y1 = [0.1, 0.7, 0.05, 0.0, 0.15, 0.0, 0.1, 0.2, 0.0]
print(cross_entropy_error(np.array(y1), np.array(t))) # 0.356674801082

# ニューラルネットワークの出力例2：一番最後の確率が最も高い→不正解
y2 = [0.1, 0.0, 0.05, 0.0, 0.15, 0.0, 0.1, 0.2, 0.7]
print(cross_entropy_error(np.array(y2), np.array(t))) # 16.118095651
```

こちらも教師データと同じ「1」の確率が最も高い場合の方が値が小さくなる。
”誤差”なので値が小さい方がより正しい結果となるのは同じである。

###**▼2.4.ミニバッチ学習**

ディープラーニングをはじめとする機械学習では訓練データを使って学習を行う。
具体的には、訓練データに対する損失関数の値ができるだけ小さくなるようなパラメータを探し出すことである。
これまでの例では1つのデータの損失関数を考えていたが、実際には訓練データすべてを対象として求める必要がある。

しかし、実際にはすべての訓練データを対象とするには、訓練データの数が多くなればなるほど処理時間が掛かり、現実的には難しくなる。

そのため実際の学習では、訓練データから一部を選び、その一部のデータを全体の近似として利用する。

訓練データから一部を無作為に抽出して学習を行う手法をミニバッチ学習と呼ぶ。

```py3
# coding: utf-8
import numpy as np

# 入力データ
x = np.array([0.1, 0.7, 0.05, 0.0, 0.15, 0.0, 0.1, 0.2, 0.0, 0.3])
# 入力データのサイズ
train_size = x.shape[0]
# ランダムに3個の数字を選び出す
batch_mask = np.random.choice(train_size, 3)
# 入力データからランダムに抽出
x_batch = x[batch_mask]
```

上記はpythonで、行列からランダムに値を抽出する処理である。

###**▼2.5.勾配法**

勾配法は機械学習の最適化問題でよく使われ、とりわけニューラルネットワークの学習でよく用いられる。
ニューラルネットワークの学習では最適なパラメータ(重みとバイアス)を見つけるのが目標となるが、損失関数が最小の値(=最適なパラメータ)を見つけるのに勾配法を用いるのが最適の方法となる。
正確には勾配法は2種類 - 勾配降下法 と 勾配上昇法 - あり、ニューラルネットワークの分野では最小値を探す勾配降下法が用いられることが多い。

###**▼2.6.ニューラルネットワークの学習を実装する**

####**▽2.6.1.手順**

ニューラルネットワークの学習は、以下の4つの手順で実装される。

**1. ミニバッチ**
　訓練データの中からランダムに一部のデータを選び出す。
　選ばれたデータをミニバッチと呼ぶ。
**2. 勾配の算出**
　各重みパラメターの勾配を求める。
　これはミニバッチの損失関数を減らすためであり、勾配は損失関数の値を最も減らす方向を示す。
**3. パラメータの更新**
　重みパラメータを勾配方向に微小量だけ更新する。
**4. 繰り返す**
　1～3を繰り返す。

この手順は確率的勾配降下法と呼ばれ、ディープラーニングのフレームワークの多くではSGDという名前の関数で実装されるのが一般的である。

####**▽2.6.2.2層ニューラルネットワークの実装**

ニューラルネットワークの学習を実装するために、まずはニューラルネットワークのクラスを作成する。ここでは2層ニューラルネットワークのクラスを作成する。

```py3
# coding: utf-8
import numpy as np

# 2層ニューラルネットワークのクラス
class TwoLayerNet:
    # 初期化
    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, x):
        # 重み、バイアスをセット
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
        
        # 入力層→第一層への伝達
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1) # シグモイド関数
        # 第一層→第二層(出力層)への伝達
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2) # ソフトマックス関数
        
        return y
        
    # 損失関数の値を求める
    def loss(self, x, t): # x:入力データ, t:教師データ
        # 認識(推論)を行う
        y = self.predict(x)
        
        return cross_entropy_error(y, t) # 交差エントロピー誤差
    
    # 認識精度を求める
    def accuracy(self, x, t): # 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): # 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
        
```

このクラスで使われる関数の定義は以下の通り。

```py3
# シグモイド関数
def sigmoid(x):
    return 1 / (1 + np.exp(-x))    

# ソフトマックス関数
def softmax(x):
    # 配列の次元数が2なら転置してから計算する
    if x.ndim == 2:
        x = x.T
        x = x - np.max(x, axis=0)
        y = np.exp(x) / np.sum(np.exp(x), axis=0)
        return y.T 

    x = x - np.max(x) # オーバーフロー対策
    return np.exp(x) / np.sum(np.exp(x))

# クロスエントロピー誤差
def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
        
    # 教師データがone-hot-vectorの場合、正解ラベルのインデックスに変換
    if t.size == y.size:
        t = t.argmax(axis=1)
             
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t])) / batch_size

# 勾配を求める
def numerical_gradient(f, x):
    h = 1e-4 # 0.0001
    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) # f(x+h)
        
        x[idx] = tmp_val - h 
        fxh2 = f(x) # f(x-h)
        grad[idx] = (fxh1 - fxh2) / (2*h)
        
        x[idx] = tmp_val # 値を元に戻す
        it.iternext()   
        
    return grad

```

理解が難しい点は、2層ニューラルネットワーククラスの

```py3
    # 重みパラメータに対する勾配を求める
    def numerical_gradient(self, x, t): # x:入力データ, t:教師データ
        loss_W = lambda W: self.loss(x, t)
        
        grads = {}
        grads['W1'] = numerical_gradient(loss_W, self.params['W1']) # 呼び出しているのは同名の別関数

　　　　・・・
        
        return grads
```

で呼ばれているnumerical_gradient関数が自身を再帰的に呼んでいるわけではなく、2番目の関数群で定義しているnumerical_gradient関数を呼んでいる点。

####**▽2.6.3.テストデータで学習の評価を行う**
ニューラルネットワークの学習は以下の観点で評価できる。

- 訓練データの損失関数の値が減っていってるか
- 過学習を起こしていないか

「過学習を起こしていないか」の確認は、訓練データに含まれない別のデータ(=テストデータ)を使い、定期的に認識精度を記録し、訓練データの認識精度と差がなければ過学習を起こしていないと言える。

####**▽2.6.4.ニューラルネットワークの学習、評価を実装する**

2.6.2で実装した2層ニューラルネットワーククラスを用いて、ニューラルネットワークの学習、評価を実装する。

```py3
# データの読み込み
・・・(省略)
# 2層ニューラルネットワークの初期設定
network = TwoLayerNet(input_size=500, hidden_size=50, output_size=10)

# 学習パラメータの設定
iters_num = 10000  # 繰り返しの回数
train_size = x_train.shape[0] # 訓練データ数
batch_size = 100 # ミニバッチ数
learning_rate = 0.1 # 学習率
# 1エポック当たりの繰り返し回数
# 1エポックとは全訓練データをすべて使いきった時のこと
iter_per_epoch = max(train_size / batch_size, 1) 

# 学習経過の記録用
train_loss_list = []
train_acc_list = []
test_acc_list = []

# 学習を行う
for i in range(iters_num):
    # 訓練データからランダムに取得(ミニバッチ)
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]

    # 勾配の計算
    grad = network.numerical_gradient(x_batch, t_batch)

    # パラメータの更新
    for key in ('W1', 'b1', 'W2', 'b2'):
        network.params[key] -= learning_rate * grad[key]

    # 学習経過の記録
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)
    
    # 1エポックごとに認識精度を求める
    if i % iter_per_epoch == 0:
        train_acc = network.accuracy(x_train, t_train)
        test_acc = network.accuracy(x_test, t_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        print("loss | " + str(loss))

# 学習経過を出力
print("train_loss | " + str(train_loss_list))
print("train_acc, test_acc | " + str(train_acc_list) + ", " + str(test_acc_list))

```

####**▽2.6.5.この実装の大きな課題**

<font color=red>**計算に時間が掛かること**</font>
一言でいうとこれに尽きる。
これは、勾配を求めるのに数値微分を用いているためで、実装は簡単だが計算には時間が掛かってしまう。

##**■参考文献**
[ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装](https://www.amazon.co.jp/%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E4%BD%9C%E3%82%8BDeep-Learning-%E2%80%95Python%E3%81%A7%E5%AD%A6%E3%81%B6%E3%83%87%E3%82%A3%E3%83%BC%E3%83%97%E3%83%A9%E3%83%BC%E3%83%8B%E3%83%B3%E3%82%B0%E3%81%AE%E7%90%86%E8%AB%96%E3%81%A8%E5%AE%9F%E8%A3%85-%E6%96%8E%E8%97%A4-%E5%BA%B7%E6%AF%85/dp/4873117585/ref=cm_cr_arp_d_product_top?ie=UTF8)