Contents

  • Back Propagation
  • Activation Functions
  • Data Preprocessing
  • Weight Initialization
  • Regularizaiton
  • Parameter Optimization
  • Tips to train Neural Network

Back Propagation


Activation Functions


Sigmoid

sigmoid 비선형 함수(nonlinearity)는 아래와 같은 수학적 형태로 나타난다.

이 함수는 함수값을 [0, 1]로 제한시키며(squash) 만약 입력값이 매우 큰 양수이면 1, 매우 큰 음수이면 0에 다가간다. sigmoid는 뇌의 뉴런과 유사한 형태를 보이기 때문에 과거에 많이 쓰였던 activation 함수이지만 지금은 잘 쓰이지 않는다. 그 이유는 아래와 같다.

gradient를 죽이는 현상이 일어난다(gradient vanishing). sigmoid 함수의 gradient 값은 $x = 0$일때 가장 크며, ${\lvert}x{\rvert}$가 클수록 gradient는 0에 수렴한다. 이는 이전의 gradient와 local gradient를 곱해서 에러를 전파하는 backprop의 특성에 의해 그 뉴런에 흐르는 gradient가 사라져버릴(vanishing) 위험성이 크다.

함수값의 중심이 0이 아니다(not zero-centered). 이 경우에 무슨 문제가 발생할까? 어떤 뉴런의 입력값($x$)이 모두 양수라 가정하자. 편미분의 체인룰에 의해 파라미터 $w$의 gradient는 다음과 같다.

여기서 $L$은 loss 함수, $a$은 ${\bf w^{T}x}+b$를 의미한다. 이 식에 의해

이고 결론적으로,

이다. 파라미터의 gradient는 입력값에 의해 영향을 받으며, 만약 입력값이 모두 양수라면 파라미터의 부호는 모두 같게 된다. 이렇게 되면 gradient descent를 할 때 정확한 방향으로 가지 못하고, 지그재그로 수렴하는 문제가 발생한다. sigmoid 함수를 거친 출력값은 다음 레이어의 입력값이 되기 때문에 함수값이 not-centered의 특성을 가진 sigmoid는 성능에 문제가 생길 수 있다.

Tanh

tanh함수는 함수값을 [-1, 1]로 제한시킨다. 값을 saturate시킨다는 점에서 sigmoid와 비슷하지만, 함수값이 zero-centerd 되어 있다는 점에서 다르다. 때문에 tanh 비선형함수는 sigmoid 보다 선호도가 높다. 그리고 tanh 함수는 다음과 같이 sigmoid함수를 이용하여 간단하게 표현할 수 있다.

ReLU

ReLU는 Rectified Linear Unit의 약자로 최근 몇년간 가장 인기 있는 activation 함수이다. 이 함수는 $f(x) = max(0, x)$의 꼴로 표현할 수 있는데, 이는 $x > 0$ 이면 기울기가 1인 직선이고, $x < 0$이면 출력값은 항상 0이다. ReLU 함수의 특징은 다음과 같다.

  • sigmoid나 tanh 함수와 비교했을 때 SGD의 수렴속도가 매우 빠른 것으로 나타났다. 이는 함수가 saturated하지 않고 linear하기 때문에 나타난다.
  • sigmoid와 tanh는 exp()에 의해 미분을 계산하는데 비용이 들지만, ReLU는 별다른 비용이 들지 않는다(미분도 0 아니면 1이다).
  • ReLU의 큰 단점으로 네트워크를 학습할 때 뉴런들이 “죽는”(die) 경우가 발생한다. $x<0$일 때 기울기가 0이기 때문에 만약 입력값이 0보다 작다면 뉴런이 죽어버릴 수 있으며, 더이상 값이 업데이트 되지 않게 된다.
  • tanh와 달리 zero-centered 하지 않는다.

위 그림은 AlexNet 논문에서 ReLU와 tanh 함수를 비교한 것이다. 이 논문에서는 ReLU가 약 6배정도의 성능 향상이 일어났다고 한다.

Leaky ReLU

Leaky ReLU는 “dying ReLU” 현상을 해결하기 위해 제시된 함수이다. ReLU는 $x < 0$인 경우 항상 함수값이 0이지만, Leaky ReLU는 작은 기울기를 부여한다.

이 때 $a$ 은 매우 작은 값이다(0.01 등). 몇몇 경우에 이 함수를 이용하여 성능 향상이 일어났다는 보고가 있지만, 모든 경우에 그렇진 않다.

PReLU

Leaky ReLU와 비슷하지만, PReLU는 파라미터 $\alpha$를 추가하여, $x<0$일 때의 기울기를 트레이닝 할 수 있게 하였다.

ELU (Exponential Linear Units)

ELU는 Clevert et al., 2015 에 의해 나온 비교적 최신 방법이다.

ReLU-like함수들과의 비교 그림과 공식을 보면 알겠지만 ELU는 ReLU의 threashold를 -1로 낮춘 함수를 $e^x$를 이용하여 근사한 것이다. ELU의 특징은 다음과 같다.

  • ReLU의 장점을 모두 포함한다.
  • dying ReLU 문제를 해결하였다.
  • 출력값이 거의 zero-centered에 가깝다.
  • 하지만 ReLU, Leaky ReLU와 달리 exp()를 계산해야하는 비용이 든다.

Maxout

이 함수는 ReLU와 Leaky ReLU를 일반화 한 것이다.

