2019년 5월 12일 일요일

신경망 훈련을 위한 처방

Andrej Karpathy가 그의 블로그에 올린 신경망 학습에 관한 유용한 내용이 있어 해당 글을 편역한다. 딥 신경망을 학습할 때 인자 선정과 조정 기준으로 사용해 볼 만한 내용이다. 


Karpathy가 올린 "가장 흔한 신경망 실수"라는 twitter이 있는데, 신경망 훈련과 관련된 몇 가지 흔한 실수들을 열거하였다. twitter은 예상보다 훨씬 많이 조회 되었고, 많은 사람들이 단순히 "어떻게 conv넷이 작동하는가"와 "자신의 convnet이 실제 최고 결과를 성취하는가" 사이에 큰 차이가 있음을 알게 되었다.

아래 글에서는 흔히 나타나는 다양한 오류를 열거하는 것에서 벗어나, 기존 오류에 대해 더 깊이 파고들어 어떻게 하면 오류를 방지하고 수정할지에 대해 살펴 본다. 잘 계획된 일련의 절차을 따른다면 오류를 방지할 수 있지만, 불행히도 이런 방법을 보여주는 문서를 찾기가 쉽지 않다.
먼저 두 가지 중요한 관찰로부터 시작해보자.

1) 신경망 훈련은 누수된 추상화

많은 초보자들은 신경망 훈련이 아주 쉽다고 말한다. 수많은 라이브러리와 프레임웍은 사용자가 가진 데이터 문제를 한번에 해결하는 30줄도 안되는 기적같은 코드 조각을 자랑하고 있으며, 활용법도 마치 Plug&Play 같은 느낌을 준다. 흔히 볼 수 있는 예는 다음과 같다.

>>> your_data = # 여기에 사용자가 가진 멋진 데이터 셋 연결
>>> model = SuperCrossValidator(SuperDuper.fit, your_data, ResNet50, SGDOptimizer)
# 사용법이 너무 너무 쉬워요

데모 라이브러리와 예제들은 매우 간결하고 명확하다. 이는 산뜻한 API와 추상화를 제공하는 기존의 표준 소프트웨어와 유사하다. 유사한 예를 들자면 웹 개발자에게 익숙한 Request library 데모를 보자.

>>> r = requests.get('https://api.github.com/user', auth=('user', 'pass'))
>>> r.status_code
200

얼마나 간결하고 멋진가? 이를 작성한 유능한 개발자는 Query 문자열, URL, GET/POST 요청, HTTP 연결 등 내부 시스템을 이해하고 구현하는 부담을 대부분 떠 안아 주었고, 몇 줄의 코드 뒤에 있는 복잡성을 대부분 감추어 주었다. 이런류의 예제는 기존 사용자인 우리에게 매우 익숙한 것으로 딥러닝에서도 기대하는 것들이다.

그러나 불행히도 신경망은 우리 기대를 져버린다. ImageNet 분류기를 훈련하는 것에서 약간 벗어나자마자 더 이상 "기존" 기술이 아니다. 역전파기법을 살펴본 후 "backprop을 이해해야 한다"는 글을 블로그에 올리는 건 쉽지만, 상황은 훨씬 더 끔찍하다. backprop+SGD 최적화만으로는 테스트하는 넷이 마법처럼 작동하지 않는다. batch norm이 마법처럼 넷을 더 빨리 수렴하게 하지도 않고, RNN은 마법처럼 당신에게 "플러그인" 타입처럼 텍스트를 처리하지도 않는다. 만약 신경망이 어떻게 작동하는지 이해하지 않고 기술 사용을 고집한다면 사용자는 실패할 가능성이 높다.

2) 신경망 학습은 조용히 실퍠한다

코딩 시에 문법 오류나 구성이 틀리면, 종종 예외 발생을 만난다. 예외나 문법 오류는 단순한 것이고 디버깅의 시작에 불과하다.

문법은 제대로지만 코드 내에는 여전히 정돈되지 않은 것이 있고 구별이 어려운 것도 있다. 오류 발생 가능성은 항상 크고 단위 테스트도 까다롭다.
예를 들어 data augmentation 중 왼쪽에서 오른쪽으로 이미지를 반전시킬 때 레이블을 뒤집는 것을 잊었지만 넷은 여전히 잘 작동할 수 있다. 왜냐하면 넷은 내부적으로 뒤집힌 이미지를 감지하는 것을 배울 수 있기 때문이다.
또는 pre-trained checkpoint로부터 weight를 초기화했지만 original mean을 사용하지 않은 경우, 또는 정규화 강도, 학습율, 감쇠율, 모델 크기 등에 대한 설정을 잘못할 수도 있다. 잘못 구성된 신경망은 운이 좋을 때만 예외를 던질 것이다. 대부분의 경우 학습은 진행되지만 조용히 실패한다.

