ニューラルネットワークでミニバッチの学習データを固定とランダムのハイブリッドにすると結果がよくなった

# はじめに
[前回](http://qiita.com/hikobotch/items/d8ff5bebcf70083de089)の記事を作成する過程で偶然よい結果を得たので紹介します。

ミニバッチの学習データをランダムにした際の学習結果が[前々回](http://qiita.com/hikobotch/items/018808ef795061176824)より、よくなったので原因を調べていたら固定時 vs ランダム時の学習結果を比較しているつもりが固定時 vs 固定時＋ランダム時の学習結果を比較していることがわかりました。
([前回](http://qiita.com/hikobotch/items/d8ff5bebcf70083de089)の記事ではちゃんと固定時 vs ランダム時の比較になっています)

# 環境
+ python: 2.7.6
+ chainer: 1.8.0

# 学習内容
一般的なエポック毎にミニバッチの学習データをランダムシャッフルにした場合と、固定とランダムシャッフルのハイブリッドにした場合の違いを確認します。
学習させるのは例のごとくsin関数です。

[training data]

+ input: theta(0~2π, 1000分割)
+ output: sin(theta)

# 実装
## モデルを2回作成
今回の発見はランダム時の学習前にモデルの学習結果をリセットし忘れたことによるものです。
リセットの方法がわからず、モデル名等を変更するのも面倒だったため同じ処理を2回コーディングています。(誰かリセット方法を教えてください…)


```python:モデルを2回作成
# モデル作成
model = MyChain(n_units)
optimizer = optimizers.Adam()
optimizer.setup(model)

'''
省略 (一般的なランダムのみ学習)
'''

# モデル作成(モデルの学習結果のリセット方法がわからないため再度モデル作成)
model = MyChain(n_units)
optimizer = optimizers.Adam()
optimizer.setup(model)
```

## 学習パラメータ

+ ミニバッチサイズ(batchsize): 20
+ エポック(n_epoch): 500
+ 隠れ層の数: 2
+ 隠れ層のユニット数(n_units): 100
+ 活性化関数: 正規化線形関数(relu)
+ ドロップアウト(dropout): なし(0%)
+ 最適化: Adam
+ 損失誤差関数: 平均二乗誤差関数(mean_squared_error)

パラメータは全て適当。

ハイブリッド時のエポックは固定とランダムをそれぞれを250回ずつ実施。

## コード全体
なぐり書きの汚いコードですが動くのでよしとします。

```python:全体
# -*- coding: utf-8 -*-

# とりあえず片っ端からimport
import numpy as np
import chainer
from chainer import cuda, Function, gradient_check, Variable, optimizers, serializers, utils
from chainer import Link, Chain, ChainList
import chainer.functions as F
import chainer.links as L
import time
from matplotlib import pyplot as plt

# データ
def get_dataset(N):
    x = np.linspace(0, 2 * np.pi, N)
    y = np.sin(x)
    return x, y

# ニューラルネットワーク
class MyChain(Chain):
    def __init__(self, n_units=10):
        super(MyChain, self).__init__(
             l1=L.Linear(1, n_units),
             l2=L.Linear(n_units, n_units),
             l3=L.Linear(n_units, 1))

    def __call__(self, x_data, y_data):
        x = Variable(x_data.astype(np.float32).reshape(len(x_data),1)) # Variableオブジェクトに変換
        y = Variable(y_data.astype(np.float32).reshape(len(y_data),1)) # Variableオブジェクトに変換
        return F.mean_squared_error(self.predict(x), y)

    def  predict(self, x):
        h1 = F.relu(self.l1(x))
        h2 = F.relu(self.l2(h1))
        h3 = self.l3(h2)
        return h3

    def get_predata(self, x):
        return self.predict(Variable(x.astype(np.float32).reshape(len(x),1))).data

# main
if __name__ == "__main__":

    # 学習データ
    N = 1000
    x_train, y_train = get_dataset(N)

    # 学習パラメータ
    batchsize = 10
    n_epoch = 500
    n_units = 100

    # モデル作成
    model = MyChain(n_units)
    optimizer = optimizers.Adam()
    optimizer.setup(model)

    # 学習ループ(一般的なランダムのみ)
    print "start..."
    normal_losses =[]
    start_time = time.time()
    for epoch in range(1, n_epoch + 1):

        # training
        perm = np.random.permutation(N)
        sum_loss = 0
        for i in range(0, N, batchsize):
            x_batch = x_train[perm[i:i + batchsize]]
            y_batch = y_train[perm[i:i + batchsize]]
            model.zerograds()
            loss = model(x_batch,y_batch)
            sum_loss += loss.data * batchsize
            loss.backward()
            optimizer.update()

        average_loss = sum_loss / N
        normal_losses.append(average_loss)

        # 学習過程を出力
        if epoch % 10 == 0:
            print "(normal) epoch: {}/{} normal loss: {}".format(epoch, n_epoch, average_loss)

    interval = int(time.time() - start_time)
    print "実行時間(normal): {}sec".format(interval)

    # モデル作成(モデルの学習結果のリセット方法がわからないため再度モデル作成)
    model = MyChain(n_units)
    optimizer = optimizers.Adam()
    optimizer.setup(model)

    # 学習ループ(ハイブリッド)
    # ミニバッチの学習データを固定とランダムでハイブリッド
    hybrid_losses =[]
    for order in ["fixed", "random"]:
        start_time = time.time()
        for epoch in range(1, (n_epoch + 1) / 2):

            # training
            perm = np.random.permutation(N)
            sum_loss = 0
            for i in range(0, N, batchsize):
                if order == "fixed": # 学習する順番が固定
                    x_batch = x_train[i:i + batchsize]
                    y_batch = y_train[i:i + batchsize]
                elif order == "random": # 学習する順番がランダム
                    x_batch = x_train[perm[i:i + batchsize]]
                    y_batch = y_train[perm[i:i + batchsize]]

                model.zerograds()
                loss = model(x_batch,y_batch)
                sum_loss += loss.data * batchsize
                loss.backward()
                optimizer.update()

            average_loss = sum_loss / N
            hybrid_losses.append(average_loss)

            # 学習過程を出力
            if epoch % 10 == 0:
                print "(hybrid) epoch: {}/{} {} loss: {}".format(epoch, n_epoch, order, average_loss)

        interval = int(time.time() - start_time)
        print "実行時間(hybrid {}): {}sec".format(order, interval)

    print "end"

    # 誤差のグラフ作成
    plt.plot(normal_losses, label = "normal_loss")
    plt.plot(hybrid_losses, label = "hybrid_loss")
    plt.yscale('log')
    plt.legend()
    plt.grid(True)
    plt.title("loss")
    plt.xlabel("epoch")
    plt.ylabel("loss")
    plt.show()
```

# 実行結果
## 誤差
一般的な方式(normal)と比べて固定とランダムのハイブリッド方式(hybrid)は誤差が一桁よい結果になりました。
横軸中央でhybrid_lossが急激に小さくなっているのが固定からランダムへの切替りです。

同じエポック数なら固定を少なく、ランダムを多くする方がよい結果になりそうです。

![sin_hybrid_order.png](https://qiita-image-store.s3.amazonaws.com/0/79218/9717eb2f-cdb3-de84-b78f-896ebeee0dec.png)

# まとめ
学術的な原因はわかりませんが今回の学習対象(0〜2πまでのsin関数)ではミニバッチの学習データを固定とランダムのハイブリッドにすると一般的なランダムのみの方式と比べて誤差が一桁よくなりました。

これが噂の過学習かと思い、テストもしたのですが学習時と同様の結果でした。

今回のような細かい工夫の積み重ねが高精度なニューラルネットワーク作成につながっているような気がしました。