FASTA 압축기 만들기

DNA 시퀀스를 다루는 사람들에겐 FASTA/FASTQ라는 이름만큼 익숙한 포맷이 또 없을 것이다. 시퀀싱된 염기서열을 저장하는, 가히 표준이라 할 수 있을만큼 널리 사용되는 포맷이다. FASTA 포맷은 샘플 이름과 샘플 시퀀스로 이루어져있다. 예를 들어 “sample1”, “sample2″이라는 이름의 샘플이 각각 “ATGCATGC”, “TTTTTTTTT”라는 시퀀스로 시퀀싱되었다면 아래와 같은 FASTA 포맷의 파일로 표현할 수 있다.

>sample1
ATGCATGC
>sample2
TTTTTTTTT

FASTA 포맷의 가장 큰 장점은 굳이 포맷이라고 이름붙이는 것이 어색해 보일 정도의 간결성과 직관성에 있다. Parsing 작업이 굳이 필요하지 않으며, 필요하더라도 아주 쉽게 코딩할 수 있다 (물론 Biopython등 상용화된 패키지를 이용하는 것이 제일 편하다).

굳이 단점을 꼽자면 용량 측면에서의 비효율성이 있을 것이다. 각 베이스를 베이스 그대로 표기하여 직관성을 얻은 대신 하나의 베이스마다 8비트(1바이트)의 메모리를 할당해야하는 것이다.

비록 그렇게 큰 용량은 아니지만, 빠르고 원활한 시퀀스 파일 공유를 위해 FASTA 압축 알고리즘을 짜보면 어떨까 싶어 재미삼아 몇 가지 코딩을 해봤다.

이 글은 “새로운”(?) FASTA용 무손실 압축 알고리즘을 짜보고 싶다는 생각으로 시작했지만 결국 상용화된 프로그램이 짱이라는 결론에 이르는, 컴퓨터 비전공자의 분투를 담은 글이다.

1. Look-and-Say 수열을 이용한 압축 알고리즘 (.say format)

Look-and-say 수열은 현재 항 숫자의 종류와 갯수를 읽고, 읽은 내용으로 다음 항을 결정하는 수열이다. 수열의 첫 항이 1일 때, “1이 한 개 있다”고 읽을 수 있다. “1이 한 개(1)”이므로 이 수열의 둘째 항은 11이된다. 둘째 항은 11이므로 “1이 두 개 있다”고 읽을 수 있다. “1이 두 개(2)”이므로 셋째 항은 12가 된다. 이 과정을 계속해서 반복하면 다음과 같은 수열이 만들어진다.

[1]: 1
[2]: 11
[3]: 12
[4]: 1121
[5]: 122111
...

베르베르의 소설 ‘개미’에도 등장하여 우리나라에선 ‘개미 수열’이라는 이름으로도 알려져 있다. 바로 이 수열을 시퀀스 데이터 압축에 사용하면 어떨까 싶었다.

  • 1바이트(8비트) 중 3비트는 베이스 종류를 나타내는 데에 사용한다.
    • 000은 A, 001은 T, 010은 G, 011은 G, 100은 N, 101은 U
    • 110과 111은 비워두었다.
  • 나머지 5바이트는 반복수를 나타내는 데에 사용한다.
    • 00000 ~ 11111, 즉 1부터 32까지의 반복수를 표현할 수 있다.
    • 32번 이상 반복되는 베이스는 1바이트로 표현할 수 없다. 두 개 이상의 바이트를 써야한다.

예를 들어 AAAAAAATGGGGGGGGG라는 시퀀스는 이 방법으로 3 바이트로 표현할 수 있다.

[-1 byte-] [-2 byte-] [-3 byte-]
[00000110] [00100000] [01001000]

같은 시퀀스를 FASTA로 저장하려면 17바이트가 필요하다.

FASTA 포맷에 비교한 .said 포맷은 장점은 다음 두 가지로 요약할 수 있다.

  1. Homopolymer가 많은 시퀀스일수록 압축 효율이 높아진다. poly A region과 같은 시퀀스에서는 비교도 안될 정도로 작은 용량의 파일을 만드는 것을 확인할 수 있다.
  2. 반복이 거의 없더라도 같은 시퀀스가 저장된 FASTA보다는 작은(<=) 파일을 만든다. 이는 한 베이스가 반복되지 않는 경우에도 FASTA에서와 같이 1바이트 용량만을 차지하기 때문이다.
    반복수를 (대체로) 늘려주는 Burrows-Wheeler transformation을 사용한 후 `.said` 압축을 하면 좀 더 좋은 압축률을 얻을 수 있지 않을까? 했는데 생각만큼 크게 개선되진 않았다.

