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

基幹系SIerがWeb系とかネイティブ系の知識を蓄えようとするブログ

MENU

ゼロから作るDeepLearning 6章を学ぶ 〜重みの初期値について〜

前回の続きです。
推奨されている重みの初期値について、まとめます。

taxa-program.hatenablog.com

重みの初期値を0にすることの危険性

正確には、重みを均一な値にすることはNGです。
なぜなら、誤差逆伝播法において、全ての重みの値が均一に更新されてしまうからです。
これでは、たくさんの重みを持つ意味がなくなってしまいますね。

例えば、重みの初期値を、標準偏差が1のガウス分布を用いて5つの層をもつニューラルネットワークにダミーデータを渡して、隠れ層のアクティベーション(活性化関数の後の出力データ)を見てみます。
下記は活性化関数にsigmoid関数を使用していますが、各層のアクティベーションは0と1に偏っていることが分かります。

f:id:taxa_program:20180614214341p:plain

Xavierの初期値

Xavierの初期値とは、前層のノードの個数をnとした場合、\dfrac {1}{\sqrt {n}}標準偏差を持つ分布を使用する、というものです。

Xavierの初期値を与えた場合のアクティベーションを見てみます。 f:id:taxa_program:20180614215621p:plain

上の層にいくにつれて、歪な形にはなっていますが、標準偏差が1のガウス分布を重みとした場合より、広がりがあることがわかると思います。

ReLU関数の場合の重み初期値〜Heの初期値〜

sigmoid関数やtanh関数は左右対称で中央付近が線形関数とみなせるため、Xavierの初期値が適していましたが、ReLU関数を用いる場合は、特化した初期値を用いることが推奨されています。
それがHeの初期値です。

Heの初期値は、前層のノードの数がn個の場合、\sqrt {\dfrac {2}{n}}標準偏差とするガウス分布を用います。

ためしに、ReLU関数の「標準偏差0.01のガウス分布」、「Xavierの初期値」、「Heの初期値」でのアクティベーション分布を見てみましょう。

f:id:taxa_program:20180614221020p:plain

  • Xavierの初期値

f:id:taxa_program:20180614221043p:plain

  • Heの初期値

f:id:taxa_program:20180614221147p:plain

これを見ると、標準偏差0.01は論外、Xavierの初期値は層がディープになるにつれて、勾配損失が問題になってきそうなことが分かります。
一方でHeの初期値に関しては、層をディープにしても、分布の広がりが均一になっているため、逆伝播の際も適切な値が流れそうです。

まとめると

  • 活性化関数にReLUを用いる場合、Heの初期値
    \sqrt {\dfrac {2}{n}}
w = np.random.randn(node_num, node_num) * np.sqrt(2.0 / node_num)
  • 活性化関数にsigmoidやtanhを用いる場合、Xavierの初期値
    \dfrac {1}{\sqrt {n}}
w = np.random.randn(node_num, node_num) * np.sqrt(1.0 / node_num)

を用いるのがベストプラクティスです。

ゼロから作るDeepLearning 6章を学ぶ 〜学習のテクニック〜

本日は6章を学んでいきます。
この章では、ニューラルネットワークの学習においてキーとなっている

  • 重みパラメータの更新方法
  • 重みパラメータの初期値設定方法

の2点について重点的に学ぶことができました。
今回は重みパラメータの更新方法について、まとめます。

復習

ニューラルネットワークの学習の目的は、損失関数の値をなるべく小さくすることでしたね。
これは言い換えれば最適なパラメータを見つける問題であり、最適化と呼ばれます。
まずは、重みパラメータの更新手法について見ていきます。

SGD確率的勾配降下法

今まで勉強してきたものは、SGDと呼ばれるものです。
これは、単純に勾配方向へある一定の距離を進む、ということを繰り返し、パラメータの更新を行います。
数式で表すと下記のようになります。
Wは重みパラメータ、Wに関する損失関数の勾配を\dfrac {\partial L}{\partial W}としており、ηは学習係数を表します。

W\leftarrow W-\eta \dfrac {\partial L}{\partial W}

pythonで実装してみます。

# 確率的勾配降下法(Stochastic Gradient Descent)
class SGD:
    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] 

