6장. 학습 관련 기술들 - 1

괴도소녀·2021년 7월 30일
0

TFMaster

목록 보기
7/9

오늘은 학습에 관련된 기술들에 대해서 얘기해볼 것이다.

매개변수 갱신

신경망 학습의 목적은 손실 함수의 값을 가능한 한 낮추는 매개변수를 찾는 것이다. 이는 곧 최적값을 찾는 문제이며, 이러한 문제를 푸는 것은 최적화(optimization)이라 한다.

지금까지는 최적의 매개변수 값을 찾는 단서로 매개변수의 기울기(미분)를 이용했다. 매개변수의 기울기를 구해, 기울어진 방향으로 매개변수 값을 갱신하는 일을 몇 번이고 반복해서 점점 최적의 값에 다가갔으며 이를 확률적 경사 하강법(SGD)라는 방법으로 부른다.

확률적 경사 하강법(SGD)

WWηLWW \leftarrow W - \eta \begin{matrix} \partial L \over \partial W \end{matrix}

WW는 갱신할 가중치 매개변수고 LW\partial L \over \partial WWW에 대한 손실 함수의 기울기이다. η\eta는 학습률을 의미한다. \leftarrow는 우변의 값으로 좌변의 값을 갱신한다는 뜻이다.

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]

lr는 learning rate(학습률)을 뜻한다. update(params, grads)메서드는 SGD과정에서 반복해서 불리며, 인수 params와 grads는 딕셔너리 변수이다.

SGD의 단점

SGD는 단순하고 구현도 쉽지만, 문제에 따라서는 비효율적일 때가 있다.

f(x,y)=120x2+y2f(x, y) = \begin{matrix} 1 \over 20 \end{matrix} \begin{matrix} x^2 + y^2 \end{matrix}


[그림 6-1]

기울기는 yy축 방향은 크고 xx축 방향은 작다는 것이 특징이다. 말하자면 yy축은 방향은 가파른데 xx축 방향은 완만한 것이다.

이제 [그림 6-1]의 함수에 SGD를 적용해보겠다. 탐색을 시작하는 장소(초깃값)은 (x, y) = (-7.0, 2.0)으로 하겠다.

SGD는 위 그림과 같이 심하게 굽이진 움직임을 보여준다. 상당히 비효율적인 움직임이며, SGD의 단점은 비등방성(방향에 따라 성질, 즉 여기에서는 기울기가 달라지는 함수)에서는 탐색 경로가 비효율적이라는 뜻이다.

이러한 단점을 개선해 주는 momentum, AdaGrad, Adam 3가지 방법이 존재한다.

모멘텀

모멘텀(Momentum)은 '운동량'을 뜻하는 단어이며, 물리와 관계가 있다.

vαvηLW\mathbf{v} \leftarrow \alpha \mathbf{v} - \eta \begin{matrix} \partial L \over \partial \mathbf{W} \end{matrix}
WW+v\mathbf{W} \leftarrow \mathbf{W} + \mathbf{v}

W\mathbf{W}는 갱신할 가중치 매개변수,
LW\partial L \over \partial \mathbf{W}W\mathbf{W}에 대한 손실 함수의 기울기,
η\eta은 학습률이다.
v\mathbf{v}라는 변수는 물리에서 말하는 속도(velocity)에 해당한다.

αv\alpha \mathbf{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: 
        	self.v = {} 
            for key, val in params.items(): 
            self.v[key] = np.zeros_list(val) 
            for key in params.keys(): 
            	self.v[key] = self.momentum * self.v[key] - self.lr * self.grads[key]
                self.params[key] += self.v[key]

인스턴스 v\mathbf{v}가 물체의 속도이다. v\mathbf{v}는 초기화 때는 아무것도 담지 않고, 대신 update()가 처음 호출될 때 매개변수와 같은 구조의 데이터를 딕셔너리 변수로 저장한다.

SGD와 비교하면 '지그재그 정도'가 덜하다. 이는 xx축의 힘은 아주 작지만 방향은 변하지 않아서 한 방향으로 일정하게 가속하기 때문이다. 거꾸로 4y축의 힘은 크지만 위아래로 번갈아 받아서 상충하여 $y축 방향의 속도는 안정적이지 않다. 전체적으로 SGD보다 xx축 방향으로 빠르게 다가가 지그재그 움직임이 줄어든다.

AdaGrad

신경망에서는 학습률(수식에서는 η\eta로 표기)값이 중요하다. 이 값이 너무 작으면 학습 시간이 너무 길어지고, 반대로 너무 크면 발산하여 학습이 제대로 이뤄지지 않는다. 이 학습률을 정하는 효과적 기술로 학습률 감소(learning rate decay)가 있다.

학습률을 서서히 낮추는 가장 간단한 방법은 매개변수 '전체'의 학습률 값을 일괄적으로 낮추는 것이며 이를 더욱 발전시킨 것이 AdaGrad이다. '각각의' 매개변수에 '맞춤형'값을 만들어준다.

AdaGrad는 개별 매개변수에 적응적으로 학습률을 조정하면서 학습을 진행하며 갱신 방법은 식으로 나타내면 다음과 같다.

W\mathbf{W}는 갱신할 가중치 매개변수,
LW\partial L \over \partial \mathbf{W}W\mathbf{W}에 대한 손실 함수의 기울기,
η\eta은 학습률이다.
h\mathbf{h}는 기존 기울기 값을 제곱하여 계속 더해준다.
그리고 매개변수를 갱신할 때 1h1\over \sqrt{\mathbf{h}}을 곱해서 학습률을 조정한다.

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]
                    	params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)