위 식을 보면 알겠지만 ReLU와 Leaky ReLU는 이 함수의 특수한 경우이다. 예를 들어 ReLU는 $w_1, b _1 = 0$ 인 경우이다. Maxout은 ReLU가 갖고 있는 장점을 모두 가지며, dying ReLU 문제도 해결한다. 하지만 ReLU 함수와 달리 한 뉴런에 대해 파라미터가 두배이기 때문에 전체 파라미터가 증가한다는 단점이 있다.

Conclusion

지금까지 여러 activation 함수들을 살펴보았는데 그럼 어떤 것을 선택해야 할까? 이에 대한 결론을 내리자면 다음과 같다.

  • 가장 먼저 ReLU를 사용하자. 변형된 버전인 Leaky ReLU, ELU, Maxout들이 있지만 가장 많이 사용되는 activation 함수는 ReLU이다.
  • Leaky ReLU / Maxout / ELU도 시도해보자.
  • tanh도 사용할 순 있지만 큰 기대는 하지 않는게 좋다.
  • sigmoid는 절대 사용하지 말자 (RNN에서는 사용하긴 하지만 다른 이유가 있기 때문이다).

Data Preprocessing


위에서 입력값이 zero-centered가 아니면 생기는 문제에 대해 알아보았다. Activation 함수의 출력값을 zero-centered로 만드는 것도 중요하지만, 최초 입력 데이터들도 이와 같이 전처리 과정을 해주는 것이 성능 향상에 도움이 된다. 데이터 전처리 과정은 크게 3가지로 나눌수 있으며, 3가지 모두 입력 데이터 X = [N, D] 로 이루어져 있고, N은 데이터의 갯수, D는 각 데이터의 차원을 의미한다.

Mean Subtraction

가장 간단하고 보편적으로 많이 쓰이는 전처리 과정이다. 데이터의 모든 feature마다 평균으로 나누며, 기하학적으로 위 그림처럼 데이터를 zero-centered 데이터로 만드는 과정이다.
이미지 처리 분야에서는 평균 이미지를 빼거나, 3개 채널 각각 평균을 구해서 각 채널별로 평균을 빼서 전처리를 한다.

Normalization

데이터의 차원을 정규화하여 모든 차원을 같은 스케일로 근사하는 것이다. 정규화는 두 방식이 있는데 하나는 데이터의 차원마다 각각의 표준편차로 나눠 zero-centered 로 만들거나, 차원의 값이 [-1, 1]이 되도록 정규화 하는 방식이 있다.
후자의 방식은 차원(특징)들의 scale은 현재 다르지만, 트레이닝 시킬 때 같은 중요도를 나타내도록 하기 위해 진행한다. 이미지 처리 분야에서 모든 픽셀들의 scale은 [0, 255]로 같기 때문에 따로 처리를 하지는 않는다.
위 그림의 오른쪽은 각 차원을 표준 편차로 나누어 편차를 비슷하게 근사시킨 것을 볼 수 있다.

PCA, Whitening

작성중

왼쪽은 2차원의 기본 데이터이다. 중간은 PCA를 적용한 것인데 데이터들이 모두 zero-centerd되어 있고 데이터의 covariance 행렬의 eigenbasis로 회전된 것을 볼 수 있다 (covariance 행렬이 diagonal 된 상태이다). 오른쪽은 eigenvalue에 의해 추가적으로 scale 되었으며, 데이터의 covariance 행렬이 indentity 행렬로 변환된다.

Conclusion

현실에서는 PCA/Whitening 기법을 컨볼루션 네트워크에서 사용하진 않는다. 하지만 데이터들이 zero-centered 되어 있는 것은 중요하다.
예를 들어 AlexNet은 입력 이미지를 평균 이미지로 나누어 전처리를 하였으며, VGG 네트워크는 3개 채널별로 평균을 구한 뒤, 각 채널별로 평균을 나누는 전처리 과정을 거친다.

참고로 실수하기 쉬운 것이, 데이터 셋을 트레이닝/테스트 셋으로 나누어 진행할 때 테스트 단계에서 테스트 셋의 평균/표준편차 혹은 전체 데이터 셋의 평균/표준편차 를 사용해서는 안된다. 트레이닝 단계에서도 트레이닝 셋의 평균/표준편차를 사용하고, validation 단계와 테스트 단계에서도 트레이닝 셋의 평균/표준편차를 사용해야만 한다.

Weight Initalizaion

지금까지 네트워크 모델을 어떻게 구축하는지 살펴보았는데, 네트워크를 만들 때 파라미터들을 어떻게 초기화 하는지도 매우 중요하다.

모두 0 으로 초기화 하기

데이터 전처리 과정에서도 이야기 했듯이 파라미터의 절반은 음수, 나머지 절반은 양수로 하는것이 나을 것이다. 그럼 한번 모두 0으로 초기화 해보자. 그럴듯하게 생각 할 수 있는 아이디어이지만 해서는 안되는 초기화 방법이다. 왜일까?
수식으로 설명할 수도 있지만 간단하게 알아보면 backprop을 진행할 때 파라미터의 값이 모두 같다면 모든 뉴런이 전부 같은 gradient를 계산하게 되고 결국 모든 파라미터의 값이 똑같이 변하게 된다.
파라미터는 0에 근접해야 하는 사실은 변함이 없지만 0으로 해서는 안되는데, 그럼 0을 기준으로 하는 랜덤 값으로 하면 어떨까.

작은 랜덤 값으로 초기화 하기

