7.3 모델의 성능을 최대로 끌어올리기

이 절에서는 작동하는 수준을 넘어 아주 잘 작동하고 머신 러닝 경연 대회에서 우승하는 모델을 만들기 위해 꼭 알아야 할 기법을 소개하겠습니다. 이런 기법을 사용하면 최고의 딥러닝 모델을 만들 수 있을 것입니다.

7.3.1 고급 구조 패턴

이전 절에서 중요한 디자인 패턴 하나를 자세히 소개했습니다. 잔차 연결입니다. 이외에도 꼭 알아야 할 디자인 패턴이 2개 더 있습니다. 정규화와 깊이별 분리 합성곱입니다. 이 패턴은 특히 고성능 심층 컨브넷을 만들 때 유용합니다. 하지만 보통 다른 종류의 구조에서도 많이 등장합니다.

배치 정규화

정규화(normalization)는 머신 러닝 모델에 주입되는 샘플들을 균일하게 만드는 광범위한 방법입니다. 이 방법은 모델이 학습하고 새로운 데이터에 잘 일반화되도록 돕습니다. 이 책에서 여러 번 나온것처럼 데이터에서 평균을 빼서 데이터를 원점에 맞추고 표준 편차로 나누어 데이터의 분산을 1로 만듭니다. 데이터가 정규 분포(가우시안 분포)를 따른다 가정하고 이 분포를 원점에 맞추고 분산이 1이 되도록 조정한 것입니다.

normalized_data = (data - np.mean(data, axis=...)) / np.std(data, axis=...)

이전 예제는 모델에 데이터를 주입하기 전에 입력에 대해 정규화했습니다. 하지만 데이터 정규화는 네트워크에서 일어나는 모든 변환 후에도 고려되어야 합니다. DenseConv2D 층에 들어가는 데이터의 평균이 0이고 분산이 1이더라도 출력되는 데이터가 동일한 분포를 가질 것이라고 기대하기 어렵습니다.

배치 정규화(batch normalization)는 2015년 아이오페와 세게디가 제안한 층의 한 종류입니다(케라스는 BatchNormalization 클래스로 제공합니다)1. 훈련하는 동안 평균과 분산이 바뀌더라도 이에 적응하여 데이터를 정규화합니다. 훈련 과정에 사용된 배치 데이터의 평균과 분산에 대한 지수 이동 평균(exponential moving average)을 내부에 유지합니다.2 배치 정규화의 주요 효과는 잔차 연결과 매우 흡사하게 그래디언트의 전파를 도와주는 것입니다.3 결국 더 깊은 네트워크를 구성할 수 있습니다. 매우 깊은 네트워크라면 여러 개의 BatchNormalization 층을 포함해야 훈련할 수 있습니다. 예를 들어 케라스에 포함된 고급 컨브넷 구조는 BatchNormalization 층을 많이 사용합니다. 여기에는 ResNet50, 인셉션 V3, 엑셉션 등이 있습니다.

BatchNormalization 층은 일반적으로 합성곱이나 완전 연결 층 다음에 사용합니다.

# Conv2D 층 다음에
conv_model.add(layers.Conv2D(32, 3, activation='relu'))
conv_model.add(layers.BatchNormalization())

# Dense 층 다음에
dense_model.add(layers.Dense(32, activation='relu'))
dense_model.add(layers.BatchNormalization())

BatchNormalization 클래스에는 정규화할 특성 축을 지정하는 axis 매개변수가 있습니다. 이 매개변수의 기본값은 입력 텐서의 마지막 축을 나타내는 -1입니다. data_format"channels_last"로 하여 Dense, Conv1D, RNN, Conv2D 층을 사용할 때는 맞는 값입니다. 하지만 data_format"channels_first"로 사용하는 경우에는 특성 축이 1입니다.4 이때는 BatchNormalization의 axis 매개변수는 1이 되어야 합니다.


Note 배치 재정규화

