응~ 역전파 이해해야되~ - (Yes you should understand backprop / Andrej Karpathy)
요즘 개인적인 일로 너무 바빴다.
깐에 좋은 옷좀 입어보겠다고, 리바이스 LVC 청바지를 구매했는데, 배가 꽤나 나왔는가, 허리에 옷을 맞추니 통이 맞지가 않아서, 교환하는데도 한참 애먹고, 개인적인 약속 및 회식들로 갑작스럽게 저녁에 술먹을 일이 많아서, 글을 쓰지 못했었다.
읽어보기로는 저번주에 다 읽어놓은 내용인데, 오늘 정리하려니, 제대로 말이 써질까 모르겠다.
(감정이 살아있을때 써야 블로그도 더 잘 써지는 것인데 말이지...)
오늘은, 이전에 한번 얘기했던,
안드레 카페시의 글 중 나오는, Yes you should understand backprop에 대해서 다뤄보고자 한다.
이야기를 해보기에 앞서, 나도 이런 얘기를 많이 들었다.
1. 딥러닝을 하려면, 머신러닝과 통계를 꼭 다 이해하고만 있어야 하는가?
2. 딥러닝을 하려면, 딥러닝의 알고리즘을 전부 공식화시켜 해결할 수 있는 능력이 있어야만 하는가?
글에 들어가보기 전에, 나는 이렇게 대답을 해오곤 했다.
1. 통계와 머신러닝을 아는 것은, 분명히 중요하다. 딥러닝의 근간은 머신러닝과 통계에 있다. 데이터만 완벽하다면, 어떤 상황에서는 머신러닝이나 혹은 통계가 더 효율적일 수 있다. (혹은 머신러닝을 먼저 해봄으로써, 더 쉽게 딥러닝으로 문제를 해결할 수 있는 방식을 얻어낼지도 모른다.)
다만, 통계와 머신러닝을 이해하기 전에, 본인이 어떤 문제를 해결할 것인지에 대해 분명히 파악하는 것이 더 중요하다.
통계와 머신러닝은, 여러가지 문제를 해결하는 여러가지 방법들을 제안한다. 그 중에 나에게 당도한 문제가 무엇인지 파악하고, 그 중에서 지식을 얻어내기위해 골라내는 작업들로부터 지식을 습득해나가기 시작하라.
2. 자신이 처한 상황에 따라 다르다. 다중 GPU 환경 등, 분산처리가 가능하다면, 설계/구현 단계에서는 충분히 경험적으로 Insight를 도출해 나가는 것도 방법일 수 있다. 다만 이런 번거로운 작업이 하기 싫고, 수려하게 모델을 이해하고 고도화하기 위해서, 혹은 환경이 용납되지 않는다면, 공식화 시켜야할 때도 분명 당도할 수 있다. 본인은 그 당도한 시점에 문제를 포기하지 않을 자신이 있는가?
실제로 현업에 있는, 아니면 현업에 있고싶은, 혹은 공부를 하고 있는 여러분들의 생각은 어떠한가?
자, 이제 본문으로 들어가보도록 하자.
번역 간 내 생각은 이전에서와 동일하게,
(참고로 내가 느끼는 점이나 내가 말하고 싶은건 '-- --' block으로 표현하겠다. 나머지는 그대로 블로그 내용을 번역, 의역한 내용이다.)
규칙을 따르겠다.
또한 참고로, 이미지까지 다 퍼오는 것은 도리가 아닌 듯하여, 이미지와 예제 소스는 원문 참조로 남기도록 하겠다.
꼭!!!! 원문과 비교해가며, 필자의 해석은 이해를 돕고, 해석의 번거로움을 조금 줄여주는 정도로 이해하고 활용하길 바란다. 당연히, 원문이 있는데 영어 비전공자의 번역본만 낼름 보겠다는 것 부터가 도둑놈 심보 아닌가?
karpathy.medium.com/yes-you-should-understand-backprop-e2f06eab496b
스탠포드에서 학생들을 가르칠때, 카페시가 역전파를 순수계산식을 소스로 짜는 것에 대해, 학생들이 컴플레인을 걸었었다.
"사실상 지가 알아서 잘되는거구만, 우리가 왜 해야됨?? ㅋㅋㄹㅃㅃ"
역전파에 대해서 감성적인 어필을 하는 친구들 말고도, 실질적인 활용적 측면에서의 논쟁이 학생들과도 좀 있었다.
> 역전파의 문제는 구멍난 추상화라는 점이다.
그냥 레이어 몇개 쌓고 잘될거라 생각하면 오산이다. 여기 예제를 보자.
(예제 이미지는 본문 참조)
Vanishing gradients on sigmoid (시그모이드의 기울기 소실)
엉성한 W초기화와, 제대로 되지 않은 데이터 전처리는, 비선형성을 포화시켜, 학습이 안될 수 있다.
z = 1/(1 + np.exp(-np.dot(W,x))) # 순전파 방향
dx = np.dot(W.T, z*(1-z)) # 역전파 방향 : x를 위한 지역 기울기
dW = np.outer(z*(1-z), x) # 역전파 방향 : W를 위한 지역 기울기
한 노드에서 W 초기값이 커지면, 행렬곱의 출력범위가 커지고, 그로인해 z(순방향)가 0이나 1로 수렴하며, 그렇게되면 체인룰에 의해 역전파가 전부 다 0이 되어버려서 학습이 진행되지 않는다.
-- z식에 해당하는 W에 매우 큰값이 들어간다고 가정하면, 분모가 무한대로 커지기에, 역전파 시 이전 노드로의 영향이 제대로 전파되지 못한다. --
(예제 이미지는 본문 참조)
z가 0.5일때, 기울기가 0.25로 최대이며, 시그모이드를 통과할때마다, 1/4 (혹은 그 이상) 감소하게 된다. 만약 기본 SGD를 사용하는 경우에, 낮은 레이어의 아키텍쳐가 높은 레이어의 아키텍쳐보다 더 느리게 학습되는 모습을 나타내게 된다.
3줄요약(TLDR) : 비선형성 네트워크의 tanh, sigmoid활용과 역전파를 이해하는 사람이라면, 초기화가 야기하는 포화성에 대해 이해해야 한다.
Dying ReLUs (죽은 렐루)
임계값 0의 아래와 같은 소스를 구성해보자.
z = np.maximum(0, np.dot(W, x)) # 순전파 방향
dW = np.outer(z > 0 ,x) # 역전파방향 : W를 위한 지역 기울기
위 문제에서는 뉴런이 0으로 고정되며 (다시말해, z=0으로 활성화 되지 않는다.), 가중치가 0 기울기를 가지게된다.
-- 이 말은, 학습이 진행 안된다는 이야기. --
가끔은 학습의 전체기간동안 학습된 네트워크의 40%가 0인 경우로 있을 수 있다.
3줄요약(TLDR) : 뉴런은 트레이닝중에도 죽을 수 있으며, 그 증상이 공격적인 학습율로 나타난다. 역전파를 이해하고 ReLU를 쓴다면, Dying ReLU문제를 항상 명심해야 한다.
Exploding gradients in RNNs (RNN에서의 기울기 폭증)
입력값 x를 주지않고, hidden state의 반복적인 계산만을 이용해서, 역전파의 어려운 부분을 설명할 수 있다.
(예제 소스 본문 참조)
-- 예제 소스의 요는 x를 주어주지 않고, 단순하게 hidden_state만을 고려하여, RNN처럼 layer를 구성하여 미적분을 구해보면, 고유값이 1보다 클때는 기울기가 폭발하고, 1보다 작았을때는 소실된다. 사실 생각해보면 당연할게, 이전값을 이용해서 계속 내적해나가 구하기때문에, 0에 가까운 소수점일수록 반복해서 곱하면 0에 가까워지고, 1보다 큰 어떤 값이라면 계속 곱하면 무한대로 커지니까, 생각해보면 당연한 얘기다. --
기울기 신호는 Whh에 재귀적으로 계속 곱해지고, 비선형적 역전파가 진행된다.
재귀로 계속 곱하면 0에 수렴하거나, 발산할 수 있으니, RNN에서는 고유값을 잘 생각해야한다.
-- 여기서의 고유값은 RNN에서의 가중치 W를 의미하는 것으로 생각한다. (hidden_state에 재귀적으로 곱해지는 값일테니까.) --
3줄요약(TLDR) : RNN에서는 기울기 클리핑을 조심하여 사용하거나, lstm을 사용하는 것을 권장한다.
-- 재귀적으로 곱해진다는 점에서, W가 한번 잘못 곱해지는 것으로, 큰 영향을 초래할 수 있기 때문에, 기울기 클리핑을 조심하라는 의미로 판단되며, 그런의미에서 lstm은 cell_state로 해당 units의 여파를 살릴지 말지 학습간 조절될 수 있기 때문에, 권장되는 것이라고 생각된다. --
Spotted in the Wild: DQN Clipping (야생에서의 발견 : 강화학습 클리핑)
이 Post에 영감을 준 내용으로, TF 메소드 사용법을 찾다가 알게된 이런 소스가 있다.
(예제 소스 본문 참조)
소스 밑에 If you`re ~ So far so good. 까지는 소스에 대한 설명부분으로 해석은 생략한다.
291번 라인은 아웃라이어 제거를 위해 순방향만 고려하면 효과적이지만, 역전파를 생각하면 버그가 있다.
clip_by_value 펑션은 min_delta와 max_delta 사이의 zero outside(0 외부?) 지역 기울기를 가진다. delta가 min/max_delta를 넘어버리게되면, 역전파간의 기울기는 0으로 고정된다. 저자는 이상치내성을 추가한 기울기를 따내기 위해서, 실제 Q delta를 클리핑하는데, 옳바른 방법은 tf.square를 사용하는 것이 아닌, Huber loss를 사용하는 것이다.
-- zero outside를 사실 잘 이해하지 못해, 번역이 살짝~ 이상하다. --
-- 중요한건 clip_by_value로 이상치를 제거하고 기울기를 클리핑하고 싶어했으나, 모종의 이유에서 square를 loss로 사용하는 것이 부적합하다는 이야기이다. (루트를 사용함으로 인해, 적분 시 값이 커져서 기울기가 0으로 수렴하는 경우가 있을까?) --
(Huber loss로 변경된 소스 본문 참조)
TensorFlow는 기울기를 직접 간섭할 수 없어 Huber loss를 사용하기에는 토치가 좀 더 편하다.
나는 이 문제를 issue로 올렸고, DQN repo에서 수정되었다.
In Conclusion (결론)
역전파는 구멍난 추상화이다. 역전파를 이해하지 못하고 텐서플로우가 알아서 해결해주겠지 식으로 대하게 된다면, 디버깅이 비효율적이며, 그로인한 효과에 대해 씨름할 준비가 되어있지 않은 것이다.
좋은 소식은 역전파가 이해하기 어렵지는 않다는 점이다. 대부분의 역전파가 수학적 날림의 복잡해보이기만하는 자료들이 많을뿐, 자기 강의자료를 보면 이해가 바싹 될 것이다.
이제 마무리이다! 순전파와 역전파를 잘 고려하고 이해하길 바란다. 또한, 쓰다보니까 자기 강의에 대한 광고가 되버린 것 같다. 그점은 유감이네 :)
-- 여기서 번역은 끝난다. --
내가 느낀점
역전파는 분명히 쉬우면서도 어려운 부분이다. 간단한 문제에 대입해보기엔 충분히 가능하나, 좀 더 복잡한 아키텍쳐의 모델을 다루다보면, 분명히 난관에 봉착되기 마련이다.
나는 주로 그럴땐 이런 방식을 사용한다. 나는 Keras를 주로 사용하는데, Model과 Layer자체를 전부 상속받아서 override 하여 작성하는 편이다. (절대로 주는대로 덥썩 받아쓰지 않는다. 초창기에 테스트해볼때 빼고는.)
그 과정속에서 tf.print나, print를 최대한 많이 찍어서, 내가 생각하는데로 w가 얼마나 많은 units에 영향을 미치고 있는지, 혹은 그 값이 어디 한곳에 집중되진 않는지, 이상이 있다면 어느 레이어의 어떤 부분에서 영향이 있는지를 파악하려고 애쓴다.
(이것보다 더 효과적인 디버깅 방법도 있을테고, 꼭 이게 맞는 방법이라고는 못하겠으나, 나는 최대한 Layer와 Model의 동작을 전부다 수학공식으로 이해하진 못하더라도, 하나하나 어떻게 전파되어 내려가고 다시 올라오는지를 확인하려 애쓴다.)
과거에는 딥러닝을 할때 Feature X가 무조건 중요하다는 생각을 했었다.
근데 요즘 직접 모델이 학습하는 방식을 톺아보다보니, 이런 생각도 든다. Input X보다, Output Y가 더 중요하다는 것 같은 생각.
그 이유는, 정작 어떤 값으로 학습을 시키던지 중요하지 않다, 모델은 최종 loss가 결정되고, 그 과정이 역전파되면서 학습이 진행되게 되는데, loss를 결정하는 것은 X가 결정하는가?
그렇지않다. Y가 결정하지. X는 단순히 loss 계산을 위한 Y가 만들어지는 과정에 불가하다.
결국 우리는 잘 생각해봐야할 필요가 있다. 학습이 진행이 안되는 부분에 대해서.
단순히 X만 탓할 것인가?
아니면 나의 loss를 탓할 것인가?
(loss를 탓하는데는, 단순 loss 계산식, 그로인한 기울기, 그로인한 W 3개를 다 고려해봐야한다.)
무엇'으로' 가르치는지보다, 무엇'을' '어떻게 채점'해가며 가르치는지가 더 중요할지도 모른다.