[밑바닥딥러닝] 12. 매개변수 갱신법

2021. 10. 16. 19:11Deep Learning

본 게시글은 한빛미디어 『밑바닥부터 시작하는 딥러닝, 사이토 고키, 2020』의 내용을 참조하였음을 밝힙니다.

 

 

'좋은 가중치'를 찾기 위해서 우리는 손실 함수를 통해 각 가중치의 값을 지속적으로 갱신한다. 

 

저번 장에서는 가중치의 값을 갱신하는 방법을 확률적 경사 하강법(SGD)를 통해 알아보았는데 

 

SGD 이외에도 매개변수(가중치) 갱신에 다양한 방법을 응용할 수 있다. 

 

다양한 매개변수 갱신법에 대해 알아보도록 하자. 

 

 

 

확률적 경사 하강법(SGD)


확률적 경사 하강법

출처 : https://www.geeksforgeeks.org/difference-between-batch-gradient-descent-and-stochastic-gradient-descent/

 

확률적 경사 하강법(SGD)는 가장 기본적인 경사 하강법이라 할 수 있다. 

 

손실함수(J(θ))에 대해서 각 가중치(θ)별로 편미분한 값을 학습률에 곱하여 기존 가중치에서 차감한다. 

 

여기서 '확률적'이라는 수식어가 붙는 이유는 모델을 훈련시킬 때 전체 훈련 데이터 중 일부 배치 데이터를

 

무작위로 추출하여 이에 대한 손실을 계산하기 때문이다. 

class SGD:
    """확률적 경사 하강법(Stochastic Gradient Descent)"""
    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]

update 메소드에서 모델의 params(매개변수 딕셔너리)와 grads(그레디언트 딕셔너리)를 매개변수로 받아서 

 

grads를 통해서 기존 매개변수들(params)을 업데이트해준다. 

        for i in range(iternum):
            .
            x_batch = x_train[batch_mask]
            t_batch = t_train[batch_mask]
            grads = predictor.gradient(x_batch, t_batch)
            self.optimizer['SGD'].update(predictor.params, grads)
            .
            .

SGD 클래스 및 추가적인 최적화 클래스들을 optimizer라는 패키지에 모두 모아두었다. 

 

모델을 훈련시킬 때 모델은 단순히 순전파를 계산하고 그레디언트 값들과 매개변수 값들을 optimizer에 

 

넘겨주기만 하면 optimizer는 매개변수 갱신만 수행하는 방법으로 모듈화해두었다.

 

확률적 경사 하강법에 대해서는 지난 장에서 살펴보았으므로 더 이상의 설명은 생략하도록 하겠다. 

 

 

 

모멘텀(Momentum)


모멘텀

출처 : https://light-tree.tistory.com/140

 

모멘텀은 물체가 이동할 때 관성을 유지하려는 성질을 의미하며,

 

이와 같은 성질을 매개변수 갱신에 적용한 것이 위의 식이다. 

 

SGD에서는 단순히 가중치에 대한 미분값을 기존 가중치에서 차감하였는데, 모멘텀에서는 v라는 개념이 등장하여 

 

학습률이 곱해진 v에서 미분값을 차감하고 이를 기존 가중치에 더해준다. 

 

직관적으로 생각하면 모멘텀은 과거의 데이터(이전 가중치들)를 새로운 가중치에 반영하는 메커니즘이다. 

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: #한번도 update하지 않았다면
            self.v = {} #딕셔너리 초기화
            for key, val in params.items():
                self.v[key] = np.zeros_like(val) #params과 같은 형상의 v 생성

        for key in params.keys():
            self.v[key] = self.momentum * self.v[key] - self.lr * grads[key]
            params[key] += self.v[key]

v는 가중치와 동일한 형상을 가져야하므로 params에 저장된 가중치 정보를 참조하여 완전히 같은 형상의 

 

빈 v를 생성하고, 이 v를 계속 갱신해나간다. 

 

 

 

