Backpropagation을 이해해야 하는 이유

우리가 deep learning를 수행하고자 할 때 이용하는 frameworks에는 deep learning에 필요한 필수적인 함수나 class들이 포함되어 있어서, deep learning의 알고리즘에 집중하게 도와주고, 수학적 기법과 같은 디테일에 대해 코딩해야 하는 수고로움을 덜어 준다. 이처럼 deep learning의 최종 목적지를 위한 tool인 이optimizer 자체의 알고리즘이나 optimizer를 구동하기 위한 gradient 값을 구하는 수고를 덜어 준다는 것은 deep learning 개발자들에게는 매우 다행스러운(?)일이라고 할 수 있다. 이러한 맥락에서 미분값을 구하기 위한 backpropagation기법에 대해 별도로 공부하지 않고 단지 그것이 미분을 구하기 위한 일이라는 사실만 알고 있어도 deep learning 알고리즘을 구현하는데 큰 방해가 되지 않는다. 그러나 그 수고로움을 기꺼히 감내해야 한다고 말하는 좋은 글이 있어 이를 간략히 소개하고자 한다. 이해를 위해 개인적 설명도 곁들였다.

Yes you should understand backprop

우리가 매우 중요한 프로젝트를 수행할때 핵심이 되는 알고리즘이 이미 개발되어 쉽게 사용할 수 있다면, 그 알고리즘을 따로 배울 필요가 없는 경우가 대부분이다. 이를 위해 집단 지성이 필요한 것이기 때문이다. 그러나, 그 핵심알고리즘이 우리가 수행하려는 프로젝트에 문제를 일으킬 수 있다는 사실을 모른체 프로젝트를 수행한 후 나중에 어떤 문제가 발생했을 때, 그 문제를 해결하기 위해 아무리 많은 시간을 쓰더라도 해결할 수 없다. 왜냐하면 문제의 발생 요인을 인지하지 못한 상태이기 때문이다. 이와 같은 은 상황을 묘사하는 단어가 leaky abstraction이라고 한다.

“The problem with Backpropagation is that it is a leaky abstraction


사실 어느정도 깊이있는 연구를 수행할때 자주 겪게되는 문제가 이 문제이다. 나 또한 이러한 경험을 많이 해보았기 때문에 fundamental에 대해 보다 관심을 가지라고 후배들에게 강조하는 이유이다. 결국 훌륭한 논문은 이러한 fundamental적인 문제의 허점을 발견하고 그동안 봉착했던 많은 문제를 깔끔히 해결하는 해결책을 제시한 연구이다. 해결책은 아니더라도 허점을 발견한 연구 그 자체로도 무척 훌륭한 연구이다. 


이제, backpropagation을 미분값을 구해주는 블랙박스로 생각할 때 발생할 수 있는 문제점을 알아 보자.

Vanishing gradients on sigmoids

이 문제는 이미 많이 언급되어 있어서, deep learning을 조금이라도 아는 사람이라면 알고 있는 문제이다. 여러 layer가 연결된 deep network의 neuron에 해당하는 unit은 sigmoid (or tanh)와 같은 non-linearity를 가지는 함수를 사용하는 경우가 많다(특히 fully connected layer). 이 함수의 특징중의 하나는 양 극단으로 갈수로 gradient값이 제로에 수렴한다는 점이다. 아래는 sigmoid function의 forward pass와 backward pass를 coding한 것이다.

z = 1/(1 + np.exp(-np.dot(W, x))) # forward pass
dx = np.dot(W.T, z*(1-z)) # backward pass: local gradient for x
dW = np.outer(z*(1-z), x) # backward pass: local gradient for W

만약 weight matrix W가 너무 큰값을 가지는 경우에, vector z 값은 binary 변수처럼(0이거나 1이거나) 극단적 값만을 가지게 될것이다(이것을 “saturated”라고 표현한다. 물리화학이나 control등의 분야에서 자주 사용하는 단어로 더 이상의 변화를 허용하지 않는 단계를 의미한다). 그러나, 이경우 local gradient 값 z*(1-z)는 zero (“vanish”)에 가까워 질것이고, chain rule에의해 이러한 특성이 있는 unit(neuron)이 포함된 backward pass값을 사용하면 패러미터의 update에 의한 학습이 불가능해 진다.

또 다른 문제로는 이 local gradient (z*(1-z)) 값이 z=0.5일 때 최대값인 0.25 값을 가짐으로써 생기는 문제이다. 이것은 gradient signal이 sigmoid gate를 거칠때 마다 적어도 1/4로 감소한다는 것을 의미하며, network train의 선행 layer는 후행 layer보다 훨씬 낮은 속도로 학습하게 될것이다.

Dying ReLUs

Sigmoid function과 함께 자주 사용하는 non-linearity를 가진 함수가 ReLU이다. 이 함수는 zero를 기점으로 activation 값의 큰변화를 가지며, forward와 backward pass는 아래와 같다.