더 이상 강조하기 어려운 사실은 신경망 훈련에 "빠르게 돌파하는" 접근법은 효과가 없고 고통만 초래한다. 어떻게 접근하던 난관이야 만나겠지만, 기본적으로는 가능한 모든 것의 시각화에 집착하고, 방어적이고, 편집증 환자처럼 매달림으로써 어려움을 완화할 수 있다. 신경망 훈련 성공은 인내심과 세부사항에 대한 높은 주의력이다.


The recipe

1. Become one with the data

학습 첫 단계는 학습 코드를 건드리지 않는 것이다. 대신 데이터를 철저히 검사하는 것에서 시작한다. 이 단계는 매우 중요하다. 수천 개 데이터를 스캔하고, 분포를 이해하고 패턴을 찾는 데 많은 시간을 보내야 한다. 다행히, 사람 뇌는 이것을 꽤 잘한다. 자료에서 중복된 샘플을 발견할 수도 있고, 손상된 이미지/라벨을 찾을 수도 있다. 또한 자료 불균형과 바이어스를 찾기도 한다. 보통 데이터 분류를 위한 프로세스에 주의를 기울이는데, 이는 탐구할 아키텍처 종류를 암시한다. 예를 들어 - 매우 국부적인 기능이 필요한지, 아니면 글로벌 컨텍스트가 필요한지, 어느 정도 차이가 있고 어떤 형태를 취하고 있는지, 어떤 변형이 비논리적이며 미리 전 처리될 수 있는지, 공간적 위치가 중요한가, 아니면 평균적으로 풀링하고 싶은지, 세부사항이 얼마나 중요하고 얼마나 이미지들을 축소할 수 있는지, 라벨이 얼마나 노이지한지 등이다.

신경망은 데이터 셋에 대한 효과적 압축/복호화 버전이기 때문에 네트워크(오류) 예측을 보면 이것이 어디에서 올 수 있는지 이해할 수 있을 것이다. 만약 네트워크가 데이터에서 본 것과 일치하지 않는 예측을 한다면, 뭔가 잘못된거다.

일단 넷을 평가할 수 있는 정성적인 감각을 얻으면, 가능한 방법(예: 라벨의 종류, 주석 크기, 주석 수 등)으로 검색/필터/구분을 할 수 있는 간단한 코드를 작성하고 그 분포와 축을 따라 특이치를 시각화하는 것도 좋은 생각이다. 특이치는 특히 데이터 품질이나 사전 처리에서 항상 일부 버그를 발견하게 해준다.

2. Set up the end-to-end training/evaluation skeleton + get dumb baselines

이제 데이터를 이해했으니, 초특급 멀티 스케일 ASPP FPN ResNet에 접속하여 멋진 모델을 학습할 수 있는가? 아니다, 그게 고통의 길이다.
해야 할 다음 단계는 완전한 학습+평가 골격을 설정하고 실험을 통해 정확성에 대한 신뢰를 얻는 것이다. 이 단계는 망쳐도 되는 간단한 모델을 선택하는 것이 좋다. 예를 들어 선형 분류기나 아주 작은 ConvNet 같은 것이다. 이를 훈련시키고, 손실과 기타 지표(예: 정확성)들을 시각화하며, 예측을 모델링하고, 그 과정에서 다양한 가설 삭제(ablation) 실험을 수행한다.

이 단계에서 팁 및 트릭을 몇가지 살펴보자.

Fix random seed.
코드를 두 번 실행해도 동일한 결과를 얻을 수 있도록 항상 고정된 랜덤 seed를 사용. 변동 요소를 제거하여 작업을 쉽게하는데 도움을 줄 것이다.

Simplify.
불필요 기능은 반드시 비 활성화한다. 예를 들어, 이 단계에서 모든 데이터 증강 기능을 해제한다. 데이터 증강은 나중에 통합할 수 있는 정규화 전략이지만, 현재는 멍청한 버그를 도입하는 기회를 줄 뿐이다.

Add significant digits to your eval.
Test loss를 그릴 때 전체 Test 샘플을 하나하나 평가한다. 배치 단위로 Test loss를 표시하지 말고, 텐서보드에서 평활화를 이용한다. 정확성 추구가 중요하며, 많은 시간을 들일 준비가 되어야 한다.

loss@init.
손실이 올바른 값으로 시작되었는지 확인한다. 예를 들어 최종 계층을 올바르게 초기화할 경우 초기화 시 softmax라면 -log(1/n_classes)를 예측해야 한다. L2 회귀 분석, 후버 손실 등에 대해서도 동일한 기본값을 도출할 수 있다.

