kaeken(嘉永島健司)のTech探究ブログ

主に情報科学/情報技術全般に関する知見をポストします。(最近は、特にData Science、機械学習、深層学習、統計学、Python、数学、ビッグデータ)

6章 学習に関するテクニック Batch Normalization 『ゼロから作るDeep Learning』

f:id:kaeken:20161111205001p:plain

6章 学習に関するテクニック『ゼロから作るDeep Learning』

続いて、Batch Normalization

Batch Normalization(Batch Norm)とは:ミニバッチごとに正規化する手法。

メリット:

学習を速く進行させることができる
初期値にそれほど依存しない(初期値にロバスト)
過学習を抑制する

以下さまざまな初期値で比較した、サンプルスクリプト

# cat batch_norm_test_save.py

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
import matplotlib.pyplot as plt
plt.switch_backend('agg')

from dataset.mnist import load_mnist
from common.multi_layer_net_extend import MultiLayerNetExtend
from common.optimizer import SGD, Adam

(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)

# 学習データを削減
x_train = x_train[:1000]
t_train = t_train[:1000]

max_epochs = 20
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.01


def __train(weight_init_std):
    bn_network = MultiLayerNetExtend(input_size=784, hidden_size_list=[100, 100, 100, 100, 100], output_size=10,
                                    weight_init_std=weight_init_std, use_batchnorm=True)
    network = MultiLayerNetExtend(input_size=784, hidden_size_list=[100, 100, 100, 100, 100], output_size=10,
                                weight_init_std=weight_init_std)
    optimizer = SGD(lr=learning_rate)

    train_acc_list = []
    bn_train_acc_list = []

    iter_per_epoch = max(train_size / batch_size, 1)
    epoch_cnt = 0

    for i in range(1000000000):
        batch_mask = np.random.choice(train_size, batch_size)
        x_batch = x_train[batch_mask]
        t_batch = t_train[batch_mask]

        for _network in (bn_network, network):
            grads = _network.gradient(x_batch, t_batch)
            optimizer.update(_network.params, grads)

        if i % iter_per_epoch == 0:
            train_acc = network.accuracy(x_train, t_train)
            bn_train_acc = bn_network.accuracy(x_train, t_train)
            train_acc_list.append(train_acc)
            bn_train_acc_list.append(bn_train_acc)

            print("epoch:" + str(epoch_cnt) + " | " + str(train_acc) + " - " + str(bn_train_acc))

            epoch_cnt += 1
            if epoch_cnt >= max_epochs:
                break

    return train_acc_list, bn_train_acc_list


# 3.グラフの描画==========
weight_scale_list = np.logspace(0, -4, num=16)
x = np.arange(max_epochs)

for i, w in enumerate(weight_scale_list):
    print( "============== " + str(i+1) + "/16" + " ==============")
    train_acc_list, bn_train_acc_list = __train(w)

    plt.subplot(4,4,i+1)
    plt.title("W:" + str(w))
    if i == 15:
        plt.plot(x, bn_train_acc_list, label='Batch Normalization', markevery=2)
        plt.plot(x, train_acc_list, linestyle = "--", label='Normal(without BatchNorm)', markevery=2)
    else:
        plt.plot(x, bn_train_acc_list, markevery=2)
        plt.plot(x, train_acc_list, linestyle="--", markevery=2)

    plt.ylim(0, 1.0)
    if i % 4:
        plt.yticks([])
    else:
        plt.ylabel("accuracy")
    if i < 12:
        plt.xticks([])
    else:
        plt.xlabel("epochs")
    plt.legend(loc='lower right')

#plt.show()
plt.savefig('batch_norm_test_save.png')

f:id:kaeken:20161111205001p:plain 若干表示が崩れたが、確かに多くの場合Batch Normの方が速く学習が進む。

6章 学習に関するテクニック 推奨される重み初期値 『ゼロから作るDeep Learning』

f:id:kaeken:20161111191540p:plain 6章 学習に関するテクニック 『ゼロから作るDeep Learning』

続き。

重みの初期値について。

結論として、重みの初期値をゼロにするのはNG。

重みの対照的な構造を崩し、ランダムな初期値を設定する。

勾配消失gradient vanishing:逆伝播での勾配の値が徐々に小さくなること。

多層NNでは勾配消失が問題になるので、推奨された初期値を使う。

活性化関数にS字カーブ(sigmoid,tanh)を使うなら「Xavierの初期値」

活性化関数にReLUを使うなら「Heの初期値」

以下、比較したサンプルコード

# cat weight_init_compare_save.py


# coding: utf-8
import os
import sys

sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
import matplotlib.pyplot as plt
plt.switch_backend('agg')

from dataset.mnist import load_mnist
from common.util import smooth_curve
from common.multi_layer_net import MultiLayerNet
from common.optimizer import SGD


# 0:MNISTデータの読み込み==========
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)

train_size = x_train.shape[0]
batch_size = 128
max_iterations = 2000


# 1:実験の設定==========
weight_init_types = {'std=0.01': 0.01, 'Xavier': 'sigmoid', 'He': 'relu'}
optimizer = SGD(lr=0.01)

networks = {}
train_loss = {}
for key, weight_type in weight_init_types.items():
    networks[key] = MultiLayerNet(input_size=784, hidden_size_list=[100, 100, 100, 100],
                                  output_size=10, weight_init_std=weight_type)
    train_loss[key] = []


# 2:訓練の開始==========
for i in range(max_iterations):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]

    for key in weight_init_types.keys():
        grads = networks[key].gradient(x_batch, t_batch)
        optimizer.update(networks[key].params, grads)

        loss = networks[key].loss(x_batch, t_batch)
        train_loss[key].append(loss)

    if i % 100 == 0:
        print("===========" + "iteration:" + str(i) + "===========")
        for key in weight_init_types.keys():
            loss = networks[key].loss(x_batch, t_batch)
            print(key + ":" + str(loss))

# 3.グラフの描画==========
markers = {'std=0.01': 'o', 'Xavier': 's', 'He': 'D'}
x = np.arange(max_iterations)
for key in weight_init_types.keys():
    plt.plot(x, smooth_curve(train_loss[key]), marker=markers[key], markevery=100, label=key)
plt.xlabel("iterations")
plt.ylabel("loss")
plt.ylim(0, 2.5)
plt.legend()
#plt.show()
plt.savefig('weight_init_compare.png')

f:id:kaeken:20161111191540p:plain

確かにXavier,Heの初期値の方が学習の進みが速い。

重みを適当に設定せず、適切な学習が進むように適切に設定する必要がある。

6章 学習に関するテクニック SGDとMomentum, AdaGrad, Adam 『ゼロから作るDeep Learning』

