4.2 머신 러닝 모델 평가

3장에서 본 3개의 예제에서 데이터를 훈련 세트, 검증 세트, 테스트 세트로 나누었습니다. 훈련에 사용된 동일한 데이터로 모델을 평가하지 않는 이유는 금방 드러났습니다. 몇 번의 에포크 후에 3개의 모델이 모두 과대적합되기 시작했습니다. 즉 훈련 데이터의 성능에 비해 처음 본 데이터에 대한 성능이 좋아지지 않습니다(또는 더 나빠집니다). 반면에 훈련 데이터의 성능은 훈련이 진행될수록 항상 증가됩니다.

머신 러닝의 목표는 처음 본 데이터에서 잘 작동하는 일반화된 모델을 얻는 것입니다. 여기에서 과대적합은 주요 장애물입니다. 관측할 수 있는 것만 제어할 수 있으므로 모델의 일반화 성능에 대한 신뢰할 수 있는 측정 방법이 아주 중요합니다. 다음 절에서 과대적합을 완화하고 일반화를 최대화하기 위한 전략을 살펴보겠습니다. 이 절에서는 일반화, 즉 머신 러닝 모델의 성능을 어떻게 측정하는지에 집중합니다.

4.2.1 훈련, 검증, 테스트 세트

모델 평가의 핵심은 가용한 데이터를 항상 훈련, 검증, 테스트 3개의 세트로 나누는 것입니다. 훈련 세트에서 모델을 훈련하고 검증 세트에서 모델을 평가합니다. 모델을 출시할 준비가 되면 테스트 세트에서 최종적으로 딱 한 번 모델을 테스트합니다.

훈련 세트와 테스트 세트 2개를 사용하면 어떨까요? 훈련 세트에서 훈련하고 테스트 세트에서 평가하는 것이죠. 훨씬 간단하네요!

이렇게 하지 않는 이유는 모델을 개발할 때 항상 모델의 설정을 튜닝하기 때문입니다. 예를 들어 층의 수나 층의 유닛 수를 선택합니다(이런 파라미터를 네트워크의 가중치와 구분하기 위해 하이퍼파라미터(hyperparameter)라고 부릅니다). 검증 세트에서 모델의 성능을 평가하여 이런 튜닝을 수행합니다. 본질적으로 이런 튜닝도 어떤 파라미터 공간에서 좋은 설정을 찾는 학습입니다. 결국 검증 세트의 성능을 기반으로 모델의 설정을 튜닝하면 검증 세트로 모델을 직접 훈련하지 않더라도 빠르게 검증 세트에 과대적합될 수 있습니다.

이 현상의 핵심은 정보 누설(information leak) 개념에 있습니다. 검증 세트의 모델 성능에 기반하여 모델의 하이퍼파라미터를 조정할 때마다 검증 데이터에 관한 정보가 모델로 새는 것입니다. 하나의 파라미터에 대해서 단 한 번만 튜닝한다면 아주 적은 정보가 누설됩니다. 이런 검증 세트로는 모델을 평가할 만합니다. 하지만 한 번 튜닝하고 나서 검증 세트에 평가한 결과를 가지고 다시 모델을 조정하는 과정을 여러 번 반복하면, 검증 세트에 관한 정보를 모델에 아주 많이 노출시키게 됩니다.

결국 검증 데이터에 맞추어 최적화했기 때문에 검증 데이터에 의도적으로 잘 수행되는 모델이 만들어집니다. 검증 데이터가 아니고 완전히 새로운 데이터에 대한 성능이 관심 대상이라면 모델을 평가하기 위해 이전에 본 적 없는 완전히 다른 데이터셋을 사용해야 합니다. 바로 테스트 세트입니다. 모델은 간접적으로라도 테스트 세트에 대한 어떤 정보도 얻어서는 안 됩니다. 테스트 세트 성능에 기초하여 튜닝한 모델의 모든 설정은 일반화 성능을 왜곡시킬 것입니다.

데이터를 훈련, 검증, 테스트 세트로 나누는 것은 간단해 보일 수 있지만 데이터가 적을 때는 몇 가지 고급 기법을 사용하면 도움이 됩니다. 대표적인 세 가지 평가 방법인 단순 홀드아웃 검증(hold-out validation), K-겹 교차 검증(K-fold cross-validation), 셔플링(shuffling)을 사용한 반복 K-겹 교차 검증(iterated K-fold cross-validation)을 살펴보겠습니다.

