일단은 로컬에서 진행했지만 성.공.적

본 문서는 [케라스 창시자에게 배우는 딥러닝] 책을 기반으로 하고 있으며, subinium(본인)이 정리하고 추가한 내용입니다. 생략된 부분과 추가된 부분이 있으니 추가/수정하면 좋을 것 같은 부분은 댓글로 이야기해주시면 감사하겠습니다.

본 장은 케라스에 대한 소개와 케라스로 할 수 있는 예시를 진행합니다.

3.4 영화 리뷰 분류: 이진 분류 예제

two-class classfication 또는 binary classfication은 가장 기본적인 머신 러닝 문제입니다. 본 장에서는 리뷰 텍스트를 기반으로 영화 리뷰를 positive와 negative로 분류하는 방법을 배웁니다.

3.4.1 IMDB 데이터셋

IMDB(Internet Movie Database)는 양극단의 5만 개의 리뷰로 이루어진 데이터입니다. 25,000개의 훈련 데이터, 25,000개의 테스트 데이터로 이루어진 데이터입니다.

당연하지만 훈련 데이터와 테스트 데이터는 같이 사용하면 안됩니다. 훈련과 다른 데이터로 진행을 해야 분류가 확실하게 일어나고 있는 지를 판단할 수 있기 때문입니다. 마치 답지보고 시험치면 안되는 것과 마찬가지입니다.

IMDB는 MNIST와 같이 이미 기존 데이터셋에 있습니다. 그렇기에 처음 실행한다면 다운로드 받는 작업이 진행됩니다.

# 코드 3-1 IMDB 데이터셋 로드하기
from keras.datasets import imdb

(train_data, train_labels), (test_data, test_labels)  imdb.load_data(num_words=10000)

num_words = 10000 매개변수는 훈련데이터에서 가장 자주 나타나는 단어 1만 개만 사용하겠다는 의미입니다. train_datatest_data는 리뷰의 목록으로 위에서 구한 10000개의 데이터 리스트에서 인덱스를 가지고 있는 리스트입니다. train_labelstest_labels는 긍정은 1, 부정은 0으로 각 리뷰에 대한 긍정/부정에 대한 value를 담고 있습니다.

3.4.2 데이터 준비

신경망에 숫자 리스트를 직접적으로 넣을 수는 없기에 텐서로 변경하는 작업이 필요합니다.

  1. 같은 길이가 되도록 리스트에 패딩을 추가하고 (samples, sequence_length) 크기의 정수 텐서로 변환합니다. 그 다음 정수 텐서를 다룰 수 있는 층을 신경망의 첫 번째 층으로 사용합니다. (Embedding 층)
  2. 리스트를 one-hot encoding하여 0과 1 벡터로 변환합니다. 이는 마치 radix sort와 비슷하게 단어의 개수 10000개와 같은 크기의 벡터를 0으로 채우고, 있는 단어 인덱스에만 1을 넣는 방식입니다. 리뷰가 10000 단어 이하라면 2번의 경우가 더 메모리를 많이 사용하는 것 입니다.

책에서는 2번 방법으로 진행합니다.

# 코드 3-2 정수 시퀸스를 이진 행렬로 인코딩하기
import numpy as np

def vectorize_sequences(sequences, dimension=10000):
  results = np.zeros((len(sequences), dimension))
  for i, sequence in enumerate(sequences):
    results[i, sequence] = 1.
  return results

x_train = vectorize_sequences(train_data)
x_test = vectorize_sequences(test_data)

# 레이블 벡터 변환
y_train = np.asarray(train_labels).astype('float32')
y_test = np.asarray(test_labels).astype('float32')

3.4.3 신경망 모델 만들기

입력 데이터가 벡터고, 레이블은 스칼라입니다. 이런 문제에서는 relu 활성화 함수를 사용한 완전 연결 층을 이용하면 됩니다.

코드에서는 Dense(16, activation='relu') 와 같이 쓸 수 있습니다. 매개변수 16은 은닉 유닛(hidden_unit) 의 개수입니다. 하나의 은닉 유닛은 층이 나타내는 표현 공간에서 하나의 차원이 됩니다.