f:id:kaeken:20161110202746p:plain

6章 学習に関するテクニック

この章では、NN学習でキーとなるアイデアについて学ぶ。

パラメータの更新
重みの初期値
Batch Normalization
正則化
ハイパーパラメータの検証
まとめ

パラメータの更新について。

最適化optimizationとは:NN学習目的である「損失関数を最小化する最適なパラメータ」を見つける問題を解くこと。

確率的勾配降下法SGDの欠点として、関数の形状が等方向でないと非効率な経路で探索する

class SGD:

    """確率的勾配降下法(Stochastic Gradient Descent)"""

    def __init__(self, lr=0.01):
        self.lr = lr

    def update(self, params, grads):
        for key in params.keys():
            params[key] -= self.lr * grads[key]

欠点を改善するため3つの手法Momentum, AdaGrad, Adamがある。以下、サンプルコード。

# Momentum: 速度概念の導入
class Momentum:

    """Momentum SGD"""

    def __init__(self, lr=0.01, momentum=0.9):
        self.lr = lr
        self.momentum = momentum
        self.v = None

    def update(self, params, grads):
        if self.v is None:
            self.v = {}
            for key, val in params.items():
                self.v[key] = np.zeros_like(val)

        for key in params.keys():
            self.v[key] = self.momentum*self.v[key] - self.lr*grads[key]
            params[key] += self.v[key]

# AdaGrad: 学習係数の減衰learning rate decayの導入
class AdaGrad:

    """AdaGrad"""

    def __init__(self, lr=0.01):
        self.lr = lr
        self.h = None

    def update(self, params, grads):
        if self.h is None:
            self.h = {}
            for key, val in params.items():
                self.h[key] = np.zeros_like(val)

        for key in params.keys():
            self.h[key] += grads[key] * grads[key]
            params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)


# RMSProp: AdaGradで更新量が0になる問題に対して指数移動平均を導入
class RMSprop:

    """RMSprop"""

    def __init__(self, lr=0.01, decay_rate = 0.99):
        self.lr = lr
        self.decay_rate = decay_rate
        self.h = None

    def update(self, params, grads):
        if self.h is None:
            self.h = {}
            for key, val in params.items():
                self.h[key] = np.zeros_like(val)

        for key in params.keys():
            self.h[key] *= self.decay_rate
            self.h[key] += (1 - self.decay_rate) * grads[key] * grads[key]
            params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)


# Adam: MomentumとAdaGradを融合したような手法
class Adam:

    """Adam (http://arxiv.org/abs/1412.6980v8)"""

    def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
        self.lr = lr
        self.beta1 = beta1
        self.beta2 = beta2
        self.iter = 0
        self.m = None
        self.v = None

    def update(self, params, grads):
        if self.m is None:
            self.m, self.v = {}, {}
            for key, val in params.items():
                self.m[key] = np.zeros_like(val)
                self.v[key] = np.zeros_like(val)

        self.iter += 1
        lr_t  = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter)

        for key in params.keys():
            #self.m[key] = self.beta1*self.m[key] + (1-self.beta1)*grads[key]
            #self.v[key] = self.beta2*self.v[key] + (1-self.beta2)*(grads[key]**2)
            self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key])
            self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key])

            params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)

            #unbias_m += (1 - self.beta1) * (grads[key] - self.m[key]) # correct bias
            #unbisa_b += (1 - self.beta2) * (grads[key]*grads[key] - self.v[key]) # correct bias
            #params[key] += self.lr * unbias_m / (np.sqrt(unbisa_b) + 1e-7)

続いて、SGD, Momentum, AdaGrad, Adamの比較

# coding: utf-8
import os
import sys
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import matplotlib.pyplot as plt
plt.switch_backend('agg')
from dataset.mnist import load_mnist
from common.util import smooth_curve
from common.multi_layer_net import MultiLayerNet
from common.optimizer import *


# 0:MNISTデータの読み込み==========
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)

train_size = x_train.shape[0]
batch_size = 128
max_iterations = 2000


# 1:実験の設定==========
optimizers = {}
optimizers['SGD'] = SGD()
optimizers['Momentum'] = Momentum()
optimizers['AdaGrad'] = AdaGrad()
optimizers['Adam'] = Adam()
#optimizers['RMSprop'] = RMSprop()

networks = {}
train_loss = {}
for key in optimizers.keys():
    networks[key] = MultiLayerNet(
        input_size=784, hidden_size_list=[100, 100, 100, 100],
        output_size=10)
    train_loss[key] = []

# 2:訓練の開始==========
for i in range(max_iterations):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]

    for key in optimizers.keys():
        grads = networks[key].gradient(x_batch, t_batch)
        optimizers[key].update(networks[key].params, grads)

        loss = networks[key].loss(x_batch, t_batch)
        train_loss[key].append(loss)

    if i % 100 == 0:
        print( "===========" + "iteration:" + str(i) + "===========")
        for key in optimizers.keys():
            loss = networks[key].loss(x_batch, t_batch)
            print(key + ":" + str(loss))


# 3.グラフの描画==========
markers = {"SGD": "o", "Momentum": "x", "AdaGrad": "s", "Adam": "D"}
x = np.arange(max_iterations)
for key in optimizers.keys():
    plt.plot(x, smooth_curve(train_loss[key]), marker=markers[key], markevery=100, label=key)
plt.xlabel("iterations")
plt.ylabel("loss")
plt.ylim(0, 1)
plt.legend()
#plt.show()
plt.savefig('optimizer_compare_mnist.png')

確かにSGDより優れた手法であることが分かった。 f:id:kaeken:20161110202746p:plain

以下、続く。

重みの初期値 Batch Normalization 正則化 ハイパーパラメータの検証 まとめ

5章 誤差逆伝播法(ごさぎゃくでんぱほう)Backpropagation『ゼロから作るDeep Learning』

5章誤差逆伝播

いよいよ誤差逆伝播法まできた。

数式より計算グラフの方が理解しやすいらしい。

詳しくは本書参照。

まずはレイヤ概念の導入。

レイヤ(層)とは:NNにおける機能の単位を指す。レイヤ単位で実装することで、ブロックのように積み上げていける。

まず、乗算レイヤの実装と、加算レイヤの実装について。

# cat layer_naive.py


# coding: utf-8


class MulLayer:
    def __init__(self):
        self.x = None
        self.y = None

    def forward(self, x, y):
        self.x = x
        self.y = y
        out = x * y

        return out

    def backward(self, dout): #dout微分
        dx = dout * self.y
        dy = dout * self.x

        return dx, dy