파라미터를 다음과 같이 초기화 해보자.

W = 0.01 * random.gaussian(n_input, n_output)

위 식은 평균은 0이고, 표준 편차는 0.01인 가우시안 분포에 의해 랜덤하게 파라미터를 초기화 한다. 이 초기화 방법은 작은 네트워크에서는 잘 동작하지만 activation 함수를 집어넣은 큰 네트워크에서는 잘 동작 하지 않는다.

한가지 실험을 해보자. 500개의 뉴런을 가진 레이어 10개와 activation 함수는 tanh를 사용하는 네트워크를 가정해보자. 파라미터 초기화는 위 식과 같고 초기 데이터는 가우시안 분포를 따르도록 임의로 초기화 한다.
그 후 fowradprop 단계에서 각 레이어에 들어오는 입력값들의 평균과 표준편차를 출력해 보자.

레이어를 따라 진행할수록 평균은 그대로 0에 가깝지만 표준 편차는 1에서 0으로 계속 줄어드는 것을 확인 할 수 있다. 다시 말하면 모든 출력값이 거의 0에 가깝게 되어버린다. 입력값들이 모두 거의 0에 가까워지기 때문에 backprop 단계에서 파라미터의 업데이트가 이루어지지 않게 된다.

만약 분포의 표준편차를 0.01에서 1.0으로 바꾼다면 pre-activation값이 너무 크기 때문에 위 그림과 같이 -1, 1에 saturated 한 결과가 나온다. 이 경우에도 gradient가 0이 되어버린다.

조금 다르게 Xavier 초기화 방법[Glorot et al., 2010]을 사용해보자. 이 초기화 방법은

W = random.gaussian(n_input, n_output) / sqrt(n_input)

파라미터 w의 초기화 값을 input 뉴런의 수에 맞게 조정한다. 뉴런마다 이전 레이어에서 들어오는 input 뉴런의 수가 많다면 파라미터의 초기화값을 조금 더 낮춰 너무 큰 pre-activation 값이 되지 않도록 조정하고 그 반대의 경우에는 초기화값을 조금 높히는 역할을 한다.
괜찮은 성능을 보이지만 이 논문은 activation 함수에 대한 고려를 하지 않았기 때문에 아래 그림처럼 ReLU 함수를 쓴다면 0으로 수렴해버린다.

He et al., 2015에 의해 다른 방법이 제시되었다.

W = random.gaussian(n_input, n_output) / sqrt(n_input / 2)

ReLU 함수는 $x < 0$일 경우 활성화가 되지 않기 때문에 입력 뉴런의 개수의 절반만큼만 해당하는 뉴런들로 조정할 경우 아래 그림 처럼 괜찮게 분포가 나타나는 것을 확인 할 수 있다.
ReLU를 사용한다면 이 방법을 이용하여 파라미터를 초기화 하는 것이 가장 좋다.

바이어스 초기화 하기

파라미터(${\bf W}$)의 초기화는 [He et al., 2015]에서 주장하는 방법을 쓰는게 가장 효과적이지만, 바이어스($b$)는 0으로 초기화 하는 것이 가장 보편적이다.
ReLU를 사용한다면 0.01와 같은 작은 값으로 초기화 하면 성능이 향상된다는 보고도 있지만 모든 경우에 그런것은 아니고 오히려 그냥 0으로 초기화 하는 것이 더 일반적인 방법이다.

Batch Normalization

BN에 관해 정리한 한글 문서는 다음과 같다.

Regularization


L2 Regularization

L1 Regularization

Max norm

Dropout

배깅(Bagging, bootstrap aggregating)은 여러개의 모델을 혼합하여 일반화 에러(generalization error)를 줄일 수 있는 방법이다. 이 방법은 서로 다른 모델들을 각자 트레이닝시키고, 이 모델들로부터 생성되는 결과값을 투표를 통해 최종 결과값으로 선출하는 방법이다. 이 때 투표는 민주적으로 1개의 모델에 1표씩 행사할 수도 있고, 신뢰도에 따라 서로 다른 가중치를 두어 투표할 수도 있다.

같은 데이터셋을 사용한 뉴럴넷이라도 평균 모델 방법은 효과적이다고 알려져 있다. 뉴럴넷 모델의 랜덤 초기화, mini-batch의 랜덤선택, 다른 hypterparameter등을 통해 서로 다른 앙상블 모델이 될 수 있다.

배깅은 매우 일반화 에러를 줄이는데 매우 강력한 방법이지만 모델이 거대한 뉴럴넷이라면, 각각 네트워크를 학습시키는데 많은 시간과 메모리가 들기 때문에 이 방법을 적용시키기는 불가능 하다.
그래서 배깅을 approximate한 방법으로 Dropout이 나오게 되었다 (Srivastava et al., 2014). Dropout은 기존의 L1, L2, Maxnorm과 같은 정규화 방법을 보안한 강력한 방법이지만 계산은 배깅과 비교해선 그렇게 비싸진 않다는 장점이 있다.

좀 더 자세히 보면, dropout은 출력 유닛이 아닌 모든 유닛을 제거하여 나올 수 있는 네트워크의 경우의 수를 통해 학습한다.

