과대적합과 과소적합

TensorFlow.org에서 보기 구글 코랩(Colab)에서 실행하기 깃허브(GitHub) 소스 보기

지금까지 그랬듯이 이 예제의 코드도 tf.keras API를 사용합니다. 텐서플로 케라스 가이드에서 tf.keras API에 대해 더 많은 정보를 얻을 수 있습니다.

앞서 영화 리뷰 분류와 주택 가격 예측의 두 예제에서 일정 에포크 동안 훈련하면 검증 세트에서 모델 성능이 최고점에 도달한 다음 감소하기 시작한 것을 보았습니다.

다른 말로 하면, 모델이 훈련 세트에 과대적합(overfitting)된 것입니다. 과대적합을 다루는 방법은 꼭 배워야 합니다. 훈련 세트에서 높은 성능을 얻을 수 있지만 진짜 원하는 것은 테스트 세트(또는 이전에 본 적 없는 데이터)에 잘 일반화되는 모델입니다.

과대적합의 반대는 과소적합(underfitting)입니다. 과소적합은 테스트 세트의 성능이 향상될 여지가 아직 있을 때 일어납니다. 발생하는 원인은 여러가지입니다. 모델이 너무 단순하거나, 규제가 너무 많거나, 그냥 단순히 충분히 오래 훈련하지 않는 경우입니다. 즉 네트워크가 훈련 세트에서 적절한 패턴을 학습하지 못했다는 뜻입니다.

모델을 너무 오래 훈련하면 과대적합되기 시작하고 테스트 세트에서 일반화되지 못하는 패턴을 훈련 세트에서 학습합니다. 과대적합과 과소적합 사이에서 균형을 잡아야 합니다. 이를 위해 적절한 에포크 횟수동안 모델을 훈련하는 방법을 배워보겠습니다.

과대적합을 막는 가장 좋은 방법은 더 많은 훈련 데이터를 사용하는 것입니다. 많은 데이터에서 훈련한 모델은 자연적으로 일반화 성능이 더 좋습니다. 데이터를 더 준비할 수 없을 때 그다음으로 가장 좋은 방법은 규제(regularization)와 같은 기법을 사용하는 것입니다. 모델이 저장할 수 있는 정보의 양과 종류에 제약을 부과하는 방법입니다. 네트워크가 소수의 패턴만 기억할 수 있다면 최적화 과정 동안 일반화 가능성이 높은 가장 중요한 패턴에 촛점을 맞출 것입니다.

이 노트북에서 널리 사용되는 두 가지 규제 기법인 가중치 규제와 드롭아웃(dropout)을 알아 보겠습니다. 이런 기법을 사용하여 IMDB 영화 리뷰 분류 모델의 성능을 향상시켜 보죠.

from __future__ import absolute_import, division, print_function, unicode_literals, unicode_literals

import tensorflow as tf
from tensorflow import keras

import numpy as np
import matplotlib.pyplot as plt

print(tf.__version__)
1.14.1-dev20190524

IMDB 데이터셋 다운로드

이전 노트북에서처럼 임베딩을 사용하지 않고 여기에서는 문장을 멀티-핫 인코딩(multi-hot encoding)으로 변환하겠습니다. 이 모델은 훈련 세트에 빠르게 과대적합될 것입니다. 과대적합을 발생시키기고 어떻게 해결하는지 보이기 위해 선택했습니다.

멀티-핫 인코딩은 정수 시퀀스를 0과 1로 이루어진 벡터로 변환합니다. 정확하게 말하면 시퀀스 [3, 5]를 인덱스 3과 5만 1이고 나머지는 모두 0인 10,000 차원 벡터로 변환한다는 의미입니다.

NUM_WORDS = 10000

(train_data, train_labels), (test_data, test_labels) = keras.datasets.imdb.load_data(num_words=NUM_WORDS)

def multi_hot_sequences(sequences, dimension):
    # 0으로 채워진 (len(sequences), dimension) 크기의 행렬을 만듭니다
    results = np.zeros((len(sequences), dimension))
    for i, word_indices in enumerate(sequences):
        results[i, word_indices] = 1.0  # results[i]의 특정 인덱스만 1로 설정합니다
    return results


train_data = multi_hot_sequences(train_data, dimension=NUM_WORDS)
test_data = multi_hot_sequences(test_data, dimension=NUM_WORDS)

만들어진 멀티-핫 벡터 중 하나를 살펴 보죠. 단어 인덱스는 빈도 순으로 정렬되어 있습니다. 그래프에서 볼 수 있듯이 인덱스 0에 가까울수록 1이 많이 등장합니다:

plt.plot(train_data[0])
[<matplotlib.lines.Line2D at 0x7f9061e65a20>]

png

과대적합 예제

과대적합을 막는 가장 간단한 방법은 모델의 규모를 축소하는 것입니다. 즉, 모델에 있는 학습 가능한 파라미터의 수를 줄입니다(모델 파라미터는 층(layer)의 개수와 층의 유닛(unit) 개수에 의해 결정됩니다). 딥러닝에서는 모델의 학습 가능한 파라미터의 수를 종종 모델의 "용량"이라고 말합니다. 직관적으로 생각해 보면 많은 파라미터를 가진 모델이 더 많은 "기억 용량"을 가집니다. 이런 모델은 훈련 샘플과 타깃 사이를 일반화 능력이 없는 딕셔너리와 같은 매핑으로 완벽하게 학습할 수 있습니다. 하지만 이전에 본 적 없는 데이터에서 예측을 할 땐 쓸모가 없을 것입니다.

