ギークなエンジニアを目指す男

機械学習系の知識を蓄えようとするブログ

ゼロから作るDeepLearning 4章を学ぶ その1

前回までの学んだことはこちら

taxa-program.hatenablog.com

taxa-program.hatenablog.com

ミニバッチ学習

機械学習は、膨大がデータセットがないと行うことはできません。
しかし、その全てのデータにおいて損失関数の計算を行うのは時間がかかります。
そこで、データの中から一部を選び出しその一部のデータを全体の「近似」として利用したりします。
このような学習方法をミニバッチ学習というようです。

訓練データの中から指定された個数のデータをランダムに選び出すコードを書いてみます。

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)

# ランダムに10枚だけ抜き出す
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]

# この出力は実行の度に変化する
# ex)[42046 53515  8543 48925 23975 16930 51302 58674 14433 35769]
# 0~60000までのインデックス中からランダムで出力される
print(batch_mask)

1に微分、2に微分、3に(ry

機械学習といえば微分
どんな書籍、サイトをみても微分と記載していないものはないだろう。

微分が必要な理由は損失関数にあります。

NNの学習では、最適なパラメータ(重み&バイアス)を探索する際に、損失関数の値をできるだけ小さくなるようにパラメータを探します。
ここで、できるだけ小さな損失関数の場所を探すために、パラメータの微分(正確には勾配)を計算し、その微分の値を手がかりにパラメータの値を徐々に更新して、最適なパラメータを探索します。

あるニューラルネットワークがあると仮定し、その中のひとつの重みパラメータに注目してみると、ひとつの重みパラメータの損失関数に対する微分
その重みパラメータの値を少しだけ変化させた時に、損失関数がどのように変化するか
ということを表します。

この微分値がマイナスだった場合(右肩下がりの接線の場合)、その重みパラメータを正の方向へ変化させることで、損失関数を減少させることができます。
逆に微分値がプラスだった場合(右肩上がりの接線の場合)、その重みパラメータを負の方向へ変化させることで、損失関数を減少させることができます。

微分の復習

微分とは、ある瞬間における変化の量を表したものでしたね。
(高校の時にやってる・・・はず・・・)

ここで簡単な微分Pythonで実装してみます。

import numpy as np
import matplotlib.pylab as plt

# 数値微分
def numerical_diff(f, x):
    h = 1e-4 # 0.0001 限りなく0に近い数
    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) # 0.1999999999990898
    y = f(x) - d*x
    return lambda t: d*t + y

x = np.arange(0.0, 20.0, 0.1) # 0から20まで0.1刻みの配列作成
y = function_1(x) # 今回微分したい関数
plt.xlabel("x")
plt.ylabel("f(x)")

tf = tangent_line(function_1, 5) # x = 5で微分
y2 = tf(x)

plt.plot(x, y)
plt.plot(x, y2)
plt.show()

f:id:taxa_program:20180602162159p:plain

画像をみて分かる通り、x = 5の部分で微分されていますね。

さて、微分には偏微分というものがあったのを覚えていますでしょうか。
微分対象の関数は上記のように変数が1つだけではなく、2つ、3つの場合もあると思います。
そういった時に、偏微分を利用することで、どの変数に対する微分かを区別することができます。

偏微分について記憶が曖昧な場合はググってみてください。

勾配

偏微分は、複数の変数がある場合に、特定の変数にのみ注目して微分することでした。
例えば、x1とx2という変数があった場合は、それそれについて微分するので、計算が2回必要です。

せっかくですから、まとめて計算を行いたいですよね。そこで登場するのが勾配です。
勾配とは、すべての変数の偏微分をベクトルとしてまとめたものです。
下に実装例を紹介します。

# coding: utf-8
# 全ての変数を一度に偏微分します
# すべての変数の偏微分をベクトルとしてまとめたもの:勾配
import numpy as np
import matplotlib.pylab as plt
from mpl_toolkits.mplot3d import Axes3D
%matplotlib inline

def _numerical_gradient_no_batch(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x) # xと同じ形状の配列を作成(全要素が0)

    for idx in range(x.size):
        tmp_val = x[idx]

        # f(x+h)の計算
        x[idx] = float(tmp_val) + h
        fxh1 = f(x) # f(x+h)

        # 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):
    # 1次元にも対応
    if X.ndim == 1:
        return _numerical_gradient_no_batch(f, X)
    else:
        grad = np.zeros_like(X) # xと同じ形状の配列を作成(全要素が0)

        # enumerate関数を用いると、[インデックス番号, 要素]が取得できる。
        for idx, x in enumerate(X):
            grad[idx] = _numerical_gradient_no_batch(f, x)

        return grad

# y = x0**2 + x1**2 の関数
def function_2(x):
    # 1次元の配列の場合とそうでない場合で場合分け(ndimで次元数を取得)
    if x.ndim == 1:
        return np.sum(x**2)
    else:
        return np.sum(x**2, axis=1)

# メイン処理
if __name__ == '__main__':
    # [-2.0, -1.75, -1.5, -1.25, -1.0, -0.75, -0.5, -0.25, 0.0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.25]
    x0 = np.arange(-2, 2.5, 0.25)
    x1 = np.arange(-2, 2.5, 0.25)

    # 格子点を生成
    X, Y = np.meshgrid(x0, x1)

    # ネストされた配列を1次元配列にフラット化
    X = X.flatten()
    Y = Y.flatten()

    # 勾配(gradient)を求める
    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()

この結果をみてみると...

f:id:taxa_program:20180603145407p:plain

向きを持ったベクトルとして表示されます。
勾配は、関数f(x)の「一番低い場所(最小値)」を指しているように見えますね。
正確に述べると、各場所において関数の値を最も減らす方向を向いています。

この勾配が指し示す方向に進むことで、関数(損失関数)の値を減らすことができます。

勾配法

勾配法とは、現在の場所から勾配方向に一定の距離だけ進み、移動した先でも同様に勾配を求め、その勾配方向へ進む、ということを繰り返して勾配方向へ移動し、関数の値を徐々に減らすことを指します。

勾配法では、1回の学習でどれだけ学習すべきか、どれだけパラメータを更新するべきか、ということを決めるのに学習率という変数を用います。

下記で実装してみます。

import numpy as np
import matplotlib.pylab as plt
from mine_gradient_2d import numerical_gradient
%matplotlib inline

# 勾配降下法
# 引数fは最適化したい関数、init_xは初期値、lrは学習率、step_numは勾配法の繰り返し回数
def gradient_descent(f, init_x, lr = 0.01, step_num = 100):
    x = init_x

    # 0~100までループ
    for i in range(step_num):
        # 関数の勾配を求める
        grad = numerical_gradient(f, x)

        # 勾配法の数式に当てはめる(xを勾配の方向へ移動)
        x -= lr * grad
    return x

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

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

gradient_descent(function_2, init_x=init_x, lr=0.1, step_num=100) # array([-6.11110793e-10,  8.14814391e-10])

結果が[-6.11110793e-10, 8.14814391e-10]なので、ほとんど(0,0)まで近づいていることが分かると思います。

ちなみに、更新のプロセスはこんな感じです。

f:id:taxa_program:20180603160603p:plain

また長くなりそうなので、続きは別記事で。
最後に参考にさせて頂いたサイトのリンクを掲載しておきます。

参考サイト

NumPyの軸と次元数はこちらのブログに詳しく載っていました。

deepage.net

また、3Dグラフに描画については、こちらのブログが参考になります。

d.hatena.ne.jp