Init well.
최종 학습층 가중치를 올바르게 초기화 한다. 예를 들어 mean이 50인 어떤 값을 regression 시킬 경우 최종 bias를 50으로 초기화한다. 비율 1:10의 pos:neg의 불균형 데이터 셋의 경우, 넷 초기화 시 0.1 확률을 예측하도록 logits에 bias를 설정한다. 제대로 설정한다면 수렴 속도가 빨라지고 넷이 단지 bias를 배우는 것에 지나지 않는 "하키 스틱" 손실 곡선을 피할 수 있을 것이다.

Human baseline.
Loss 값은 직관적 분석이 어려우므로 해석가능하고 확인할 수 있는 지표를 사용하여(예: accuracy) 모니터링한다. 언제나 확인가능한 정확성을 평가하고 비교하라. 대안으로 test data에 prediction과 GT 주석을 붙인체 확인하라.

Input-independent baseline.
입력과 상관없는 독립적인 기준으로 학습해 본다(예: 모든 입력을 0으로 설정하는 것이 가장 쉽다). 이것은 실제 데이터일 때보다 더 나쁜 성능을 보일 것이다. 실제 그런지 확인하라. 즉, 모델이 입력에서 정보를 추출하는 것을 배우는가?

Overfit one batch.
최소의 학습 샘플(예: 2개 배치 정도의 작은 경우)만을 준비한 단일 배치를 과적합 시킨다. 과적합을 만들려면 모델 용량만 늘리면 된다(예: 레이어 또는 필터 추가). 학습 시 달성 가능한 최저 손실(예: 0)에 도달할 수 있는지 확인한다.

이 때 라벨과 예측값을 같이 시각화하여 최소 손실에 도달하면 완벽하게 정렬되는지 확인한다. 만약 그렇지 않다면, 어딘가에 버그가 있고 다음 단계로 진행하면 안된다.

Verify decreasing training loss.
현재 단계는 Toy 모델로 작업 중이기 때문에 데이터 세트에 under-fitting할 것이다. 이 때 조금만 모델 용량을 늘려본다. 손실이 예상처럼 주는지 확인한다.

넷 직전 입력 확인.
데이터를 명확하게 시각화할 수 있는 정확한 위치는 모델로 데이터가 들어가기 직전 위치인 y = model(x) 바로 앞에 있다. 즉, 넷으로 들어가는 것을 정확하게 시각화 한다 - raw tensor 데이터 및 레이블을 시각화로 확인한다. 이 부분이 유일한 진실의 원천이다.

이를 확인함으로써 수 많은 개발 사례에서 데이터 처리와 증강에서 문제점을 찾을 수 있었다.

Visualize prediction dynamics.
학습 동안 고정된 test batch로 넷 예측치를 시각화하라. 예측이 어떻게 움직이는가에 대한 "동적거동"은 학습이 어떻게 진행 되는지에 대한 믿을 수 없을 정도로 좋은 직관력을 줄 것이다.

넷이 어떤 식으로든 너무 많이 흔들려 불안정성이 드러나면 데이터에 맞추기 위해 "고군분투"하는 것을 느낄 수 있는 경우가 많다. 너무 낮거나 높은 학습율은 jitter 양으로도 쉽게 눈에 띈다.

Use backprop to chart dependencies.
딥러닝 코드는 복잡하고, 벡터화된, 다양한 연산을 포함한다. 몇 번 마주친 비교적 흔한 버그는 사람들이 문제를 잘못 이해하고(예: 어딘가에서 transpose/permute 대신 view를 사용한다) 무심코 batch의 dimension을 섞어버린다는 것이다.

넷은 또 다른 샘플을 통해 몇몇 데이터를 무시하도록 학습할 수 있으므로 여전히 잘 훈련될 것이라는 것은 우울한 사실이다.

이(와 기타 관련 문제)를 디버깅하는 한 가지 방법은 어떤 예에 대한 손실을 1.0로 설정하고, 입력까지 역방향 패스를 실행하며, i번째 예에서만 0이 아닌 구배를 얻도록 하는 것이다.

일반적으로 말하면 gradients는 네트워크 내용에 따라 무엇이 달라지는지에 대한 정보를 제공하며 debugging에 유용하다.

Generalize a special case.
일반적인 코딩 팁이지만 사람들은 씹을 수 있는 것보다 더 많이 물어뜯을 때 버그를 만들어 내는 것을 종종 보았다.

코딩 시 세부적인 기능을 먼저 작성하고, 일단 작동하게 한 다음, 나중에 같은 결과를 얻도록 일반화하 하라. 이는 종종 벡터화 코드에도 적용된다. 먼저 완전한 루프 버전을 작성한 후, 그 다음 벡터화 코드로 변환한다.


