인공신경망(ANN)에 대한 이론이 필요하신 분들은 아래 링크를 참조해주시기 바랍니다.
[인공지능][개념&실습] 인공 신경망(ANN)에 대해 알아보고 패션 MNIST 데이터셋으로 패션 아이템을 분류해보자
: https://itstory1592.tistory.com/21
지난 포스팅에서는 인공신경망(ANN)에 대한 이론을 알아보고 직접 신경망을 통해 패션 아이템을 분류하는 실습을 진행해보았다.
확실히 이전에 알아보았던 머신러닝 알고리즘에 비해 직접 모델의 구조를 만든다는 느낌이 강하게 들었을 것이다.
층을 추가하고 층에 있는 뉴런 개수와 활성화 함수를 결정하는 등 해야 하는 일이 추가되었기 때문이다.
그럼 모델에는 무조건 층을 많이 추가하고, 훈련을 많이 시키기만 하면 좋아지는 것일까?
정답은 NO!
복잡한 모델이 요구되는 경우에서 모델을 과도하게 훈련을 시키게 되면 과대적합(Overfitting)은 물론 손실값이 높아지는 현상이 발생하여 모델의 성능이 크게 저하될 수 있다.
이번 글에서는 이런 문제들을 해결할 수 있는 몇 가지 방법들에 대해 알아보도록 하자!
손실 곡선
이전 예제에서 모델을 훈련시키면 손실값이 점차 줄어드는 현상을 확인할 수 있었다.
우선, 손실값을 그래프로 표현해보면서 훈련 세트에서만 손실값이 줄어들었을 때 발생하는 문제를 살펴보도록 하자.
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)
손실 곡선 그래프를 표현하기 위해 이전과 동일하게 케라스에서 제공해주는 패션 MNIST 데이터셋을 불러온 후,
훈련 세트, 검증 세트, 테스트 세트로 각각 분리시켜준다.
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
이번 실습에서는 모델을 여러번 만들어야 하는 일이 많을 것이기 때문에, 사용자 정의 함수를 정의하여
간단한 층을 만들어주는 기능을 미리 만들어재는 것이 좋다.
기본적으로 패션MNIST 데이터셋은 28 x 28 크기의 이미지이기 때문에,
이 이미지를 1 x 786으로 펴줄 수 있는 Flatten 객체를 포함하고 있고,
은닉층(Hidden Layer)으로는 렐루(Relu) 함수를 사용하며, 100개의 뉴런으로 구성되어 있는 층을 추가해줄 것이다.
그리고 사용자가 임의로 층을 하나 더 추가하고 싶다면, 매개변수로 레이어를 입력해주어 그 사이에 층을 추가할 수 있다.
마지막 층으로는 소프트맥스(Softmax) 함수를 사용한 출력층(Output Layer)을 추가하여, 해당 모델을 반환해주는 함수이다.
model = model_fn()
model.summary()
위에서 만든 함수를 호출하여 summary() 메소드를 통해 모델의 구조를 확인해보면,
이전 포스팅과 동일한 모델이 생성되었음을 알 수 있다.
그럼 테스트를 위해 해당 모델을 훈련시켜보도록 하자.
model.compile(loss='sparse_categorical_crossentropy', metrics='accuracy')
history = model.fit(train_scaled, train_target, epochs=5, verbose=0)
print(history.history.keys())
모델의 구조는 동일하지만, 매개변수 중 verbose를 0으로 설정하여 훈련 과정을 나타내지 않도록 설정하였다.
또한, fit() 메소드를 출력할 때, 메소드의 결과를 history 변수에 담아보도록 하겠다.
history 객체에는 훈련 측정값이 담겨 이는 history 딕셔너리가 들어있다.
이 딕셔너리에는 어떤 값이 있는지 확인해보자.
(verbose의 기본값은 1로 과정을 진행 막대와 함께 손실 등의 지표가 출력되며, 2로 바꾸면 막대를 빼고 출력한다.)
출력해본 결과, 손실(loss)과 정확도(accuracy)가 포함되어 있다.
케라스는 기본적으로 에포크마다 손실을 계산하는데, 정확도까지 포함되어 있는 이유는 compile() 메소드에서 metrics 매개변수에 accuracy를 추가했기 때문이다.
이 손실과 정확도는 에포크마다 계산한 값을 순서대로 나열한 단순한 리스트이다.
맷플롯립을 통해 그래프로 그려보면 아래와 같은 결과가 출력된다.
import matplotlib.pyplot as plt
#손실곡선 출력
plt.plot(history.history['loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.savefig('7_3-01', dpi=300)
plt.show()
#정확도 그래프 출력
plt.plot(history.history['accuracy'])
plt.xlabel('epoch')
plt.ylabel('accuracy')
plt.savefig('7_3-02', dpi=300)
plt.show()
|
|
파이썬 리스트이 인덱스는 0부터 시작하기 때문에 5개의 에포크를 출력할 때 0부터 4까지 x축에 표현된다.
왼쪽 그래프는 에포크가 증가함에 따라 감소하는 손실값을 표현해주고 있다.
반면 오른쪽 그래프를 보면 훈련을 반복하여 손실값이 줄어들 때마다 정확도가 계속해서 상승하고 있다.
에포크 횟수를 늘려 훈련을 반복해나가면 손실값이 줄어들고, 정확도가 높아진다는 사실을 알게 되었다.
그렇다면 이번에는 더 낮은 손실값과 높은 정확도를 위해, 에포크를 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.savefig('7_3-03', dpi=300)
plt.show()
예상대로 손실값이 계속해서 감소하는 모습을 확인할 수 있다.
하지만...
여기엔 한가지 문제점이 존재한다.
''' 손실값도 줄어들고 정확도도 증가하였는데 대체 뭐가 문제라는 거지?!😫 '''
무엇이 문제인지 파악하기 위해서는 검증 손실에 대해 알아보도록 하자.
검증 손실
인공 신경망에서는 모두 일종의 경사 하강법 개념을 사용하기 때문에,
확률적 경사 하강법을 사용했을 때처럼 과대/과소 적합과 에포크 사이의 관계도 동일하게 적용된다.
에포크에 따른 과대/과소 적합을 파악하려면 훈련 세트에 대한 점수뿐만 아니라 검증 세트에 대한 점수도 알 필요가 있다.
따라서 위처럼 훈련 세트의 손실 곡선만 표현해서는 안된다는 것이다.
이번에는 훈련 세트와 검증 세트에 대한 손실 곡선을 동시에 그려서 무엇이 문제였는지 파악해보자! 🔥
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))
print(history.history.keys())
에포크마다 검증 손실을 계산하기 위해 케라스 모델의 fit() 메소드에 검증 데이터를 전달할 수 있다.
위 코드처럼 validation_data 매개변수에 검증에 사용할 입력과 타겟값을 튜플로 전달하면 된다.
그럼 이런 결과가 출력될 것이다.
예상대로라면, 검증 세트에 대한 손실은 '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.savefig('7_3-04', dpi=300)
plt.show()
초기에는 검증 손실이 감소하지만, 다섯 번째 에포크만에 다시 상승하기 시작한다.
훈련 손실은 꾸준히 감소하기 때문에 전형적인 과대적합 모델이 만들어지게 된다.
''' 그렇다면, 검증 손실이 상승하는 시점을 가능한 뒤로 늦추면
검증 세트에 대한 손실이 줄어들 뿐만 아니라 정확도도 증가하지 않을까? '''
일단 이전 시간처럼, 옵티마이저(Optimizer) 하이퍼파라미터를 조정하여 과대적합을 완화시킬 수 있는지 시도해보자.
optimizer의 기본값은 RMSprop이다.
이 기본값 대신에 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.savefig('7_3-05', dpi=300)
plt.show()
compile() 메소드의 optimizer를 'adam'으로 설정하여 모델을 훈련시켰고,
해당 모델의 훈련 손실과 검증 손실을 출력해보았다.
여전히 그래프에 요동이 남아있기는 하지만 옵티마이저로 'RMSprop'을 사용했을 때와는 다르게,
과대적합이 훨씬 줄어들었을 뿐만 아니라 불안정성도 어느 정도 해결된 모습이다.
더 나은 손실 곡선을 얻기 위해서는 옵티마이저를 변경하는 것 외에도, 학습률(learning rate)을 조정하는 방법도 있다.
이번에는 인공 신경망의 대표적인 규제(Regularization) 방법에 대해 알아보겠다.
드롭아웃(Dropout)
드롭아웃(Dropout)은 딥러닝의 아버지로 불리는 '제프리 힌턴'이 소개한 규제 방법이다.
이 방식은 아래 그림처럼 훈련 과정에서 층에 있는 일부 뉴런을 랜덤하게 꺼서 (뉴런의 출력을 0으로 만들어) 과대적합을 막는다.
드롭아웃을 적용하기 전의 이미지를 보면, 모든 뉴런이 제 역할을 수행하고 있지만,
드롭아웃을 적용하여 랜덤하게 뉴런을 꺼버렸을 땐, 아무런 수행을 하지 않는 것을 확인할 수 있다.
이 방식은 뉴런의 출력을 '랜덤하게' 0으로 만들기 때문에 모델을 훈련시킬 때마다 드롭아웃되는 뉴런은 달라지게 된다.
이처럼 일부 뉴런을 랜덤하게 껐을 때의 장점은
특정 뉴런에 과대하게 의존하는 것을 줄일 수 있고,
모든 입력에 대해 집중을 기울여야 한다는 점이다.
일부 뉴런의 출력이 없을 수 있다는 것을 감안하면 신경망은 더 안정적인 예측을 만들 수 있을 것이다.
물론 얼마나 많은 뉴런을 드롭할지는 우리가 정해야 할 또 다른 하이퍼파라미터이다.
model = model_fn(keras.layers.Dropout(0.3))
model.summary()
케라스에서는 드롭아웃을 keras.layers 패키지 아래 Dropout 클래스로 제공해준다.
어떤 층의 뒤에 드롭아웃을 두어 해당 층의 출력을 랜덤하게 0으로 만드는 것이다.
Dropout 또한 Flatten과 마찬가지로 층 개념은 아니기 때문에 훈련되는 모델 파라미터는 따로 없다.
앞에서 정의한 model_fn() 함수에 30% 정도를 드롭아웃하는 클래스를 추가하여 전달해보고, 모델의 구조를 확인해보자.
출력 결과에서 볼 수 있듯이 은닉층 뒤에 추가된 드롭아웃 층은 훈련되는 모델 파라미터가 없다.
또한 입력과 출력의 크기가 같다.
일부 뉴런의 출력을 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.savefig('7_3-06', dpi=300)
plt.show()
단순히 옵티마이저를 변경했을 때보다 과대적합이 확실히 줄어들었다.
열 번째 에포크쯤에서 검증 손실의 감소가 멈추지만 크게 상승하지 않고 어느 정도 유지되는 모습이다.
하지만 이 모델은 20번의 에포크 동안 훈련을 했기 때문에 결국엔 다소 과대적합 되어 있다.
과대적합 문제를 해결하기 위해 epoch 매개변수를 다시 10으로 바꾸어주기만 하면 되지만,
에포크 횟수 말고도 다른 매개변수를 함께 테스트해야 하는 상황이라면 모델을 훈련할 때마다 일일이 매개변수를 하나하나 수정해주어야 해야 하는 것일까?
이런 귀찮음을 해결해주기 위해 케라스에서는 수차례의 훈련 동안 찾아낸 최상의 파라미터를 저장해주고 복원해주는 기능을 제공해준다.
모델 저장(Save)과 복원(Restore)
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.save_weights('model-weights.h5')
model.save('model-whole.h5')
케라스의 모델 객체에는 훈련된 모델의 파라미터를 저장하는 간편한 save_weights() 메소드를 제공한다.
또한, 모델 구조와 모델 파라미터를 함께 저장하는 save() 메소드도 제공해준다.
기본적으로 이 메소드는 각각 텐서플로의 체크포인트 포맷과 SavedModel 포맷으로 저장하지만 파일의 확장자가 '. h5'인 경우엔 HDF5 포맷으로 저장한다.
파일명은 'model-weights.h5'와 'model-whole.h5'로 지정하고,
파일이 정상적으로 저장되었는지 출력을 통해 확인해보자.
!ls -al *.h5
예상했던대로 두 파일이 모두 정상적으로 저장되었다.
이제 두 가지 테스트를 진행해보자.
첫 번째는 훈련을 하지 않은 새로운 모델을 만들고 model-weights.h5 파일에서 훈련된 모델 파라미터를 읽어서 사용해보고,
두 번째는 model-whole.h5 파일에서 새로운 모델을 바로 만들어 사용해보는 것이다.
첫 번째 테스트부터 진행해보자.
model = model_fn(keras.layers.Dropout(0.3))
model.load_weights('model-weights.h5')
훈련되지 않은 새로운 모델을 model_fn()을 통해 만들고 기존에 저장했던 모델 파라미터를 적재시켰다.
저장은 save_weights() 메소드이지만 불러올 땐 load_weights() 메소드를 사용하면 된다.
단 여기서 불러오는 조건이 있는데,
save_weights() 메소드로 저장했던 모델의 구조와 정확히 동일한 구조를 이루고 있어야 한다.
그렇지 않으면 오류가 발생하여 문제가 되지만,
우리는 동일한 구조의 모델을 생성하여 불러오는 것이기 때문에 정상적으로 모델 파라미터를 불러올 것이다.
이제는 아예 모델 자체를 불러오는 load_model() 메소드를 호출해보자!
model = keras.models.load_model('model-whole.h5')
model.evaluate(val_scaled, val_target)
모델을 파일에서 불러오고 평가해본 결과 이전에 출력했던 결과와 동일한 정확도를 얻을 수 있었다.
이유는 같은 모델을 저장하고 다시 불러들였기 때문이다.
load_model() 함수는 모델 파라미터뿐만 아니라 모델 구조와 옵티마이저 상태까지 모두 복원해주는 장점이 있다.
하지만 텐서플로 2.3에서는 load_model() 메소드에 버그가 있기 때문에
evaluate() 메소드 사용 전에 compile() 메소드를 호출해주어야 한다.
지금까지 최적의 모델이나 파라미터를 저장하는 저장과 복원에 대해 알아보았다.
그런데 이 과정을 다시 생각해보면 20번의 에포크 동안 모델을 멈추지 않고 계속 훈련시켰다.
여기서는 간단한 예제이기 때문에 20번의 훈련이 짧은 시간에 이루어졌지만 많은 양의 데이터를 훈련할 때에는 너무 많은 시간이 소요될 것이다.
''' 그럼 최적의 파라미터를 찾게 되면 더 이상 의미 없는 훈련을 하지 않게 할 수는 없을까? '''
콜백(Callback)
콜백(Callback)은 훈련 과정 중간에 어떤 작업을 수행할 수 있도록 해주는 객체이다.
마치 특정 조건이 되면 그 상황을 전화로 알려주듯이 말이다.
모델 객체의 fit() 메소드의 callbacks 매개변수에 사용할 콜백을 리스트로 전달해주는 것만으로 간단히 사용할 수 있다.
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])
print(early_stopping_cb.stopped_epoch)
코드에 보이는 ModelCheckpoint 콜백은 최상의 검증 점수를 만드는 모델을 저장하고,
EarlyStopping은 조기 종료라고 불리는 콜백으로, 과대적합이 커지기 전에 훈련을 미리 중지해주는 역할을 수행한다.
EarlyStopping의 매개변수로 patience가 있는데 값을 2로 지정하여 2번 연속 검증 점수가 향상되지 않으면 훈련을 조기 종료하도록 설정해준다.
또한, restore_best_weights를 True로 지정하면 가장 낮은 검증 손실을 낸 모델 파라미터로 되돌린다.
두 콜백을 사용한 모델을 훈련시키고, early_stopping_cb 객체의 stopped_epoch에 접근하여 몇 번째 에포크에서 훈련이 중지되었는지 확인해보면 . . .
11에서 모델의 훈련이 종료되었다.
에포크 횟수는 0부터 시작하기 때문에 에포크가 11이라는 것은 열두 번째 에포크에서 훈련이 종료되었다는 뜻이다.
patience를 2로 지정했기 때문에 사실상 최상의 모델은 열 번째 에포크일 것이다.
한 번 우리가 만들어낸 최상의 모델의 훈련 손실과 검증 손실을 출력하여 확인해보자.
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(['train', 'val'])
plt.savefig('7_3-07', dpi=300)
plt.show()
결과를 확인해보면 epoch 9에서 가장 낮은 검증 손실을 기록하였고,
patience를 2로 설정하였기 때문에 epoch 11에서 조기종료되었다.
이처럼 조기 종료 기법을 사용하면 에포크 횟수를 크게 지정해도 스스로 적절한 때에 맞춰 훈련을 종료해주기 때문에,
컴퓨터 자원과 시간을 아낄 수 있으면 ModelCheckpoint 콜백까지 함께 사용해주면 최상의 모델을 자동 저장해주므로 매우 편리하다.
정리하자면, 여러 종류의 옵티마이저를 지정해보면서 해당 모델에 적절한 옵티마이저를 찾고,
ModelCheckpoint와 EarlyStopping 콜백을 사용하여 최적의 파라미터 또는 모델을 저장하고, 적절한 에포크 횟수를 구한다면 불필요한 훈련을 줄일 수 있으며 최상의 모델을 만들어낼 수 있다.
전체 소스 코드 :
https://colab.research.google.com/drive/1EJbymLn7J2VjQR4XBUAPzzApDKPTx_e2#scrollTo=hZsGl9udlqZk
👍클릭으로 구독하기👍
(이해가 다소 힘들거나, 틀린 부분이 있다면 댓글 부탁드리겠습니다! 😊)
💖도움이 되셨다면 '구독'과 '공감' 부탁드립니다!💖
'인공지능' 카테고리의 다른 글
[인공지능][실습] 합성곱 신경망(Convolution Neural Network) - CNN 모델로 패션 MNIST 데이터셋 훈련시키기 (33) | 2021.06.01 |
---|---|
[인공지능][개념] 합성곱 신경망(CNN) - 패딩(Padding)과 스트라이드(Strides), 풀링(Pooling) 완전정복하기 (10) | 2021.05.31 |
[인공지능][개념&실습] 인공 신경망(ANN)에 대해 알아보고 패션 MNIST 데이터셋으로 패션 아이템을 분류해보자 (16) | 2021.05.27 |
[인공지능][개념&실습] 차원 축소 알고리즘 - 주성분 분석(PCA)을 알아보고 사진의 크기를 줄여보자 (2) | 2021.05.25 |
[인공지능][개념] K-Means 알고리즘의 문제점과 'K-Means++ 클러스터링'을 통해 개선하기 (0) | 2021.05.24 |