위 그림을 통해 예를들면, 왼쪽의 베이스 네트워크는 두 개의 입력 유닛과 한 개의 출력 유닛, 그리고 은닉 층 한 개로 이루어져 있다. 이 네트워크에서 출력 유닛을 제외한 유닛들을 제거하여 나올 수 있는 네트워크의 가짓수는 오른쪽과 같다. 오른쪽 그림을 보면 16개의 경우의 수 중 4가지는 입력, 은닉층이 아예 없거나 출력층과 연결되지 않는데, 만약 네트워크의 크기가 매우 커진다면 이런 경우의 수가 일어날 확률이 매우 작아지므로 걱정하지 않아도 된다.

위 그림은 dropout이 적용된 네트워크의 feedforward 방법을 나타낸 것이다. 이 네트워크는 두 개의 입력 유닛, 두 개의 은닉 유닛을 가진 은닉층과 한 개의 출력 유닛이 있다.
Dropout은 출력 유닛을 제외한 나머지 유닛을 제거하여 배깅을 근사하지만, 일반적인 뉴럴넷은 어파인변환과 nonlinearity들로 이루어져 있기 때문에 단순히 유닛의 출력값에 0을 곱하면 이 유닛을 제거한 것과 같은 의미가 된다.

이 그림에서 $\boldsymbol{\mu}$ 유닛은 바이너리 유닛이며, 일반적인 구현에서는 입력층의 유닛은 0.8의 확률로 1을, 은닉층의 유닛은 0.5의 확률로 1이 된다. 유닛의 결과값에 $\boldsymbol{\mu}$ 유닛의 결과값(0 또는 1)을 곱한 것이 유닛의 최종 결과값이 된다는 점을 빼면 일반적인 뉴럴넷의 feedfoward 방식과 동일하다.

위 그림은 dropout이 적용된 네트워크를 도식화 한 것이다. 트레이닝 단계에서 일반적인 뉴럴넷으로 트레이닝시키는 경우는 왼쪽 그림과 같지만, dropout이 적용된 후에는 매우 sparse하게 연결이 되어 있는 것을 확인 할 수 있다.

추론시, 서브 네트워크들의 결과값을 평균을 내어 최종 결과로 선택하기에는 시간이 많이 걸리기 때문에 이를 근사한 방법을 사용한다. 아이디어는 dropout을 사용하지 않은 네트워크 하나를 이용하여 추론을 하되, feedfoward에서 각 유닛의 결과값에 확률 $p$를 곱하는 것이다. 위의 그림의 오른쪽이 추론 단계에 해당한다.

[Srivastava et al., 2014]에 의하면 dropout은 기존의 정규화 방법, 예를 들어 weight decay, sparse activity 보다 좋은 성능을 낸다고 한다. 그리고 dropout은 이런 정규화 방법과 결합하여 더 좋은 성능을 낼 수도 있다.

dropout의 의사코드를 통해 더 자세히 알아보도록 하자. 예제를 간단히 하기 위해 네트워크는 3개의 레이어로 구성되어 있으며, 각 레이어는 한개의 뉴런만 존재한다.

""" Vanilla Dropout: """

p = 0.5 # probability of keeping a unit active. 
	# higher = less dropout

""" X contains the data """
def train_step(X):
	H1 = np.maximum(0, np.dot(W1, X) + b1)
	U1 = np.random.rand(H1.shape) < p
	H1 *= U1 # drop!

	H2 = np.maximum(0, np.dot(W2, H1) + b2)
	U2 = np.random.rand(H2.shape) < p
	H2 *= U2 # drop!

	out = np.dot(W3, H2) + b3

	# backward pass: compute gradients... (not shown)
	# perform parameter update... (not shown)

def predict(X):
	# ensembled forward pass
	H1 = np.maximum(0, np.dot(W1, X) + b1) * p
	H2 = np.maximum(0, np.dot(W2, H1) + b2) * p
	out = np.dot(W3, H2) + b3

코드가 크게 어렵지는 않다. train_step 함수에서 feedfoward로 dropout을 하면서 출력값을 내는 것을 볼 수 있고, backprop은 생략하였다.
predict 함수에서는 dropout 하지 않는다. 대신 pre-activation 값에 $p$ 만큼 곱한다. 이는 최종 출력값의 expectation 값은 $px + (1-p)0$ 이 되는 것을 의미한다.

위 코드 자체로도 dropout을 구현할 수 있지만 predict 함수에서 pre-activation 값에 $p$ 만큼 곱하는 것이 마음에 걸린다. predict는 시간이 생명이니 inverted dropout을 구현하여 트레이닝 시간에 scaling을 하면 더 성능이 좋을 것이다.

""" Inverted Dropout: """

p = 0.5 # probability of keeping a unit active. 
		# higher = less dropout

def train_step(X):
	# forward pass for example 3-layer neural network
	H1 = np.maximum(0, np.dot(W1, X) + b1)
	U1 = (np.random.rand(H1.shape) < p) / p # Notice /p!
	H1 *= U1 # drop!

	H2 = np.maximum(0, np.dot(W2, H1) + b2)
	U2 = (np.random.rand(H2.shape) < p) / p # Notice /p!
	H2 *= U2 # drop!
	out = np.dot(W3, H2) + b3

	# backward pass: compute gradients... (not shown)
	# perform parameter update... (not shown)

def predict(X):
	# ensembled forward pass
	H1 = np.maximum(0, np.dot(W1, X) + b1) # no scaling necessary
	H2 = np.maximum(0, np.dot(W2, H1) + b2)
	out = np.dot(W3, H2) + b3

