DNN - (3)

호랑·2022년 7월 17일
0

케라스의 fit() 객체는 history 라는 클래스를 반환해 준다.
이 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)
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

모델을 만드는 함수를 정의했다. 다만 여기에는 if문이 있는데, 이 if문은 model_fn()에 a_layer을 매개 변수로 하는 케라스 층을 추가하면 은닉층 뒤에 또 하나의 층을 추가하는 것이다.

만약 a_layer로 층을 추가하지 않으면

model = model_fn()

model.summary()

(2)에서 만들었던 모델과 같은 모델이 나온다.
그럼 이제 이 결과를 history 변수에 담아 보자. 코드는 다음과 같다.

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

history = model.fit(train_scaled, train_target, epochs=5, verbose=0)

여기서 verbose 는 훈련출력 과정에 대한 변수로, 1이면 진행 막대와 손실 등의 지표가, 2로 하면 진행 막대가 나타나지 않는다.

무튼 이렇게 history 변수에 넣어 주면, history는 딕셔너리가 들어 있게 된다.

print(history.history.keys())

dict_keys(['loss', 'accuracy'])

한 에포크마다 계산된 loss와 accuracy를 담아 놓은 딕셔너리인데,

print(history.history)

{'loss': [0.5311834216117859, 0.3947155773639679, 0.3589134216308594, 0.3360195457935333, 0.3160899579524994],
'accuracy': [0.8118958473205566, 0.8557708263397217, 0.8694375157356262, 0.8796250224113464, 0.8849583268165588]}

요런 식이다.

그럼 이제 이걸 그래프로 그려볼 수 있겠다.

plt.subplot(1,2,1)                # nrows=1, ncols=2, index=1
plt.plot(history.history['loss'])
plt.title('loss')
plt.xlabel('epoke')
plt.ylabel('loss')

plt.subplot(1,2,2)                  # nrows=1, ncols=2, index=2
plt.plot(history.history['accuracy'])
plt.title('accuracy')
plt.xlabel('epoke')
plt.ylabel('accuracy')

plt.tight_layout()
plt.show()


여기서 에포크 수를 늘리면 어떻게 될까? 아마 loss는 더 작아지고 accuracy는 커질 것이다. 학습을 더 하는 거니까! 하지만.. 인생은 그렇게 호락호락하지 않다. 그러면 train set에 과대적합될 테니까. 즉, 검증 세트와 훈련 세트의 차이가 점점 커질 것이다. 그럼 좋은 모델이 될 수 없다.

그럼 우리는 여기서 뭘 해야 하는가. 학습은 하되, 검증 세트와 비교했을 때 차이가 제일 적게 나는 에포크만큼만! 학습을 해야 할 것이다. 그걸 위해 밑의 코드에서 validation_data 매개변수에 검증에 사용할 입력과 타깃값을 튜플로 만들어 전달해 줄 것이다. 그리고 믿는 구석이 생겼으니 에포크 수도 늘려 보겠다.

model=model_fn()
model.compile(loss='sparse_categorical_crossentropy',metrics='accuracy')
history=model.fit(train_scaled,train_target,epochs=20,validation_data=(val_scaled,val_target))

