Adagrad

* 모든 코드는 제 깃허브 (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는 위 링크의 Ada시리즈와 RMSProp을 구현할 것입니다.

 

 

 

자, 저번 시간에 이어, 이번에는 바로 Adagrad를 구현해 봅시다.

우선 AdaGrad의 식은 다음과 같습니다.

 

$$ G_t = G_{t-1} + (\nabla_\theta J(\theta_t))^2 $$

$$ \theta_{t+1} = \theta_t - \frac {\eta} {\sqrt{G_t + \epsilon}} \cdot \nabla_\theta J(\theta_t) $$

 

여기서, $\epsilon$은 분모가 0이 되는 것을 방지하기 위한 값입니다.

 

 

간단하게만 설명하자면, $G_t$는 저번의 velocity와 같이, params(또는 grads)만큼의 크기를 갖는 새로운 변수입니다.

학습하면 할수록, 계속해서 grads의 제곱의 합을 받아오는 변수입니다.

이 변수는 해당 grads의 절대값이 크면 G의 값을 크게, grads의 절대값이 작으면 G의 값을 작게 둡니다.

그리고 update rule에서 보면 알겠지만 G의 값이 크면 learning rate가 작고, G의 값이 작으면 learning rate가 큽니다.

 

우리의 변수 params들은 grads의 값이 크면 많이 움직이고, 값이 작으면 조금 움직이게 됩니다.

그러므로, 위의 G가 가지는 의미는 "이미 많이 움직인 params는 좀 덜 움직이고, 많이 안 움직였던 params은 더 크게 움직이자" 하는 뜻입니다.

 

저번의 NAG처럼 복잡하게 수식을 정리할 필요는 없으니, 빠르게 구현해 봅시다.

 

 

class Adagrad:
    def __init__(self, batch_size):
        self.x = None
        self.y = None
        self.model = None
        self.batch_size = batch_size
        self.epsilon = 10e-8

    def train(self, x_train, y_train, epoch, learning_rate, model):
        G = {}
        for p in model.params:
            G[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
                    G[key] += np.square(model.grads[key])
                    model.params[key] -= np.multiply(learning_rate / (np.sqrt(G[key] + self.epsilon)), 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)

(아무래도 코드가 많이 중복되는 것을 보아하니, 코드를 다시 한번 리팩토링을 언젠가 해야겠습니다.)

 

이번에 볼 부분은, 다음과 같습니다.

 

1. __init__의 epsilon 등장

2. 변수 G의 등장

3. Update Rule의 적용

 

 

이 때, 변수 G는 저번 Momentum / NAG 때와 동일하게 선언해 주시면 됩니다. (결국 변수의 크기 자체는 동일하기 때문)

 

그리고, 식을 그대로 코드에 적용시켰습니다.

                    model.grads[key] = model.layers[key].grad
                    G[key] += np.square(model.grads[key])
                    model.params[key] -= np.multiply(learning_rate / (np.sqrt(G[key] + self.epsilon)), model.grads[key])

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

G에는 grads의 제곱을 계속해서 더해주고, params에는 위 계산식 그대로 넣어주시면 되겠습니다.

 

 

 

하지만, Adagrad에는 큰 단점이 있는데, 아무리 학습이 안되었어도 / 잘되었어도 가면 갈수록 계속해서 learning rate가 작아집니다.

G에는 계속해서 grad의 제곱이 더해지므로, 가면 갈수록 점점 더 G가 커지기 때문입니다.

 

이를 보완한 알고리즘 중 하나가 바로 RMSProp입니다.

 

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

$$ G_t = \gamma G_{t-1} + (1-\gamma)(\nabla_\theta J(\theta_t))^2 $$

$$ \theta_{t+1} = \theta_t - \frac {\eta} {\sqrt{G_t + \epsilon}} \cdot \nabla_\theta J(\theta_t) $$

 

여기서 gamma는 보통 0.9, 0.99 등의 값으로 많이 놓습니다.

또한, update rule은 동일함을 알 수 있습니다.

이렇게 구현하게 되면, gamma로 인해 G값이 무한으로 뻗어나가는 일은 생기지 않게 됩니다.

 

식에 큰 변화가 없으니, 그냥 간단하게 짜봅시다.

 

class RMSProp:
    def __init__(self, batch_size, gamma=0.9):
        self.x = None
        self.y = None
        self.model = None
        self.batch_size = batch_size
        self.epsilon = 10e-8
        self.gamma = gamma

    def train(self, x_train, y_train, epoch, learning_rate, model):
        G = {}
        for p in model.params:
            G[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
                    G[key] = self.gamma * G[key] + (1 - self.gamma) * np.square(model.grads[key])
                    model.params[key] -= np.multiply(learning_rate / (np.sqrt(G[key] + self.epsilon)), 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)

 

__init__ 부분에 gamma가 생겼다는 것, 그리고 식에 gamma와 관련된 식이 생겼다는 것을 제외하고는 모두 그대로입니다.

 

 

 

다음은 AdaDelta입니다.

 

AdaDelta는 위의 두 알고리즘과는 달리 식 4개로 이루어져 있습니다.

$$ G = \gamma G + (1 - \gamma ) (\nabla_\theta J(\theta))^2 $$

$$ \Delta_\theta = \frac{\sqrt{s+\epsilon}}{\sqrt{G+\epsilon}} \cdot \nabla_\theta J(\theta)$$

$$ \theta = \theta - \Delta_\theta $$

$$ s = \gamma s + (1 - \gamma) \Delta^2 _\theta $$

식을 딱 보았을 때의 첫느낌은 아마, "대체 뭐라는거지?" 일 것 같습니다.

일단 G의 식은 지금까지와 같으나, $\theta$를 update하는 과정에서 분자에 learning rate $\eta$가 들어가는 것이 아니라, 새로운 인자 $\sqrt{s+\epsilon}$이 들어갑니다.

또한 눈치챌 수 있는 점은, learning rate는 AdaDelta에서는 사용하지 않습니다.

즉, 따로 learning rate를 설정해 주지 않아도 작동을 한다는 뜻입니다.

 

대충 정리하자면, learning rate를 변수 $\sqrt{s+\epsilon} = \sqrt{\gamma s + (1 - \gamma) \Delta^2 _\theta + \epsilon}$ 가 대체한다는 것입니다.

$\Delta_\theta^2 = \frac{s+\epsilon}{G+\epsilon} \cdot (\nabla_\theta J(\theta))^2$ 이므로, s에 관한 식을 다시 풀어 쓴다면 $s = \gamma s + (1 - \gamma)(\frac{s+\epsilon}{G+\epsilon} \cdot (\nabla_\theta J(\theta))^2)$가 되겠습니다.

이것을 보면, G와 s가 매우 비슷한 방식으로 update된다는 것을 알 수 있습니다.

 

그리고, G와 s를 간단하게나마 설명하자면, G는 "지금까지 grads의 변화의 합", s는 "지금까지의 params의 변화의 합"이 됩니다.

즉, params가 크게 변화하면 learning rate (즉, $\sqrt{s+\epsilon}$값)이 커지고, params가 작게 변화하면 learning rate가 작아지는 것입니다.

 

이를 직관적으로 보자면, local minima에 갇혀 있다가 풀려난 경우에는 갑자기 params가 변화해야 하는 정도가 커지는데, 그렇게 된다면 자동으로 learning rate를 증가시켜 더욱 빠르게 (또, 관성을 갖게) 됩니다.

그리고, global minima에 가까워지면 저절로 params의 변화 정도가 적어지므로, learning rate를 감소시켜 더욱 세밀한 update가 가능해 지는 것입니다.

 

 

코드는 이번에도 크게 바뀌진 않습니다.

class AdaDelta:
    def __init__(self, batch_size, gamma=0.9):
        self.x = None
        self.y = None
        self.model = None
        self.batch_size = batch_size
        self.epsilon = 10e-8
        self.gamma = gamma

    def train(self, x_train, y_train, epoch, learning_rate, model):
        G = {}
        s = {}
        for p in model.params:
            G[p] = np.zeros(model.params[p].shape)
            s[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
                    G[key] = self.gamma * G[key] + (1 - self.gamma) * np.square(model.grads[key])
                    d_t = np.multiply(np.sqrt(s[key] + self.epsilon) / np.sqrt(G[key] + self.epsilon), model.grads[key])
                    s[key] = self.gamma * s[key] + (1 - self.gamma) * np.square(d_t)
                    model.params[key] -= d_t

            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)

 

__init__은 동일하고,

train 함수에서 s변수를 새로 만들어서 s값을 저장하게 합니다.

그리고, 계산 식은 위에서 쓴 식과 동일하게 적용했습니다.

(d_t는 $\Delta_\theta$를 줄여 쓴 것입니다.)

 

 

 

마지막으로, Adam에 대해 설명하겠습니다.

Adam은 RMSProp와 Momentum을 섞은 듯한 알고리즘입니다.

식으로 먼저 볼까요?

 

$$ m_t = \beta _1m_{t-1} + (1-\beta _1)\nabla_\theta J(\theta) $$

$$ v_t = \beta _2v_{t-1} + (1-\beta _2)(\nabla_\theta J(\theta))^2 $$

 

여기서 $\beta_1, \beta_2$는 위의 $\gamma$와 동일한 역할을 합니다.

그러면... 지금까지 RMSProp과 Momentum만 제대로 이해하셨다면 어떤 느낌인지 아실 겁니다!

이제 이것들을 사용해서 $\theta$를 update를 할 텐데, 왜 $\theta$의 update 식을 안썼을까요?

 

잘 생각해 보시면 알겠지만, m와 v값은 처음에 0으로 초기화 될 겁니다.

그런데, 위 식을 그대로 따라간다면, m값과 v값은 초기에 0에 매우 가깝게 다가가므로, 이 값들이 편향성을 갖게 됩니다.

그래서 위 식을 그대로 가져다 쓰지 않고, 다음과 같은 식을 사용합니다.

 

$$ \hat{m_t} = \frac{m_t}{1-\beta_1^t}$$

$$ \hat{v_t} = \frac{v_t}{1-\beta_2^t} $$

$$ \theta = \theta - \frac{\eta}{\sqrt{\hat{v_t}+\epsilon}}\hat{m_t} $$

 

그리고, 보통 $\beta_1 = 0.9, \beta_2 = 0.999, \epsilon = 10^{-8}$를 사용합니다.

 

 

자, 이제 이를 토대로 코드를 구현해 봅시다!

 

 

class Adam:
    def __init__(self, batch_size, beta_1 = 0.9, beta_2 = 0.999):
        self.x = None
        self.y = None
        self.model = None
        self.batch_size = batch_size
        self.epsilon = 10e-8
        self.beta_1 = beta_1
        self.beta_2 = beta_2

    def train(self, x_train, y_train, epoch, learning_rate, model):
        m = {}
        m_hat = {}
        v = {}
        v_hat = {}
        for p in model.params:
            m[p] = np.zeros(model.params[p].shape)
            m_hat[p] = np.zeros(model.params[p].shape)

            v[p] = np.zeros(model.params[p].shape)
            v_hat[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
                    m[key] = self.beta_1 * m[key] + (1 - self.beta_1)*model.grads[key]
                    m_hat[key] = m[key] / (1 - self.beta_1 * self.beta_1)
                    v[key] = self.beta_2 * v[key] + (1 - self.beta_2) * model.grads[key] * model.grads[key]
                    v_hat[key] = v[key] / (1 - self.beta_2 * self.beta_2)
                    model.params[key] -= learning_rate * m_hat[key] / np.sqrt(v_hat[key] + self.epsilon)


            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)

사실 여기도 위 식을 그냥 그대로 갖다 박은거 밖에 없어서 설명할 게 딱히 없습니다.

 

 

다음 시간에는, Regularization을 비롯한 다양한 테크닉들을 적용해 보도록 하겠습니다!

 

+ Recent posts