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

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

ゼロから作る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より、他の手法を用いたほうが早く正確に学習できていることが分かります。

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