3.4 영화 리뷰 분류: 이진 분류 예제 | 목차 | 3.6 주택 가격 예측: 회귀 문제 Show 이전 절에서 완전 연결된 신경망을 사용하여 벡터 입력을 어떻게 2개의 클래스로 분류하는지 보았습니다. 2개 이상의 클래스가 있을 때는 어떻게 해야 할까요? 이 절에서 로이터(Reuter) 뉴스를 46개의 상호 배타적인 토픽으로 분류하는 신경망을 만들어 보겠습니다. 클래스가 많기 때문에 이 문제는 다중 분류multiclass classification의 예입니다. 각 데이터 포인트가 정확히 하나의 범주로 분류되기 때문에 좀 더 정확히 말하면 단일 레이블 다중 분류single-label, multiclass classification 문제입니다. 각 데이터 포인트가 여러 개의 범주(예를 들어 토픽)에 속할 수 있다면 이것은 다중 레이블 다중 분류multi-label, multiclass classification 문제가 됩니다. 1986년에 로이터에서 공개한 짧은 뉴스 기사와 토픽의 집합인 로이터 데이터셋을 사용하겠습니다.32 이 데이터셋은 텍스트 분류를 위해 널리 사용되는 간단한 데이터셋입니다. 46개의 토픽이 있으며 어떤 토픽은 다른 것에 비해 데이터가 많습니다. 각 토픽은 훈련 세트에 최소한 10개의 샘플을 가지고 있습니다. IMDB, MNIST와 마찬가지로 로이터 데이터셋은 케라스에 포함되어 있습니다. 한번 살펴보죠. from keras.datasets import reuters (train_data, train_labels), (test_data, test_labels) = reuters.load_data( num_words=10000) IMDB 데이터셋처럼 num_words=10000 매개변수는 데이터에서 가장 자주 등장하는 단어 1만 개 로 제한합니다. 여기에는 8,982개의 훈련 샘플과 2,246개의 테스트 샘플이 있습니다.33 >>> len(train_data) 8982 >>> len(test_data) 2246 IMDB 리뷰처럼 각 샘플은 정수 리스트입니다(단어 인덱스). >>> 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] 궁금한 경우를 위해 어떻게 단어로 디코딩하는지 알아보겠습니다. word_index = reuters.get_word_index() reverse_word_index = dict([(value, key) for (key, value) in word_index.items()]) decoded_newswire = ' '.join([reverse_word_index.get(i - 3, '?') for i in train_data[0]]) 0, 1, 2는 ‘패딩’, ‘문서 시작’, ‘사전에 없음’을 위한 인덱스이므로 3을 뺍니다. 샘플에 연결된 레이블은 토픽의 인덱스로 0과 45 사이의 정수입니다. >>> train_labels[10] 3 3.5.2 데이터 준비이전의 예제와 동일한 코드를 사용해서 데이터를 벡터로 변환합니다.34 import numpy as np def vectorize_sequences(sequences, dimension=10000): results = np.zeros((len(sequences), dimension)) for i, sequence in enumerate(sequences): results[i, sequence] = 1. return results x_train = vectorize_sequences(train_data) # 훈련 데이터 벡터 변환 x_test = vectorize_sequences(test_data) # 테스트 데이터 벡터 변환 레이블을 벡터로 바꾸는 방법은 두 가지입니다. 레이블의 리스트를 정수 텐서로 변환하는 것과 원–핫 인코딩을 사용하는 것입니다. 원–핫 인코딩이 범주형 데이터에 널리 사용되기 때문에 범주형 인코딩categorical encoding이라고도 부릅니다. 원–핫 인코딩에 대한 자세한 설명은 6.1절을 참고하세요. 이 경우 레이블의 원–핫 인코딩은 각 레이블의 인덱스 자리는 1이고 나머지는 모두 0인 벡터입니다. 다음과 같습니다.35 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 one_hot_train_labels = to_one_hot(train_labels) # 훈련 레이블 벡터 변환 one_hot_test_labels = to_one_hot(test_labels) # 테스트 레이블 벡터 변환 MNIST 예제에서 이미 보았듯이 케라스에는 이를 위한 내장 함수가 있습니다. from keras.utils.np_utils import to_categorical one_hot_train_labels = to_categorical(train_labels) one_hot_test_labels = to_categorical(test_labels) 3.5.3 모델 구성이 토픽 분류 문제는 이전의 영화 리뷰 분류 문제와 비슷해 보입니다. 두 경우 모두 짧은 텍스트를 분류하는 것이죠. 여기에서는 새로운 제약 사항이 추가되었습니다. 출력 클래스의 개수가 2에서 46개로 늘어난 점입니다. 출력 공간의 차원이 훨씬 커졌습니다. 이전에 사용했던 것처럼 Dense 층을 쌓으면 각 층은 이전 층의 출력에서 제공한 정보만 사용할 수 있습니다. 한 층이 분류 문제에 필요한 일부 정보를 누락하면 그다음 층에서 이를 복원할 방법이 없습니다. 각 층은 잠재적으로 정보의 병목information bottleneck이 될 수 있습니다. 이전 예제에서 16차원을 가진 중간층을 사용했지만 16차원 공간은 46개의 클래스를 구분하기에 너무 제약이 많을 것 같습니다. 이렇게 규모가 작은 층은 유용한 정보를 완전히 잃게 되는 정보의 병목 지점처럼 동작할 수 있습니다. 이런 이유로 좀 더 규모가 큰 층을 사용하겠습니다. 64개의 유닛을 사용해 보죠. from keras import models from keras import layers model = models.Sequential() model.add(layers.Dense(64, activation='relu', input_shape=(10000,))) model.add(layers.Dense(64, activation='relu')) model.add(layers.Dense(46, activation='softmax')) 이 구조에서 주목해야 할 점이 두 가지 있습니다.
이런 문제에 사용할 최선의 손실 함수는 categorical_crossentropy입니다. 이 함수는 두 확률 분포 사이의 거리를 측정합니다. 여기에서는 네트워크가 출력한 확률 분포와 진짜 레이블의 분포 사이의 거리입니다. 두 분포 사이의 거리를 최소화하면 진짜 레이블에 가능한 가까운 출력을 내도록 모델을 훈련하게 됩니다. model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy']) 3.5.4 훈련 검증훈련 데이터에서 1,000개의 샘플을 따로 떼어서 검증 세트로 사용하겠습니다. x_val = x_train[:1000] partial_x_train = x_train[1000:] y_val = one_hot_train_labels[:1000] partial_y_train = one_hot_train_labels[1000:] 이제 20번의 에포크로 모델을 훈련시킵니다. history = model.fit(partial_x_train, partial_y_train, epochs=20, batch_size=512, validation_data=(x_val, y_val)) 마지막으로 손실과 정확도 곡선을 그립니다(그림 3-9와 그림 3-10 참고). import matplotlib.pyplot as plt 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() 그림 3-9 훈련과 검증 손실 plt.clf() 그래프를 초기화합니다. acc = history.history['acc'] val_acc = history.history['val_acc'] 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() 그림 3-10 훈련과 검증 정확도 이 모델은 아홉 번째 에포크 이후에 과대적합이 시작됩니다. 아홉 번의 에포크로 새로운 모델을 훈련하고 테스트 세트에서 평가하겠습니다. model = models.Sequential() model.add(layers.Dense(64, activation='relu', input_shape=(10000,))) model.add(layers.Dense(64, activation='relu')) model.add(layers.Dense(46, activation='softmax')) model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy']) model.fit(partial_x_train, partial_y_train, epochs=9, batch_size=512, validation_data=(x_val, y_val)) results = model.evaluate(x_test, one_hot_test_labels) 최종 결과는 다음과 같습니다. >>> results [1.022498257544459, 0.7756010686194165] 대략 78%의 정확도를 달성했습니다. 균형 잡힌 이진 분류 문제에서 완전히 무작위로 분류하면 50%의 정확도를 달성합니다. 이 문제는 불균형한 데이터셋을 사용하므로 무작위로 분류하면 18% 정도를 달성합니다. 여기에 비하면 이 결과는 꽤 좋은 편입니다. >>> import copy >>> test_labels_copy = copy.copy(test_labels) >>> np.random.shuffle(test_labels_copy) >>> hits_array = np.array(test_labels) = = np.array(test_labels_copy) >>> float(np.sum(hits_array)) / len(test_labels) 0.182546749777382 3.5.5 새로운 데이터에 대해 예측하기모델 인스턴스의 predict 메서드는 46개의 토픽에 대한 확률 분포를 반환합니다. 테스트 데이터 전체에 대한 토픽을 예측해 보겠습니다. predictions = model.predict(x_test) predictions의 각 항목은 길이가 46인 벡터입니다. >>> predictions[0].shape (46,) 이 벡터의 원소 합은 1입니다. >>> np.sum(predictions[0])
1.0 가장 큰 값이 예측 클래스가 됩니다. 즉 가장 확률이 높은 클래스입니다. >>> np.argmax(predictions[0])
3 3.5.6 레이블과 손실을 다루는 다른 방법앞서 언급한 것처럼 레이블을 인코딩하는 다른 방법은 다음과 같이 정수 텐서로 변환하는 것입니다.36 y_train = np.array(train_labels) y_test = np.array(test_labels) 이 방식을 사용하려면 손실 함수 하나만 바꾸면 됩니다. 코드3 –21에 사용된 손실 함수 categorical_crossentropy는 레이블이 범주형 인코딩되어 있을 것이라고 기대합니다. 정수 레이블을 사용할 때는 sparse_categorical_crossentropy를 사용해야 합니다. model.compile(optimizer='rmsprop', loss='sparse_categorical_crossentropy', metrics=['acc']) 이 손실 함수는 인터페이스만 다를 뿐이고 수학적으로는 categorical_crossentropy와 동일합니다. 3.5.7 충분히 큰 중간층을 두어야 하는 이유앞서 언급한 것처럼 마지막 출력이 46차원이기 때문에 중간층의 히든 유닛이 46개보다 많이 적어서는 안 됩니다. 46차원보다 훨씬 작은 중간층(예를 들어 4차원)을 두면 정보의 병목이 어떻게 나타나는지 확인해 보겠습니다. model = models.Sequential() model.add(layers.Dense(64, activation='relu', input_shape=(10000,))) model.add(layers.Dense(4, activation='relu')) model.add(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)) 검증 정확도의 최고 값은 약 71%로 8% 정도 감소되었습니다. 이런 손실의 원인 대부분은 많은 정보(클래스 46개의 분할 초평면을 복원하기에 충분한 정보)를 중간층의 저차원 표현 공간으로 압축하려고 했기 때문입니다. 이 네트워크는 필요한 정보 대부분을 4차원 표현 안에 구겨 넣었지만 전부는 넣지 못했습니다. 3.5.8 추가 실험
3.5.9 정리다음은 이 예제에서 배운 것들입니다.
32 역주 이 데이터셋은 원본 로이터 데이터셋(Reuters-21578, https://bit.ly/2JPwSa0)의 135개 토픽 중에서 샘플이 많은 것을 뽑아 간단하게 만든 것입니다. 이 데이터셋의 토픽은 금융과 관련된 카테고리입니다. 3.4 영화 리뷰 분류: 이진 분류 예제 | 목차 | 3.6 주택 가격 예측: 회귀 문제 이 글은 도서출판 길벗에서 출간한 “케라스 창시자에게 배우는 딥러닝“의 1장~3장입니다. 이 책의 저작권은 (주)도서출판 길벗에 있으므로 무단 복제 및 무단 전제를 금합니다. |