감사말: 프랑소와 숄레의 Deep Learning with Python, Second Edition 4장에 사용된 코드에 대한 설명을 담고 있으며 텐서플로우 2.6 버전에서 작성되었습니다. 소스코드를 공개한 저자에게 감사드립니다.
tensorflow 버전과 GPU 확인
구글 코랩 설정: '런타임 -> 런타임 유형 변경' 메뉴에서 GPU 지정 후 아래 명령어 실행 결과 확인
!nvidia-smi
사용되는 tensorflow 버전 확인
import tensorflow as tf
tf.__version__
tensorflow가 GPU를 사용하는지 여부 확인
tf.config.list_physical_devices('GPU')
영화 리뷰가 긍정적인지 부정적인지를 판단하는 이항 분류 모델을 구성한다.
IMDB 데이터셋 불러오기
imdb
모듈의 load_data()
함수로 데이터 적재num_words=10000
: 가장 많이 사용되는 10,000개의 단어로만 구성된 리뷰를 불러오도록 지정from tensorflow.keras.datasets import imdb
(train_data, train_labels), (test_data, test_labels) = imdb.load_data(num_words=10000)
훈련셋, 테스트셋의 크기 모두 25,000이다.
len(train_data)
25000
len(test_data)
25000
각 샘플은 num_words=10000
에 의해 1~9999 사이의 정수로 이루어진 리스트이다.
최솟값은 1, 최댓값은 9999임은 아래와 같이 확인한다.
min([min(sequence) for sequence in train_data])
1
max([max(sequence) for sequence in train_data])
9999
샘플들의 크기는 서로 다르다.
len(train_data[0])
218
len(train_data[1])
189
0번 샘플의 처음 10개 값은 다음과 같다.
train_data[0][:10]
[1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65]
각 샘플의 레이블은 0(부정) 또는 1(긍정)이다.
train_labels[0]
1
test_labels[0]
0
리뷰 내용 확인하기
주의사항: 모델 훈련을 위해 반드시 필요한 사항은 아님!
word_index = imdb.get_word_index()
word_index
에 포함된 10개 항목을 확인하면 다음과 같다.
for item in list(word_index.items())[:10]:
print(item)
('fawn', 34701) ('tsukino', 52006) ('nunnery', 52007) ('sonja', 16816) ('vani', 63951) ('woods', 1408) ('spiders', 16115) ('hanging', 2345) ('woody', 2289) ('trawling', 52008)
reverse_word_index = dict([(value, key) for (key, value) in word_index.items()])
reverse_word_index
에 포함된 10개 항목을 확인하면 다음과 같다.
for item in list(reverse_word_index.items())[:10]:
print(item)
(34701, 'fawn') (52006, 'tsukino') (52007, 'nunnery') (16816, 'sonja') (63951, 'vani') (1408, 'woods') (16115, 'spiders') (2345, 'hanging') (2289, 'woody') (52008, 'trawling')
첫째 리뷰 내용을 아래와 같이 확인할 수 있다.
first_review = train_data[0]
decoded_review = " ".join(
[reverse_word_index.get(i-3, "?") for i in first_review])
decoded_review
"? this film was just brilliant casting location scenery story direction everyone's really suited the part they played and you could just imagine being there robert ? is an amazing actor and now the same being director ? father came from the same scottish island as myself so i loved the fact there was a real connection with this film the witty remarks throughout the film were great it was just brilliant so much that i bought the film as soon as it was released for ? and would recommend it to everyone to watch and the fly fishing was amazing really cried at the end it was so sad and you know what they say if you cry at a film it must have been good and this definitely was also ? to the two little boy's that played the ? of norman and paul they were just brilliant children are often left out of the ? list i think because the stars that play them all grown up are such a big profile for the whole film but these children are amazing and should be praised for what they have done don't you think the whole story was so lovely because it was true and was someone's life after all that was shared with us all"
정수들의 리스트, 그것도 길이가 다른 여러 개의 리스트를 신경망의 입력값으로 사용할 수 없다.
또한 모든 샘플의 길이를 통일시킨다 하더라도 정수들의 리스트를 직접 신경망 모델의 입력값으로
사용하려면 나중에 다룰 Embedding
층(layer)과 같은 전처리 층을 사용해야 한다.
여기서는 대신에 멀티-핫-인코딩을 이용하여 정수들의 리스트를
0과 1로만 이루어진 일정한 길이의 벡터(1차원 어레이)로 변환한다.
앞서 보았듯이 리뷰 리스트에 사용된 숫자들은 1부터 9999 사이의 값이다. 이 정보를 이용하여 리뷰 샘플을 길이가 10,000인 벡터(1차원 어레이)로 변환할 수 있다.
예를 들어, [1, 18, 13]
은 길이가 10,000인 1차원 어레이(벡터)로 변환되는데
1번, 18번, 13번 인덱스의 항목만 1이고 나머지는 0으로 채워진다.
이러한 변환을 멀티-핫-인코딩(multi-hot-encoding)이라 부른다.
다음 vectorize_sequences()
함수는 앞서 설명한 멀티-핫-인코딩을
모든 주어진 샘플에 대해 실행한다.
이처럼 각 샘플을 지정된 크기의 1차원 어레이로 변환하는 과정을 벡터화(vectorization)이라 한다.
import numpy as np
def vectorize_sequences(sequences, dimension=10000):
results = np.zeros((len(sequences), dimension))
for i, sequence in enumerate(sequences): # 모든 샘플에 대한 멀티-핫-인코딩
for j in sequence:
results[i, j] = 1.
return results
이제 훈련셋와 테스트셋를 벡터화한다.
x_train = vectorize_sequences(train_data).astype("float32")
x_test = vectorize_sequences(test_data).astype("float32")
첫째 훈련 샘플의 변환 결과는 다음과 같다.
x_train[0]
array([0., 1., 1., ..., 0., 0., 0.], dtype=float32)
레이블 또한 정수 자료형에서 float32
자료형으로 변환해서 자료형을 일치시킨다.
y_train = np.asarray(train_labels).astype("float32")
y_test = np.asarray(test_labels).astype("float32")
입력 샘플의 특성이 벡터(1차원 어레이)로 주어지고
레이블이 스칼라(하나의 숫자)로 주어졌을 때
밀집층(densely-connected layer)과
Sequential
모델을 이용하면 성능 좋은 모델을 얻는다.
이때 사용하는 활성화 함수는 일반적으로 다음과 같다.
relu
함수sigmoid
함수몇 개의 층을 사용하는가와, 각 층마다 몇 개의 유닛(unit)을 사용하는가를 결정해야 하는데 이에 대해서 다음 장에서 자세히 설명한다. 여기서는 일단 다음과 같은 구성을 추천한다.
참고: 이항 분류 모델의 최상위 층은 스칼라 값을 출력하도록 하나의 유닛을 사용하는
Dense
밀집층을 사용한다.
또한 활성화 함수로 0과 1사이의 확률값을 계삲하는 sigmoid
를 활성화 함수로 사용한다.
그러면 사이킷런의 로지스틱 회귀(logistic regression) 모델처럼 작동한다.
from tensorflow import keras
from tensorflow.keras import layers
model = keras.Sequential([
layers.Dense(16, activation="relu"),
layers.Dense(16, activation="relu"),
layers.Dense(1, activation="sigmoid")
])
모델 컴파일링
"rmsprop"
"binary_crossentropy"
(로그 손실)"accuracy"
model.compile(optimizer="rmsprop",
loss="binary_crossentropy",
metrics=["accuracy"])
훈련 중인 모델을 에포크마다 검증하려면 검증 세트를 따로 지정하면 된다. 여기서는 처음 10,000개의 샘플을 검증 세트로 활용한다.
# 검증 세트
x_val = x_train[:10000]
y_val = y_train[:10000]
# 훈련셋
partial_x_train = x_train[10000:]
partial_y_train = y_train[10000:]
훈련을 시작할 때 validation_data
옵션 인자를 지정하면 에포크마다 검증 세트를 이용하여
훈련중인 모델을 평가한다.
validation_data=(x_val, y_val)
: 검증 세트 지정history = model.fit(partial_x_train,
partial_y_train,
epochs=20,
batch_size=512,
validation_data=(x_val, y_val))
Epoch 1/20 30/30 [==============================] - 1s 23ms/step - loss: 0.5248 - accuracy: 0.7867 - val_loss: 0.4199 - val_accuracy: 0.8421 Epoch 2/20 30/30 [==============================] - 0s 14ms/step - loss: 0.3199 - accuracy: 0.8997 - val_loss: 0.3450 - val_accuracy: 0.8600 Epoch 3/20 30/30 [==============================] - 0s 14ms/step - loss: 0.2284 - accuracy: 0.9260 - val_loss: 0.2778 - val_accuracy: 0.8933 Epoch 4/20 30/30 [==============================] - 0s 14ms/step - loss: 0.1817 - accuracy: 0.9399 - val_loss: 0.2725 - val_accuracy: 0.8897 Epoch 5/20 30/30 [==============================] - 0s 14ms/step - loss: 0.1458 - accuracy: 0.9549 - val_loss: 0.2945 - val_accuracy: 0.8812 Epoch 6/20 30/30 [==============================] - 0s 14ms/step - loss: 0.1191 - accuracy: 0.9623 - val_loss: 0.3541 - val_accuracy: 0.8658 Epoch 7/20 30/30 [==============================] - 0s 14ms/step - loss: 0.0990 - accuracy: 0.9721 - val_loss: 0.3103 - val_accuracy: 0.8824 Epoch 8/20 30/30 [==============================] - 0s 14ms/step - loss: 0.0827 - accuracy: 0.9761 - val_loss: 0.3414 - val_accuracy: 0.8814 Epoch 9/20 30/30 [==============================] - 0s 15ms/step - loss: 0.0668 - accuracy: 0.9815 - val_loss: 0.3452 - val_accuracy: 0.8809 Epoch 10/20 30/30 [==============================] - 0s 14ms/step - loss: 0.0526 - accuracy: 0.9878 - val_loss: 0.3763 - val_accuracy: 0.8783 Epoch 11/20 30/30 [==============================] - 0s 14ms/step - loss: 0.0444 - accuracy: 0.9889 - val_loss: 0.4313 - val_accuracy: 0.8724 Epoch 12/20 30/30 [==============================] - 0s 14ms/step - loss: 0.0344 - accuracy: 0.9925 - val_loss: 0.4352 - val_accuracy: 0.8753 Epoch 13/20 30/30 [==============================] - 0s 14ms/step - loss: 0.0284 - accuracy: 0.9945 - val_loss: 0.4716 - val_accuracy: 0.8709 Epoch 14/20 30/30 [==============================] - 0s 14ms/step - loss: 0.0216 - accuracy: 0.9966 - val_loss: 0.5030 - val_accuracy: 0.8689 Epoch 15/20 30/30 [==============================] - 0s 14ms/step - loss: 0.0164 - accuracy: 0.9976 - val_loss: 0.5267 - val_accuracy: 0.8726 Epoch 16/20 30/30 [==============================] - 0s 14ms/step - loss: 0.0120 - accuracy: 0.9990 - val_loss: 0.5764 - val_accuracy: 0.8641 Epoch 17/20 30/30 [==============================] - 0s 14ms/step - loss: 0.0111 - accuracy: 0.9986 - val_loss: 0.6000 - val_accuracy: 0.8709 Epoch 18/20 30/30 [==============================] - 0s 15ms/step - loss: 0.0084 - accuracy: 0.9988 - val_loss: 0.6261 - val_accuracy: 0.8671 Epoch 19/20 30/30 [==============================] - 0s 14ms/step - loss: 0.0045 - accuracy: 0.9999 - val_loss: 0.6670 - val_accuracy: 0.8657 Epoch 20/20 30/30 [==============================] - 0s 15ms/step - loss: 0.0072 - accuracy: 0.9987 - val_loss: 0.7000 - val_accuracy: 0.8648
History
객체 활용¶fit()
메서드가 반환하는 객체는 Callback
클래스를 상속하는
History
클래스의 인스턴이며, 케라스의 모든 모델 훈련과정 중에 발생하는
다양한 정보를 저장한다.
참고: 콜백(Callback
) 클래스에 대해서는 나중에 자세히 살펴볼 예정이다.
params
속성: 모델 훈련에 사용된 파라미터 저장history.params
{'verbose': 1, 'epochs': 20, 'steps': 30}
history
속성: 평가지표를 사전 자료형으로 저장history_dict = history.history
history_dict.keys()
dict_keys(['loss', 'accuracy', 'val_loss', 'val_accuracy'])
예를 들어, history
속성에 저장된 정보를 이용하여
훈련셋와 검증 세트에 대한 에포크별 손실값과 정확도의 변화를 그래프로 그릴 수 있다.
import matplotlib.pyplot as plt
history_dict = history.history
loss_values = history_dict["loss"]
val_loss_values = history_dict["val_loss"]
epochs = range(1, len(loss_values) + 1)
plt.plot(epochs, loss_values, "bo", label="Training loss")
plt.plot(epochs, val_loss_values, "b", label="Validation loss")
plt.title("Training and validation loss")
plt.xlabel("Epochs")
plt.ylabel("Loss")
plt.legend()
plt.show()
plt.clf() # 이전 이미지 삭제
acc = history_dict["accuracy"]
val_acc = history_dict["val_accuracy"]
plt.plot(epochs, acc, "bo", label="Training acc")
plt.plot(epochs, val_acc, "b", label="Validation acc")
plt.title("Training and validation accuracy")
plt.xlabel("Epochs")
plt.ylabel("Accuracy")
plt.legend()
plt.show()
과대적합(overfitting)은 모델이 훈련셋에 익숙해진다는 의미이다. 시험에 비유하면 연습문제의 답을 외워버리는 것을 의미한다. 과대적합을 방지하기 위한 다양한 기법은 다음 장(chapter)에서 다룬다. 위 문제의 경우 넷째 또는 다섯째 에포크 정도만 훈련 반복을 진행하면 된다. 아래 코드는 다시 처음부터 네 번의 에포크만을 사용하여 훈련한 결과를 보여준다.
model = keras.Sequential([
layers.Dense(16, activation="relu"),
layers.Dense(16, activation="relu"),
layers.Dense(1, activation="sigmoid")
])
model.compile(optimizer="rmsprop",
loss="binary_crossentropy",
metrics=["accuracy"])
model.fit(x_train, y_train, epochs=4, batch_size=512)
Epoch 1/4 49/49 [==============================] - 1s 8ms/step - loss: 0.4426 - accuracy: 0.8233 Epoch 2/4 49/49 [==============================] - 0s 8ms/step - loss: 0.2526 - accuracy: 0.9104 Epoch 3/4 49/49 [==============================] - 0s 9ms/step - loss: 0.1966 - accuracy: 0.9282 Epoch 4/4 49/49 [==============================] - 0s 8ms/step - loss: 0.1641 - accuracy: 0.9397
<keras.callbacks.History at 0x2cf3cb63ac0>
테스트셋에 대한 성능은 아래와 같이 88% 정도의 정확도를 보인다. 앞으로 보다 좋은 성능의 모델을 살펴볼 것이며, 현존하는 가장 좋은 모델의 정확도는 95% 정도이다.
results = model.evaluate(x_test, y_test)
results
782/782 [==============================] - 1s 2ms/step - loss: 0.3139 - accuracy: 0.8771
[0.3139097988605499, 0.8770800232887268]
model.predict(x_test)
array([[0.25440323], [0.9999424 ], [0.95840394], ..., [0.17153321], [0.10725482], [0.6672551 ]], dtype=float32)
아래처럼 데이터셋이 클 경우 배치 단위로 묶어서 예측할 수도 있다.
model.predict(x_test, batch_size=512)
array([[0.25440323], [0.9999424 ], [0.95840394], ..., [0.17153329], [0.10725482], [0.6672551 ]], dtype=float32)
binary_crossentropy
대신 mse
를 손실함수로 지정한 후
검증 세트와 테스트셋에 대한 평가지표의 변화를 확인하라.relu
함수 대신 이전에 많이 사용됐었던 tanh
함수를 손실함수로 지정한 후
검증 세트와 테스트셋에 대한 평가지표의 변화를 확인하라.reuters
모듈의 load_data()
함수로 데이터 적재num_words=10000
: 10,000등 이내의 단어만 대상으로 함.from tensorflow.keras.datasets import reuters
(train_data, train_labels), (test_data, test_labels) = reuters.load_data(num_words=10000)
len(train_data)
8982
len(test_data)
2246
주제별 기사 수가 다르다. 훈련셋의 타깃에 사용된 값들의 빈도수를 확인하면 다음과 같다.
from collections import Counter
target_counter = Counter(train_labels)
target_counter
Counter({3: 3159, 4: 1949, 16: 444, 19: 549, 8: 139, 21: 100, 11: 390, 1: 432, 13: 172, 20: 269, 18: 66, 25: 92, 35: 10, 9: 101, 38: 19, 10: 124, 28: 48, 2: 74, 6: 48, 12: 49, 7: 16, 30: 45, 34: 50, 15: 20, 14: 26, 32: 32, 41: 30, 40: 36, 45: 18, 23: 41, 42: 13, 26: 24, 24: 62, 37: 19, 27: 15, 31: 39, 39: 24, 0: 55, 22: 15, 33: 11, 36: 49, 17: 39, 43: 21, 29: 19, 44: 12, 5: 17})
가장 많이 언급된 주제는 총 3159번, 자장 적게 언급딘 주제는 총 10번 기사로 작성되었다.
print(f"최대 기사 수: {max(target_counter.values())}")
print(f"최소 기사 수: {min(target_counter.values())}")
최대 기사 수: 3159 최소 기사 수: 10
각 샘플은 정수들의 리스트이다.
train_data[10]
[1, 245, 273, 207, 156, 53, 74, 160, 26, 14, 46, 296, 26, 39, 74, 2979, 3554, 14, 46, 4689, 4329, 86, 61, 3499, 4795, 14, 61, 451, 4329, 17, 12]
각 샘플 리스트의 길이가 일반적으로 다르다.
len(train_data[10])
31
len(train_data[11])
59
각 샘플에 대한 레이블은 0부터 45까지의 정수로 표현된다. 예를 들어, 10번 기사의 주제는 3이다.
train_labels[10]
3
주제 3은 'earn'(이익)과 연관된다.
참고: 데이터 분석을 위해 반드시 필요한 사항은 아니지만 언급된 46개의 주제와 번호 사이의 관계는 GitHub: Where can I find topics of reuters dataset #12072를 참조할 수 있다.
reuter_topics = {'cocoa': 0,
'grain': 1,
'veg-oil': 2,
'earn': 3,
'acq': 4,
'wheat': 5,
'copper': 6,
'housing': 7,
'money-supply': 8,
'coffee': 9,
'sugar': 10,
'trade': 11,
'reserves': 12,
'ship': 13,
'cotton': 14,
'carcass': 15,
'crude': 16,
'nat-gas': 17,
'cpi': 18,
'money-fx': 19,
'interest': 20,
'gnp': 21,
'meal-feed': 22,
'alum': 23,
'oilseed': 24,
'gold': 25,
'tin': 26,
'strategic-metal': 27,
'livestock': 28,
'retail': 29,
'ipi': 30,
'iron-steel': 31,
'rubber': 32,
'heat': 33,
'jobs': 34,
'lei': 35,
'bop': 36,
'zinc': 37,
'orange': 38,
'pet-chem': 39,
'dlr': 40,
'gas': 41,
'silver': 42,
'wpi': 43,
'hog': 44,
'lead': 45}
실제로 10번 기사 내용을 확인해보면 'earn'과 관련되어 있어 보인다. 데이터를 해독(decoding)하는 방법은 IMDB 데이터셋의 경우와 동일하다.
word_index = reuters.get_word_index()
reverse_word_index = dict([(value, key) for (key, value) in word_index.items()])
10번 기사 내용은 다음과 같다.
decoded_newswire = " ".join([reverse_word_index.get(i - 3, "?") for i in train_data[10]])
decoded_newswire
'? period ended december 31 shr profit 11 cts vs loss 24 cts net profit 224 271 vs loss 511 349 revs 7 258 688 vs 7 200 349 reuter 3'
데이터 벡터화
IMDB의 경우와 동일하게 길이가 10,000인 벡터로 모든 샘플을 변환한다.
x_train = vectorize_sequences(train_data)
x_test = vectorize_sequences(test_data)
앞서 보았듯이 타깃은 0부터 45 사이의 값이다. 이런 경우 정수를 텐서로 변환해서 사용하는 것보다 원-핫-인코딩(one-hot-encoding) 기법을 적용하는 게 좋다.
원-핫-인코딩은 멀티-핫-인코딩 기법과 유사하다. 차이점은 1인 오직 한 곳에서만 사용되고 나머지 항목은 모두 0이 된다. 예를 들어, 3은 길이가 46인 벡터(1차원 어레이)로 변환되는데 3번 인덱스에서만 1이고 나머지는 0이 된다.
아래 함수는 원-핫-인코딩을 실행하는 함수이다. 입력값은 레이블 데이터셋 전체를 대상으로 함에 주의하라.
def to_one_hot(labels, dimension=46):
results = np.zeros((len(labels), dimension))
for i, label in enumerate(labels):
results[i, label] = 1.
return results
훈련셋의 레이블과 테스트셋의 레이블을 인코딩한다.
y_train = to_one_hot(train_labels)
y_test = to_one_hot(test_labels)
인코딩된 레이블 하나를 살펴보자.
y_train[0]
array([0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
to_categorical()
함수¶원-핫-인코딩을 지원하는 함수를 케라스가 지원한다.
참고: 사용된 레이블의 최댓값에 1을 더한 값을 어레이의 길이로 사용한다.
from tensorflow.keras.utils import to_categorical
y_train = to_categorical(train_labels)
y_test = to_categorical(test_labels)
y_train[0]
array([0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], dtype=float32)
참고: 원-핫-인코딩, 멀티-핫-인코딩 등 정수를 사용하는 데이터를 범주형 데이터로 변환하는 전처리 과정을 지원하는 층(layer)이 있다. 예를 들어 tf.keras.layers.CategoryEncoding 층은 원-핫-인코딩과 멀티-핫-인코딩을 지원한다.
모델 정의
IMDB 데이터셋의 경우와는 달리 3 개 이상의 클래스로 분류하는
다중 클래스 분류 모델의 최종 층은
분류해야 하는 클래스의 수 만큼의 유닛과 함께
각 클래스에 속할 확률을 계산하는
softmax
활성화 함수를 이용한다.
참고: 각 클래스에 속할 확률을 모두 더하면 1이 되며, 가장 높은 확률을 가진 클래스를 예측값으로 사용한다.
반면에 이항 분류의 경우보다 복잡한 문제이기에 은닉층의 유닛은 64개씩 정하여 보다 많은 정보를 상위 층으로 전달하도록 한다.
model = keras.Sequential([
layers.Dense(64, activation="relu"),
layers.Dense(64, activation="relu"),
layers.Dense(46, activation="softmax")
])
모델 컴파일
다중 클래스 분류 모델의 손실함수는 categorical_crossentropy
을 사용한다.
categorical_crossentropy
는 클래스의 실제 분포와 예측 클래스의 분포 사이의
오차를 측정하는 손실함수이며,
자세한 내용은 생략한다.
model.compile(optimizer="rmsprop",
loss="categorical_crossentropy",
metrics=["accuracy"])
검증 세트 지정
처음 1,000개의 샘플을 검증 세트 용도로 사용한다.
# 검증 세트
x_val = x_train[:1000]
y_val = y_train[:1000]
# 훈련셋
partial_x_train = x_train[1000:]
partial_y_train = y_train[1000:]
모델 훈련
훈련 방식은 이전과 동일하다.
history = model.fit(partial_x_train,
partial_y_train,
epochs=20,
batch_size=512,
validation_data=(x_val, y_val))
Epoch 1/20 16/16 [==============================] - 1s 25ms/step - loss: 2.7101 - accuracy: 0.4583 - val_loss: 1.7988 - val_accuracy: 0.6410 Epoch 2/20 16/16 [==============================] - 0s 12ms/step - loss: 1.4512 - accuracy: 0.7033 - val_loss: 1.3068 - val_accuracy: 0.7140 Epoch 3/20 16/16 [==============================] - 0s 11ms/step - loss: 1.0563 - accuracy: 0.7727 - val_loss: 1.1291 - val_accuracy: 0.7660 Epoch 4/20 16/16 [==============================] - 0s 11ms/step - loss: 0.8374 - accuracy: 0.8246 - val_loss: 1.0463 - val_accuracy: 0.7930 Epoch 5/20 16/16 [==============================] - 0s 10ms/step - loss: 0.6677 - accuracy: 0.8663 - val_loss: 0.9715 - val_accuracy: 0.7960 Epoch 6/20 16/16 [==============================] - 0s 10ms/step - loss: 0.5328 - accuracy: 0.8934 - val_loss: 0.9264 - val_accuracy: 0.8140 Epoch 7/20 16/16 [==============================] - 0s 11ms/step - loss: 0.4300 - accuracy: 0.9142 - val_loss: 0.9193 - val_accuracy: 0.8210 Epoch 8/20 16/16 [==============================] - 0s 10ms/step - loss: 0.3504 - accuracy: 0.9295 - val_loss: 0.8917 - val_accuracy: 0.8200 Epoch 9/20 16/16 [==============================] - 0s 10ms/step - loss: 0.2853 - accuracy: 0.9377 - val_loss: 0.9481 - val_accuracy: 0.8060 Epoch 10/20 16/16 [==============================] - 0s 11ms/step - loss: 0.2434 - accuracy: 0.9451 - val_loss: 0.9156 - val_accuracy: 0.8150 Epoch 11/20 16/16 [==============================] - 0s 10ms/step - loss: 0.2103 - accuracy: 0.9494 - val_loss: 0.9088 - val_accuracy: 0.8200 Epoch 12/20 16/16 [==============================] - 0s 10ms/step - loss: 0.1840 - accuracy: 0.9509 - val_loss: 0.9644 - val_accuracy: 0.8060 Epoch 13/20 16/16 [==============================] - 0s 10ms/step - loss: 0.1670 - accuracy: 0.9531 - val_loss: 0.9432 - val_accuracy: 0.8180 Epoch 14/20 16/16 [==============================] - 0s 10ms/step - loss: 0.1492 - accuracy: 0.9558 - val_loss: 0.9799 - val_accuracy: 0.8070 Epoch 15/20 16/16 [==============================] - 0s 10ms/step - loss: 0.1446 - accuracy: 0.9563 - val_loss: 1.0445 - val_accuracy: 0.7920 Epoch 16/20 16/16 [==============================] - 0s 12ms/step - loss: 0.1335 - accuracy: 0.9565 - val_loss: 0.9940 - val_accuracy: 0.8170 Epoch 17/20 16/16 [==============================] - 0s 9ms/step - loss: 0.1255 - accuracy: 0.9564 - val_loss: 1.0369 - val_accuracy: 0.8080 Epoch 18/20 16/16 [==============================] - 0s 10ms/step - loss: 0.1203 - accuracy: 0.9565 - val_loss: 1.0909 - val_accuracy: 0.8020 Epoch 19/20 16/16 [==============================] - 0s 10ms/step - loss: 0.1176 - accuracy: 0.9574 - val_loss: 1.0424 - val_accuracy: 0.8140 Epoch 20/20 16/16 [==============================] - 0s 10ms/step - loss: 0.1140 - accuracy: 0.9582 - val_loss: 1.0658 - val_accuracy: 0.8130
손실값의 변화
loss = history.history["loss"]
val_loss = history.history["val_loss"]
epochs = range(1, len(loss) + 1)
plt.plot(epochs, loss, "bo", label="Training loss")
plt.plot(epochs, val_loss, "b", label="Validation loss")
plt.title("Training and validation loss")
plt.xlabel("Epochs")
plt.ylabel("Loss")
plt.legend()
plt.show()
정확도의 변화
plt.clf()
acc = history.history["accuracy"]
val_acc = history.history["val_accuracy"]
plt.plot(epochs, acc, "bo", label="Training accuracy")
plt.plot(epochs, val_acc, "b", label="Validation accuracy")
plt.title("Training and validation accuracy")
plt.xlabel("Epochs")
plt.ylabel("Accuracy")
plt.legend()
plt.show()
모델 재훈련
9번 에포크를 지나면서 과대적합이 발생하는 것으로 보인다. 따라서 에포크 수를 9로 줄이고 처음부터 다시 훈련시켜보자. 모델 구성부터, 컴파일, 훈련을 모두 다시 시작해야 가중치와 편향이 초기화된 상태서 훈련을 시작한다.
model = keras.Sequential([
layers.Dense(64, activation="relu"),
layers.Dense(64, activation="relu"),
layers.Dense(46, activation="softmax")
])
model.compile(optimizer="rmsprop",
loss="categorical_crossentropy",
metrics=["accuracy"])
model.fit(x_train,
y_train,
epochs=9,
batch_size=512)
Epoch 1/9 18/18 [==============================] - 0s 8ms/step - loss: 2.3815 - accuracy: 0.5605 Epoch 2/9 18/18 [==============================] - 0s 8ms/step - loss: 1.2837 - accuracy: 0.7190 Epoch 3/9 18/18 [==============================] - 0s 7ms/step - loss: 0.9710 - accuracy: 0.7927 Epoch 4/9 18/18 [==============================] - 0s 10ms/step - loss: 0.7652 - accuracy: 0.8370 Epoch 5/9 18/18 [==============================] - 0s 9ms/step - loss: 0.6081 - accuracy: 0.8726 Epoch 6/9 18/18 [==============================] - 0s 8ms/step - loss: 0.4839 - accuracy: 0.8952 Epoch 7/9 18/18 [==============================] - 0s 9ms/step - loss: 0.3859 - accuracy: 0.9188 Epoch 8/9 18/18 [==============================] - 0s 8ms/step - loss: 0.3175 - accuracy: 0.9323 Epoch 9/9 18/18 [==============================] - 0s 8ms/step - loss: 0.2638 - accuracy: 0.9404
<keras.callbacks.History at 0x2cf3b42d550>
훈련된 모델을 이용한 테스트셋에 대한 예측의 정확도는 80% 정도이다.
results = model.evaluate(x_test, y_test)
results
71/71 [==============================] - 0s 2ms/step - loss: 0.9436 - accuracy: 0.7996
[0.9436188340187073, 0.7996438145637512]
80%의 정확도가 얼마나 좋은지/나쁜지를 판단하려면 무작위로 찍을 때의 정확도를 계산해봐야 한다. 아래 코드가 이를 실천하며, 20% 정도의 정확도가 나온다. 따라서 80% 정도의 정확도는 상당히 좋은 편이다.
import copy
# 원 데이터를 건드리지 않기 위해 사본 사용
test_labels_copy = copy.copy(test_labels)
# 무작위로 섞은 후 원 데이터의 순서와 비교
np.random.shuffle(test_labels_copy)
hits_array = test_labels == test_labels_copy
# 1 또는 0으로만 이루어졌기에 평균값을 계산하면 무작위 선택의 정확도를 계산함
hits_array.mean()
0.1923419412288513
훈련된 모델을 테스트셋에 적용한다.
predictions = model.predict(x_test)
예측값의 모두 길이가 46인 1차원 어레이다.
predictions[0].shape
(46,)
예측값은 46개 클래스에 들어갈 확률들로 이루어지며 합은 1이다.
np.sum(predictions[0])
0.99999994
가장 큰 확률값을 가진 인덱스가 모델이 예측하는 클래스가 된다. 예를 들어 테스트셋의 0번 샘플(로이터 기사)은 3번 레이블을 갖는다고 예측된다.
np.argmax(predictions[0])
3
정수 텐서 레이블(타깃)을 이용하여 훈련하려면 모델을 컴파일할 때 손실함수로
sparse_categorical_crossentropy
를
사용하면 된다.
y_train = np.array(train_labels)
y_test = np.array(test_labels)
model.compile(optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
은닉층에 사용되는 유닛은 마지막 층의 유닛보다 많아야 한다. 그렇지 않으면 정보전달 과정에 병목현상(bottleneck)이 발생할 수 있다. 아래 코드의 둘째 은닉층은 4 개의 유닛만을 사용하는데 훈련된 모델의 성능이 많이 저하된다.
model = keras.Sequential([
layers.Dense(64, activation="relu"),
layers.Dense(4, activation="relu"),
layers.Dense(46, activation="softmax")
])
model.compile(optimizer="rmsprop",
loss="categorical_crossentropy",
metrics=["accuracy"])
model.fit(partial_x_train,
partial_y_train,
epochs=20,
batch_size=128,
validation_data=(x_val, y_val))
Epoch 1/20 63/63 [==============================] - 1s 6ms/step - loss: 2.7547 - accuracy: 0.3588 - val_loss: 2.0262 - val_accuracy: 0.5630 Epoch 2/20 63/63 [==============================] - 0s 5ms/step - loss: 1.7311 - accuracy: 0.5808 - val_loss: 1.5582 - val_accuracy: 0.5870 Epoch 3/20 63/63 [==============================] - 0s 4ms/step - loss: 1.3904 - accuracy: 0.6163 - val_loss: 1.4050 - val_accuracy: 0.6390 Epoch 4/20 63/63 [==============================] - 0s 5ms/step - loss: 1.1950 - accuracy: 0.6695 - val_loss: 1.3178 - val_accuracy: 0.6670 Epoch 5/20 63/63 [==============================] - 0s 5ms/step - loss: 1.0543 - accuracy: 0.7235 - val_loss: 1.2940 - val_accuracy: 0.6960 Epoch 6/20 63/63 [==============================] - 0s 5ms/step - loss: 0.9497 - accuracy: 0.7512 - val_loss: 1.2820 - val_accuracy: 0.6980 Epoch 7/20 63/63 [==============================] - 0s 5ms/step - loss: 0.8585 - accuracy: 0.7786 - val_loss: 1.2812 - val_accuracy: 0.7120 Epoch 8/20 63/63 [==============================] - 0s 5ms/step - loss: 0.7729 - accuracy: 0.8036 - val_loss: 1.3157 - val_accuracy: 0.7140 Epoch 9/20 63/63 [==============================] - 0s 5ms/step - loss: 0.6988 - accuracy: 0.8227 - val_loss: 1.3189 - val_accuracy: 0.7260 Epoch 10/20 63/63 [==============================] - 0s 5ms/step - loss: 0.6370 - accuracy: 0.8444 - val_loss: 1.3645 - val_accuracy: 0.7300 Epoch 11/20 63/63 [==============================] - 0s 5ms/step - loss: 0.5862 - accuracy: 0.8535 - val_loss: 1.3981 - val_accuracy: 0.7280 Epoch 12/20 63/63 [==============================] - 0s 5ms/step - loss: 0.5424 - accuracy: 0.8596 - val_loss: 1.4963 - val_accuracy: 0.7290 Epoch 13/20 63/63 [==============================] - 0s 5ms/step - loss: 0.5066 - accuracy: 0.8681 - val_loss: 1.5458 - val_accuracy: 0.7180 Epoch 14/20 63/63 [==============================] - 0s 5ms/step - loss: 0.4722 - accuracy: 0.8735 - val_loss: 1.5992 - val_accuracy: 0.7270 Epoch 15/20 63/63 [==============================] - 0s 5ms/step - loss: 0.4450 - accuracy: 0.8807 - val_loss: 1.6689 - val_accuracy: 0.7180 Epoch 16/20 63/63 [==============================] - 0s 5ms/step - loss: 0.4192 - accuracy: 0.8854 - val_loss: 1.7057 - val_accuracy: 0.7210 Epoch 17/20 63/63 [==============================] - 0s 5ms/step - loss: 0.3994 - accuracy: 0.8920 - val_loss: 1.7751 - val_accuracy: 0.7230 Epoch 18/20 63/63 [==============================] - 0s 5ms/step - loss: 0.3830 - accuracy: 0.8948 - val_loss: 1.8543 - val_accuracy: 0.7160 Epoch 19/20 63/63 [==============================] - 0s 5ms/step - loss: 0.3646 - accuracy: 0.8993 - val_loss: 1.8992 - val_accuracy: 0.7220 Epoch 20/20 63/63 [==============================] - 0s 5ms/step - loss: 0.3506 - accuracy: 0.9018 - val_loss: 2.0408 - val_accuracy: 0.7110
<keras.callbacks.History at 0x2cf31967370>
테스트셋에 대한 정확도가 80% 정도에서 65% 정도로 낮아진다.
model.evaluate(x_test, y_test)
71/71 [==============================] - 0s 2ms/step - loss: 2.1817 - accuracy: 0.6923
[2.1817328929901123, 0.6923419237136841]
이항 분류, 다중 클래스 분류 모델은 지정된 숫자들로 이루어진 특정 클래스의 번호 하나를 예측한다. 반면에 임의의 수를 예측하는 문제는 회귀(regression)이라 부른다. 예를 들어 온도 예측, 가격 예측 등을 다루는 것이 회귀 문제이다. 여기서는 보스턴 시의 주택가격을 예측하는 회귀 문제를 예제로 다룬다.
주의사항: '로지스틱 회귀'(logistic regression) 알고리즘는 분류 모델임에 주의하라.
사용하는 데이터셋은 다음과 같다.
boston_housing
모듈의 load_data()
함수로 데이터 적재보스턴 주택가격 데이터셋 적재
from tensorflow.keras.datasets import boston_housing
(train_data, train_targets), (test_data, test_targets) = boston_housing.load_data()
train_data.shape
(404, 13)
test_data.shape
(102, 13)
훈련셋 샘플의 타깃은 아래처럼 범위가 지정되지 않은 부동소수점 값이다.
train_targets[:10]
array([15.2, 42.3, 50. , 21.1, 17.7, 18.5, 11.3, 15.6, 15.6, 14.4])
특성에 따라 사용되는 값들의 크기가 다르다. 어떤 특성은 0과 1사이, 다른 특성은 한 자리리부터 세 자리의 수를 갖기도 한다.
import pandas as df
df.DataFrame(train_data).describe()
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
count | 404.000000 | 404.000000 | 404.000000 | 404.000000 | 404.000000 | 404.000000 | 404.000000 | 404.000000 | 404.000000 | 404.000000 | 404.000000 | 404.000000 | 404.000000 |
mean | 3.745111 | 11.480198 | 11.104431 | 0.061881 | 0.557356 | 6.267082 | 69.010644 | 3.740271 | 9.440594 | 405.898515 | 18.475990 | 354.783168 | 12.740817 |
std | 9.240734 | 23.767711 | 6.811308 | 0.241238 | 0.117293 | 0.709788 | 27.940665 | 2.030215 | 8.698360 | 166.374543 | 2.200382 | 94.111148 | 7.254545 |
min | 0.006320 | 0.000000 | 0.460000 | 0.000000 | 0.385000 | 3.561000 | 2.900000 | 1.129600 | 1.000000 | 188.000000 | 12.600000 | 0.320000 | 1.730000 |
25% | 0.081437 | 0.000000 | 5.130000 | 0.000000 | 0.453000 | 5.874750 | 45.475000 | 2.077100 | 4.000000 | 279.000000 | 17.225000 | 374.672500 | 6.890000 |
50% | 0.268880 | 0.000000 | 9.690000 | 0.000000 | 0.538000 | 6.198500 | 78.500000 | 3.142300 | 5.000000 | 330.000000 | 19.100000 | 391.250000 | 11.395000 |
75% | 3.674808 | 12.500000 | 18.100000 | 0.000000 | 0.631000 | 6.609000 | 94.100000 | 5.118000 | 24.000000 | 666.000000 | 20.200000 | 396.157500 | 17.092500 |
max | 88.976200 | 100.000000 | 27.740000 | 1.000000 | 0.871000 | 8.725000 | 100.000000 | 10.710300 | 24.000000 | 711.000000 | 22.000000 | 396.900000 | 37.970000 |
데이터 정규화
따라서 모든 특성의 값을 정규화 해주어야 모델 훈련이 더 잘된다. 모든 특성값들을 특성별로 표준 정규분포를 따르도록 한다. 즉, 평균값 0, 표준편차 1이 되도록 특성값을 특성별로 변환한다.
주의사항: 테스트셋의 정규화는 훈련셋의 평균값과 표준편차를 이용해야 한다. 이유는 테스트셋의 정보는 모델 훈련에 절대로 사용되지 않아야 하기 때문이다.
# 훈련셋의 평균값
mean = train_data.mean(axis=0)
# 훈련셋 정규화
train_data -= mean
std = train_data.std(axis=0)
train_data /= std
# 테스트셋 정규화: 훈련셋의 평균값과 표준편차 활용
test_data -= mean
test_data /= std
모델 정의
이전과는 달리 모델 구성과 컴파일을 동시에 진행하는 함수를 이용한다.
참고: 데이터셋이 클 수록 보다 많은 층과 보다 많은 유닛 사용하는 것이 일반적임.
def build_model():
model = keras.Sequential([
layers.Dense(64, activation="relu"),
layers.Dense(64, activation="relu"),
layers.Dense(1)
])
model.compile(optimizer="rmsprop", loss="mse", metrics=["mae"])
return model
데이터셋이 작기에 훈련 중에 사용할 검증 세트를 따로 분리하는 것은 훈련의 효율성을 떨어뜨린다. 대신에 K-겹 교차검증을(K-fold cross-validation) 사용한다. 아래 이미지는 3-겹 교차검증을 사용할 때 훈련 중에 사용되는 훈련셋과 검증셋의 사용법을 보여준다.
예제: 4-겹 교차검증
validation_data
옵션 인자 활용verbose=0
: 손실값과 평가지표를 출력하지 않음.k = 4
num_val_samples = len(train_data) // k
num_epochs = 500
all_mae_histories = [] # 모든 에포크에 대한 평균절대오차 저장
for i in range(k): # 교차 검증
print(f"{i+1}번 째 폴드(fold) 훈련 시작")
val_data = train_data[i * num_val_samples: (i + 1) * num_val_samples]
val_targets = train_targets[i * num_val_samples: (i + 1) * num_val_samples]
partial_train_data = np.concatenate(
[train_data[:i * num_val_samples],
train_data[(i + 1) * num_val_samples:]],
axis=0)
partial_train_targets = np.concatenate(
[train_targets[:i * num_val_samples],
train_targets[(i + 1) * num_val_samples:]],
axis=0)
model = build_model() # 유닛 수: 64
history = model.fit(partial_train_data, partial_train_targets,
validation_data=(val_data, val_targets),
epochs=num_epochs, batch_size=16, verbose=0)
mae_history = history.history["val_mae"]
all_mae_histories.append(mae_history)
1번 째 폴드(fold) 훈련 시작 2번 째 폴드(fold) 훈련 시작 3번 째 폴드(fold) 훈련 시작 4번 째 폴드(fold) 훈련 시작
K-겹 교차검증 훈련 과정 그래프: 평가지표 기준
500번의 에포크마다 4 번의 교차 검증을 진행하였기에 에포크 별로 검증세트를 대상으로하는 평균절대오차의 평균값을 계산한다.
average_mae_history = [
np.mean([x[i] for x in all_mae_histories]) for i in range(num_epochs)]
에포크별 평균절대오차의 평균값의 변화를 그래프로 그리면 다음과 같다.
plt.plot(range(1, len(average_mae_history) + 1), average_mae_history)
plt.xlabel("Epochs")
plt.ylabel("Validation MAE")
plt.show()
첫 10개의 에포크의 성능이 매우 나쁘기에 그 부분을 제외하고 그래프를 그리면 보다 정확하게 변환 과정을 파악할 수 있다.
truncated_mae_history = average_mae_history[10:]
plt.plot(range(1, len(truncated_mae_history) + 1), truncated_mae_history)
plt.xlabel("Epochs")
plt.ylabel("Validation MAE")
plt.show()
재훈련
model = build_model()
model.fit(train_data, train_targets,
epochs=130, batch_size=16, verbose=0)
<keras.callbacks.History at 0x2cf386a5670>
재훈련된 모델의 테스트셋에 대한 성능을 평가하면 주택가격 예측에 있어서 평균적으로 2,500달러 정도의 차이를 갖는다.
test_mse_score, test_mae_score = model.evaluate(test_data, test_targets)
test_mae_score
4/4 [==============================] - 0s 0s/step - loss: 15.9659 - mae: 2.5950
2.5949742794036865
predict()
메서드를 활용한다. predictions = model.predict(test_data)
predictions[0]
array([8.091597], dtype=float32)
사이킷런의 KFold
를 이용하면 봅다 간단하게 K-겹 교차검증을 진행할 수 있다.
from sklearn.model_selection import KFold
k = 4
num_epochs = 500
kf = KFold(n_splits=k)
all_mae_histories = []
for train_index, val_index in kf.split(train_data, train_targets):
val_data, val_targets = train_data[val_index], train_targets[val_index]
partial_train_data, partial_train_targets = train_data[train_index], train_targets[train_index]
model = build_model()
history = model.fit(partial_train_data, partial_train_targets,
validation_data=(val_data, val_targets),
epochs=num_epochs, batch_size=16, verbose=0)
mae_history = history.history["val_mae"]
all_mae_histories.append(mae_history)
test_mse_score, test_mae_score = model.evaluate(test_data, test_targets)
test_mae_score
4/4 [==============================] - 0s 0s/step - loss: 15.7864 - mae: 2.7254
2.725350856781006