마지막 줄에 0에 가까운 1e-7을 더해줘서 self.h[key]에 0이 담겨 있다 해도 0으로 나누는 사태를 막아준다.

yy축 방향은 기울기가 커서 처음에는 크게 움직이지만, 그 큰 움직임에 비례해 갱신 정도도 큰 폭으로 작아지도록 조정된다. 그래서 yy축 방향을 갱신 강도가 빠르게 약해지고, 지그재그 움직임이 줄어든다.

Adam

AdaGrad와 Momentum방식을 혼합한 방법이다.

Adam은 하이퍼파라미터를 3개 설정한다.

  • 학습률 (learning rate)
  • 일차 모멘텀 계수 β1\beta_1
  • 이차 모멘텀 계수 β2\beta_2

원본 논문

어느 갱신 방법을 이용할 것인가?

지금까지 매개변수의 갱신 방법을 4개 알아보았다.

  • SGD
  • Momentum
  • AdaGrad
  • Adam

사용한 기법에 따라 갱신 경로가 다르며, 하이퍼파라미터를 어떻게 설정하느냐에 따라서도 결과가 바뀐다.

많은 연구에서 SGD를 사용하고 있지만, 요즘 많은 사람들이 Adam에 만족해하는 경향들이 두드러진다.

가중치의 초깃값

신경망 학습에서 특히 중요한 것이 가중치의 초깃값이다.

초깃값을 0으로 하면?

이제부터 오버피팅을 억제해 범용 성능을 높이는 테크닉인 가중치 감소(weight decay)기법을 소개하겠다. 가중치 감소는 간단히 말해 가중치 매개변수의 값이 작아지도록 학습하는 방법이다. 가중치 값을 작게 하여 오버피팅이 일어나지 않게 하는 것이다.

가중치를 작게 만들고 싶으면 초깃값도 최대한 작은 값에서 시작하는 것이 정석이며, 사실 지금까지의 가중치의 초깃값은 0.01 * np.random.randn(10, 100)처럼 정규분포에서 생성되는 값을 0.01배 한 작은 값(표준편차가 0.01인 정규분포)를 사용했다.

가중치의 초깃값을 모두 0으로 설정하면 학습이 올바로 이뤄지지 않는다.
초깃값을 모두 0으로 해서는 안 되는 이유는 (정확히는 가중치를 균일한 값으로 설정해서는 안 된다.) 오차역전파법에서 모든 가중치의 값이 똑같이 갱신되기 때문이다.
예를 들어 2층 신경망에서 첫 번째와 두 번째 층의 가중치가 0이라고 가정하자. 그럼 순전파 때는 입력층의 가중치가 0이기 때문에 두 번째 층의 뉴런에 모두 같은 값이 전달된다.
두 번째 충의 모든 뉴런에 같은 값이 입력된다는 것은 역전파 때 두 번째 층의 가중치가 모두 똑같이 갱신된다는 말이 된다. 그래서 가중치들은 같은 초깃값에서 시작하고 갱신을 거쳐도 여전히 같은 값을 유지하는 것이다. 이는 가중치를 여러 개 갖는 의미를 사라지게 한다. 이 '가중치가 고르게 되어버리는 상황'을 막으려면 초깃값을 무작위로 설정해야만 한다.

은닉층의 활성화값 분포

은닉층의 활성화값(활성화 함수의 출력 데이터)의 분포를 관찰하면 중요한 정보를 얻을 수 있다. 가중치의 초깃값에 따라 은닉층 활성화값들이 어떻게 변화하는지 간단한 실험을 해보자. 구체적으로는 활성화 함수로 시그모이드 함수를 사용하는 5층 신경망에 무작위로 생성한 입력 데이터를 흘리며 각 층의 활성화값 분포를 히스토그램으로 그려보겠다.

import numpy as np
import matplotilb.pyplot as plt

def sigmoid(x):
	return 1/ (1 + np.exp(-x))
    
x = np.random(1000, 100) # 1000개의 데이터
node_num = 100	# 각 은닉층의 노드(뉴런) 수
hidden_layer_size = 5	# 은닉층이 5개
activations = {}	# 이곳에 활성화 결과(활성화값)를 저장

for i in range(hidden_layer_size):
	if i != 0:
		x = activations[i-1]
    
    	w = np.random.randn(node_num, node_num) * 1
    	a = np.dot(x, w)
    	z = sigmoid(a)
    	activations[i] = z