이 스크립트를 사용하면 .said 포맷으로 FASTA 변환할 수 있다: [압축, 압축해제]

# COMPRESS
python fa2said.py input.fa
# DECOMPRESS
python said2fa.py input.said

2. 한 베이스를 2 비트만으로 표현하기 (.two format)

genome / CDS 시퀀스는 생각보다 반복이 그리 많지 않았다. .said format으로는 높은 압축률을 얻기 어려운 상황이다. 그래서 아예 반복수를 기록하지 않고 한 베이스가 차지하는 용량을 극도로 줄이면 어떨까 싶었다.

압축 할 시퀀스에 A, T, G, C 네 종류의 베이스밖에 없다고 가정하면 (2^2개 이므로) 2비트만 사용하면 한 베이스를 인코딩할 수 있다. 한 베이스를 표현하는 데에 8비트를 사용해야 했던 FASTA 포맷에 비해 용량을 무려 1/4로 줄일 수 있는 것이다. 이렇게 압축한 파일 포맷을 .two 포맷이라고 부르도록 하자.

ATGCAATC의 경우 .two 포맷으론 2바이트(16비트)로 인코딩할 수 있다.

A T G C    A A T C
[00011011] [00000111]

이 스크립트를 사용하면 FASTA파일을 .two 포맷으로 압축할 수 있다: [압축, 압축해제]

# COMPRESS
python fa2two.py input.fa
# DECOMPRESS
python two2fa.py input.say

맺으며

이 기회에 야매로 압축 알고리즘을 공부하면서 첫 번째 시도(.said)가 run-length encoding이라 불리는 가장 원시적인 압축 알고리즘이라는 것을 알았다.

두 번째 시도 (.two)는 Huffman coding과 상당히 닮은 구석이 있다. 차이점은 DNA/RNA의 경우 인코딩하려는 데이터의 종류(A, T, U, G 또는 C, 추가로 N)가 이미 알려져있다는 것이다. DNA의 경우 데이터의 종류가 4개(A, T, G, C) 뿐이기 때문에 사람이 직접 2비트만으로 모든 데이터를 인코딩할 수 있었다.

두 번째 시도는 생물정보학계의 영웅 Jim Kent가 과거에 이미 .2bit라는 이름으로 상용화해서 쓰이고 있는 포맷이었다 [Jim Kent’s Repo]. C로 짜인 코드여서 내 것보다 훨씬 빠르게 압축할 수 있다. .2bit 포맷은 원하는 샘플의 시퀀스만 가져올 수도 있도록 각 샘플의 해시 딕셔너리도 함께 저장한다.

DNA 시퀀스와 달리 데이터 종류가 알려져 있지 않은 경우엔 Huffman coding을 사용할 수 있다. Huffman coding은 binary tree를 사용해서 자동으로 최적의 인코딩을 찾아준다. .two는 binary tree를 사용하지 않고 사람이 직접 인코딩 딕셔너리를 정해주는 특수한 변형이라고 할 수 있겠다.

.zip 압축에 사용되는 Lempel-Ziv 알고리즘은 인코딩 딕셔너리가 Huffman coding과 같이 고정되어 있지 않고 현재까지 압축한 데이터에 따라 유동적으로 변한다. 비슷한 패턴이 반복해서 나타나는 파일에 있어서 효율적인 압축을 가능케 한다. 압축을 풀기 위해서는 데이터 인코딩에 사용한 binary tree의 정보를 어떤 형태로든 압축된 결과 파일과 함께 저장해야하는 Huffman coding과 달리, 압축된 데이터 그 자체에 인코딩 딕셔너리가 포함된 것과 마찬가지이기 때문에 별도의 정보를 저장할 필요도 없다.

코딩한 결과물 자체는 성능 면에서나 속도 면에서나 무쓸모하지만 그 과정에서 많이 배웠다.

참고

Weakly-Supervised localization: 뉴럴넷은 어디를 바라보고 있는가