항상 기억해야 할 점은 딥러닝 모델이 훈련 세트에는 학습이 잘 되는 경향이 있지만 진짜 해결할 문제는 학습이 아니라 일반화라는 것입니다.

반면에 네트워크의 기억 용량이 부족하다면 이런 매핑을 쉽게 학습할 수 없을 것입니다. 손실을 최소화하기 위해서는 예측 성능이 더 많은 압축된 표현을 학습해야 합니다. 또한 너무 작은 모델을 만들면 훈련 데이터를 학습하기 어렵울 것입니다. "너무 많은 용량"과 "충분하지 않은 용량" 사이의 균형을 잡아야 합니다.

안타깝지만 어떤 모델의 (층의 개수나 뉴런 개수에 해당하는) 적절한 크기나 구조를 결정하는 마법같은 공식은 없습니다. 여러 가지 다른 구조를 사용해 실험을 해봐야만 합니다.

알맞은 모델의 크기를 찾으려면 비교적 적은 수의 층과 파라미터로 시작해서 검증 손실이 감소할 때까지 새로운 층을 추가하거나 층의 크기를 늘리는 것이 좋습니다. 영화 리뷰 분류 네트워크를 사용해 이를 실험해 보죠.

Dense 층만 사용하는 간단한 기준 모델을 만들고 작은 규모의 버전와 큰 버전의 모델을 만들어 비교하겠습니다.

기준 모델 만들기

baseline_model = keras.Sequential([
    # `.summary` 메서드 때문에 `input_shape`가 필요합니다
    keras.layers.Dense(16, activation=tf.nn.relu, input_shape=(NUM_WORDS,)),
    keras.layers.Dense(16, activation=tf.nn.relu),
    keras.layers.Dense(1, activation=tf.nn.sigmoid)
])

baseline_model.compile(optimizer='adam',
                       loss='binary_crossentropy',
                       metrics=['accuracy', 'binary_crossentropy'])

baseline_model.summary()
WARNING: Logging before flag parsing goes to stderr.
W0524 23:31:20.915361 140259921626880 deprecation.py:323] From /home/kbuilder/.local/lib/python3.5/site-packages/tensorflow/python/ops/nn_impl.py:180: add_dispatch_support.<locals>.wrapper (from tensorflow.python.ops.array_ops) is deprecated and will be removed in a future version.
Instructions for updating:
Use tf.where in 2.0, which has the same broadcast rule as np.where

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense (Dense)                (None, 16)                160016    
_________________________________________________________________
dense_1 (Dense)              (None, 16)                272       
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 17        
=================================================================
Total params: 160,305
Trainable params: 160,305
Non-trainable params: 0
_________________________________________________________________
baseline_history = baseline_model.fit(train_data,
                                      train_labels,
                                      epochs=20,
                                      batch_size=512,
                                      validation_data=(test_data, test_labels),
                                      verbose=2)
Train on 25000 samples, validate on 25000 samples
Epoch 1/20
25000/25000 - 5s - loss: 0.4638 - acc: 0.8085 - binary_crossentropy: 0.4638 - val_loss: 0.3272 - val_acc: 0.8787 - val_binary_crossentropy: 0.3272
Epoch 2/20
25000/25000 - 4s - loss: 0.2407 - acc: 0.9170 - binary_crossentropy: 0.2407 - val_loss: 0.2860 - val_acc: 0.8869 - val_binary_crossentropy: 0.2860
Epoch 3/20
25000/25000 - 5s - loss: 0.1776 - acc: 0.9393 - binary_crossentropy: 0.1776 - val_loss: 0.2970 - val_acc: 0.8806 - val_binary_crossentropy: 0.2970
Epoch 4/20
25000/25000 - 5s - loss: 0.1447 - acc: 0.9515 - binary_crossentropy: 0.1447 - val_loss: 0.3390 - val_acc: 0.8693 - val_binary_crossentropy: 0.3390
Epoch 5/20
25000/25000 - 4s - loss: 0.1187 - acc: 0.9608 - binary_crossentropy: 0.1187 - val_loss: 0.3428 - val_acc: 0.8731 - val_binary_crossentropy: 0.3428
Epoch 6/20
25000/25000 - 4s - loss: 0.0964 - acc: 0.9709 - binary_crossentropy: 0.0964 - val_loss: 0.3728 - val_acc: 0.8705 - val_binary_crossentropy: 0.3728
Epoch 7/20
25000/25000 - 4s - loss: 0.0773 - acc: 0.9792 - binary_crossentropy: 0.0773 - val_loss: 0.4117 - val_acc: 0.8660 - val_binary_crossentropy: 0.4117
Epoch 8/20
25000/25000 - 4s - loss: 0.0631 - acc: 0.9848 - binary_crossentropy: 0.0631 - val_loss: 0.4486 - val_acc: 0.8630 - val_binary_crossentropy: 0.4486
Epoch 9/20
25000/25000 - 4s - loss: 0.0484 - acc: 0.9906 - binary_crossentropy: 0.0484 - val_loss: 0.4837 - val_acc: 0.8612 - val_binary_crossentropy: 0.4837
Epoch 10/20
25000/25000 - 5s - loss: 0.0385 - acc: 0.9931 - binary_crossentropy: 0.0385 - val_loss: 0.5270 - val_acc: 0.8590 - val_binary_crossentropy: 0.5270
Epoch 11/20
25000/25000 - 5s - loss: 0.0292 - acc: 0.9960 - binary_crossentropy: 0.0292 - val_loss: 0.5658 - val_acc: 0.8567 - val_binary_crossentropy: 0.5658
Epoch 12/20
25000/25000 - 5s - loss: 0.0227 - acc: 0.9972 - binary_crossentropy: 0.0227 - val_loss: 0.6062 - val_acc: 0.8558 - val_binary_crossentropy: 0.6062
Epoch 13/20
25000/25000 - 5s - loss: 0.0175 - acc: 0.9986 - binary_crossentropy: 0.0175 - val_loss: 0.6384 - val_acc: 0.8542 - val_binary_crossentropy: 0.6384
Epoch 14/20
25000/25000 - 5s - loss: 0.0133 - acc: 0.9991 - binary_crossentropy: 0.0133 - val_loss: 0.6792 - val_acc: 0.8537 - val_binary_crossentropy: 0.6792
Epoch 15/20
25000/25000 - 5s - loss: 0.0101 - acc: 0.9995 - binary_crossentropy: 0.0101 - val_loss: 0.7050 - val_acc: 0.8539 - val_binary_crossentropy: 0.7050
Epoch 16/20
25000/25000 - 5s - loss: 0.0079 - acc: 0.9996 - binary_crossentropy: 0.0079 - val_loss: 0.7545 - val_acc: 0.8528 - val_binary_crossentropy: 0.7545
Epoch 17/20
25000/25000 - 4s - loss: 0.0062 - acc: 0.9996 - binary_crossentropy: 0.0062 - val_loss: 0.7791 - val_acc: 0.8525 - val_binary_crossentropy: 0.7791
Epoch 18/20
25000/25000 - 4s - loss: 0.0048 - acc: 0.9998 - binary_crossentropy: 0.0048 - val_loss: 0.8201 - val_acc: 0.8526 - val_binary_crossentropy: 0.8201
Epoch 19/20
25000/25000 - 4s - loss: 0.0037 - acc: 0.9999 - binary_crossentropy: 0.0037 - val_loss: 0.8448 - val_acc: 0.8520 - val_binary_crossentropy: 0.8448
Epoch 20/20
25000/25000 - 4s - loss: 0.0029 - acc: 0.9999 - binary_crossentropy: 0.0029 - val_loss: 0.8752 - val_acc: 0.8516 - val_binary_crossentropy: 0.8752

