안 쓰던 블로그

회귀 문제: 케라스로 주택 가격 예측하기 본문

머신러닝/머신러닝

회귀 문제: 케라스로 주택 가격 예측하기

proqk 2021. 4. 5. 19:34
반응형

앞선 글에서 이진 분류과 다중 분류, 즉 분류 문제를 다루었다. 분류 문제는 입력 데이터 포인트의 개별적인 레이블 하나를 예측하는 것이 목적이다. 또 다른 종류의 머신 러닝 문제에는, 개별적인 레이블 대신에 연속적인 값을 예측하는 회귀(regression)가 있다. 예를 들어 기상 데이터가 주어졌을 때 내일 기온을 예측하거나 소프트웨어 명세가 주어졌을 때 소프트웨어 프로젝트가 완료될 시간을 예측하는 등의 일을 한다.

 

(참고로 로지스틱 회귀 알고리즘은 이름만 회귀이지 회귀 알고리즘이 아니라 분류 알고리즘이다. 로지스틱 회귀는 선형 회귀의 분류 버전으로 중간층이 없고 하나의 유닛과 시그모이드 활성화 함수를 사용한 출력층만 있는 네트워크와 비슷하다.)

 

보스턴 주택 가격 데이터셋

1970년 중반 보스턴 외곽 지역의 범죄율, 지방세율 등의 데이터가 주어졌을 때 주택 가격의 중간 값을 예측해 보는 회귀 문제를 풀어보겠다. 여기서 사용할 데이터셋은 데이터 포인트가 506개로 비교적 개수가 적고 404개는 훈련 샘플로 102개는 테스트 샘플로 나누어져 있다. 입력 데이터에 있는 각 특성(예를 들어 범죄율)은 스케일이 서로 다르다. 어떤 값은 0과 1 사이의 비율을 나타내고 어떤 것은 1과 12 사이의 값을 가지거나 1과 100 사이의 값을 가진다.

from keras.datasets import boston_housing

(train_data, train_targets), (test_data, test_targets) =  boston_housing.load_data()

총 404개의 훈련 샘플과 102개의 테스트 샘플이 있고 모두 13개의 수치 특성을 가지고 있다. 13개의 특성은 다음과 같다.

1. Per capita crime rate.
2. Proportion of residential land zoned for lots over 25,000 square feet.
3. Proportion of non-retail business acres per town.
4. Charles River dummy variable (= 1 if tract bounds river; 0 otherwise).
5. Nitric oxides concentration (parts per 10 million).
6. Average number of rooms per dwelling.
7. Proportion of owner-occupied units built prior to 1940.
8. Weighted distances to five Boston employment centres.
9. Index of accessibility to radial highways.
10. Full-value property-tax rate per $10,000.
11. Pupil-teacher ratio by town.
12. 1000 * (Bk - 0.63) ** 2 where Bk is the proportion of Black people by town.
13. % lower status of the population.

타깃은 주택의 중간 가격으로 천달러 단위이며, 일반적으로 10000~50000달러 사이이다. (1970년대라서 저렴하다.)

 

데이터 준비

이런 다른 스케일을 가진 값을 신경망에 주입하면 문제가 된다. 네트워크가 이런 다양한 데이터에 자동으로 맞추려고 할 수 있지만 이는 확실히 학습을 더 어렵게 만든다. 특성의 스케일이 다르면 전역 최소 점을 찾아가는 경사 하강법의 경로가 스케일이 큰 특성에 영향을 많이 받기 때문이다.

이런 데이터를 다룰 때 대표적인 방법은 특성별로 정규화를 하는 것이다. (정규화는 다른 의미로도 쓰이기 때문에 좀 애매한데, 표준화라고 하면 정확하다). 입력 데이터에 있는 각 특성(입력 데이터 행렬의 열)에 대해서 특성의 평균을 빼고 표준 편차로 나누어 정규화를 할 수 있다. 특성의 중앙이 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

여기서 중요한 점은, 테스트 데이터를 정규화할 때 사용한 값이 훈련 데이터에서 계산한 값인 것이다. 머신 러닝 작업 과정에서 절대로 테스트 데이터에서 계산한 어떤 값도 사용해서는 안 된다. 데이터 정규화처럼 간단한 작업조차도 말이다. 쉽게 말하면 훈련 데이터와 테스트 데이터를 각각 다른 스케일로 변환하게 되면 훈련 데이터에서 학습한 정보가 쓸모없게 되는 셈이다. 마찬가지로 실전에 투업하여 새로운 데이터에 대한 예측을 만들 때도 훈련 데이터에서 계산한 값을 사용하여 정규화해야 한다.

 

모델 구성

샘플의 개수가 적기 때문에 64개의 유닛을 가진 두 개의 은닉층으로 작은 네트워크를 구성하여 사용하겠다. 일반적으로 훈련 데이터의 개수가 적을수록 과대적합이 더 쉽게 일어나므로 작은 모델을 사용하는 것이 과대적합을 피하는 방법 중에 하나이다.

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

동일한 모델을 여러 번 생성할 필요가 있기 때문에 함수 형태로 만든다.

이 네트워크의 마지막 층은 하나의 유닛을 가지고 있고 활성화 함수가 없다. 이를 선형 층이라고 한다. 이것이 전형적인 스칼라 회귀(하나의 연속적인 값을 예측하는 회귀)를 위한 구성이다.

