[혼자공부하는 머신러닝 + 딥러닝] 18_신경망 모델 훈련
# 17. 신경망 모델 훈련
- 지금까지 인공신경망에 대해 배우고 텐서플로의 케라스 API를 사용해 직접 만들어 보았다. 1개 이상의 층을 추가해 심층 신경망을 구성하고 다양한 고급 옵티마이저를 적용하는 방법도 알아보았다.
- 일반적으로 사이킷런과 같은 머신러닝 알고리즘은 좋은 성능을 내기 위해 매개변수를 조정하고 훈련하는 과정을 반복한다. 이런 알고리즘들은 모델의 구조가 어느정도 고정되어 있다고 느낄 수 있다.
- 딥러닝에서는 모델의 구조를 직접 만든다는 느낌이 훨씬 강하다. 층을 추가하고 층에 있는 뉴런의 개수와 활성 함수를 결정하는 일들이 그렇다.
- 이번 시간에는 케라스 API를 사용해 모델을 훈련하는데 필요한 다양한 도구들을 알아보자.
1. 손실함수
-
지난 2장에서 fit()메서드로 모델을 훈련하면 훈련 과정이 상세하게 출력되어 확인할 수 있었다. 여기에는 에포크 횟수, 손실, 정확도 등이 들어있었다. 마지막줄에 이상한 문장이 출력된다.
<tensorflow.python.keras.calbacks.History at 0x7fef38bfc080>
- 노트북의 코드셀은 print()명령을 사용하지 않아도 마지막 라인의 실행결과를 자동으로 출력한다. 이 메세지는 fit()메서드의 실행 결과를 출력한 것이다. 다시 말해 fit() 메서드가 무엇인가 반환한다는 증거이다. 실젤 케라스의 fit()메서드는 History 클래스 객체를 반환한다. History객체에는 훈련 과정에서 계산한 지표, 즉 손실과 정확도 값이 저장되어있다.
- 이 값을 이용하면 그래프를 그릴 수 있을 것이다.
import numpy as np
from tensorflow import keras
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
(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)
print(train_scaled.shape, train_target.shape)
print(val_scaled.shape, val_target.shape)
(45000, 28, 28) (45000,)
(15000, 28, 28) (15000,)
- 그 다음 모델을 만든다. 이전과 달리 모델을 만드는 간단한 함수를 정의하겠다. 이 함수는 하나의 매개변수를 가진다.
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 구문의 역할은 model_fn()함수에 a_layer 매개변수로 케라스 층을 추가하면 은닉층 뒤어 또 하나의 층을 추가하는 것이다. 여기서는 a_layer매개변수로 층을 추가하지 않고 단순하게 model_fn()함수를 호출한다. 그리고 모델 구조를 출력하면 이전 결과와 동일한 모델이라는 것을 확인할 수 있다.
model = model_fn()
model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
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
_________________________________________________________________
2021-10-10 16:33:28.545226: I tensorflow/core/platform/cpu_feature_guard.cc:142] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations: AVX2 FMA
To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
- 이후 이전과 동일하게 모델을 훈련하지만 메서드의 결과를 history 변수에 담아보자.
model.compile(loss='sparse_categorical_crossentropy', metrics='accuracy')
history = model.fit(train_scaled, train_target, epochs=5, verbose=0)
2021-10-10 16:33:29.036692: I tensorflow/compiler/mlir/mlir_graph_optimization_pass.cc:185] None of the MLIR Optimization Passes are enabled (registered 2)
- verbose 매개변수는 훈련 과정 출력을 조정한다. 기본값은 1로, 2로 바꾸면 진행 막대를 빼고 출력된다. 이번 절에서는 훈련 결과를 그래프로 나타내는 대신 verbose 매개변수를 0으로 지정해 훈련과정을 나타내지 않았다.
- history객체에는 훈련 측정값이 담겨 있는 history 딕셔너리가 들어있다. 이 딕셔너리에 어떤 값이 들어있는지 확인해보자.
print(history.history.keys())
dict_keys(['loss', 'accuracy'])
- 손실과 정확도가 포함되어있다. 이전 장에서처럼 케라스는 기본적으로 에포크마다 손실을 계산한다. 정확도는 complie() 메서드에서 metrics 매개변수에 ‘accuracy’를 추가했기 때문에 history 속성에 포함되었다.
- history속성에 포함된 손실과 정확도는 에포크마다 계산한 값이 순서대로 나열된 단순한 리스트이다.
- 이를 맷플롯립으로도 그릴 수 있다.
plt.plot(history.history['loss'])
plt.title('Loss based on the results of the epoch')
plt.xlabel('epoch')
plt.ylabel('loss')
plt.show()
- 파이썬 리스트의 인덱스는 0부터 시작하므로 5개의 에포크가 0에서부터 4까지 x축에 표현된다. y축은 손실값을 나타낸다.
- 정확도 출력은 아래와 같다.
plt.plot(history.history['accuracy'])
plt.title('Accutacy based on the results of the epoch')
plt.xlabel('epoch')
plt.ylabel('accuracy')
plt.show()
- 결과를 살펴보면 에포크마다 손실이 감소하고 정확도가 향상하는 것을 확인할 수 있다.에포크를 늘려서 확인해보도록 하자.
model = model_fn()
model.compile(loss='sparse_categorical_crossentropy', metrics='accuracy')
history = model.fit(train_scaled, train_target, epochs=20, verbose=0)
plt.plot(history.history['loss'])
plt.title('Loss based on the results of the epoch')
plt.xlabel('epoch')
plt.ylabel('loss')
plt.show()
- 예상대로 손실이 감소하는 모습을 보인다.
2. 검증 손실
- 이전에 확률적 경사 하강법을 사용했을 떄, 과대/과소적합과 에포크 사이의 관계를 알아보았다. 인공 신경망은 모두 일종의 경사하강법을 사용하기 떄문에 동일한 개념이 여기에도 적용된다.
- 에포크에 따른 과대적합과 과소적합을 파악하려면 훈련 세트에 대한 점수뿐만 아니라 검증 세트에 대한 점수도 필요하다. 따라서 앞처럼 훈련 세트의 손실만을 그려서는 안된다. 손실을 사용해 과대/과소적합을 다뤄야 한다.
- 에포크마다 검증 손실을 계산하기 위해 케라스 모델의 fit() 메서드에 검증 데이터를 전달할 수 있다. validation_data 매개변수에 검증에 사용할 타깃값을 튜플로 만들어 전달하면 된다.
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'])
- 검증 세트에 대한 손실은 ‘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()
- 다섯번쨰 에포크에 손실이 상승하는 모습을 보인다. 훈련 손실은 꾸준히 감소하고 있다. 이를 종합적으로 살펴볼 때 전형적인 과대적합 모델이 만들어 졌다. 검증 손실이 상승하는 시점을 가능한 뒤로 늦추면 검증 세트에 대한 손실이 줄어들 뿐 아니라 검증 세트에 대한 정확도도 증가할 것으로 예상된다.
- 과대적합을 막기 위해 이전에 배운 규제 방식 대신 신경망에 특화된 규제 방법을 이후 다루도록 한다. 지금은 옵티마이저 하이퍼파라미터를 조정해 과대적합을 완화시킬 수 있는지 알아보자.
- 기본 ‘RMSprop’ 옵티마이저는 많은 문제에서 잘 동작한다. 대체제를 찾으라하면 ‘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()
- 이전보다 과대적합이 줄어든 모습을 모인다. 검증 손실 그래프에 여전히 요동이 남아 있지만 전체적으로 감소추세를 보이는 경향이 있다. 이는 Adam 옵티마이저가 이 데이터 셋에 잘 맞는다는 것을 보여준다.
- 이어서 신경망에서 사용하는 대표적인 규제 방법을 알아보자.
3. 드롭아웃
-
드롭아웃은 딥러닝의 아버지로 불리는 제프리 힌턴이 소개했다. 이 방식은 훈련 과정에서 층에 있는 일부 뉴런을 랜덤하게 껴서(뉴런의 출력을 0으로 만들어)과대적합을 막는다.
- 어떤 샘플을 처리할 때는 은닉층의 두 번째 뉴런이 드롭아웃되어 출력이 없어진다. 다른 샘플을 처리할때는 첫번째 뉴런이 드롭아웃 되어 출력이 없어진다. 이처럼 뉴런은 랜덤하게 드롭아웃되고 얼마나 많은 뉴런을 드롭할지는 우리가 정해야할 또 다른 하이퍼파라미터가 된다.
- 이전 층의 일부 뉴런이 줄어들면 특정 뉴런에 과대하게 의존하는 것을 줄일 수 있고, 모든 입력에 대해 주의를 기울여야 한다. 일부 뉴런의 출력이 없을 수 있다는 것을 감안하면 이 신경망은 더 안정적인 예측을 할 수 있다.
- 케라스는 드롭아웃을 keras.layers 패키지 아래의 Dropout클래스로 제공한다. 어떤 층의 뒤에 드롭아웃을 두어 이층의 출력을 랜덤하게 0으로 만드는 것이다. 드롭아웃은 층처럼 사용되지만 훈련되는 모델 파라미터는 없다.
model = model_fn(keras.layers.Dropout(0.3))
model.summary()
Model: "sequential_3"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
flatten_3 (Flatten) (None, 784) 0
_________________________________________________________________
dense_6 (Dense) (None, 100) 78500
_________________________________________________________________
dropout (Dropout) (None, 100) 0
_________________________________________________________________
dense_7 (Dense) (None, 10) 1010
=================================================================
Total params: 79,510
Trainable params: 79,510
Non-trainable params: 0
_________________________________________________________________
- 출력 결과에서 볼 수 있듯이 은닉층 뒤에 추가된 드롭아웃 층은 훈련되는 모델 파라미터가 없다. 또한 입력과 출력의 크기가 같습니다. 일부 뉴런의 출력을 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()
- 과대적합이 줄어든 모습을 볼 수 있다. 이 모델은 20번의 에포크 동안 훈련을 했기 때문에 아직 다소 과대적합이 남아있다. 과대적합되지 않은 모델을 얻기 위해 에포크 횟수를 10으로 조정하고 다시 수행해 보자.
4. 모델 저장과 복원
- 에포크 횟수를 10으로 다시 저장하고 모델을 훈련해보자. 케라스 모델은 훈련된 파라미터를 저장하는 간편한 save_weights()메서드는 제공합니다. 기본적으로 텐서플로의 체크포인트 포맷으로 저장하지만 파일의 확장자가 ‘.h5’ 일 경우 HDF5포맷으로 저장한다.
model.save_weights('model-weights.h5')
- 또한 모델 구조와 모델 파라미터를 함께 저장하는 save()메서드도 제공한다. 기본적으로 이 메서드는 텐서플로의 SaveModel 포멧으로 저장하지만 파일의 확장자가 동일하게 ‘.h5’일 경우 HDF5 포맷으로 저장한다.
model.save('model-whole.h5')
!ls -al *.h5
-rw-r--r-- 1 janghyeseong staff 332480 10 10 16:45 model-weights.h5
-rw-r--r-- 1 janghyeseong staff 981736 10 10 16:45 model-whole.h5
- 두 가지 실험을 진행해보자.
- 훈련을 하지 않은 새로운 모델을 만들고 model-weights.h5 파일에서 훈련된 모델 파라미터를 읽어서 사용한다.
- 아예 model-whole.h5파일에서 새로운 모델을 만들어 바로 사용한다.
- 위 두가지 실험을 진행해보자.
model = model_fn(keras.layers.Dropout(0.3))
model.load_weights('model-weights.h5')
- 이 모델의 정확도를 확인해보자. 케라스에서 예측을 수행하는 predict()메서드는 사이킷런과 달리 샘플마다 10개의 클래스에 대한 확률을 반환한다. 패션 MNIST 데이터셋이 다중분류이기 떄문이다.(이진 분류 문제라면 양성 클래스에 대한 확률 하나로 변환된다.)
val_labels = np.argmax(model.predict(val_scaled), axis=-1)
print(np.mean(val_labels == val_target))
0.9059333333333334
- 모델의 predict() 메서드 결과에서 가장 큰 값을 고리기 위해 넘파이 argmax() 함수를 사용했다. 이 함수는 배열에서 가장 큰 값을 반환한다.
- argmax()로 고른 인덳스와 타깃을 비교해 두 배열에서 각 위치의 값이 같으면 1이되고 다르면 0이 된다. 이를 평균하면 정확도가 된다.
- 다음으로는 모델 전체를 파일에서 읽은 다음 검증 세트의 정확도를 출력해보자. 모델이 저장된 파일을 읽을 때는 케라스가 제공하면 load_model()함수를 이용한다.
model = keras.models.load_model('model-whole.h5')
model.evaluate(val_scaled, val_target)
469/469 [==============================] - 1s 1ms/step - loss: 0.2572 - accuracy: 0.9059
[0.25717154145240784, 0.9059333205223083]
- 같은 모델을 저장하고 다시 불러들였기 때문에 동일한 정확도를 얻는 결과를 보인다.
- 지금까지 20번 동안의 에포크 과정을 거쳐 검증 점수가 상승하는 지점을 확인했다 그 다음 모델을 과대적합되지 않는 에포크만큼 다시 훈련했다. 모델을 두번 훈련하지 않고 한 번에 끝낼 수 있는 콜백에 대해 알아보자.
5. 콜백
- 콜백은 훈련 과정 중간에 어떤 작업을 수행할 수 있게 하는 객체를 말한다.
- 여기서 사용한 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])
<keras.callbacks.History at 0x182c43f10>
- ModelCheckpoint 클래스의 객체 checkpoint_cb를 만든 후 fit() 메서드의 callbacks 매개변수에 리스트로 감싸서 전달한다. 모델이 훈련한 후 best-model.h5에 최상의 검증 점수를 낸 모델이 저장된다. 이 모델을 load.model() 함수로 다시 불러와 예측을 수행해보자.
model = keras.models.load_model('best-model.h5')
model.evaluate(val_scaled, val_target)
469/469 [==============================] - 1s 1ms/step - loss: 0.3230 - accuracy: 0.8833
[0.3230270743370056, 0.8832666873931885]
- 검증 점수가 상승하기 시작하면 그 이후에는 과대적합이 더 커지기 때문에 훈련을 계속할 필요가 없다. 이때 훈련을 중지하면 컴퓨터 자원과 시간을 아낄 수 있다. 이렇게 과대적합이 시작되기 전에 훈련을 미리 중지하는 것을 조기 종료라고 한다.
- 조기 종료는 훈련 에포크 횟수를 제한하는 역할이지만 모델이 과대적합되는 것을 막아주기 떄문에 규제 방법 중 하나로 생각할 수 있다.
- 케라스에는 조기 종료를 위한 EarlyStopping 콜백을 제공한다. 이 콜백의 patience 매개변수는 검증 점수가 향상되지 않더라도 참을 에포크 횟수로 지정한다. 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])
- 훈련을 마치고 나면 몇 번쨰 에포크에서 훈련이 중지되었는지 early_stopping_cb 객체의 stopped_epoch속성에서 확인이 가능하다.
print(early_stopping_cb.stopped_epoch)
15
- 결과는 에포크 횟수가 0부터 시작하기 때문에 15는 16번째 에포크에서 훈련이 중지되었다는 것을 의미한다. patience를 2로 지정했기때문에 최상의 모델은 14번째 애포크일 것이다.
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(['train', 'val'])
plt.show()
- 컴퓨터 자원과 시간을 아낄 수 있고 ModelCheckpoint 콜백과 함께 사용하면 최상의 모델을 자동으로 저장해 주므로 편리하다. 마지막으로 이를 이용해 검증 세트에 대한 성능을 확인해보자.
model.evaluate(val_scaled, val_target)
469/469 [==============================] - 1s 1ms/step - loss: 0.3168 - accuracy: 0.8837
[0.31676578521728516, 0.883733332157135]
Leave a comment