AI/혼자공부하는머신러닝딥러닝

[DL] 07-3 신경망 모델 훈련

inthyes 2024. 2. 22. 17:33

손실곡선

아래와 같이 패션 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

if구문은 model_fn() 함수에 케라스 층을 추가하면 은닉층 뒤에 또 하나의 층을 추가하는 역할을 한다.

 

a_layer 매개변수로 층을 추가하지 않고 단순하게 model_fn()을 출력하는 것도 가능하다.

model = model_fn()
model.summary()

 

fit() 메서드의 결과를 history 변수에 담아보자.

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

 

이때 versboe 매개변수는 훈련 과정 출력을 조절하는 역할을 한다. 기본값은 1로 에포크마다 진행 막대와 함께 손실 등의 지표가 출력되며 2로 바꾸면 진행 막대를 빼고 출력된다. 0으로 지정하면 훈련 과정을 나타내지 않을 수 있다.

 

 

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

print(history.history.keys())

 

출력을 통해 history 딕셔너리에는 손실과 정확도가 포함되어 있음을 확인할 수 있다. 케라스는 기본적으로 에포크마다 손실을 계산한다. 정확도는 compile() 메서드에서 metrics 매개변수에 'accuracy'를 추가했기 때문에 history 속성에 포함되는 것이다.

 

history 속성에 포함된 손실과 정확도는 에포크마다 계산한 값이 순서대로 나열된 단순한 리스트이며 맷플롯립을 사용해 쉽게 그래프로 그리는 것이 가능하다.

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

 

 

에포크마다 손실이 감소하고 정확도가 향상하는 것을 확인할 수 있다.

 

에포크 횟수를 20으로 늘려서 모델을 훈련하고 손실 그래프를 그려보자.

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

 

검증 세트에 대한 손실은 '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 에포크를 기준으로 다시 상승하는 것을 확인할 수 있다. 반면 훈련 손실은 꾸준히 감소하기 때문에 전형적인 과대적합 모델이 만들어진다. 검증 손실이 상승하는 시점을 가능한 뒤로 늦추면 검증 세트에 대한 손실이 줄어들 뿐만 아니라 검증 세트에 대한 정확도도 증가할 것이다.

 

기본 RMSprop 옵티마이저는 많은 문제에서 잘 동작한다. 이 옵티마이저 대신 다른 옵티마이저를 테스트해 본다면 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()

 

과대적합이 많이 줄어든 것을 확인할 수 있다. 이는 Adam 옵티마이저가 이 데이터셋에 잘 맞는다는 것을 보여준다.

 

더 나은 손실 곡선을 얻으려면 학습률을 조정해서 다시 시도해 볼 수 있다.

 

드롭아웃

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

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

 

케라스에서는 드롭아웃을 keras.layers 패키지 아래 Dropout 클래스로 제공한다. 어떤 층의 뒤에 드롭아웃을 두어 이 층의 출력을 랜덤하게 0으로 만드는 것이다. 드롭아웃이 층처럼 사용되지만 훈련되는 모델 파라미터는 없다.

 

앞서 정의한 model_fn()함수에 드롭아웃 객체를 전달하여 층을 추가해보자.

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

과대적합이 확실히 줄어든 것을 확인할 수 있다. 열 번째 에포크 정도에서 검증 손실의 감소가 멈추지만 크게 상승하지 않고 어느 정도 유지된다.

이 모델은 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')

 

위 메서드를 통해 파일이 잘 생성되었는지 확인해보자.

!ls -al *.h5

 

model = model_fn(keras.layers.Dropout(0.3))
model.load_weights('model-weights.h5')

훈련하지 않은 새로운 모델을 만들고 이전에 저장했던 모델 파라미터를 적재했다. 이때 사용하는 메서드는 save_weights()와 쌍을 이루는 load_weights()메서드이다.

 

이 모델의 검증 정확도를 확인해보자. 케라스에서 예측을 수행하는 predict() 메서드는 사이킷런과 달리 샘플마다 10개의 클래스에 대한 확률을 반환한다. 패션 MNIST 데이터셋이 다중 분류 문제이기 때문이다. 이진 분류 문제라면 양성 클래스에 대한 확률 하나만 반환하게 된다.

 

패션 MNIST 데이터셋에서 덜어낸 검증 세트의 샘플 개수는 12,000개이기 때문에 predict() 메서드는 (12000, 10)크기의 배열을 반환한다.

 

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

모델의 predict() 메서드 결과에서 가장 큰 값을 고르기 위해 넘파이 argmax() 함수를 사용한다. 이 함수는 배열에서 가장 큰 값의 인덱스를 반환한다.

argmax() 함수의 axis = -1은 배열의 마지막 차원을 따라 최댓값을 고른다. 검증 세트는 2차원 배열이기 때문에 마지막 차원은 1이 된다. 

 

모델 전체를 파일에서 읽은 다음 검증 세트의 정확도를 출력해보자.

model = keras.models.load_model('model-whole.h5')
model.evaluate(val_scaled, val_target)

 

같은 모델을 저장하고 다시 불러들였기 때문에 위와 동일한 정확도를 얻는다.

 

콜백

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

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

 

model_fn() 함수로 모델을 만들고 compile() 메서드를 호출하는 것은 이전과 동일하다.

ModelCheckpoint 클래스의 객체 checkpoint_cb를 만든 후 fit() 메서드의 callbacks 매개변수에 리스트로 감싸서 전달한다. 

모델이 훈련한 후에 best-model.h5에 최상의 검증 점수를 낸 모델이 저장된다. 이 모델을 load_model() 함수로 다시 읽어서 예측을 수행해보자.

 

model = keras.models.load_model('best-model.h5')
model.evaluate(val_scaled, val_target)

 

검증 점수가 상승하기 시작하면 그 이후에는 과대적합이 더 커지기 때문에 훈련을 계속할 필요가 없다. 이 때 훈련을 중지하면 컴퓨터 자원과 시간을 아낄 수 있으며 이를 조기 종료라고 부른다.

 

조기 종료는 훈련 에포크 횟수를 제한하는 역할이지만 모델이 과대적합되는 것을 막아 주기 때문에 규제 방법 중 하나로 생각할 수도 있다.

케라스는 조기 종료를 위한 EarlyStopping 콜백을 제공한다. 이 콜백의 patience 매개변수는 검증 점수가 향상되지 않더라도 참을 에포크 횟수로 지정한다.

 

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', 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 객체의 stopped_epoch 속서에서 확인 가능하다.

print(early_stopping_cb.stopped_epoch)

10번째 에포크에서 훈련이 중지되었음을 확인할 수 있다. 

 

이번에는 훈련 손실과 검증 손실을 출력해보자

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)

'AI > 혼자공부하는머신러닝딥러닝' 카테고리의 다른 글

[DL] 07-2 심층 신경망  (0) 2024.02.21
[DL] 07-1 인공 신경망  (0) 2024.02.21
[ML] 06-3 주성분 분석  (1) 2024.01.26
[ML] 06-2 k-평균  (1) 2024.01.23
[ML] 06-1 군집 알고리즘  (1) 2024.01.23