결론적으로, dropout의 장점은 다음과 같다.

  • 많은 계산을 요구하진 않지만 성능이 뛰어나다. dropout을 사용하면 트레이닝 단계에서 시간과 공간에서 $O(n)$이 들며, 추론 단계에서는 dropout을 사용하지 않은 것과 같다.
  • dropout은 어떤 모델에서든지 사용 가능하다. SGD와 같이 사용한다면 일반적인 MLP 방식의 네트워크 뿐만 아니라, 확률에 기반한 RBM, RNN등에도 사용이 가능하다. (다른 정규화 방식들은 몇몇 아키텍쳐에선 제한적으로 밖에 사용이 불가능하다.) (참고 논문 리뷰 Recurrent Neural Network Regularization)

하지만 레이블된 데이터가 매우 적을 경우 dropout은 오히려 성능을 떨어뜨릴수있다.
Srivastava et al. 2014에 의하면 5000개의 레이블된 데이터를 가지고 실험했을 경우 dropout은 베이지안 뉴럴 네트워크보다 낮은 성능을 보인 것으로 알려졌다. 만약 추가적으로 레이블되지 않은 데이터들이 있다면 그냥 unsupervised learning을 사용하는 것이 나을 것이다.

Conclusion

  • 가장 일반적인 방법은 global L2 정규화를 사용하는 것이며 $\lambda$는 cross-validation을 통해 조정해나간다.
  • 최근에는 모든 레이어에 dropout을 사용한다. 이 때 $p = 0.5$가 가장 괜찮은 기본 값이며, 이 또한 cross-validation을 통해 조정할 수 있다.

Parameter Optimization


Momentum

모멘텀(Momentum)은 깊은 네트워크에서 수렴 속도를 향상시키는 방법 중 하나이며, 물리학적 관점에서 영감을 받아 만들어졌다.

loss 값은 언덕의 높이로 볼 수 있으며, 포텐셜 에너지 ($U = mgh$)로도 볼 수 있다. 파라미터의 초기화는 어떤 한 점에서 공을 놓는 것, 최적화의 진행은 이 공을 시작 점에서 언덕 아래로 굴리는 시뮬레이션과 같다.

공에 받는 힘은 포텐셜 에너지의 gradient와 관련이 있는데($F = -{\nabla} U$), 포텐셜 에너지는 loss 함수이므로 공이 받는 힘은 loss 함수의 (음의) 그라디언트이다.
공의 속도는 $F = ma$에 의해 영향을 받는데, 이 말은 힘은 loss 함수의 gradient이므로 속도 또한 loss 함수의 그라디언트에 영향을 받는다는 의미이다.

