Study/MachineLearning

Deep Learning with Python] 2. 신경망 시작하기

soohwan_justin 2021. 9. 12. 11:12

이번 포스트는 케라스 창시자에게 배우는 딥러닝(Deep Learning with Python)의 ch.3을 참고하였습니다.

 

이번 포스트를 읽기 전에, [Do it!] 시리즈를 먼저 읽는것을 권장합니다. 이 포스트에서는 [Do it!] 시리즈에 중복해서 나온 내용들을 많이 생략하였습니다.

 

이 책은 딥 러닝의 입문서로서는 적절하지 않다고 생각됩니다. 딥 러닝에 대한 어느정도 지식이 있고, 케라스의 사용방법에 대해 좀 더 자세히 알고 싶은 분께 권장합니다.

 

 

 

 

신경망의 구조

 

 

층 : 딥 러닝의 구성 단위

 

층(layer)은 하나 이상의 텐서를 입력으로 받아서 하나 이상의 텐서를 출력하는 데이터 처리 모듈입니다. 상태가 없는 층의 종류도 있지만, 대부분의 경우 가중치라는 층의 상태를 갖습니다.

 

적절한 텐서 포맷과 데이터 처리 방식은 층마다 다릅니다. 예를 들어, (samples, features)의 2D 텐서는 완전 연결 층에 의해 처리되는 경우가 많습니다. (samples, timesteps, features)의 3D 텐서는 보통 LSTM같은 순환 층(recurrent layer)에 의해 처리되고, 4D 텐서로 저장된 이미지는 일반적으로 2D 합성곱 층(convolution layer)에 의해 처리됩니다.

 

 

모델 : 층의 네트워크

 

딥러닝 모델은 층으로 쌓은 비순환 유향 그래프(Directed Acyclic Graph, DAG)입니다. 가장 일반적인 예시는 하나의 입력을 하나로 출력으로 맵핑하는 층을 순서대로 쌓는 것입니다.

 

네트워크 구조는 가설 공간(hypothesis space)를 정의합니다. 우리는 네트워크 구조를 선택함으로써 가능성이 있는 공간(가설 공간)을 입력 데이터에서 출력 데이터로 매핑하는 특정 텐서 연산으로 제한하게 됩니다. 딥러닝에서 우리가 구하고자 하는 것은 이런 텐서 연산에 포함된, 적절한 값을 가진 가중치 텐서입니다.

 

 

손실 함수와 옵티마이저 : 학습 과정을 조절하는 열쇠

 

네트워크 구조를 정의하고 나서, 우리는 두 가지를 더 선택해야 합니다.

 

- 손실 함수(목적 함수)

- 옵티마이저

 

여러 개의 출력을 내는 신경망은 출력 당 하나씩, 여러 개의 손실 함수를 가질 수 있습니다. 하지만 경사 하강법은 하나의 스칼라 손실 값을 기준으로 하기 때문에, 손실이 여러 개인 네트워크에서는 모든 손실의 평균을 낸 하나의 스칼라값을 사용합니다.

 

문제에 맞는 목적 함수를 찾는 것은 매우 중요합니다. 이전에 많이 봤던 네트워크의 목표는, 단순히 손실 함수의  값을 최소화 하는 것이었습니다. 예를 들어, 만약 SGD로 트레이닝된 모델이 있다고 가정하겠습니다. 이 모델의 목적 함수는 "모든 인류의 평균 행복 지수를 최대화하기" 입니다. 그러나 이 훈련된 모델은 우리가 생각하는 것과 다른 방향으로 훈련이 되어있을 수 있습니다. 예를 들면 현재 불행한 사람을 죽여서 남은 사람들의 행복에 초점을 맞추는 방향으로 훈련이 되어있을 수도 있다는 것입니다. 따라서 목적 함수를 현명하게 선택하지 않으면 원하지 않는 효과가 발생할 수 있습니다.

 

 

 

 

이진 분류 : 영화 리뷰 분류하기

 

이번에는 양극단의 리뷰 5만개로 이루어진 IMDB데이터셋을 사용합니다. 이 데이터셋에는 2만5천개의 긍정 리뷰가 있고, 2만5천개의 부정 리뷰가 있으며, 절반은 훈련 데이터이고 절반은 테스트 데이터입니다.

 

이번 예시로 구현해볼 것은 벡터 입력을 받아 이진 분류를 하는 신경망입니다.

 

 

1. 데이터셋 로드하기

from keras.datasets import imdb
 
(train_data, train_labels), (test_data, test_labels) = imdb.load_data(num_words=10000)

 

가장 많이 사용하는 단어 1만개를 사용합니다. 이렇게 불러온 데이터 중 하나를 디코딩하여 확인해보면, 다음과 같습니다.

word_index = imdb.get_word_index()
reverse_word_index = dict([(value, key) for (key, value) in word_index.items()])
decoded_review = ' '.join([reverse_word_index.get(i-3, '?') for i in train_data[0]])
decoded_review