しかしこのSGDには欠点があり、関数の形状が等方向でないと非効率な経路で探索してしまいます。
例えば、下記のような関数の場合は非効率に探索します。

f\left( x,y\right) =\dfrac {1}{20}x^{2}+y^{2}

図を描画してみます。

from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import numpy as np

def func1(x, y):
    return (1/20*x**2) + (y**2)

x = np.arange(-10, 10, 0.2)
y = np.arange(-10, 10, 0.2)

X, Y = np.meshgrid(x, y)
Z = func1(X, Y)

fig = plt.figure()
ax = Axes3D(fig)

ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_zlabel("f(x, y)")

ax.plot_wireframe(X, Y, Z)
plt.show()

f:id:taxa_program:20180613210207p:plain

このように、必ずしも勾配が(0, 0)時点を向かない関数に関しては、SGDは最適ではないと言えます。

Momentum

この手法は、物体が勾配方向に力を受け、その力によって物体が動くイメージです。
ここでは物体 = 重みだと思ってください。  
数式にすると下記のようになります。
v\leftarrow \alpha v-\eta \dfrac {\partial L}{\partial W}
W\leftarrow W+v

これは、ボールが地面を転がるような動きで最小値点を探索することを表しています。
pythonで実装してみます。

# Momentum
class Momentum:

    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]

インスタンス変数のvは物体の速度を保持します。
vは初期化時には何も保持せず、update()が初めて呼ばれた時に、パラメータと同じ構造のデータをディクショナリ変数として保持します。

AdaGrad

ニューラルネットワークでは、学習係数(η)の値が重要になります。
学習係数が小さすぎると学習に時間がかかり過ぎてしまい、逆に大きすぎると発散してしまい、正しく学習できません。

この学習係数に関するテクニックとして、学習係数の減衰という方法があります。
これは、学習が進むにつれて、学習係数を小さくするという方法です。

AdaGradは、「一つ一つ」のパラメータ対して、オーダーメイドの値を生成します。
AdaGradの更新方法を数式で表してみます。

h\leftarrow h+\dfrac {\partial L}{\partial W}\cdot \dfrac {\partial L}{\partial W}

W\leftarrow W-\eta\dfrac {1}{\sqrt{h}}\cdot \dfrac {\partial L}{\partial W}

数式だけみると難しく見えますが、SGDと変わりません。
Wは更新する重みパラメータ、\dfrac {\partial L}{\partial W}はWに関する損失関数の勾配、ηは学習係数を表します。
ここで新たにhという変数が登場しています。これは、式にも表れているように、これまで経験した勾配の値を2乗和として保持しています。
上記の式は、パラメータの要素の中で、よく動いた(大きく更新された)要素は、学習係数が小さくなることを意味しています。  

pythonで実装してみます。

class 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)

Adam

最後に、AdaGrad と Momentum を融合した手法の紹介です。
pythonで実装してみます。

class Adam:

    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] += (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)

MNISTデータセットによる更新手法の比較

上記であげた4つの更新手法について、MNISTデータセットで学習進捗がどれだけ異なるか比較してみると、下記のようになりました。 f:id:taxa_program:20180613235516p:plain

SGDより、他の手法を用いたほうが早く正確に学習できていることが分かります。

次は重みの初期値について、まとめようと思います。

ゼロから作るDeepLearning 5章を学ぶ 〜誤差逆伝播法〜

本日から5章に入りました。
余談ですが、現在仕事の関係で、電車で1時間ほどかかる場所へよく出張に行っています。
普段の通勤は電車に乗る時間が15分ほどなため、ゆっくり読書などはできないのですが(もちろん、時間だけが原因でなく、混雑しているのも原因の一つです)、1時間も電車に揺られていると、読書も捗ります。

さて、今日から誤差逆伝播法です。

順伝播と逆伝播

逆伝播に関しては、こちらの記事がとても勉強になりました。

誤差逆伝播法のノート

読んで字のごとく、順方向とは逆に向かっての伝播を考えるアルゴリズムが逆伝播です。

連鎖律

逆方向の伝播では「局所的な微分」を順方向とは逆方向に伝達します。
この「局所的な微分」を伝達する原理を連鎖律と呼びます。

