들어가며
이 포스팅의 내용은 <<밑바닥부터 시작하는 딥러닝>> 6장, 학습 관련 기술들을 정리한 내용입니다.
학습 관련 기술들
이전 장에서는(여기 있는 포스팅은 3장, 5장만 있지만) 딥러닝 신경망은 어떻게 구성되고, 경사하강법을 사용하여 가중치는 어떻게 수정하고, 역전파를 이용하여 빠르게 기울기를 구하는 방법을 익혔습니다.
이번 장에서 다룰 내용은 다음과 같습니다
- 가중치 매개변수의 최적값을 탐색하는 최적화 방법
- 가중치 매개변수 초깃값
- 하이퍼파라미터 설정 방법
- 오버피팅의 대응책
- 가중치 감수
- 드롭아웃
매개변수 갱신
신경망 학습의 목적은 손실 함수의 값을 가능한 한 낮추는 매개변수를 찾는 것입니다. 이는 곧 최적 매개변수를 찾는 문제이고,이러한 문제를 푸는 것을 최적화라고 합니다. 매개변수 공간은 매우 넓고 복잡하여, 최적 매개변수를 찾는 것은 쉽지 않은 문제입니다.
이전 장에서 최적 매개변수를 찾기 위해서 매개변수의 기울기를 이용했습니다. 손실 함수를 구하고, 손실 함수의 값이 0에 가까워지게 매개변수를 지속적으로 수정해 나갔습니다. 이를 확률적 경사 하강법(SGD)라고 합니다.
그러나 문제에 따라서는,SGD보다 똑똑한 방법이 있습니다. 이 장에서는 그런 방법을 이야기하고자 합니다.
확률적 경사 하강법(SGD)
먼저 고전적인 방법인 SGD를 다시 한번 확인해 보겠습니다. 수식으로는 다음과 같이 쓸 수 있습니다.
W←W−ησLσWW\xleftarrow { } W-\eta \frac { \sigma L }{ \sigma W }WW−ησWσL
여기에서 W는 갱신할 가중치 매개변수이고, σLσW\frac { \sigma L }{ \sigma W }σWσL 는 W에 대한 손실 함수의 기울기입니다.
η\etaη는 여기서 학습률을 의미하는데, 학습률은 실제로는 0.01이나 0.001과 같은 값을 미리 정하여 사용합니다. 이 SGD를 파이썬 클래스로 구현하면 다음과 같습니다.
class SDG:
def __init__(self, lr=0.01):
self.lr = lr # 학습률을 의미한다.
def update(self, param, grads):
for key in params.keys():
params[key] -= self.lr * grads[key] # 가중치 매개변수 -= 학습률 * 기울기SGD의 단점
SDG는 구현은 쉽지만, 문제에 따라 비효율적일 때가 있습니다. 다음 함수의 최솟값을 구하는 문제를 생각해보겠습니다.
f(x,y)=120x2+y2f\left( x,y \right) =\frac { 1 }{ 20 } { x }^{ 2 }+{ y }^{ 2 }f(x,y)=201x2+y2
이 함수는 밥그릇을 x축 방향으로 늘린 듯한 모습입니다. 이 함수의 기울기를 그려 보면, y축 방향은 가파른 데 비해 x축 방향은 작게 그려집니다.
또한 위의 식이 최솟값이 되는 장소는 (x,y) = (0, 0)이지만, 실제로 기울기 대부분은 (0,0)을 가리키지 않습니다. 즉, 경사하강법을 사용하면 이상한 곳에 도달할 확률이 높습니다.
이를 정리하면 다음과 같습니다.
SGD는 비등방성(방향에 따라 성질, 즉 여기서는 기울기가 달라지는 함수)에서는 탐색 경로가 비효율적이다.
이제부터 위의 문제점을 개선해주는 모멘텀, AdaGrad, Adam이라는 세 방법을 소개하겠습니다. 이들은 모두 SGD를 대체하는 방법입니다.
모멘텀
이 기법은 수식으로는 다음과 같이 나타낼 수 있습니다.
υ
←
α
υ
−
η
σ
L
σ
W
\upsilon \quad \xleftarrow { } \alpha \upsilon -\eta \frac { \sigma L }{ \sigma W } \
υ
αυ
−
η
σW
σ
L
위에서 갱신한 v값을 가지고 아래 W를 갱신합니다.
W <- W + v
W는 갱신할 가중치 매개변수, σLσW\frac { \sigma L }{ \sigma W } σWσL 는 W에 대한 손실함수의 기울기, η\etaη 는 학습률입니다. v라는 변수가 새로 나오는데, 이는 물리에서 말하는 속도에 해당합니다.
모멘텀이 추가되면, 다음 두 가지 속성을 갖게 됩니다.
- 공이 그릇의 바닥을 구르는 듯한 움직임으로 손실함수값이 갱신됩니다.
- av값은 물체가 아무런 힘을 받지 않을 때, 서서히 하강하게 만듭니다. (a값은 0.9 등의 상수로 설정합니다)
소스코드는 다음과 같습니다.
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 paras.items():
self.v[key] =np.zeros_like(val) # update 시에 None 이면 매개변수와 같은 구조의 데이터로, 0을 채워서 생성
for key in params.keys():
# self.v[key]는 이전 update때 움직였던 가중치값(항상 마이너스)
self.v[key] = self.momentum * self.v[key] - self.lr*grads[key]
params[key] += self.v[key]
# self.v[key]는 음수로 시작. 이전에 많이 이동했다면(-가 크다면), 다음 update에도 많이 움직인다. 이전에 조금 이동했다면 다음 update에는 더 조금 움직이게 된다. 즉, 갑작스럽게 방향전환이 되지 않고 한 방향으로 이동하게 된다.
이를 이용하여 위의 문제를 풀면, 공이 그릇의 바닥을 구르듯 움직입니다.
- x축의 힘은 아주 작지만 방향이 변하지 않습니다.
- y축의 힘은 크지만 위아래로 번갈아 움직여 속도는 안정적이지 않습니다.
AdaGrad
신경망 학습에서는 학습률이 중요합니다. 수식으로는 η\etaη입니다. 이 학습률을 정하는 기술로 학습률 감소가 있습니다. 이는 학습을 진행하면서 학습률을 점차 줄여가는 방법입니다.
학습률을 낮추는 방법으로는 전체의 학습률 값을 일괄적으로 낮추는 것이 있는데요. 이를 발전시킨 것이 AdaGrad 입니다. AdaGrad는 각각의 매개변수의 맞춤형 값을 만들어줍니다.
수식으로는 다음과 같습니다.
h←h+σLσW⊚σLσWh\quad \xleftarrow { } h+\frac { \sigma L }{ \sigma W } \circledcirc \frac { \sigma L }{ \sigma W }hh+σWσL⊚σWσL
W←W−η1hσLσWW\xleftarrow { } W-\eta \frac { 1 }{ \sqrt { h } } \frac { \sigma L }{ \sigma W }WW−ηh1σWσL
- W : 갱신할 가중치 매개변수
- σLσW\frac { \sigma L }{ \sigma W }σWσL : W에 대한 손실 함수의 기울기
- h : 기존 기울기 값을 계속 제곱하여 더해준 값
이런 후에 매개변수를 갱신할 때, 1h\frac { 1 }{ \sqrt { h } }h1를 곱해 학습률을 조정합니다. 즉, 많이 갱신된 함수는 학습률 η\etaη가 낮아지게 되는 것입니다.
또한, AdaGrad는 과거의 기울기를 계속 제곱하여 더해가기 때문에, 학습을 진행할수록 갱신 강도가 약해집니다. 어느 순간 갱신량이 0이 되어서 전혀 갱신되지 않게 됩니다. 이를 개선한 기법으로 RMSProp이 있는데, 이는 먼 과거의 기울기는 서서히 잊고 새로운 기울기 정보를 크게 반영하는 학습 방법입니다.
코드는 다음과 같습니다.
class AdaGrad:
def __init__(self, lr=0.01):
self.lr = lr
self.h = None
def update(self, paras, 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]
paras[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7) # 계속 제곱하다 보면 self.h[key]값이 0이 될 수 있기 때문에, 이를 막기 위해 1e-7을 더해준다.이를 통해 최적화 문제를 풀면, 맨 처음에는 y축 방향으로 크게 움직이지만, 학습 강도가 크기 때문에, 갱신 강도도 큰 폭으로 줄어듭니다. 따라서 y축으로 갱신 강도가 약해져 지그재그 움직임이 줄어듭니다.
RMSprop
AdaGrad와 흡사하지만, 과거의 기울기를 계속 제곱하여 더해가지 않고, 먼 과거의 기울기는 서서히 잊고 새로운 기울기 정보를 반영하는 학습 방법입니다. 여기서는 decay_rate라는 값이 등장합니다. 이 값은 시간이 지날수록 얼마나 값을 decay하는지를 나타내는 값입니다. 이 값이 클수록 이전 기울기는 더 빨리 잊고, 새로운 기울기 정보를 더 많이 반영하게 됩니다. decay는 보통 0.9 혹은 0.95로 둡니다.
def __init__(self, lr=0.01, decay_rate = 0.99):
self.lr = lr
self.decay_rate = decay_rate
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) # 맨 처음에 들어온 경우, h 값을 0으로 초기화한다.
for key in params.keys():
self.h[key] *= self.decay_rate # decay_rate를 곱한다. decay_rate를 계속 곱하기 때문에 오래 전에 누적된 기울기는 영향력이 점점 작아진다.
self.h[key] += (1 - self.decay_rate) * grads[key] * grads[key] # 기울기를 제곱한다. 최근에 추가된 기울기가 더 큰 영향을 미치게 된다. 업데이트는 (1-decay_rate) 만큼 수행한다. 이를 지수이동평균이라고 한다(http://zetawiki.com/wiki/%EC%A7%80%EC%88%98_%EC%9D%B4%EB%8F%99_%ED%8F%89%EA%B7%A0) 이 코드에서 식의 알파값은 0.01이 된다.
params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7) # 1e-7은 0으로 나눠지는 것을 막기 위해 더해주는 값Adam
모멘텀은 공이 구르는 듯한 물리 법칙을 따르는 움직임을 보였습니다. RMSprop은 매개변수의 원소마다 적응적으로 갱신 강도를 조정하였습니다. RMSprop에 모멘텀을 도입한 것이 Adam입니다.
코드는 다음과 같습니다.
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] = self.beta1*self.m[key] + (1-self.beta1)*grads[key]
#self.v[key] = self.beta2*self.v[key] + (1-self.beta2)*(grads[key]**2)
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)
# 학습의 초반부에 시행하는 프로세스로 추측됨 - m, v가 0에 가깝게 bisa 되어 있을 거라고 판단하여 unbiased 하게 해주는 작업을 거침
#unbias_m += (1 - self.beta1) * (grads[key] - self.m[key]) # correct bias
#unbisa_b += (1 - self.beta2) * (grads[key]*grads[key] - self.v[key]) # correct bias
#params[key] += self.lr * unbias_m / (np.sqrt(unbisa_b) + 1e-7)Adam또한 갱신 과정도 그릇에서 공이 구르는 듯 움직입니다. 다음의 블로그 글 또한 좋습니다.
어떤 갱신 방법을 이용할 것인가?
풀어야 할 문제에 따라 어떤 갱신 기법을 사용할지 달라집니다. 각각의 장단이 있어서 잘 푸는 문제와 서툰 문제가 있습니다.
지금도 많은 연구에서 SGD를 사용하고 있고, 모멘텀과 AdaGrad도 시도해볼 만한 가치가 있습니다. 또한 요즘에는 많은 분들이 Adam에 만족해하고 계십니다. 각자의 상황을 고려하여 여러 가지 시도해보고, 가장 효율적인 것을 사용하면 될 거 같습니다.
가중치의 초깃값
신경망 학습에서 특히 중요한 것이 가중치의 초깃값입니다. 권장하는 가중치 초깃값과 실험을 통해 실제로 어떻게 초깃값이 신경망 학습에 영향을 미치는지 확인할 예정입니다.
초깃값을 0으로 하면?
신경망 학습 시에, 훈련용 데이터로 학습 시에는 제대로 분류할 수 있지만 실제 데이터 분류시에는 제대로 분류하지 못하는 현상을 오버피팅이라고 합니다. 일정 데이터에만 신경망이 제대로 동작하는 것이지요. 이것을 막기 위해 가중치 감소라는 기법을 사용할 수 있습니다. 가중치 매개변수의 값이 작아지도록 학습하여, 오버피팅이 일어나지 않게 하는 것입니다.
가중치를 작게 만들고 싶으면, 초기값도 작은 값부터 시작해야 할 것입니다. 지금까지 가중치의 초깃값은 정규분포에서 생성되는 값을 0.01배 한 값을 사용하였습니다. 즉, 표준편차가 0.01인 정규분포입니다.
그렇다면, 가중치의 초깃값을 0으로 설정하면 어떻게 될까요? 이는 나쁜 아이디어입니다. 가중치를 0으로(정확히 말하면, 균일한 값으로 설정하면) 오차역전파법에서 모든 가중치의 값이 동일하게 계산되기 때문입니다.
예를 들어, 2층 신경망에서 첫 번째와 두 번채 층의 가중치가 0이라고 가정하겠습니다. 그럴 때, 순전파 입력치의 가중치가 0이기 때문에, 두번째 층의 뉴런에 모두 같은 값이 전달됩니다. 이렇게 되면, 역전파 시에 두 번째 층의 가중치가 모두 동일하게 갱신됩니다. 이는 가중치를 여러 개 쓰는 것을 의미없게 만듭니다.
이것을 막기 위해서는, 초깃값을 무작위로 설정하여야 합니다.
은닉층의 활성화값 분포
은닉층의 활성화 함수의 출력 데이터의 분포를 관찰해봅시다. 가중치의 초깃값에 따라 은닉층 활성화값들이 어떻게 변화하는지 확인해보려고 합니다.

