본문 바로가기

AI/딥러닝(Deep Learning)

[딥러닝/DL]3. 신경망 모델 훈련

728x90
반응형

이번 글에서는 케라스 API를 사용하여 딥러닝 모델을 훈련하는데 필요한 다양한 도구들을 알아보자.

손실 곡선

fit() 메서드를 사용하여 모델을 훈련하면 출력의 마지막에 다음과 같은 메시지가 보인다.

노트북의 코드 셀은 print() 명령을 사용하지 않아도 fit() 메서드의 마지막 라인의 실행 결과를 자동으로 출력한다.

즉 이 메시지는 fit() 메서드의 실행 결과를 출력한 것이다.

fit() 메서드는 History 클래스 객체를 반환한다.

History 클래스 객체는 훈련 과정에서 계산한 지표가 저장되어 있다.

  • 손실, 정확도 값이 저장

이 값을 사용하여 그래프를 그려보자.

 

먼저 데이터(MNIST)를 불러온다.

from tensorflow import keras
from sklearn.model_selection import train_test_split

(train_input, train_target), (test_input, test_target) = keras.datasets.fashion_mnist.load_data()

train_scaled = train_input / 255.0

train_scaled, val_scaled, train_target, val_target = train_test_split(
    train_scaled, train_target, test_size=0.2, random_state=42)

모델을 만드는 함수를 작성한다.

# 층을 매개변수로 받아서 추가한다.
def model_fn(a_layer=None):
    model = keras.Sequential()
    model.add(keras.layers.Flatten(input_shape=(28, 28)))
    model.add(keras.layers.Dense(100, activation='relu'))
    if a_layer:
        model.add(a_layer)
    model.add(keras.layers.Dense(10, activation='softmax'))
    return model

간단한 모델을 만들어 본다.

model = model_fn()
model.summary()

모델을 컴파일 후 학습을 진행하고 History 객체를 history 변수에 저장한다.

model.compile(loss='sparse_categorical_crossentropy', metrics='accuracy')
history = model.fit(train_scaled, train_target, epochs=5, verbose=0)

history 객체에는 측정값이 담겨 있는 history 딕셔너리가 들어 있다.

이 딕셔너리에는 lossaccuracy가 들어 있다.(compile 할 때 metrics="accuracy"를 추가해서 accuracy가 들어 있음.)

  • loss : 에포크 마다의 손실값
  • accuracy : 에포크 마다 정확도

케라스는 기본적으로 에포크마다 손실 값을 계산한다.

이 loss 값들을 그래프로 시각화 해보자.

import matplotlib.pyplot as plt