ConvNet이 이미지를 다루는 데 강력한 성능을 보이기는 하지만, layer가 깊어지면 깊어질수록 이미지의 어떤 특징을 보고 판단을 내리는지를 파악하기란 쉽지 않다.

이미지 하나를 인풋으로 넣고 layer를 차례차례 통과하면서 activation이 어떻게 변하는지를 살펴볼 수도 있겠지만 VGG와 같은 모형처럼 레이어가 매우 깊어지거나 GoogLeNet, ResNet처럼 특수한 모듈 구조를 사용, 또는 residual connection을 사용하는 네트워크에서는 뉴럴넷이 이미지의 어느 부분을 중요하게 생각하는지의 정보를 얻기가 힘들어진다.

블랙박스인 듯 보이는 뉴럴넷(특히 ConvNet)의 내부를 조금이나마 투명하게 드러내려는 시도는 꾸준히 있어왔다. 구글에서도 최근 “Inceptionism”이라는 이름으로 재미난 연구 결과를 블로그에 공개한 바 있다.

여기서는 이보다 뉴럴넷이 “어디를 집중해서 보고 있는지”를 제시한 아주 단순하고도 유용한 방법을 살펴보고자 한다. 바로 weakly-supervised localization의 시초격 되는 방법인데, localizer 모형에게 이미지에 어떤 사물이 있는지만을 학습시키면 이미지에 어떤 사물이 있는지는 물론, 이미지의 어느 위치에 그 사물이 있는지 까지 꽤나 정확하게 예측할 수 있다.

지도학습으로 학습시키긴 했지만 학습 과정에서 알려주지 않은 사물의 위치까지 학습한다는 점에서 이러한 방식을 weakly-supervised라고 이름 붙였다. 해당 논문은 재작년에 ArXiv에 올라온 것으로 발표 당시 큰 주목을 받았었다.

 

Architecture

이 논문에서 제시한 Weakly-supervised localizer의 구조는 매우 단순하다. 어떻게 localizer를 만드는지 그 과정을 하나하나 짚어보자.

스크린샷 2018-06-17 오후 10.12.16.png

0. (Pre-trained) CNN model

CNN classifier를 가져온다. 이 classifier는 이미지를 보고 이미지에 상응하는 class label을 예측해야한다. 밑바닥부터 학습시켜도 되지만 pre-trained 모형을 사용하면 좀 더 빠르게 좋은 성능을 낼 수 있다.

가져온 CNN classifier에서 말단의 fully connected layer를 모두 제거한다. 즉 가장 깊이 있는 CONV layer가 최말단 레이어가 되도록 한다.

1. CONV layer

0.의 CNN classifier의 최말단 CONV layer 뒤에 CONV layer를 하나 추가한다. 논문에서는 1024개의 filter를 가지는 CONV layer를 추가했다. 여기서도 filter 갯수는 1024라고 가정하자.

이 레이어의 출력은 1024개의 feature map (depth=1024)이 된다.

2. Global Average Pooling (GAP)

1024개의 feature map을 크기가 1024인 벡터로 변환시킨다. 다시 말해 feature map 하나당 수치 하나로 변환시킨다.

이 과정에서 global average pooling (GAP)을 사용한다. GAP layer는 pool size가 input feature map의 크기와 같은, 즉 이미지 전체에서 딱 하나의 값(평균값)만을 pooling하는 레이어이다.

이 논문 이전에 global max pooling (GMP)를 사용한 weakly-supervised localization 방법이 공개되었었는데 GMP는 가장 activation이 강한 region의 정보만을 가져오기 때문에 사물의 경계선을 파악하는 데에 그쳤다고 한다. 사물의 위치 전체를 localization하는 데에는 모든 activation의 정보를 평균내는 GAP가 더 적합하다.

GAP layer의 출력은 각 feature map(\text{width} \times \text{height} \times 1024)의 평균값의 벡터(1 \times 1 \times 1024)가 된다.

3. Fully connected layer

fully connected layer를 GAP layer 뒤에 하나 추가한다.