To be continued ...

2019년 1월 20일 일요일

RTX Titan FP16 comparison

Environment
-Baseline: cifar10 example[1], CEloss, DPN structure
-No test used, only training
-Scratch Initialization, 1-epoch only
-With data augmentation
-i7, 32gb cpu memory under windows10
-Anaconda env., pytorch1.0, python 3.7, cuda10, cudnn 7.4.2[3]

* RTX titan has 24Gb memory size, but video display shares it and occupies about 3 Gb,
so about 21 Gb mem can be used for dnn training.
* Turing architecture(rtx series) does not support cuda 9.x versions.


(1) Baseline(fp32) computation according to changes of batch_size
batch_size=128
--------------------------------
Epoch: 0
2019-01-21 11:15:03.973084
0 391 Loss: 2.436 | Acc: 6.250% (8/128)
50 391 Loss: 2.351 | Acc: 14.982% (978/6528)
100 391 Loss: 2.168 | Acc: 20.707% (2677/12928)
150 391 Loss: 2.065 | Acc: 23.996% (4638/19328)
200 391 Loss: 1.988 | Acc: 26.877% (6915/25728)
250 391 Loss: 1.922 | Acc: 29.451% (9462/32128)
300 391 Loss: 1.867 | Acc: 31.452% (12118/38528)
350 391 Loss: 1.817 | Acc: 33.224% (14927/44928)
2019-01-21 11:18:36.301463 ==>  3min 33sec.

batch_size=256
----------------------
Epoch: 0
2019-01-21 11:21:24.807364
0 196 Loss: 2.315 | Acc: 10.156% (26/256)
50 196 Loss: 2.201 | Acc: 17.831% (2328/13056)
100 196 Loss: 2.018 | Acc: 24.760% (6402/25856)
150 196 Loss: 1.905 | Acc: 29.134% (11262/38656)
2019-01-21 11:24:48.638777 ==> 3min 24sec.


batch_size=512: Memory error!!
----------------------------------



(2) FP16 for baseline 
-Change batch_size with fixed static_loss_scale(128.0)[2].
-Check GPU memory for increasing batch_size

batch_size=128
--------------------------------
Epoch: 0
2019-01-21 11:10:35.342605
0 391 Loss: 2.371 | Acc: 7.812% (10/128)
50 391 Loss: 2.421 | Acc: 13.480% (880/6528)
100 391 Loss: 2.242 | Acc: 18.162% (2348/12928)
150 391 Loss: 2.115 | Acc: 21.797% (4213/19328)
200 391 Loss: 2.029 | Acc: 24.662% (6345/25728)
250 391 Loss: 1.951 | Acc: 27.596% (8866/32128)
300 391 Loss: 1.891 | Acc: 29.893% (11517/38528)
350 391 Loss: 1.831 | Acc: 31.987% (14371/44928)
2019-01-21 11:13:24.088977 : 2min 49sec

batch_size=256 ==> 13Gb occupied in GPU memory
--------------------------------
Epoch: 0
2019-01-21 11:37:30.438707
0 196 Loss: 2.406 | Acc: 7.031% (18/256)
50 196 Loss: 2.273 | Acc: 15.709% (2051/13056)
100 196 Loss: 2.078 | Acc: 22.625% (5850/25856)
150 196 Loss: 1.947 | Acc: 27.491% (10627/38656)
2019-01-21 11:39:59.184161 ==> 2min 30sec

batch_size=512 ==>  21.87Gb occupied in GPU
--------------------------------
Epoch: 0
2019-01-21 11:27:54.565118
0 98 Loss: 2.329 | Acc: 13.086% (67/512)
50 98 Loss: 2.182 | Acc: 17.601% (4596/26112)
2019-01-21 11:30:14.626127 ==> 2min 20sec


batch_size=512+128 ==> Memory Error!!
--------------------------------


(3) For changes of static_loss_scale with fixed batch_size(512)

static_loss_scale=64.0
-----------------------------------------------
Epoch: 0
2019-01-21 11:45:43.910621
0 98 Loss: 2.337 | Acc: 11.914% (61/512)
50 98 Loss: 2.167 | Acc: 18.624% (4863/26112)
2019-01-21 11:48:03.947331 ==> 2min 20sec


static_loss_scale=256.0
-------------------------------------------
Epoch: 0
2019-01-21 11:49:50.158676
0 98 Loss: 2.413 | Acc: 8.789% (45/512)
50 98 Loss: 2.224 | Acc: 16.885% (4409/26112)
2019-01-21 11:52:10.582739 ==> 2min 20sec



References
1. pytorch cifar10
2. NVIDIA apex
3. cuDNN support matrix