plt.plot(history.history['loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.show()

정확도도 그래프로 그려보자

plt.plot(history.history['accuracy'])
plt.xlabel('epoch')
plt.ylabel('accuracy')
plt.show()

그래프를 통해 에포크마다 손실이 감소하고 정확도가 향상하는 것을 알 수 있다.

에포크를 늘려서 한번 손실 그래프를 그려보자.

plt.plot(history.history['loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.show()

예상대로 손실이 잘 감소한다.

 

검증 손실

경사 하강법에서는 과대/과소적합과 에포크 사이의 관계를 알아봤었다.

신경망은 모두 일종의 경사 하강법을 사용하기 때문에 동일한 개념이 적용된다.

에포크에 따른 과대/과소적합을 파악하려면 훈련 세트뿐만 아니라 검증세트에 대한 점수도 필요하다.

검증 세트의 손실값을 얻어보자.

fit() 메서드에 validation_data 매개변수에 검증 세트를 튜플 형태로 전달할 수 있다.

model = model_fn()
model.compile(loss='sparse_categorical_crossentropy', metrics='accuracy')

history = model.fit(train_scaled, train_target, epochs=20, verbose=0, 
                    validation_data=(val_scaled, val_target))

history.history 딕셔너리에 어떤 값이 들어있는지 확인해보자.

print(history.history.keys())

검증 세트의 손실값은 history.history의 val_loss에 있고 정확도는 val_accuracy에 있다.

이제 훈련 손실과 검증 손실을 한 그래프에 그려보자.

plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(['train', 'val'])
plt.show()

 

초기에 검증 손실이 감소하다가 다섯 번쨰 에포크 만에 다시 상승한다.

훈련 손실은 꾸준히 감소하기 때문에 전형적인 과대적합 모델이 만들어 진다.

검증 손실이 상승하는 시점을 가능한 뒤로 늦추면 검증 세트에 대한 손실이 줄어들 뿐만 아니라 검증 세트에 대한 정확도도 증가할 것 같다.

이런 과대적합을 완화시킬 수 있는 방법은 여러가지가 있다.

이러한 방법 중 옵티마이저를 Adam을 사용하여 과대적합을 줄일 수 있다.

  • Adam은 적응적 학습률을 사용하기 때문에 에포크가 진행되면서 학습률의 크기를 조정할 수 있다.

옵티마이저를 Adam으로 적용해 보고 훈련 손실과 검증 손실을 그래프에 다시 그려보자.

model = model_fn()
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics='accuracy')

history = model.fit(train_scaled, train_target, epochs=20, verbose=0, 
                    validation_data=(val_scaled, val_target))
                    
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(['train', 'val'])
plt.show()

드롭아웃(dropout)

딥러닝의 아버지인 제프리 힌턴(Geoffrey Hinton)이 소개한 방식다.

훈련 과정에서 층에 있는 일부 뉴런을 랜덤하게 꺼서(뉴런의 출력을 0으로 만든다.) 과대적합을 막는 방법이다.

얼마나 많은 뉴런을 드롭할지는 사람이 정해야하는 하이퍼파라미터다.

드롭아웃이 과대적합을 막는 이유는 다음과 같다.

  • 이전 층의 일부 뉴런이 랜덤하게 꺼지면 특정 뉴런에 과대하게 의존하는 것을 줄일 수 있고 모든 입력에 대해 주의를 기울이기 때문이다.
  • 일부 뉴런의 출력이 없을 수도 있다는 것을 감안하면 신경망은 더 안정적인 예측을 만들 수 있을 것으로 예상한다.

또 위의 그림을 보면 마치 2개의 신경망을 앙상블하는 것 처럼 볼 수 있다.

  • 앙상블은 과대적합을 막는 아주 좋은 기법이다.

이제 드롭아웃을 해보자.

케라스는 드롭아웃을 keras.layers 패키지 아래 Dropout 클래스로 제공한다.

  • 어떤 층 뒤에 드롭아웃을 두어 그 층의 출력을 랜덤하게 0으로 만든다.
  • 매개변수는 0~1(0~100%) 사이의 값을 준다.
  • 훈련된 모델파라미터는 없다.
model = model_fn(keras.layers.Dropout(0.3))
model.summary()

이제 모델을 학습하고 위에서 처럼 훈련 세트와 검증 세트의 에포크 당 손실값을 그래프로 그려보자.

model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics='accuracy')

history = model.fit(train_scaled, train_target, epochs=20, verbose=0, 
                    validation_data=(val_scaled, val_target))

plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(['train', 'val'])
plt.show()

과대적합이 줄었고 10번쨰 epoch부터 정도에서 검증 손실의 감소가 멈추지만 크게 상승하지 않고 유지되고 있다.

이 모델은 20번 에포크 되었기 때문에 결국 과대적합일 수 밖에 없다. 

과대적합을 되지 않은 모델을 얻기 위해 에포크 횟수를 10으로 줄이고 다시 훈련해야 할 것 같다.

 

모델 저장과 복원

에포크 횟수를 10으로 지정하고 모델을 재훈련하고 이 모델을 저장해보자.

model = model_fn(keras.layers.Dropout(0.3))
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics='accuracy')

history = model.fit(train_scaled, train_target, epochs=10, verbose=0, 
                    validation_data=(val_scaled, val_target))

 

훈련된 모델의 파라미터를 저장하는 간편한 save_weights() 메서드를 제공한다.

  • 기본적으로 텐서플로의 체크포인트 포맷을 저장한다.
  • 파일 확장자가 .h5일 경우 HDF5 포맷으로 저장한다.
model.save_weights('model-weights.h5')

모델 구조와 모델 파라미터를 함께 저장하는 save() 메서드도 제공한다.

  • 기본적으로 텐서플로의 SavedModel 포맷으로 저장한다.
  • 파일 확장자가 .h5일 경우 HDF5 포맷으로 저장한다.
model.save('model-whole.h5')

이제 만들어진 파일을 확인해보자.

# 현재 디렉토리의 파일 정보를 출력하는 명령어
# -al를 지정하면 상세 정보
# *.h5는 .h5로 끝나는 파일만 출력
!ls -al *.h5

이제 저장한 모델이 진짜 잘 저장되었는지 확인해보자

  • 먼저 훈련을 하지 않은 모델을 하나 만든다.
  • 이 모델에 이전에 저장한 모델 파라미터를 적재한다.
    • load_weights() 메서드를 통해 저장한 모델 파라미터를 저장할 수 있다.
model = model_fn(keras.layers.Dropout(0.3))
model.load_weights('model-weights.h5')

이제 이 모델을 사용해 검증 정확도를 확인해  보자.

예측을 수행하는 predict() 메서드는 샘플마다 10개의 클래스에 대한 확률을 반환한다.

  • MNIST 데이터셋이 다중분류이기 떄문

검증 세트의 샘플 개수가 12000개 이므로 predict()가 반환하는 값의 형태는 (12000,10)의 배열을 반환한다.

이렇게 반환된 확률 값 중 가장 큰 값의 인덱스를 골라 타깃 레이블과 비교하여 정확도를 계산한다.

  • argmax 함수는 배열에서 가장 큰 값의 인덱스를 반환한다.
    • argmax 함수의 axis=-1은 배열의 마지막 차원을 따라 최댓값을 고른다.
  • 이렇게 나온 배열 val_labels와 val_target를 비교한다.
    • 같으면 1, 다르면 0이 되고 이렇게 만들어진 배열의 요소 값들을 평균하면 정확도가 된다.
import numpy as np

val_labels = np.argmax(model.predict(val_scaled), axis=-1)
print(np.mean(val_labels == val_target))

검증세트의 정확도는 0.88075가 나왔다.

 

이제 아까 저장했던 모델 전체를 불러온다음 검증 세트의 정확도를 출력해보자.

model = keras.models.load_model('model-whole.h5')
# 텐서플로 2.3에서는 버그(https://github.com/tensorflow/tensorflow/issues/42890) 때문에 compile() 메서드를 호출해야 합니다.
# model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics='accuracy')

model.evaluate(val_scaled, val_target)

검증세트의 정확도는 0.88075가 나왔다.

 

동일한 정확도를 얻었다 == 저장이 제대로 되었다.

 

콜백(Callback)

앞의 과정을 돌이켜보면 20번의 에포크 동안 모델을 훈련하여 검증 점수가 상승하는 지점을 찾아 모델이 과대적합되지 않는 에포크만큼 다시 훈련했다.

한번 훈련할 때 알 수 있는 방법이 없을까? 

 

케라스의 콜백이 기능을 수행한다.

콜백은 훈련 과정 중간에 어떤 작업을 수행할 수 있게 하는 객체로 keras.callbacks 패키지 아래 있는 클래스들이다.

fit() 메서드callbacks 매개변수에 리스트로 전달하여 사용한다.

 

ModelCheckpoint 콜백은 기본적으로 최상의 검증 점수를 만드는 모델을 저장한다.

저장될 파일 이름을 'best-model.h5'로 지정하여 콜백을 적용해보자.

model = model_fn(keras.layers.Dropout(0.3))
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics='accuracy')

