Loading [MathJax]/jax/output/CommonHTML/jax.js
본문 바로가기

밑바닥부터 시작하는 딥러닝

Learning Skill

공부를 하는 입장이기 때문에, 내용에 오류가 있을 수 있습니다. 오류가 있다면 적극적으로 알려주시면 감사합니다!

1. 가중치의 초기 값

  오버피팅을 억제하는 방법들 중에는 가중치를 줄이거나(weight decay) 가중치를 랜덤으로 0으로 만드는 등(dropout) 가중치를 줄이려고 노력하는 경우가 많다. 그렇다면 가중치를 아예 0으로 시작하면 어떨까 라는 생각을 할 수 있다. 하지만 가중치를 0으로 하는 것은 층을 나누는 것의 의미가 없기 때문이다. 

 

 예를 들어 2층 짜리 신경망에 가중치가 0이라면 역전파에서 0이 전달이 되기 때문에 학습이 진행이 되지 않는다.(곱셈계층의 역전파를 생각해 보자.) 

곱셈 계층의 역전파

 이런 상황에서는 파라미터가 갱신이 되어도 여전히 같은 값을 유지하게 된다. 가중치의 대칭적인 구조(ex. 가중치가 동일한 경우 등)를 무너뜨리려면 초기값을 무작위로 설정해야 한다. 그렇다면 그냥 무작위로 설정하면 되는 걸까? 가중치를 크게 하거나 작게 하면 어떻게 될까? 

 

 각 층마다 노드가 100개인 5층짜리 신경망에 데이터 1000개를 흘려보낸다고 해보자. 가중치를 표준편차가 1인 정규분포를 사용하면 시그모이드의 활성화 값이 1과 0에 몰려 있는 것을 알 수 있다.

 

가중치를 표준편차가 1인 정규분포로 초기화할 때의 각층의 활성화값 분포

 

 시그모이드 함수의 경우 0과 1로 가면 갈수록 기울기가 0이 된다. 활성화 값이 0과 1에 많이 분포한다는 것은 역전파에서 기울기 값이 점점 작아지다가 사라진다는 것을 의미한다. 이 현상을 기울기 소실(gradient vanishing)이라고 한다. 이 문제는 층을 깊게 할수록 학습이 진행이 되지 않아 문제가 생길 수 있다.

 

 그러면 가중치의 표준편차를 작게 0.01로 하면 어떻게 될까? 이번에는 가운데에 몰려있는 것을 알 수 있다.

가중치를 표준편차가 1인 정규분포로 초기화할 때의 각층의 활성화값 분포

 

 기울기 소실 문제가 해결되었다고 볼 수도 있지만, 활성화값들이 하나로 몰려있다는 것은 다수의 뉴런들이 같은 값을 하고 있다는 것이고, 이는 뉴런이 하나 있는 것과 다른 것이 없다는 의미이다. 

 

2. Xaiver 초기값

활성화 함수로 시그모이드 함수를 설정하는 경우 xaiver 초기값을 추천한다. 책에서는 앞계층의 노드가 n개라면 표준편차가 1(n)인 정규 분포를 설명하고 있다.

가중치를 xaiver 방법으로 초기화할 때의 각층의 활성화값 분포

 

 층이 깊어지면서 조금씩 일그러지지만 앞선 방식보다는 넓게 분포되어 있는 것을 확인할 수 있다. xaiver논문에서는 출력 노드도 고려한 균등 분포를 제안한다.

WU[6nin+nout,6nin+nout]

 대부분의 인공지능 라이브러리는 xaiver initialization에서 정규 분포와 균등 분포를 둘 다 제공한다. 두 개중 어느 것이 나은지 결정적인 증거는 없고, 균등 분포는 조금 더 작은 사이즈에서 모든 가중치가 비슷한 중요도를 가질 때 수렴을 더 잘할 수 있고, 정규 분포는 많은 가중치들이 0에 가까울 거라 예상되고, 깊은 신경망에서는 정규 분포가 더 효율적이라고 한다. 하지만 두 개의 차이가 성능에 큰 차이는 주지 않는다고 한다.

 

Why is there a Uniform and Normal version of He / Xavier initialization in DL libraries?

Two of the most popular initialization schemes for neural network weights today are Xavier and He. Both methods propose random weight initialization with a variance dependent on the number of input...