활성화 함수를 적용하면 출력 값의 범위를 제한하게 된다. 예를 들어 마지막 층에 sigmoid 활성화 함수를 적용하면 네트워크가 0과 1 사이의 값을 예측하도록 학습될 것이다. 여기서는 마지막 층이 순수한 선형이므로 네트워크가 어떤 범위의 값이라도 예측하도록 자유롭게 학습된다.

 

이 모델은 mse 손실 함수를 사용하여 컴파일한다. mse는 평균 제곱 오차(Mean Squared Error, MSE)의 약자로, 예측과 타깃 사이 거리의 제곱이다. 회귀 문제에서 널리 사용되는 손실 함수이다.

 

훈련하는 동안 모니터링을 위해 새로운 지표인 평균 절대 오차(Mean Absolute Error, MAE)를 측정한다. 이는 예측과 타깃 사이 거리의 절댓값이다. 예를 들어 이 예제에서 MAE가 0.5이면 예측이 평균적으로 $500 정도 차이가 난다는 뜻이다.

 

k-겹 겁증을 사용한 훈련 검증

훈련에 사용할 에포크의 수 같은 매개변수들을 조정하면서 모델을 평가하기 위해 데이터를 훈련 세트와 검증 세트로 나눈다. 데이터 포인트가 많지 않기 때문에 검증 세트도 약 100개의 샘플 정도로 매우 작아진다. 결국 검증 세트와 훈련 세트로 어떤 데이터 포인트가 선택됐는지에 따라 검증 점수가 크게 달라진다. 검증 세트의 분할에 대한 검증 점수의 분산이 높게 되면 신뢰있는 모델 평가를 신뢰있게 할 수 없다.

이런 상황에서 가장 좋은 방법은 K-겹 교차 검증(K-fold cross-validation)을 사용하는 것이다.

1. 데이터를 K개의 분할(즉, 폴드)로 나누고(일반적으로 K=4 또는 5)

2. K개의 모델을 각각 만들어 K-1개의 분할에서 훈련하고 (K번째 모델은 검증, 나머지는 훈련)

3. 나머지 분할에서 평가하는 모델이다

4. 모델의 검증 점수는 K개의 검증 점수의 평균이다

import numpy as np

k = 4
num_val_samples = len(train_data) // k
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]

    #훈련 데이터 - k번째 분할이 아닌 다른 분할 전체
    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)

검증 세트가 다르므로 확실히 검증 점수가 2.1에서 2.9까지 변화가 크다. 평균값(2.6)이 각각의 점수보다 훨씬 신뢰할 만한 점수이다. 이것이 K-겹 교차 검증의 핵심이다. 이 예에서는 평균적으로 2600달러 정도 차이가 나는데, 주택 가격의 범위가 10,000달러에서 50,000달러 사이인 것을 감안하면 비교적 큰 값이다.

 

신경망을 조금 더 오래 500 에포크 동안 훈련해 본다. 각 에포크마다 모델이 얼마나 개선되는지 기록하기 위해 훈련 루프를 조금 수정해서 에포크의 검증 점수를 로그에 저장하겠다. 실행에 시간이 조금 걸린다.

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_mae']
    all_mae_histories.append(mae_history)

 

참고로 history 딕셔너리의 키 값은 다음과 같다. (책에서는 val_mean_absolute_error값을 사용했는데 val_mae를 사용해야 오류가 나지 않았다)

 

그다음 모든 폴드에 대해 에포크의 MAE 점수의 평균을 계산한다.

average_mae_history = [
    np.mean([x[i] for x in all_mae_histories]) for i in range(num_epochs)]

 

그래프로 나타내면 다음과 같다.

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

 

이 그래프는 범위가 크고 변동이 심하기 때문에 보기가 좀 어렵다. 다음처럼 바꾸어 본다.

  • 곡선의 다른 부분과 스케일이 많이 다른 첫 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()

이 그래프를 보면 검증 MAE가 50번째 에포크 이후에 줄어드는 것이 멈추었다. 이 지점 이후로는 과대적합이 시작된다는 의미이다.

모델의 다른 매개변수에 대한 튜닝이 끝나면(에포크 수뿐만 아니라 은닉층의 크기도 조절할 수 있다) 모든 훈련 데이터를 사용하고 최상의 매개변수로 최종 실전에 투입될 모델을 훈련시킨다. 그다음 테스트 데이터로 성능을 확인한다.

 

model = build_model() #새롭게 컴파일한 모델

#전체 데이터로 훈련한다
model.fit(train_data, train_targets,
          epochs=80, batch_size=16, verbose=0)
test_mse_score, test_mae_score = model.evaluate(test_data, test_targets)

이제 2,585달러 정도 차이가 난다.

 

정리

  • 회귀는 분류에서 사용했던 것과는 다른 손실 함수를 사용한다. 평균 제곱 오차(MSE)는 회귀에서 자주 사용되는 손실 함수이다.
  • 또한 회귀에서 사용되는 평가 지표는 분류와 다르다. 정확도 개념은 회귀에는 적용되지 않으며, 일반적인 회귀 지표는 평균 절대 오차(MAE)이다.
  • 입력 데이터의 특성이 서로 다른 범위를 가지면 전처리 단계에서 각 특성을 개별적으로 스케일 조정해야 한다.
  • 가용한 데이터가 적다면 K-겹 검증을 사용하는 것이 신뢰할 수 있는 모델 평가 방법이다.
  • 가용한 훈련 데이터가 적다면 과대적합을 피하기 위해 은닉층의 수를 줄인 모델이 좋다(일반적으로 하나 또는 두 개)
반응형
Comments