06.4 바른 학습을 위해

기계학습에서는 오버피팅이 문제가 되는 일이 많습니다. 오버피팅이란 신경망이 훈련 데이터에만 지나치게 적응되어 그 외의 데이터에는 제대로 대응하지 못하는 상태를 말합니다. 기계학습은 범용 성능을 지향하죠. 훈련 데이터에는 포함되지 않는, 아직 보지 못한 데이터가 주어져도 바르게 식별해내는 모델이 바람직합니다. 복잡하고 표현력이 높은 모델을 만들 수는 있지만, 그만큼 오버피팅을 억제하는 기술이 중요해지는 것입니다.

6.4.1 오버피팅

오버피팅은 주로 다음의 두 경우에 일어납니다.

  • 매개변수가 많고 표현력이 높은 모델
  • 훈련 데이터가 적음

이번 절에서는 이 두 요건을 일부러 충족하여 오버피팅을 일으켜보겠습니다. 그러기 위해 원래 60,000개인 MNIST 데이터셋의 훈련 데이터 중 300개만 사용하고, 7층 네트워크를 사용해 네트워크의 복잡성을 높이겠습니다. 각 층의 뉴런은 100개, 활성화 함수는 ReLU를 사용합니다.

여기에서는 실험에 필요한 코드를 발췌해 설명하겠습니다(전체 파일은 ch06/overfit_weight_decay.py입니다). 우선 데이터를 읽는 코드부터 볼까요?

(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)
# 오버피팅을 재현하기 위해 학습 데이터 수를 줄임
x_train = x_train[:300]
t_train = t_train[:300]

이어서 훈련을 수행하는 코드입니다. 지금까지의 코드와 같지만 에폭마다 모든 훈련 데이터와 모든 시험 데이터 각각에서 정확도를 산출합니다.

network = MultiLayerNet(input_size=784, hidden_size_list=[100, 100, 100, 100, 100, 100], output_size=10)
optimizer = SGD(lr=0.01) # 학습률이 0.01인 SGD로 매개변수 갱신
max_epochs = 201
train_size = x_train.shape[0]
batch_size = 100

train_loss_list = []
train_acc_list = []
test_acc_list = []

iter_per_epoch = max(train_size / batch_size, 1)
epoch_cnt = 0