작은 모델 만들기

앞서 만든 기준 모델과 비교하기 위해 적은 수의 은닉 유닛을 가진 모델을 만들어 보죠:

smaller_model = keras.Sequential([
    keras.layers.Dense(4, activation=tf.nn.relu, input_shape=(NUM_WORDS,)),
    keras.layers.Dense(4, activation=tf.nn.relu),
    keras.layers.Dense(1, activation=tf.nn.sigmoid)
])

smaller_model.compile(optimizer='adam',
                loss='binary_crossentropy',
                metrics=['accuracy', 'binary_crossentropy'])

smaller_model.summary()
Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_3 (Dense)              (None, 4)                 40004     
_________________________________________________________________
dense_4 (Dense)              (None, 4)                 20        
_________________________________________________________________
dense_5 (Dense)              (None, 1)                 5         
=================================================================
Total params: 40,029
Trainable params: 40,029
Non-trainable params: 0
_________________________________________________________________

같은 데이터를 사용해 이 모델을 훈련합니다:

smaller_history = smaller_model.fit(train_data,
                                    train_labels,
                                    epochs=20,
                                    batch_size=512,
                                    validation_data=(test_data, test_labels),
                                    verbose=2)
Train on 25000 samples, validate on 25000 samples
Epoch 1/20
25000/25000 - 4s - loss: 0.6416 - acc: 0.6030 - binary_crossentropy: 0.6416 - val_loss: 0.5949 - val_acc: 0.7499 - val_binary_crossentropy: 0.5949
Epoch 2/20
25000/25000 - 4s - loss: 0.5471 - acc: 0.7886 - binary_crossentropy: 0.5471 - val_loss: 0.5315 - val_acc: 0.8098 - val_binary_crossentropy: 0.5315
Epoch 3/20
25000/25000 - 4s - loss: 0.4851 - acc: 0.8524 - binary_crossentropy: 0.4851 - val_loss: 0.4883 - val_acc: 0.8518 - val_binary_crossentropy: 0.4883
Epoch 4/20
25000/25000 - 4s - loss: 0.4378 - acc: 0.8874 - binary_crossentropy: 0.4378 - val_loss: 0.4556 - val_acc: 0.8633 - val_binary_crossentropy: 0.4556
Epoch 5/20
25000/25000 - 4s - loss: 0.3976 - acc: 0.9077 - binary_crossentropy: 0.3976 - val_loss: 0.4296 - val_acc: 0.8721 - val_binary_crossentropy: 0.4296
Epoch 6/20
25000/25000 - 4s - loss: 0.3619 - acc: 0.9233 - binary_crossentropy: 0.3619 - val_loss: 0.4092 - val_acc: 0.8753 - val_binary_crossentropy: 0.4092
Epoch 7/20
25000/25000 - 4s - loss: 0.3294 - acc: 0.9346 - binary_crossentropy: 0.3294 - val_loss: 0.3929 - val_acc: 0.8771 - val_binary_crossentropy: 0.3929
Epoch 8/20
25000/25000 - 4s - loss: 0.2995 - acc: 0.9429 - binary_crossentropy: 0.2995 - val_loss: 0.3751 - val_acc: 0.8837 - val_binary_crossentropy: 0.3751
Epoch 9/20
25000/25000 - 4s - loss: 0.2719 - acc: 0.9512 - binary_crossentropy: 0.2719 - val_loss: 0.3616 - val_acc: 0.8849 - val_binary_crossentropy: 0.3616
Epoch 10/20
25000/25000 - 5s - loss: 0.2472 - acc: 0.9576 - binary_crossentropy: 0.2472 - val_loss: 0.3541 - val_acc: 0.8832 - val_binary_crossentropy: 0.3541
Epoch 11/20
25000/25000 - 4s - loss: 0.2237 - acc: 0.9627 - binary_crossentropy: 0.2237 - val_loss: 0.3486 - val_acc: 0.8807 - val_binary_crossentropy: 0.3486
Epoch 12/20
25000/25000 - 4s - loss: 0.2031 - acc: 0.9671 - binary_crossentropy: 0.2031 - val_loss: 0.3432 - val_acc: 0.8797 - val_binary_crossentropy: 0.3432
Epoch 13/20
25000/25000 - 4s - loss: 0.1840 - acc: 0.9708 - binary_crossentropy: 0.1840 - val_loss: 0.3423 - val_acc: 0.8782 - val_binary_crossentropy: 0.3423
Epoch 14/20
25000/25000 - 4s - loss: 0.1668 - acc: 0.9740 - binary_crossentropy: 0.1668 - val_loss: 0.3425 - val_acc: 0.8763 - val_binary_crossentropy: 0.3425
Epoch 15/20
25000/25000 - 4s - loss: 0.1517 - acc: 0.9762 - binary_crossentropy: 0.1517 - val_loss: 0.3444 - val_acc: 0.8750 - val_binary_crossentropy: 0.3444
Epoch 16/20
25000/25000 - 4s - loss: 0.1380 - acc: 0.9792 - binary_crossentropy: 0.1380 - val_loss: 0.3519 - val_acc: 0.8734 - val_binary_crossentropy: 0.3520
Epoch 17/20
25000/25000 - 4s - loss: 0.1255 - acc: 0.9822 - binary_crossentropy: 0.1255 - val_loss: 0.3541 - val_acc: 0.8728 - val_binary_crossentropy: 0.3541
Epoch 18/20
25000/25000 - 4s - loss: 0.1109 - acc: 0.9830 - binary_crossentropy: 0.1109 - val_loss: 0.3742 - val_acc: 0.8718 - val_binary_crossentropy: 0.3742
Epoch 19/20
25000/25000 - 4s - loss: 0.0837 - acc: 0.9828 - binary_crossentropy: 0.0837 - val_loss: 0.3990 - val_acc: 0.8704 - val_binary_crossentropy: 0.3990
Epoch 20/20
25000/25000 - 4s - loss: 0.0691 - acc: 0.9870 - binary_crossentropy: 0.0691 - val_loss: 0.4144 - val_acc: 0.8663 - val_binary_crossentropy: 0.4144