class AddLayer:
    def __init__(self):
        pass

    def forward(self, x, y):
        out = x + y

        return out

    def backward(self, dout):
        dx = dout * 1
        dy = dout * 1

        return dx, dy

つぎに、活性化関数レイヤ(ReLUレイヤ、Sigmoidレイヤ)の実装について。

# cat common/layers.py

# coding: utf-8
import numpy as np
from common.functions import *
from common.util import im2col, col2im


class Relu:
    def __init__(self):
        self.mask = None

    def forward(self, x):
        self.mask = (x <= 0)
        out = x.copy()
        out[self.mask] = 0

        return out

    def backward(self, dout):
        dout[self.mask] = 0
        dx = dout

        return dx


class Sigmoid:
    def __init__(self):
        self.out = None

    def forward(self, x):
        out = sigmoid(x)
        self.out = out
        return out

    def backward(self, dout):
        dx = dout * (1.0 - self.out) * self.out

        return dx

つぎに、Affineレイヤの実装について。

Affineとは:行列の内積を行うレイヤ。幾何学で「アフィン変換」と呼ばれる。

class Affine:
    def __init__(self, W, b):
        self.W =W
        self.b = b

        self.x = None
        self.original_x_shape = None
        # 重み・バイアスパラメータの微分
        self.dW = None
        self.db = None

    def forward(self, x):
        # テンソル対応
        self.original_x_shape = x.shape
        x = x.reshape(x.shape[0], -1)
        self.x = x

        out = np.dot(self.x, self.W) + self.b

        return out

    def backward(self, dout):
        dx = np.dot(dout, self.W.T)
        self.dW = np.dot(self.x.T, dout)
        self.db = np.sum(dout, axis=0)

        dx = dx.reshape(*self.original_x_shape)  # 入力データの形状に戻す(テンソル対応)
        return dx

つぎに、Softmaxレイヤの実装について。

Softmaxレイヤは学習時のみ必要で推論時には必要ない。

サンプルでは、Softmax関数と交差エントロピー誤差のレイヤSoftmax-with-Lossを定義している

導出過程が複雑なので、付録Aをあとで参照。

class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None
        self.y = None # softmaxの出力
        self.t = None # 教師データ

    def forward(self, x, t):
        self.t = t
        self.y = softmax(x)
        self.loss = cross_entropy_error(self.y, self.t)

        return self.loss

    def backward(self, dout=1):
        batch_size = self.t.shape[0]
        if self.t.size == self.y.size: # 教師データがone-hot-vectorの場合
            dx = (self.y - self.t) / batch_size
        else:
            dx = self.y.copy()
            dx[np.arange(batch_size), self.t] -= 1
            dx = dx / batch_size

        return dx

最後に、誤差逆伝播法の実装について。

以下の学習4ステップのうち、ステップ2勾配の算出で登場

学習の4ステップ

ステップ1ミニバッチ:ミニバッチをランダムに選択
ステップ2勾配算出:ミニバッチの損失関数を減らす勾配を算出
ステップ3パラメータ更新:重みパラメータを勾配方向に微小量だけ更新
ステップ4反復:ステップ1−3を反復
# cat two_layer_net.py


# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
from common.layers import *
from common.gradient import numerical_gradient
from collections import OrderedDict


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)

        # レイヤの生成
        self.layers = OrderedDict()
        self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])
        self.layers['Relu1'] = Relu()
        self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2'])

        self.lastLayer = SoftmaxWithLoss()

    def predict(self, x):
        for layer in self.layers.values():
            x = layer.forward(x)

        return x

    # x:入力データ, t:教師データ
    def loss(self, x, t):
        y = self.predict(x)
        return self.lastLayer.forward(y, t)

    def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        if t.ndim != 1 : t = np.argmax(t, axis=1)

        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy

    # x:入力データ, t:教師データ
    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

    def gradient(self, x, t):
        # forward
        self.loss(x, t)

        # backward
        dout = 1
        dout = self.lastLayer.backward(dout)

        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)

        # 設定
        grads = {}
        grads['W1'], grads['b1'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
        grads['W2'], grads['b2'] = self.layers['Affine2'].dW, self.layers['Affine2'].db

        return grads

実装した誤差逆伝播法の勾配確認をおこなう。

勾配確認gradient checkとは:誤差逆伝播法と数値微分の結果を比較し、ほぼ近似できることで誤差逆伝播法の正しさを確認すること。

# cat deep-learning-from-scratch/ch05/gradient_check.py
# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet

# データの読み込み
(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)

x_batch = x_train[:3]
t_batch = t_train[:3]

grad_numerical = network.numerical_gradient(x_batch, t_batch)
grad_backprop = network.gradient(x_batch, t_batch)

for key in grad_numerical.keys():
    diff = np.average( np.abs(grad_backprop[key] - grad_numerical[key]) )
    print(key + ":" + str(diff))

勾配確認のサンプルコードを実行してみる:

# py gradient_check.py
W2:9.77654477536e-13
W1:2.37797639143e-13
b2:1.2012613404e-10
b1:7.43019632366e-13

ほぼ誤差がないことが分かった。

そして、誤差逆伝播法を訓練データを学習させてみる。

# py train_neuralnet.py

0.102266666667 0.1016
0.90125 0.9073
0.9205 0.9221
0.936033333333 0.9349
0.945816666667 0.9461
0.9512 0.9504
0.957433333333 0.955
0.961133333333 0.959
0.965266666667 0.962
0.967566666667 0.9636
0.970366666667 0.9637
0.97065 0.9635
0.974466666667 0.9674
0.974416666667 0.9671
0.976816666667 0.968
0.97775 0.9698
0.978783333333 0.971

認識精度がかなり向上していることがわかる。

5章まとめ問題

・◯◯1を用いれば計算過程を視覚的に把握できる
・◯◯2は局所的な計算によって全体の計算を構成する
・◯◯1の◯◯3は通常の計算を行い、◯◯4によって各ノードの微分が求まる
・◯◯5とは、NNの構成要素を◯◯6で実装することで勾配計算を効率化する
・◯◯6とは、◯◯5と◯◯7の結果を比較して◯◯5の実装が誤っていないことが確認できる

ニューラルネットワークを図解するためグラフ描画ツールgraphviz導入

こんな感じのグラフがスクリプトで作成できるgraphviz f:id:kaeken:20161108200731p:plain

以下、ギャラリー。

Gallery | Graphviz - Graph Visualization Software

まずは、インストール。

# まずは、Anacondaでgraphvizパッケージをインストール
conda install -c anaconda graphviz=2.38.0
# が、importできず、改めてpipで入れなおし
pip install graphviz

# 確認
dot -V
dot - graphviz version 2.38.0 (20140413.2041)

次に、dotファイル。

# cat neuralnet.dot


digraph G {

  rankdir=LR
  splines=line
  nodesep=.05;

  node [label=""];

  subgraph cluster_0 {
  color=white;
  node [style=solid,color=blue4, shape=circle];
  x1 [label="x1"];
  x2 [label="x2"];
  label = "input";
  }

  subgraph cluster_1 {
  color=white;
  node [style=solid,color=red2, shape=circle];
  a12 a22 a32;
  label = "layer 2";
  }

  subgraph cluster_2 {
  color=white;
  node [style=solid,color=red2, shape=circle];
  a13 a23;
  label = "layer 3";
  }

  subgraph cluster_3 {
  color=white;
  node [style=solid,color=seagreen2, shape=circle];
  y1 [label="y1"];
  y2 [label="y2"];
  label="output";
  }

  x1 -> a12;
  x1 -> a22;
  x1 -> a32;

  x2 -> a12;
  x2 -> a22;
  x2 -> a32;

  a12 -> a13
  a22 -> a13
  a32 -> a13

  a12 -> a23
  a22 -> a23
  a32 -> a23

  a13 -> y1
  a23 -> y1

  a13 -> y2
  a23 -> y2
}

最後に、画像出力。

# dot -Tpng -O neuralnet.dot

ただ、手元の環境だとなぜかラベルがASCIIなのに文字化けるので、オンライン上で実行できるサービスを探してみると、いろいろある。

http://www.webgraphviz.com/

ただ、ソースを貼って実行するだけ。

一方、こちらは、 HTMLのimageタグにそのままdotファイル内の記述をsrcに指定してやれば画像生成される便利なサービス

http://www.gravizo.com/

こんな感じで指定。面白い。

<img src='http://g.gravizo.com/g?
 digraph G {
   main -> parse -> execute;
   main -> init;
   main -> cleanup;
   execute -> make_string;
   execute -> printf
   init -> make_string;
   main -> printf;
   execute -> compare;
 }
'/>

2〜4章まとめの確認問題『ゼロから作るDeep Learning』

いったん4章までの復習がてら、2・3・4章末まとめから問題を作成してみる。

2章
・パーセプトロンは、ある◯◯を与えたら、決まった◯◯を行うアルゴリズムである。
・パーセプトロンでは、◯◯と◯◯をパラメータとして設定する。
・パーセプトロンを用いれば、◯◯や◯◯ゲートなどの論理回路を表現できる。
・単層パーセプトロンでは◯◯ゲートを表現できないが、2層パーセプトロンなら表現できる。
・単層パーセプトロンでは◯◯領域だけしか表現できないが、二層パーセプトロンでは◯◯領域を表現できる。
・多層パーセプトロンは、理論上◯◯を表現できる。


3章
・ニューラルネットワーク(NN)では、活性化関数として、◯◯や◯◯のような滑らかに変化する関数を利用する。
・NumPyの◯◯を上手く使うことでNNを効率良く実装できる。
・機械学習(ML)の問題は、◯◯1問題と◯◯2問題に大別できる。
・出力層で使用する活性化関数は、◯◯1問題では◯◯関数、◯◯2問題では◯◯関数を一般的に利用する。
・◯◯2問題では、出力層のニューロン数を◯◯に設定する。
・入力データのまとまりである◯◯単位で推論処理を行えば、計算が高速化する。


4章
・MLで使用するデータセットは、◯◯1データと◯◯2データに分けて使用する。
・◯◯1データで学習を行い、◯◯能力を◯◯2データで評価する。
・NNの学習は、◯◯を指標とし、◯◯の値が小さくなるように◯◯3を更新する。
・◯◯3を更新する際には、◯◯3の◯◯を利用して、◯◯方向に◯◯を更新する作業を繰り返す。
・微小な値を与えたときの差分によって微分を求めることを◯◯4という。
・◯◯4によって、◯◯を求めることができる。
・◯◯4の計算は低速だが実装は簡単である一方、◯◯の実装は複雑だが高速に◯◯を求められる。


3章辺りから理解曲線勾配の上昇率が高くなってきてキツいが、3歩進んで2歩下がっても、理論上最終ゴールに到達できるのでコツコツ進む。

4章ニューラルネットワークの学習 学習アルゴリズムの実装『ゼロから作るDeep Learning』

4章続き。 学習アルゴリズムの実装

改めて学習とは:適応可能な重みとバイアスを訓練データに適応するように調整すること

学習の4ステップ

ステップ1:ミニバッチをランダムに選択
ステップ2:ミニバッチの損失関数を減らす勾配を算出
ステップ3:重みパラメータを勾配方向に微小量だけ更新
ステップ4:ステップ1−3を反復

ミニバッチをランダム選択することから、
確率的勾配下降法(SGD, stochastic gradient descent)と呼ぶ。

2層ニューラルネットワークのクラスプログラムを参照。今までの総まとめ。

# cat two_layer_net.py


# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
from common.functions import *
from common.gradient import numerical_gradient


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

    # x:入力データ, t:教師データ
    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

    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)

        da1 = np.dot(dy, W2.T)
        dz1 = sigmoid_grad(a1) * da1
        grads['W1'] = np.dot(x.T, dz1)
        grads['b1'] = np.sum(dz1, axis=0)

        return grads

そして、バッチサイズ100を10000回SGDでパラメータ更新するサンプルプログラム

# cat train_neuralnet_save.py

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
import matplotlib.pyplot as plt
plt.switch_backend('agg')
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet

# データの読み込み
(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 = []

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)

    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()
plt.savefig('train_neuralnet.png')

そして、実行結果。1エポックごとの認識精度が向上していることがわかる。

# py train_neuralnet_save.py
train acc, test acc | 0.0987166666667, 0.098
train acc, test acc | 0.788433333333, 0.7953
train acc, test acc | 0.874716666667, 0.8796
train acc, test acc | 0.898033333333, 0.9014
train acc, test acc | 0.9083, 0.9109
train acc, test acc | 0.9141, 0.9171
train acc, test acc | 0.919933333333, 0.9215
train acc, test acc | 0.924516666667, 0.9264
train acc, test acc | 0.927233333333, 0.9293
train acc, test acc | 0.930866666667, 0.9316
train acc, test acc | 0.93405, 0.9344
train acc, test acc | 0.937083333333, 0.9367
train acc, test acc | 0.93835, 0.9372
train acc, test acc | 0.941066666667, 0.9402
train acc, test acc | 0.94335, 0.9426
train acc, test acc | 0.945, 0.9442
train acc, test acc | 0.946916666667, 0.9459

f:id:kaeken:20161108192243p:plain