2장에서는 relu 활성화 함수를 사요완 Dense 층을 다음 텐서 연산을 연결하여 구현했습니다.

output = relu(dot(W, input)+b)

16개의 은닉 유닛이 있다는 것은 가중치 행렬 W 크기가 (input_dimension, 16)이라는 뜻 입니다. 그렇다면 위 연산의 결과는 입력을 16차원으로 출력을 하고, 편향 벡터 b를 더 한후 relu 연산을 적용하는 것입니다. 즉 은닉 유닛은 신경망의 데이터를 얼마나 복잡하게 표현할 것인가 를 결정할 수 있다고 생각하면 됩니다.

Dense 층을 쌓을 때 두 가지 중요한 구조상의 결정이 필요합니다.

  • 얼마나 많은 층을 사용할 것인가?
  • 각 층에 얼마나 많은 은닉 유닛을 둘 것인가?

이 부분은 4장에서 더 본격적으로 다룬다고 합니다. 이번 문제에서는 다음과 같은 구조를 사용합니다.

  • 16개의 은닉 유닛을 가진 2개의 층
  • 현재 리뷰의 감정을 스칼라 값의 예측으로 출력하는 세 번째 층

즉 앞의 층은 relu를 이용하고, 마지막 층은 확률 표현을 위해 시그모이드 함수를 사용합니다. 이제 층은 다음과 같이 서술할 수 있습니다. (수업에서 C로 하던걸 이렇게 편하게 하다니 상당합니다.)

# 코드 3-3 모델 정의하기
from keras import models
from keras import layers