이 dense layer의 입력은 GAP layer의 출력, 즉 크기 1 \times 1 \times 1024의 벡터이고 출력은 예측하려는 class label의 갯수이다. Weight matrix W의 모양은 1024 \times \text{(\#classes)}가 된다.

4. Class Activation Map (CAM) 계산

1.의 CONV layer에서의 feature map과 3.의 dense layer에서의 weight W를 사용해서 class activation map (CAM)을 계산한다.

Label=c인 class의 activation map CAM_c는 다음과 같이 계산한다.

CAM_c = \sum_{k=1}^{1024}{w_{k, c}f_k}

여기서 w_{k, c}k번째 GAP 벡터에서 class c로 이어지는 가중치값(Wkc열 원소)이고, f_k는 1.에서 추가한 CONV layer의 k번째 feature map이다.

이렇게 구한 CAM_c는 해당 이미지가 클래스 c에 속한다고 판단내릴 때 ConvNet이 이미지의 어떤 부분을 보고있었는지의 정보를 가지고 있다.

CAM_c의 값이 큰 부분이 클래스 c에 속하는 사물의 위치라고 ConvNet이 판단내린 것이다.

 

응용

Weakly-supervised localization은 여러 방면에 응용될 수 있다. Semantic image segmentation을 위한 데이터를 만드는 데에도 사용될 수 있고, ConvNet이 보고있는 지점을 파악할 수 있다는 점에서 ConvNet이 이미지로부터 학습한 feature를 사람이 직관적으로 이해할 수 있도록 시각화하는 데에도 사용될 수 있다.

이미 학습된 모형을 사용해서 간단히 만들 수 있다는 큰 장점이 있어 여러 분야에 매우 쉽게 응용할 수 있다. 비록 localizer가 아닌 classifier에 비해서는 classification 성능이 떨어지지만 그 차이가 크지 않으며 localization 기능까지 덤으로 추가할 수 있다.

여기서는 직접 간단한 ConvNet을 만들어 두 가지 작업에 응용해보았다.

1) Facial emotion recognition

학습과 예측에 사용한 데이터는 다음과 같다. [Kaggle]

  • Feature: 48 \times 48 크기의 흑백 얼굴 사진.
  • Target: 사진 속 인물의 표정(감정). 7가지 중 하나이다(neutral, fear, happy, sad, surprise, angry, disgust).
  • train set: 25120 imgs / test set: 5382 imgs

이 데이터를 학습해서 사람 표정의 어떤 부분을 보고 감정을 유추하는지를 알아보려 했다.

모형 구조와 학습에 사용한 하이퍼 파라미터는 이 노트북[1, 2]을 참고하시길 바란다.

결과를 살펴보면 꽤 그럴듯하다.

fer-1

주로 행복한 표정은 입 모양을 보고 판단하는 듯하다. 입모양이 크고 반달 모양이면 happy라고 판단할 확률이 크게 올라간다. 놀란 표정은 눈동자의 크기를 보고 판단하고 무표정은 이목구비의 전체적 형태를 보고 판단하는 듯하다.

fer-2

이 예시에서는 True class는 FEAR로 예측에는 실패했지만  SURPRISE라고 판단하는 것도 틀린 판단은 아닐 수 있다는 것을 보여준다. ConvNet이 무엇을 보고있는지를 주의 깊게 봐야하는 이유다.

2) Chest X-Ray (Pneumonia)

의료 이미지를 분석하는 경우엔 올바르게 질병 또는 정상상태를 분류하는 것도 중요하지만, 이미지의 어떤 부분(환부)을 보고 그런 판단을 내렸는지를 살펴보는 것이 매우 중요하다. 어디까지나 최종 판단은 의료 분야의 전문 종사자가 내리는 것인 만큼 전문가가 판단을 내리는 데에 도움이 되도록 뉴럴넷이 판단한 환부의 위치까지 표시해 준다면 더욱 유용할게다.

학습과 예측에 사용한 데이터는 다음과 같다. [Kaggle]

  • Feature: 다양한 크기의 흉부 엑스레이 사진.
  • Target: 폐렴인지 정상인지. [PNEUMONIA / NORMAL]
  • train set: 5216 imgs / test set: 624 imgs

데이터 학습이 용이하도록 모든 사진을 가로 256 픽셀, 세로 200 픽셀로 resize한 후 모형에 입력시켰다. 모형 구조와 학습에 사용한 하이퍼 파라미터는 이 노트북을 참고하시길 바란다 (실수로 zero-padding을 하지 않아서 이미지의 가장자리 부분의 정보 손실이 일어났다).