訓練データとテストデータに差異がほぼないことから過学習が起きていないことも分かった。

4章ニューラルネットワークの学習 数値微分、偏微分、勾配法、学習率『ゼロから作るDeep Learning』

『ゼロから作るDeep Learning』4章続き。

微分とは:ある瞬間の変化量

数値微分numerical differentiationとは:微小な差分によって微分を求めること

def numerical_diff(f, x):
  h = 1e-4 #0.0001程度の値が適当な微小値
  return ( f(x+h) - f(x-h) ) / (2*h)

サンプルコードを改変してグラフを生成してみる

# cat gradient_1d_save.py
# coding: utf-8
import numpy as np
import matplotlib.pylab as plt
plt.switch_backend('agg')


def numerical_diff(f, x):
    h = 1e-4 # 0.0001
    return (f(x+h) - f(x-h)) / (2*h)


def function_1(x):
    return 0.01*x**2 + 0.1*x


def tangent_line(f, x):
    d = numerical_diff(f, x)
    print(d)
    y = f(x) - d*x
    return lambda t: d*t + y

x = np.arange(0.0, 20.0, 0.1)
y = function_1(x)
plt.xlabel("x")
plt.ylabel("f(x)")

tf = tangent_line(function_1, 5) # 5->10に変更すれば2つ目の画像になる
y2 = tf(x)

plt.plot(x, y)
plt.plot(x, y2)
plt.savefig('gradient_1d.png')

f:id:kaeken:20161107192131p:plain f:id:kaeken:20161107192138p:plain

続いて、偏微分

偏微分とは:複数の変数からなる関数の微分

x0, x1の2変数関数の定義とグラフを表示

def function_2(x):
  return x[0]**2 + x[1]**2

# 3Dグラフを描画
# cat multivariate_func_save.py
# coding: utf-8
import numpy as np
import matplotlib.pylab as plt
plt.switch_backend('agg')
from mpl_toolkits.mplot3d import Axes3D

x = np.meshgrid(np.arange(-3, 3, 0.1), np.arange(-3, 3, 0.1))
z = x[0]**2 + x[1]**2

fig = plt.figure()
ax = Axes3D(fig)
ax.plot_wireframe(x[0], x[1], z)

plt.xlim(-3.5, 3.5)
plt.ylim(-4.5, 4.5)
plt.xlabel("x0")
plt.ylabel("x1")
plt.savefig('multivariate_func.png')

f:id:kaeken:20161107195025p:plain

ところで、Python微分コマンドなどあるのだろうか、と調査するとSympyで数学できるらしいので、さっそく導入

# conda install -c anaconda sympy=1.0
# py
>>> from sympy import *
>>> var('x')
x
>>> diff(sin(x),x)
cos(x)

こちらがドキュメント

Welcome to SymPy’s documentation! — SymPy 1.0 documentation

さて、続いて、勾配。

勾配gradientとは:すべての変数の偏微分をベクトルとしてまとめたもの

# f(x0, x1) = x0**2 + x1**2の勾配図を描くサンプルコード実行

# cat gradient_2d_save.py

# coding: utf-8
# cf.http://d.hatena.ne.jp/white_wheels/20100327/p3
import numpy as np
import matplotlib.pylab as plt
plt.switch_backend('agg')
from mpl_toolkits.mplot3d import Axes3D


def _numerical_gradient_no_batch(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x)

    for idx in range(x.size):
        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 # 値を元に戻す

    return grad


def numerical_gradient(f, X):
    if X.ndim == 1:
        return _numerical_gradient_no_batch(f, X)
    else:
        grad = np.zeros_like(X)

        for idx, x in enumerate(X):
            grad[idx] = _numerical_gradient_no_batch(f, x)

        return grad


def function_2(x):
    if x.ndim == 1:
        return np.sum(x**2)
    else:
        return np.sum(x**2, axis=1)


def tangent_line(f, x):
    d = numerical_gradient(f, x)
    print(d)
    y = f(x) - d*x
    return lambda t: d*t + y

if __name__ == '__main__':
    x0 = np.arange(-2, 2.5, 0.25)
    x1 = np.arange(-2, 2.5, 0.25)
    X, Y = np.meshgrid(x0, x1)

    X = X.flatten()
    Y = Y.flatten()

    grad = numerical_gradient(function_2, np.array([X, Y]) )

    plt.figure()
    plt.quiver(X, Y, -grad[0], -grad[1],  angles="xy",color="#666666")#,headwidth=10,scale=40,color="#444444")
    plt.xlim([-2, 2])
    plt.ylim([-2, 2])
    plt.xlabel('x0')
    plt.ylabel('x1')
    plt.grid()
    plt.legend()
    plt.draw()
    #plt.show()
    plt.savefig('gradient_2d.png')

f:id:kaeken:20161107201154p:plain 確かに3Dグラフの谷に向かってベクトルが向いている

次に、勾配法。

勾配法gradient methodとは:現在の場所から勾配方向に一定距離だけ進み、移動先でも同様に繰り返し勾配方向へ移動することで関数の値を徐々に減らす方法

勾配降下法gradient descent methodとは:最小値を探す勾配法で、ニューラルネットワークの分野で使われる

学習率learning rateとは:一回の学習でどれだけ学習しパラメータを更新するのかを決める割合。大きすぎても小さすぎても最適化できないので調整・確認が必要。

ハイパーパラメータhyperparameterとは:学習率のように、ニューラルネットワークのパラメータとは性質の異なる人為設定するパラメータ。

以下勾配法で最小値を探索するサンプルコード

# cat gradient_method_save.py
# coding: utf-8
import numpy as np
import matplotlib.pylab as plt
plt.switch_backend('agg')
from gradient_2d import numerical_gradient


def gradient_descent(f, init_x, lr=0.01, step_num=100):
    x = init_x
    x_history = []

    for i in range(step_num):
        x_history.append( x.copy() )

        grad = numerical_gradient(f, x)
        x -= lr * grad

    return x, np.array(x_history)


def function_2(x):
    return x[0]**2 + x[1]**2

init_x = np.array([-3.0, 4.0])

lr = 0.1
step_num = 20
x, x_history = gradient_descent(function_2, init_x, lr=lr, step_num=step_num)

plt.plot( [-5, 5], [0,0], '--b')
plt.plot( [0,0], [-5, 5], '--b')
plt.plot(x_history[:,0], x_history[:,1], 'o')

plt.xlim(-3.5, 3.5)
plt.ylim(-4.5, 4.5)
plt.xlabel("X0")
plt.ylabel("X1")
#plt.show()
plt.savefig('gradient_method.png')