Adagrad


Adagrad를 학습률을 점차 감소시켜 나가는 갱신 방법이다. 

 

학습률이 너무 크면 손실 최저지점에서 최적값을 찾는 데 해맬 수 있고, 반대로 학습률이 너무 작으면 

 

최적 가중치까지 도달하는데 너무 오랜 시간이 걸린다. 

 

처음에는 가중치를 크게하여 학습하고, 최적값에 가까워짐에 따라서 가중치를 작아지게 하면 

 

각자의 단점을 모두 극복할 수 있을 것이다. 

 

Adagrad

출처 : https://towardsdatascience.com/introduction-and-implementation-of-adagradient-rmsprop-fad64fe4991

 

s를 이전 s와 그레디언트의 제곱(원소별 곱)과 더하고 이 값으로 그레디언트를 나눈다. 

 

s를 갱신할 때 그레디언트의 원소별 곱셈을 수행하므로 그레디언트가 크게 움직인 원소는 작게 갱신된다.

 

s는 이전 s에서 계속 더해지기 때문에 s의 값은 점차 커져만 간다. 따라서 학습이 진행됨에 따라

 

가중치는 점점 조금씩만 갱신된다. 

class AdaGrad:
    """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의 개념을 융합한 Adam이라는 방법에 대해서 알아보자. 

m과 v

출처 : https://towardsdatascience.com/adam-latest-trends-in-deep-learning-optimization-6be9a291375c

 

Adam에서는 m과 v라는 개념이 등장한다. 베타(β1, 2)가 m과 v에, 1-β(1, 2)가 그레디언트에 적용되어 

 

m과 v가 갱신된다.

Adam

Adam의 가중치 갱신 공식은 위와 같다.

class Adam:
    """Adam (http://arxiv.org/abs/1412.6980v8)"""

    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)

Adam에서는 학습률 역시 갱신되는데 베타(β1, 2)와 학습반복횟수(iter)에 영향을 받는다. 

 

 

 

갱신법 비교


옵티마이저 별 매개변수 변화 양상

출처 : https://laptrinhx.com/ml04-from-ml-to-dl-to-nlp-morton-kuo-3162303318/

 

위 그림은 SGD, Momentum, Adagrad, Adam에 따라 갱신되는 매개변수 변화를 나타낸 것이다.

 

확실히 모멘텀은 이전 변화에 의해 저항을 받는 편이고, Adagrad는 최적해에 빠르게 접근하는 양상을 보인다. 

 

Adam은 모멘텀과 Adagard 두 가지 옵티마이저의 특징을 모두 보여주고 있다.

    optimizers = {}
    optimizers['SGD'] = optimizer.SGD()
    optimizers['Momentum'] = optimizer.Momentum()
    optimizers['Adagrad'] = optimizer.AdaGrad()
    optimizers['Adam'] = optimizer.Adam()

    train_loss_list = {}
    train_loss_list['SGD'] = []
    train_loss_list['Momentum'] = []
    train_loss_list['Adagrad'] = []
    train_loss_list['Adam'] = []

    for key in optimizers:
        predictor = TwoLayerNet(784, 50, 10)
        .
        .
        for i in range(iternum):
            .
            .
            grads = predictor.gradient(x_batch, t_batch)
            optimizers[key].update(predictor.params, grads)
            .
            train_loss_list[key].append(loss)
    loss_array = {}

    for key in train_loss_list:
        loss_array[key] = np.array(train_loss_list[key])
        print(key," : ",np.mean(loss_array[key]))

각 매개변수 갱신법(SGD, momentum, adagrad, adam)마다 손실을 계산하였다.

    >>> 결과값
    SGD  :  0.5394052491776588
    Momentum  :  0.1710974149078524
    Adagrad  :  0.20954943225360934
    Adam  :  0.12610166012684842

Adam이 SGD, Momentum, Adagrad에 비해 더 나은 결과를 보여준다.