배치 정규화의 최근 발전은 2017년 아이오페가 소개한 배치 재정규화(batch renormalization)입니다.5 이 방법은 추가적인 비용을 들이지 않고 배치 정규화보다 이득이 많습니다. 이 책을 쓸 시점에는 배치 정규화를 대체한다고 말하기 이를지 모르지만 가능성이 높습니다. 조금 더 최근에는 클람바우어(Klambauer) 등이 자기 정규화 신경망(self-normalizing neural networks)을 발표했습니다.6 특정 활성화 함수(selu)와 초기화 방법(lecun_normal)7을 사용하여 Dense 층의 출력을 정규화합니다. 이 방법이 흥미롭기는 하지만 지금은 완전 연결 네트워크에만 제한되어 있어 널리 사용되지 못하고 있습니다.


깊이별 분리 합성곱

깊이별 분리 합성곱은 Conv2D를 대체하면서 더 가볍고(훈련할 모델 파라미터가 더 적고) 더 빨라(부동 소수 연산이 더 적고) 모델의 성능을 몇 퍼센트 포인트 높일 수 있는 층입니다(케라스에서 SeparableConv2D). 이 층은 입력 채널별로 따로따로 공간 방향의 합성곱을 수행합니다. 그다음 그림 7-16과 같이 점별 합성곱(1×1 합성곱)을 통해 출력 채널을 합칩니다. 이는 공간 특성의 학습과 채널 방향 특성의 학습을 분리하는 효과를 냅니다. 입력에서 공간상 위치는 상관관계가 크지만 채널별로는 매우 독립적이라고 가정한다면 타당합니다. 이 방법은 모델 파라미터와 연산의 수를 크게 줄여 주기 때문에 작고 더 빠른 모델을 만듭니다. 합성곱을 통해 더 효율적으로 표현을 학습하기 때문에 적은 데이터로도 더 좋은 표현을 학습하고, 결국 성능이 더 높은 모델을 만듭니다.

이 장점은 제한된 데이터로 작은 모델을 처음부터 훈련시킬 때 특히 더 중요합니다. 다음은 작은 데이터셋에서 이미지 분류 문제(소프트맥스 분류)를 위한 가벼운 깊이별 분리 컨브넷을 만드는 예입니다.

나타낼 수 없음
그림 7-16. 깊이별 분리 합성곱: 깊이별 합성곱 다음에 점별 합성곱이 뒤따른다

from keras.models import Sequential, Model
from keras import layers

height = 64
width = 64
channels = 3
num_classes = 10

model = Sequential()
model.add(layers.SeparableConv2D(32, 3,
                                 activation='relu',
                                 input_shape=(height, width, channels,)))
model.add(layers.SeparableConv2D(64, 3, activation='relu'))
model.add(layers.MaxPooling2D(2))

model.add(layers.SeparableConv2D(64, 3, activation='relu'))
model.add(layers.SeparableConv2D(128, 3, activation='relu'))
model.add(layers.MaxPooling2D(2))

model.add(layers.SeparableConv2D(64, 3, activation='relu'))
model.add(layers.SeparableConv2D(128, 3, activation='relu'))
model.add(layers.GlobalAveragePooling2D())

model.add(layers.Dense(32, activation='relu'))
model.add(layers.Dense(num_classes, activation='softmax'))

model.compile(optimizer='rmsprop', loss='categorical_crossentropy')

대규모 모델에 적용된 사례로는 케라스에 포함된 엑셉션에서 깊이별 분리 합성곱이 사용되었습니다. 필자 논문 “Xception: Deep Learning with Depthwise Separable Convolutions.”8에서 깊이별 분리 합성곱에 대한 좀 더 자세한 이론적 배경을 읽을 수 있습니다.

7.3.2 하이퍼파라미터 최적화

딥러닝 모델을 만들 때는 다음과 같이 무작위로 보이는 결정들을 해야만 합니다.

  • 얼마나 많은 층을 쌓아야 할까요?
  • 층마다 얼마나 많은 유닛이나 필터를 두어야 할까요?
  • relu 활성화 함수를 사용해야 할까요?
  • 아니면 다른 함수를 사용해야 할까요?
  • 어떤 층 뒤에 BatchNormalization을 사용해야 할까요?
  • 드롭아웃은 얼마나 해야 할까요?