f:id:kaeken:20161107202238p:plain

最後にニューラルネットワークの勾配を求めるサンプルコード

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
from common.functions import softmax, cross_entropy_error
from common.gradient import numerical_gradient


class simpleNet:
    def __init__(self):
        self.W = np.random.rand(2,3)

    def predict(self, x):
        return np.dot(x, self.W)

    def loss(self, x, t):
        z = self.predict(x)
        y = softmax(z)
        loss = cross_entropy_error(y, t)

        return loss

x = np.random.rand(2)
t = np.array([0,0,1])

net = simpleNet()

f = lambda w: net.loss(x, t)
dW = numerical_gradient(f, net.W)

print(dW)

4章ニューラルネットワークの学習 データ駆動アプローチ、損失関数、ミニバッチ『ゼロから作るDeep Learning』

3章ではニューラルネットワークの「推論」を実装したが、4章からニューラルネットワークの「学習」を実装する。

「学習」とは:訓練データから最適な重みパラメータ値を自動で獲得すること

パラメータの数は、実際数千〜数億にも及ぶため、手動で調整することは不可能

「データ駆動アプローチ」:いままでの「人」を中心としたアプローチではなく、「データ」を中心としたアプローチ

ニューラルネットワークディープラーニングでは、従来の機械学習以上に、属人性を排している

・機械学習(ML)以前 入力データ → 人力処理 → 出力データ
↓
・ML 入力データ → 人力特徴量 → ML自動処理 → 出力データ
↓
・NNやDL 入力データ → NN/DL自動処理 → 出力データ

機械学習におけるデータの取扱について

2種類のデータ:「訓練(教師)データ」と「テストデータ」

まず「訓練(教師)データ」で学習し、最適なパラメータを探索

つぎに「テストデータ」で汎化能力を評価し、一部のデータセットだけ過度に対応した「過学習overfitting」を避ける

「損失関数loss function」:ニューラルネットワークの学習で用いられる指標で、主に「二乗和誤差」「交差エントロピー誤差」が用いられる

「二乗和誤差」について。

# 二乗和誤差
def mean_squared_error(y, t): #y: ニューラルネットワークの出力、t:訓練データ
  return np.sum( (y - t)**2 ) / 2

# 実行例
# cat mean_squared_error.py
#!/usr/bin/env python

import numpy as np
import my_module as my

# 訓練データ(「2」を正解とするone-hot表現
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

# 例1: '2'の確率が最も高い場合
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
print(np.sum(y)) #=> 1.0

y = my.mean_squared_error(np.array(y), np.array(t))
print(y) #=> 0.0975

# 例2: '7'の確率が最も高い場合
y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
print(np.sum(y)) #=> 1.0

y = my.mean_squared_error(np.array(y), np.array(t))
print(y) #=> 0.5975

「交差エントロピー誤差」について。

#交差エントロピー誤差
def cross_entropy_error(y, t):
  delta = 1e-7 # マイナス無限大を回避するための微小値
  return - np.sum( t * np.log( y + delta ) )


# 実行例
# cat cross_entropy_error.py
#!/usr/bin/env python

import numpy as np
import my_module as my

# 訓練データ(「2」を正解とするone-hot表現
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

# 例1: '2'の確率が最も高い場合
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
print(np.sum(y)) #=> 1.0

y = my.cross_entropy_error(np.array(y), np.array(t))
print(y) #=> 0.510825457099

# 例2: '7'の確率が最も高い場合
y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
print(np.sum(y)) #=> 1.0

y = my.cross_entropy_error(np.array(y), np.array(t))
print(y) #=> 2.30258409299

続いて、大量にある全データのうち一部を選出して近似とする「ミニバッチ学習」について。

# 0から指定された数字(60000)未満までの数字をランダムに指定個数(10)選択する処理
>>> import numpy as np
>>> np.random.choice(60000, 10)
array([54904, 15528, 35786, 44249, 25077, 37764, 46607,   552, 33463, 12885])
>>> np.random.choice(60000, 10)
array([38745,  8181,  8602, 37811, 24747, 18214, 50371, 13052, 13100, 36289])
>>> np.random.choice(60000, 10)

MNISTデータセットで動作確認

# cat mini_batch.py
# coding: utf-8
import sys, os
sys.path.append(os.pardir)
import numpy as np
from dataset.mnist import load_mnist

(x_train, t_train), (x_test, t_test) = \
  load_mnist(normalize=True, one_hot_label=True)

print(x_train.shape) #=>(60000, 784)
print(t_train.shape) #=>(60000, 10)

# mini_batch
train_size = x_train.shape[0]
batch_size = 10
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]

print(x_batch) #=>ランダム結果
'''
[[ 0.  0.  0. ...,  0.  0.  0.]
 [ 0.  0.  0. ...,  0.  0.  0.]
 [ 0.  0.  0. ...,  0.  0.  0.]
 ...,
 [ 0.  0.  0. ...,  0.  0.  0.]
 [ 0.  0.  0. ...,  0.  0.  0.]
 [ 0.  0.  0. ...,  0.  0.  0.]]
'''

print(t_batch) #=>ランダム結果
'''
[[ 0.  0.  0.  0.  0.  1.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  0.  0.  1.  0.  0.]
 [ 0.  0.  0.  1.  0.  0.  0.  0.  0.  0.]
 [ 1.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
 [ 1.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
 [ 1.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
 [ 0.  0.  0.  1.  0.  0.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  0.  0.  0.  0.  1.]
 [ 0.  0.  0.  0.  1.  0.  0.  0.  0.  0.]
 [ 0.  0.  0.  1.  0.  0.  0.  0.  0.  0.]]
'''

交差エントロピー誤差関数の改変

#交差エントロピー誤差(バッチ対応版)
def cross_entropy_error_batch(y, t):
  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 ) ) / batch_size )


#交差エントロピー誤差(バッチ対応、教師データラベル版)
#one-hot表現ではなく'2'などのラベル
def cross_entropy_error_batch_label(y, t):
  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[np.arange(batch_size), t] ) ) / batch_size )

ニューラルネットワークの学習では、

認識精度を「指標」にしてはいけない。

なぜなら、認識精度を指標にすると、

パラメータの微分がほとんどの場所で0

になってしまうから。

だから、損失関数が必要なのである。

手書き数字認識とバッチ処理『ゼロから作るDeep Learning』

『ゼロから作るDeep Learning』3章最後

いよいよ手書き数字認識に入る。ここでは、「学習」フェーズは完了している前提で、「推論」フェーズのみ順方向伝播方式で実施。