각 층별로 활성화값의 히스토그램을 확인하면, 다음과 같은 사실을 알 수 있습니다.
- 각 층의 활성화값이 0과 1 사이에 분포되어 있습니다.
시그모이드 함수는 출력이 0, 혹은 1에 가까워지면 그 미분값은 0에 가까워집니다. 그래서 데이터가 0과 1에 치우쳐서 분포하게 되면, 역전파의 기울기값이 점점 작아지다가 사라집니다. 이것이 기울기 소실이라고 알려진 문제입니다. 층이 깊어지면, 이 문제는 더 심각해 질 수 있습니다.
이번에는 가중치의 표준편차를 0.01로 바꾸어서 같은 실험을 해 보겠습니다.

이번에는 0.5 부분에 집중되었습니다. 0/1에 집중되지 않았다는 것은 기울기 소실 문제는 없다는 것이지만, 다수의 뉴런이 거의 같은 값을 출력하고 있다는 의미기 때문에, 표현력에 문제가 있다는 의미가 됩니다. 활성화값은 적당히 고르게 분포되어야, 신경망 학습이 효율적으로 이루어지게 됩니다.
이 문제를 해결하기 위해, Xavier 초깃값을 사용해보도록 하겠습니다. 현재 이 초깃값은 일반적인 딥러닝 프레임워크들이 표준적으로 이용하고 있습니다.
앞 계층의 노드가 n개라면, 표준편차가 1n\frac { 1 }{ \sqrt { n } }n1 인 분포를 사용하면 됩니다. 이 분포를 사용하면 다음과 같은 결과가 나오게 됩니다.

