티스토리 뷰

이전에 사이킷런에서 제공하는 일반적인 머신러닝 알고리즘은 좋은 성능을 내기 위해 매개변수를 조정하고 훈련하는 과정을 반복한다. 다소 모델의 구조가 어느 정도 고정되어 있다고 느껴진다. 

 

반면에 딥러닝에서는 층을 추가하고 층에 있는 뉴런의 개수와 활성화 함수를 결정하는 등의 과정이 있어 모델의 구조를 직접 만든다는 느낌이 훨씬 강하다. 

 

이번에는 Keras를 사용하여 모델을 훈련하는데 필요한 다양한 도구들을 알아보려고 한다. 

 

바로 시작해보자.


Keras의 fit() 메서드는 Histroy 객체를 반환하는데, 이 History 객체는 훈련 과정에서 계산한 지표, 즉 손실과 정확도 값이 저장되어 있다. 

 

이 값을 잘 사용하면 그래프를 그릴 수 있다.

 

우선, 데이터를 준비하고

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
)

 

아래와 같이 매개변수로 layer를 받아 모델을 만드는 model_fn() 함수를 선언하여 model을 하나 만든다.

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()
# == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == =
# flatten(Flatten)(None, 784)
# 0
# 
# dense(Dense)(None, 100)
# 78500
# 
# dense_1(Dense)(None, 10)
# 1010
# 
# == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == =
# Total
# params: 79, 510
# Trainable
# params: 79, 510
# Non - trainable
# params: 0

 

이제 fit() 함수의 리턴 값을 변수에 담아 확인해보자.

model.compile(loss='sparse_categorical_crossentropy', metrics='accuracy')
# verbose -> 훈련 과정 출력 조절, default는 1로 훈련 과정, 손실, 정확도를 콘솔에 표시하며
#  0으로 설정시 진행 과정을 표시하지 않음
history = model.fit(train_scaled, train_target, epochs=5, verbose=0)

print(history.history.keys())
# dict_keys(['loss', 'accuracy'])

 

history 객체가 손실과 정확도를 가지고 있다는 걸 알았으니, 이제 그래프를 그려보자.

 

- Loss

import matplotlib.pyplot as plt

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

 

- Accuracy

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


이제 에포크를 20으로 조금 늘려서 그래프를 확인해보자.

확실히 에포크를 늘리니, 정확도는 올라가고 손실은 내려갔다. 하지만 에포크가 늘어남에 따라 훈련 데이터에 과대적합이 되었는 지를 파악해야 한다.

 

확인해보자.

 

아래와 같이 fit() 메서드에 검증 데이터를 넣어 에포크마다 검증 손실을 계산할 수 있다.

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))

print(history.history.keys())
# dict_keys(['loss', 'accuracy', '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()

초기에 검증 손실이 감소하다 다섯번째 에포크 뒤에 다시 상승하는 것을 알 수 있다. 이후에 다시 줄어들기도 하지만 전체적으로 봤을 때 약 7.5번째 에포크 이후로는 우상향하여 과대적합 모델이 만들어진다. 

 

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

 

compile() 메서드에서 사용하는 기본 옵티마이저인 RMSprop 옵티마이저 대신에 적응적 학습률을 사용하는 Adam 옵티마이저를 적용해보면 어떨까?

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))

print(history.history.keys())
# dict_keys(['loss', 'accuracy', '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 옵티마이저가 이 데이터셋에 잘 맞는다는 것을 보여준다.

 

옵티마이저를 바꾸는 방식이 아닌, 이전에 배운 규제를 사용하여 과대적합을 막는 방법은 없을까?

 

당연히 존재한다. 


드롭아웃(dropout)

드롭아웃은 아래와 같이 훈련 과정에서 층에 있는 일부 뉴런을 랜덤하게 꺼내어(뉴런의 출력을 0으로 만들어) 과대적합을 막는 방식이다. 

출처: https://ko.d2l.ai/chapter_deep-learning-basics/dropout.html

 

어떤 샘플을 처리할 때는 은닉층의 n번째 뉴런이 드롭아웃되어 은닉층 출력이 하나 사라지며, 또 다른 샘플을 처리할 때는 은닉층의 m번째 누련이 드롭아웃되어 하나의 출력이 없는 방식이다. 

 

얼마나 많은 뉴런을 드롭할지는 개발자가 정해야 하는 또 다른 하이퍼파라미터이다.

 

그럼 드롭아웃이 어떻게 과대적합을 막는 것일까? 

 

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

 

또 다른 해석은 드롭아웃이 적용된 층을 각각 떼어서 생각을 해보면,  드롭아웃을 적용하여 훈련하는 것은 마치 2개의 신경망을 앙상블 하는 것처럼 상상할 수 있다.

 

이전에 다뤘듯, 앙상블은 과대적합을 막아주는 아주 좋은 기법이다.

 

model_fn() 함수에 드롭아웃 객체를 전달하여 층을 추가한 뒤, 검증 샘플의 손실을 확인해보자.

model = model_fn(keras.layers.Dropout(0.3))
model.summary()
# == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == =
# flatten(Flatten)(None, 784)
# 0
# 
# dense(Dense)(None, 100)
# 78500
# 
# dropout(Dropout)(None, 100)
# 0
# 
# dense_1(Dense)(None, 10)
# 1010
# 
# == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == =
# Total
# params: 79, 510
# Trainable
# params: 79, 510
# Non - trainable
# params: 0

위와 같이 은닉층 뒤에 추가된 드롭아웃 층은 훈련되는 모델 파라미터가 없다. 또한 입력과 출력 뉴련의 크기에 전혀 영향을 주지 않는다. 일부 뉴런의 출력을 0으로 만들지만, 전체 출력 배열의 크기를 바꾸지는 않는다.

 