ai.stackexchange.com

 

numpy에서 randn함수로 정규분포를 만든 후  scale을 곱해 xaiver initialization 할 수 있다.

...
elif str(weight_init_std).lower() in ('sigmoid', 'xavier'):
    scale = np.sqrt(1.0 / all_size_list[idx - 1])  # sigmoid를 사용할 때의 권장 초깃값
self.params['W' + str(idx)] = scale * np.random.randn(all_size_list[idx-1], all_size_list[idx])
self.params['b' + str(idx)] = np.zeros(all_size_list[idx])
...

 

3. He 초기값

 활성화 함수가 Relu함수인 경우  xaiver 초기화 방법보다는 He 초기화 방법을 권장한다. He 초기화 방법은 간단한데, 표준편차가 2n인 정규분포를 사용하는 것이다. 간단하게 이해하는 방법은 음의 영역이 0이라서 더 넓게 분포시키기 위해 필요하다고 생각하면 된다.

 

 왼쪽이 xaiver, 오른쪽이 he방식으로 활성화 값을 비교한 것인데, xaiver 초기값의 경우 층이 깊어질수록 0쪽으로 치우쳐지는데, he 초기값의 경우는 층이 깊어져도 비교적 넓게 퍼져있는 것을 알 수 있다. 그림처럼 넓게 펴지게 하기 위해 2 배 한다고 생각하면 편하다.

xaiver vs he 활성화 값 비교

 

3. Batch Normalization

 배치 정규화란 내부 공변량 변화(reduce internal covariate shift)를 줄이기 위해 입력값의 분포를 활성화 값을 정규분포로 바꿔주는 것을 말한다.

 

 여기서 공변량 변화란, 신경망의 입력 분포가 학습 중에 바뀌는 현상을 말한다. 예를 들어 첫 번째 층이 학습을 통해 변하면 두 번째 층에 들어가는 입력값 또한 변하게 된다. 이게 각 층마다 반복되면서 입력값의 분포가 계속 바뀌게 되는 것이 공변량 변화이다. 입력값의 분포가 바뀌면 각 계층이 이에 맞춰서 계속 바뀌면서 학습하기 때문에 학습이 불안해지고, 학습이 느려진다

 

 이를 해결하는 것이 배치 정규화인데, 배치 정규화에는 많은 이점이 있다.

  • 학습을 빨리 진행할 수 있다.
  • 초기값에 크게 의존하지 않는다.
  • 오버피팅을 억제하는데 도움을 준다. (드롭아웃의 필요성이 낮아지고, 오버피팅을 막는데 도움을 준다.)

배치 정규화를 사용한 신경망의 예(첵)

 

 배치 정규화는 학습 시 미니 배치를 단위로 정규화한다. 데이터의; 분포가 평균μB이 0, 분산\( \sigma_B^2  \)이 1이 되도록 정규화를 한다. 또한 정규화 계층마다 데이터에 최적의 범위로 맞추기 위하여 고유한 확대와 이동 변환을 수행한다.

μB=1mmi=1xi

σ2B=1mmi=1(xiμB)2

ˆxi=xiμBσ2B+ϵ

yi=γˆxi+β

 

  • xi : 배치의 i번째 노드.
  • m : 미니배치의 크기.
  • ˆxi : 정규화된 활성화 값.
  • ϵ : 0을 막기 위한 작은 값 10e7.
  • γ :  학습 가능한 확대(scale) 파라미터 (γ=1 에서 시작). 
  • β :  학습 가능한 이동(shift) 파라미터( β=0 에서 시작).

전체 수식은 다음과 같다.

yi=γ(xiμBσ2B+ϵ)+β

 

 순전파 코드는 다음과 같다. 추가적으로 알아야 할 점은 추론 시에는 기존에 사용한 정규화를 할 수 없다. 그래서 지수 이동평균을 통하여 평균과 분산을 저장해 놓았다가, 추론 시 이를 이용하여 정규화를 진행한다.