모멘텀이 SGD와 다른점은 SGD는 공이 받는 힘이 공의 위치를 직접 결정짓지만, 모멘텀은 힘이 공의 속도를 결정하고 이 속도가 공의 위치를 결정한다. 이렇기 때문에 관성의 법칙에 의해 이전 공의 속도가 현재 공의 속도와 연관이 있게 된다. (위치는 속도의 미분, 속도는 힘의 미분이다.

vel = mu * vel - learning_rate * dx
x  += vel

초기 단계에서 vel은 0으로 초기화하며, SGD와 다르게 또 다른 hyperparameter인 모멘텀 값 mu가 있다. 모멘텀 값은 물리학적인 관점에서 마찰계수에 해당된다고 볼 수 있다. 마찰력이 없다면 공은 언덕 아래에 도달해도 멈추지 않고 움직일 것이기 때문에 꼭 필요하다. 일반적으로 모멘텀 값은 cross-validation으로 결정하는데 보통 0.5, 0.9, 0.95, 0.99정도를 많이 선택한다.

decay 방법처럼 학습을 진행시키면서 모멘텀 값을 변화시키는 방법도 많이 쓰인다. 이 때는 보통 초기에 0.5로 두고 0.99까지 decay 시킨다.

Nesterov Momentum

Nesterov 모멘텀은 모멘텀 알고리즘의 변형된 버전으로 최근에 많이 사용되고 있다. convex한 경우에 수렴이 더 잘된 다고 증명 되었으며, 실제로도 모멘텀보다 약간 더 성능이 좋다고 알려져 있다.

x_ahead = x + mu * vel
vel     = mu * vel - learning_rate * dx_ahead
x      += vel

Nesterov 모멘텀의 핵심 아이디어는 gradient를 구할 때 과거의 위치를 이용하여 구하지 말고, 미래의 위치를 근사하여 이 근사된 위치를 이용해 그라디언트를 구하자는 것이다. 따라서 그라디언트는 x가 아닌 x + mu * vel를 이용하여 구한다.

AdaGrad

AdaGrad(adaptive gradient)는 gradient-based인 최적화 방법 중 하나이다. AdaGrad를 설명하기 앞서, 이 알고리즘을 왜 고안하게 되었는지 직관적(intuitive)으로 설명하고자 한다.

문서를 분석해서 이 문서의 주제를 판별하는 모델을 만든다고 생각해보자. is, the, a와 같이 빈번하게(commonly) 나오는 단어들은 주제를 판별하는데 큰 도움이 되지 않을 것이다. 하지만 위 문서에서 Xerox와 같이 빈번하게 나오지 않는(rarely) 단어는 매우 중요한 단서이다. AdaGrad는 이 빈번히 나오지 않는 단어에 더 많은 weight를 부여한다.

다른 예로 인간의 뇌를 생각해보자. 인간의 뇌에는 수많은 뉴런들이 존재하고 각 뉴런은 역할을 맡아서 신호를 처리한다. 어떤 뉴런은 후각에 관련된 신호만 처리하지만, 대부분의 신호를 전부 처리하는 뉴런도 있다. 이 경우 후각과 관련된 모델을 만든다면 모든 신호를 처리하는 뉴런에 집중하기 보다 후각에만 관여를 하는 뉴런에만 집중하는 것이 옳을 것이다.

accm   += dx**2
weight += - learning_rate / sqrt(accm + 1e-8) * dx

위 의사코드는 AdaGrad의 의사코드이다. 부연설명을 하자면 다음과 같다.
accm 변수는 파라미터 개수만큼 필요하며, 파라미터의 gradient의 sum-of-squared를 저장한다. 그리고 SGD와 달리 accm 값의 제곱근을 곱해서 다음 weight를 계산한다.

이는 과거에 빈번하게(frequently) 신호가 들어왔거나 높은 gradient를 가지는 weight는 learning rate를 감소시키고, 그렇지 않은 weight들은 learning rate를 증가시키는 효과를 일으킨다. 그리고 divide-by-zero와 같은 오류를 피하기 위해 제곱근 안의 분모에 매우 작은 숫자를 더해준다.

재미있게도 제곱근을 취하지 않으면 성능이 떨어진다(어쩌면 당연하겠지만.. 그럼 제곱근 이외에 비슷한 역할을 하는 다른 함수는 어떨지 궁금하다).

Adagrad 알고리즘은 지나치게 learning rate를 낮추는 경우가 있어 모든 딥러닝 모델에서 좋은 성능을 보이진 않는다고 한다.
더 자세한 사항은 Duchi et al. 에서 확인 할 수 있다. (수식이 매우 많다..)

RMSProp

AdaGrad는 convex한 경우에는 빠르게 수렴하지만 non-convex한 경우는 모든 과거 gradient의 제곱을 고려하면서 learning rate를 줄이기 때문에 급격하게 learning rate가 줄어드는 단점이 있다고 한다. 이를 보완하고자 나온 것이 RMSProp이다.

RMSProp는 이 문제를 지수 이동 평균(exponential moving average)을 통해 해결하고자 했는데, 지수 이동 평균으로 과거의 급격한 gradient등을 어느정도 무시하기 때문에 AdaGrad보다 수렴 속도가 빠르다.

accm = p * accm + (1-p) * dx**2
x   += - learning_rate / sqrt(accm + 1e-8) * dx

이 때문에 또 다른 hyperparameter인 p ($\rho$)가 추가되었는데 일반적으로 p는 0.9, 0.99, 0.999를 많이 사용한다고 한다.

Adam

Adam은 RMSProp과 모멘텀을 합친 알고리즘이다. 대략적인 의사코드는 다음과 같다.

m  = beta_1*m + (1-beta_1)*dx
v  = beta_2*v + (1-beta_2)*(dx**)
x += - learning_rate * m / (sqrt(v) + 1e-7)

beta_1beta_2는 각각 모멘텀에서의 mu, RMSProp에서의 p이다. 첫번째 줄은 모멘텀에 해당하고 2~3번째 줄은 RMSProp처럼 동작한다.

m, v = #... init to zeros
for t in xrange(1, big_number):
    dx = # ... calc gradient
    m  = beta_1*m + (1-beta_1)*dx
    v  = beta_2*v + (1-beta_2)*(dx**2)

    mb = m / (1-beta_1**t) # bias correction
    vb = v / (1-beta_2**t) # bias correction
    x += - learning_rate * mb / sqrt(vb) + 1e-7)

실제로 Adam을 적용할때는 위와 같은 형태로 적용을 한다. 첫 몇번의 반복을 진행할 때 변화량을 높이기 위해 beta_1beta_2에 제곱을 취한 값을 m, v에 각각 나눠서 더 높은 값으로 보상한다.

Comparison

위 이미지는 지금까지 살펴본 몇가지 최적화 알고리즘이 convex한 경우에 얼마나 잘 수렴하는지를 보여준다. 참고로 NAG는 Nesterov Accerlate Gradient로 Nesterov 모멘텀과 같은 말이다.

기본 SGD 알고리즘이 가장 느리게 converge하는 것을 볼 수 있다. 그리고 모멘텀의 궤적은 관성에 의해 최적점을 매우 빠르게 지나쳤다가 다시 되돌아오는 것을 볼 수 있고, NAG는 기본 모멘텀보다 조금 더 정확한 위치에서 멈춘 것을 알 수 있다.
AdaGrad와 RMSProp을 비교해보면 AdaGrad가 성능이 더 좋은 것을 볼 수 있다. 위 예제처럼 convex하거나 간단한 경우는 AdaGrad가 더 빠르지만 non-convex 한 경우나 깊은 네트워크에서는 learning rate를 지나치게 낮추는 경향이 있어 RMSProp가 성능이 더 좋은 편이다.

Decay Learning Rate

위 이미지는 learning rate에 따라 얼마나 잘 converge 할 수 있는지를 보여준다. 그렇다면 어떤 learning rate를 사용하는게 가장 효과적일까?
당연하게도 “good learning rate”를 선택하는게 좋은, 어떻게 보면 약간 바보 같은 질문일 수 있겠다. 하지만 가장 좋은 learning rate는 초기에는 높은 learning rate로 학습 시키다 점점 learning rate를 감소시키는 것이 가장 효과적일 것이다.