이런 구조에 관련된 파라미터를 역전파로 훈련되는 모델 파라미터와 구분하여 하이퍼파라미터(hyperparameter)라고 부릅니다.

실제로 경험 많은 머신 러닝 엔지니어와 연구자는 하이퍼파라미터에 따라 작동하는 것과 작동하지 않는 것에 대한 직관 즉, 하이퍼파라미터 튜닝에 관한 기술을 가지고 있습니다. 하지만 공식적인 규칙은 없습니다. 옵션을 수정하고 모델을 반복적으로 다시 훈련하여 선택 사항을 개선해야 합니다. 이것이 머신 러닝 엔지니어와 연구자들이 대부분의 시간을 쓰는 일입니다.

하지만 하루 종일 하이퍼파라미터를 사람이 수정하기 어렵기 때문에 하나의 연구분야로 하이퍼파라미터 자동 최적화가 만들어집니다. 이는 가능한 결정 공간을 자동적, 조직적, 규칙적 방법으로 탐색해야 하고, 가능성 있는 구조를 탐색해서 실제 가장 높은 성능을 내는 것을 찾아야 합니다.

전형적인 하이퍼파라미터 최적화 과정은 다음과 같습니다.

  1. 일련의 하이퍼파라미터를 (자동으로) 선택합니다.
  2. 선택된 하이퍼파라미터로 모델을 만듭니다.
  3. 훈련 데이터에 학습하고 검증 데이터에서 최종 성능을 측정합니다.
  4. 다음으로 시도할 하이퍼파라미터를 (자동으로) 선택합니다.
  5. 이 과정을 반복합니다.
  6. 마지막으로 테스트 데이터에서 성능을 측정합니다.

주어진 하이퍼파라미터에서 얻은 검증 성능을 사용하여 다음 번에 시도할 하이퍼파라미터를 선택하는 알고리즘이 이 과정의 핵심입니다. 여러 가지 기법을 사용할 수 있습니다. 베이지안 최적화(bayesian optimization), 유전 알고리즘(genetic algorithms), 간단한 랜덤 탐색(random search) 등입니다.

모델의 가중치를 훈련하는 것은 비교적 쉽습니다. 반면에 하이퍼파라미터를 업데이트하는 것은 매우 어려운 일입니다. 다음을 생각해 보죠.

  • 피드백 신호를 계산하는 것은 매우 비용이 많이 듭니다(이 하이퍼파라미터가 성능이 높은 모델을 만들어 낼까요?). 새로운 모델을 만들고 데이터셋을 사용하여 처음부터 다시 훈련해야 합니다.
  • 하이퍼파라미터 공간은 일반적으로 분리되어 있는 결정들로 채워집니다. 즉 연속적이지 않고 미분 가능하지 않습니다. 그러므로 하이퍼파라미터 공간에 경사 하강법을 사용할 수 없습니다. 그 대신 경사 하강법보다 훨씬 비효율적인 그래디언트-프리(gradient-free) 최적화 기법을 사용해야 합니다.

이 분야가 어렵고 아직 초창기이기 때문에 모델 최적화에 사용할 도구가 매우 적습니다. 가장 단순하지만 종종 랜덤 탐색(반복적으로 랜덤하게 하이퍼파라미터를 선택합니다)이 제일 좋은 방법일 때가 많습니다.9 랜덤 탐색보다 더 나은 도구는 하이퍼파라미터 최적화를 위한 파이썬 라이브러리인 Hyperopt입니다. 이 라이브러리는 잘 작동할 것 같은 하이퍼파라미터 조합을 예측하기 위해 내부적으로 Parzen 트리 추정기를 사용합니다.10 다른 라이브러리로 HyperasHyperopt와 연동하여 케라스 모델에 사용할 수 있습니다.


Note 검증 세트로의 과대적합