連鎖律とは、合成関数の微分についての性質であり、下記のように定義されます。

ある関数が合成関数で表される場合、その合成関数の微分は、合成関数を構成するそれぞれの関数の微分の積によって表すことができる

リンゴの買い物による逆伝播

この書籍では、リンゴの買い物を例に逆伝播の解説を行なっています。

Q. 太郎くんはスーパーで1個100円のリンゴを2個買いました。消費税が10%適用された場合、支払金額はいくらか

上記に問いに関する答えは、おそらく暗算で解けてしまうと思います。

続きを読む

ゼロから作るDeepLearning 4章を学ぶ その3 ニューラルネットワーク構築編

前回に引き続き、4章で学んだことを残しておきます。
今回は、MNISTデータセットを使用して、手書き数字を学習するニューラルネットワークを構築してみます。

前回までの記事はこちら
taxa-program.hatenablog.com
taxa-program.hatenablog.com

2層ニューラルネットワークのクラス

はじめに、2層ニューラルネットワークを一つのクラスとして実装してみます。
処理の内容は、なるべくコメントに記載しています。(したつもりでいます)
誤っている部分がある場合はご指摘いただけると助かります。

続きを読む

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

前回に引き続き、4章で学んだことを残しておきます。

前回記事

taxa-program.hatenablog.com

ニューラルネットワークでの勾配

ニューラルネットワークでも勾配を求める必要があります。
その時、対象となる関数は......
そう、損失関数ですね。この損失関数の最小値を求めたいのです。

実際にコードを見てみます。

続きを読む

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

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

taxa-program.hatenablog.com

taxa-program.hatenablog.com

  • ミニバッチ学習
  • 1に微分、2に微分、3に(ry
  • 微分の復習
  • 勾配
  • 勾配法
  • 参考サイト

ミニバッチ学習

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

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

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

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

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

続きを読む

ニューラルネットワークの構築 〜活性化関数と実際の構築〜

前回の続きです。

本日はconnpassで募集していた新宿のもくもく会で勉強しています。
集中できて素晴らしい。(主催者の方、ありがとうございます)
また参加しようと思います。(コーヒーとお菓子食べれるし。笑)

前回記事はこちら

taxa-program.hatenablog.com

  • ソフトマックス関数
  • 出力層ニューロンの数
  • MNSITからデータを拝借してNNを構築してみる
  • NNでバッチ処理を行なってみる

ソフトマックス関数

ソフトマックス関数の分子は入力信号の指数関数、分母はすべての入力信号の指数関数の和から構成されています。

mathtrain.jp

早速pythonでソフトマックス関数を実装してみます。

# ソフトマックス関数の実装
def softmax(a):
    c = np.max(a)
    exp_a = np.exp(a - c) # オーバーフロー対策でcを引いている
    sum_exp_a = np.sum(exp_a)
    y = exp_a / sum_exp_a
    return y

# 試しに計算してみる
a = np.array([0.3, 2.9, 4.0])
y = softmax(a)
print(y) # [0.01821127 0.24519181 0.73659691]

出力を見て分かるように、ソフトマックス関数の出力は0~1.0の間の実数になります。 また、総和は1になっていますね。
この総和が1という性質がとても重要で、この性質によってソフトマックス関数の出力を確率として解釈することができます。

上の例ではy[0]の確率が0.018(1.8%)、y[1]の確率が0.245(24.5%)、y[2]の確率が0.737(73.7%)のように解釈できます。この確率の結果から、2番目の要素が最も大きいため、2番目のクラスだろう、と分類できます。

しかし、ソフトマックス関数を利用しても、各要素の大小関係は変化しないため、ニューラルネットワークで分類を行う際には、ソフトマックス関数を省略することが多いみたい。
(学習フェーズでソフトマックス関数を用いて、推論のフェーズでは省略されることが多いようです)

出力層ニューロンの数

出力数のニューロン数は解く問題によって適宜決定する必要があります。
当たり前だが、クラス分類したい場合は分類したいクラス数に出力ニューロン数を決定する必要があります。

MNSITからデータを拝借してNNを構築してみる

続きを読む