checkpoint_cb = keras.callbacks.ModelCheckpoint('best-model.h5')

model.fit(train_scaled, train_target, epochs=20, verbose=0, 
          validation_data=(val_scaled, val_target),
          callbacks=[checkpoint_cb])

이제 load_model() 함수로 저장된 모델을 불러와 예측을 수행보자.

model = keras.models.load_model('best-model.h5')
# 텐서플로 2.3에서는 버그(https://github.com/tensorflow/tensorflow/issues/42890) 때문에 compile() 메서드를 호출해야 합니다.
# model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics='accuracy')

model.evaluate(val_scaled, val_target)

이렇게 앞에 보다 더 쉽게 최상의 검증 점수를 낸 모델을 얻을 수 있다.

하지만 여전히 fit()은 20번의 에포크를 실행한다.

검증 점수가 상승하기 시작하면 과대적합이 더 커지기 때문에 계속 훈련할 필요가 없기 때문에 훈련을 계속할 필요가 없다.

이때 훈련을 중지할 수는 없을까? 

 

과대적합 전에 훈련을 중지하는 것을 조기 종료(Early Stopping)이라고 부르며, 딥러닝 분야에 널리 사용한다.

케라스는 조기 종료를 위한 콜백으로 EarlyStopping 콜백을 제공한다.

  • patience 매개변수는 검증 점수가 향상되지 않더라도 참을 에포크 횟수로 지정한다.
    • 예를 들어 patience=2로 설정하면 2번 연속 검증 점수가 향상되지 않으면 훈련을 중지한다.
  • restore_best_weights 매개변수를 True로 지정하면 가장 낮은 검증 손실을 낸 모델 파라미터로 되돌린다.