ReLU를 사용할 때의 가중치 초깃값
Xavier 초깃값은 활성화 함수가 선형인 것을 전제로 이끈 결과입니다. 그렇기 때문에, ReLU를 사용할때는 ReLU에 특화된 초깃값을 이용하라고 권장합니다. 이를 He 초깃값이라고 하는데, 앞 노드가 n개일때, 표준편차가 2n\sqrt { \frac { 2 }{ n } }n2인 정규분포를 사용하는 것입니다.
He 초깃값을 사용하면, 활성화 함수 결과값이 모든 층에 균일하게 분포되기 때문에, 층이 깊어져도 적절한 값이 나올 것으로 기대할 수 있습니다.
정리하자면 다음과 같습니다.
- 활성화 함수가 sigmoid나 tanh같이 S자 모양 곡선인 경우 : Xavier
- 활성화 함수가 ReLU인 경우 : He
가중치 초깃값은 신경망 학습에 큰 영향을 미치기 때문에, 신중하게 결정해야 합니다.
배치 정규화
앞에서 가중치의 초깃값을 적절하게 설정하여, 각 층의 활성화값 분포를 균일하게 퍼트리면 학습이 원활하게 된다는 것을 알게 되었습니다. 그렇다면, 각 층이 활성화를 적당히 퍼트리도록 강제하면 어떻게 될까요? 배치 정규화가 그런 아이디어에서 출발한 방법입니다.
배치 정규화 알고리즘
2015년에 세상에 나온 기법입니다만, 많은 연구자와 기술자들이 즐겨 사용하고 있습니다. 이 방법이 주목받는 이유는 다음과 같습니다.
- 학습을 빨리 진행할 수 있다.
- 초깃값에 크게 의존하지 않는다.
- 오버피팅을 억제한다.
기본적인 아이디어는 행렬곱과 활성화 함수 처리과정 중간에, 배치 정규화 계층(Batch Norm)을 삽입하여, 활성화값이 적당히 분포되도록 조정하는 것입니다.
미니배치 단위로 정규화를 진행하는데, 데이터 분포가 평균이 0, 분산이 1이 되도록 정규화합니다. 수식으로는 다음과 같습니다.
μB←1m∑_i=1mxi{ \mu }_{ B }\xleftarrow { } \frac { 1 }{ m } \sum _{ i=1 }^{ m }{ { x }{ i } }μ_Bm1∑_i=1mxi
σB2←1m∑_i=1m(x_i−μB)2{ \sigma }_{ B }^{ 2 }\xleftarrow { } \frac { 1 }{ m } \sum _{ i=1 }^{ m }{ { (x }_{ i }- } { { \mu }{ B }) }^{ 2 }σ_B2m1∑_i=1m(x_i−μB)2
xi←x_i−μ_BσB2+ε{ x }_{ i }\xleftarrow { } \frac { { x }_{ i }-{ \mu }_{ B } }{ \sqrt { { \sigma }{ B }^{ 2 }+\varepsilon } }x_iσB2+εx_i−μ_B
미니배치 m개의 입력 데이터에 대해 평균과 분산을 구하고, 그 값을 가지고 어파인 출력값을 평균 0, 분산 1인 데이터로 정규화하는 식입니다.
또한 배치 정규화 계층마다, 정규화된 데이터에 확대와 이동 변환을 수행합니다. 수식으로는 다음과 같습니다.
yi←γxi+β{ y }_{ i }\xleftarrow { } \gamma { x }{ i }+\betay_iγxi+β
γ\gammaγ가 확대를, β\betaβ가 이동을 담당하는데요. 두 값은 처음에는 γ\gammaγ=1, β\betaβ=0부터 시작합니다. 즉, 원본 그대로 시작하다가 학습하면서 적합한 값으로 조절해 나갑니다.
거의 모든 경우에서 배치 정규화를 사용할 때 학습 진도가 빠르고, 가중치 초깃값에 크게 의존하지 않아도 됩니다.
바른 학습을 위해
기계학습에서는 위에서 언급한 오버피팅이 문제가 되는 경우가 많습니다. 훈련 데이터에는 포함되지 않은 데이터도 바르게 식별해내는 모델을 만들어야 하기 때문에, 오버피팅을 억제하는 기술이 중요합니다. 여기서는 오버피팅을 억제하는 방법에 대해 이야기 해 보겠습니다.
오버피팅
주로 다음의 두 가지 경우에 일어납니다.
- 매개변수가 많고 표현력이 높은 모델
- 훈련 데이터가 적음
가중치 감소
학습 과정에서 큰 가중치에 대해서는 큰 페널티를 부과하여 오버피팅을 억제하는 방법입니다. 신경망 학습의 목적은 손실 함수의 값을 줄이는 것인데요. 이 때, 가중치의 제곱(L2 법칙)을 손실 함수에 더하면 가중치가 커지는 것을 억제할 수 있습니다.
가중치를 W라고 하면
- 가중치 감소를 구한다 : 12ΛW2\frac { 1 }{ 2 } \Lambda { W }^{ 2 }21ΛW2
- 손실 함수 <- 손실함수 + 12ΛW2\frac { 1 }{ 2 } \Lambda { W }^{ 2 }21ΛW2
- 여기서 람다는 정규화의 세기를 조정하는 하이퍼파라미터입니다. 람다를 크게 설정할수록 큰 가중치에 대한 패널티가 커집니다.
- 앞의 1/2은 미분시 ΛW2\Lambda { W }^{ 2 }ΛW2 이 값이 나오게 하기 위한 상수입니다.
가중치 감소는 모든 가중치 각각의 손실 함수에 12ΛW2\frac { 1 }{ 2 } \Lambda { W }^{ 2 }21ΛW2를 더합니다. 따라서 가중치의 기울기를 구하려면은, 오차역전파법에 따른 결과에 정규화 항을 미분한 ΛW2\Lambda { W }^{ 2 }ΛW2를 더합니다.
드롭아웃
앞에서는 손실 함수에 가중치의 L2 값을 더한 가중치 감소 방법을 설명했습니다. 신경망 모델이 단순할 때는 위의 방법도 유용합니다만, 복잡해지면 가중치 감소만으로는 대응하기 어렵습니다. 이럴 때는 드롭아웃이라는 기법을 사용합니다.
드롭아웃은 뉴런을 임의로 삭제하면서 학습하는 방법입니다. 훈련 때 은닉층의 뉴런을 무작위로 골라 삭제합니다. 삭제된 뉴런은 신호를 전달하지 않게 됩니다.
- 훈련 때는 데이터를 흘릴 때마다 삭제할 뉴런을 무작위로 선택합니다.
- 시험 때는 모든 뉴런에 신호를 전달합니다. 단, 각 뉴런의 출력 * 훈련 때 삭제한 비율을 곱하여 출력합니다.
순전파 시에는 훈련 때만 잘 계산해두면, 시험 때는 삭제한 비율을 곱하지 않아도 좋습니다. 실제 딥러닝 프레임워크들도 비율을 곱하지 않습니다.
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에 삭제할 뉴런을 False로 표시한다.
self.mask = np.random.rand(*x.shape) > self.dropout_ratio # 배열을 무작위로 생성하고, 그 값이 dropout_ratio보다 큰 것만 True로 설정한다.
return x * self.mask
self:
return x * (1.0 - self.dropout_ratio)
def backward(self, dout):
return dout * self.mask # 순전파에서 False가 된 것들은, 역전파에도 신호를 차단한다.