층은 5개가 있고, 각 층의 뉴런은 100기썍이다. 입력 데이터로서 1000개의 데이터를 정규분포로 무작위로 생성하여 이 5층 신경망에 흘린다. 활성화 함수로는 시그모이드 함수를 이용했고, 각 층의 활성화 결과를 activations 변수에 저장한다. 이 코드에서는 가중치의 분포에 주의해야 한다.
이번에는 표준편차가 1인 정규분포를 이용했는데, 이 분포된 정도(표준 편차)를 바꿔가며 활성화값들의 분포가 어떻게 변화하는지 관찰하는 것이 이 실험의 목적이다. 그럼 activations에 저장된 각 층의 활성화값 데이터를 히스토그램으로 그려보자.

# 히스토그램 그리기 
for i, a in activations.items(): 
	plt.subplot(1, len(activations), i+1) 
    	plt.title(str(i+1) + '-layer') 
    	plt.hist(a.flatten(), 30, range=(0,1)) 

plt.show()

각 층의 활성화값들이 0과 1에 치우쳐 분포되어 있다. 여기에서 사용한 시그모이드 함수는 그 출력이 0에 가까워지자(또는 1에 가까워지자) 그 미분은 0에 다가간다. 그래서 데이터가 0과 1에 치우쳐 분포하게 되면 역전파의 기울기 값이 점점 작아지다가 사라진다. 이것이 기울기 소실(gradient vanishing)이라 알려진 문제이다. 층을 깊게 하는 딥러닝에서는 기울기 소실은 더 심각한 문제가 될 수 있다.

이번에는 가중치의 표준편차를 0.01로 바꿔 같은 실험을 반복해 보자. 앞의 코드에서 가중치 초깃값 설정 부분을 다음과 같이 바꾸면 된다.

# w = np.random.randn(node_num, node_num) * 1 
w = np.random.randn(node_num, node_num) * 0.01

표준편차를 0.01로 한 정규분포의 경우 각 층의 활성화값 분포는 다음과 같다.

이번에는 0.5 부근에 집중되었다. 앞의 예처럼 0과 1로 치우치진 않았으니 기울기 소실 문제는 일어나지 않았지만, 활성화값들이 치우쳐져 있어 뉴런을 여러 개 둔 의미가 사라진다. 그래서 활성화값들이 치우치면 표현력을 제한한다는 관점에서 문제가 된다.

이어서 사비에르 글로로트와 요슈아 벤지오의 논문에서 권장하는 가중치 초깃값인, 일변 Xavier 초깃값을 써 보자.이 논문은 각 층의 활성화값들을 광범위하게 분포시킬 목적으로 가중치의 적절한 분포를 찾고자 했다. 그리고 앞 계층의 노드가 nn개라면 표준편차가 1n1 \over \sqrt n인 분포를 사용하면 된다는 결론을 이끌었다.

Xavier 초깃값을 사용하면 앞 층에 노드가 많을수록 대상 노드의 초깃값으로 설정하는 가중치가 좁게 퍼진다. 이제 Xavier 초깃값을 써서 실험해 보자.

node_num = 100 # 앞 층의 노드 수
w = np.random.randn(node_num, node_num) / np.sqrt(node_num)

Xavier 초깃값을 사용한 결과를 보면, 형태가 일그러지지만, 값들이 적당히 퍼져 있으며, 시그모이드 함수의 표현력도 제한받지 않고 학습이 효율적으로 이루어질 것을 기대할 수 있다.

위 그림에서의 일그러짐은 sigmoid 함수 대신 tanh함수를 사용하면 개선된다. 일반적으로 활성화 함수용으로는 원점에서 대칭인 함수가 바람직하다고 알려져 있다.

ReLU를 사용할 때의 가중치 초깃값

Xavier 초깃값은 활성화 함수가 선형인 것을 전제로 이끈 결과이다. sigmoid함수와 tanh함수는 좌우 대칭이라 중앙 부근이 선형인 함수로 볼 수 있지만 ReLU를 이용할 때는 이에 특화된 초깃값을 이용하라고 권장하고 있다.

  • ReLU 활성화 함수에 특화된 초깃값 : He 초깃값
    He 초깃값은 앞 계층의 노드가 nn개 일때, 표준편차가 2n\sqrt 2 \over n인 정규분포를 사용한다.

다음은 각각 순서대로 표준편차를 0.01, Xavier초깃값, He 초깃값을 사용했을 때의 활성화값들의 분포이다.

위 실험 결과를 바탕으로, 활성화 함수로 ReLU를 사용할 때는 He초깃값을, sigmoid나 tanh 등의 S자 모양 곡선일 때는 Xavier 초깃값을 쓰는 것이 현재의 모범 사례이다.

MNIST 데이터셋으로 본 가중치 초깃값 비교

이번에는 '실제'데이터를 가지고 가중치의 초깃값을 주는 방법이 신경망 하습에 얼마나 영향을 주는지 보자.

적절한 가중치를 주는 것은 학습에 매우 중요한 요소이며, 가중치의 초깃값에 따라 신경망 학습의 성패가 갈리는 경우가 종종 있다.

0개의 댓글