큰 모델 만들기

아주 큰 모델을 만들어 얼마나 빠르게 과대적합이 시작되는지 알아 볼 수 있습니다. 이 문제에 필요한 것보다 훨씬 더 큰 용량을 가진 네트워크를 추가해서 비교해 보죠:

bigger_model = keras.models.Sequential([
    keras.layers.Dense(512, activation=tf.nn.relu, input_shape=(NUM_WORDS,)),
    keras.layers.Dense(512, activation=tf.nn.relu),
    keras.layers.Dense(1, activation=tf.nn.sigmoid)
])

bigger_model.compile(optimizer='adam',
                     loss='binary_crossentropy',
                     metrics=['accuracy','binary_crossentropy'])

bigger_model.summary()
Model: "sequential_2"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_6 (Dense)              (None, 512)               5120512   
_________________________________________________________________
dense_7 (Dense)              (None, 512)               262656    
_________________________________________________________________
dense_8 (Dense)              (None, 1)                 513       
=================================================================
Total params: 5,383,681
Trainable params: 5,383,681
Non-trainable params: 0
_________________________________________________________________

역시 같은 데이터를 사용해 모델을 훈련합니다:

bigger_history = bigger_model.fit(train_data, train_labels,
                                  epochs=20,
                                  batch_size=512,
                                  validation_data=(test_data, test_labels),
                                  verbose=2)
