딥러닝

[Deep Learning from scratch] 6. 학습 관련 기술들

PKkomi 2023. 5. 24. 11:36

신경망 학습의 목적은 loss(cost)를 가능한 한 낮추는 최적의 매개변수를 찾는 것이며, 이 과정을 최적화(optimization)이라 한다.

이번 장에서는 여러 방법들이 각각 어떤 특징을 가지고 있는지 살펴보고 weight의 초기값이나 hyperparameter를 어떻게 구하는지 등을 자세히 알아보자.


 

지금까지 사용한 최적화는 매개변수의 기울기(미분)을 이용하는 확률적 경사 하강법(SGD)이란 방법이다.

SGD의 수식은 다음과 같다.

W : 갱신할 weight parameter

에타 : 학습률(learning rate)

W에 대한 L의 편미분 : loss function의 gradient

 

SGD를 코드로 구현하면 다음과 같다.

class SGD:
    def __init__(self, lr=0.01):
        self.lr = lr
        
    def update(self, params, grads):
        for key in params.keys():
            params[ket] -= self.lr * grads[key]
            
 # 실제로 동작하지 않는 의사코드이다.

network = TwoLayerNet(...)
optimizer = SGD()

for i in range(10000):
    ...
    x_batch, t_batch = get_mini_batch(...)
    grads = network.gradient(x_batch, t_batch)
    params = network.params
    optimizer.update(params, grads) # SGD를 10000번 반복하여 optimize.
    ...

params, grads : 딕셔너리 형태의 변수로 각각 weight에 따른 weight와 gradient를 저장하고 있다.

 

SGD는 매우 단순하면서도 효과적이지만, 단점도 명확하다.

다음 그림을 살펴보자.

f(x,y) = 1/20 * x**2 + y**2의 기울기

위 그림의 f는 y축에 비해 x축이 완만한 경사를 가져 실제 최솟값이 (0,0)임에도 그쪽으로의 기울기가 거의 없다.

또한 아래 그림을 보면 지그재그로 움직이며, 이는 SGD가 비등방성(anisotropy) 함수에서 탐색 경로가 매우 비효율적임을 나타낸다.

시작점 (-7.0, 2.0)

 

 

이러한 SGD의 단점을 보완하기 위한 기법으로 모멘텀, AdaGrad, Adam이라는 세 방법이 존재한다.

 

먼저 모멘텀(momentum)을 알아보자.

모멘텀은 공이 그릇의 곡면을 따라 움직이듯이 움직이게 한다는 의미이다.

수식으로 나타내면 다음과 같다.

새로운 변수 v : 물리에서의 속도(velocity)

alpha*v 항 : 물체가 아무런 힘을 받지 않을 때 서서히 하강시키는 역할을 한다(0.9 등의 값을 설정한다).

 

이를 코드로 구현하면 다음과 같다.

class Momentum:
    def __init__(self, lr=0.01, momentum=0.9):
        self.lr = lr
        self.momentum = momentum
        self.v = None # v는 초기화할 때 아무값도 주지 않는다.
        
    def update(self, params, grads):
        if self.v is None: # update 함수가 처음 호출될 때 한 번만 작동 - self.v에 값을 지정하므로
            self.v = {}
            for key, val in params.items():
                self.v[key] = np.zeros_like(val) # self.v에 매개변수와 같은 구조의 0으로 이루어진 data를 딕셔너리 변수로 저장한다.
                
        for key in params.keys():
            self.v[key] = self.momentum*self.v[key] - self.lr*grads[key]
            params[key] += self.v[key] # momentum 수식 계산

 

이를 통한 최적화 갱신 경로는 다음과 같다.

SGD에 비해 지그재그가 심하지 않고, 빠르게 최솟값에 도달하는 것을 확인할 수 있다.

 

 

두번째로, AdaGrad를 알아보자.

learning rate를 학습의 진행에 따라 점차 줄여가는 방식을 학습률 감소(learning rate decay)라고 한다.

가장 간단한 방법은 모든 매개변수에 대한 학습률을 일괄적으로 동일하게 줄이는 것이다.