위가 그냥 학습한 것이고, 아래가 드롭아웃을 적용하여 학습한 것입니다.
- 드롭아웃을 적용시, 훈련 데이터에 대한 정확도가 1.0에 도달하지 못했습니다.
- 훈련 데이터와 실제 데이터 사이의 정확도 차이가 줄었습니다.
적절한 하이퍼파라미터 값 찾기
신경망에는 하이퍼파라미터가 다수 등장합니다. 예를 들면
- 각 층의 뉴런 게수
- 배치 크기
- 매개변수 갱신 시의 학습률과 가중치 감소
등입니다. 하이퍼파라미터의 값은 중요하지만, 이를 결정하는 데 많은 시행착오를 겪게 됩니다. 여기서는 하이퍼파라미터의 값을 효율적으로 탐색하는 법에 대해 이야기할 것입니다.
검증 데이터
지금까지는 데이터셋을 훈련 데이터와 시험 데이터로 나누어서 학습했는데요. 앞으로 하이퍼파라미터를 다양한 값으로 설정, 검증할 것인데 이 때는 시험 데이터를 사용해서는 안 됩니다. 매우 중요하지만 놓치기 쉬운 부분입니다.
시험 데이터를 사용하여 하이퍼파라미터를 조정하면, 하이퍼파라미터 값이 시험 데이터에 오버피팅되기 때문입니다. 하이퍼파라미터 값의 ‘좋음’이 시험 데이터로 확인하게 되므로, 하이퍼파라미터의 값이 시험 데이터에 적합하게 맞춰져 다른 데이터에서는 활용하기 어려워질 수 있기 때문입니다.
그래서 하이퍼파라미터를 조정할 때는 전용 데이터가 필요합니다. 이를 일반적으로 검증 데이터라고 합니다. 데이터를 정리하면 다음과 같습니다.
- 훈련 데이터 : 매개변수 학습
- 검증 데이터 : 하이퍼파라미터 성능 평가
- 시험 데이터 : 신경망의 범용 성능 평가
MNIST에서는 훈련/시험 데이터만 있어서, 사용자가 직접 검증 데이터를 분리해야 합니다. 가장 간단한 방법은 훈련 데이터의 20% 정도를 검증 데이터로 분리하는 것입니다.
하이퍼파라미터 최적화
하이퍼파라미터 최적화의 핵심은, 하이퍼파라미터 ‘최적 값’이 존재하는 범위를 조금씩 줄여 가는 것입니다. 순서는
- 대략적인 범위를 설정한다
- 무작위로 하이퍼파라미터 값을 골라낸다(샘플링)
- 샘플링한 하이퍼파라미터 값을 가지고 학습하고, 검증 데이터로 정확도를 평가한다.
- 정확도를 살피면서 2~3번을 여러 번 반복하여(100회 등) 최적값의 범위를 좁혀간다.
범위는 대략적으로 지정하는 것이 효과적입니다. 실제로도 0.001에서 1000사이와 같이, 10의 계승 단위로 범위를 지정합니다. 이를 로그 스케일로 지정한다고 합니다.
딥러닝 학습에는 오랜 시간이 걸립니다. 1~3번을 계속해서 반복해야 하기 때문에, 나쁠 듯한 값은 일찍 포기하는 것이 좋습니다. 그래서 에폭을 적게 하여 여러 1회 평가에 걸리는 시간을 단축하는 것이 효과적입니다.
하이퍼파라미터 최적화 구현하기
학습률과 가중치 감소의 세기를 조절하는 계수를 탐색하는 문제를 풀어보겠습니다. 이 예제에서는, 가중치 감소 계수를 10−8 10−4{ 10 }^{ -8 } ~ { 10 }^{ -4 }10−8 10−4, 학습률을 10−6 10−2{ 10 }^{ -6 } ~ { 10 }^{ -2 }10−6 10−2 범위부터 시작합니다. 이 경우, 코드는 다음과 같습니다.
weight_decay = 10 ** np.random.uniform(-8, -4)
lr = 10 ** np.random.uniform(-6, -2)이렇게 무작위로 추출한 값을 가지고 학습을 시작합니다. 그 후에는 다양한 하이퍼파라미터 값으로 학습을 반복하여, 신경망에 좋을 거 같은 값이 어디에 존재하는지 관찰합니다.
여러 번 학습을 한 후에, 학습이 잘 된 모델을 추출하여 그 때의 하이퍼파라미터 값을 확인합니다. 예를 들어, 학습이 잘 진행될 때의 학습률은 0.001~0.01 이고, 가중치 감소 계수는 10−8 10−6{ 10 }^{ -8 } ~ { 10 }^{ -6 }10−8 10−6 이라고 하면, 축소된 값을 가지고 다시 변수를 할당하여 학습을 반복합니다. 이렇게 범위를 좁혀가다가, 특정 단계에서 최종 하이퍼파라미터 값을 선택합니다.
베이지안 최적화
위에서 설명한 하이퍼파라미터 최적화는 경험적인 기법입니다. 일정 범위 내의 랜덤한 하이퍼파라미터 값을 취한 후, 결과값을 보고 하이퍼파라미터의 범위를 점점 줄여 나가기 때문입니다. 반면에, 지금 소개할 베이지안 최적화는 좀 더 이론적인 기법입니다. 자료를 조사하던 중 찾은 블로그가 설명이 잘 되어 있어, 링크 남깁니다.
정리
- 매개변수 갱신 방법 : SGD, 모멘텀, AdaGrad, Adam
- 가중치 초깃값 설정은 학습에 매우 중요하다 : 초깃값으로는 Xavier와 He 초깃값이 효과적이다
- 배치 정규화를 이용하면 학습이 빨라지고 초깃값에 영향을 덜 받게 된다
- 오버피팅을 억제하는 기술 : 가중치 감소와 드롭아웃
- 하이퍼파라미터 값 탐색은 최적 값이 존재할만한 범위를 점차 좁히면서 하는 것이 효과적이다.