그럼 이제 history의 키 값에
dict_keys(['loss', 'accuracy', 'val_loss', 'val_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()


보니까, 5번째까지는 줄어들다가 이후 다시 상승하기 시작했다. 훈련 에포크가 5번째를 넘어서면 모델이 train 데이터에 과대적합이 된다는 것이다. 그럼 우리는 어떻게 해야 하는가?

검증 손실이 상승하는 시점을 가능한 뒤로 늦춰야 한다!

과대 적합을 막는 방법

우선은 간단하게 optimizer를 바꿔주는 방법이 있다. 예를 들면 Adam은 적응적 학습률을 사용하기 때문에 에포크가 진행되면서 학습률의 크기를 조정할 수 있다. 실제로 이 데이터셋에 아담 옵티마이저를 적용하면 한 10에포크쯤까지는 감소추세가 이어진다. 또, 학습률을 조정해서 다시 시도해 볼 수도 있다. 하지만 우리는 보다 세세하게 건드려 볼 것이다.

드롭 아웃

말 그대로 노드를 랜덤하게 없애 주는 것이다.
뉴런(노드)은 랜덤하게 없어지고 노드를 얼마만큼 드롭시킬지는 하이퍼파라미터로 정해줄 수 있다.
이전 층의 일부 뉴런이 랜덤하게 꺼져 버리면 특정 뉴런에 과대하게 의존하는 것을 줄여줄 수 있다. 즉, 일부에 집착하지 않고 모든 입력에 주의를 기울이며 더 안정적으로 예측을 할 수 있게 만들어 준다는 것이다.
또 저런 식으로 훈련 과정에서 랜덤하게 노드를 끊어주면, 마치 2개의 모델에 적용시키는 것 같은 앙상블 효과도 있을 수 있다. 이미 알고 있다시피, 앙상블 학습은 과대적합에 좋다.

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

model = model_fn(keras.layers.Dropout(0.3))

model.summary()

요렇게 적용해볼 수 있겠다.

결과에서 볼 수 있다시피 드롭아웃 (층)은 훈련되는 모델 파라미터도 없고 입력과 출력의 크기가 같다. 일부 뉴런의 출력을 0으로 만들긴 하지만 전체 출력 배열의 크기를 바꾸지 않는다.
드롭아웃은 훈련 중에만 적용되고 예측과 검증에는 사용되지 않는다. 훈련된 모든 노드를 사용해야 올바른 예측을 할 수 있으니까!

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번째 에포크 정도에서 충분히 손실을 줄였고 그 이후부터는 좀 상승하는 것 같으니까 에포크를 10으로 줄여서 다시 학습을 시키면 될 것이다.

모델 저장과 복원

이 모델을 매번 학습을 시켜서 사용할 수는 없다. 어딘가 보여주거나 사용하려면 이미 훈련을 끝낸 모델을 가지고 그것만 사용을 해야 될 것이다. 그래서! 있는 것이 바로 저장이다. 훈련된 모델의 파라미터, 즉 weight를 저장하면 나중에 그냥 최적의 파라미터로 모델에 바로 적용시키면 되는 것이다. save_weight()는 원래는 체크포인트 포맷으로 저장을 하지만, h5확장자로 지정을 해주면 HDF5 포맷으로 저장할 수 있다.

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

모델 구조와 모델 파라미터까지 함께 저장하려면 save()를 쓰면 된다. 이건 원래 SavedModel 포맷이지만 이 역시 h5로 저장 가능하다.

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

어디, 보면,

!ls -al *.h5


굳.

그럼 새로운 모델에 model_weight만 올려서 실행시켜 보자.

model = model_fn(keras.layers.Dropout(0.3))

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

이 때, load_weight를 하려면 save_weight를 했던 모델과 같은 구조를 가져야 한다.
패션 MNIST는 10개 다중 분류기 때문에 predict() 메서드가 10개의 클래스에 대한 확률을 반환한다. 10개 확률 중에 가장 큰 값의 인덱스를 골라 타깃 레이블과 비교하여 정확도를 계산해 보자.

import numpy as np

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

정확도는 0.8825가 나왔다.

여기서 argmax()는 predict()메서드 결과에서 가장 큰 값의 인덱스를 반환하는 함수다. 즉,
인덱스 0 인 물건의 확률이 [0.34, 0.28, 0.87 ...] 이런 식이라면 이 중에 제일 높은 0.87의 인덱스, 즉 2를 반환하는 것이다.
argmax()의 axis=-1은 배열의 마지막 차원을 따라 최댓값을 고른다. 검증 세트는 2차원이므로 마지막 차원은 1이 된다.

예를 들어, axis=0이면 행을 따라 제일 큰 인덱스를 반환한다.
[1,0,5]니까 제일 큰 인덱스 2를 반환하는 것이다.
axis=1이면 열을 따라 제일 큰 인덱스를 반환하게 될테니,
[1,7,8,3,2] 중 가장 큰 인덱스, 즉 2를 반환하게 된다!

그 후, argmax()로 고른 인덱스(val_labels)와 타깃(val_target)을 비교한다. 둘이 같으면 1, 다르면 0이 되고 이를 평균하면 정확도가 될 것이다.

model = keras.models.load_model('model-whole.h5')

model.evaluate(val_scaled, val_target)

[0.33268266916275024, 0.8824999928474426]

같은 모델이므로 같은 정확도가 나왔다(위는 반올림).

그런데 돌이켜 생각해 보면, 이걸 우리가 일일이 다 해야 할까? 그러니까 모델을 한 번 깊게 훈련시켰다가, epochs보고 다시 훈련시키고. 이렇게 두 번씩 말이다. 그냥 알아서 한 번에 해 주면 안 되나?

콜백

콜백은 훈련 과정 중간에 작업을 수행할 수 있게 해주는 객체다.
fit() 메서드의 callbacks 매개변수에 리스트로 전달하여 사용한다.
ModelCheckPoint 콜백은 기본적으로 에포크마다 모델을 저장한다. save_best_only=True는 가장 낮은 검증 점수를 만드는 모델을 저장할 수 있다.

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)

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

다른 건 같고 checkpoint_cb를 만들어 fit()메서드 콜백 매개변수에 리스트로 감싸 넣어준다. 이제 모델이 훈련한 후에 best-model.h5에 최상의 검증 점수를 낸 모델이 저장될 것이다!

model = keras.models.load_model('best-model.h5')

model.evaluate(val_scaled, val_target)

[0.31479981541633606, 0.8901666402816772]

자동으로 저장이 되었다. 하지만 인간의 욕심은 끝이 없다. 만약 최적의 에포크를 찾았으면 그냥 거기서 알아서 끝내주면 안 되나?

조기 종료

외않되. early stopping으로 가능하다.
조기 종료는 에포크 횟수를 제한하는 역할이지만 모델이 과대적합되는 것을 막아주기 때문에 규제 방법 중 하나로 볼 수도 있다.

케라스에 역시 EarlyStopping 콜백이 있다.
patience 매개변수로 검증 점수가 향상되지 않더라도 참을 에포크 횟수를 지정할 수 있다.(검증 점수 향상되지 않는다고 냅다 멈춰버리면 학습을 덜 하거나 잠시 올랐다가 다시 내려갈 수도 있으니까)
그래서 만약 이걸 2로 지정하면 2번 연속 검증 점수가 향상되지 않으면 훈련을 중지한다. 그 이후
restore_best_weights=True를 하면 가장 낮은 검증 손실을 낸 모델 파라미터로 되돌린다.

즉, EarlyStopping 콜백을 ModelCheckPoint 콜백과 함께 사용하면
1. 가장 낮은 검증 손실의 모델을 파일에 저장하고,
2. 검증 손실이 다시 상승할 때 훈련을 중지할 수 있다!
3. 또, 훈련을 중지한 후 현재 모델의 파라미터를 최상의 파라미터로 돌려 준다.

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

early_stopping_cb만 추가해 줬다. fit()의 callbacks 매개변수에 두개의 콜백을 리스트로 전달해 줬다.
나중에 훈련이 끝나면, 몇번째 에포크에서 훈련이 중지되었는지 early_stopping_cb의 stopped_epoch에서 확인할 수 있다.

print(early_stopping_cb.stopped_epoch)

9

9번째 에포크에서 멈췄다. 에포크는 0에서부터 시작하기 때문에 10번째 에포크에서 훈련이 중지되었다는 것을 의미한다. patience를 2로 지정했으니까 최상의 모델은 8번째 에포크일 것이다.

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

9번째에서 모델이 멈췄고, 8번째에서 가장 낮은 손실을 기록했다.

그럼 이제 조기 종료로 얻은 모델 성능을 확인해 보자.

model.evaluate(val_scaled, val_target)

[0.33248990774154663, 0.8788333535194397]

끝!

출처 : 혼자 공부하는 머신 러닝+ 딥러닝

profile
데이터리터러시 기획자 / 데이터 분석가가 꿈!

0개의 댓글