6 minute read

오늘은 BatchNormalization을 파이썬 넘파이로 구현하고 하나씩 뜯어서 공부해보려합니다.

Batch Normalization 복습하기 👈 클릭!

배치정규화 논문 원본은 여기 링크를 참고해주세요!

오늘은 논문을 직접 읽어보고, 어떻게 연산이 이루어지는지 그리고 더 나아가 코드로서는 어떻게 구현이 되는지까지 알아보겠습니다. (그냥 제가 해석한거라 정확하지 않을 수 있습니다!)

논문은 제가 읽으며 빠르게 해석한 내용이니 참고하시면 좋을 것 같습니다. 🌝

Batch Normalization 논문

저자 : Sergey Ioffe & Christian Szegedy

Abstract

딥 뉴럴 네트워크를 학습시키는 것은 매 레이어의 입력값들이 학습을 거치면서 변화하기 때문에 복잡합니다. 이것은 학습 속도를 낮추므로, 학습률을 더 낮추거나 파라미터 값을 더 조심스럽게 초기화해야하는 작업이 필요해집니다. 그래서 모델을 학습시키기에 정말 말도 안되게 어렵게 만듭니다.

우리는 이 현상을 내부공변량변화라고 지칭하겠습니다. 그리고 이 현상은 입력갑의 레이어들을 정규화시킴으로써 극복이 가능합니다.

배치 정규화는 더 높은 학습률을 사용가능하게하고, 초기화에 덜 신경 쓸 수 있도록 해줍니다. 배치 정규화는 또한 regularizer 역할도 합니다. 그리고 Dropout 을 사용하지 않아도 되는 장점이 있습니다.

이미지 분류 작업같은 경우, 배치 정규화를 사용하면 일반적인 방법보다 14번의 학습 단계를 줄일 수 있습니다. 배치 정규화를 이용하여 이미지넷에서 오차를 4.9%까지 줄였습니다. 이는 사람의 오차보다도 낮은 수치입니다.

Introduction

딥러닝은 비전, 음성인식 등 여러 분야에서 정말 드라마틱하게 발전해왔습니다. SGD는 깊은 신경망에서 효과적인 방법이라고 증명해왔습니다, 그리고 SGD에서 파생된 Momentum 그리고 Adagrad는 예술적인 경지의 퍼포먼스를 이루었습니다.

SGD방식에서의 네트워크의 파라미터 $\theta$ 는 다음 식과 같이 손실을 최소화합니다. $x_{1…N}$ 는 훈련 데이터 셋입니다.

\[\theta = arg min_{\theta} \ {1\over N} \sum_{i=1}^{N}l(x_{i}, \theta)\]

SGD의 훈련과정 매 스텝마다, 사이즈가 m인 ($x_{1…m}$) 미니 배치를 고려합니다. 미니배치는 아래와 같은 식을 이용하여 손실함수의 미분값을 업데이트합니다.

\[\frac{1}{m} \frac{\partial l(x_{i}, \theta) }{\partial \theta}\]

미니배치 예제로 BN을 사용하면서 시간의 측면은 빼고, 여러 부분에서 도움이 되었습니다.

  • 첫째, 미니배치에서 손실함수의 기울기가 학습 데이터 셋 전체의 기울기로 추정됩니다. 배치 사이즈가 커질 수록 성능도 올라갑니다.

  • 둘째, 배치를 계산함에 있어서 효율적입니다.

SGD방식이 간단하고 효율적인 반면, 모델의 하이퍼파라미터를 조심스럽게 조절해야한다는 단점이 있습니다.(특히, 최적화할 때 학습률과 모델의 파라미터들의 초기값에서)

학습은 매 layer들의 입력값이 다음 층에 영향을 줘서 어려워집니다. 그래서 파라미터들의 조그마한 변화도 신경망이 깊어질수록 영향을 크게 끼치게 됩니다.

이러한 layer들의 입력값의 분포의 변화는 문제를 야기합니다. 왜냐하면 layer들은 지속적으로 새로운 분포에 적용되기 때문입니다.

학습시스템이 변화하면서 입력값의 분포가 달라지면, 공변량 변화 라는 것을 경험하게 됩니다. 이것은 주로 domain adaptation에서 다뤄졌습니다.

그러나 공변량 이동의 개념은 하위 네트워크 또는 계층과 같은 부분들에 적용하기 위해 학습 시스템 전체를 넘어 확장될 수 있습니다.

네트워크 연산을 살펴보면

\[l = F_{2}(F_{1}(u, \theta_{1}), \theta_{2})\]

(추가설명 : 손실값( $l$ ) 은 첫번째 스텝연산 후 그 결과값으로 다시 다음 연산을해서 Gradient Descent 하는 방식)