# MNISTという手書き数字画像セットを準備
# git clone https://github.com/oreilly-japan/deep-learning-from-scratch.git
# cd deep-learning-from-scratch/ch03
# ls ../dataset/mnist.py
# ../dataset/mnist.py
# cat mnist_check.py

import sys, os
sys.path.append(os.pardir) #親ディレクトリのファイルをインポート
from dataset.mnist import load_mnist

# load_minst((訓練画像、訓練ラベル), (テスト画像、テストラベル)
# 初回呼び出しはネットDL、2回目以降はローカルpickleファイル読込
# 引数
# normalize: 0.0..1.0に正規化
# flatten: 1次元配列化
# one_hot_label: 正解ラベルのみ1でそれ以外は0にするone-hot表現として格納するか
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False)

print(x_train.shape) # (60000, 784)
print(t_train.shape) # (60000,)
print(x_test.shape) # (10000, 784)
print(t_test.shape) # (10000,)

続いて、MNIST画像の表示スクリプトmnist_show.pyを参考に、保存処理mnist_save.pyを作って確認

# cat mnist_save.py
# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
from dataset.mnist import load_mnist
from PIL import Image

def img_save(img):
    pil_img = Image.fromarray(np.uint8(img))
    pil_img.save('mnist_save_sample.png')

(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False)

img = x_train[0]
label = t_train[0]
print(label)  # 5

print(img.shape)  # (784,)
img = img.reshape(28, 28)  # 形状を元の画像サイズに変形
print(img.shape)  # (28, 28)

img_save(img)

# py mnist_save.py
5
(784,)
(28, 28)

出力された画像

f:id:kaeken:20161107013732p:plain

続いて、ニューラルネットワークの推論処理を確認

# cat neuralnet_mnist.py
# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
import pickle
from dataset.mnist import load_mnist
from common.functions import sigmoid, softmax


def get_data():
  # 前処理pre-processingとして、正規化normalizationを実施
    (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, flatten=True, one_hot_label=False)
    return x_test, t_test

# pickleファイルに保存された学習済重みパラメータの読込
def init_network():
    with open("sample_weight.pkl", 'rb') as f:
        network = pickle.load(f)
    return network


def predict(network, x):
    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']

    a1 = np.dot(x, W1) + b1
    z1 = sigmoid(a1)
    a2 = np.dot(z1, W2) + b2
    z2 = sigmoid(a2)
    a3 = np.dot(z2, W3) + b3
    y = softmax(a3)

    return y


x, t = get_data()
network = init_network()
accuracy_cnt = 0
for i in range(len(x)):
    y = predict(network, x[i])
    p= np.argmax(y) # 最も確率の高い要素のインデックスを取得
    if p == t[i]:
        accuracy_cnt += 1

print("Accuracy:" + str(float(accuracy_cnt) / len(x)))
#=>Accuracy:0.9352で、
# 93.52%正しく分類できた

続いてバッチ処理

バッチbatch:ひとまとまりの入力データ束

バッチ処理によって1枚あたりの処理時間を短縮できる

# cat neuralnet_mnist_batch.py
# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
import pickle
from dataset.mnist import load_mnist
from common.functions import sigmoid, softmax


def get_data():
    (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, flatten=True, one_hot_label=False)
    return x_test, t_test


def init_network():
    with open("sample_weight.pkl", 'rb') as f:
        network = pickle.load(f)
    return network


def predict(network, x):
    w1, w2, w3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']

    a1 = np.dot(x, w1) + b1
    z1 = sigmoid(a1)
    a2 = np.dot(z1, w2) + b2
    z2 = sigmoid(a2)
    a3 = np.dot(z2, w3) + b3
    y = softmax(a3)

    return y


x, t = get_data()
network = init_network()

batch_size = 100 # バッチの数
accuracy_cnt = 0

for i in range(0, len(x), batch_size):
    x_batch = x[i:i+batch_size]
    y_batch = predict(network, x_batch)
    p = np.argmax(y_batch, axis=1)
    accuracy_cnt += np.sum(p == t[i:i+batch_size])

print("Accuracy:" + str(float(accuracy_cnt) / len(x)))

実際に比較してみると、確かに高速化していることが確認できた

# バッチなし
# time py neuralnet_mnist.py
Accuracy:0.9352

real    0m2.220s
user    0m2.859s
sys 0m0.521s

# バッチあり
# time py neuralnet_mnist_batch.py
Accuracy:0.9352

real    0m0.991s
user    0m0.857s
sys 0m0.312s

出力層の設計(分類問題で使うソフトマックス関数)『ゼロから作るDeep Learning』

『ゼロから作るDeep Learning』3章続き

# ニューラルネットワークの問題は、回帰問題と分類問題に大別できる
# 回帰問題とは、入力データから数値予測を行う問題(例 入力:人物画像 → 出力:体重予測)
# 分類問題とは、入力データがどのクラスに属するかという問題(例 入力:人物画像 → 出力:男性クラスor女性クラス)
# 回帰問題では恒等関数を使い、分類問題ではソフトマックス関数softmaxを使う
# ソフトマックス関数は、分子に入力信号の指数関数、分母にすべての入力信号の指数関数の和から構成される
def softmax(x):
  return np.exp(x) / np.sum(np.exp(x))

# 指数関数の値が巨大になりすぎる場合のオーバーフロー対策として、
# 補正値を導入する必要がある(通常、補正値は入力信号の中で最大の値)
def softmax(x):
  c = np.max(x) # オーバーフロー対策の補正値
  return np.exp(x - c) / np.sum(np.exp(x - c))

# 例
import numpy as np
import my_module as my

a = np.array([0.3, 2.9, 4.0])
y = my.softmax(a)
print(y) #=> [ 0.01821127  0.24519181  0.73659691]
print(np.sum(y)) #=> 1.0


# ソフトマックス関数の出力は、0から1の間の実数で、総和は1なので確率として解釈できる
# ソフトマックス関数を適用しても、元データの大小関係は変わらない
# 機械学習の問題を解く手順として、「学習」と「推論」の2フェーズのうち、推論フェーズではソフトマックス関数は省略する
# 出力層のニューロン数は、分類したいクラス数に設定する (例 入力:数字画像 → 出力層:10クラス)

ニューラルネットワーク フォワード方向処理『ゼロから作るDeep Learning』3章

『ゼロから作るDeep Learning』3章続き。

ニューラルネットワークの順方向forward処理について。

# 順方向forward処理:入力から出力方向への伝達処理。あとで逆方向backward処理について学ぶ
# 以下3層ニューラルネットワーク構成とする
# 入力層(第0層)
# 隠れ層1(第1層)
# 隠れ層2(第2層)
# 出力層(第3層)

# cat forward.py
#!/usr/bin/env python