for i in range(10000000000):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
    
    grads = network.gradient(x_batch, t_batch)
    optimizer.update(network.params, grads)
    
    if i % iter_per_epoch == 0:
        train_acc = network.accuracy(x_train, t_train)
        test_acc = network.accuracy(x_test, t_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        
        epoch_cnt += 1
        if epoch_cnt >= max_epochs:
            break

train_acc_listtest_acc_list에는 에폭 단위(모든 훈련 데이터를 한 번씩 본 단위)의 정확도를 저장합니다. 이 두 리스트를 그래프로 그리면 [그림 6-20]처럼 됩니다.

그림 6-20 훈련 데이터(train)와 시험 데이터(test)의 에폭별 정확도 추이

그림 6-20

훈련 데이터를 사용하여 측정한 정확도는 100 에폭을 지나는 무렵부터 거의 100%입니다. 그러나 시험 데이터에 대해서는 큰 차이를 보입니다. 이처럼 정확도가 크게 벌어지는 것은 훈련 데이터에만 적응${}^{\text{fitting}}$해버린 결과입니다. 훈련 때 사용하지 않은 범용 데이터(시험 데이터)에는 제대로 대응하지 못하는 것을 이 그래프에서 확인할 수 있습니다.

오버피팅 암기 교육의 비극 도로시가 훈련 데이터 300개의 정답 노트를 통째로 완벽하게 암기해 훈련 시험은 100점을 받았지만, 처음 마주하는 실전 문제(시험 데이터)에서는 쩔쩔매며 오답을 고르는 모습을 토토가 땀을 흘리며 걱정하고 있어요.

도로시의 오버피팅 좌절

  • 도로시: “지니! 내가 훈련용 문제들을 완벽하게 다 외워서 훈련 점수는 100%인데, 처음 보는 시험 문제(테스트 데이터)들은 절반도 못 풀겠어! 왜 이렇게 유연성이 떨어지는 걸까?”
  • 지니: “그게 바로 오버피팅(Overfitting, 과적합)이란다. 데이터 수는 너무 적은데 모델의 표현력(파라미터 수)이 너무 높으면, 보편적인 원리를 깨닫는 대신 훈련 데이터 자체를 통째로 암기해 버리기 때문이지.”

6.4.2 가중치 감소

오버피팅 억제용으로 예로부터 많이 이용해온 방법 중 가중치 감소${}^{\text{weight decay}}$라는 것이 있습니다. 이는 학습 과정에서 큰 가중치에 대해서는 그에 상응하는 큰 페널티를 부과하여 오버피팅을 억제하는 방법입니다. 원래 오버피팅은 가중치 매개변수의 값이 커서 발생하는 경우가 많기 때문이죠.

자, 복습해봅시다. 신경망 학습의 목적은 손실 함수의 값을 줄이는 것입니다. 이때, 예를 들어 가중치의 제곱 법칙(L2 법칙)을 손실 함수에 더합니다. 그러면 가중치가 커지는 것을 억제할 수 있죠. 가중치를 $\mathbf{W}$라 하면 L2 법칙에 따른 가중치 감소는 $\frac{1}{2}\lambda\mathbf{W}^2$이 되고, 이 $\frac{1}{2}\lambda\mathbf{W}^2$을 손실 함수에 더합니다. 여기에서 $\lambda$${}^{\text{람다}}$는 정규화의 세기를 조절하는 하이퍼파라미터입니다. $\lambda$를 크게 설정할수록 큰 가중치에 대한 페널티가 커집니다. 또 $\frac{1}{2}\lambda\mathbf{W}^2$의 앞쪽 $\frac{1}{2}$은 $\frac{1}{2}\lambda\mathbf{W}^2$의 미분 결과인 $\lambda\mathbf{W}$를 조정하는 역할의 상수입니다.

가중치 감소는 모든 가중치 각각의 손실 함수에 $\frac{1}{2}\lambda\mathbf{W}^2$을 더합니다. 따라서 가중치의 기울기를 구하는 계산에서는 그동안의 오차역전파법에 따른 결과에 정규화 항을 미분한 $\lambda\mathbf{W}$를 더합니다.

NOTE_ L2 법칙은 각 원소의 제곱들을 더한 것에 해당합니다. 가중치 $\mathbf{W} = (w_1, w_2, \dots, w_n)$이 있다면, L2 법칙에서는 $\sqrt{w_1^2 + w_2^2 + \dots + w_n^2}$으로 계산할 수 있습니다. L2 법칙 외에 L1 법칙과 $L_{\infty}$ 법칙도 있습니다. L1 법칙은 절댓값의 합, 즉 $ w_1 + w_2 + \dots + w_n $에 해당합니다. $L_{\infty}$ 법칙은 Max 법칙이라고도 하며, 각 원소의 절댓값 중 가장 큰 것에 해당합니다. 정규화 항으로 L2 법칙, L1 법칙, $L_{\infty}$ 법칙 중 어떤 것도 사용할 수 있습니다. 각자 특징이 있는데, 이 책에서는 일반적으로 자주 쓰는 L2 법칙만 구현합니다.

그럼 실험을 해봅시다. 방금 수행한 실험에서 $\lambda = 0.1$로 가중치 감소를 적용합니다. 결과는 [그림 6-21]과 같습니다(가중치 감소를 적용한 네트워크는 common/multi_layer_net.py에, 실험용 코드는 ch06/overfit_weight_decay.py에 있습니다).

그림 6-21 가중치 감소를 이용한 훈련 데이터(train)와 시험 데이터(test)에 대한 정확도 추이

그림 6-21

[그림 6-21]과 같이 훈련 데이터에 대한 정확도와 시험 데이터에 대한 정확도에는 여전히 차이가 있지만, 가중치 감소를 이용하지 않은 [그림 6-20]과 비교하면 그 차이가 줄었습니다. 다시 말해 오버피팅이 억제됐다는 소리입니다. 그리고 앞서와 달리 훈련 데이터에 대한 정확도가 100%(1.0)에 도달하지 못한 점도 주목해야 하겠습니다.

가중치 감소 페널티 적용 지니가 너무 커져서 오버피팅의 주범이 된 가중치 알갱이 무리에 억제용 고무 밴드($\frac{1}{2}\lambda\mathbf{W}^2$)를 타이트하게 묶어 크기를 제한하고 있어요.

지니의 L2 패널티 특약 (가중치 감소)

  • 지니: “도로시, 가중치 값들이 과도하게 비대해지면 오버피팅이 쉽게 일어나. 그래서 손실 함수에 가중치의 크기(L2 노름)를 더해주어 가중치들이 너무 커지지 않도록 패널티를 주는 가중치 감소(Weight Decay) 규제를 가하는 거야.”
  • 도로시: “아! 가중치를 적당한 크기로 꽉 묶어서 통제하니까, 특정 특징에만 비정상적으로 집중하지 않고 적당히 둥글둥글하고 균형 잡힌 모델이 만들어지는구나!”

6.4.3 드롭아웃

앞 절에서는 오버피팅을 억제하는 방식으로 손실 함수에 가중치의 L2 법칙을 더한 가중치 감소 방법을 설명했습니다. 가중치 감소는 간단하게 구현할 수 있고 어느 정도 지나친 학습을 억제할 수 있습니다. 그러나 신경망 모델이 복잡해지면 가중치 감소만으로는 대응하기 어려워집니다. 이럴 때는 흔히 드롭아웃${}^{\text{Dropout}}$[14]이라는 기법을 이용합니다.

드롭아웃은 뉴런을 임의로 삭제하면서 학습하는 방법입니다. 훈련 때 은닉층의 뉴런을 무작위로 골라 삭제합니다. 삭제된 뉴런은 [그림 6-22]와 같이 신호를 전달하지 않게 됩니다. 훈련 때는 데이터를 흘릴 때마다 삭제할 뉴런을 무작위로 선택하고, 시험 때는 모든 뉴런에 신호를 전달합니다. 단, 시험 때는 각 뉴런의 출력에 훈련 때 삭제한 비율을 곱하여 출력합니다.

드롭아웃 임의 뉴런 차단 지니가 마법봉을 휘둘러 훈련 단계에서 중간 은닉층의 특정 뉴런들을 임의로 감추며(x 표시) 역전파 및 순전파 신호를 무작위 차단하는 훈련 시범을 보이고 있어요.

지니의 뉴런 독립성 훈련 (Dropout)

  • 지니: “가중치 감소만으로 억제하기 힘든 거대 신경망에서는, 학습할 때마다 무작위로 뉴런 연결을 탈락시키는 드롭아웃(Dropout)을 사용해. 특정 뉴런에만 전적으로 의존하는 동맹(Co-adaptation) 현상을 부수는 거지!”
  • 도로시: “와! 매번 무작위로 다른 조합의 뉴런이 협동해서 시험을 치르게 하니까, 뉴런들이 각자 알아서 강인해지고 마치 여러 개의 신경망을 앙상블 학습시키는 것 같은 놀라운 효과가 나네!”
  • 토토: “왈왈! (X자 뉴런들을 보며) 팀워크 훈련 완료다 멍!”

그림 6-22 드롭아웃의 개념(문헌[14]에서 인용) : 왼쪽이 일반적인 신경망, 오른쪽이 드롭아웃을 적용한 신경망. 드롭아웃은 뉴런을 무작위로 선택해 삭제하여 신호 전달을 차단한다.

그림 6-22

이제 드롭아웃을 구현할 차례입니다. 다음 코드는 되도록 이해하기 쉽게 구현한 것입니다. 순전파를 담당하는 forward 메서드에서는 훈련 때(train_flg = True일 때)만 잘 계산해두면 시험 때는 단순히 데이터를 흘리기만 하면 됩니다. 삭제한 비율은 곱하지 않아도 좋습니다. 실제 딥러닝 프레임워크들도 비율을 곱하지 않습니다. 더 효율적인 구현이 궁금하면 체이너${}^{\text{Chainer}}$ 프레임워크(http://chainer.org/)의 드롭아웃 구현을 참고하면 좋을 겁니다.

class Dropout:
    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

여기에서의 핵심은 훈련 시에는 순전파 때마다 self.mask에 삭제할 뉴런을 False로 표시한다는 것입니다. self.maskx와 형상이 같은 배열을 무작위로 생성하고, 그 값이 dropout_ratio보다 큰 원소만 True로 설정합니다. 역전파 때의 동작은 ReLU와 같습니다. 즉, 순전파 때 신호를 통과시키는 뉴런은 역전파 때도 신호를 그대로 통과시키고, 순전파 때 통과시키지 않은 뉴런은 역전파 때도 신호를 차단합니다.

그럼 드롭아웃의 효과를 MNIST 데이터셋으로 확인해보겠습니다. 소스 코드는 ch06/overfit_dropout.py입니다. 참고로, 소스 코드에서는 Trainer라는 클래스를 이용하여 구현을 간소화했습니다.

NOTE_ common/trainer.py에는 Trainer라는 클래스를 구현해두었습니다. 이 클래스는 지금까지 해온 것과 같은 네트워크 학습을 대신 해줍니다. 자세한 내용은 common/trainer.pych06/overfit_dropout.py를 참고하세요.

자, 드롭아웃 실험은 앞의 실험과 마찬가지로 7층 네트워크(각 층의 뉴런 수는 100개, 활성화 함수는 ReLU)를 써서 진행했습니다. 결과는 [그림 6-23]과 같습니다.

그림 6-23 왼쪽은 드롭아웃 없이, 오른쪽은 드롭아웃을 적용한 결과(dropout_ratio = 0.15)

그림 6-23

그림과 같이 드롭아웃을 적용하니 훈련 데이터와 시험 데이터에 대한 정확도 차이가 줄었습니다. 또, 훈련 데이터에 대한 정확도가 100%에 도달하지도 않게 되었습니다. 이처럼 드롭아웃을 이용하면 표현력을 높이면서도 오버피팅을 억제할 수 있습니다.

NOTE_ 기계학습에서는 앙상블 학습${}^{\text{ensemble learning}}$을 애용합니다. 앙상블 학습은 개별적으로 학습시킨 여러 모델의 출력을 평균 내어 추론하는 방식입니다.* 신경망의 맥락에서 얘기하면, 가령 같은 (혹은 비슷한) 구조의 네트워크를 5개 준비하여 따로따로 학습시키고, 시험 때는 그 5개의 출력을 평균 내어 답하는 것이죠. 앙상블 학습을 수행하면 신경망의 정확도가 몇% 정도 개선된다는 것이 실험적으로 알려져 있습니다. 앙상블 학습은 드롭아웃과 밀접합니다. 드롭아웃이 학습 때 뉴런을 무작위로 삭제하는 행위를 매번 다른 모델을 학습시키는 것으로 해석할 수 있기 때문이죠. 그리고 추론 때는 뉴런의 출력에 삭제한 비율(이를테면 0.5 등)을 곱함으로써 앙상블 학습에서 여러 모델의 평균을 내는 것과 같은 효과를 얻는 것이죠. 즉, 드롭아웃은 앙상블 학습과 같은 효과를 (대략) 하나의 네트워크로 구현했다고 생각할 수 있습니다.


* 옮긴이_ 평균 말고도 투표(voting) 등 다른 방법도 사용합니다.

서브목차