예를 들어서, Gradient Descnet step에서 아래식과 같이 더 자세히 표현이 가능합니다.

\[\theta_{2} \Leftarrow \theta_{2} - \frac{\alpha}{m} \sum_{i=1}^{m} \frac{\partial F_{2}(x_{i},\theta_{2})}{\partial \theta_{2}}\]

(m : batch_size, $\alpha$ : 학습률) 이 두가지는 입력값 x를 받은 $F_{2}$ 네트워크에서와 완전히 동일합니다. 그러므로 학습을 용이하게 하는 입력값 분포 특징들(동일한 분포를 가지는 학습과 테스트 데이터간에) 은 하위 네트워크 학습에도 똑같이 적용됩니다. 그래서 x의 분포에 대한 효과가 계속 지속됩니다. 결국, 공변량 변화 때문에 $\theta_{2}$ 를 다시 조정할 필요가 없게됩니다.

하위 네트워크의 입력값에 대한 수정된 분포는 layer들의 결과에 대해 긍정적인 효과를 가져옵니다.

시그모이드 함수를 사용하는 layer를 생각해보면, $z = g(Wu + b)$ (u는 layer의 입력값, 가중치는 행렬 W, 편향은 b) 이들은 학습되어야 할 layer의 파라미터값입니다. 그리고 $g(x)=\frac{1}{1+exp(-x)}$ 에서 절대값 x의 값이 증가할수록, $g^\prime(x)$ 는 0으로 수렴하게됩니다.

이것은 $x = Wu + b$ 의 모든 값들을 사라지게하고 모델의 학습속도는 느려지게 됩니다.

그러나, x는 W, b 및 아래 모든 레이어의 파라미터의 영향을 받기 때문에 (학습하는동안 x의 크기들을 이동시키는) 그러한 파라미터들의 비선형성을 포화상태로 만들고, 수렴값으로 향하는 것을 느리게 만듭니다. 이러한 효과는 네트워크가 깊어질수록 증폭됩니다.

실제로, 포화문제 그리고 기울기소실 효과를 야기하는 것들은 보통

  • ReLU(X) = MAX(X,0) 함수 사용
  • 섬세한 초기화
  • 더 작은 학습률 사용 의 방법들로 해결이 가능합니다.

그렇게 한다면, 입력값들의 비선형성의 분포는 네트워크를 학습시킬시 안정적이고, 최적화는 포화상태에 덜 빠질 수 있고 학습속도는 더 빨라질 수 있다고 확신할 수 있습니다.

우리는 깊은 신경망에서 내부 노드들 사이의 분포 변화를 내부 공변량 변화(Internal Covariate Shift) 라고 부르기로 합니다.

이러한 내부 공변량 변화를 없앰으로써, 학습을 가속시킬 수 있습니다. 우리는 다음과 같은 새로운 방법을 소개합니다, Batch Normalization 은 내부 공변량 변화를 줄이는 step을 가집니다.

그리고 이 BN은 깊은 신경망에서 드라마틱하게 학습속도를 높여줍니다. BN은 정규화하는 스텝을 통해 이루어지는데, 이는 layer의 입력값의 평균과 분산을 수정합니다.

BN은 또한 초기 파라미터값 또는 gradient의 크기에 의존하지 않고 네트워크의 gradient를 쉽게 구할 수 있습니다. 이것은 우리가 gradient가 발산하는 위험없이 더 큰 학습률을 사용할 수 있게 해줍니다.

더 나아가, BN은 모델을 규칙화시키고, 드랍아웃의 필요성을 줄여줍니다. 최종적으로, BN은 비선형성의 포화상태를 사용하는 것을 가능하게 해줍니다.

BN을 사용함으로써, ImageNet classification에서 사람의 오차율을 능가하는 퍼포먼스를 볼 수 있습니다.

Towards Reducing Internal Covariate Shift

우리는 내부 공변량 변화를 훈련중 네트워크 매개 변수의 변화로 인한 네트워크 활성화 분포의 변화로 정의합니다. 훈련 과정을 개선하기 위해서, 우리는 내부 공변량 변화를 줄이는 방향을 알아보았습니다. layer의 x 입력값마다의 분포량을 고침으로써, 학습 과정의 속도를 높일 수 있을 것이라 기대할 수 있습니다. 입력값들일 백색화(whitened) 된다면 네트워크 학습이 빨라진다는 것은 오랫동안 알고있던 사실이였습니다.(평균과 단위 분산을 0으로 선형 변환)


Code 구현