대규모로 자동화하여 하이퍼파라미터 최적화를 진행할 때 기억해야 할 중요한 이슈는 검증 세트의 과대적합입니다. 검증 데이터에서 계산한 신호를 바탕으로 하이퍼파라미터를 업데이트하기 때문에 검증 데이터에 맞추어 모델을 훈련하는 효과가 생깁니다. 따라서 모델이 빠르게 검증 데이터에 과대적합될 것입니다. 이 점을 항상 기억하세요.

전체적으로 보았을 때 하이퍼파라미터 최적화는 어느 작업에서 최고의 모델을 얻거나 머신 러닝 경연 대회에서 우승하기 위한 강력한 도구입니다. 다음을 생각해 보죠. 오래 전에는 사람들이 얕은 머신 러닝 모델에 넣을 특성을 직접 만들었습니다. 이는 매우 최적화되지 않은 방법입니다. 요즘에는 딥러닝이 계층적인 특성 엔지니어링 작업을 자동화합니다. 수작업이 아니라 피드백 신호를 사용하여 특성이 학습됩니다. 어찌 보면 당연한 일입니다. 같은 식으로 수작업으로 모델 구조를 만드는 것이 아니라 이론에 근거하여 모델 구조를 최적화해야 합니다. 이 글을 쓰는 시점에는 하이퍼파라미터 자동 최적화 분야는 매우 초기 단계이고 미성숙합니다. 딥러닝도 수년 전에는 그랬습니다. 하지만 다음 몇 년 동안 크게 성장할 것으로 기대합니다.


7.3.3 모델 앙상블

모델 앙상블(model ensemble)은 여러 개 다른 모델의 예측을 합쳐서 더 좋은 예측 결과를 얻을 수 있는 또 다른 강력한 기법입니다. 캐글 같은 머신 러닝 경연 대회에서는 우승자들이 대규모 모델 앙상블을 사용합니다. 이런 앙상블은 아주 뛰어난 단일 모델보다도 성능이 좋습니다.

앙상블은 독립적으로 훈련된 다른 종류의 좋은 모델이 각기 다른 장점을 가지고 있다는 가정을 바탕으로 합니다. 각 모델은 예측을 만들기 위해 조금씩 다른 측면을 바라봅니다. 데이터의 모든 면이 아니고 부분 특징입니다.

분류 예를 들어 보죠. 분류기 예측을 (앙상블하기 위해) 합치는 가장 쉬운 방법은 추론할 때 나온 예측을 평균 내는 것입니다.

# 4개의 다른 모델을 사용하여 초기 예측을 계산합니다.
preds_a = model_a.predict(x_val)
preds_b = model_b.predict(x_val)
preds_c = model_c.predict(x_val)
preds_d = model_d.predict(x_val)

# 새로운 예측은 어떤 초기 예측보다 더 정확해야 합니다.
final_preds = 0.25 * (preds_a + preds_b + preds_c + preds_d)

이 방식은 분류기들이 어느 정도 비슷하게 좋을 때 잘 작동합니다. 분류기 중 하나가 다른 모델보다 월등히 나쁘면 최종 예측은 앙상블에 있는 가장 좋은 분류기만큼 좋지 않을 수 있습니다.

분류기를 앙상블하는 좋은 방법은 검증 데이터에서 학습된 가중치를 사용하여 가중 평균하는 것입니다. 전형적으로 분류기가 좋을수록 높은 가중치를 가지고 나쁜 분류기일수록 낮은 가중치를 갖습니다. 좋은 앙상블 가중치를 찾기 위해 랜덤 서치나 넬더-미드(Nelder-Mead) 방법11 같은 간단한 최적화 알고리즘을 사용할 수 있습니다.

preds_a = model_a.predict(x_val)
preds_b = model_b.predict(x_val)
preds_c = model_c.predict(x_val)
preds_d = model_d.predict(x_val)

# 가중치 (0.5, 0.25, 0.1, 0.15)는 경험적으로 학습되었다고 가정합니다.
final_preds = 0.5 * preds_a + 0.25 * preds_b + 0.1 * preds_c + 0.15 * preds_d

