非情報系大学院生が一から機械学習を勉強してみた #5：ニューラルネットワークの学習

#はじめに
非情報系大学院生が一から機械学習を勉強してみました。勉強したことを記録として残すために記事に書きます。
進め方はやりながら決めますがとりあえずは有名な「ゼロから作るDeep-Learning」をなぞりながら基礎から徐々にステップアップしていこうと思います。環境はGoogle Colabで動かしていきます。第5回はニューラルネットワークの学習です。

#目次
1. ニューラルネットワークの学習とは？
2. 損失関数
3. ミニバッチ学習
4. 学習アルゴリズムの実装

#1. ニューラルネットワークの学習とは？
以前の[第3回](https://qiita.com/jirojiro/items/40b9c747bcb0b16662aa)では学習済みのパラメータをニューラルネットワークに読み込んでMNIST手書き数字認識を行いました。今回はパラメータの学習から自分でできるようにします。
さて、ちまたのAIブームに乗って「AIすごい！」「ニューラルネットワーク良くわかんないけどすごい！」となりがち（？）ですが、所詮は決められたパラメータに従って計算を行うだけです。そしてそのパラメータも（層が増えると数は膨大になりますが）基本は重みとバイアスの2種類です。ニューラルネットワークの学習とは結局はこの重みとバイアスを決めるというだけのことになります。
といったものの、これらのパラメータはどうやって決めればよいのでしょうか？まず基本的なところですが、ニューラルネットワークではデータを訓練データ（教師データ）とテストデータに分けます。

訓練データ（教師データ）…パラメータ決定の学習を行うのに用いられるデータ。
テストデータ…訓練データを用いて決定されたパラメータの性能の評価実験を行うためのデータ。

訓練データを完璧に学習したパラメータを設定すれば訓練時の性能は高くなります。しかし実用を考えると訓練データに適応しすぎてその他の類似のデータが入力されたときに正しく動作しなければ意味がありません。この状態を過学習といいます。本質を理解せず過去問による勉強に適応しすぎてテスト本番で単位を落とす大学生も過学習が進んでいると言えますね（？）これを避けるため評価を訓練に用いていない別データで行ったり、学習の際に過学習を防ぐ工夫を行ったりします。

#2. 損失関数
訓練データを用いて学習を行う際、どの程度性能が向上したかの評価手法が必要となります。これを損失関数と言います。損失関数は「性能の悪さ」に注目した指標です。なんだか不自然に思えますが結局は「性能の良さ」＝「どれだけ性能が悪くないか」の逆になるので本質的には同じことです。また、後ででてくるようにニューラルネットワークの学習では「損失関数の値を小さくする」＝「性能の悪さを最小限にする」ように学習を進めていきますが、これが結局最小値探索問題であるので[第4回](https://qiita.com/jirojiro/items/3a37a26a1ee06e3e2ad2)で紹介した勾配計算と相性が良いというのも損失関数を考える理由の一つではないかと思いました。

###2乗和誤差
ニューラルネットワークの各出力$y_k$と各教師データ$t_k$の差の2乗の和です。式では以下のように書けます。
$$
E = \frac{1}{2}\sum_{k}\left(y_k-t_k\right)^2
$$


###交差エントロピー誤差
交差エントロピー誤差は以下の式で書けます。
$$
E = - t_k \log y_k
$$
情報学におけるエントロピーと同じ式の形をしています。違うのはエントロピーでは同じ確率が入るところがそれぞれ$y_k, t_k$になっているところです（交差している）。

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

# 交差エントロピー誤差
def cross_entropy_error(y, t):
    delta = 1e-7
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

    batch_size = y.shape[0]
    return -np.sum(t * np.log(y+delta)) / batch_size

# 教師データ：2が正解
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

# 2の確率が最大のとき（正しい）
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
print(mean_squared_error(np.array(y), np.array(t)))       #0.09750000000000003
print(cross_entropy_error(np.array(y), np.array(t)))      #0.510825457099338

# 7の確率が最大のとき（誤り）
y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
print(mean_squared_error(np.array(y), np.array(t)))       #0.5975
print(cross_entropy_error(np.array(y), np.array(t)))      #2.302584092994546
```
上の実装を見ると確かに教師データと同じ2が最大確率になっているとき損失関数の値は小さく、誤った7が最大確率となっているとき損失関数の値が大きくなっていることが分かります。ニューラルネットワークではこの損失関数の値が小さくなるように学習を進めていきます。

#3. ミニバッチ学習
実際に教師データから学習を行おうとするとその数は大量にあるので学習が非常に重くなってしまいます。そこで通常はすべての教師データを用いる方法（バッチ学習）で学習は行いません。代わりに行われるのがミニバッチ学習です。ミニバッチ学習では全教師データで学習を行う代わりに無作為に抽出したデータ（ミニバッチ）を用いて学習を行います。（テレビの視聴率調査のイメージです。）MNIST手書き数字を例に見ていきます。
MNIST手書き数字では60000枚の教師データが確保されています。この中から例えばデータ数100のミニバッチを無作為に抽出します。このミニバッチは母集団から無作為に抽出されているので母集団の教師データの特徴を反映していると考えられます。そこでこのミニバッチを用いて学習します。こうしたミニバッチ学習を600ステップ行えば学習に用いたデータセット数は100×600=60000より60000枚の教師データ全てを"見た"とみなせます。このすべての教師データを"見た"単位を1エポック（Epoch）と言います。今回の例では600ステップで1エポックとなります。実際の学習ではこれを数エポック繰り返します。

#4. 学習アルゴリズムの実装
以上でニューラルネットワークの学習を行うための基本知識がまとめられました。勾配、勾配降下法については[第4回](https://qiita.com/jirojiro/items/3a37a26a1ee06e3e2ad2)でまとめているのでそちらをご参照ください。いよいよ学習を実装していきます。ニューラルネットワークの学習は次の4つの手順で行われます。

1. ミニバッチ  
教師データの中からランダムに一部を選び出す。
2. 勾配の算出  
ミニバッチの損失関数を減らすために各重みパラメータの勾配を求める。勾配は損失関数の値を最も減らす方向を示す。
3. パラメータの更新  
重みパラメータを勾配方向に微小量だけ更新する。
4. イタレーション  
1～3を繰り返す。

ここで勾配計算はすべての教師データから求めた勾配ではなく無作為に選ばれたミニバッチのデータを使用した勾配です。よってこの手法を確率的勾配降下法（SGD：stochastic gradient descent）と呼びます。

では実際に実装していきます。

```python:準備
import numpy as np
import matplotlib.pyplot as plt

# MNIST読み込み
!git clone https://github.com/oreilly-japan/deep-learning-from-scratch
import sys, os
sys.path.append("/content/deep-learning-from-scratch")      #setting path
from dataset.mnist import load_mnist

# 活性化関数定義
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_grad(x):
    return (1.0 - sigmoid(x)) * sigmoid(x)

def softmax(x):
    x = x - np.max(x, axis=-1, keepdims=True)   # オーバーフロー対策
    return np.exp(x) / np.sum(np.exp(x), axis=-1, keepdims=True)
```
お決まりのところです。MNIST手書き数字を読み込んで活性化関数を定義します。

```python: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
        
    # 損失関数計算
    # x:入力データ, t:教師データ
    def loss(self, x, t):
        y = self.predict(x)
        
        return cross_entropy_error(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
        
    # 誤差逆伝播法（Back Propagation）
    def gradient(self, x, t):
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
        grads = {}
        
        batch_num = x.shape[0]
        
        # forward
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)
        
        # backward
        dy = (y - t) / batch_num
        grads['W2'] = np.dot(z1.T, dy)
        grads['b2'] = np.sum(dy, axis=0)
        
        dz1 = np.dot(dy, W2.T)
        da1 = sigmoid_grad(a1) * dz1
        grads['W1'] = np.dot(x.T, da1)
        grads['b1'] = np.sum(da1, axis=0)

        return grads
```
前半推論処理までは以前実装したものとほぼ同じです。`params`変数内に重み、バイアスを保存し、`predict`関数でニューラルネットワークの層の計算を行います。`loss`関数では先ほど定義した損失関数を計算します。`accuracy`関数で入力画像データ`x`からニューラルネットワークの計算の結果得られた`y`と正解ラベル`t`を比較して「合っていたもの/全体」で精度を計算します。
残りの`numerical_gradient`、`gradient`関数は共に勾配を求める関数です。`numerical_gradient`は前回定義した数値微分によって勾配を求める関数です。これでも良いのですが、数値微分は実装が分かりやすい一方演算時間が非常にかかる欠点があります。そこで誤差逆伝播法（Back Propagation）によって勾配を計算する`gradient`関数を用います。誤差逆伝播法は勾配計算を効率よく行う手法で現在一般に用いられていますが、それだけで一本書けるくらい長くなってしまうので今回はそのまま用いることにします（余裕があればまたまとめて記事にします）。

```python:ミニバッチ学習
# データの読み込み
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

# ハイパーパラメータ
iters_num = 10000  # 繰り返しの回数を適宜設定する
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1

train_loss_list = []
train_acc_list = []
test_acc_list = []

# 1エポックあたりの繰り返し数
iter_per_epoch = max(train_size / batch_size, 1)

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)    #数値微分
    grad = network.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("train acc, test acc | " + str(train_acc) + ", " + str(test_acc))

# グラフの描画
markers = {'train': 'o', 'test': 's'}
x = np.arange(len(train_acc_list))
plt.plot(x, train_acc_list, label='train acc')
plt.plot(x, test_acc_list, label='test acc', linestyle='--')
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()
```
実際の学習プログラムです。まず`load_mnist`で画像データを読み込みます。このとき`one_hot_label`をTrueにすることで正解となるラベルだけが1でその他が0となる表現で取得できます。つまり2章 損失関数コードの教師データ`t`の表現です。`TwoLayerNet`のパラメータは以前実装したMNIST手書き数字認識コードを2層にしたものと同じです。画像が28×28ビットなので入力は28×28=784次元、0～9を認識するので出力は10分類です。次に定義している`iters_num`～`learning_rate`はハイパーパラメータといい、ニューラルネットワークが学習して自動で決める重み、バイアスのパラメータと異なり使う側の人間が設定してあげるパラメータです。
続くfor文で実際に学習を行います。`batch_mask`でランダムに選ばれたインデックスで訓練データから抜き出すことでミニバッチ学習を行うことができます。選ばれたミニバッチを対象に勾配を求め、確率的勾配降下法（SGD）によりパラメータを更新します。更新を行うたびに`loss`メソッドで損失関数を計算し、記録します。
さらに`accuracy`メソッドで1エポックごとの精度を記録します。このとき学習を行っている訓練データに対してのみ精度を計算するのではなくテストデータに対しても行ってネットワークの汎化能力を評価します。最後にエポックごとの精度をグラフにします。

![accuracy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/802861/a28708b3-07cc-4521-527d-30763dd8dccf.png)

図よりエポック（学習）を重ねるごとに精度が向上していき95%近い精度まで到達できました。今回は訓練データの精度とテストデータの精度がほぼ同じで進行しているのでニューラルネットワークが学習した結果は十分な汎化能力を持っていると分かります。これがもし訓練データに対して過学習が進んでいると青線の訓練データ精度は高いのにオレンジ点線のテストデータ精度が低いという結果になります。

以上でニューラルネットワークの学習ができるようになりました。



#参考文献
[ゼロから作るDeep-Learning](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)
[ゼロから作るDeep-Learning GitHub](https://github.com/oreilly-japan/deep-learning-from-scratch)
[深層学習 (機械学習プロフェッショナルシリーズ)](https://www.amazon.co.jp/dp/B018K6C99A/ref=dp_kinw_strp_2)