때문에 SGD 베이스 알고리즘을 사용하는 깊은 네트워크를 학습시킬 때 learning rate를 decay 하는 것이 도움이 될 수 있다. 이 때 learning rate를 얼마나 decay 하냐에 따라 성능이 좌우된다. 만약 decay를 천천히 한다면 파라미터가 요동치면서 수렴하기 때문에 매우 느리게 수렴하며, decay를 빠르게 하면 시스템이 너무 일찍 식기 때문에 최적점에 도달하기 전에 멈춰버릴 수도 있다. 다음은 learning rate의 decay를 구현하는 일반적인 방법 세가지를 소개한다.

Step decay는 몇 번의 epoch마다 일정하게 learning rate를 줄이는 방법이다. 보통 5번의 epoch마다 절반으로 줄이거나, 매 20번의 epoch마다 0.1배로 줄인다. 얼마나, 언제 줄일지는 풀어야 할 문제와 모델에 따라 다른데, 실제로 이 수치를 휴리스틱하게 정하는 방법은 트레이닝 시 validation 에러를 관찰하는 것이다. 1/t decay는 $a = \frac{a0}{1 + kt}$ 에 의해 learning rate를 결정한다. Exponential decay : $a = a0 e^{-kt}$에 의해 learning rate를 결정한다. 이 때 1/t decay와 exponential decay에서 $a_0$, $k$는 hyperparameter이며, $t$는 반복 횟수이다.

Second-Order Method

GD를 구현할 때 learning rate를 어떻게 정해야 할까? 만약 learning rate가 너무 작다면 convex 최적화일지라도 너무 느리게 수렴하기 때문에 엉뚱한 곳에서 멈추는 것 처럼 보일 것이다. 반면에 너무 크다면 진동하면서 수렴하기 때문에 매우 느리게 수렴한다.
이를 해결할 수 있는 방법은 헤시안을 사용하는 것이다. 헤시안을 사용한다는 뜻은 2차 미분값을 사용한다는 의미이며, 만약 2차 미분을 사용한다면 learning rate를 정할 필요가 없다.

위 식은 뉴턴랩슨법을 이용한 최적화 방법이다. $Hf(x)$는 헤시안 행렬로, 함수의 2차 편미분 값을 정방행렬의 형태로 나타낸 것이다.

2차 미분을 사용한 최적화 방법은 learning rate를 정하지 않는다는 매우 큰 이점이 있지만 잘 사용하지는 않는다. 이는 헤시안 행렬을 구하는데 너무 큰 비용이 들기 때문이다. 만약 네트워크의 파라미터가 100만개라면 헤시안 행렬의 크기는 100만x100만인데 이 거대한 크기의 행렬을 저장하는 것도 문제고 역행렬을 구하는 것도 큰 문제이다. 그래서 헤시안 행렬을 근사하여 사용하는데, 그 중 제일 유명한 방법이 BFGS와 L-BFGS이다.

하지만 실제로 2차 미분을 사용해서 사용하기보단 SGD 베이스의 알고리즘을 많이 사용한다.

Conclusion

  • 보통 Adam을 사용하는게 가장 나은 방법일 것이다.
  • 만약 데이터의 갯수가 작아 full-batch 방법을 사용할 수 있다면 L-BFGS를 고려하는 것도 나쁘진 않다.

Tips to train Neural Network


사실 CS231n 강의를 들으면서 이 내용은 건너뛰고 다른 중요한 파트들을 먼저 들었었는데 막상 과제에 나온 뉴럴넷을 구현하고, hyperparameter들을 cross-validation 하다보니 잘 안되는 경험을 하곤 했다. 그래서 이 부분을 들어보았는데 만약 뉴럴넷을 직접 구현하거나, 혹은 외부 라이브러리르 사용하더라도 최적의 hyperparameter를 고르는 작업을 한다면 도움이 많이 될 것 같아 정리 하고자 한다.

loss 값 확인하기

만약 뉴럴넷을 맨땅부터 구현하기 위해 loss와 gradient를 구현해야 할 경우 먼저 loss 값이 제대로 계산되는지 확인하는게 좋다. 먼저 확인하기 위한 용도로 2개 정도의 레이어를 가지는 네트워크를 구성하고 loss 값을 출력해본다.

def init_model(input_size, hidden_size, output_size):
    # 모델 초기화
    model = {}
    model["W1"] = 0.0001 * np.random.rand(input_size, hidden_size)
    model["b1"] = np.zeros(hidden_size)
    model["W2"] = 0.0001 * np.random.rand(hidden_size, output_size)
    model["b2"] = np.zeros(hidden_size)
    
    return model
model = init_model(32*32*3, 50, 10) # input_size, hidden_size, # of classes
loss, grad = two_layer_net(X_train, model, y_train, 0.0) # train, model, label, reg
print(loss) # 2.30261216167

loss가 제대로 계산되었는지 체크할 때 먼저 reg값을 0.0으로 준 뒤 값을 출력한다. 만약 weight들을 모두 guassian으로 랜덤하게 초기화했다면 예상가능한 loss값은 $ln(N)$ 이다 (N은 class의 개수). 왜냐하면 softmax를 사용하는 경우 각 class가 가질 수 있는 확률의 기대값은 $N^{-1}$이고 (랜덤하게 weight를 초기화 했기 때문이다), softmax는 loss를 negative log probability하게 구하기 때문에 $-ln(N^{-1})$이다. 위의 예시는 CIFAR-10 데이터셋을 사용했기 때문에 $log(0.1)$ 값인 2.3에 근접하게 나오는 것을 확인 할 수 있다.

