본문 바로가기
Study/혼자 공부하는 머신러닝

혼자 공부하는 머신러닝 + 딥러닝 - 신경망 모델 훈련

by Wanooky 2022. 4. 21.

사이킷런에서 제공하는 머신러닝 알고리즘들은 좋은 성능을 내기 위해 매개변수를 조정하고 훈련하는 과정을 반복한다.

 

모델의 구조가 어느 정도 고정되어 있다고 느낄 수 있다.

 

반면, 딥러닝은 모델의 구조를 직접 만든다는 느낌을 받을 수 있다.

 


손실 곡선

 

케라스의 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

 

앞에와는 달리 모델을 만드는 간단한 함수를 정의하여 만든다.

 

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

 

일단, 단순하게 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
_________________________________________________________________

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

 

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

 

여기서 verbose 매개변수는 훈련 과정 출력을 조절한다. 0으로 지정하면 훈련 과정을 나타내지 않고

 

기본 값은 1인데, 진행 막대와 함께 손실 등의 지표가 출력된다

 

2는 진행 막대를 빼고 출력된다.

 

history 딕셔너리가 생성이 됐는데, 어떤 값들이 있는지 확인하면 다음과 같다.

 

print(history.history.keys())

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

손실과 정확도가 포함되어 있는데, 케라스는 기본적으로 손실을 계산하고 metrics 매개변수에 accuracy를 지정해서

 

history 속성에 포함되어 있다.

 

그래프를 그려볼 수 있다.

 

plt.plot(history.histroy['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.histroy['loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.show()

손실이 잘 감소하지만, 더 나은 모델을 훈련한 것인지 의문이 든다.

 


검증 손실

 

인공 신경망도 경사 하강법을 사용하기 때문에, 과대/과소 적합이 나타날 가능성이 있다.

 

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

 

에포크마다 검증 손실 계산하기 우해, 케라스 모델의 fit() 메서드에 검증 데이터를 전달하자.

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

 

validation_data 매개변수에 검증에 사용할 입력과 타깃값을 튜플로 만들어 전달했다.

 

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

 

 

초기에는 검증셋의 손실이 감소하다가 5번째 에포크 부터 증가한다. 반면 훈련 셋의 손실은 꾸준히 감소한다. 이는 전형

 

적인 과대적합 모델이다.

 

만약 검증 손실이 상승하는 시점을 가능한 뒤로 늦추면 검증 셋에 대한 손실이 줄어들고 검증 셋에 대한 정확도도 증가

 

할 것이다.

 

과대적합을 막기 위해 규제 방식 대신에 신경망에 특화된 규제 방법이 있다. 

 

지금은 옵티마이저 하이퍼파라미터를 조정하여 과대적합을 완화시켜보자.

 

RMSprop 옵티마이저는 많은 문제에서 잘 동작한다. 그렇지만 이 옵티마이저 대신 Adam 옵티마이저를 선택해본다. 

 

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

 

model = model_fn()
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy',
			metrics='accuracy')
history = model.fit(train_scaled, train_target, epoch=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번 째 에포크까지 전반적인 감소 추세가 이어지고 있다. 이는 Adam 옵티마이저가 이 데이터셋에 잘 맞는다는 것을 보여준다.

 


드롭아웃

 

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

 

 

드롭아웃이 과대적합을 막는 이유는, 이전 층의 일부 뉴런이 랜덤하게 꺼지면 특정 뉴런에 과대하게 의존하는 것을 줄일

 

수 있고 모든 입력에 대해 주의를 기울일 수 있다. 일부 뉴런의 출력이 없을 수 있다는 것을 감안하면,

 

이 신경망은 더 안정적인 예측을 만들 수 있다.

 

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

 

어떤 층의 뒤에 드롭아웃을 두어서 이 층의 출력을 랜덤하게 0으로 만드는 것이다.

 

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

Model: "sequential_4"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 flatten_4 (Flatten)         (None, 784)               0         
                                                                 
 dense_8 (Dense)             (None, 100)               78500     
                                                                 
 dropout (Dropout)           (None, 100)               0         
                                                                 
 dense_9 (Dense)             (None, 10)                1010      
                                                                 
=================================================================
Total params: 79,510
Trainable params: 79,510
Non-trainable params: 0
_________________________________________________________________

 

Dropout 층은 훈련되는 모델 파라미터가 없다.

 

훈련이 끝난 뒤 평가나 예측을 수행할 때는 드롭아웃을 적용하지 말아야 한다. 

 

다행히, 텐서플로와 케라스는 모델을 평가와 예측에 사용할 때 자동으로 드롭아웃을 적용하지 않는다.

 

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_weight() 메서드를 제공한다.

 

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

 

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

 

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

 

첫번째 방법은 훈련하지 않은 새로운 모델을 만들고, model-weight.h5 파일에서 훈련된 모델 파라미터를 읽어서 사용하는 것과

두 번째는 아예 model-whole.h5 파일에서 새로운 모델을 만들어 바로 사용하는 방법이 있다.

 

먼저 첫 번째 방법을 사용해보자.

 

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

 

save_weights()가 있다면 load_weights() 매서드가 있다.

 

이 모델의 검증 정확도를 확인하기 위해서는 predict() 매서드를 사용한다.

 

이 메서드는 사이킷런과 달리 샘플마다 10개의 클래스에 대한 확률을 반환한다.

 

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

0.8790833333333333

predict() 메서드 결과에서 가장 큰 값을 고르기 위해 넘파이 argmax() 함수를 사용했다. 

 

axis=-1은 배열의 마지막 차원을 따라 최댓값을 고르는 것이다.

 

axis=1이면 열을 따라 각 행의 최댓값의 인덱스를 선택하고

axis=0이면 행을 따라 각 열의 최댓값의 인덱스를 선택한다.

 

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

 

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

375/375 [==============================] - 1s 3ms/step - loss: 0.3252 - accuracy: 0.8791
[0.32518628239631653, 0.8790833353996277]

 


콜백

 

콜백은 훈련 과정 중간에 어떤 작업을 수행할 수 있게 하는 객체로, 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_scaeld, train_target, epochs=20, verbose=0,
		  validation_data=(val_scaled, val_target),
          callbacks=[checkpoint_cb])

 

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

 

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

375/375 [==============================] - 1s 3ms/step - loss: 0.3185 - accuracy: 0.8871
[0.31851494312286377, 0.8870833516120911]

 

가장 낮은 검증 점수의 모델을 자동으로 저장하준다. 하지만 20번의 에포크 동안 훈련을 하는데. 검증 점수가

 

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

 

중지하면 자원과 시간을 아낄 수 있는데, 이를 조기 종료라고 한다.

 

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

 

생각할 수 있다.

 

케라스는 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_weight=True)
history = model.fit(train_scaled, train_target, epochs=20, verbose=0,
					validation_data = (val_scaled, val_target),
                    callbacks = [checkpoint_cb, early_stopping_cb])

 

print(early_stopping_cb.stopped_epoch)

9

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

 

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

훈련 손실과 검증 손실을 출력해서 확인해보면 다음과 같다. 7번째에서 가장 낮은 손실을 기록했고 9번째에서

 

훈련이 중지되었다. 조기 종료 기법을 사용하면 안심하고 에포크 횟수를 크게 지정해도 상관없다.

 

조기 종료로 얻은 모델을 사용하여 검증 셋에 대한 성능을 확인해보자.

 

model.evaluate(val_scaled, val_target)

375/375 [==============================] - 1s 3ms/step - loss: 0.3355 - accuracy: 0.8801
[0.33551713824272156, 0.8800833225250244]