def __forward(self, x, train_flg):
        # 처음 시작할때, 차원을 맞춰서 0으로 초기화
        if self.running_mean is None: 
            N, D = x.shape
            self.running_mean = np.zeros(D)
            self.running_var = np.zeros(D)
        # 학습중이라면                
        if train_flg:
            # 평균 구하기
            mu = x.mean(axis=0)
            # 분산 구하기 
            # 입력값 - 평균값 
            xc = x - mu
            # (입력값 - 평균값)의 제곱의 평균
            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 + (1-self.momentum) * var            
        else:
            # 학습이 아닌 경우(추론시)>> 학습된 running_mean과 running_var로 정규화
            # 입력값 - 평균값 
            xc = x - self.running_mean
             # 분산의 제곱의 제곱 근
            xn = xc / ((np.sqrt(self.running_var + 10e-7)))
        # 결과값    
        out = self.gamma * xn + self.beta 
        return out

 

4. Dropout

 드롭아웃 기법은 학습할 때, 임의로 뉴런을 선택하고 비활성화해 신호를 전달하지 않게 하여서 오버피팅을 막는 기술이다.

https://kh-kim.github.io/nlp_with_deep_learning_blog/docs/1-14-regularizations/04-dropout/

 

코드는 다음과 같다. 입력크기와 같은 0~1 값을 만들어서 dropout_ratio보다 작으면 0으로 그 노드를 끄는 방식으로 작동한다.(그냥 대략 dropout_ratio만큼 꺼진다고 가정한다.) 추론할 때는 입력 데이터에 학습 때 사용한 데이터의 비율을 곱한다. 왜냐하면 학습 시에는 (1.0 - self.dropout_ratio)만큼의 데이터가 사용되는데, 추론 시에는 1 그대로 데이터가 사용된다. 따라서 훈련 시 출력값이 추론 시 보다 작게 된다. 따라서 이 비율을 맞추기 위하여 (1.0 - self.dropout_ratio)를 곱하여 사용한다.

class Dropout:
    """
    http://arxiv.org/abs/1207.0580
    """
    def __init__(self, dropout_ratio=0.5):
        self.dropout_ratio = dropout_ratio
        self.mask = None

    def forward(self, x, train_flg=True):
        # 훈련할때
        if train_flg:
            self.mask = np.random.rand(*x.shape) > self.dropout_ratio
            return x * self.mask
        else:
        # 테스트 할때 출력층의 개수 차이로 인한 오차를 줄이기 위해 사용
            return x * (1.0 - self.dropout_ratio)

    def backward(self, dout):
        return dout * self.mask

 

 

4. Hyperparameter optimization

 하이퍼 파라미터를 경험적으로 최적화하기 위해서는 검증용 데이터를 사용한다. 검증용 데이터는 테스트 데이터와는 별개로 훈련데이터에서 일정비율 나누어서 훈련사이에 검증을 하기 위해 사용한다.(테스트를 위한 테스트 용도)

데이터의 형태

 

 Learning_skill.hyperparameter_optimization.py의 예제를 보겠다. 예제에서는 검증용 데이터를 나누고 일정 가중치 감소 계수의 범위와 학습률의 범위를 나누어서 값을 무작위로 일정범위 내에서 바꾼다. 그 후 검증용 데이터를 통하여 확인하여 잘 학습이 된 case를 알아내는 예제이다.

...
# 20%를 검증 데이터로 분할
validation_rate = 0.20
validation_num = int(x_train.shape[0] * validation_rate)
x_train, t_train = shuffle_dataset(x_train, t_train)
...

# 하이퍼파라미터 무작위 탐색======================================
optimization_trial = 100
results_val = {}
results_train = {}

for _ in range(optimization_trial):
    # 탐색한 하이퍼파라미터의 범위 지정===============
    weight_decay = 10 ** np.random.uniform(-8, -4)
    lr = 10 ** np.random.uniform(-6, -2)
    # ================================================

    val_acc_list, train_acc_list = __train(lr, weight_decay)
    print("val acc:" + str(val_acc_list[-1]) + " | lr:" + str(lr) + ", weight decay:" + str(weight_decay))
    key = "lr:" + str(lr) + ", weight decay:" + str(weight_decay)
    results_val[key] = val_acc_list
    results_train[key] = train_acc_list
...
# 결과(검증데이터의 정확도)를 정렬후 사용 >> 가장 큰게 best
for key, val_acc_list in sorted(results_val.items(), key=lambda x:x[1][-1], reverse=True):
    print("Best-" + str(i+1) + "(val acc:" + str(val_acc_list[-1]) + ") | " + key)
    
...

 

예시 결과 (점선 검증데이터, 실선 훈련데이터)