model = models.Sequential()
model.add(layers.Dense(16, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(16, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

이제 앞에서 말한 것과 같이 손실 함수와 옵티마이저를 선택해야 합니다. 다음과 같이 정리할 수 있을 것 같습니다.

  • 이진 분류 문제 + 확률 출력 : binary_crossentropy 손실이 적합
  • RMSprop 옵티마이저 사용
  • 측정지표 : accuracy

여기서 RMSprop은 다음 자료에서 더 자세하게 알 수 있습니다.

A Look at Gradient Descent and RMSprop Optimizers

기존의 경사 하강법보다 빠르다는 점 정도만 알고 일단 진행해도 괜찮을 것 같습니다. 위의 3가지를 설정하였으니 compile함수를 통해 학습 과정을 설정하면 됩니다.

# 코드 3-4 모델 컴파일하기
model.compile(optimizer='rmsprop',
              loss='binary_crossentropy',
              metrics=['accuracy'])

compile 함수에서 사용자의 목적에 따라 custom이 가능한데, 그 부분은 다음과 같습니다.

# 코드 3-5 옵티마이저 설정하기
from keras import optimizers

model.compile(optimizer=optimizers.RMSprop(lr=0.001),
              loss='binary_crossentropy',
              metrics=['accuracy'])
# 코드 3-6 손실과 측정을 함수 객체로 지정하기
from keras import losses
from keras import metrics

model.compile(optimizer=optimizers.RMSprop(lr=0.001),
              loss=losses.binary_crossentropy,
              metrics=[metrics.binary_accuracy])

저는 그냥 default로 하였습니다. 역시 모를 때는 기본값이 최고입니다.ㅎㅎ

3.4.4 훈련 검증

훈련하는 동안 처음 본 데이터에 대한 모델의 정확도를 측정하기 위해서는 원본 훈련 데이터에서 10,000의 샘플을 떼어 검증 세트를 만들어야 합니다.

# 코드 3-7 검증 세트 준비하기
x_val = x_train[:10000]
partial_x_train = x_train[10000:]

y_val = y_train[:10000]
partial_y_train = y_train[10000:]

모델을 512개의 샘플씩 미니 배치를 만들어 20번의 에포크 동안 훈련시킵니다. 또한 동시에 따로 떼어 놓은 10,000개의 샘플에서 손실과 정확도를 측정합니다. 케라스에서는 validation_data 매개변수에 검증 데이터를 전달합니다.

# 코드 3-8 모델 훈련하기
history = model.fit(partial_x_train,
                    partial_y_train,
                    epochs=20,
                    batch_size=512,
                    validation_data=(x_val, y_val))

history에는 훈련하는 동안 발생한 모든 정보를 담고 있는 딕셔너리인 history 속성을 가지고 있습니다. 속성에는 4가지가 포함되어 있습니다.

  • acc : 훈련 정확도
  • loss : 훈련 손실값
  • val_acc : 검증 손실값
  • val_loss : 검증 정확도

이제 matplotlib으로 그래프를 그려 결과를 확인해봅시다.

# 코드 3-9 훈련과 검증 손실 그리기
import matplotlib.pyplot as plt

acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(acc) + 1)

plt.plot(epochs, loss, 'bo', label='Training loss')

plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()

plt.show()

우선 훈련과 검증 손실 그래프입니다. imdb 다운은 이미 되어있다는 가정하에 그래프 그리기까지 40초 내외로 완료되었습니다. epoch 당 1~2초정도 소모되었습니다.

왜 빈칸?

터미널에서 plt.show() 결과, 빈 화면으로 나옵니다. 예전에는 matplotlib이 안됬는데, 이제는 결과가 안나오네요. 저는 이전에 DB과목 과제에서 했던 방식으로 png파일로 저장해서, 이미지 파일로 결과를 확인했습니다. 결과는 다음과 같습니다.

# 변경파트
fig1 = plt.gcf()
plt.show()
plt.draw()
fig1.savefig('fig1.png', dpi=100)
훈련과 검증 손실

비슷한 방법으로 훈련과 검증 손실까지 그려봅시다.

# 코드 3-10 훈련과 검증 정확도 그리기 (이미지 저장으로 변경)
plt.clf()   # 그래프를 초기화합니다
acc = history_dict['acc']
val_acc = history_dict['val_acc']

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend()

fig2 = plt.gcf()

plt.draw()
fig2.savefig('fig2.png', dpi=100)
훈련과 검증 정확도

훈련에 대한 결과의 경우에는 기대치만큼 나오는 것을 볼 수 있지만, 검증 손실과 정확도는 그렇지는 않습니다. 그 이유는 overfitting이 일어나 훈련데이터에 대한 과도한 최적화로 일반 데이터에서는 정확도가 떨어지기 때문입니다. 3~4번째부터 경향성이 보이니 4 번째 epoch 동안만 훈련을 진행하여 결과를 봅니다. fit()함수를 재호출하면 학습된 가중치에서 다시 시작하니, 처음부터 층을 쌓아야 합니다.

저는 귀찮아서 python파일을 하나 더 만들어서 진행했습니다. 이제 부분 데이터가 아닌 전체 데이터로 훈련과 학습을 진행해봅시다.

# 코드 3-11 모델을 처음부터 다시 훈련하기
model = models.Sequential()
model.add(layers.Dense(16, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(16, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop',
              loss='binary_crossentropy',
              metrics=['accuracy'])

model.fit(x_train, y_train,epochs=4,batch_size=512)
results = model.evaluate(x_test, y_test)
print(results)

결과는 아래와 같습니다.

Using TensorFlow backend.
Epoch 1/4
2019-01-07 01:49:31.627801: I tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 FMA
25000/25000 [==============================] - 2s 77us/step - loss: 0.4493 - acc: 0.8163
Epoch 2/4
25000/25000 [==============================] - 1s 54us/step - loss: 0.2550 - acc: 0.9095
Epoch 3/4
25000/25000 [==============================] - 1s 53us/step - loss: 0.1969 - acc: 0.9294
Epoch 4/4
25000/25000 [==============================] - 1s 54us/step - loss: 0.1668 - acc: 0.9410
25000/25000 [==============================] - 2s 61us/step
[0.2944643885755539, 0.88324]

새로한 학습은 15초 내외로 걸렸고, 88%의 정확도를 달성했습니다.

3.4.5 훈련된 모델로 새로운 데이터에 대해 예측하기

predict 메서드를 이용하면 어떤 리뷰가 긍정일 확률을 예측할 수 있습니다.

3.4.6 추가 실험

생략

3.4.7 정리

  • 원본 데이터를 신경망에 텐서로 주입하기 위해서는 많은 전처리가 필요하다.
  • rmsprop은 문제와 상관없이 좋은 옵티마이저입니다.
  • 과대적합을 막기 위해 항상 훈련 데이터 이외의 데이터에서 성능 모니터링이 필요합니다.

3.5 뉴스 기사 분류: 다중 분류 문제

이제는 2개 이상의 클래스를 분류하는 문제입니다. 이번 문제는 로이터 뉴스를 46개의 상호 배타적인 토픽으로 분류하는 신경망을 만드는 것입니다. 1개의 뉴스는 1개의 토픽으로 매칭되므로 단일 레이블 다중 분류 문제라고 할 수 있습니다.

3.5.1 로이터 데이터셋

1986년에 로이터에서 공개한 짧은 뉴스 기사와 토픽의 집합인 로이터 데이터 셋 은 텍스트 분류를 위해 사용되는 well-known 데이터셋입니다. 46개의 토픽을 가지고 있으며 토픽에 따라 샘플의 개수는 다르며, 각 토픽은 최소 10개의 샘플을 가지고 있습니다.

역시 keras에서는 데이터를 가지고 있습니다. 이런 점에서 케라스는 학습용으로 적합한 것 같습니다. IMDB와 같이 가장 자주 등장하는 단어 10,000개로 제한하여 학습을 진행합니다.

# 로이터 데이터셋 로드하기
from keras.datasets import reuters

(train_data, train_labels), (test_data, test_labels) = reuters.load_data(num_words=10000)

8,982개의 훈련 샘플과 2246개의 테스트 샘플이 있습니다.

3.5.2 데이터 준비

3.4에서와 같이 데이터를 벡터로 변환합니다. 레이블을 벡터로 바꾸는 것도 비슷한 방식으로 변환합니다. 이번엔 레이블값도 원-핫 인코딩을 통해 데이터를 변환해봅시다.

# 코드 3-14 데이터 인코딩하기
import numpy as np

def vectorize_sequences(sequences, dimension=10000):
  results = np.zeros((len(sequences), dimension))
  for i, sequence in enumerate(sequences):
    results[i, sequence] = 1.
  return results

x_train = vectorize_sequences(train_data)
x_test = vectorize_sequences(test_data)

# 레이블 벡터 변환
def to_one_hot(labels, dimension=46):
  results = np.zeros((len(labels), dimension))
  for i, label in enumerate(labels):
    results[i, label] = 1.
  return results

one_hot_train_labels = to_one_hot(train_labels)
one_hot_test_labels = to_one_hot(test_labels)

케라스에서는 이를 위한 내장 함수가 있습니다.

# keras 내장 함수
from keras.utils.np_utils  import to_categorical

one_hot_train_labels = to_categorical(train_labels)
one_hot_test_labels = to_categorical(test_labels)

3.5.3 모델 구성

imdb와 거의 같지만, 이번에는 출력결과가 2개로 나누어지는 것이 아닌 46개라는 차이점이 있습니다. 16차원은 46개의 데이터를 구분하지 못할 수 있으니 이번에는 64개의 유닛을 이용합니다.

# 코드 3-15 모델 정의하기
from keras import models
from keras import layers

model = models.Sequential()
model.add(layers.Dense(64, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(46, activation='softmax'))

이번에는 차이점이 두 가지를 주목해야합니다.

  1. 마지막 출력이 46입니다. 각 입력 샘플에 대해 46차원 벡터를 출력한다는 뜻입니다.
  2. softmax 활성화 함수를 사용하였습니다. 이는 46개의 출력 클래스에 대한 확률 분포를 출력하며, 총합은 1입니다.

이런 분류에서 손실 함수는 categorical_crossentropy를 사용합니다. 이 함수는 두 확률 분포 사이의 거리를 측정하고, 확률 분포 사의 거리를 최소화하며 최적화 합니다.

# 코드 3-16 모델 컴파일하기
model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics='accuracy')

3.5.4 훈련 검증

이번엔 비교적 데이터가 적기 때문에 훈련 데이터에서 1,000 개의 샘플을 떼어서 검증 세트로 사용합니다.

# 코드 3-17 검증 세트 준비하기
x_val = x_train[:1000]
partial_x_train = x_train[1000:]

y_val = one_hot_train_labels[:1000]
partial_y_train = one_hot_train_labels[1000:]

이제 20번 에포크로 모델을 훈련시킵니다.

# 코드 3-18 모델 훈련하기
history = model.fit(partial_x_train,
                    partial_y_train,
                    epochs=20,
                    batch_size=512,
                    validation_data=(x_val, y_val))

마지막으로 이전과 같이 그래프를 그려 확인합니다. 위의 코드와 같으니 그대로 가져다 쓰면 됩니다. 다음은 결과 입니다. 이번 문제는 16초동안 학습이 진행되었습니다.

훈련과 검증 손실
훈련과 검증 정확도

위 그래프에서 알 수 있듯이 이번엔 8-9번째에서 오버피팅이 이뤄지는 것을 알 수 있습니다. 따라서 전체 데이터에 대한 테스트는 9번의 epoch만 진행합니다.

# 코드 3-21 모델을 처음부터 다시 훈련하기
model = models.Sequential()
model.add(layers.Dense(64, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(46, activation='softmax'))

model.compile(optimizer='rmsprop',
              loss='categorical_crossentropy',
              metrics=['accuracy'])
model.fit(partial_x_train,
          partial_y_train,
          epochs=9,
          batch_size=512,
          validation_data=(x_val, y_val))
results = model.evaluate(x_test, one_hot_test_labels)

print(results)

실행결과 다음과 같은 결과를 얻을 수 있습니다.

Using TensorFlow backend.
Train on 7982 samples, validate on 1000 samples
Epoch 1/9
2019-01-07 02:52:46.229536: I tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 FMA
7982/7982 [==============================] - 1s 131us/step - loss: 2.4997 - acc: 0.4899 - val_loss: 1.6813 - val_acc: 0.6480
Epoch 2/9
7982/7982 [==============================] - 1s 76us/step - loss: 1.3915 - acc: 0.7038 - val_loss: 1.2790 - val_acc: 0.7190
Epoch 3/9
7982/7982 [==============================] - 1s 71us/step - loss: 1.0487 - acc: 0.7699 - val_loss: 1.1181 - val_acc: 0.7610
Epoch 4/9
7982/7982 [==============================] - 1s 71us/step - loss: 0.8246 - acc: 0.8282 - val_loss: 1.0217 - val_acc: 0.7760
Epoch 5/9
7982/7982 [==============================] - 1s 71us/step - loss: 0.6599 - acc: 0.8637 - val_loss: 0.9688 - val_acc: 0.7970
Epoch 6/9
7982/7982 [==============================] - 1s 71us/step - loss: 0.5254 - acc: 0.8931 - val_loss: 0.9200 - val_acc: 0.8090
Epoch 7/9
7982/7982 [==============================] - 1s 71us/step - loss: 0.4291 - acc: 0.9118 - val_loss: 0.9108 - val_acc: 0.8030
Epoch 8/9
7982/7982 [==============================] - 1s 72us/step - loss: 0.3497 - acc: 0.9277 - val_loss: 0.8937 - val_acc: 0.8150
Epoch 9/9
7982/7982 [==============================] - 1s 72us/step - loss: 0.2893 - acc: 0.9386 - val_loss: 0.9128 - val_acc: 0.8090
2246/2246 [==============================] - 0s 94us/step
[1.002043602833871, 0.7787177204183457]

대략 78%의 정확도를 가집니다. 원래 데이터를 불균등한 분포에 따라 랜덤으로 분류하는 경우 18%의 확률을 가지는 것에 비해 좋은 결과 입니다.

3.5.5 새로운 데이터에 대해 예측하기

생략

3.5.6 레이블과 손실을 다루는 다른 방법

앞에서 언급한 정수 텐서로 변환은 다음과 같이 할 수 있습니다.

y_train = np.array(train_labels)
y_test = np.array(test_labels)

이 경우 손실 함수만 변경해주면 됩니다.

model.compile(optimizer='rmsprop', loss='sparse_categorical_crossentropy', metrics=['acc'])

기존 categorical_crossentropy와 인터페이스만 다를 뿐이고, 수학적으로는 같습니다.

3.5.7 충분히 큰 중간층을 두어야 하는 이유

출력이 46차원이기 때문에 중간층의 히든 유닛이 46개에 비해 많이 적으면 정보의 병목이 발생합니다. 중간층을 46차원보다 매우 적은 4차원으로 진행할 경우, 진행해본 결과

[1.5965227675246854, 0.6767586821015138]

의 결과를 얻을 수 있었습니다. 거의 10% 정도 차이가 나는 결과입니다.

3.5.8 추가 실험

생략

3.5.9 정리

  • softmax 사용
  • 정보의 병목 방지

3.6 주택 가격 예측: 회귀 문제

이번 문제는 분류가 아닌 회귀(regression) 문제입니다. 개별적인 레이블이 아닌 연속적인 값을 예측하는 것입니다.

3.6.1 보스턴 주택 가격 데이터셋

1970년대 중반 보스턴 외곽 지역의 범죄율, 지방세율 등의 데이터가 주어졌을 때 주택 가격의 중간 값을 예측하는 문제입니다. 이 문제의 데이터는 다음과 같은 특징을 지닙니다.

  • 데이터 포인트가 506개로 비교적 적음
  • 404개의 훈련 샘플, 102개의 테스트 샘플
  • 입력 데이터의 특성의 스케일이 서로 다름. ([0,1],[1,12],[1,100] 등)
  • 13개의 특성을 가짐

일단 데이터를 받아봅시다.

# 코드 3-24 보스턴 주택 데이터셋 로드하기
from keras.datasets import boston_housing

(train_data, train_targets), (test_data, test_targets) =  boston_housing.load_data()

타깃값의 기본단위는 1,000 달러입니다.

3.6.2 데이터 준비

상이한 스케일을 가진 값을 신경망에 주입하면 학습을 어렵게 만듭니다. 이런 데이터는 특성별로 정규화(표준화)가 필요합니다. 각 특성을 특성의 평균을 빼고 표준 편차로 나눕니다. 그렇다면 특성의 평균은 0, 표준 편차는 1인 데이터셋으로 만들어집니다. 본 정규화는 훈련 데이터 값만 이용해야 함을 주의해야합니다.

# 코드 3-25 데이터 정규화하기
mean = train_data.mean(axis=0)
train_data -= mean
std = train_data.std(axis=0)
train_data /= std

test_data -= mean
test_data /= std

3.6.3 모델 구성

샘플 개수가 적어 64개의 유닛을 가진 2개의 은닉 층으로 네트워크르 구성합니다. 데이터가 적으므로 오버피팅을 막기 위한 방법입니다.

from keras import models
from keras import layers

def build_model():
    model = models.Sequential()
    model.add(layers.Dense(64, activation='relu',
                           input_shape=(train_data.shape[1],)))
    model.add(layers.Dense(64, activation='relu'))
    model.add(layers.Dense(1))
    model.compile(optimizer='rmsprop', loss='mse', metrics=['mae'])
    return model

이 코드에는 다음과 같은 점을 확인할 수 있습니다.

  • 동일한 모델을 여러번 생성하기 위한 함수
  • 출력에 dense에 활성화 함수가 없음
  • 이 방법은 전형적인 스칼라 회귀를 위한 구성입니다.
  • 평균 제곱 오차(mse) 손실 함수를 이용함
  • 평균 절대 오차(MAE) 로 모니터링함 (예측과 타깃의 절대적 거리)

3.6.4 K-겹 검증을 사용한 훈련 검증

이 문제는 데이터가 매우 작기 때문에 검증 세트와 훈련 세트에서 어떤 데이터 포인트가 선택되었는지에 따라 검증 점수가 크게 달라집니다. 이런 상황에서 좋은 방법은 K-겹 교차 검증(K-fold cross-validation) 을 사용하는 것입니다.

  • 데이터를 K개의 fold로 나누고, K개의 모델을 만듬
  • 각각의 모델에서 K-1개의 분할에서 훈련하고, 나머지 분할에서 평가
  • 모델의 검증 점수는 K개의 검증 점수 평균

이 문제는 코드로 더 확인해봅시다.

# 코드 3-27 K-겹 검증하기
import numpy as np

k = 4
num_val_samples = len(train_data) // k
num_epochs = 100
all_scores = []
for i in range(k):
    print('처리중인 폴드 #', i)
    # 검증 데이터 준비: k번째 분할
    val_data = train_data[i * num_val_samples: (i + 1) * num_val_samples]
    val_targets = train_targets[i * num_val_samples: (i + 1) * num_val_samples]

    # 훈련 데이터 준비: 다른 분할 전체
    partial_train_data = np.concatenate(
        [train_data[:i * num_val_samples],
         train_data[(i + 1) * num_val_samples:]],
        axis=0)
    partial_train_targets = np.concatenate(
        [train_targets[:i * num_val_samples],
         train_targets[(i + 1) * num_val_samples:]],
        axis=0)

    # 케라스 모델 구성(컴파일 포함)
    model = build_model()
    # 모델 훈련(verbose=0 이므로 훈련 과정이 출력되지 않습니다)
    model.fit(partial_train_data, partial_train_targets,
              epochs=num_epochs, batch_size=1, verbose=0)
    # 검증 세트로 모델 평가
    val_mse, val_mae = model.evaluate(val_data, val_targets, verbose=0)
    all_scores.append(val_mae)

print(all_scores)
print(np.mean(all_scores))

다음과 같은 결과가 나왔습니다. 1분 30초정도 시간이 걸렸습니다. 중간에 저 문구는 무시해도 괜찮을 것 같습니다.

문구이유

Using TensorFlow backend.
처리중인 폴드 # 0
2019-01-07 03:23:45.417319: I tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 FMA
처리중인 폴드 # 1
처리중인 폴드 # 2
처리중인 폴드 # 3
[2.13456125306611, 2.178143912022657, 2.8739126531204375, 2.4534413932573678]
2.410014802866643

검증 세트에 따라 폭이 꽤 크므로 평균값을 이용하는게 더 신뢰할 만합니다. 신경망을 이번엔 500 epoch만큼 훈련하고, 그에 따라 개선되는 정도를 확인해봅시다. 그래프 개형상 200~300정도만 해도 괜찮을 것 같지만, 시간 측정을 위해 저는 500으로 두고 진행했습니다.

# 코드 3-28 각 폴드에서 검증 점수를 로그에 저장하기
num_epochs = 500
all_mae_histories = []
for i in range(k):
    print('처리중인 폴드 #', i)
    # 검증 데이터 준비: k번째 분할
    val_data = train_data[i * num_val_samples: (i + 1) * num_val_samples]
    val_targets = train_targets[i * num_val_samples: (i + 1) * num_val_samples]

    # 훈련 데이터 준비: 다른 분할 전체
    partial_train_data = np.concatenate(
        [train_data[:i * num_val_samples],
         train_data[(i + 1) * num_val_samples:]],
        axis=0)
    partial_train_targets = np.concatenate(
        [train_targets[:i * num_val_samples],
         train_targets[(i + 1) * num_val_samples:]],
        axis=0)

    # 케라스 모델 구성(컴파일 포함)
    model = build_model()
    # 모델 훈련(verbose=0 이므로 훈련 과정이 출력되지 않습니다)
    history = model.fit(partial_train_data, partial_train_targets,
                        validation_data=(val_data, val_targets),
                        epochs=num_epochs, batch_size=1, verbose=0)
    mae_history = history.history['val_mean_absolute_error']
    all_mae_histories.append(mae_history)

그 후 모든 폴드에 대해 epoch의 MAE 점수 평균을 계산합니다.

# 코드 3-29 k-겹 검증 점수 평균을 기록하기
average_mae_history = [
    np.mean([x[i] for x in all_mae_histories]) for i in range(num_epochs)]

그리고 그래프를 그려봅시다.

# 코드 3-30 검증 점수 그래프
import matplotlib.pyplot as plt

plt.plot(range(1, len(average_mae_history) + 1), average_mae_history)
plt.xlabel('Epochs')
plt.ylabel('Validation MAE')

fig5 = plt.gcf()

plt.draw()
fig5.savefig('fig5.png', dpi=100)
에포크별 검증 MAE

위 그래프는 범위가 크고 변동이 심해 보기 어렵습니다. 따라서 다음과 같은 방법으로 그래프를 보기 좋게 그립니다.

  • 스케일이 다른 첫 10개의 데이터를 제외
  • 부드러운 곡선을 위해 지수 이동 평균(exponential moving average) 으로 대체

지수 이동 평균은 시계열 데이터를 부드럽게 만드는 기법으로, 이전에 계산된 이동 평균에 factor를 곱하고 현재 포인트에 (1-factor)만큼을 곱해 합산하는 방법입니다. 책 코드는 factor에 0.9로 설정하여 진행하였습니다.

# 코드 3-31 처음 10개의 데이터 포인트를 제외한 검증 점수 그리기
def smooth_curve(points, factor=0.9):
  smoothed_points = []
  for point in points:
    if smoothed_points:
      previous = smoothed_points[-1]
      smoothed_points.append(previous * factor + point * (1 - factor))
    else:
      smoothed_points.append(point)
  return smoothed_points

smooth_mae_history = smooth_curve(average_mae_history[10:])

plt.plot(range(1, len(smooth_mae_history) + 1), smooth_mae_history)
plt.xlabel('Epochs')
plt.ylabel('Validation MAE')
plt.show()

이번 코드는 약 9분 정도 걸렸습니다. k-fold라 시간복잡도가 큰 것 같습니다.

위 그래프에서 MAE는 40번째와 80번째에서 줄어드는 것을 멈춥니다. 이 지점으로 다시 오버피팅이 시작됩니다. 그렇기에 모델은 80 epoch만 진행하면 됩니다.

수정된 에포크별 검증 MAE

다음은 학습 코드와 결과입니다.

# 코드 3-32 최종 모델 훈련하기
model = build_model()
model.fit(train_data, train_targets,
          epochs=80, batch_size=16, verbose=0)
test_mse_score, test_mae_score = model.evaluate(test_data, test_targets)

print(test_mae_score)
102/102 [==============================] - 0s 214us/step
2.5375854735280954

마지막 학습은 터미널에서 따로 시간이 안나올 정도로 빠르게 진행됩니다. 10초 이하인것 같습니다. 실제 가격과 2,500달러 정도 차이가 나는 것을 확인할 수 있습니다. (기존 가격의 스케일은 1만에서 5만 사이의 값입니다.)

3.6.5 정리

  • 회귀에서는 평균 제곱 오차를 손실 함수를 자주 사용
  • 평가 지표는 평균 절대 오차를 일반적으로 사용
  • 특성 스케일이 다른 경우, 스케일을 전처리 단계에서 조정
  • 가용 데이터가 적은 경우 K-겹 검증 사용
  • 훈련 데이터가 적다면 오버피팅을 피하기 위하여 은닉 층을 줄임

3.7 요약

  • 일반적인 머신 러닝인 이진 분류, 다중 분류, 스칼라 회귀 작업
  • 원본 데이터를 신경망에 넣기 위해서는 전처리 필요
  • 스케일 조정
  • 훈련이 진행됨에 따라 오버피팅이 일어남
  • 훈련 데이터가 적은 경우, 적은 수의 은닉 층 : 보통 1개, 2개
  • 회귀는 분류와 다른 손실 함수와 평가 지표 사용
  • 적은 데이터는 K-겹 검증이 신뢰할 수 있는 모델 평가를 도와줌

Leave a Comment