import numpy as np
import my_module as my

def init(): #初期化
  n = {}
  # weight
  n['W1'] = np.array([ [0.1, 0.3, 0.5], [0.2, 0.4, 0.6] ])
  n['W2'] = np.array([ [0.1, 0.4], [0.2, 0.5], [0.3, 0.6] ])
  n['W3'] = np.array([ [0.1, 0.3], [0.2, 0.4] ])
  # bias
  n['b1'] = np.array([0.1, 0.2, 0.3])
  n['b2'] = np.array([0.1, 0.2])
  n['b3'] = np.array([0.1, 0.2])

  return n


def forward(n, x): #入力を出力へ変換する処理をまとめた関数
  W1, W2, W3 = n['W1'], n['W2'], n['W3']
  b1, b2, b3 = n['b1'], n['b2'], n['b3']

  # 1層目
  a1 = np.dot(x, W1) + b1
  z1 = my.sigmoid(a1)
  # 2層目
  a2 = np.dot(z1, W2) + b2
  z2 = my.sigmoid(a2)
  # 出力層
  a3 = np.dot(z2, W3) + b3
  y = my.identity(a3) #何もせずそのまま返す恒等関数

  return y


n = init()
x = np.array([1.0, 0.5])
y = forward(n, x)
print(y)

# 実行
 py forward.py
[ 0.31682708  0.69627909]

次は、出力層の設計に入る。

Pythonで自作関数をモジュールとして読み込む

参考: Python3のimport・下位/上位階層のモジュールをインポートしたい【import】【Python3】 - DRYな備忘録

『ゼロから作るDeep Learning』で自作関数がだんだん説明なしに使われていくので、いったんモジュールにまとめて読み込むことにする。

本番環境ならどこに配置するか考える必要はあるが、ひとまず同一ディレクトリ内にmy_module.pyとしてまとめておく。

$ cat my_module.py # 自作関数をまとめたモジュール
import numpy as np

def step(x): #step関数
  return np.array(x > 0, dtype=np.int)

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

def relu(x): #ReLU関数
  return np.maximum(0, x)

def identity(x): #恒等関数
  return x

# 呼び出し
import my_module as my
...
print(my.sigmoid(param))
...

新たに定義された関数はmy_module.pyに追加していく。

ニューラルネットワーク計算準備としてNumPyで多次元配列処理(内積=ドット積)

f:id:kaeken:20161105113024p:plain 『ゼロから作るDeep Learning』3章続き。 ニューラルネットワークの実装で多次元配列が必要。 そこでPythonのNumPyの多次元配列操作を改めて詳しく学ぶ。

>>> import numpy as np
>>> A = np.array([ [1,2], [3,4], [5,6] ])

>>> A
array([[1, 2],
       [3, 4],
       [5, 6]])

>>> A.shape # 配列の各次元の大きさをタプルで返す
(3, 2)

>>> A.ndim # 配列の次元数
2

# 次元が統一されていない場合は、1次元扱いの様子
>>> B = np.array([ [1], [2,3], [4,5,6] ])
>>> B
array([[1], [2, 3], [4, 5, 6]], dtype=object)
>>> B.shape
(3,)
>>> B.ndim
1

>>> C = np.array([ [1,2,3], [4,5], [6] ])
>>> C
array([[1, 2, 3], [4, 5], [6]], dtype=object)
>>> C.shape
(3,)
>>> C.ndim
1

# 続いて内積
# 行列の内積とは、行と列の要素ごとの積と和によって計算する
# 2次元配列は行列matrixと呼び、横方向の行rowと縦方向の列columnがある

>>> A = np.array([ [1,2], [3,4] ]) 
>>> A
array([[1, 2],
       [3, 4]])
>>> A.shape
(2, 2)

>>> B = np.array([ [5,6], [7,8] ])
>>> B
array([[5, 6],
       [7, 8]])
>>> B.shape
(2, 2)

>>> np.dot(A, B) # 行列A,Bの内積(ドット積)
array([[19, 22],
       [43, 50]])

#計算内容内訳
>>> 1*5 + 2*7
19
>>> 3*5 + 4*7
43
>>> 1*6 + 2*8
22
>>> 3*6 + 4*8
50

#行列の積は、被演算子(A,B)の順番が異なると結果も異なる
>>> np.dot(B, A)
array([[23, 34],
       [31, 46]])

#続いて、2x3の行列と3x2の行列の積
>>> C = np.array([ [1,2,3], [4,5,6] ])
>>> C
array([[1, 2, 3],
       [4, 5, 6]])
>>> C.shape
(2, 3)

>>> D = np.array([ [1,2], [3,4], [5,6] ])
>>> D
array([[1, 2],
       [3, 4],
       [5, 6]])
>>> D.shape
(3, 2)

>>> np.dot(C, D)
array([[22, 28],
       [49, 64]])
>>> np.dot(C, D).shape
(2, 2)

#逆
>>> np.dot(D, C)
array([[ 9, 12, 15],
       [19, 26, 33],
       [29, 40, 51]])
>>> np.dot(D, C).shape
(3, 3)


>>> # 内積できる行列の形状には条件がある
... # 行列Aの1次元目の要素数(列数)と
... # 行列Bの0次元目の要素数(行数)を同じにする必要がある

>>> # 2x3行列と3x2行列は内積できるが、
... # 2x3行列と2x3行列の内積はできない

>>> C
array([[1, 2, 3],
       [4, 5, 6]])
>>> E = C
>>> E
array([[1, 2, 3],
       [4, 5, 6]])

>>> C.shape
(2, 3)
>>> E.shape
(2, 3)

>>> np.dot(C, E)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: shapes (2,3) and (2,3) not aligned: 3 (dim 1) != 2 (dim 0)

# 再度確認
>>> C.shape[1]
3
>>> D.shape[0]
3
>>> E.shape[0]
2

# バリデーション
>>> if C.shape[1] == D.shape[0]:
...   print('.dot() OK!')
... else:
...   print('.dot() NG!')
...
.dot() OK!

>>> if C.shape[1] == E.shape[0]:
...   print('.dot() OK!')
... else:
...   print('.dot() NG!')
...
.dot() NG!

以上、行列演算を活用してニューラルネットワークの計算を効率化していく。

参考:

行列の乗法 - Wikipedia

『ゼロから作るDeep Learning』ソースコード、正誤表

ソースコードは本を写経していたが、 独自のライブラリを使って説明があったので ソースコードリポジトリを参照した

github.com

正誤表もついていて、間違いを正せる。 5章まるごと「連鎖律」を「連鎖率」に間違えるという
盛大な誤植を見つけたので、ちょっと修正しながら読んでいこう。