[핸즈온 머신러닝2] Chaper 2. 머신러닝 프로젝트 처음부터 끝까지
Chapter 2. 머신러닝 프로젝트 처음부터 끝까지¶
이번 시간에는 실제 데이터로 작업해봅시다.
오늘의 할 일은 바로 캘리포니아 인구조사 데이터를 사용해 캘리포니아의 주택 가격 모델을 만드는 것입니다. 이번 시간에서 사용하는 데이터는 캘리포니아의 블록 그룹마다 인구, 중간 소득, 중간 주택 가격 등을 담고 있습니다. 블록 그룹은 미국 인구조사국에서 샘플 데이터를 발표하는 데 사용하는 최소한의 지리적 단위입니다.
이 데이터로 모델을 학습시켜서 다른 측정 데이터가 주어졌을 때 구역의 중간 주택 가격을 예측해봅시다!
1. 데이터 불러오기¶
아래 코드는 데이터를 추출하는 함수입니다.
import os
import tarfile
from six.moves import urllib
DOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml/master/"
HOUSING_PATH = os.path.join("datasets", "housing")
HOUSING_URL = DOWNLOAD_ROOT + "datasets/housing/housing.tgz"
def fetch_housing_data(housing_url=HOUSING_URL, housing_path = HOUSING_PATH):
if not os.path.isdir(housing_path):
os.makedirs(housing_path)
tgz_path = os.path.join(housing_path, "housing.tgz")
urllib.request.urlretrieve(housing_url, tgz_path)
housing_tgz = tarfile.open(tgz_path)
housing_tgz.extractall(path=housing_path)
housing_tgz.close()
fetch_housing_data()
fetcho_housing_data()
함수를 호출하면 작업공간에 datasets/hosuing
디렉토리를 만들고 housing.tgz
파일을 내려받고 같은 디렉토리에 압축을 풀어 housing.csv
파일을 만듭니다.
판다스를 이용해서 housing.csv
파일을 읽어봅시다!
2. 데이터 살펴보기¶
import pandas as pd
def load_housing_data(housing_path=HOUSING_PATH):
csv_path = os.path.join(housing_path, "housing.csv")
return pd.read_csv(csv_path)
housing = load_housing_data()
housing.head()
longitude | latitude | housing_median_age | total_rooms | total_bedrooms | population | households | median_income | median_house_value | ocean_proximity | |
---|---|---|---|---|---|---|---|---|---|---|
0 | -122.23 | 37.88 | 41.0 | 880.0 | 129.0 | 322.0 | 126.0 | 8.3252 | 452600.0 | NEAR BAY |
1 | -122.22 | 37.86 | 21.0 | 7099.0 | 1106.0 | 2401.0 | 1138.0 | 8.3014 | 358500.0 | NEAR BAY |
2 | -122.24 | 37.85 | 52.0 | 1467.0 | 190.0 | 496.0 | 177.0 | 7.2574 | 352100.0 | NEAR BAY |
3 | -122.25 | 37.85 | 52.0 | 1274.0 | 235.0 | 558.0 | 219.0 | 5.6431 | 341300.0 | NEAR BAY |
4 | -122.25 | 37.85 | 52.0 | 1627.0 | 280.0 | 565.0 | 259.0 | 3.8462 | 342200.0 | NEAR BAY |
특성으로는 longtitude, latitude, housing_median_age, totla_rooms, total_bedrooms, popluation, households, median_income, median_house_value, ocean_proximity 등이 있네요.
info()
메서드를 통해 조금 더 자세히 알아봅시다.
housing.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 20640 entries, 0 to 20639 Data columns (total 10 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 longitude 20640 non-null float64 1 latitude 20640 non-null float64 2 housing_median_age 20640 non-null float64 3 total_rooms 20640 non-null float64 4 total_bedrooms 20433 non-null float64 5 population 20640 non-null float64 6 households 20640 non-null float64 7 median_income 20640 non-null float64 8 median_house_value 20640 non-null float64 9 ocean_proximity 20640 non-null object dtypes: float64(9), object(1) memory usage: 1.6+ MB
데이터셋에 20,640개의 샘플이 들어있습니다.
total_bedrooms의 경우에는 약 200개정도 null값이 들어있네요. ocean_proximity 값을 제외하곤 모두 숫자형 특성입니다.
ocean_proximty는 범주형 카테고리에 속할텐데, 어떤 특성들이 있는지 한번 살펴봅시다.
housing['ocean_proximity'].value_counts()
<1H OCEAN 9136 INLAND 6551 NEAR OCEAN 2658 NEAR BAY 2290 ISLAND 5 Name: ocean_proximity, dtype: int64
5개의 카테고리가 있네요.
describe()
메서드는 숫자형 특성의 요약 정보를 보여줍니다.
housing.describe()
longitude | latitude | housing_median_age | total_rooms | total_bedrooms | population | households | median_income | median_house_value | |
---|---|---|---|---|---|---|---|---|---|
count | 20640.000000 | 20640.000000 | 20640.000000 | 20640.000000 | 20433.000000 | 20640.000000 | 20640.000000 | 20640.000000 | 20640.000000 |
mean | -119.569704 | 35.631861 | 28.639486 | 2635.763081 | 537.870553 | 1425.476744 | 499.539680 | 3.870671 | 206855.816909 |
std | 2.003532 | 2.135952 | 12.585558 | 2181.615252 | 421.385070 | 1132.462122 | 382.329753 | 1.899822 | 115395.615874 |
min | -124.350000 | 32.540000 | 1.000000 | 2.000000 | 1.000000 | 3.000000 | 1.000000 | 0.499900 | 14999.000000 |
25% | -121.800000 | 33.930000 | 18.000000 | 1447.750000 | 296.000000 | 787.000000 | 280.000000 | 2.563400 | 119600.000000 |
50% | -118.490000 | 34.260000 | 29.000000 | 2127.000000 | 435.000000 | 1166.000000 | 409.000000 | 3.534800 | 179700.000000 |
75% | -118.010000 | 37.710000 | 37.000000 | 3148.000000 | 647.000000 | 1725.000000 | 605.000000 | 4.743250 | 264725.000000 |
max | -114.310000 | 41.950000 | 52.000000 | 39320.000000 | 6445.000000 | 35682.000000 | 6082.000000 | 15.000100 | 500001.000000 |
count
, mean
, std
, min
, max
가 의미하는 바는 쉽게 이해하실 수 있을겁니다.
25% 등의 숫자는 백분위수를 의미합니다. 예를들어 25%의 구역은 housing_median_age
가 18보다 작고 50%는 29보다 작고 그런 것을 의미하는 것이죠.
데이터 시각화¶
데이터의 형태를 빠르게 검토하는 다른 방법은 각 숫자형 특성을 히스토그램으로 그려보는 것입니다.
히스토그램은 주어진 값의 범위에 속한 샘플 수를 나타냅니다. 특성마다 따로 히스토그램을 그릴 수 있고 전체 데이터셋에 대해 hist()
메서드를 호출하면 모든 숫자형 특성에 대한 히스토그램을 출력할 수 있습니다.
import matplotlib.pyplot as plt
%matplotlib inline
housing.hist(bins=50, figsize=(20, 15))
plt.show()
화면에 그래프를 그리기 위해 사용자 컴퓨터의 그래픽 백엔드를 필요로합니다.
그래서 그래프를 그리기 전에 matplot이 사용할 백엔드를 지정해주어야 하는데,
%matplotlib inline을 사용하면 편합니다.
이 명령은 matplot이 주피터 자체에 백엔드를 사용하도록 설정합니다.
위 그래프를 보시면 다양한 사실을 알 수 있습니다.
- 특성들의 스케일이 많이 다릅니다.
- 히스토그램의 분포는 모두 앞에 쏠려있는 편입니다. 이런 형태는 일부 머신러닝 알고리즘에서 패턴을 찾기 어렵게 만들기 때문에 이런 특성들을 좀 더 종모양의 분포(정규분포)로 만들어주는 것이 좋습니다.
- 중간주택연도(housing median age)와 중간 주택 가격(median house value)는 최댓값과 최솟값을 한정했습니다. 특히, 중간주택가격의 경우 label로 사용되기 때문에 심각한 문제가 될 수 있습니다.
- 중간소득(median income) 특성이 us 달러로 표현되어있지 않습니다.
3. 테스트 세트 만들기¶
테스트 세트를 따로 분리해줍시다. 테스트 세트를 생성하는 것은 간단합니다. 그냥 무작위로 어떤 샘플을 선택해 데이터셋의 20% 정도를 떼어놓으면 됩니다.
import numpy as np
def split_train_test(data, test_ratio):
shuffled_indices = np.random.permutation(len(data))
test_set_size = int(len(data) * test_ratio)
test_indices = shuffled_indices[:test_set_size]
train_indices = shuffled_indices[test_set_size:]
return data.iloc[train_indices], data.iloc[test_indices]
train_set, test_set = split_train_test(housing, 0.2)
print(len(train_set), len(test_set))
16512 4128
위와 같은 방법 외에도 사이킷런에서 제공하는 방법도 있습니다.
가장 간단한 함수는 train_test_split
으로, 우리가 위에서 만든 함수와 아주 비슷하지만 몇가지 특징이 더 있습니다.
- 난수 초기값을 지정할 수 있는
random_state
매개변수가 있고 - 행의 개수가 같은 여러개의 데이터셋을 넘겨서 같은 인덱스를 기반으로 나눌 수 있습니다.
from sklearn.model_selection import train_test_split
train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)
위는 순수한 무작위 샘플링 방식입니다. 일반적으로 괜찮지만, 편향이 큰 데이터셋의 경우에는 샘플링 편향이 발생할 수 있습니다.
그렇기 때문에 각 계층별로 샘플링을 해주는 것이 좋습니다.
계층적 샘플링을 통해 특정 부분의 데이터들을 모아모아 테스트세트로 만들 수 있습니다.
housing['income_cat'] = np.ceil(housing['median_income'] / 1.5)
housing['income_cat'].where(housing['income_cat'] < 5, 5.0, inplace=True)
위 코드는 중간 소득을 1.5로 나누고 (소득의 카테고리 수를 제한하기 위해), ceil
함수를 사용해 올림해서 소득 카테고리 특성을 만들고 5보다 큰 카테고리는 5로 잡힙니다.
housing['income_cat'].hist()
<matplotlib.axes._subplots.AxesSubplot at 0x7f31a3519a90>
그럼 이제 계층별로 샘플링을 해봅시다. 사이킷런에서는 계층 샘플링을 StrarifiedShuffleSplit
으로 제공하고 있습니다.
from sklearn.model_selection import StratifiedShuffleSplit
split = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
for train_index, test_index in split.split(housing, housing['income_cat']):
strat_train_set = housing.loc[train_index]
strat_test_set = housing.loc[test_index]
제대로 되었는지 한번 확인해봅시다.
전체 주택 데이터셋에서 소득 카테고리의 비율을 먼저 살펴봅시다.
housing['income_cat'].value_counts() / len(housing)
3.0 0.350581 2.0 0.318847 4.0 0.176308 5.0 0.114438 1.0 0.039826 Name: income_cat, dtype: float64
확실히 무작위 샘플링보다는 제대로 반영한 것 같습니다.
그럼 다시 income_cat
특성을 삭제해서 데이터를 원래 상태로 되돌려줍시다.
for set_ in (strat_train_set, strat_test_set):
set_.drop('income_cat', axis=1, inplace=True)
4. 데이터 이해를 위한 탐색과 시각화¶
지금까지 데이터를 잘 나누었습니다. 이제 조금 더 깊이 알아봅시다.
먼저 테스트 세트를 떼어놓고 훈련세트에 대해서만 탐색을 해봅시다. 훈련세트를 손상시키지 않기 위해 복사본을 만들어 사용합니다.
housing = strat_train_set.copy()
4.1 지리적 데이터 시각화¶
지리정보가 있으니 모든 구역을 산점도로 만들어 데이터 시각화를 해봅시다.
housing.plot(kind='scatter', x='longitude', y='latitude')
<matplotlib.axes._subplots.AxesSubplot at 0x7f31928fc3c8>
alpha
옵션을 0.1로 주면 데이터 포인트가 밀집된 영역을 잘 보여줍니다.
housing.plot(kind='scatter', x='longitude', y='latitude', alpha=0.1)
<matplotlib.axes._subplots.AxesSubplot at 0x7f31a29a97b8>
주택 가격을 한번 나타내봅시다.
원의 반지름은 구역의 인구를 나타내고(매개변수 s), 색깔은 가격을 나타냅니다. (매개변수 c). 여기서는 미리 정의된 컬러 맵 중 파란색(낮은 가격)에서 빨간색(높은 가격)까지 범위를 가지는 jet
을 사용합니다. (매개변수 cmap)
housing.plot(kind='scatter', x='longitude', y='latitude', alpha=0.4,
s = housing['population']/100, label='population', figsize=(10, 7),
c='median_house_value', cmap=plt.get_cmap('jet'), colorbar=True, sharex=False)
plt.legend()
plt.show()
위 그림을 보시면 아시겠지만 바다와 인접한 곳과 인구밀도과 관련이 큰 것을 알 수 있습니다. 이런 내용은 군집 알고리즘을 사용해 주요 군집을 찾고 군집의 중심까지의 거리를 재는 특성을 추가할 때 도움이 됩니다.
4.2 상관관계 조사¶
데이터셋이 너무 크지 않으므로 모든 특성간의 표준 상관계수를 corr()
메서드를 이용해 쉽게 계산할 수 있습니다.
corr_matrix = housing.corr()
corr_matrix['median_house_value'].sort_values(ascending=False)
median_house_value 1.000000 median_income 0.687160 total_rooms 0.135097 housing_median_age 0.114110 households 0.064506 total_bedrooms 0.047689 population -0.026920 longitude -0.047432 latitude -0.142724 Name: median_house_value, dtype: float64
상관관계의 범위는 -1부터 1까지이비다. 1에 가까우면 강한 양이 상관관계를 가진다는 뜻입니다. 예를들어 중간 주택가격은 중간소득이 올라갈 때 증가하는 경향이 있습니다. 계수가 -1에 가까우면 강한 음의 상관관계를 나타냅니다. 위도 (latitude)와 같은 중간 주택 가격 사이에는 약한 음의 상관관계가 보입니다. 마지막으로 계수가 0에 가까우면 선형적인 상관관계가 없다는 뜻입니다.
특성 사이의 상관관계를 확인하는 다른 방법은 숫자형 특성 사이에 산점도를 그려주는 판다스의 scatter_matrix
함수를 사용하는 것입니다. 여기서는 숫자형 특성이 11개이므로 총 $11^2=121$개의 그래프가 되어 한 페이지에 모두 나타낼 수 없으므로, 중간 주택 가격과 상관관계가 높아보이는 특성 몇개만 살펴보겠습니다.
from pandas.plotting import scatter_matrix
attributes = ["median_house_value", "median_income", "total_rooms",
"housing_median_age"]
scatter_matrix(housing[attributes], figsize=(12, 8))
array([[<matplotlib.axes._subplots.AxesSubplot object at 0x7f31927b9828>, <matplotlib.axes._subplots.AxesSubplot object at 0x7f319268acf8>, <matplotlib.axes._subplots.AxesSubplot object at 0x7f3192781f60>, <matplotlib.axes._subplots.AxesSubplot object at 0x7f3192741208>], [<matplotlib.axes._subplots.AxesSubplot object at 0x7f31926f5470>, <matplotlib.axes._subplots.AxesSubplot object at 0x7f31926a76d8>, <matplotlib.axes._subplots.AxesSubplot object at 0x7f31926db940>, <matplotlib.axes._subplots.AxesSubplot object at 0x7f3190e4db70>], [<matplotlib.axes._subplots.AxesSubplot object at 0x7f3190e4dbe0>, <matplotlib.axes._subplots.AxesSubplot object at 0x7f3190dc20b8>, <matplotlib.axes._subplots.AxesSubplot object at 0x7f3190d75320>, <matplotlib.axes._subplots.AxesSubplot object at 0x7f3192905c88>], [<matplotlib.axes._subplots.AxesSubplot object at 0x7f319274b208>, <matplotlib.axes._subplots.AxesSubplot object at 0x7f3190d6ce80>, <matplotlib.axes._subplots.AxesSubplot object at 0x7f319281ce48>, <matplotlib.axes._subplots.AxesSubplot object at 0x7f31927e1fd0>]], dtype=object)
대각선 방향(왼쪽 위에서 오른쪽 아래로)은 각 변수 자신에 대한 것이라 그냥 직선이 되므로 유용하지 않습니다. 그래서 판다스는 그냥 이곳에 히스토그램을 그리죠.
중간 주택 가격(median_house_value
)를 예측하는데 가장 유용할 것 같은 특성은 중간소득(median_income
)이므로 상관관계 산점도를 확대해보겠습니다.
housing.plot(kind='scatter', x='median_income', y='median_house_value', alpha=0.1)
<matplotlib.axes._subplots.AxesSubplot at 0x7f3190cbf160>
이 그래프를 통해 알수있는 사실에는 몇가지가 있습니다.
- 강한 상관관계 : 위쪽으로 향하는 방향을 볼 수 있으며 포인트들이 너무 멀리 퍼져있지 않습니다.
- 가격 제한값 \$500,000 달러가 잘 보입니다.
- 직선에 가까운 형태들이 더 있습니다. \$450,000과 \$350,000, 그리고 $280,000 에도 있습니다.
4.3 특성 조합으로 실험¶
머신러닝 알고리즘용 데이터를 실제로 준비하기 전에 마지막으로 해볼 수 있는 것은 여러 특성의 조합을 시도해보는 것입니다.
방 갯수와 가구당 인원 등을 통해 특성을 한번 만들어봅시다.
housing['rooms_per_household'] = housing['total_rooms']/housing['households']
housing['bedrooms_per_room'] = housing['total_bedrooms']/housing['households']
housing['population_per_household'] = housing['population']/housing['households']
corr_matrix = housing.corr()
corr_matrix['median_house_value'].sort_values(ascending=False)
median_house_value 1.000000 median_income 0.687160 rooms_per_household 0.146285 total_rooms 0.135097 housing_median_age 0.114110 households 0.064506 total_bedrooms 0.047689 population_per_household -0.021985 population -0.026920 bedrooms_per_room -0.043343 longitude -0.047432 latitude -0.142724 Name: median_house_value, dtype: float64
상관관계 행렬을 살펴보았습니다. 새로운 bedrooms_per_room
특성은 전체 방 개수나 침대 개수보다 중간 주택 가격과의 상관관계가 훨씬 높습니다. 확실히 침대/방의 비율이 낮은 집은 더 비싼 경향이 있습니다.
그럼 이제 탐색을 마쳤습니다. 이제 본격적으로 머신러닝 알고리즘을 위한 데이터를 준비해볼까요?
5. 머신러닝 알고리즘을 위한 데이터 준비¶
먼저 원래 훈련세트로 복원하고 예측변수와 타깃값에 같은 변형을 적용하지 않기 위해 예측변수와 레이블을 분리해줍니다.
housing = strat_train_set.drop("median_house_value", axis=1)
housing_labels = strat_train_set["median_house_value"].copy()
5.1 데이터 정제¶
먼저, null값을 어떻게 처리할지를 생각해봅시다. 앞서 말했다싶이 total_bedrooms
의 경우에는 null값이 존재하는데, 이를 해결하는 방법은 아래와 같습니다.
- 해당 구역을 제거한다
- 전체 특성을 삭제한다
- 어떤 값으로 채운다. (0, 평균, 중간값 등)
데이터 프레임의 dropna()
, drop()
, fillna()
등의 메서드를 이용해 이런 작업을 간단하게 처리할 수 있습니다.
housing.dropna(subset=['total_bedrooms']) # 옵션 1
housing.drop("total_bedrooms", axis=1) # 옵션 2
median = housing['total_bedrooms'].median() # 옵션 3
housing['total_bedrooms'].fillna(median, inplace=True)
사이킷런의 SimpleImputer
는 누락된 값을 손쉽게 다루도록 해줍니다.
먼저 누락된 값을 중간값으로 대체한다고 지정하여 SimpleImputer
객체를 생성합니다.
from sklearn.impute import SimpleImputer
imputer = SimpleImputer(strategy="median")
중간값이 수치형 특성에서만 계산될 수 있기 때문에 텍스트 특성인 ocean_proximity
를 제외한 데이터 복사본을 생성합니다.
housing_num = housing.drop("ocean_proximity", axis=1)
imputer 객체의 fit()
메소드를 사용하면 훈련 데이터에 적용할 수 있습니다.
imputer.fit(housing_num)
SimpleImputer(add_indicator=False, copy=True, fill_value=None, missing_values=nan, strategy='median', verbose=0)
imputer
는 각 특성의 중간값을 계산해서 그 결과를 객체의 statistics_
속성에 저장합니다.
imputer.statistics_
array([-118.51 , 34.26 , 29. , 2119.5 , 433. , 1164. , 408. , 3.5409])
housing_num.median().values
array([-118.51 , 34.26 , 29. , 2119.5 , 433. , 1164. , 408. , 3.5409])
이제 학습된 imputer
객체를 사용해 훈련 세트에서 누락된 값을 학습한 중간값으로 바꿀 수 있습니다.
X = imputer.transform(housing_num)
housing_tr = pd.DataFrame(X, columns=housing_num.columns, index=list(housing.index.values))
저렇게 하면 변화된 특성들이 있는 넘파이 배열을 판다스 데이터 프레임으로 바꿀 수 있습니다.
5.2 텍스트와 범주형 특성 다루기¶
범주형 특성 ocean_proximity
가 텍스트라 중간값을 계산할 수 없어서 남겨두었었습니다.
범주형 데이터는 어떻게 처리하면 좋을지 한번 살펴봅시다.
housing_cat = housing['ocean_proximity']
housing_cat.head(10)
17606 <1H OCEAN 18632 <1H OCEAN 14650 NEAR OCEAN 3230 INLAND 3555 <1H OCEAN 19480 INLAND 8879 <1H OCEAN 13685 INLAND 4937 <1H OCEAN 4861 <1H OCEAN Name: ocean_proximity, dtype: object
대부분의 머신러닝 알고리즘은 이 범주형 데이트를 숫자로 바꿔서 사용합니다.
이를 위해 각 카테고리를 다른 정숫값으로 매핑해주는 판다스의 factorize()
메소드를 사용합니다.
housing_cat_encoded, housing_categories = housing_cat.factorize()
housing_cat_encoded[:10]
array([0, 0, 1, 2, 0, 2, 0, 2, 0, 0])
housing_categories
Index(['<1H OCEAN', 'NEAR OCEAN', 'INLAND', 'NEAR BAY', 'ISLAND'], dtype='object')
그렇지만 이러한 표현상식의 문제점은 가까이 있는 두 값이 멀리 떨어져 있는 두 값보다 더 비슷하다고 생각한다는 점입니다. 실제로는 그렇지 않죠.
이 문제는 일반적으로 카테고리별 이진 특성을 만들어 해결합니다. 잘 아시는 원핫 인코딩이 그 방법이죠.
사이킷런은 숫자로 된 범주형 값을 원-핫 벡터로 만들어주는 OneHotEncoder
를 제공합니다. 카테고리들을 원-핫 벡터로 인코딩해보겠습니다.
from sklearn.preprocessing import OneHotEncoder
encoder = OneHotEncoder(categories='auto')
housing_cat_1hot = encoder.fit_transform(housing_cat_encoded.reshape(-1, 1))
housing_cat_1hot
<16512x5 sparse matrix of type '<class 'numpy.float64'>' with 16512 stored elements in Compressed Sparse Row format>
fit_transform()
메소드는 2차원 배열을 넣어줘야 하는데 housing_cat_encoded
는 1차원 배열이므로 구조를 바꿔줘야 합니다.
또한 출력을 보면 넘파이 배열이 아니고 사이파이(scipy) 희소 행렬(sparse matrix)입니다. 이는 수천개의 카테고리가 있는 범주형 특성일 경우 매우 효율적입니다.
이런 특성을 원-핫 인코딩하면 열이 수천개인 행렬로 변하고 각 행은 1이 하나뿐이고 그 외에는 모두 0으로 채워져 있을 것입니다. 0을 모두 메모리에 저장하는 것은 낭비이므로 희소행렬은 0이 아닌 위치만 저장합니다.
이 행렬을 넘파이 배열로 바꾸려면 toarray()
메소드를 사용하면 됩니다.
housing_cat_1hot.toarray()
array([[1., 0., 0., 0., 0.], [1., 0., 0., 0., 0.], [0., 1., 0., 0., 0.], ..., [0., 0., 1., 0., 0.], [1., 0., 0., 0., 0.], [0., 0., 0., 1., 0.]])
5.3 나만의 변환기¶
사이킷런이 유용한 변환기를 많이 제공하긴 하지만 특별한 정제 작업이나 어떤 특성들을 조합하는 등의 작업을 위해 자신만의 변환기를 만들어야 할 때가 있습니다.
사이킷런은 덕 타이핑을 지원하므로 fit()
, transform()
, fit_transform()
메서드를 구현한 파이썬 클래스를 만들면 됩니다.
(덕 타이핑(duck typing) : 상속이나 인터페이스 구현이 아니라 객체의 속성이나 메서드가 객체의 유형을 결정하는 방식)
마지막 메서드는 TransformerMixin
을 상속하면 자동으로 생성됩니다. 또한 BaseEstimator
를 상속하면 하이퍼 파라미터 튜닝에 필요한 두 메서드(get_params()
와 set_params()
)를 추가로 얻게됩니다.
아래 코드는 앞서 이야기한 조합 특성을 추가하는 간단한 변환기입니다.
from sklearn.base import BaseEstimator, TransformerMixin
rooms_ix, bedrooms_ix, population_ix, household_ix = 3, 4, 5, 6
class CombinedAttributesAdder(BaseEstimator, TransformerMixin):
def __init__(self, add_bedrooms_per_room=True):
self.add_bedrooms_per_room = add_bedrooms_per_room
def fit(self, X, y=None):
return self
def transform(self, X, y=None):
rooms_per_household = X[:, rooms_ix] / X[:, household_ix]
population_per_household = X[:, population_ix] / X[:, household_ix]
if self.add_bedrooms_per_room:
bedrooms_per_room = X[:, bedrooms_ix] / X[:, rooms_ix]
return np.c_[X, rooms_per_household, population_per_household, bedrooms_per_room]
else:
return np.c_[X, rooms_per_household, population_per_household]
attr_adder = CombinedAttributesAdder(add_bedrooms_per_room=False)
housing_extra_attribs = attr_adder.transform(housing.values)
이 경우에는 변환기가 add_bedrooms_per_room
하이퍼 파라미터 하나를 가지고 있고 기본값을 True로 지정합니다.
이 특성을 추가하는 것이 머신러닝 알고리즘에 도움이 될지 안될지 이 하이퍼파라미터로 쉽게 확인해볼 수 있습니다. 일반적으로 100% 확신이 없는 모든 데이터 준비 단계에 대해 하이퍼 파라미터를 추가할 수 있습니다.
5.4 특성 스케일링¶
머신러닝 알고리즘은 숫자 특성들의 스케일이 많이 다르면 잘 작동하지 않습니다.
모든 특성의 범위를 같도록 만들어주는 방법으로 (1)min-max 스케일링과 (2)표준화(standardization)이 있습니다.
min-max 스케일링¶
많은 사람들이 정규화라고도 부르는 min-max 스케일링은 간단합니다. 0~1 범위에 들도록 값을 이동하고 스케일을 조정하면 됩니다. 데이터에서 최솟값을 뺀 후 최댓값과 최솟값의 차이로 나누면 이렇게 할 수 있습니다.
$$x' = \frac{x-min(x)}{max(x)-min(x)}$$사이킷런에서는 이에 해당하는 MinMaxScaler
변환기를 제공합니다. 0~1 사이를 원하지 않는다면 feature_range
매개변수 범위를 변경할 수 있습니다.
표준화 (standardization)¶
표준화는 먼저 평균을 뺀 후 표준편차로 나누어 결과 분포의 분산이 1이 되도록 합니다.
$$X' = \frac{X - \mu}{\sigma}$$min-max 스케일링과 달리 표준화는 범위의 상한과 ㅂ하한이 없어 어떤 알고리즘에서는 문제가 될 수 있습니다. 그러나 표준화는 이상치에 영향을 덜받는다는 장점이 있습니다.
사이킷런에서는 표준화를 위한 StandardScaler
변환기가 있습니다.
5.5 변환 파이프라인¶
앞서 보았듯이 변호나 단계가 많으며 정확한 순서대로 실행되어야 합니다. 다행히 사이킷런에는 연속된 변환을 순서대로 처리할 수 있도록 도와주는 Pipeline
클래스가 있습니다.
아래는 숫자 특성을 처리하는 간단한 파이프라인입니다.
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
num_pipeline = Pipeline([
('imputer', SimpleImputer(strategy='median')),
('attribs_adder', CombinedAttributesAdder()),
('std_scaler', StandardScaler())
])
housing_num_tr = num_pipeline.fit_transform(housing_num)
Pipeline
은 연속된 단계를 나타내는 이름/추정기 쌍의 목록을 입력으로 받습니다. 마지막 단계에는 변환기와 추정기를 모두 사용할 수 있고, 그 외에는 모두 변환기여야 합니다. (즉, fit_transform()
메소드를 가지고 있어야 합니다.)
파이프라인의 fit()
메소드를 호출하면 모든 변환기의 fit_transform()
메서드를 순서대로 호출하면서 한 단계의 출력을 다음 단계의 입력으로 전달합니다. 마지막 단계에서는 fit()
메소드만 호출합니다.
파이프라인의 객체는 마지막 추정기와 동일한 객체를 제공합니다. 이 예에서는 마지막 추정기가 변환기 StandardScaler이므로 파이프라인이 데이터애 대해 모든 변환을 순서대로 적용하는 transform()
메서드를 가지고 있습니다.
수치형 칼럼을 넘파이 배열로 추출하는 대신 판다스의 데이터 프레임을 파이프라인에 직접 주입할 수 있다면 좋을 것입니다. 사이킷런이 판다스의 데이터프레임을 다룰 수는 없지만, 이를 처리하는 변환기를 직접 만들 수는 있습니다.
from sklearn.base import BaseEstimator, TransformerMixin
class DataFrameSelector(BaseEstimator, TransformerMixin):
def __init__(self, attribute_names):
self.attribute_names = attribute_names
def fit(self, X, y=None):
return self
def transform(self, X):
return X[self.attribute_names].values
DataFrameSelector
는 나머지는 버리고 필요한 특성을 선택하여 데이터프레임을 넘파이 배열로 바꾸는 식으로 데이터를 변환합니다. 이를 이용해 데이터프레임을 받아 수치형만 다루는 파이프라인을 손쉽게 만들 수 있습니다.
수치형 특성을 선택한 DataFrameSelector
로 파이프라인을 시작해서 앞서 이야기한 다른 전처리 단계들을 나열합니다. 범주형 특성을 다루는 또 다른 파이프라인도 DataFrameSelector
로 범주형 특성을 선택하고 CategoricalEncoder
를 적용하면 간단히 만들 수 있습니다.
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.utils import check_array
from sklearn.preprocessing import LabelEncoder
from scipy import sparse
class CategoricalEncoder(BaseEstimator, TransformerMixin):
def __init__(self, encoding='onehot', categories='auto', dtype=np.float64,
handle_unknown='error'):
self.encoding = encoding
self.categories = categories
self.dtype = dtype
self.handle_unknown = handle_unknown
def fit(self, X, y=None):
if self.encoding not in ['onehot', 'onehot-dense', 'ordinal']:
template = ("encoding should be either 'onehot', 'onehot-dense' "
"or 'ordinal', got %s")
raise ValueError(template % self.handle_unknown)
if self.handle_unknown not in ['error', 'ignore']:
template = ("handle_unknown should be either 'error' or "
"'ignore', got %s")
raise ValueError(template % self.handle_unknown)
if self.encoding == 'ordinal' and self.handle_unknown == 'ignore':
raise ValueError("handle_unknown='ignore' is not supported for"
" encoding='ordinal'")
X = check_array(X, dtype=np.object, accept_sparse='csc', copy=True)
n_samples, n_features = X.shape
self._label_encoders_ = [LabelEncoder() for _ in range(n_features)]
for i in range(n_features):
le = self._label_encoders_[i]
Xi = X[:, i]
if self.categories == 'auto':
le.fit(Xi)
else:
valid_mask = np.in1d(Xi, self.categories[i])
if not np.all(valid_mask):
if self.handle_unknown == 'error':
diff = np.unique(Xi[~valid_mask])
msg = ("Found unknown categories {0} in column {1}"
" during fit".format(diff, i))
raise ValueError(msg)
le.classes_ = np.array(np.sort(self.categories[i]))
self.categories_ = [le.classes_ for le in self._label_encoders_]
return self
def transform(self, X):
X = check_array(X, accept_sparse='csc', dtype=np.object, copy=True)
n_samples, n_features = X.shape
X_int = np.zeros_like(X, dtype=np.int)
X_mask = np.ones_like(X, dtype=np.bool)
for i in range(n_features):
valid_mask = np.in1d(X[:, i], self.categories_[i])
if not np.all(valid_mask):
if self.handle_unknown == 'error':
diff = np.unique(X[~valid_mask, i])
msg = ("Found unknown categories {0} in column {1}"
" during transform".format(diff, i))
raise ValueError(msg)
else:
X_mask[:, i] = valid_mask
X[:, i][~valid_mask] = self.categories_[i][0]
X_int[:, i] = self._label_encoders_[i].transform(X[:, i])
if self.encoding == 'ordinal':
return X_int.astype(self.dtype, copy=False)
mask = X_mask.ravel()
n_values = [cats.shape[0] for cats in self.categories_]
n_values = np.array([0] + n_values)
indices = np.cumsum(n_values)
column_indices = (X_int + indices[:-1]).ravel()[mask]
row_indices = np.repeat(np.arange(n_samples, dtype=np.int32),
n_features)[mask]
data = np.ones(n_samples * n_features)[mask]
out = sparse.csc_matrix((data, (row_indices, column_indices)),
shape=(n_samples, indices[-1]),
dtype=self.dtype).tocsr()
if self.encoding == 'onehot-dense':
return out.toarray()
else:
return out
num_attribs = list(housing_num)
cat_attribs = ["ocean_proximity"]
num_pipeline = Pipeline([
('selector', DataFrameSelector(num_attribs)),
('imputer', SimpleImputer(strategy='median')),
('attribs_adder', CombinedAttributesAdder()),
('std_scaler', StandardScaler())
])
cat_pipeline = Pipeline([
('selector', DataFrameSelector(cat_attribs)),
('cat_encoder', CategoricalEncoder(encoding="onehot-dense"))
])
하지만 어떻게 이 두 파이프라인을 하나의 파이프라인으로 합칠 수 있을까요?
사이킷런의 FeatureUnion
을 이용하면쉽게 해결할 수 있습니다. 변환기 목록을 전달하고 transform()
메서드를 병렬로 실행합니다. 그런 다음 각 변환기의 결과를 합쳐 반환합니다.
숫자형과 범주형을 모두 다루는 전체 파이프라인은 다음과 같습니다.
from sklearn.pipeline import FeatureUnion
full_pipeline = FeatureUnion(transformer_list=[
("num_pipeline", num_pipeline),
("cat_pipeline", cat_pipeline)
])
전체 파이프라인을 간단하게 실행할 수 있습니다.
housing_prepared = full_pipeline.fit_transform(housing)
housing_prepared
array([[-1.15604281, 0.77194962, 0.74333089, ..., 0. , 0. , 0. ], [-1.17602483, 0.6596948 , -1.1653172 , ..., 0. , 0. , 0. ], [ 1.18684903, -1.34218285, 0.18664186, ..., 0. , 0. , 1. ], ..., [ 1.58648943, -0.72478134, -1.56295222, ..., 0. , 0. , 0. ], [ 0.78221312, -0.85106801, 0.18664186, ..., 0. , 0. , 0. ], [-1.43579109, 0.99645926, 1.85670895, ..., 0. , 1. , 0. ]])
housing_prepared.shape
(16512, 16)
6. 모델 선택과 훈련¶
이제 마지막 단계입니다. 만든 모델을 훈련시켜봅시다.
6.1 훈련 세트에서 훈련하고 평가하기¶
선형 회귀모델을 한번 훈련시켜봅시다.
from sklearn.linear_model import LinearRegression
lin_reg = LinearRegression()
lin_reg.fit(housing_prepared, housing_labels)
LinearRegression(copy_X=True, fit_intercept=True, n_jobs=None, normalize=False)
훈련 세트에 있는 몇개 샘플에 대해 적용해봅시다.
some_data = housing.iloc[:5]
some_labels = housing_labels.iloc[:5]
some_data_prepared = full_pipeline.transform(some_data)
print("예측 : ", lin_reg.predict(some_data_prepared))
print("레이블 : ", list(some_labels))
예측 : [210644.60459286 317768.80697211 210956.43331178 59218.98886849 189747.55849879] 레이블 : [286600.0, 340600.0, 196900.0, 46300.0, 254500.0]
아주 정확한 예측은 아니지만 작동합니다.
사이킷런의 mean_square_error
함수를 사용해 전체 훈련 세트에 대한 RMSE를 측정해봅시다.
from sklearn.metrics import mean_squared_error
housing_predictions = lin_reg.predict(housing_prepared)
lin_mse = mean_squared_error(housing_labels, housing_predictions)
lin_rmse = np.sqrt(lin_mse) # 루트 씌워줌
lin_rmse
68628.19819848923
없는것보다는 낫지만 좋은 점수는 아닙니다. (참고로, RMSE의 경우 0에 가까울수록 좋은 숫자입니다.)
그럼 한번 다른 모델로 학습을 시켜보도록 하겠습니다. 이번에는 DecisionTreeRegressor
로 학습을 시켜봅시다.
from sklearn.tree import DecisionTreeRegressor
tree_reg = DecisionTreeRegressor()
tree_reg.fit(housing_prepared, housing_labels)
DecisionTreeRegressor(ccp_alpha=0.0, criterion='mse', max_depth=None, max_features=None, max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=None, min_samples_leaf=1, min_samples_split=2, min_weight_fraction_leaf=0.0, presort='deprecated', random_state=None, splitter='best')
# 평가
housing_predictions = tree_reg.predict(housing_prepared)
tree_mse = mean_squared_error(housing_labels, housing_predictions)
tree_rmse = np.sqrt(tree_mse)
tree_rmse
0.0
오차가 0입니다. 모델이 과대적합된 것 같죠?
훈련 세트의 일부분으로 훈련을 하고 다른 일부분은 모델 검증에 사용해야 합니다. 교차 검증을 통해서 한번 그렇게 해봅시다.
6.2 교차 검증을 사용한 평가¶
우리는 홀드아웃검증을 기존에 사용했었습니다. (train_test_split
을 통해) 나쁘지 않은 방법이었습니다만, 데이터가 작을 경우에 사용하기 좋은 방법이 바로 교차검증입니다.
아래 코드는 K-겹교차 검증(K-fold cross validation)을 수행하는 코드입니다. 훈련세트를 폴드(fold)라 불리는 10개의 서브셋으로 무작위로 분할한 뒤 그런 다음 결정 트리 모델을 10번 훈련하고 평가합니다. 매번 다른 폴드를 선택해 평가에 사용합니다.
from sklearn.model_selection import cross_val_score
scores = cross_val_score(tree_reg, housing_prepared, housing_labels,
scoring="neg_mean_squared_error", cv=10)
tree_rmse_scores = np.sqrt(-scores)
def display_scores(scores):
print("Scores : ", scores)
print("Mean : ", scores.mean())
print("Standard devitation : ", scores.std()) # 표준편차
display_scores(tree_rmse_scores)
Scores : [70238.28437144 66032.54905203 70836.93076996 70668.80951451 71622.90489975 74491.52002872 70201.03475187 71153.30646739 74801.86184172 70776.21480836] Mean : 71082.34165057619 Standard devitation : 2303.672309549565
결정트리 결과가 그닥 좋아보이진 않습니다. 실제로 선형 회귀 모델보다 나쁩니다.
한번 비교를 위해 선형회귀 모델의 점수를 계산해보겠습니다.
lin_scores = cross_val_score(lin_reg, housing_prepared, housing_labels,
scoring="neg_mean_squared_error", cv=10)
lin_rmse_scores = np.sqrt(-lin_scores)
display_scores(lin_rmse_scores)
Scores : [66782.73843989 66960.118071 70347.95244419 74739.57052552 68031.13388938 71193.84183426 64969.63056405 68281.61137997 71552.91566558 67665.10082067] Mean : 69052.46136345083 Standard devitation : 2731.674001798344
훨씬 나은 것 같습니다.
하지만 훈련 세트에 대한 점수가 검증 세트에 대한 점수보다 훨씬 낮으므로 여전히 훈련 세트에 과대적합 되어있습니다. 과대적합을 해결하는 방법은 모델을 간단히 하거나, 제한을 하거나, 더 많은 훈련 데이터를 모으는 것입니다.
그러나 랜덤 포레스트를 더 깊이 들어가기 전에 여러 종류의 머신러닝 알고리즘으로 하이퍼파라미터 조정에 너무 많은 시간을 들이지 않으면서도 다양한 모델을 시도해봐야 합니다.
c.f> 모델 저장¶
실험한 모델을 모두 저장해두면 필요할 때 쉽게 모델을 복원할 수 있습니다.
교차 검증 점수와 실제 예측값은 물론 하이퍼파라미터와 훈련된 모델 파라미터 모두 저장해야 합니다. 이렇게 하면 여러 모델의 점수와 모델이 만든 오차를 쉽게 비교할 수 있습니다. 파이썬의 pickle
패키지나 sklearn.externals.joblib
을 사용하여 사이킷런 모델을 간단하게 저장할 수 있습니다.
from sklearn.externals import joblib
joblib.dump(my_model, "my_model.pkl")
# 나중에 모델 load 하는 방법
my_model_loaded = joblib.load("my_model.pkl")
7. 모델 세부 튜닝¶
가능성 있는 모델들을 추렸다고 가정해봅시다. 이제 이 모델들을 세부 튜닝해야 합니다. 가장 단순한 방법은 만족할만한 하이퍼파라미터 조합을 찾을 때 까지 수동으로 하이퍼파라미터를 조정하는 것입니다. 그렇지만 이는 매우 지루한 작업이고, 많은 경우의 수를 탐색하기에는 시간이 부족할수도 있습니다. 이럴때 사용하는 방법들을 알아봅시다.
7.1 그리드 탐색¶
탐색하고자 하는 하이퍼파라미터와 시도해볼 값을 지정하는 방식입니다. 사이킷런에서는 GridSearchCV
로 제공하고 있습니다. 아래는 RandomForestRegressor
에 대한 최적의 하이퍼파리미터 조합을 탐색하는 방법입니다.
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestRegressor
param_grid = [
{'n_estimators' : [3, 10, 30], 'max_features' : [2, 4, 6, 8]},
{'bootstrap' : [False], 'n_estimators' : [3, 10], 'max_features' : [2, 3, 4]}
]
forest_reg = RandomForestRegressor()
grid_search = GridSearchCV(forest_reg, param_grid, cv=5,
scoring='neg_mean_squared_error',
return_train_score = True)
grid_search.fit(housing_prepared, housing_labels)
GridSearchCV(cv=5, error_score=nan, estimator=RandomForestRegressor(bootstrap=True, ccp_alpha=0.0, criterion='mse', max_depth=None, max_features='auto', max_leaf_nodes=None, max_samples=None, min_impurity_decrease=0.0, min_impurity_split=None, min_samples_leaf=1, min_samples_split=2, min_weight_fraction_leaf=0.0, n_estimators=100, n_jobs=None, oob_score=False, random_state=None, verbose=0, warm_start=False), iid='deprecated', n_jobs=None, param_grid=[{'max_features': [2, 4, 6, 8], 'n_estimators': [3, 10, 30]}, {'bootstrap': [False], 'max_features': [2, 3, 4], 'n_estimators': [3, 10]}], pre_dispatch='2*n_jobs', refit=True, return_train_score=True, scoring='neg_mean_squared_error', verbose=0)
어떤 하이퍼파라미터 값을 지정해야할지 모를 때는 연속된 10의 거듭제곱 수로 시도해보는 것도 좋습니다.
param_grid
설정에 따라 사이킷런이 먼저 첫번째 dict
안에 있는 n_estimators
와 max_features
하이퍼파라미터의 조합인 3x4=12개를 평가하고 그런 다음 두번째 dict
에 있는 하이퍼 파라미터 조합인 2x3=6개를 시도합니다. 하지만 두번째는 bootstrap
하이퍼 파라미터를 True
(기본값)가 아닌 False
로 설정했습니다.
모두 합하면 그리드 탐색이 12+6의 18개 조합을 탐색하고, 각각 다섯번 모델을 훈련시킵니다. (cv=5
) 다시 말해 전체 훈련의 횟수는 18 x 5 = 90이 됩니다!
최적의 조합은 아래와 같은 방법으로 얻을 수 있습니다.
grid_search.best_params_
{'max_features': 6, 'n_estimators': 30}
최적의 추정기에 직접 접근할 수도 있습니다.
grid_search.best_estimator_
RandomForestRegressor(bootstrap=True, ccp_alpha=0.0, criterion='mse', max_depth=None, max_features=6, max_leaf_nodes=None, max_samples=None, min_impurity_decrease=0.0, min_impurity_split=None, min_samples_leaf=1, min_samples_split=2, min_weight_fraction_leaf=0.0, n_estimators=30, n_jobs=None, oob_score=False, random_state=None, verbose=0, warm_start=False)
물론 평가 점수도 확인할 수 있습니다.
cvres = grid_search.cv_results_
for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]):
print(np.sqrt(-mean_score), params)
63572.3011238392 {'max_features': 2, 'n_estimators': 3} 55373.49416173937 {'max_features': 2, 'n_estimators': 10} 52767.49255894511 {'max_features': 2, 'n_estimators': 30} 60593.11574055512 {'max_features': 4, 'n_estimators': 3} 52816.501938027366 {'max_features': 4, 'n_estimators': 10} 50885.10758065373 {'max_features': 4, 'n_estimators': 30} 59505.76582966177 {'max_features': 6, 'n_estimators': 3} 52512.8128522099 {'max_features': 6, 'n_estimators': 10} 49882.01949943324 {'max_features': 6, 'n_estimators': 30} 58520.348971784966 {'max_features': 8, 'n_estimators': 3} 51844.706855974066 {'max_features': 8, 'n_estimators': 10} 50158.34509149532 {'max_features': 8, 'n_estimators': 30} 63197.82995802215 {'bootstrap': False, 'max_features': 2, 'n_estimators': 3} 54487.780514650854 {'bootstrap': False, 'max_features': 2, 'n_estimators': 10} 59907.10630619957 {'bootstrap': False, 'max_features': 3, 'n_estimators': 3} 52954.83925161456 {'bootstrap': False, 'max_features': 3, 'n_estimators': 10} 59086.50342822546 {'bootstrap': False, 'max_features': 4, 'n_estimators': 3} 51368.388308773116 {'bootstrap': False, 'max_features': 4, 'n_estimators': 10}
여기서는 max_features
가 8, n_estimators
가 30일때 최적의 솔루션입니다.
7.2 랜덤 탐색¶
그리드 탐색 방법은 비교적 적은 수의 조합을 탐구할 때 괜찮은 방법입니다. 하지만 하이퍼파라미터의 탐색공간이 커지면 RandomizedSearchCV
를 사용하는 편이 더 좋습니다.
RandomizedSearchCV
는 그리드서치와 거의 같은 방식으로 사용하지만 가능한 모든 조합을 시도하는 대신 각 반복마다 하이퍼파라미터에 임의의 수를 대입하여 지정한 횟수만큼 평가합니다. 이 방식의 주요 장점은 아래 두가지입니다.
- 랜덤 탐색을 1000회 반복하도록 실행하면 하이퍼파라미터마다 각기 다른 1000개의 값을 탐색합니다.
- 단순히 반복 횟수를 조절하는 것 만으로 하이퍼 파라미터 탐색에 투입할 컴퓨팅 자원을 제어할 수 있습니다.
7.4 최상의 모델과 오차 분석¶
최상의 모델을 분석하면 문제에 대한 좋은 통찰을 얻는 경우가 많습니다.
예를 들어, RandomForestRegressor
가 정확한 예측을 만들기 위한 각 특성의 상대적인 중요도를 알려줍니다.
feature_importances = grid_search.best_estimator_.feature_importances_
feature_importances
array([7.07247650e-02, 6.56659118e-02, 4.29759340e-02, 1.79332420e-02, 1.72725001e-02, 1.74693846e-02, 1.63157126e-02, 3.62337141e-01, 4.80338150e-02, 1.06574511e-01, 6.50098296e-02, 8.38570585e-03, 1.53962809e-01, 7.96963852e-05, 2.43802544e-03, 4.82101628e-03])
위는 중요도를 나타낸 것입니다.
중요도 다음에 그에 대응하는 특성 이름을 표시해보겠습니다.
extra_attribs = ["rooms_per_hhold", "pop_per_hhold", "bedrooms_per_room"]
cat_one_hot_attribs = list(encoder.categories_)
attributes = num_attribs + extra_attribs + cat_one_hot_attribs
sorted(zip(feature_importances, attributes), reverse=True)
[(0.3623371414702453, 'median_income'), (0.10657451069847343, 'pop_per_hhold'), (0.0707247650378225, 'longitude'), (0.06566591181823479, 'latitude'), (0.06500982955054917, 'bedrooms_per_room'), (0.048033814970260366, 'rooms_per_hhold'), (0.04297593395796787, 'housing_median_age'), (0.017933242015904724, 'total_rooms'), (0.017469384575063678, 'population'), (0.01727250010761098, 'total_bedrooms'), (0.016315712620324937, 'households'), (0.008385705848695356, array([0, 1, 2, 3, 4]))]
이 정보를 바탕으로 덜 중요한 특성들을 제외할 수 있습니다.
시스템이 특정한 오차를 만들었다면 왜 그런 문제가 생겼는지 이해하고 문제를 해결하는 방법이 무엇인지 찾아야 합니다.
7.5 테스트 세트로 시스템 평가하기¶
어느정도 모델을 튜닝하면 만족할만한 모델을 얻게됩니다.
그럼 이제 테스트세트에서 최종 모델을 평가할 차례입니다. 이 과정에서 특별하게 다른 점은 없습니다.
테스트세트에서 예측 변수와 레이블을 얻은 후 full_pipeline
을 사용해서 데이터를 변환하고(fit_transform()
이 아닌 transform()
을 호출해야 합니다.) 테스트 세트에서 최종 모델을 평가합니다.
final_model = grid_search.best_estimator_
X_test = strat_test_set.drop("median_house_value", axis=1)
y_test = strat_test_set['median_house_value'].copy()
X_test_prepared = full_pipeline.transform(X_test)
final_predictions = final_model.predict(X_test_prepared)
final_mse = mean_squared_error(y_test, final_predictions)
final_rmse = np.sqrt(final_mse)
final_rmse
47748.19223478248
하이퍼파라미터 튜닝을 많이 했다면 교차검증을 사용해 측정한 것보다 조금 성능이 낮은 것이 보통입니다.
이 예제에서는 성능이 낮아지진 않았지만, 이런 경우가 생기더라도 테스트세트에서 성능 수치를 좋게 하려고 하이퍼파라미터를 튜닝하려 시도해서는 안됩니다. 그렇게 향상된 성능은 새로운 데이터에 일반화되기 어렵습니다.
지금까지 따라오신 여러분들 수고 많으셨습니다!
from IPython.core.display import display, HTML
display(HTML("<style> .container{width:90% !important;}</style>"))
'머신러닝 꿈나무 > Hands-on!' 카테고리의 다른 글
[Pyspark] Pyspark의 여러가지 함수들 (0) | 2021.11.12 |
---|---|
[Pyspark] udf에서 2개 이상의 multiple column 리턴하기 (0) | 2021.11.04 |
[핸즈온 머신러닝2] Chaper 5. 서포트 벡터 머신 (0) | 2021.01.24 |
[핸즈온 머신러닝2] Chapter 3. 분류 (0) | 2021.01.15 |
댓글
이 글 공유하기
다른 글
-
[Pyspark] Pyspark의 여러가지 함수들
[Pyspark] Pyspark의 여러가지 함수들
2021.11.12 -
[Pyspark] udf에서 2개 이상의 multiple column 리턴하기
[Pyspark] udf에서 2개 이상의 multiple column 리턴하기
2021.11.04 -
[핸즈온 머신러닝2] Chaper 5. 서포트 벡터 머신
[핸즈온 머신러닝2] Chaper 5. 서포트 벡터 머신
2021.01.24 -
[핸즈온 머신러닝2] Chapter 3. 분류
[핸즈온 머신러닝2] Chapter 3. 분류
2021.01.15