class BatchNormalization:

    def __init__(self, gamma, beta, momentum=0.9, running_mean=None, running_var=None):
        self.gamma = gamma
        self.beta = beta
        self.momentum = momentum
        self.input_shape = None # 합성곱계층은 4차원, 완전연결 계층은 2차원

        # 시험할 때 사용할 평균과 분산
        self.running_mean = running_mean
        self.running_var = running_var

        # backward시 사용할 중간 데이터
        self.batch_size = None
        self.xc = None # 평균을 0으로 만들기 위한...
        self.std = None #표준편차
        self.dgamma = None #감마미분값
        self.dbeta = None #베타미분값

    def forward(self, x, train_flg=True):
        self.input_shape = x.shape 
        if x.ndim != 2: 
            N, C, H, W = x.shape 
            x = x.reshape(N, -1) 
        
        out = self.__forward(x, trainflg)

        return out.reshape(*self.input_shape)

    def __forward(self, x, train_flg):
        if self.running_mean is None:
            N, D = x.shape # N: batch_size, D: Features
            # 각 feature 별로 정규화를 진행하기 때문에 평균과 분산을 feature 개수만큼 0으로 초기화
            self.running_mean = np.zeros(D)
            self.running_var = np.zeros(D)
        
        if train_flg:
            mu = x.mean(axis=0) #mu : 평균
            xc = x - mu # 평균이 0이 되도록 x에서 평균만큼 빼기
            var = np.mean(xc**2, axis=0)
            std = np.sqrt(var + 10e-7)
            xn = xc/std

            self.batch_size = x.shape[0]
            self.xc = xc
            self.xn = xn
            self.std = std
            self.running_mean = self.momentum * self.running_mean + (1-self.momentum) * mu
            self.running_var = self.momentum * self.running_var
        else:
            xc = x - self.running_mean
            xn = xc / ((np.sqrt(self.running_var + 10e-7)))

        out = self.gamma * xn + self.beta
        return out
    
    def backward(self, dout):
        if dout.ndim != 2:
            N, C, H, W = dout.shpae
            dout = dout.reshape(N, -1)
        
        dx = self.__backward(dout)

        dx = dx.reshape(*self.input_shape)
        return dx
    
    def __backward(self, dout):
        dbeta = dout.sum(axis=0)
        dgamma = np.sum(self.xn * dout, axis=0)
        dxn = self.gamma * dout
        dxc = dxn / self.std
        dstd = -np.sum((dxn * self.xc) / (self.std * self.std), axis=0)
        dvar = 0.5 * dstd / self.std
        dxc += (2.0 / self.batch_size) * self.xc * dvar
        dmu = np.sum(dxc, axis=0)
        dx = dxc - dmu / self.batch_size

        self.dgamma = dgamma
        self.dbeta = dbeta

        return dx


Code Review

이제 한블럭씩 뜯어서 분석해보겠습니다.

def __forward(self, x, train_flg):
        if self.running_mean is None:
            N, D = x.shape # N: batch_size, D: Features
            self.running_mean = np.zeros(D)
            self.running_var = np.zeros(D)
        
        if train_flg:
            mu = x.mean(axis=0) #mu : 평균
            xc = x - mu # 평균이 0이 되도록 x에서 평균만큼 빼기
            var = np.mean(xc**2, axis=0)
            std = np.sqrt(var + 10e-7)
            xn = xc/std

            self.batch_size = x.shape[0]
            self.xc = xc
            self.xn = xn
            self.std = std
            self.running_mean = self.momentum * self.running_mean + (1-self.momentum) * mu
            self.running_var = self.momentum * self.running_var
        else:
            xc = x - self.running_mean
            xn = xc / ((np.sqrt(self.running_var + 10e-7)))

        out = self.gamma * xn + self.beta
        return out

__forward 함수의 경우 처음 배치 정규화를 진행하는 경우에 해당됩니다.

running_mean 이 처음에는 None 이므로, 이럴 때 각 feature 별로 정규화를 진행하기 때문에 평균과 분산을 feature 크기만큼에 해당되는 영행렬로 초기화 합니다.

if train_flg 학습을 진행시

def forward(self, x, train_flg=True):
        self.input_shape = x.shape #입력 데이터 모양
        if x.ndim != 2: #입력 차원값이 2가 아니라면, 합성곱계층
            N, C, H, W = x.shape # 순서대로 BatchSize, Channel수, 높이, 너비
            x = x.reshape(N, -1) # Batchsize크기만큼을 행으로 가지는 행렬로 resize
        
        out = self.__forward(x, trainflg) #out값에 BN 순전파 값을 저장

        return out.reshape(*self.input_shape) # *(asterisk), 입력모양을 정확히 알 수 없으므로 사용! 어떤 입력모양이 들어와도 동작하게끔

train_flg 로 훈련상태인지 테스트 상태인지 확인합니다

Leave a comment