line 3에서 3을 빼는 이유는 인덱스 0,1,2는 단어가 아니기 때문입니다. 0은 '패딩', 1은 '문서 시작', 2는 '사전에 없음'이므로, 이 값을 빼고 디코딩해야합니다.

 

 

2. 데이터 전처리

 

신경망에 숫자 리스트를 주입할 수는 없으므로, 리스트를 텐서로 바꿉니다. 단어를 임베딩 하거나, 원-핫 인코딩을 합니다. 이번 예시에서는 원-핫 인코딩을 사용합니다. 다만, 이번에 사용할 원-핫 인코딩은 순환 신경망을 사용할 것이 아니므로 약간 다른데, 해당 리뷰 안에 특정 단어가 있으면 그 인덱스를 1로 인코딩합니다. 다음과 같이 원-핫 인코딩을 합니다.

import numpy as np
 
def vectorize_sequence(sequences, dimension=10000):
    results = np.zeros((len(sequences), dimension))
    for i, sequence in enumerate(sequences):
        results[i, sequence] = 1
    return results
 
x_train = vectorize_sequence(train_data)
x_test = vectorize_sequence(test_data)
y_train = np.asarray(train_labels).astype('float32')
y_test = np.asarray(test_labels).astype('float32')

 

 

3. 신경망 모델 만들기

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'))
 
model.compile(optimizer='rmsprop',
              loss='binary_crossentropy',
              metrics=['accuracy'])

입력 데이터가 벡터 형태이고, 레이블이 스칼라인 경우에는 relu 함수를 활성화 함수로 하는 완전 연결층으로 구성된 네트워크가 잘 작동합니다.

 

하나의 은닉층에 유닛이 n개가 있다는 것은, 입력 데이터가 가중치 행렬과 곱해지면 n차원 공간에 투영된다는 의미입니다. 이 때, 이 공간의 차원을 '신경망이 내재된 표현을 학습할 때 가질 수 있는 자유도'라고 생각할 수 있습니다. 우리는 이를 모델 복잡도 라고도 합니다. 유닛을 늘리면 신경망이 더욱 복잡한 표현을 학습할 수 있지만, 계산 비용이 커지고, 원하지 않는 패턴을 학습하여 과대적합이 될 수도 있습니다.

 

Dense 층을 쌓을 때는 두 가지 중요한 구조상의 결정이 필요합니다.

 

- 얼마나 많은 층을 사용할 것인가?

- 각 층에 얼마나 많은 은닉 유닛을 둘 것인가?

 

이에 대한 자세한 내용은 나중에 설명합니다.

 

 

4. 검증 세트 준비하고, 훈련하기

x_val = x_train[:10000]
partial_x_train = x_train[10000:]
 
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))

 

5. 훈련 및 검증 손실 확인하기

 

손실 함수를 확인합니다.

import matplotlib.pyplot as plt
history_dict = history.history
loss = history_dict['loss']
val_loss = history_dict['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_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()

 

위 그래프를 보니, 네번째 에포크에서 과대적합이 시작되는 것 같으므로 훈련을 4번의 에포크동안 진행해보고 테스트 데이터를 사용하여 평가해보겠습니다.

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)

정확도는 약 87%입니다.

 

 

다중 분류 : 뉴스 기사 분류

 

이번에는 뉴스를 46개의 상호 배타적인 레이블로 분류하는 신경망을 구현해봅니다. 각 샘플이 정확히 하나로 구별되기 때문에 단일 레이블 다중 분류입니다. 만약 여러 레이블에 속할 수 있다면, 이는 다중 레이블 다중 분류 문제가 됩니다.

 

 

1. 데이터셋 확인하기

from keras.datasets import reuters
 
(train_data, train_labels), (test_data, test_labels) = reuters.load_data(num_words=10000)
 
print(len(train_data))
print(len(test_data))

 

확인 결과, 8982개의 훈련 샘플과 2246개의 테스트 샘플로 구성된 것을 알 수 있습니다.

 

 

2. 데이터 전처리 하기

from keras.utils.np_utils import to_categorical
 
x_train = vectorize_sequence(train_data)
x_test = vectorize_sequence(test_data)
 
one_hot_train_labels = to_categorical(train_labels)
one_hot_test_labels = to_categorical(test_labels)

vectorize_sequence는 이전에 영화 리뷰를 이진 분류하는 예시에서 사용했던 함수를 그대로 사용합니다.

 

그리고, 이번에는 케라스의 내장 함수를 사용하여 출력을 원-핫 인코딩 합니다.

 

 

3. 모델 구성하기

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'])

이번에 달라진 점은, 출력 클래스의 개수가 2개에서 46개로 늘었다는 점입니다. 또한, Dense 층을 쌓을 때, 유닛의 개수가16개에서 64개로 늘었습니다. 직관적으로 생각해봐도, 46개의 클래스를 구분하기에 16차원의 공간은 복잡도가 떨어질 것 같습니다.

 