이때, 이를 발전시킨 것이 AdaGrad로 '각각의' 매개변수에 적절한 학습률 값을 만들어준다.

수식으로 나타내면 다음과 같다.

새로운 변수 h : 첫번째 식에서 동그라미 기호는 행렬의 원소별 곱셈을 의미한다. 따라서, h는 기존 기울기값을 제곱하여 기존의 h에 계속 더해준다.

학습률 : 매개변수를 갱신할 때 1/sqrt(h)를 곱해 조정한다.

 

위 식들을 해석해보면, 매개변수의 원소 중에서 많이 움직인(크게 갱신된) 원소는 학습률이 낮아진다. 기울기가 큰만큼 그 원소의 h는 커질 것이고, 이에 따라 학습률이 더욱 작아질 것이기 때문이다. 이를 바꿔 말하면 각 원소에 따라 학습률 감소가 다르게 적용된다는 것이다.

 

* 이때, 계속해서 학습한다면 학습률은 점점 작아져 0에 수렴할 것이다. 그러면 더이상 학습되지 않게 된다. 이를 해결하기 위해 RMSProp이라는 방법이 있다. 과거의 모든 기울기에 새로운 기울기를 더하는 것이 아니라, 먼 과거의 기울기는 서서히 잊고 새로운 기울기 정보를 크게 반영하는 방식으로 이 문제를 해결한다. 이를 지수이동평균(Exponential Moving Average; EMA)이라 하여, 과거 기울기의 반영 규모를 기하급수적으로 감소시킨다.

 

AdaGrad를 구현한 코드는 다음과 같다.

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] # 기울기가 클수로 각 원소별 h는 커진다
            params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key] + 1e-7)) # h의 값에 따라 학습률은 달라진다

 

학습률이 0이 되는 것을 방지하기 위해 코드의 마지막줄에서 1e-7이라는 매우 작은 값을 더해준다.

이 방식으로 구한 최적화 갱신 경로는 다음과 같다.

확실히 최솟값을 향해 효율적으로 움직이는 것을 알 수 있다.

 

 

세번째로, Adam을 알아보자.

Adam은 모멘텀과 AdaGrad를 합치려는 시도에서 나온 기법이다.

특징으로는 hyperparameter도 '편향보정'이 진행된다는 점이 있다.

다소 복잡하므로 코드를 따로 다루진 않는다.

 

이 방식으로 구한 최적화 갱신 경로는 다음과 같다.

 

 

SGD부터 그 단점을 보완한 3개의 방법을 살펴보았다.

각 문제마다, hyperparameter를 어떻게 설정하느냐에 따라 다른 결과를 보인다.

하지만 대체로 다른 세 기법이 SGD보다 빠르게 학습하고, 때로는 최종 정확도도 높게 나타내는 경향을 보였다.


 

가중치의 초깃값을 설정하는 것은 매우 중요하다. 

어떻게 설정하느냐에 따라 신경망 학습의 성패가 갈리는 경우도 꽤 존재한다.

권장 초깃값에 대해 알아보자.

 

오버피팅을 억제해 범용 성능을 높이는 테크닉인 가중치 감소(weight decay)가 존재한다.

간단하게 설명하면 가중치 매개변수 자체를 작게 하는 것인데, 만약 0으로 한다면 제대로 학습이 이뤄지지 않는다.

오차역전파법의 곱셈 노드의 역전파에서 노드를 갱신해도 여전히 같은 값을 유지하여 가중치가 여러 개가 아닌 하나의 값을 가지게 된다.

그렇기 때문에 초깃값을 0 또는 균일한 값으로 하는 것이 아니라 무작위로 설정해야 한다.

 

가중치 분포의 표준편차를 1 혹은 0.01로 하면 각 층의 활성화값의 분포는 다음과 같다.

가중치 분포의 표준편차가 1

 

데이터가 0과 1에 치우쳐 분포하게 되어 역전파의 기울기값이 계속 작아지다가 0이 된다. 이를 기울기 소실(gradient vanishing)이라 한다.

가중치 분포의 표준편차가 0.01

