딥러닝으로 보스턴의 집값을 예측해보자.

선형 회귀 문제 : Linear Regression

머신러닝으로 해결할 수 있는 문제 중 분류 문제들을 이전 포스팅에서 다뤄보았고,
이번에는 연속된 데이터를 예측해야하는 회귀 문제를 보겠습니다.

이전의 포스팅은 데이터가 주어졌을 때 리뷰의 긍정/부정 분류, 뉴스 기사의 토픽 분류 등의 문제를 다뤘었고,
회귀 문제란 데이터가 주어졌을 때 주택 가격을 예측하는것처럼 연속된 값을 예측하는걸 회귀 문제라고 합니다.

오늘은 선형 회귀 모델을 이용하여 1970년대 보스턴의 집값을 예측하는 모델을 만들어 보겠습니다.

Dataset

사용하는 데이터셋은 카네기 멜런 대학교에서 제공하는 데이터셋으로 데이터의 설명은 여기에서 확인할 수 있습니다.

Download

1
2
from tensorflow.keras.datasets import boston_housing
(train_data, train_targets), (test_data, test_targets) = boston_housing.load_data()

Dataset shape 확인

1
2
print("Train Datas Shape : {}".format(train_data.shape))
print("Train Labels Shape : {}".format(train_targets.shape))
Train Datas Shape : (404, 13)
Train Labels Shape : (404,)

데이터 확인

각 컬럼별로 해당하는 데이터들을 확인할 수 있고, 레이블에는 해당 주택의 가격값이 들어가있는걸 확인할 수 있습니다.

1
2
display(train_data[0])
display(train_targets[0:10])
array([  1.23247,   0.     ,   8.14   ,   0.     ,   0.538  ,   6.142  ,
        91.7    ,   3.9769 ,   4.     , 307.     ,  21.     , 396.9    ,
        18.72   ])



array([15.2, 42.3, 50. , 21.1, 17.7, 18.5, 11.3, 15.6, 15.6, 14.4])

데이터 변환

신경망 학습 시 차이가 큰 값을 신경망에 주입하면 학습이 잘 되지 않습니다.
(0.01, 0.1)과 (1, 10)을 보면 비율대로만 보게되면 10배의 차이를 가지는걸 볼 수 있지만,
실수 체계에서 값의 차이는 어마어마하게 큰 차이로 볼 수 있습니다.
(1, 10)과 같은 데이터를 Sparse 하다라고 표현하며,
(0.01, 0.1)과 같은 밀도가 높은 데이터를 Dense 데이터라고 표현합니다.

이렇게 Dense 데이터로 변경하는 과정이 필요한데, 이 부분을 Scaling 혹은 일반화 한다고 합니다.

데이터의 일반화를 위해서 각 특성별 평균값을 뺀 이후에 표준편차로 나눠주게 되면,
특성의 중앙이 0으로, 표준편차는 1인 정규분포가 됩니다.

1
2
3
4
5
6
7
mean = train_data.mean(axis=0)
train_data -= mean
std = train_data.std(axis=0)
train_data /= std

test_data -= mean
test_data /= std

모델 구성

데이터셋이 작으므로 간단한 모델을 구성하면 과대적합을 피할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
from tensorflow.keras import models
from tensorflow.keras import layers

def build_network(input_shape=(0,)):
model = models.Sequential()
model.add(layers.Dense(64, activation='relu', input_shape=input_shape))
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(1))
model.compile(optimizer='adam', loss='mse', metrics=['mae'])
return model

모델 구성의 마지막 Dense layer를 보면 활성함수가 지정 되어있지 않은걸 확인할 수 있는데,
우리가 필요한 데이터는 선형 데이터를 원하기 때문에 활성함수를 지정하지 않으면 스칼라 회귀를 위한 구성이 완성됩니다.
반대로 sigmoid 같은 활성화 함수를 적용하면 네트워크가 0~1 사이의 값을 만들도록 만듭니다.