이외에도 여러 가지 변종이 있습니다. 예를 들어 예측의 지수 값을 평균할 수 있습니다. 일반적으로 검증 데이터에서 찾은 최적의 가중치로 단순하게 가중 평균하는 방법이 좋은 기본값입니다.

앙상블이 잘 작동하게 만드는 핵심은 분류기의 다양성입니다. 모든 모델이 같은 방향으로 편향되어 있다면 앙상블은 동일한 편향을 유지할 것입니다. 모델이 서로 다른 방향으로 편향되어 있다면 편향은 서로 상쇄되고 앙상블이 더 견고하고 정확해질 것입니다.

이런 이유 때문에 가능한 최대한 다르면서 좋은 모델을 앙상블해야 합니다. 일반적으로 매우 다른 구조를 가지거나 다른 종류의 머신 러닝 방법을 말합니다. 같은 네트워크를 랜덤 초기화를 다르게 하여 따로따로 여러 번 훈련해서 앙상블하는 것은 거의 가치가 없습니다. 모델 사이 차이점이 랜덤 초기화와 모델에 주입되는 훈련 데이터의 순서라면 이 앙상블은 다양성이 낮고 하나의 모델보다 아주 조금 성능이 향상될 것입니다.

모든 문제에 적용하지는 못하지만 실전에서 잘 동작하는 한 가지 방법은 트리 기반 모델(랜덤 포레스트나 그래디언트 부스팅 트리)이나 심층 신경망을 앙상블하는 것입니다. 2014년에 안드레이 콜레프(Andrei Kolev)와 필자는 캐글의 힉스 보손 붕괴(Higgs Boson decay) 감지 대회(https://www.kaggle.com/c/higgs-boson)에서 여러 가지 트리 모델과 심층 신경망을 사용하여 4위를 했습니다. 특별하게 앙상블 모델 중 하나는 다른 방법을 사용해서 만들었습니다(RGF(Regularized Greedy Forest) 모델이었습니다). 이 모델은 다른 모델보다 눈에 띄게 나빴습니다. 당연히 앙상블에서 낮은 가중치를 할당했습니다. 하지만 놀랍게도 이 모델이 전체 앙상블의 성능을 크게 향상시켰습니다. 이 모델이 다른 모델과 매우 달라 다른 모델이 가지지 못한 정보를 제공했기 때문입니다. 이것이 앙상블의 핵심입니다. 최상의 모델이 얼마나 좋은지보다 앙상블의 후보 모델이 얼마나 다양한지가 중요합니다.

최근에 실전에서 매우 성공적으로 사용되는 기본 앙상블 스타일은 딥러닝과 얕은 모델을 섞은 넓고 깊은(wide and deep) 모델입니다. 이런 모델은 심층 신경망과 많은 선형 모델을 함께 훈련합니다. 다양한 종류의 모델들을 함께 훈련하는 것은 모델 앙상블을 만드는 또 다른 방법입니다.

7.3.4 정리

  • 고성능 심층 컨브넷을 만들려면 잔차 연결, 배치 정규화, 깊이별 분리 합성곱을 사용해야 합니다. 미래에는 깊이별 분리 합성곱이 일반적인 합성곱을 완전히 대체할 것입니다. 애플리케이션이 1D나 2D 또는 3D인지와 상관없이 아주 효율적으로 표현을 학습하기 때문입니다.
  • 심층 네트워크를 만들 때 많은 하이퍼파라미터와 네트워크 구조를 선택해야 합니다. 이 값들이 모여 모델의 성능을 결정합니다. 이런 선택을 직관이나 랜덤한 선택에 의존하지 않고 최적의 선택을 찾기 위해 하이퍼파라미터 공간을 조직적으로 탐색하는 것이 좋습니다. 현재는 이 과정에 비용이 많이 들고 좋은 도구도 없습니다. 하지만 HyperoptHyperas 라이브러리가 도움이 될 수 있습니다. 하이퍼파라미터 최적화를 할 때 검증 세트에 과대적합된다는 것을 잊지 마세요!
  • 머신 러닝 경연 대회에서 우승하거나 어떤 문제에서 최상의 결과를 얻으려면 대규모로 모델을 앙상블하게 됩니다. 최적화가 잘된 가중 평균으로 만든 앙상블은 보통 충분히 좋은 결과를 만듭니다. 다양성이 중요하다는 것을 기억하세요. 비슷한 모델을 앙상블하는 것은 거의 쓸모가 없습니다. 가장 좋은 앙상블은 가능한 서로 다른 모델로 만드는 것입니다(당연히 가능한 예측 성능이 높은 것 중에서 고릅니다).
  1. Sergey Ioffe and Christian Szegedy, “Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift,” Proceedings of the 32nd International Conference on Machine Learning (2015), https://arxiv.org/abs/1502.03167

  2. 배치 정규화는 입력 배치의 평균과 표준 편차를 지수 이동 평균으로 계산하여 전체 데이터셋의 평균과 표준 편차를 대신합니다. 이 값은 테스트 데이터에 배치 정규화가 적용될 때 사용됩니다. 지수 이동 평균은 3장에서 본 것처럼 v = v × momentum + v_new × (1 - momentum)으로 계산됩니다. momentum이 클수록 이전 값(v)의 관성이 크며 새로운 값(v_new)이 미치는 영향이 적습니다. 케라스의 BatchNormalization 클래스의 momentum 기본값은 0.99입니다. 

  3. 입력에 비하여 활성화 함수의 출력이 너무 작거나 커지면 변화율이 급격히 작아져 (그림 3-5의 시그모이드 함수 참고) 역전파되는 그래디언트도 매우 줄어들게 됩니다. 배치 정규화는 입력과 출력의 분포를 유지하도록 도와주므로 그래디언트가 더 잘 전파됩니다. 

  4. 0번째 축은 항상 배치 차원입니다. 

  5. Sergey Ioffe, “Batch Renormalization: Towards Reducing Minibatch Dependence in Batch-Normalized Models” (2017), https://arxiv.org/abs/1702.03275

  6. Günter Klambauer et al., “Self-Normalizing Neural Networks,” Conference on Neural Information Processing Systems (2017), https://arxiv.org/abs/1706.02515](https://arxiv.org/abs/1706.02515

  7. initializers.lecun_normal() 함수는 입력 유닛 개수의 역수에 대한 제곱근을 표준 편차로 하는 절단 정규 분포로 가중치를 초기화하는 방법입니다. 이 방법은 얀 르쿤(Yann LeCun)이 소개했습니다(https://bit.ly/2DeQC3Q). 

  8. 주석 13번을 참고하세요. 

  9. 역주 케라스의 keras.wrappers.scikit_learn 모듈 아래에 있는 KerasClassifier와 KerasRegressor 클래스를 이용하면 사이킷런의 RandomizedSearchCV를 사용하여 랜덤한 하이퍼파라미터 탐색을 수행할 수 있습니다. 

  10. Hyperopt는 베이지안 최적화 도구 중 하나로 2011년에 발표된(https://bit.ly/2Jru9Tx) Parzen 트리 추정기(Parzen Tree Estimator, PTE)를 사용합니다. 케라스를 위한 하이퍼파라미터 탐색 도구인 Auto-Keras(https://bit.ly/2MmAZfs)가 최근 공개되었습니다. 사이킷런과 함께 사용할 수 있는 대표적인 하이퍼파라미터 탐색 라이브러리로 TPOT(https://bit.ly/2LhAXVr)과 auto-sklearn(https://bit.ly/2NT2wX4)이 있습니다. 

  11. 넬더-미드 방법은 존 넬더(John Nelder)와 로저 미드(Roger Mead)가 1965년에 소개한 비선형 최적화 문제를 위한 방법으로 아메바 방법(amoeba method)이라고도 부릅니다. 이 책의 저자 프랑소와 숄레가 구현한 넬더-미드 알고리즘이 깃허브에 공개되어 있습니다(https://bit.ly/2Ll5jGR). 

댓글남기기