Dense 층을 쌓으면 바로 이전 층의 출력에서 제공한 정보만 사용할 수 있습니다. 만약 어떤 층이 분류 문제에 필요한 일부 정보를 누락해버리면 다음 층에서는 이를 복원할 방법이 없습니다. 이렇게 중간에 정보가 줄어드는 것을 정보의 병목(information bottleneck)이라고 합니다. 이번 예시에서는 유닛을 16개만 사용하면 정보의 병목이 생길 수 있으므로, 충분히 큰 개수의 유닛을 사용합니다.

 

 

4. 훈련 검증하기

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

 

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

 

 

그래프를 보니 9번째 에포크부터 과대적합이 시작되는 것 같습니다. 따라서 9번의 에포크로 새로운 모델을 훈련해보고, 평가해봅니다.

 

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

새로 훈련한 모델의 정확도는 약 79%입니다.

 

 

5. 정보 병목 확인해보기

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))
 
results = model.evaluate(x_test, one_hot_test_labels)
print(results)

정확도가 약 70%로, 9%정도가 줄었습니다. 이런 손실의 대부분의 원인은 많은 정보를 저차원 표현 공간으로 압축하려 했기 때문입니다.

 

 

 

회귀 문제 : 주택 가격 예측

 

이번에는 1970년 중반 보스턴 외곽 지역의 범죄율, 지방세율 등의 데이터가 주어졌을 때 주택 가격의 중간값을 예측하는 문제입니다. 이번에는 506개의 데이터를 사용하며, 이중 404개는 훈련 샘플, 102개는 테스트 샘플입니다. 그리고 이 데이터셋의 각 특성은 스케일이 다릅니다.

 

1. 데이터 불러오고, 확인하기

 

타깃 데이터의 단위는 천 달러입니다.

from keras.datasets import boston_housing
 
(train_data, train_targets), (test_data, test_targets) = boston_housing.load_data()
 
print(train_data.shape)
print(test_data.shape)
print(train_targets)

2. 데이터 전처리하기

 

각 특성의 스케일이 다르므로, 데이터를 표준화 합니다. 테스트 세트를 표준화 할때도 훈련 세트에서의 평균과 표준 편차를 사용한다는 점에 주의하세요

mean = train_data.mean(axis=0)
train_data -= mean
std = train_data.std(axis=0)
train_data /= std
 
test_data -= mean
test_data /= std

 

3. 모델 구성하기

 

이번에는 모델을 여러번 만들면서 확인해보는 예시가 있기 때문에 모델을 만드는 함수를 정의하여 사용합니다.

from keras import models
from keras import layers
 
def build_model():
    model = model.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

선형 회귀이므로 마지막 출력층의 활성화 함수는 없습니다. 손실 함수는 평균 제곱 오차(mean squared error)를 사용하며, 훈련하는 동안 모니터링할 지표는 평균 절대 오차(mean absolute error)입니다. 단위는 천 달러이므로, 만약 mae가 0.5면 예측값이 평균 500달러 정도 차이가 난다는 의미입니다.

 

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

 

4. K-폴드 교차 검증 사용하여 검증하기

k = 4
num_val_samples = len(train_data) // k
num_epochs = 100
all_scores = []
 
for i in range(k):
    print('처리중인 폴드 : ', i)
    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()
    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)

line 11.에서의 np.concatenate 함수는 배열을 이어붙이는 함수입니다. 데이터의 중간에서 검증 세트를 뺀 훈련 세트를 만들기 위해 사용합니다.

 

line 22.에서 vebose=0으로 설정하면 훈련 과정이 출력되지 않습니다.

 

검증 점수를 확인해보겠습니다.

print(all_scores)
np.mean(all_scores)

훈련 때마다 검증 점수, 즉 손실 함수의 값이 달라지는데, 평균 값은 약 2.5로, 주택 가격의 범위가 약 1만~5만달러의 범위임을 생각하면 2500달러 정도의 오차는 꽤 큰 값입니다.

 

따라서 신경망을 좀 더 오래 훈련해보겠습니다. 그리고, 훈련하며 모델이 얼마나 개선되는지 확인하기 위해 검증 점수를 저장합니다.

num_epochs = 500
all_mae_histories = []
 
for i in range(k):
    print('처리중인 폴드 : ', i)
    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()
    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_mae']
    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)]
 
plt.plot(range(1, len(average_mae_history) + 1), average_mae_history)
plt.xlabel('Epochs')
plt.ylabel('Validation MAE')
plt.show()

 

위 그래프는 범위가 크고 변동이 심하기 때문에, 다른 부분과 스케일이 많이 다른 첫 10개의 데이터 포인트를 제외하고, 나머지 데이터에 지수 이동 평균을 사용하여 데이터를 그려봅니다.

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

 

위 그래프를 보니 약 70번째 에포크 이후 과대적합이 시작되는 것 같습니다. 따라서, 70번의 에포크에서 훈련을 종료해보고, 결과를 확인해보겠습니다.

model = build_model()
 
model.fit(train_data, train_targets,
          epochs=70, batch_size=16, verbose=0)
 
test_mse_score, test_mae_score = model.evaluate(test_data, test_targets)
 
test_mae_score

 

아직 2503달러 정도의 오차를 갖는 것을 확인할 수 있습니다.