Train on 25000 samples, validate on 25000 samples
Epoch 1/20
25000/25000 - 7s - loss: 0.3454 - acc: 0.8512 - binary_crossentropy: 0.3454 - val_loss: 0.3001 - val_acc: 0.8769 - val_binary_crossentropy: 0.3001
Epoch 2/20
25000/25000 - 7s - loss: 0.1460 - acc: 0.9472 - binary_crossentropy: 0.1460 - val_loss: 0.3314 - val_acc: 0.8716 - val_binary_crossentropy: 0.3314
Epoch 3/20
25000/25000 - 7s - loss: 0.0505 - acc: 0.9856 - binary_crossentropy: 0.0505 - val_loss: 0.4512 - val_acc: 0.8680 - val_binary_crossentropy: 0.4512
Epoch 4/20
25000/25000 - 7s - loss: 0.0083 - acc: 0.9984 - binary_crossentropy: 0.0083 - val_loss: 0.5934 - val_acc: 0.8658 - val_binary_crossentropy: 0.5934
Epoch 5/20
25000/25000 - 7s - loss: 0.0012 - acc: 1.0000 - binary_crossentropy: 0.0012 - val_loss: 0.6868 - val_acc: 0.8692 - val_binary_crossentropy: 0.6868
Epoch 6/20
25000/25000 - 7s - loss: 2.6467e-04 - acc: 1.0000 - binary_crossentropy: 2.6467e-04 - val_loss: 0.7389 - val_acc: 0.8693 - val_binary_crossentropy: 0.7389
Epoch 7/20
25000/25000 - 7s - loss: 1.5262e-04 - acc: 1.0000 - binary_crossentropy: 1.5262e-04 - val_loss: 0.7715 - val_acc: 0.8696 - val_binary_crossentropy: 0.7715
Epoch 8/20
25000/25000 - 7s - loss: 1.0830e-04 - acc: 1.0000 - binary_crossentropy: 1.0830e-04 - val_loss: 0.7959 - val_acc: 0.8696 - val_binary_crossentropy: 0.7959
Epoch 9/20
25000/25000 - 7s - loss: 8.2300e-05 - acc: 1.0000 - binary_crossentropy: 8.2300e-05 - val_loss: 0.8159 - val_acc: 0.8696 - val_binary_crossentropy: 0.8159
Epoch 10/20
25000/25000 - 7s - loss: 6.5027e-05 - acc: 1.0000 - binary_crossentropy: 6.5027e-05 - val_loss: 0.8340 - val_acc: 0.8698 - val_binary_crossentropy: 0.8340
Epoch 11/20
25000/25000 - 7s - loss: 5.2224e-05 - acc: 1.0000 - binary_crossentropy: 5.2224e-05 - val_loss: 0.8517 - val_acc: 0.8697 - val_binary_crossentropy: 0.8517
Epoch 12/20
25000/25000 - 8s - loss: 4.2613e-05 - acc: 1.0000 - binary_crossentropy: 4.2613e-05 - val_loss: 0.8686 - val_acc: 0.8697 - val_binary_crossentropy: 0.8686
Epoch 13/20
25000/25000 - 7s - loss: 3.4708e-05 - acc: 1.0000 - binary_crossentropy: 3.4708e-05 - val_loss: 0.8846 - val_acc: 0.8695 - val_binary_crossentropy: 0.8846
Epoch 14/20
25000/25000 - 7s - loss: 2.8494e-05 - acc: 1.0000 - binary_crossentropy: 2.8494e-05 - val_loss: 0.9028 - val_acc: 0.8696 - val_binary_crossentropy: 0.9028
Epoch 15/20
25000/25000 - 7s - loss: 2.3385e-05 - acc: 1.0000 - binary_crossentropy: 2.3385e-05 - val_loss: 0.9205 - val_acc: 0.8696 - val_binary_crossentropy: 0.9205
Epoch 16/20
25000/25000 - 7s - loss: 1.9340e-05 - acc: 1.0000 - binary_crossentropy: 1.9340e-05 - val_loss: 0.9379 - val_acc: 0.8695 - val_binary_crossentropy: 0.9379
Epoch 17/20
25000/25000 - 7s - loss: 1.6062e-05 - acc: 1.0000 - binary_crossentropy: 1.6062e-05 - val_loss: 0.9549 - val_acc: 0.8695 - val_binary_crossentropy: 0.9549
Epoch 18/20
25000/25000 - 7s - loss: 1.3423e-05 - acc: 1.0000 - binary_crossentropy: 1.3423e-05 - val_loss: 0.9716 - val_acc: 0.8695 - val_binary_crossentropy: 0.9716
Epoch 19/20
25000/25000 - 7s - loss: 1.1331e-05 - acc: 1.0000 - binary_crossentropy: 1.1331e-05 - val_loss: 0.9862 - val_acc: 0.8695 - val_binary_crossentropy: 0.9862
Epoch 20/20
25000/25000 - 7s - loss: 9.6548e-06 - acc: 1.0000 - binary_crossentropy: 9.6548e-06 - val_loss: 1.0016 - val_acc: 0.8697 - val_binary_crossentropy: 1.0016

훈련 손실과 검증 손실 그래프 그리기

실선은 훈련 손실이고 점선은 검증 손실입니다(낮은 검증 손실이 더 좋은 모델입니다). 여기서는 작은 네트워크가 기준 모델보다 더 늦게 과대적합이 시작되었습니다(즉 에포크 4가 아니라 6에서 시작됩니다). 또한 과대적합이 시작되고 훨씬 천천히 성능이 감소합니다.

def plot_history(histories, key='binary_crossentropy'):
  plt.figure(figsize=(16,10))

  for name, history in histories:
    val = plt.plot(history.epoch, history.history['val_'+key],
                   '--', label=name.title()+' Val')
    plt.plot(history.epoch, history.history[key], color=val[0].get_color(),
             label=name.title()+' Train')

  plt.xlabel('Epochs')
  plt.ylabel(key.replace('_',' ').title())
  plt.legend()

  plt.xlim([0,max(history.epoch)])


plot_history([('baseline', baseline_history),
              ('smaller', smaller_history),
              ('bigger', bigger_history)])

png

큰 네트워크는 거의 바로 첫 번째 에포크 이후에 과대적합이 시작되고 훨씬 더 심각하게 과대적합됩니다. 네트워크의 용량이 많을수록 훈련 세트를 더 빠르게 모델링할 수 있습니다(훈련 손실이 낮아집니다). 하지만 더 쉽게 과대적합됩니다(훈련 손실과 검증 손실 사이에 큰 차이가 발생합니다).