단순 홀드아웃 검증

데이터의 일정량을 테스트 세트로 떼어 놓습니다. 남은 데이터에서 훈련하고 테스트 세트로 평가합니다. 앞 절에서 설명했듯이 정보 누설을 막기 위해 테스트 세트를 사용하여 모델을 튜닝해서는 안 됩니다. 이런 이유로 검증 세트도 따로 떼어 놓아야 합니다.

그림으로 나타내면 홀드아웃 검증은 그림 4-1과 같습니다. 다음 코드는 간단한 구현 예입니다.1

나타낼 수 없음 그림 4-1. 단순 홀드아웃 검증 분할

# 코드 4-1. 홀드아웃 검증 구현 예
num_validation_samples = 10000

# 데이터를 섞는 것(셔플링)이 일반적으로 좋습니다.
np.random.shuffle(data)

# 검증 세트를 만듭니다.
validation_data = data[:num_validation_samples]
data = data[num_validation_samples:]

# 훈련 세트를 만듭니다.
training_data = data[:]

# 훈련 세트에서 모델을 훈련하고 검증 세트를 평가합니다.
model = get_model()
model.train(training_data)
validation_score = model.evaluate(validation_data)

# 여기에서 모델을 튜닝하고,
# 다시 훈련하고, 평가하고, 또 다시 튜닝하고...

# 하이퍼파라미터 튜닝이 끝나면 테스트 데이터를 제외한 모든 데이터를 사용하여 다시 훈련시킵니다.
model = get_model()
model.train(np.concatenate([training_data, validation_data]))
test_score = model.evaluate(test_data)

2

이 평가 방법은 단순해서 한 가지 단점이 있습니다. 데이터가 적을 때는 검증 세트와 테스트 세트의 샘플이 너무 적어 주어진 전체 데이터를 통계적으로 대표하지 못할 수 있습니다. 쉽게 이를 확인할 수 있습니다. 다른 난수 초깃값으로 셔플링해서 데이터를 나누었을 때 모델의 성능이 매우 달라지면 바로 이 문제입니다. 다음에 이야기할 K-겹 교차 검증과 반복 K-겹 교차 검증이 이 문제를 해결할 수 있습니다.

K-겹 교차 검증

이 방식에서는 데이터를 동일한 크기를 가진 K개 분할로 나눕니다. 각 분할 i에 대해 남은 K - 1개의 분할로 모델을 훈련하고 분할 i에서 모델을 평가합니다. 최종 점수는 이렇게 얻은 K개의 점수를 평균합니다. 이 방법은 모델의 성능이 데이터 분할에 따라 편차가 클 때 도움이 됩니다. 홀드아웃 검증처럼 이 방법은 모델의 튜닝에 별개의 검증 세트를 사용하게 됩니다.

그림으로 나타내면 K-겹 교차 검증은 그림 4-2와 같습니다. 코드 4-2는 간단한 구현 예입니다.

나타낼 수 없음 그림 4-2. 3-겹 교차 검증

# 코드 4-2. K-겹 교차 검증 구현 예
k = 4
num_validation_samples = len(data) // k

np.random.shuffle(data)
validation_scores = []
for fold in range(k):
  # 검증 데이터 부분을 선택합니다.
  validation_data = data[num_validation_samples * fold:num_validation_samples * (fold+1)]
  # 남은 데이터를 훈련 데이터로 사용합니다. 리스트에서 + 연산자는 두 리스트를 더하는 것이 아닌 연결하는 것입니다.
  training_data = data[:num_validation_samples * fold] + data[num_validation_samples * (fold+1)]

  # 훈련되지 않은 새로운 모델을 만듭니다.
  model = get_model()
  model.train(training_data)
  validation_score = model.evaluate(validation_data)
  validation_scores.append(validation_score)

# 검증 점수: K개 폴드의 검증 점수 평균
validation_score = np.average(validation_scores)

# 텍스트 데이터를 제외한 전체 데이터로 최종 모델을 훈련합니다.
model = get_model()
model.train(data)
test_score = model.evaluate(test_data)

3

셔플링을 사용한 반복 K-겹 교차 검증