활성화 값이 0.5 부근에 집중되었다. 이 경우에 뉴런의 개수가 많아도 모두 같은 값을 나타내므로 표현력을 제한하여 학습이 잘 이뤄지지 않는다는 문제가 있다.

 

이처럼 초깃값의 설정은 매우 중요한데, 각 층의 활성화값들을 광범위하게 분포시키는 권장치인 Xavier 초깃값이라는 것이 존재한다.

앞 계층의 노드가 n개면 1/sqrt(n)이 표준편차인 분포를 사용하면 된다고 한다.

실제로는 앞 층의 입력 노드 수 외에 다음 층의 출력 노드 수도 고려한 설정값을 제안하기도 한다.

앞 층의 노드 수를 100개로 통일하고 이 방식을 쓰면 각 층의 활성화값 분포는 다음과 같다.

분포가 확실히 넓게 분포됨을 알 수 있다. 

 

Xavier 초깃값은 활성화 함수가 선형인 것을 전제로 이끈 결과이다. sigmoid 함수나 tanh 함수는 좌우 대칭이라 중앙 부근을 선형 함수로 볼 수 있다.

하지만 ReLU를 이용할 때는 다른 초깃값을 사용해야 하는데, 이를 He 초깃값이라고 한다.

앞 계층의 노드가 n개일 때, 표준편차가 sqrt(2/n)인 정규분포를 사용한다.

이 방식 역시 Xavier 초깃값과 마찬가지로 넓게 분포된 히스토그램을 얻을 수 있다.


 

다음으로 배치 정규화(Batch Normalization)에 대해 알아보자.

 

배치 정규화가 널리 쓰이는 이유는 다음과 같다.

1) 학습 속도 개선이 가능하다.

2) 초깃값에 크게 의존하지 않는다.

3) 오버피팅을 억제한다.(드롭 아웃 등의 필요성 감소)

 

배치 정규화는 학습 시 미니배치를 단위로 정규화한다. 즉 데이터 분포가 평균이 0, 분산이 1이 되게 한다.

또한 배치 정규화 계층을 다음과 같이 신경망에 삽입한다.

배치 정규화가 사용된 신경망의 예

다음의 수식을 이용하여 정규화를 진행한다.

 

이 과정을 통해 데이터 분포가 덜 치우치게 한다.

그리고 정규화된 데이터에 고유한 확대와 이동을 적용하여 변환시킨다.

감마: 확대, 베타: 이동

 


 

오버피팅은 딥러닝을 하다 보면 직면하는 큰 문제 중 하나다.

주로 매개변수가 많고 표현력이 높은 모델을 사용하거나, 훈련 데이터 자체가 적은 경우에 쉽게 발생한다.

오버피팅이 발생하면 모델이 훈련 데이터에만 적응하여 훈련에 사용하지 않은 데이터에 대해 제대로 대응하지 못한다.

 

이를 방지하기 위해 주로 쓰는 방법 중 하나는 가중치 감소(weight decay)이다.

오버피팅은 가중치 매개변수의 값이 커서 발생하는 경우가 많다.

예를 들어, 손실함수에 가중치 W의 L2norm을 더하는 방법이 있다. 이에 따른 가중치 감소는 0.5*람다*W^2이 되며 람다는 hyperparameter로, 람다가 클수록 큰 가중치에 대한 페널티가 부여되어 오버피팅을 억제한다.

 

이 방법은 구현이 간단하다는 장점이 있지만 신경망 모델이 복잡해지면 이것만으로는 대처하기가 어렵다는 단점도 존재한다.

 

 

이를 해결하기 위한 또다른 방법으로 드롭아웃(Dropout)이 있다.

드롭아웃은 뉴런을 임의로 삭제하면서 학습하는 방법이다. 데이터를 훈련할 때는 데이터를 전달할때마다 삭제할 뉴런을 무작위로 선택하고, validation 과정에서는 모든 뉴런에 신호를 전달한다. (이때, 검증 단계에서는 각 뉴런의 출력에 훈련 때 삭제한 비율을 곱하여 출력하기도 한다.)

 

이를 간단한 코드로 구현하면 다음과 같다.