EarlyStopping 콜백ModelCheckpoint 콜백과 같이 사용하면 가장 낮은 검증 손실의 모델을 파일에 저장하고 검증 손실이 다시 상승할 때 훈련을 중지할 수 있다.

그리고 훈련을 중지한 다음 현재 모델의 파라미터를 최상의 파라미터로 되돌린다. 

model = model_fn(keras.layers.Dropout(0.3))
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics='accuracy')

checkpoint_cb = keras.callbacks.ModelCheckpoint('best-model.h5')
early_stopping_cb = keras.callbacks.EarlyStopping(patience=2,
                                                  restore_best_weights=True)

history = model.fit(train_scaled, train_target, epochs=20, verbose=0, 
                    validation_data=(val_scaled, val_target),
                    callbacks=[checkpoint_cb, early_stopping_cb])

훈련이 끝난 EarlyStopping 콜백 객체  early_stopping_cb에는 stopped_epoch 속성에서 몇 번째 에포크에서 훈련이 중지되었는지 알수 있다. 

# 조기 종료한 에포크 횟수
print(early_stopping_cb.stopped_epoch)

에포크 횟수는 0부터 시작하기 때문에 8은 9번쨰 에포크에서 훈련이 중지되었다는 것을 의미한다.

patience를 2로 지정했으므로 최상의 모델은 7번째 에포크 일 것이다.

  • patience=2 때문에 두번 연속으로 검증 점수가 향상되지 않을 때 종료한다.
  • 9번쨰 에포크에 종료했으므로 8,9번쨰 에포크에서 검증 점수가 향상되지 않았다.
  • 그러므로 그 전 에포크인 7번째 에포크가 최상의 검증 점수를 가진 에포크이다.

훈련 손실과 검증 손실을 그래프로 그려서 진짜 그런지 확인해보자.

plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(['train', 'val'])
plt.show()

진짜 7번째 에포크에 정확하게 두 그래프가 겹친다.

 

이제 마지막으로 조기종료로 얻은 모델을 사용해 검증 세트에 대한 성능을 알아보자.

model.evaluate(val_scaled, val_target)

 

728x90
반응형