동일하게 그래프를 확인해보면,

이와 같이 과대적합이 확실하게 줄어들었다. 그래프에 따르면 에포크를 10회 정도 돌리는게 가장 좋은 성능을 낼 것으로 기대할 수 있다.


모델의 저장과 복원

지금까지 실습을 진행하면서 매번 실행마다 새로 모델을 학습시킨 뒤 결과를 확인했다. 하지만 실무에서 어떤 모델을 사용한 서비스를 오픈하기 위해서는 훈련 과정이 매번 서비스를 재실행할 때마다 반복된다는 건 생각하기도 싫다.

 

당연하게도 Keras 모델은 훈련된 모델의 파라미터를 저장하는 save_weights() 메서드를 제공한다. 

 

이 메서드는 기본적으로 텐서플로의 체크포인트 포맷으로 저장하지만, 파일의 확장자가 '.h5'일 경우 HDF5 포맷으로 저장한다.

 

마찬가지로 모델의 구조와 파라미터를 함께 저장하는 save() 메서드도 있다.

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))

model.save_weights('model-weights.h5')

model.save('model-whole.h5')

위와 같이 실행 파일과 같은 경로에 파라미터와 전체 모델 파일이 생성된 것을 확인할 수 있다.

 

이제 훈련되지 않은 새 모델을 만들곡 파일에서 훈련된 모델 파라미터를 읽어서 사용하자.

 

이 때, load_weights 메서드를 사용하기 위해서는 save_weights() 메서드로 저장했던 모델과 정확히 같은 구조를 가지지 않으면 에러가 발생한다.

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))

model.load_weights('model-weights.h5')

 이 모델의 검증 정확도를 확인해보자. Keras에서 예측을 수행하는 predict() 메서드는 사이킷런과 다르게 샘플마다 10개의 클래스에 대한 확률을 반환한다. 

 

검증 세트의 샘플 개수가 12000개이기 때문에 predict() 메서드가 (12000, 10) 크기의 배열을 반환한다.

 

compile() 메서드 없이 저장된 파라미터를 불러왔기 때문에 evaluate() 를 사용한 검증이 불가능하다. 

따라서 아래와 같이 2차원 배열의 마지막 차원(클래스의 개수)를 따라 최대 확률을 얻어낸 뒤, 이를 타깃 인덱스와 비교하여 평균내면 정확도가 될 것이다.

import numpy as np
val_labels = np.argmax(model.predict(val_scaled), axis=-1)
print(np.mean(val_labels == val_target))
# 0.8811666666666667

 

이번엔 전체 모델 파일을 불러와 사용하자. 전체 모델 파일의 경우, compile 한 정보까지 가지고 있기 때문에 바로 evaluate()를 사용하면 된다.

model = keras.models.load_model('model-whole.h5')
model.evaluate(val_scaled, val_target)
# 375/375 [==============================] - 0s 779us/step - loss: 0.3241 - accuracy: 0.8848

콜백

콜백은 훈련 과정 중 어떤 작업을 수행할 수 있게 하는 객체로 keras.callbacks 패키지 아래에 있는 클래스들이다. fit() 메서드의 callbacks 매개변수에 리스트로 전달하여 사용한다. 

 

- ModelCheckPoint

기본적으로 에포크마다 모델을 저장하는 콜백으로 save_best_only 매개변수를 지정하여 가장 낮은 검증 점수를 만드는 모델을 저장할 수 있다. 아래와 같이 사용한다.

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', save_best_only=True)

history = model.fit(train_scaled, train_target, epochs=20, verbose=0,
                    validation_data=(val_scaled, val_target),
                    callbacks=[checkpoint_cb])
model = keras.models.load_model('best-model.h5')
model.evaluate(val_scaled, val_target)
# 375/375 [==============================] - 0s 789us/step - loss: 0.3101 - accuracy: 0.8875

ModelCheckpoint 콜백이 최상의 점수의 모델을 자동으로 저장해주었다. 하지만 여전히 20번의 에포크를 모두 훈련한 뒤에 종료한다. 

 

- earlyStopping

검증 점수가 상승하기 시작하면 그 이후에는 과대적합이 더 커지기 때문에 훈련을 계속할 필요는 없다. 이때 훈련을 중지하면 그 만큼 컴퓨터 자원과 시간을 아낄 수 있다.

 

이렇게 과대적합이 시작되기 전에 훈련을 미리 중지하는 것을 조기 종료라고 부르며, 딥러닝 분야에서 널리 사용된다.

 

아래와 같이 EarlyStopping 콜백을 사용하면 된다.

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', save_best_only=True)
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의 patience는 n번 연속 검증 점수가 향상되지 않으면 훈련을 중지하는 매개변수이며, restore_best_weights 매개변수를 True로 지정하면 가장 낮은 검증 손실을 낸 모델 파라미터로 되돌린다.

print(early_stopping_cb.stopped_epoch)
# 12

위 속성으로 훈련이 중지된 에포크를 알 수 있는데, 인덱스 값이므로 13번 에포크를 의미한다. 2번 연속 점수가 향상되지 않은 지점이므로 10번 에포크의 점수가 가장 좋을 것이다.

검증 손실 또한 10번 에포크에서 최저점을 찍은 뒤, 계속 상승하는 것을 볼 수 있다.

 

이렇게 EarlyStopping 콜백을 사용하면 안심하고 에포크 횟수를 크게 지정해도 상관이 없으며, MedelCheckPoint 콜백과 함께 사용하여 최상의 모델을 자동으로 저장해주니 매우 편리하다.

 

 

 

끝!

Comments