81%의 꽤 높은 정확도로 test set을 올바르게 진단할 수 있었다.

 

xray-1

폐렴 환자의 흉부 엑스레이 사진에서는 주로 좌폐와 우폐 사이의 공간을 유심히 살펴본 것으로 보인다.

xray-2.png

 

반면 정상 환자의 사진에서는 옆구리 부분의 흉골, 양쪽 폐 아래의 공간을 중요하게 본 듯 하다.

임상 전문가라면 뉴럴넷이 살펴본 위치를 보고 더 많은 아이디어를 얻을 수 있지 않을까?

 

참고

 

 

 

 

 

 

Batch Normalization 이해하기

현대적인 딥러닝 모델을 디자인할 거의 항상 빠지지 않고 쓰이는 테크닉들이 있다. 하나는 recurrent 구조 (LSTM, Attention)이고 다른 하나는 batch normalization (BatchNorm)이다. LSTM과 attention 대해서는 recurrent neural net 다루면서 자세히 살펴보도록 하고 이번 글에서는 학습 과정에서 뉴럴넷을 안정시켜주는 표준화 기법 하나인 batch normalization 대해 다뤄보겠다.

 

  • 기존 방법의 문제점
  • BatchNorm
    • 알고리즘
    • 테스트할 때
    • BN layer
  • TensorFlow 구현

 

기존 방법의 문제점

BatchNorm이 어떤 의미를 가지는지를 알기 위해서는 BatchNorm이 고안되기 이전의 딥러닝 모형 초기화 및 학습 과정 표준화 과정을 둘러볼 필요가 있다.

뉴럴넷이 안정적으로 잘 학습되기 위해서는 입력층에 넣을 인풋과 각 층의 weight를 표준화할 필요가 있다. BatchNorm이 고안되기 전에는 두 가지 방법을 주로 사용했는데, 이전 포스트[1, 2]에서 각각의 방법을 간단히 다룬 바 있다. 간단히 복기하자면 이렇다: (1) 인풋은 centering scaling하고 (2) 인풋 뉴런 n개인 층의 weight \div \sqrt{n/2}로 표준화한다. 단순한 방법이지만 표준화하지 않은 입력, 가중치값을 사용했을 때에 비해 더 빨리, 더 좋은 성능으로 수렴하는 것을 경험적으로 확인할 수 있다.

여기서 중요한 문제가 발생한다. 입력층에 넣는 인풋은 표준화할 수 있다. 뉴럴넷에 넣기 전에 우리가 원하는 방식으로 원하는 만큼 preprocessing을 하면 된다. 그 결과 입력층의 input distribution은 항상 비슷한 형태로 유지가 되고 안정적으로 가중치 학습을 진행할 수 있다.

e18489e185b3e1848fe185a6e1848ee185b53.png

그러나 은닉층은 인풋의 분포가 학습이 진행됨에 따라 계속 변한다. 은닉층은 이전 레이어의 activation f(XW)을 입력으로 받는다. 학습 과정에서 가중치 W의 값이 W^\prime로 업데이트되면 이전 레이어의 activation 또한 f(XW^\prime)로 바뀌게 된다. 은닉층의 입장에서는 인풋 값의 분포가 계속 널뛰는 것이나 마찬가지이다. 입력 분포의 형태가 유지되지 않으므로 학습도 잘 진행되지 않는다. 그라디언트 값이 큰 학습 초기일수록 문제가 더 심각해진다.

스케치

 

Batch Normalization

알고리즘

바로 위에서 언급한 문제를 internal covariate shift라고 한다. 그대로 입력층보다 깊은, 내부에 있는(internal) 층의 입력값, 공변량(covariate) 고정된 분포를 갖지 않고 이리저리 움직인다(shift) 의미이다. BatchNorm 바로 internal covariate shift 해결하는 테크닉이다.

[1]

은닉층의 입력도 표준화한다면 안정적으로 깊은 레이어의 가중치도 학습시킬 수 있을 것이다. “은닉층의 입력을 표준화한다는 것은 곧이전 층의 출력(raw activation)을 표준화한다는 의미와 같다.

딥러닝은 거의 항상 전체 샘플을 mini batch로 나누어 학습하고 가중치를 업데이트하므로 이전 층의 raw activation을 표준화할때도 각 batch마다 따로 표준화하면 된다.