전략

가중치를 규제하기

아마도 오캄의 면도날(Occam's Razor) 이론을 들어 보았을 것입니다. 어떤 것을 설명하는 두 가지 방법이 있다면 더 정확한 설명은 최소한의 가정이 필요한 가장 "간단한" 설명일 것입니다. 이는 신경망으로 학습되는 모델에도 적용됩니다. 훈련 데이터와 네트워크 구조가 주어졌을 때 이 데이터를 설명할 수 있는 가중치의 조합(즉, 가능한 모델)은 많습니다. 간단한 모델은 복잡한 것보다 과대적합되는 경향이 작을 것입니다.

여기서 "간단한 모델"은 모델 파라미터의 분포를 봤을 때 엔트로피(entropy)가 작은 모델입니다(또는 앞 절에서 보았듯이 적은 파라미터를 가진 모델입니다). 따라서 과대적합을 완화시키는 일반적인 방법은 가중치가 작은 값을 가지도록 네트워크의 복잡도에 제약을 가하는 것입니다. 이는 가중치 값의 분포를 좀 더 균일하게 만들어 줍니다. 이를 "가중치 규제"(weight regularization)라고 부릅니다. 네트워크의 손실 함수에 큰 가중치에 해당하는 비용을 추가합니다. 이 비용은 두 가지 형태가 있습니다:

  • L1 규제는 가중치의 절댓값에 비례하는 비용이 추가됩니다(즉, 가중치의 "L1 노름(norm)"을 추가합니다).

  • L2 규제는 가중치의 제곱에 비례하는 비용이 추가됩니다(즉, 가중치의 "L2 노름"의 제곱을 추가합니다). 신경망에서는 L2 규제를 가중치 감쇠(weight decay)라고도 부릅니다. 이름이 다르지만 혼돈하지 마세요. 가중치 감쇠는 수학적으로 L2 규제와 동일합니다.

tf.keras에서는 가중치 규제 객체를 층의 키워드 매개변수에 전달하여 가중치에 규제를 추가합니다. L2 가중치 규제를 추가해 보죠.

l2_model = keras.models.Sequential([
    keras.layers.Dense(16, kernel_regularizer=keras.regularizers.l2(0.001),
                       activation=tf.nn.relu, input_shape=(NUM_WORDS,)),
    keras.layers.Dense(16, kernel_regularizer=keras.regularizers.l2(0.001),
                       activation=tf.nn.relu),
    keras.layers.Dense(1, activation=tf.nn.sigmoid)
])

l2_model.compile(optimizer='adam',
                 loss='binary_crossentropy',
                 metrics=['accuracy', 'binary_crossentropy'])

l2_model_history = l2_model.fit(train_data, train_labels,
                                epochs=20,
                                batch_size=512,
                                validation_data=(test_data, test_labels),
                                verbose=2)
Train on 25000 samples, validate on 25000 samples
Epoch 1/20
25000/25000 - 5s - loss: 0.5182 - acc: 0.8058 - binary_crossentropy: 0.4787 - val_loss: 0.3761 - val_acc: 0.8776 - val_binary_crossentropy: 0.3341
Epoch 2/20
25000/25000 - 5s - loss: 0.3006 - acc: 0.9085 - binary_crossentropy: 0.2538 - val_loss: 0.3348 - val_acc: 0.8874 - val_binary_crossentropy: 0.2848
Epoch 3/20
25000/25000 - 5s - loss: 0.2493 - acc: 0.9301 - binary_crossentropy: 0.1967 - val_loss: 0.3391 - val_acc: 0.8850 - val_binary_crossentropy: 0.2847
Epoch 4/20
25000/25000 - 5s - loss: 0.2280 - acc: 0.9399 - binary_crossentropy: 0.1718 - val_loss: 0.3586 - val_acc: 0.8783 - val_binary_crossentropy: 0.3012
Epoch 5/20
25000/25000 - 5s - loss: 0.2128 - acc: 0.9478 - binary_crossentropy: 0.1543 - val_loss: 0.3694 - val_acc: 0.8761 - val_binary_crossentropy: 0.3100
Epoch 6/20
25000/25000 - 5s - loss: 0.2013 - acc: 0.9534 - binary_crossentropy: 0.1410 - val_loss: 0.3819 - val_acc: 0.8741 - val_binary_crossentropy: 0.3211
Epoch 7/20
25000/25000 - 5s - loss: 0.1943 - acc: 0.9558 - binary_crossentropy: 0.1328 - val_loss: 0.4034 - val_acc: 0.8711 - val_binary_crossentropy: 0.3413
Epoch 8/20
25000/25000 - 5s - loss: 0.1882 - acc: 0.9576 - binary_crossentropy: 0.1255 - val_loss: 0.4145 - val_acc: 0.8703 - val_binary_crossentropy: 0.3514
Epoch 9/20
25000/25000 - 5s - loss: 0.1829 - acc: 0.9581 - binary_crossentropy: 0.1190 - val_loss: 0.4327 - val_acc: 0.8650 - val_binary_crossentropy: 0.3685
Epoch 10/20
25000/25000 - 5s - loss: 0.1784 - acc: 0.9620 - binary_crossentropy: 0.1138 - val_loss: 0.4419 - val_acc: 0.8668 - val_binary_crossentropy: 0.3769
Epoch 11/20
25000/25000 - 5s - loss: 0.1736 - acc: 0.9641 - binary_crossentropy: 0.1082 - val_loss: 0.4552 - val_acc: 0.8643 - val_binary_crossentropy: 0.3894
Epoch 12/20
25000/25000 - 5s - loss: 0.1700 - acc: 0.9645 - binary_crossentropy: 0.1039 - val_loss: 0.4729 - val_acc: 0.8618 - val_binary_crossentropy: 0.4065
Epoch 13/20
25000/25000 - 5s - loss: 0.1655 - acc: 0.9676 - binary_crossentropy: 0.0988 - val_loss: 0.4820 - val_acc: 0.8620 - val_binary_crossentropy: 0.4152
Epoch 14/20
25000/25000 - 5s - loss: 0.1631 - acc: 0.9671 - binary_crossentropy: 0.0959 - val_loss: 0.5007 - val_acc: 0.8591 - val_binary_crossentropy: 0.4332
Epoch 15/20
25000/25000 - 5s - loss: 0.1630 - acc: 0.9670 - binary_crossentropy: 0.0954 - val_loss: 0.5167 - val_acc: 0.8596 - val_binary_crossentropy: 0.4486
Epoch 16/20
25000/25000 - 5s - loss: 0.1612 - acc: 0.9681 - binary_crossentropy: 0.0922 - val_loss: 0.5280 - val_acc: 0.8551 - val_binary_crossentropy: 0.4587
Epoch 17/20
25000/25000 - 5s - loss: 0.1593 - acc: 0.9686 - binary_crossentropy: 0.0901 - val_loss: 0.5346 - val_acc: 0.8571 - val_binary_crossentropy: 0.4653
Epoch 18/20
25000/25000 - 4s - loss: 0.1557 - acc: 0.9709 - binary_crossentropy: 0.0859 - val_loss: 0.5653 - val_acc: 0.8516 - val_binary_crossentropy: 0.4956
Epoch 19/20
25000/25000 - 5s - loss: 0.1558 - acc: 0.9694 - binary_crossentropy: 0.0858 - val_loss: 0.5550 - val_acc: 0.8538 - val_binary_crossentropy: 0.4847
Epoch 20/20
25000/25000 - 5s - loss: 0.1479 - acc: 0.9747 - binary_crossentropy: 0.0776 - val_loss: 0.5661 - val_acc: 0.8536 - val_binary_crossentropy: 0.4961

l2(0.001)는 네트워크의 전체 손실에 층에 있는 가중치 행렬의 모든 값이 0.001 * weight_coefficient_value**2만큼 더해진다는 의미입니다. 이런 페널티(penalty)는 훈련할 때만 추가됩니다. 따라서 테스트 단계보다 훈련 단계에서 네트워크 손실이 훨씬 더 클 것입니다.

L2 규제의 효과를 확인해 보죠:

plot_history([('baseline', baseline_history),
              ('l2', l2_model_history)])

png

결과에서 보듯이 모델 파라미터의 개수는 같지만 L2 규제를 적용한 모델이 기본 모델보다 과대적합에 훨씬 잘 견디고 있습니다.

드롭아웃 추가하기

드롭아웃(dropout)은 신경망에서 가장 효과적이고 널리 사용하는 규제 기법 중 하나입니다. 토론토(Toronto) 대학의 힌튼(Hinton)과 그의 제자들이 개발했습니다. 드롭아웃을 층에 적용하면 훈련하는 동안 층의 출력 특성을 랜덤하게 끕니다(즉, 0으로 만듭니다). 훈련하는 동안 어떤 입력 샘플에 대해 [0.2, 0.5, 1.3, 0.8, 1.1] 벡터를 출력하는 층이 있다고 가정해 보죠. 드롭아웃을 적용하면 이 벡터에서 몇 개의 원소가 랜덤하게 0이 됩니다. 예를 들면, [0, 0.5, 1.3, 0, 1.1]가 됩니다. "드롭아웃 비율"은 0이 되는 특성의 비율입니다. 보통 0.2에서 0.5 사이를 사용합니다. 테스트 단계에서는 어떤 유닛도 드롭아웃하지 않습니다. 훈련 단계보다 더 많은 유닛이 활성화되기 때문에 균형을 맞추기 위해 층의 출력 값을 드롭아웃 비율만큼 줄입니다.

tf.keras에서는 Dropout 층을 이용해 네트워크에 드롭아웃을 추가할 수 있습니다. 이 층은 바로 이전 층의 출력에 드롭아웃을 적용합니다.

IMDB 네트워크에 두 개의 Dropout 층을 추가하여 과대적합이 얼마나 감소하는지 알아 보겠습니다:

dpt_model = keras.models.Sequential([
    keras.layers.Dense(16, activation=tf.nn.relu, input_shape=(NUM_WORDS,)),
    keras.layers.Dropout(0.5),
    keras.layers.Dense(16, activation=tf.nn.relu),
    keras.layers.Dropout(0.5),
    keras.layers.Dense(1, activation=tf.nn.sigmoid)
])

dpt_model.compile(optimizer='adam',
                  loss='binary_crossentropy',
                  metrics=['accuracy','binary_crossentropy'])

dpt_model_history = dpt_model.fit(train_data, train_labels,
                                  epochs=20,
                                  batch_size=512,
                                  validation_data=(test_data, test_labels),
                                  verbose=2)
Train on 25000 samples, validate on 25000 samples
Epoch 1/20
25000/25000 - 5s - loss: 0.6385 - acc: 0.6343 - binary_crossentropy: 0.6385 - val_loss: 0.5177 - val_acc: 0.8501 - val_binary_crossentropy: 0.5177
Epoch 2/20
25000/25000 - 5s - loss: 0.4702 - acc: 0.8056 - binary_crossentropy: 0.4702 - val_loss: 0.3525 - val_acc: 0.8821 - val_binary_crossentropy: 0.3525
Epoch 3/20
25000/25000 - 5s - loss: 0.3575 - acc: 0.8685 - binary_crossentropy: 0.3575 - val_loss: 0.3044 - val_acc: 0.8808 - val_binary_crossentropy: 0.3044
Epoch 4/20
25000/25000 - 5s - loss: 0.2930 - acc: 0.9021 - binary_crossentropy: 0.2930 - val_loss: 0.2770 - val_acc: 0.8884 - val_binary_crossentropy: 0.2770
Epoch 5/20
25000/25000 - 6s - loss: 0.2504 - acc: 0.9172 - binary_crossentropy: 0.2504 - val_loss: 0.2767 - val_acc: 0.8888 - val_binary_crossentropy: 0.2767
Epoch 6/20
25000/25000 - 5s - loss: 0.2126 - acc: 0.9304 - binary_crossentropy: 0.2126 - val_loss: 0.2834 - val_acc: 0.8868 - val_binary_crossentropy: 0.2834
Epoch 7/20
25000/25000 - 5s - loss: 0.1871 - acc: 0.9398 - binary_crossentropy: 0.1871 - val_loss: 0.2992 - val_acc: 0.8854 - val_binary_crossentropy: 0.2992
Epoch 8/20
25000/25000 - 5s - loss: 0.1669 - acc: 0.9461 - binary_crossentropy: 0.1669 - val_loss: 0.3161 - val_acc: 0.8825 - val_binary_crossentropy: 0.3161
Epoch 9/20
25000/25000 - 4s - loss: 0.1492 - acc: 0.9508 - binary_crossentropy: 0.1492 - val_loss: 0.3344 - val_acc: 0.8817 - val_binary_crossentropy: 0.3344
Epoch 10/20
25000/25000 - 4s - loss: 0.1345 - acc: 0.9569 - binary_crossentropy: 0.1345 - val_loss: 0.3577 - val_acc: 0.8812 - val_binary_crossentropy: 0.3577
Epoch 11/20
25000/25000 - 4s - loss: 0.1210 - acc: 0.9601 - binary_crossentropy: 0.1210 - val_loss: 0.3667 - val_acc: 0.8805 - val_binary_crossentropy: 0.3667
Epoch 12/20
25000/25000 - 4s - loss: 0.1119 - acc: 0.9620 - binary_crossentropy: 0.1119 - val_loss: 0.3866 - val_acc: 0.8778 - val_binary_crossentropy: 0.3866
Epoch 13/20
25000/25000 - 4s - loss: 0.1028 - acc: 0.9661 - binary_crossentropy: 0.1028 - val_loss: 0.4139 - val_acc: 0.8786 - val_binary_crossentropy: 0.4139
Epoch 14/20
25000/25000 - 4s - loss: 0.0951 - acc: 0.9679 - binary_crossentropy: 0.0951 - val_loss: 0.4153 - val_acc: 0.8762 - val_binary_crossentropy: 0.4153
Epoch 15/20
25000/25000 - 4s - loss: 0.0878 - acc: 0.9724 - binary_crossentropy: 0.0878 - val_loss: 0.4529 - val_acc: 0.8767 - val_binary_crossentropy: 0.4529
Epoch 16/20
25000/25000 - 5s - loss: 0.0811 - acc: 0.9724 - binary_crossentropy: 0.0811 - val_loss: 0.4738 - val_acc: 0.8772 - val_binary_crossentropy: 0.4738
Epoch 17/20
25000/25000 - 5s - loss: 0.0777 - acc: 0.9740 - binary_crossentropy: 0.0777 - val_loss: 0.4998 - val_acc: 0.8756 - val_binary_crossentropy: 0.4998
Epoch 18/20
25000/25000 - 5s - loss: 0.0734 - acc: 0.9748 - binary_crossentropy: 0.0734 - val_loss: 0.4926 - val_acc: 0.8740 - val_binary_crossentropy: 0.4926
Epoch 19/20
25000/25000 - 5s - loss: 0.0715 - acc: 0.9750 - binary_crossentropy: 0.0715 - val_loss: 0.5374 - val_acc: 0.8756 - val_binary_crossentropy: 0.5374
Epoch 20/20
25000/25000 - 5s - loss: 0.0631 - acc: 0.9796 - binary_crossentropy: 0.0631 - val_loss: 0.5771 - val_acc: 0.8761 - val_binary_crossentropy: 0.5771
plot_history([('baseline', baseline_history),
              ('dropout', dpt_model_history)])

png

드롭아웃을 추가하니 기준 모델보다 확실히 향상되었습니다.

정리하면 신경망에서 과대적합을 방지하기 위해 가장 널리 사용하는 방법은 다음과 같습니다:

  • 더 많은 훈련 데이터를 모읍니다.
  • 네트워크의 용량을 줄입니다.
  • 가중치 규제를 추가합니다.
  • 드롭아웃을 추가합니다.

이 문서에서 다루지 않은 중요한 방법 두 가지는 데이터 증식(data-augmentation)과 배치 정규화(batch normalization)입니다.

#@title MIT License
#
# Copyright (c) 2017 François Chollet
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.