우리가 필요한 데이터는 주택가격을 예측하기 위함으로 활성함수를 지정하지 않고 스칼라 회귀 값을 예측하도록 구성합니다.

K-Fold Validation

데이터셋의 크기가 매우 작기 때문에 검증셋(Validation)의 크기도 매우 작아지게 되는데,
이때 데이터셋이 적어지면서 훈련세트 중 어느 한 특정 부분이 훈련세트로 사용되는지에 따라 모델의 정확도가 크게 달라질 수 있습니다.
이 이유는 훈련세트와 검증세트로 나눴을 때 각 값들의 분포도가 고르게 되지 못할 경우가 생기기 때문입니다.

이런 상황에서 가장 좋은 방법은 K-겹 교차 검증(K-Fold Cross Validation)을 실시하는것인데,
데이터를 K개의 분할로 나누고 각각 K개의 모델을 만들어 K-1 개의 분할에서 훈련하고 나머지 분할된 데이터에서 평가를 하게됩니다.
이 점수는 각 검증 데이터셋의 평균으로 모델을 평가하게 됩니다.

즉, 여러 폴드의 데이터셋으로 나누어 교차검증을 함으로써 데이터 분포에 신경쓰지 않고 훈련세트의 모든 부분을 사용해 모델을 평가할 수 있는 장점이 있습니다.

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
34
35
36
37
import numpy as np

k = 4
num_val_samples = len(train_data) // k
num_epochs = 150
all_scores = []
all_history = []
for i in range(k):
print('폴드 번호 #{}'.format(i))
fold_start_index = i * num_val_samples
fold_end_index = (i + 1) * num_val_samples

val_data = train_data[fold_start_index : fold_end_index]
val_targets = train_targets[fold_start_index : fold_end_index]

partial_train_data = np.concatenate(
[train_data[:fold_start_index], train_data[fold_end_index:]],
axis=0
)

partial_train_targets = np.concatenate(
[train_targets[:fold_start_index], train_targets[fold_end_index:]],
axis=0
)

model = build_network((partial_train_data.shape[1], ))
history = model.fit(
partial_train_data,
partial_train_targets,
epochs=num_epochs,
validation_data=(val_data, val_targets),
batch_size=1,
verbose=0
)
val_mse, val_mae = model.evaluate(val_data, val_targets, verbose=0)
all_scores.append(val_mse)
all_history.append(history.history)
폴드 번호 #0
폴드 번호 #1
폴드 번호 #2
폴드 번호 #3
1
2
3
4
5
val_mae_lst = map(lambda x: x['val_mean_absolute_error'], all_history)
val_mae_lst = np.array(list(val_mae_lst))
avg_mae = [
np.mean([x[i] for x in val_mae_lst]) for i in range(num_epochs)
]

검증 데이터 시각화

검증 데이터 시각화를 위해 history 데이터를 에폭별 평균 데이터로 치환시킵니다.

1
2
3
4
5
val_mae_lst = map(lambda x: x['val_mean_absolute_error'], all_history)
val_mae_lst = np.array(list(val_mae_lst))
avg_mae = [
np.mean([x[i] for x in val_mae_lst]) for i in range(num_epochs)
]

그래프 변동이 심하지 않도록 이전 포인트의 지수 이동 평균값으로 변경시켜주는 함수를 만듭니다.

1
2
3
4
5
6
7
8
9
10
import matplotlib.pyplot as plt
def smooth_curve(points, factor=.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
1
2
3
4
5
6
def show_graph(data):
smooth_data = smooth_curve(data)
plt.plot(range(1, len(smooth_data) + 1), smooth_data)
plt.xlabel('Epochs')
plt.ylabel('Validation MAE')
plt.show()
1
show_graph(avg_mae[10:])

png

Powered by Hexo and Hexo-theme-hiker

Copyright © 2019 - 2019 코딩크루 All Rights Reserved.

UV : | PV :