z = np.maximum(0, np.dot(W, x)) # forward pass
dW = np.outer(z > 0, x) # backward pass: local gradient for W

문제는, zero보다 낮은 값에서 gradient값 zero라는 점에서 발생한다. 이것을 “dead ReLU” 문제라고 부르며, 초기화나 학습도중에 이런 상황에 다다르면, 소위 그 neuron은 사망했다고 할 수 밖에 없다. 즉 이것은 마치 뇌의 neuron이 완전한 damage를 입은 상태와 마찬가지이며, 사용된 neuron의 상당부분이 이런 상태에 빠지게 되는 경우가 많다.

Exploding gradients in RNNs

Vanilla RNN에서 backpropagation시 발생할 수 있는 문제를 설명하기 위해 아래 간단한 code를 살펴보기로 하자.

rnn-exploding-gradient

T time step을 펼쳐놓은(for loop) RNN구조를 생각하면, gradient signal은 모든 hidden state를 거치면서 시간/시퀀스상 앞쪽으로 전달되게 되는데, parameter matrix인 Whh를 계속해서 곱해가는 것을 알수 있다. 이점을 잘 생각해 보면,  W matrix의 element가 1보다 큰 경우 gradient는 계속해서 커지게 될것이고(gradient exploding), <1 인 경우는 계속해서 작아지게(gradient vanishing) 될것이다. 그러므로, Whh matrix의 eigenvalue값이 중요해 지게 된다. 발산을 유도하는지 수렴을 유도하든지, 너무 많은 time step을 가지는 RNN구조는 이 두 문제를 내재하고 있으며, exploding 문제는 gradient 값의 clipping을 통해 해결하고, vanishing gradient의 문제는 새로운 RNN인 LSTM을 사용함으로써 문제를 해결한다.

Spotted in the Wild: DQN Clipping

이 섹션은 Andrej Karpathy가 최근 backpropagation과 관련하여 발견한 오류를 예를 들고자 마련한것이다. 먼저 그의 글을 설명하기 전에, Deep Q Learning은 이 개인적 블로그의 주요 테마인 강화학습(Reinforcement Learning)의 주요한 기법중 하나이나, 이 섹션을 이해하기위해 Deep Q Learning 기법에 대해 먼저 알아야 할 필요가 없다는 사실을 말하고자 한다.

그는 Deep Q Learning의 TensorFlow coding version을 살펴보던 중, backpropagation 관점에서 잘못된 code snippet의 발견하였다. Action value Q값의 target과 estimation과의 차이값(일종의 loss function임)을 최소값과 최대값 사이의 값만을 clipping하는 code line이 존재하는데, 그 의도는 그 바깥 영역에 대해서는 알고리즘상에 최소값 또는 최대값으로 대치하여 사용함으로써 알고리즘을 보다 안정성을 보이도록 만들기 위함이다. 이 방법은 forward pass관점에서는 의미가 있다고 할 수 있으나, 미분값을 구해야 하는 점을 생각해 보면 logical bug가 아닐 수 없다. 미분값은 절대값의 크기가 일정한 영역이 존재하면, 그 구간에서는 zero이기 때문에 절대값보다는 미분값이 중요한 ML의 학습과정에서 이 bug는 치명적인 오류가 아닐 수 없다.

사실 우리가 clipping해야 하는것은 action value값 자체가 아니라 그와 관련된 gradient값이다. 이경우에 사용해야 하는 것든 tf.square 대신에 아래와 같은 Huber loss를 사용해야 한다.

def clipped_error(x): 
  return tf.select(tf.abs(x) < 1.0, 
                   0.5 * tf.square(x), 
                   tf.abs(x) - 0.5) # condition, true, false

사실 TensorFlow에서 직접적으로 gradient값을 다루기 어려우므로 이러한 우회책을 사용해야 하지만, Torch에서는 이 문제를 쉽게 해결할 수 있다.


Karpathy는 TensorFlow에서 gradient를 직접 핸들링하기 어렵다는 표현을 사용하였는데, TensorFlow가 엄청나게 빠른 속도로 upgrade되다 보니 이 표현이 맞는 말이라고 할 수 없다는 생각이 든다. Tensorflow에는 gradient를 clipping하는 API가 추가되었으며, gradient를 별도로 먼저 구하고 이것을 optimizer에 입력하는 방법도 있으므로, 구해진 gradient를 의도해 맞게 clipping한후 optimizer를 구동하면 이 문제를 해결할 수 있다.


이상으로 살펴본 바와 같이 Backpropagation은 leaky abstraction 성격을 지니고 있다. 이 점은 본질적 학습을 위해서는 반드시 알고 넘어가야 한다는 것을 의미한다. 이것에 관한 좋은 자료를 아래에 링크 하였으니 별도로 참조할 필요가 있다고 생각한다.

CS231n backpropagation

How the backpropagation algorithm works

CS231n lecture on backprop

Advertisements

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google+ photo

Google+의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중