Momentum

* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.

 

 * 시작하기에 앞서, 해당 포스트는 "Gradient Descent Optimization Algorithms 정리" 포스팅

(http://shuuki4.github.io/deep%20learning/2016/05/20/Gradient-Descent-Algorithm-Overview.html)

을 참조하였습니다. 구현 중인 알고리즘에 대해 알고 싶다면, 위 포스팅에서 확인해 주세요.

또한, 이번 차시에 구현할 optimizer는 위 링크의 Momentum과 NAG를 구현할 것입니다.

 

 

 

* 저번 시간의 코드 그대로 이어집니다.

 

import numpy as np
import pandas as pd
from Model import Model
import Layers
import Optimizer

# load data
train = pd.read_csv("../Data/MNIST_data/mnist_train.csv")
y_train = train["label"]
x_train = train.drop("label", 1)
x_train = x_train.values / x_train.values.max()
y_train = y_train.values

# y to one-hot
one_hot = np.zeros((y_train.shape[0], y_train.max() + 1))
one_hot[np.arange(y_train.shape[0]), y_train] = 1
y_train = one_hot

# initialize parameters
num_classes = y_train.shape[1]
hidden = 50

model = Model()
model.addlayer(Layers.MulLayer(), input_size=(784, 32), name="w1")
model.addlayer(Layers.AddLayer(), input_size=32, name='b1')
model.addlayer(Layers.SigmoidLayer(), activation=True, name='sigmoid1')
model.addlayer(Layers.MulLayer(), input_size=(32, 10), name="w2")
model.addlayer(Layers.AddLayer(), input_size=10, name='b2')
model.addlayer(Layers.SoftmaxLayer(), activation=True, name='softmax')

optimizer = Optimizer.Momentum(batch_size=128, momentum=0.9, nesterov=True)
model.train(x_train, y_train, optimizer, 10000, 0.01)

위 코드는 원래 저번 코드에서 딱 두 부분만 바뀌었습니다. Optimizer를 import하였고, 새로 optimizer를 만들어 train하는 방식입니다.

 

또, Model.py의 train함수 부분을 다음과 같이 바꾸고 시작하겠습니다.

    def train(self, x_train, y_train, optimizer, epoch, learning_rate):
        optimizer.train(x_train, y_train, epoch, learning_rate, self)

 

전체적인 훈련은 Optimizer.py라는 코드에서 진행하도록 하고, 훈련시키는 방식에 대한 코드는 모두 Optimizer.py에 집어 넣는 방식입니다.

그리고, 위에서 바꾸기 전의 train 함수 부분을 복사해서, Optimizer.py에 다음과 같이 넣어봅시다.

import numpy as np


class SGD:
    def __init__(self, batch_size):
        self.x = None
        self.y = None
        self.model = None
        self.batch_size = batch_size

    def train(self, x_train, y_train, epoch, learning_rate, model):
        for epochs in range(epoch):
            batch_mask = np.random.choice(x_train.shape[0], self.batch_size)
            x = x_train[batch_mask]
            y = y_train[batch_mask]

            model.predict(x, y)
            dout = model.layers[model.keys[-1]].backward()
            for i in reversed(range(len(model.keys) - 1)):
                key = model.keys[i]
                dout = model.layers[key].backward(dout)
                if key in model.params:
                    model.grads[key] = model.layers[key].grad
                    model.params[key] -= learning_rate * model.grads[key]

            if epochs % (epoch / 10) == 0:
                print("ACC on epoch %d : " % epochs, (model.pred.argmax(1) == y.argmax(1)).mean())
                print("LOSS on epoch %d : " % epochs, model.loss)
        model.predict(x_train, y_train)
        print("Final train_ACC : ", (model.pred.argmax(1) == y_train.argmax(1)).mean())
        print("Final train_LOSS : ", model.loss)

 

Optimizer.py에서는 다양한 optimization function (SGD, Momentum, NAG, ...)들이 클래스 형식으로 구현될 것입니다.

 

SGD의 구현은 그냥 동일하니, 바로 Momentum을 만들어 봅시다.

 

 

Momentum optimization은 기존의 SGD와 다르게, 미분값에 일종의 "가속도"를 붙여 주며 학습시킵니다.

이는 학습 시의 자잘한 local minima들을 피하기 위함인데, 실제로 SGD의 경우 작은 local minima들에 gradient가 갇히게 되는 경우도 존재하고, 빠져나온다 하더라도 그 안에서 오랜 시간 머무르게 됩니다.

따라서, 진행 방향에 가속도를 붙여주어 빠르게 local minima를 벗어나고, global minima에 다가가게 하는 것이죠.

 

 

Momentum의 식은 다음과 같습니다.

$$ v_t = \gamma v_{t-1} + \eta\nabla_\theta J(\theta_{t-1}) $$

$$ \theta_t = \theta_{t-1} - v_t $$

 

(참고로, 위 식은 아래와 같이 부호를 살짝 바꾸어 쓴 것과 동일합니다.)

$$ v_t = \gamma v_{t-1} - \eta\nabla_\theta J(\theta_{t-1}) $$

$$ \theta_t = \theta_{t-1} + v_t $$

 

이제, 각각의 파라미터들을 설명하겠습니다.

v는 velocity의 약어로, 파라미터가 이동할 속도 (방향과 속력)을 나타냅니다. 즉, 파라미터는 $v_t$만큼 이동하게 됩니다.

$\gamma$는 momentum의 hyperparameter인데, 0과 1 사이의 값을 넣습니다. (보통 0.9로 많이 넣습니다.) 해당하는 값이 높으면 가속도가 크고, 값이 낮으면 가속도가 낮습니다.

$\eta$는 learning rate의 값입니다.

$\theta$는 학습시킬 parameter (w1, w2, b1, b2, ..)입니다.

$\nabla_\theta J(\theta_{t-1})$ 는 현재 학습시킬 parameter에 대한, 오차함수인 $J(\theta)$의 미분값을 의미합니다.

 

이제 생각해야 할 것은 어떻게 velocity를 구현할지만 고민하면 됩니다.

 

 

class Momentum:
    def __init__(self, batch_size, momentum):
        self.x = None
        self.y = None
        self.model = None
        self.batch_size = batch_size
        self.momentum = momentum

    def train(self, x_train, y_train, epoch, learning_rate, model):
        velocity = {}
        for p in model.params:
            velocity[p] = np.zeros(model.params[p].shape)
        for epochs in range(epoch):
            batch_mask = np.random.choice(x_train.shape[0], self.batch_size)
            x = x_train[batch_mask]
            y = y_train[batch_mask]

            model.predict(x, y)
            dout = model.layers[model.keys[-1]].backward()
            for i in reversed(range(len(model.keys) - 1)):
                key = model.keys[i]
                dout = model.layers[key].backward(dout)
                if key in model.params:
                    model.grads[key] = model.layers[key].grad
                    velocity[key] = self.momentum * velocity[key] + learning_rate * model.grads[key]
                    model.params[key] -= velocity[key]

            if epochs % (epoch / 10) == 0:
                print("ACC on epoch %d : " % epochs, (model.pred.argmax(1) == y.argmax(1)).mean())
                print("LOSS on epoch %d : " % epochs, model.loss)
        model.predict(x_train, y_train)
        print("Final train_ACC : ", (model.pred.argmax(1) == y_train.argmax(1)).mean())
        print("Final train_LOSS : ", model.loss)
    

위 코드의 대부분은 SGD와 동일합니다.

특이할 부분만 살펴보자면,

 

1. __init__에 momentum을 입력한다. ($\gamma$)

2. 변수 velocity가 새로 생겼고, grad를 바로 학습시키는 대신 velocity의 연산을 한 후에 학습시킨다.

 

정도로만 보면 되겠습니다.

 

우선 momentum의 hyperparameter (변수 momentum)은 이 optimizer에만 사용될 것이므로, 클래스에 선언하였습니다.

 

 

        velocity = {}
        for p in model.params:
            velocity[p] = np.zeros(model.params[p].shape)

velocity는 다른 grads나 params같이 딕셔너리로 만들었습니다.

그리고 model.params의 크기(model.grads의 크기와 동일함)만큼 그대로 0으로 초기화해서 만들어 줍니다.

$ v_t = \gamma v_{t-1} + \eta\nabla_\theta J(\theta_{t-1}) $ 였으므로, 초기값은 0이 되어야 그 뒤의 가속도 값을 구할 수 있기 때문입니다.

 

                    model.grads[key] = model.layers[key].grad
                    velocity[key] = self.momentum * velocity[key] + learning_rate * model.grads[key]
                    model.params[key] -= velocity[key]

그리고, 위에서 언급했던 식과 동일하게 구현해 주면 됩니다.

momentum은 이렇게나 구현이 간단합니다.

 

 

 

 

 

 

다음은 NAG (Nesterov Accelerated Gradient)입니다.

NAG는 기본 틀이 momentum과 동일하기 때문에, 보통 다른 라이브러리에서도 SGD 안에 momentum과 nesterov를 넣어서 하나로 만듭니다.

그래서, NAG를 구현할 때는 momentum에서 nesterov를 True로 할지, False로 할지만 정해서 NAG를 적용시킬지 아닐지 결정하는 방식으로 가겠습니다.

(사실 원래 위의 momentum도 SGD에 넣어서 하려 했는데, 그냥 느낌 내려고 뺐습니다 ㅎㅎ)

 

 

 

NAG는 위의 momentum보다는 조금 까다로운 면이 있습니다.

momentum은 그냥 가속도만 붙여서 간다면, NAG는 가속이 붙은 방향으로 갔을 때의 미분값까지 계산해야 합니다.

우선 수식으로 볼까요?

 

$$ v_t = \gamma v_{t-1} - \eta\nabla_\theta J(\theta_{t-1} + \gamma v_{t-1}) $$

$$ \theta_t = \theta_{t-1} + v_t $$

 

(파라미터들은 모두 위와 동일하므로 설명하진 않겠습니다.)

 

이게 무슨 뜻인고 하니, "가속도가 붙은 방향으로 그대로 돌진하지 말고, 돌진했을 때를 생각해서 적절하게 가자" 하는 것입니다.

위의 momentum만을 활용하게 되면, local minima도 빠르게 빠져나올 수 있지만, 사실 global minima에서도 빠져나올 수 있다는 단점도 있고, 가속도가 너무 크게 붙었을 경우 제동이 쉽지 않다는 단점도 있습니다.

하지만 미리 "내가 이 방향으로 갔을 때, 제동을 걸어야 할까?" 하는 것을 $\nabla_\theta J(\theta_{t-1} - \gamma v_{t-1})$로 계산해 줄 수 있는 것입니다.

 

하지만, NAG도 위의 momentum처럼 식만 보고 구현할 수만 있다면 정말 좋았겠으나 그렇게 간단하지는 않습니다.

우리가 구현한 Layer들은 오차함수를 각각의 파라미터에 대해서만 미분 가능하지, $\nabla_\theta J(\theta_{t-1} - \gamma v_{t-1})$의 값을 계산할 수는 없기 때문입니다.

 

물론, NAG용 파라미터를 하나 더 만들어서 $\theta = \theta - v_t$로 만들어서 미분을 처음부터 다시 하고 ... 하면 되겠습니다...만

그러면 한번 연산할 것이 두배가 되고, 그러면 애초에 시간복잡도가 (거의) 두배가 되어 버립니다.

조금 더 효율적인 방법은 없을까요?

 

*혹시라도 아래 설명이 이해가 잘 안된다면, 제가 참고한 아래 논문들의 원문을 읽어보시는 것을 추천드립니다.

https://arxiv.org/pdf/1212.0901v2.pdf (3.5. Simplified Nesterov Momentum)

https://www.cs.utoronto.ca/~ilya/pubs/ilya_sutskever_phd_thesis.pdf

 

 

자, 아까 위에 언급했던 NAG의 수식은 다음과 같았습니다.

$$ v_t = \gamma v_{t-1} - \eta\nabla_\theta J(\theta_{t-1} + \gamma v_{t-1}) $$

$$ \theta_t = \theta_{t-1} + v_t $$

 

그런데, 이는 위에서 언급했던 다음과 같은 momentum의 모습과 상당히 유사합니다.

$$ v_t = \gamma v_{t-1} - \eta\nabla_\theta J(\theta_{t-1}) $$

$$ \theta_t = \theta_{t-1} + v_t $$

 

우리의 목적은 위의 NAG의 수식을 위의 momentum과 같이 theta에 대한 미분값만으로 계산할 수 있도록 하는 것입니다.

 

 

일단, $ \theta_{t-1} + \gamma v_{t-1} $ 를 $k_{t-1}$라는 새로운 파라미터로 생각해 봅시다.

그러면 위의 NAG 식은 다음과 같이 쓸 수 있습니다.

$$ v_t = \gamma v_{t-1} - \eta\nabla_k J(k_{t-1})$$

$$ \theta_t = \theta_{t-1} + v_t $$

$$ \theta_t = \theta_{t-1} + \gamma v_{t-1} - \eta\nabla_k J(k_{t-1}) $$

 

이렇게 하면 원래 momentum의 velocity update와 동일한 식을 만들 수 있었습니다.

또한, $k_t$의 식을 조금 써본다면...

$$k_t = \theta_t + \gamma v_t $$

$$k_t = \theta_{t-1} + \gamma v_{t-1} - \eta\nabla_k J(k_{t-1}) + \gamma v_t $$

$$k_t = k_{t-1} + \gamma v_t - \eta\nabla_k J(k_{t-1}) $$

 

자, 이렇게 $\theta$를 대체할 $k$라는 파라미터의 update식을 구할 수 있습니다!

또한, 아까 언급했듯 $k_{t-1} = \theta_{t-1} + \gamma v_{t-1}$이었습니다.

그런데 우선 최초의 velocity값인 $v_1$은 0이므로, $t=1$일 때의 $k_t$값은 $\theta_t$의 값과 동일합니다.

그리고, optimal convergence ($v_{t-1}=0$인 지점)에서는 $\theta_t$와 $k_t$는 완벽하게 동일합니다.

 

그러므로 우리는 원래의 파라미터 $\theta$를 우리가 새로 만든 파라미터 $t$로 근사가 가능합니다.

즉, update rule을 다음과 같이 정의할 수 있습니다.

$$ v_t = \gamma v_{t-1} - \eta\nabla_\theta J(\theta_{t-1})$$

$$ \theta_t = \theta_{t-1} + \gamma v_t - \eta\nabla_\theta J(\theta_{t-1}) $$

 

이제 이것을 코드로 구현해 봅시다.

 

class Momentum:
    def __init__(self, batch_size, momentum, nesterov=False):
        self.x = None
        self.y = None
        self.model = None
        self.batch_size = batch_size
        self.momentum = momentum
        self.nesterov = nesterov

    def train(self, x_train, y_train, epoch, learning_rate, model):
        velocity = {}
        for p in model.params:
            velocity[p] = np.zeros(model.params[p].shape)
        for epochs in range(epoch):
            batch_mask = np.random.choice(x_train.shape[0], self.batch_size)
            x = x_train[batch_mask]
            y = y_train[batch_mask]

            model.predict(x, y)
            dout = model.layers[model.keys[-1]].backward()
            for i in reversed(range(len(model.keys) - 1)):
                key = model.keys[i]
                dout = model.layers[key].backward(dout)
                if key in model.params:
                    model.grads[key] = model.layers[key].grad
                    if self.nesterov:
                        velocity[key] = self.momentum * velocity[key] - learning_rate * model.grads[key]
                        model.params[key] += self.momentum * velocity[key] - learning_rate * model.grads[key]
                    else:
                        velocity[key] = self.momentum * velocity[key] - learning_rate * model.grads[key]
                        model.params[key] += velocity[key]

            if epochs % (epoch / 10) == 0:
                print("ACC on epoch %d : " % epochs, (model.pred.argmax(1) == y.argmax(1)).mean())
                print("LOSS on epoch %d : " % epochs, model.loss)
        model.predict(x_train, y_train)
        print("Final train_ACC : ", (model.pred.argmax(1) == y_train.argmax(1)).mean())
        print("Final train_LOSS : ", model.loss)
    

 

위의 코드에서 바뀐 점만을 집중해서 봐 주시길 바랍니다.

 

1. __init__에 nesterov=False와, self.nesterov = nesterov를 넣어줌으로써 momentum 클래스에 nesterov를 키고 끌 수 있도록 했습니다.

 

2. if self.nesterov: 로 nesterov를 켰을 경우에 nesterov를 적용시켜 학습시킵니다.

 

 

            for i in reversed(range(len(model.keys) - 1)):
                key = model.keys[i]
                dout = model.layers[key].backward(dout)
                if key in model.params:
                    model.grads[key] = model.layers[key].grad
                    if self.nesterov:
                        velocity[key] = self.momentum * velocity[key] - learning_rate * model.grads[key]
                        model.params[key] += self.momentum * velocity[key] - learning_rate * model.grads[key]
                    else:
                        velocity[key] = self.momentum * velocity[key] - learning_rate * model.grads[key]
                        model.params[key] += velocity[key]

이 부분만 다시 자세히 보도록 하겠습니다.

if 구문으로 nesterov가 True일 때와 False일 때를 나누어서 계산합니다.

nesterov가 False인 경우에는 당연히 위의 Momentum과 동일한 식으로 나오게 됩니다.

 

nesterov가 True인 경우엔, 위에서 언급한 식 그대로 코드로 구현하시면 됩니다.

테스트하고 싶으시다면, 원래 코드에서 변수로 nesterov = True를 넣어주시면 됩니다.

 

 

이렇게, Momentum과 NAG의 구현을 마무리하도록 하겠습니다.

나머지 Adagrad, Adadelta, RMSProp, Adam은 다음 시간에 구현해 보도록 합시다!

+ Recent posts