class Dropout:
	def __init__(self, dropout_ratio=0.5):
    	self.dropout_ratio = dropout_ratio
        self.mask = None
        
	def forward(self, x, train_flg=True):
    	if train_flg:
        	self.mask = np.random.rand(*x.shape) > self.dropout_ratio # 삭제할 원소는 False로 설정
            # x와 형상이 같은 배열을 무작위로 생성하고, 그 값이 dropout_ratio보다 큰 원소만 True로 설정
            return x * self.mask
        else:
        	return x * (1.0 - self.dropout_ratio) # 삭제한 비율을 곱함
            
    def backward(self, dout):
    	return dout * self.mask

이때, 삭제할 비율을 굳이 곱하진 않아도 된다. 실제 딥러닝 프레임워크들도 비율을 곱하진 않는다.

 

드롭아웃을 7층 네트워크(뉴런 수는 100개, 활성화 함수는 ReLu)로 진행하면 훈련 데이터와 시험 데이터에 대한 정확도 차이가 확실히 줄어드는 결과를 얻을 수 있다.

왼쪽은 드롭아웃 없이, 오른쪽은 드롭아웃을 적용한 결과(dropout_ratio=0.15)

 

* 기계학습에서는 앙상블 학습(ensemble learning)을 애용한다. 

앙상블 학습: 개별적으로 학습시킨 여러 모델의 출력을 평균 내어 추론하는 방식

앙상블 학습과 드롭아웃은 밀접하다. 뉴런을 무작위로 삭제하는 드롭아웃의 방식은 매번 다른 모델을 학습시키는 것으로 해석할 수 있다. 또한, 삭제한 비율을 곱하는 것으로 앙상블 학습에서 여러 모델의 평균을 내는 것과 같은 효과를 얻을 수 있다.

 


 

* validation data 관련 내용은 추후 업로드될 [Incarnate the Algorithm] Naive Cross-Validation에서 다루도록 하겠다.

 

하이퍼파라미터를 최적화 하는 방법이 존재하는데, 그 과정은 다음과 같다.

1) 신경망에서 최적화는 하이퍼파라미터의 최적값이 존재할 범위를 설정한다.

2) 그 범위에서 무작위로 값을 골라낸다(샘플링).

3) 샘플링한 값으로 학습하여 정확도를 평가한다.

4) 2~3단계를 반복하여, 정확도를 통해 최적값의 범위를 좁혀가는 방식이다. (그리드 서치 같은 규칙적인 탐색보다는 무작위로 샘플링해 탐색하는 편이 좋은 결과를 낸다고 알려져있다)

 

이때, 하이퍼파라미터의 범위는 대략적으로 로그 스케일로 지정한다.

이 과정은 오랜 시간이 걸리기 때문에, 학습을 위한 에폭(epoch)을 작게 하여 1회 평가에 걸리는 시간을 단축하는 것이 효과적이다.

더 엄밀하고 효율적인 기법으로는 베이즈 정리(Bayes' theorem)을 기반으로 한 베이즈 최적화(Bayesian optimization)가 있다.

 

초기 가중치 감소 계수의 범위를 1e-8 ~ 1e-4, 학습률은 1e-6 ~ 1e-2로 하여 실험하면 결과는 다음과 같다.

정확도가 높은 순으로 나열되어 있다.

이때, best1 ~ best5까지의 lr와 weight decay값을 관찰하여 범위를 계속해서 좁혀나갈 수 있다.

 

 


 

이번 장에서 주요하게 다룬 것들은 다음과 같다.

 

- 매개변수 갱신 방법: 확률적 경사 하강법(SGD), momentum, AdaGrad, Adam, etc.

- 가중치 초깃값 설정은 중요하다. 

- 배치 정규화를 통해 효율적인 학습과 함께 초깃값의 영향을 줄일 수 있다.

- 오버피팅을 억제하는 정규화 기법으로는 가중치 감소(weitght decay), 드롭아웃이 있다.

- 하이퍼파라미터 최적화는 최적값의 존재 범위를 줄여나가는 형식이 효과적이다.