신경망의 기초 활용에 대해 알아보자
벌써 3번째지만 중요한 신경망의 구조다.
이번에는 각각의 문제를 해결하기 위해 적합한 신경망의 기초 활용에 대해 알아보겠다.
- 이진 분류
이진 분류는 쉽게 말해 0과 1, 참과 거짓, 긍정과 부정 등으로 이분법 가능한 경우에 쓰이는 기법이다.
책(케라스 창시자에게 배우는 딥러닝)에서 나오는 대표 예제는 IMDB(Internet Movie DataBase)를
활용한 영화 평가에 대한 예제이다. 이 데이터 셋 또한 keras.datasets에 있다. 이 예제로 알아보자.
아래 코드는 그를 활용하여 전처리 하는 과정이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
from keras.datasets import imdb import numpy as np (train_data, train_labels), (test_data, test_labels) = imdb.load_data(num_words=10000) def vectorize_sequences(sequences, dimension=10000): # 크기가 (len(sequences), dimension))이고 모든 원소가 0인 행렬을 만듭니다 results = np.zeros((len(sequences), dimension)) for i, sequence in enumerate(sequences): # enumerate는 맨 앞 배열에 순서를 알고 싶을 때 쓴다 (0번째 sequence, 1번째 sequence) results[i, sequence] = 1. # results[i]에서 특정 인덱스의 위치를 1로 만듭니다 return results ''' 신경망에 숫자 리스트를 주입할 수 없으므로 가능한 형태로 변환한다 여기서는 단어 인덱스 범위가 10000이므로 0-10000 사이의 단어들이 있는지 없는지 판단 할 수 있는 배열로 만든다 예를들어, sequence 중에 4번과 18번이 있다면 그 인덱스는 1.0이 된다. ''' x_train = vectorize_sequences(train_data) x_test = vectorize_sequences(test_data) print(x_train[0]) # [0. 1. 1. ... 0. 0. 0.] # 입력 데이터는 소수점이며 numpy의 배열이어야 하는 것 같다 y_train = np.asarray(train_labels).astype('float32') y_test = np.asarray(test_labels).astype('float32') print(y_train) # [1. 0. 0. ... 0. 1. 0.] print(type(y_train)) # <class 'numpy.ndarray'> |
먼저 imdb 데이터 셋을 불러오는데 가장 많이 사용되는 단어 10000개 이하로만 불러온다. (num_words=10000)
무슨 뜻 이냐 하면,
imdb의 train_data 는 25000개이며 하나당 여러개의 정수 배열로 되어 있는 이차원 배열(벡터) 인데
여러개의 정수 배열 중에 10000이상이 넘는 요소는 빼고 불러온다는 얘기다
각 정수는 단어 하나를 뜻하며 imdb에 빈도 순으로 dictionary 형태로 되어 있다
예를들어 {5: “the”, …. 12503:”approach”, ….} 이런식의 dictionary 가 있다면
숫자 5는 “the”를 가리키며 10000 안에 있으므로 정수 배열에 그대로 담겨온다.
하지만 12503는 “approach”는 10000 이상 이므로 포함되지 않을 것이다.
이렇게 자주 사용하는 단어들만 불러온 2차원 배열을 가지고 또 한번 전처리를 해 준다.
하나의 영화 리뷰당 자주 사용하는 단어들이 있는지 (1) 없는지 (0) 여부만 표시하도록 바꾸는 것이다.
10000개의 자주사용하는 단어 안에서 가져 왔으므로 10000개의 공간이 필요하다
즉, 25000개 train_data 하나당 10000개의 공간을 가지게 되는 것이다.
그 전에는 (25000, ) 이었다면 전처리 후에는 (25000, 10000) 의 확실한 이차원 배열이 된다.
그리고 어떤 영화 리뷰가 긍정인지 아닌지에 대한 해답인 train_label 또한 소수점(float32) 형태로 변환해 준다.
이제 신경망에 주입하기 위한 데이터 전처리가 끝났다.
아래는 신경망에 데이터를 주입하고 훈련하는 과정이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
from keras import models from keras import layers model = models.Sequential() model.add(layers.Dense(16, activation='relu', input_shape=(10000,))) model.add(layers.Dense(16, activation='relu')) model.add(layers.Dense(1, activation='sigmoid')) from keras import losses from keras import metrics from keras import optimizers model.compile(optimizer=optimizers.RMSprop(lr=0.001), loss=losses.binary_crossentropy, metrics=[metrics.binary_accuracy]) x_val = x_train[:10000] # 0 - 9999 partial_x_train = x_train[10000:] # 10000 - 24999 y_val = y_train[:10000] partial_y_train = y_train[10000:] history = model.fit(partial_x_train, partial_y_train, epochs=20, batch_size=512, validation_data=(x_val, y_val)) |
여기서는 훈련용 데이터에서 10000개를 떼어내 검증용 데이터로 활용하고
나머지 15000개 데이터만 훈련용 데이터로 사용 하였다.
이번에는 이것을 시각화 해보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
history_dict = history.history print(history_dict.keys()) # dict_keys(['val_loss', 'val_binary_accuracy', 'loss', 'binary_accuracy']) import matplotlib.pyplot as plt acc = history.history['binary_accuracy'] val_acc = history.history['val_binary_accuracy'] loss = history.history['loss'] val_loss = history.history['val_loss'] epochs = range(1, len(acc) + 1) # 1, 2, 3 ... (len(acc) + 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_dict['binary_accuracy'] val_acc = history_dict['val_binary_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() |
먼저그래프를 보면 Training loss (점) 와 Validation loss (선) 추이가 다름을 알 수 있다.
20번 (epochs)의 반복 하는 동안 훈련 데이터 15000개 로 하는 훈련(Training loss)은 거듭할 수록 손실률이 떨어진다.
그런데 검증용 데이터(Validation loss) 10000개로 하는 검증은 오히려 어느지점부터 손실률이 올라가고 있다.
이 그래프에서도 Training acc (점) 와 Validation acc (선) 추이가 다름을 알 수 있다.
훈련 데이터로 하는 훈련은 거듭될 수록 정확도가 높아지는 반면
검즘용 데이터로 하는 검증은 서서히 정확도가 떨어진다.
정리하면, 훈련데이터는 점점 손실률이 떨어지고 정확도가 올라가는데
검증데이터는 조금 더 정확해 지다가 오히려 정확도가 떨어지고 손실률이 많아진다.
이는 훈련 데이터에 과도하게 최적화되어 일반 데이터에는 적합하지 않게 되기 때문이다.
이를 과대 적합(Overfitting)이라 한다.
과대 적합을 완화하는 다양한 기술이 있지만, (다음에)
일단 간단하게는 검증 데이터의 정확도가 오르다가 떨어지는 지점까지만 하는 것이다.
아래를 보면 더이상 나빠지지 않도록 4번만 반복하는 과정이 나타나 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
model = models.Sequential() model.add(layers.Dense(16, activation='relu', input_shape=(10000,))) model.add(layers.Dense(16, activation='relu')) model.add(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) results = model.evaluate(x_test, y_test) print(results) # [0.32377878576278685, 0.8736] print(model.predict(x_test)) ''' [[0.1398218 ] [0.9997168 ] [0.2917408 ] ... [0.07110936] [0.04260657] [0.47290033]] ''' |
원점으로 돌아와 신경망 구성에 대한 것을 보면,
이진 분류 문제에서 네트워크(층)는 마지막 층에서 하나의 유닛과 sigmoid 활성화 함수를 가지는 것이 좋다.
sigmoid 활성화 함수는 임의의 값을 0과 1사이로 압축하므로 확률를 출력하기 위해 주로 사용한다.
그에 이어지는 손실 함수는 binary_crossentropy 를 보통 이용한다고 한다.
옵티마이저 rmsprop는 굳이 이진분류 문제 뿐만 아니라 다른 문제에서 보편적으로 사용해도 좋다고 한다.
2. 다중 분류
이전에는 0과 1로만 해답(Label)을 제시 했다면 다중 분류는 그 이상으로 제시할 수 있다.
이번 예제는 뉴스 기사 토픽 분류로 각 기사는 총 46개의 토픽으로 분류가 된다.
목적은 각각의 뉴스 기사가 훈련을 통해 높은 정확도로 알맞은 토픽으로 분류되는 것이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
from keras.datasets import reuters (train_data, train_labels), (test_data, test_labels) = reuters.load_data(num_words=10000) # [{단어 : 정수}, ... ] 로 이루어진 Dictionary를 불러와서 {단어 : 정수}를 {정수 : 단어}로 바꾼다 word_index = reuters.get_word_index() reverse_word_index = dict([(value, key) for (key, value) in word_index.items()]) # 0, 1, 2는 '패딩', '문서 시작', '사전에 없음'을 위한 인덱스이므로 3을 뺍니다 decoded_newswire = ' '.join([reverse_word_index.get(i - 3, '?') for i in train_data[3]]) ''' ? the farmers home administration the u s agriculture department's farm lending arm could lose about seven billion dlrs in outstanding principal on its severely ? borrowers or about one fourth of its farm loan portfolio the general accounting office gao said in remarks prepared for delivery to the senate agriculture committee brian crowley senior associate director of gao also said that a preliminary analysis of proposed changes in ? financial eligibility standards indicated as many as one half of ? borrowers who received new loans from the agency in 1986 would be ? under the proposed system the agency has proposed evaluating ? credit using a variety of financial ratios instead of relying solely on ? ability senate agriculture committee chairman patrick leahy d vt ? the proposed eligibility changes telling ? administrator ? clark at a hearing thatthey would mark a dramatic shift in the agency's purpose away from being farmers' lender of last resort toward becoming a big city bank but clark defended the new regulations saying the agency had a responsibility to ? its 70 billion dlr loan portfolio in a ? yet ? manner crowley of gao ? ? arm said the proposed credit ? system attempted to ensure that ? would make loans only to borrowers who had a reasonable change of repaying their debt reuter 3 ''' |
이번에도 정수로 이루어진 각각의 뉴스 기사를 범위 10000개로 제한해서 불러온다.
그 내용을 살짝 살펴보려면 위와 같이 단어 인덱스 dictionary 를 불러와서 각각의 정수를 단어와 매칭시키면 된다.
그러면 위와 같은 내용으로 나타나는데 단어 10000개 제한이므로 빠진 단어들이 있을 수도 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import numpy as np # [0... 9999] 배열을 만들어 해당하는 정수가 있으면 1로 만든다 (다중선택) 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) # [0.. 45] 배열을 만들어 그 중에 해당하는 한 개를 1로 한다 (단일선택) from keras.utils.np_utils import to_categorical one_hot_train_labels = to_categorical(train_labels) one_hot_test_labels = to_categorical(test_labels) # 만약 다른 방식으로 Label을 처리하기 원한다면, # 정수 배열로 만든 다음, sparse_categorical_crossentropy를 이용한다. # 그러면 기존대로 스칼라 값으로 처리가 가능하다. y_train = np.array(train_labels) y_test = np.array(test_labels) # model.compile(optimizer='rmsprop', loss='sparse_categorical_crossentropy', metrics=['acc']) |
전처리는 이진분류와 비슷하다. 다만 Labels을 전처리 할 때는 기존과는 달리 벡터(1차원 배열)를 만들어
그에 해당하는 하나만 1로 만들고 나머지는 0으로 한다. to_categorical() 함수가 그 역할을 한다.
그런데 만약 다른 방식으로 Label을 처리하고 싶으면, 기존 정수 배열로 두고 (원래는 0 .. 45 의 스칼라 값을 가진다)
아래 모델 구성에서 손실함수만 categorical_crossentropy에서 sparse_categorical_crossentropy를 이용하면 된다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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')) model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy']) 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:] history = model.fit(partial_x_train, partial_y_train, epochs=20, batch_size=512, validation_data=(x_val, y_val)) |
이번에는 샘플 개수가 더 적으므로 1000개의 검증 세트와 약 8000개의 훈련 세트로 구성한다.
또한 46개의 분류 이므로 중간 층의 히든 유닛이 46개 이상이 좋다.
마지막 층은 이진 분류 때와 달리 46개의 히든 유닛을 둔다.
활성화 함수는 N개의 클래스에 대한 확률 분포를 출력하기 위해 softmax 함수를 사용한다.
그리고 돌려보면 결과는 이진분류 때와 비슷하게 나타난다.
그래프에서 보듯이 역시 과대 적합이 9번째 정도의 시도부터 나타나고 있다.
3. 회귀
이진 분류, 다중 분류와 같이 종류가 정확하게 정해져 있는 분류 문제와는 달리,
회귀(Regression)는 종류가 정해져 있지 않은 연속적인 값을 구할 때 사용하는 방법이다.
보스턴의 주택 가격을 예측하는 데이터셋을 이용해 알아보기로 하자.
각 각의 값은 보스턴 외곽지역의 범죄율, 지방세율 등 13가지 항목에 대한 값이 주어져 있고
해당 지역의 주택 중간 가격이 Label(해답)으로 주어 진다.
1 2 3 4 5 6 7 8 9 10 11 |
from keras.datasets import boston_housing (train_data, train_targets), (test_data, test_targets) = boston_housing.load_data() mean = train_data.mean(axis=0) train_data -= mean std = train_data.std(axis=0) train_data /= std test_data -= mean test_data /= std |
문제는 이전과 달리 13가지 특성마다 값의 범위가 다 다르고(0과 10 사이, 0과 1000 사이 등)
이대로 신경망에 주입하면 좋은 결과를 얻기 힘들다는 것이다.
네트워크는 상이한 스케일에 맞추려 하지만 학습을 어렵하기 때문에
대표적인 방법으로 특성별로 정규화를 해 주는 것이다.
13가지 특성에 대한 평균을 구하고 그 평균값을 빼준 후에
각 값을 제곱하여 더하고 배열 길이로 나눠준 후 (분산) 루트√를 씌워 표준편차를 구한다.
그 표준편차로 다시 13가지 특성에서 평균값을 빼준 값을 나눈다.
test_data 또한 같은 과정을 거치는데 주의할 점은 이미 구한 훈련데이터의 평균과
표준편차를 이용하여야 한다는 것이다.
(평균이 m이고, 표준편차가 3이라고 할때, 실제 값은 m+-3)
1 2 3 4 5 6 7 8 9 10 11 12 |
from keras import models from keras import layers def build_model(): # 동일한 모델을 여러 번 생성할 것이므로 함수를 만들어 사용합니다 model = models.Sequential() model.add(layers.Dense(64, activation='relu', input_shape=(train_data.shape[1],))) model.add(layers.Dense(64, activation='relu')) model.add(layers.Dense(1)) model.compile(optimizer='rmsprop', loss='mse', metrics=['mae']) return model |
샘플 개수가 적을 수록 과대적합이 쉽게 나타나므로 작은 층 모델을 사용하는 것이 좋다.
마지막 층은 하나의 은닉 유닛만을 가지고 있고 활성화 함수가 없는데 이는
스칼라 회귀(하나의 연속적인 값을 예측) 를 위한 구성이다.
손실 함수는 평균 제곱 오차 mse(mean squared error)를 이용하며
이는 예측값과 실제 값 사이 거리의 제곱을 의미 한다.
또한 모니터링을 위해 평균 절대 오차 mae(mean absolute error)를 이용하며
이는 예측값과 실제값 사이 거리의 절대값을 의미 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
k = 4 # len(train_data), 404 num_val_samples = len(train_data) // k # 101 num_epochs = 100 all_scores = [] for i in range(k): print('처리중인 폴드 #', i) # 검증 데이터 준비: k번째 분할 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() # 모델 훈련(verbose=0 이므로 훈련 과정이 출력되지 않습니다) model.fit(partial_train_data, partial_train_targets, epochs=num_epochs, batch_size=1, verbose=0) # 검증 세트로 모델 평가 val_mse, val_mae = model.evaluate(val_data, val_targets, verbose=0) all_scores.append(val_mae) print(all_scores) # [2.048123823534144, 2.2299161405846624, 2.875645656396847, 2.33851559386395] print(np.mean(all_scores)) # 2.3730503035949004 |
샘플 수가 (404 개) 적으므로 그대로 실행한다면 신뢰 있는 모델 평가를 할 수 없다.
따라서 K 겹 교차 검증으로 검증 데이터와 훈련 데이터를 나눠서 여러번 실행한다.
검증 | 훈련 | 훈련 | 훈련 |
훈련 | 검증 | 훈련 | 훈련 |
훈련 | 훈련 | 검증 | 훈련 |
훈련 | 훈련 | 훈련 | 검증 |
원래 대로라면 맨 위에 처럼 맨 앞에 검증 데이터 조각 하나 나머지 훈련 데이터 형식으로만 진행한다.
K-겹 교차 검증은 맨 위 뿐만 아니라 검증 데이터를 K 조각으로 나누고 K 만큼 검증 데이터 영역을 옮겨 가며
훈련을 진행 시킨다. 그리고 각각의 검증 데이터의 mae를 구하고 평균값을 출력한다.
여기서는 mae 평균값이 2.373이고 천 달러 단위므로 2373 달러 쯤 평균적으로 예측에 오차가 있다는 뜻이 된다.
이번에는 훈련 반복수를 500으로 늘려서 검증데이터의 mae의 평균을 구해 보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
num_epochs = 500 all_mae_histories = [] for i in range(k): print('처리중인 폴드 #', i) # 검증 데이터 준비: k번째 분할 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() # 모델 훈련(verbose=0 이므로 훈련 과정이 출력되지 않습니다) history = model.fit(partial_train_data, partial_train_targets, validation_data=(val_data, val_targets), epochs=num_epochs, batch_size=1, verbose=0) mae_history = history.history['val_mean_absolute_error'] all_mae_histories.append(mae_history) average_mae_history = [np.mean([x[i] for x in all_mae_histories]) for i in range(num_epochs)] |
4겹이므로 한번 실행마다 500번 반복하고 결국 2000번을 훈련하게 된다.
0번대의 4개의 mae 평균, 1번대의 4개의 mae 평균 .. 499번대의 4개의 mae 평균 이렇게 500개 mae를 구한다
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import matplotlib.pyplot as plt plt.plot(range(1, len(average_mae_history) + 1), average_mae_history) plt.xlabel('Epochs') plt.ylabel('Validation MAE') plt.show() def smooth_curve(points, factor=0.9): smoothed_points = [] for point in points: if smoothed_points: previous = smoothed_points[-1] smoothed_points.append(previous * factor + point * (1 - factor)) else: smoothed_points.append(point) return smoothed_points smooth_mae_history = smooth_curve(average_mae_history[10:]) plt.plot(range(1, len(smooth_mae_history) + 1), smooth_mae_history) plt.xlabel('Epochs') plt.ylabel('Validation MAE') plt.show() |
각 각의 epoch 당 검증용 mae 평균을 표시하면 그림과 같이 그래프가 나타나게 된다.
들쭉날쭉 보기 안 좋으므로 처음 10개를 빼고 이전 점의 90%와
그 다음 점의 10%를 합쳐 새로운 점을 만들어 넣으면 아래 그래프가 된다.
그래프를 보면 80번까지는 mae가 줄었지만 그 이후부터는 과대적합이 시작된다.
앞에 3가지 예처럼 딥러닝 구조를 잘 아는 것도 중요하지만 진짜 실력은,
알맞은 데이터 전처리, 적절한 모델 구성, 과대 적합 해결 인 것 같다.