reg값을 0.0을 주고 확인 한 뒤, reg 값을 조금 준 뒤 loss 값을 다시 관찰해본다. 만약 loss 값이 약간 증가했다면 괜찮은 것이다.

Overfit 되는지 확인해보기

전체 데이터셋에 학습하기 전에 20개 미만의 작은 데이터를 돌려보자. 작은 데이터를 네트워크에 학습시켰을 때 overfit 하지 않는다면 (train accuracy가 1.0이 아니라면) 잘못 된 것이다. learning rate나 re값을 요리조리 바꿔가면서 작은 데이터셋에 overfit 하는지 확인해보는데, 네트워크에 capacity가 충분치 않다고 생각하면 더 작은 셋에 돌려도 괜찮다.

참고로 Karpathy는 hyperparameter를 조정할 때 먼저 reg값은 작게 설정해 놓은 뒤(0.000001 정도) 적절한 learning rate를 찾는다고 한다.

learning rate를 조절할 때 너무 작으면 위 그림처럼 loss값이 줄지 않거나 정말 작게 줄어든다. 한가지 유의할 점은 loss값은 거의 똑같은데 train/val accuracy는 증가하는 것인데 이건 처음엔 모든 데이터셋에 조금씩 loss가 있는 상태에서 iteration이 증가하면서 어떤 데이터는 잘 맞지만, 다른 데이터는 loss가 증가하기 때문에 발생한다.

또한 learning rate를 너무 크게 잡으면 몇번의 iteration 이후 inf가 되거나 NaN이 되는 현상이 생기기 때문에 유의해야 한다. 그래서 learning rate를 cross validation 할 때는 대충 [1e-3, 1e-5] 정도의 범위에서 조절하는 편이다.

Hyperparameter optimization

hyperparameter를 최적화 시킬 때는 먼저 대충 어느정도 범위를 좁히고, 범위가 좁혀진 뒤 정밀하게 조절하는 단계를 거치는게 좋다고 한다. 처음에는 epoch를 몇 번만 돌려 (5번 정도) 대략적인 방향을 잡고, 어느정도 윤곽이 잡히면 학습을 더 오래시키는 식으로 구현한다.

lr이나 reg는 위 코드처럼 log-space 상에서 정해질 수 있도록 조절하는 것이 가장 좋다. 위 경우 reg값이 0보다 크면 성능이 좋지 않기 때문에 아래와 같이 약간 범위를 좁혀준다.

lr값도 (-3, -4) 범위로 줄여서 다시 돌리니 약 53%의 accuracy가 나온다. 하지만 조심해야 할 것은 14번째 결과를 맹신하지 말라는 것이다. 왜냐하면 14번째 결과의 lr값이 9.4e-04인데 이는 우리가 설정 했던 범위의 거의 끝에 걸쳐있다. 그렇기 때문에 만약 다시 한번 범위를 좁혀서 validation을 할 경우 0, 7, 8번 정도에 해당하는 lr, reg값을 선택하는 것이 좋다.

여기서 한가지 의문을 가질 수 있는데, validation을 할 때 왜 lr, reg값을 랜덤하게 고르냐는 것이다. 대부분의 경우 lr, reg를 고를 때 grid search 방식을 사용한다. 그러니까 for문 2개로 구현하는 식인데 이 방식은 정말 좋지 않다. 반대로 랜덤하게 hyperparameter을 선택하는 방법은 random search라고 부른다.

grid search는 모든 범위를 같은 간격으로 쭉 훑을 수 있는 장점이 있지만 뉴럴넷에는 부적합하다. 뉴럴넷의 hyperparameter는 learning rate와 같이 성능에 매우 중요한 것도 있고, 비교적 중요하지 않는 것도 있는데 이를 grid search를 통해 정하게 되면 왼쪽 그림과 같이 optimal에 도달하지 못할 가능성이 크다.

loss curve 관찰하기

loss curve를 plot하는 코드를 삽입해서 관찰하는 것도 효과적이다. 왼쪽 그래프는 조금 느리게 수렴하는 경향이 있는데 (linear 비슷하게 수렴한다) 아마 learning rate를 조금 더 높이는게 좋을 것이다.

왼쪽 그래프와 같이 초반에 plateau한 경우는 대부분 weight의 초기화가 나쁘기 때문에 발생할 수 있다고 한다.
오른쪽 그래프처럼 train/val accuracy를 그려보는 것도 도움이 된다. 만약 두 그래프 사이의 gap이 너무 크다면 overfit을 의심해야 할 것이고, 이 경우 reg값을 늘리거나 다른 regularization 방법을 도입하는것이 한가지 방법이 될 수 있다. 반면 gap이 너무 작거나 없는 경우는 모델의 capacity를 늘리는 것이 좋다.

weight update/magnitude의 비율 추적하기

param_scale = np.linalg.norm(W.ravel())
update = -lr*dW
update_scale = np.linalg.norm(update.ravel())
W += update
print(update_scale / param_scale) # ~ 1e-3 정도!

위 코드와 같이 weight의 update 값과 weight 값의 비율을 확인해보자. 대충 0.001정도가 적당하고 만약 너무 높다면 lr을 낮추고 반대로 낮다면 lr을 높여보자.

레퍼런스

CS231n Convolutional Neural Networks for Visual Recognition
Fast and Accurate Deep Network Learning by Exponential Linear Units(ELUs)
Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift
Deep Learning
Caffe Tutorial