이 방법은 비교적 가용 데이터가 적고 가능한 정확하게 모델을 평가하고자 할 때 사용합니다. 캐글 경연에서는 이 방법이 아주 크게 도움이 됩니다. 이 방법은 K-겹 교차 검증을 여러 번 적용하되 K개의 분할로 나누기 전에 매번 데이터를 무작위로 섞습니다. 최종 점수는 모든 K-겹 교차 검증을 실행해서 얻은 점수의 평균이 됩니다. 결국 P × K개(P는 반복 횟수)의 모델을 훈련하고 평가하므로 비용이 매우 많이 듭니다.4

4.2.2 기억해야 할 것

평가 방식을 선택할 때 다음 사항을 유념해야 합니다.

  • 대표성 있는 데이터: 훈련 세트와 테스트 세트가 주어진 데이터에 대한 대표성이 있어야 합니다. 예를 들어 숫자 이미지를 분류하는 문제에서 샘플 배열이 클래스 순서대로 나열되어 있다고 가정합시다. 이 배열의 처음 80%를 훈련 세트로 나머지 20%를 테스트 세트로 만들면 훈련 세트에는 0~7 숫자만 담겨 있고 테스트 세트에는 8~9 숫자만 담기게 됩니다. 어처구니없는 실수처럼 보이지만 놀랍게도 자주 일어나는 일입니다. 이런 이유 때문에 훈련 세트와 테스트 세트로 나누기 전에 데이터를 무작위로 섞는 것이 일반적입니다.5
  • 시간의 방향: 과거로부터 미래를 예측하려고 한다면(예를 들어 내일의 날씨, 주식 시세 등) 데이터를 분할하기 전에 무작위로 섞어서는 절대 안 됩니다. 이렇게 하면 미래의 정보가 누설되기 때문입니다. 즉 모델이 사실상 미래 데이터에서 훈련될 것입니다. 이런 문제에서는 훈련 세트에 있는 데이터보다 테스트 세트에 있는 모든 데이터가 미래의 것이어야 합니다.
  • 데이터 중복: 한 데이터셋에 어떤 데이터 포인트가 두 번 등장하면(실제 데이터셋에서 아주 흔한 일입니다), 데이터를 섞고 훈련 세트와 검증 세트로 나누었을 때 훈련 세트와 검증 세트에 데이터 포인트가 중복될 수 있습니다. 이로 인해 훈련 데이터의 일부로 테스트하는 최악의 경우가 됩니다! 훈련 세트와 검증 세트가 중복되지 않는지 확인하세요.6
  1. 그림 4-1~4-2와 코드 4-1~4-2는 테스트 세트를 이미 떼어 놓은 후를 가정한 것입니다. 여기처럼 직접 데이터를 나누기보다는 종종 사이킷런의 train_test_split() 함수를 사용하여 훈련, 검증, 테스트 세트로 나누는 것이 편리합니다. 

  2. 훈련- 평가 -튜닝을 반복할 때 계속 새로운 모델을 만듭니다. 최적의 하이퍼파라미터를 구한 후 마지막 모델을 훈련시킬 때 훈련 데이터와 검증 데이터를 모두 사용하는 것이 중요합니다. 

  3. K-겹 교차 검증은 사이킷런의 cross_validate() 함수를 사용하여 쉽게 구현할 수 있습니다. 이 함수를 사용하려면 케라스 모델을 사이킷런과 호환되도록 KerasClassifierKerasRegressor 클래스로 모델을 감싸야 합니다. 

  4. 반복 K-겹 교차 검증은 사이킷런 0.19 버전에 추가된 RepeatedKFold(회귀)와 RepeatedStratifiedKFold(분류) 클래스를 cross_validate() 함수에 적용하여 구현할 수 있습니다. 이에 대한 간단한 예제는 제 블로그를 참고하세요(https://bit.ly/2rSVwjB). 

  5. 특정 클래스의 비율이 현저히 작다면 무작위로 섞기보다 클래스 비율이 훈련 세트와 테스트 세트에 고르게 나누어지도록 고려해야 합니다. 이를 계층별(stratified) 분할이라고도 합니다. 사이킷런의 train_test_split() 함수는 stratify 매개변수로 타깃 레이블을 전달받아 계층별 분할을 수행할 수 있습니다. 

  6. 비슷한 예로 사람 얼굴을 찾아 주는 사진 애플리케이션에서는 훈련 세트와 테스트 세트에 동일한 사람의 얼굴 사진이 포함되지 않도록 해야 합니다. 이렇게 섞이지 않아야 할 그룹을 지정하여 교차 검증을 하기 위해서는 사이킷런의 GroupKFold 클래스를 cross_validate() 함수에 적용합니다. 

댓글남기기