스케치

이와 같이 각각의 minibatch 평균 \mu_{\mathcal{B}} = \frac{1}{m} \sum_i {x_iw_i} 표준편차 \sigma_{\mathcal{B}} = \frac{1}{m} \sum_i {(x_iw_i - \mu_{\mathcal{B}})^2} 표준화한 activation a_s = f(\frac{XW_1 - \mu_{\mathcal{B}}}{\sigma_{\mathcal{B}}}) 은닉층 B 입력으로 사용하면 은닉층 B 입력은 고정된 분포를 따른다.

쉬워도 너무 쉽다. 이렇게만 하면 될 것 같지만..

[1 문제점]

문제가 가지 있다. 이렇게 은닉층의 입력을 표준화하면 gradient update 과정에서 bias(편향)값이 무시된다. [1]만을 사용해서 표준화한다고 그라디언트 업데이트 과정을 자세히 살펴보자. Raw activation a_r = wx + b라고 E(a_r) = \frac{1}{n} \sum_i a_{r_{i}}이므로

  1. 그라디언트를 계산한다.
    • \Delta b \propto - {\partial L}/{\partial b},  where L is a loss function.
  2. 편향(과 가중치)을 업데이트한다.
    • b \gets b + \Delta b
  3. 편향을 업데이트한 이후의 raw activation:
    • a_r ^\prime = wx + (b + \Delta b)
  4. [1] 이용해서 센터링만 raw activation:
    • \begin{array}{lcl} a_{r_{centered}} ^\prime &=& a_r ^\prime - E(a_r ^\prime) \\ &=& \{(wx + b) + \Delta b\} - \{ E[wx + b] + \Delta b \} \\ &=& (wx + b) - E[wx + b] \end{array}

Bias b 업데이트 \Delta b 완벽하게 캔슬되었다. 초기 편향값에서 이상 업데이트가 되지 않는 것이다. 종류의 파라미터 w, b 사용했는데 파라미터 w 가지만 사용하는 단순한 모형으로 irreversible하게 변환된 것이다.

이 때문에 b 대신 편향의 역할을 할 파라미터를 추가해야한다. 이 파라미터는 그라디언트 업데이트 과정에서 무시되어서는 안된다.

다른 문제도 있다. raw activation 분포를 고정시키는 것은 좋지만 항상 N(0, 1) 고정시킬 필요는 없다. 적절하게 scaling, shifting activation \gamma \cdot \frac{a_r - \mu_{\mathcal{B}}}{\sigma_{\mathcal{B}}} + \beta 사용하는 것이 학습에 도움될 수도 있다.

형태의 activation 사용할 경우 필요하다면 표준화를 되돌릴 수도 있다. \gamma = \sigma_{\mathcal{B}}, \beta = \mu_{\mathcal{B}} \gamma \cdot \frac{a_r - \mu_{\mathcal{B}}}{\sigma_{\mathcal{B}}} + \beta = a_r이기 때문이다.

[2]

위의 문제를 극복하기 위해 표준화한 scaling shifting raw activation, 즉

a_{BN} = \gamma \cdot \frac{XW_1 - \mu_{\mathcal{B}}}{\sigma_{\mathcal{B}}} + \beta

activation function f 입력으로 사용한다. 은닉층 B 입력으로는 f(a_{BN}) 사용한다. 방법을 BatchNorm이라고 한다. \gamma, \beta 파라미터로 학습 과정에서 업데이트되는 값이다.

BatchNorm 장점이 꽤나 많은데

  • bias 업데이트를 무시하지 않는다. \beta bias처럼 행동한다. \beta 업데이트는 표준화해도 캔슬되지 않는다.
  • 은닉층마다 적절한 input distribution 가질 있다. scaling factor \gamma shifting factor \beta 사용해서 적절한 모양으로 입력분포를 조정할 있다.
  • 필요한 경우 표준화를 하지 않을 수도 있다. 위에서 언급한 \gamma = \sigma_{\mathcal{B}}, \beta = \mu_{\mathcal{B}} 경우이다.
  • Activation 값을 적당한 크기로 유지하기 때문에 vanishing gradient 현상을 어느정도 막아준다. 덕분에 tanh, softmax같은 saturating nonlinearity 사용해도 문제가 생긴다.
  • batch-wise로 계산하기 때문에 컴퓨팅하기 용이하다.
  • 위의 장점들을 모두 가지면서, 동시에 층마다 입력 분포를 특정 형태로 안정시켜서 internal covariate shift 방지할 있다.
  • 입력 분포가 안정되므로 학습시 손실함수가 더 빨리, 더 좋은 값으로 수렴한다.
  • 초기 learning rate를 크게 설정해도 안정적으로 수렴한다고 한다.
  • Weak regularizer로도 작용한다고 한다.

이쯤 되면 거의 만능이다.

테스트

지금까지 다룬 내용은 모두 학습 과정에서 일어나는 일들이다. 학습 과정에서는 raw activation minibatch mean, stdev 표준화하면 됐었다. 그런데 학습을 마치고 테스트(또는 evaluation, inference) 때에는 minibatch mean, stdev 존재하지 않는다.

테스트 과정에서는 대신 전체 training data mean, stdev 사용해서 BatchNorm 한다. 전체 training data mean, stdev 번에 계산하기에는 메모리의 제약이 있으므로, minibatch statistic 평균낸 값을 대신 사용한다.

, n개의 minibatch 있을 ,

\hat{\mu} = \frac{1}{n} \sum_i {\mu_{\mathcal{B}}^{(i)}}
\hat{\sigma} = \frac{1}{n} \sum_i {\sigma_{\mathcal{B}}^{(i)}}

Minibatch statistic 따로 저장할 필요 없이 학습 과정에서 moving average \hat{\mu}, \hat{\sigma} 계산하면 된다. Exponential moving average 사용해도 좋다.

i번째 minibatch statistic 각각 \mu_{\mathcal{B}}^{(i)}, \sigma_{\mathcal{B}}^{(i)}라고 ,

\hat{\mu} \gets \alpha \hat{\mu} + (1-\alpha) \mu_{\mathcal{B}}^{(i)}
\hat{\sigma} \gets \alpha \hat{\sigma} + (1-\alpha) \sigma_{\mathcal{B}}^{(i)}

BatchNorm layer

ReLU activation 뉴럴넷의 레이어로 나타낼 있듯 BatchNorm 또한 레이어로 표현할 있다. BN layer raw activation activation function 사이에 위치한다. Convolutional layer에 BatchNorm을 적용하고 싶을 때에도 동일하게 raw feature map과 ReLU layer 사이에 BN layer를 추가하면 된다.

e18489e185b3e1848fe185a6e1848ee185b57.png

BN layer mini batch raw activations a_r 입력받아 아래와 같은 연산을 수행하여 다음 레이어(activation function f) 전달한다.

BN_{\gamma, \beta}(a_r) = \gamma \cdot \frac{a_r - \mu_{\mathcal{B}}}{\sigma_{\mathcal{B}}} + \beta

또한 테스트 사용하기 위해 학습 과정에서 minibatch statistic exponential moving average(또는 그냥 MA) minibatch마다 업데이트한다.

 

TensorFlow 구현

구글에서 고안한 방법답게 TensorFlow에 이 내용들이 친절히 함수로 구현되어 있다. tf.nn.batch_normalization, tf.contrib.slim.batch_norm를 쓰면 간단히 위 알고리즘을 모형 구축에 사용할 수 있다.

tf.nn.batch_normalization을 사용할 경우, minibatch statistic의 EMA를 계산하는 코드를 따로 작성해야 한다.

tf.contrib.slim.batch_norm를 사용할 경우 is_training 옵션을 True로 주면 자동으로 EMA를 계산해서 저장하고, False로 주면 저장된 EMA 값으로 activation을 표준화한다.

TF-Slim 레이어에도 쉽게 적용시킬 수 있다.

import tensorflow as tf
import tensorflow.contrib.slim as slim

bn_params = {"decay": .9,
             "updates_collections": None,
             "is_training": tf.placeholder(tf.bool)}
net = slim.fully_connected(input, 1024,
                           normalizer_fn=slim.batch_norm,
                           normalizer_params=bn_params)

Convolutional layer에도 마찬가지다.

net = slim.conv2d(input, 64, [5,5], padding="SAME",
                  normalizer_fn=slim.batch_norm,
                  normalizer_params=bn_params)

 

 

참고