2. 머신러닝 프로젝트 처음부터 끝까지¶
- 큰 그림을 봅니다.
- 데이터를 구합니다.
- 데이터로부터 통찰을 얻기 위해 탐색하고 시각화합니다.
- 머신러닝 알고리즘을 위해 데이터를 준비합니다.
- 모델을 선택하고 훈련시킵니다.
- 모델을 상세하게 조정합니다.
- 솔루션을 제시합니다.
- 시스템을 론칭하고 모니터링하고 유지 보수합니다.
2.2 큰 그림 보기¶
2.2.1 문제 정의¶
Q. 캘리포니아 인구조사 데이터를 사용하여 주택 가격 모델을 만드는 일.
- 현재 솔루션이 어떻게 구성되어 있는가? = 참고 성능으로 사용 가능
- 지도 학습 / 비지도 학습 / 강화 학습 중 무엇? (분류 / 회귀 / 다른 무엇)
- 지도 학습 – 다변량 회귀
- 배치 학습 / 온라인 학습
- 배치 학습 – 시스템으로 들어오는 데이터에 연속적인 흐름이 없고, 빠르게 변하는 데이터에 적응하지 않아도 되고, 데이터가 메모리에 들어갈 만큼 충분히 작다.
*파이프라인이란?
- 데이터 처리 컴포넌트들이 연속되어 있는 것
2.2.2 성능 측정 지표 선택¶
회귀 문제의 전형적인 성능 지표 : 평균 제곱근 오차 (RMSE)
이상치로 보이는 구역이 많을 경우 : 평균 절대 오차 (MAE)
2.2.3 가정 검사¶
- 지금까지 만든 가정을 나열하고 검사해보기
- 회귀가 맞는지, 분류가 맞는지 : 나중에 큰 문제가 될 수 있음. 가장 중요함.
2.3 데이터 가져오기¶
2.3.2. 데이터 다운로드¶
import os
import tarfile
from six.moves import urllib
DOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml/master/"
HOUSING_PATH = "datasets/housing"
HOUSING_URL = DOWNLOAD_ROOT + HOUSING_PATH + "/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()
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)
2.3.3. 데이터 구조 훑어보기¶
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 |
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
housing['ocean_proximity'].value_counts()
<1H OCEAN 9136 INLAND 6551 NEAR OCEAN 2658 NEAR BAY 2290 ISLAND 5 Name: ocean_proximity, dtype: int64
ocean_proximimty 컬럼의 object특성이 범주형 변수임을 알 수 있다.
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 |
수치형 변수들의 분포를 살펴보기 위해 describe()함수를 실행하였고, 이를 더 시각적으로 표현하기 위해 그래프로 나타내 본다.
%matplotlib inline
import matplotlib.pyplot as plt
housing.hist(bins=50, figsize=(20,15))
plt.show()
median_income의 경우 US달러로 표현되어있지 않은 것으로 보인다. 데이터를 취합한 팀에게 문의하여 데이터가 어떻게 계산된건지 파악해야 한다.
housing_median_age와 median house value는 최댓값에 몰려있는 것으로 보아 최대/최소를 한정했을 수 있다.
median house value는 레이블 값이기 때문에 예측이 정확하게 되지 않을 수도 있다.
따라서, 클라이언트 팀과 함께 검토해봐야 한다.
$500,000를 넘어가더라도 정확한 예측값이 필요하다면, 선택할 수 있는 방법은 두 가지다.
- 한곗값 밖의 구역에 대한 정확한 레이블을 구한다.
- 훈련 세트에서 이 구역을 제거하고, 테스트 세트에서도 $500,000가 넘는 값에 대한 예측은 평과 결과가 나쁠 것이라 판단해 이 구역을 제거한다.
2.3.4 테스트 세트 만들기¶
데이터 스누핑 편향 :
테스트 세트에서 겉으로 드러난 어떤 패턴에 속아 특정 머신러닝 모델을 선택하게 된다.
이 테스트 세트로 일반화 오차를 추정하면 매우 낙관적인 추정이 되며 시스템을 론칭했을 때 기대한 성능이 나오지 않을 것이다.
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), "train +", len(test_set), "test")
16512 train + 4128 test
위 코드는 계속해서 다른 테스트 세트가 생성된다.
이를 해결하기 위해,
- 처음 실행에서 테스트 세트를 저장하고 다음 번 실행에서 불러들인다.
- np.random.permutation() 호출 전에 np.random.seed(42)를 사용하여 난수 발생기의 초깃값을 지정한다.
- 샘플의 식별자를 사용하여 테스트 세트로 보낼지 정함(샘플이 고유하고 변경 불가능한 식별자를 가지고 있다고 가정)
from zlib import crc32
def test_set_check(identifier, test_ratio):
return crc32(np.int64(identifier)) & 0xffffffff < test_ratio * 2**32
def split_train_test_by_id(data, test_ratio, id_column):
ids = data[id_column]
in_test_set = ids.apply(lambda id_: test_set_check(id_, test_ratio))
return data.loc[~in_test_set], data.loc[in_test_set]
housing_with_id = housing.reset_index()
train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "index")
housing_with_id["id"] = housing["longitude"] * 1000 + housing["latitude"]
train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "id")
행의 인덱스를 고유 식별자로 사용 = 구역의 위도와 경도는 변경되지 않으므로, id로 활용.
from sklearn.model_selection import train_test_split
train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)
sklearn의 자체 함수로 데이터셋을 여러 서브셋으로 나눌 수 있다. (무작위 샘플링)
housing["income_cat"] = np.ceil(housing["median_income"] / 1.5)
housing["income_cat"].where(housing["income_cat"] < 5, 5.0, inplace=True)
전체 모수를 고르게 잘 대표할 수 있도록 계층적 샘플링을 진행한다. 전문가가 중간 소득이 중간 주택 가격을 예측하는 데 매우 중요하다고 하였으므로, 테스트 세트가 전체 데이터셋에 있는 여러 소득 카테고리를 잘 대표해야한다. income_cat특성을 생성한다.
housing["income_cat"].hist()
<matplotlib.axes._subplots.AxesSubplot at 0x7f90d7a5f610>
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
for set_ in (strat_train_set, strat_test_set):
set_.drop("income_cat", axis=1, inplace=True)
계층적 샘플링을 진행하였다면, 사용된 income_cat은 더 이상 필요 없으므로, 특성을 삭제하여 데이터를 원래 상태로 되돌린다.
housing = strat_train_set.copy()
훈련 세트에 대한 탐색을 하기에 앞서, 훈련 세트를 손상시키지 않기 위해 복사본을 만들어 사용한다.
housing.plot(kind="scatter", x="longitude", y="latitude")
<matplotlib.axes._subplots.AxesSubplot at 0x7f90d7b6a8e0>
지리 정보(위도와 경도)를 활용하여 산점도를 만들어 데이터를 시각화 해본다.
housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.1)
<matplotlib.axes._subplots.AxesSubplot at 0x7f90d7706d00>
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()
<matplotlib.legend.Legend at 0x7f90d77b1b50>
- 원의 반지름(s) = 구역의 인구
- 색깔(c) = 가격
- 투명도(alpha) = 밀집된 정도
- 색깔 범위(cmap) = 파랑(낮은 가격) ~ 빨강(높은 가격) 범위 jet
2.4.2 상관관계 조사¶
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
label값인 median_house_value와의 상관관계를 살펴본다.
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 0x7f90d786b640>, <matplotlib.axes._subplots.AxesSubplot object at 0x7f90d776a0d0>, <matplotlib.axes._subplots.AxesSubplot object at 0x7f90d79d3df0>, <matplotlib.axes._subplots.AxesSubplot object at 0x7f90d78f8a60>], [<matplotlib.axes._subplots.AxesSubplot object at 0x7f90d73098e0>, <matplotlib.axes._subplots.AxesSubplot object at 0x7f90d7480610>, <matplotlib.axes._subplots.AxesSubplot object at 0x7f90d74803d0>, <matplotlib.axes._subplots.AxesSubplot object at 0x7f90d729c640>], [<matplotlib.axes._subplots.AxesSubplot object at 0x7f90d7314580>, <matplotlib.axes._subplots.AxesSubplot object at 0x7f90d781c130>, <matplotlib.axes._subplots.AxesSubplot object at 0x7f90d76fc520>, <matplotlib.axes._subplots.AxesSubplot object at 0x7f90d785f970>], [<matplotlib.axes._subplots.AxesSubplot object at 0x7f90d797cdc0>, <matplotlib.axes._subplots.AxesSubplot object at 0x7f90d7912250>, <matplotlib.axes._subplots.AxesSubplot object at 0x7f90d74b36a0>, <matplotlib.axes._subplots.AxesSubplot object at 0x7f90d7757af0>]], dtype=object)
housing.plot(kind="scatter", x="median_income", y="median_house_value", alpha=0.1)
<matplotlib.axes._subplots.AxesSubplot at 0x7f90d7db2670>
가장 상관관계가 높은 median_income의 산점도를 자세히 보면, 500000과 400000등 이상한 형태의 데이터 분포가 보인다.
2.4.3 특성 조합으로 실험¶
housing["rooms_per_household"] = housing["total_rooms"]/housing["households"]
housing["bedrooms_per_room"] = housing["total_bedrooms"]/housing["total_rooms"]
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 longitude -0.047432 latitude -0.142724 bedrooms_per_room -0.259984 Name: median_house_value, dtype: float64
새롭게 만든 특성 중 bedrooms_per_room 특성은 침대/방의 비율이 낮은 집이 더 비싼 경향이 있다는 것을 보여준다.
하지만 이 탐색은 완벽하지는 않다. 시작을 잘해서 빨리 통찰을 얻는 것이 처음 프로토타입을 잘 만드는데 도움이 될 것이다.
이는 반복적인 과정이며, 프로토타입을 만들고 실행한 후 그 결과를 분석해서 더 많은 통찰을 얻고 다시 이 탐색 단계로 돌아오게 된다. (반복적)
2.5 머신러닝 알고리즘을 위한 데이터 준비¶
housing = strat_train_set.drop("median_house_value", axis=1)
housing_labels = strat_train_set["median_house_value"].copy()
예측 변수와 레이블을 분리한다.
2.5.1 데이터 정제¶
total_bedrooms특성의 결측값 처리
- 해당 구역을 제거한다.
- 전체 특성을 삭제한다.
- 어떤 값으로 채운다(0, 평균, 중간값 등)
# 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)
옵션 3을 선택할 때는 중간값은 꼭 저장해서 적용해야 한다.
from sklearn.impute import SimpleImputer
imputer = SimpleImputer(strategy="median")
housing_num = housing.drop("ocean_proximity", axis=1)
imputer.fit(housing_num)
SimpleImputer(strategy='median')
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])
oceanproximity(범주형)를 제외한 컬럼들을 바탕으로 imputer객체의 fit()메서드를 사용해 훈련 데이터에 적용하여 statistics 속성에 저장한다.
total_bedrooms특성에만 누락된 값이 있지만, 새로운 서비스에서 어떤 데이터가 누락될지 확신할 수 없기때문에, 모든 수치형 특성에 imputer를 적용하는 것이 바람직하다.
X = imputer.transform(housing_num)
housing_tr = pd.DataFrame(X, columns=housing_num.columns, index=list(housing.index.values))
housing_tr
longitude | latitude | housing_median_age | total_rooms | total_bedrooms | population | households | median_income | |
---|---|---|---|---|---|---|---|---|
17606 | -121.89 | 37.29 | 38.0 | 1568.0 | 351.0 | 710.0 | 339.0 | 2.7042 |
18632 | -121.93 | 37.05 | 14.0 | 679.0 | 108.0 | 306.0 | 113.0 | 6.4214 |
14650 | -117.20 | 32.77 | 31.0 | 1952.0 | 471.0 | 936.0 | 462.0 | 2.8621 |
3230 | -119.61 | 36.31 | 25.0 | 1847.0 | 371.0 | 1460.0 | 353.0 | 1.8839 |
3555 | -118.59 | 34.23 | 17.0 | 6592.0 | 1525.0 | 4459.0 | 1463.0 | 3.0347 |
... | ... | ... | ... | ... | ... | ... | ... | ... |
6563 | -118.13 | 34.20 | 46.0 | 1271.0 | 236.0 | 573.0 | 210.0 | 4.9312 |
12053 | -117.56 | 33.88 | 40.0 | 1196.0 | 294.0 | 1052.0 | 258.0 | 2.0682 |
13908 | -116.40 | 34.09 | 9.0 | 4855.0 | 872.0 | 2098.0 | 765.0 | 3.2723 |
11159 | -118.01 | 33.82 | 31.0 | 1960.0 | 380.0 | 1356.0 | 356.0 | 4.0625 |
15775 | -122.45 | 37.77 | 52.0 | 3095.0 | 682.0 | 1269.0 | 639.0 | 3.5750 |
16512 rows × 8 columns
사이킷런의 API¶
- 일관성 : 모든 객체가 일관되고 단순한 인터페이스를 공유
- 추정기 : 데이터셋을 기반으로 일련의 모델 파라미터들을 추정하는 객체
- ex) imputer - 추정 자체는 fit() 메서드에 의해 수행
- 변환기 : 데이터셋을 변환하는 추정기를 변환기라고 함
- ex) imputer - 변환은 데이터셋을 매개변수로 전달받은 transform() 메서드에 의해 수행, 모든 변환기는 fit()과 transform()을 연달아 호출하는 것과 동일한 fit_transform() 메서드도 가지고 있다.
- 예측기 : 일부 추정기는 주어진 데이터셋에 대해 예측을 만들 수 있음
- ex) : LinearRegression 모델이 예측기 - predict() 메서드로 예측값을 반환, score()메서드로 품질을 측정
- 추정기 : 데이터셋을 기반으로 일련의 모델 파라미터들을 추정하는 객체
- 검사 가능 :
- 모든 추정기의 하이퍼파라미터는 공개 인스턴스 변수로 직접 접근할 수 있음,
- 모든 추정기의 학습된 모델 파라미터도 접미사로 밑줄을 붙여서 공개 인스턴스 변수로 제공됨
- ex) imputer.strategy , imputer.statistics_
- 클래스 남용 방지 : 데이터셋을 별도의 클래스가 아니라 넘파이 배열이나 사이파이 희소 행렬로 표현
- 조합성 : 기존의 구성요소를 최대한 재사용, 여러 변환기를 연결한 다음 마지막에 추정기 하나를 배치한 Pipeline 추정기를 쉽게 만들 수 있음
- 합리적인 기본값 : 시스템을 빠르게 만들 수 있도록 대부분의 매개변수에 합리적인 기본값을 저장해두었음
2.5.2 텍스트와 범주형 특성 다루기¶
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
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')
대부분의 머신러닝 알고리즘은 숫자형을 다루므로 이 카테고리를 텍스트에서 숫자로 바꾸어 준다.
이를 위해 각 카테고리를 다른 정숫값으로 매핑해주는 판다스의 factorize() 메서드를 사용한다.
from sklearn.preprocessing import OneHotEncoder
encoder = OneHotEncoder()
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>
위 단순 범주형의 숫자화는 수치의 순서화 문제가 발생할 수 있기 때문에,
각각 카테고리를 원-핫 인코딩을 진행해주어야 한다.
type(housing_cat_1hot)
scipy.sparse.csr.csr_matrix
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.]])
fit_transform() 메서드는 2차원 배열을 넣어줘야하기 때문에 넘파이 배열로 변환해준다.
import sklearn
print(sklearn.__version__)
0.24.2
# pip install scikit-learn==0.24.2
Collecting scikit-learn==0.24.2 Downloading scikit_learn-0.24.2-cp38-cp38-macosx_10_13_x86_64.whl (7.2 MB) |████████████████████████████████| 7.2 MB 1.1 MB/s eta 0:00:01 Requirement already satisfied: numpy>=1.13.3 in ./opt/anaconda3/lib/python3.8/site-packages (from scikit-learn==0.24.2) (1.19.5) Requirement already satisfied: threadpoolctl>=2.0.0 in ./opt/anaconda3/lib/python3.8/site-packages (from scikit-learn==0.24.2) (2.1.0) Requirement already satisfied: scipy>=0.19.1 in ./opt/anaconda3/lib/python3.8/site-packages (from scikit-learn==0.24.2) (1.5.0) Requirement already satisfied: joblib>=0.11 in ./opt/anaconda3/lib/python3.8/site-packages (from scikit-learn==0.24.2) (0.16.0) Installing collected packages: scikit-learn Attempting uninstall: scikit-learn Found existing installation: scikit-learn 0.23.1 Uninstalling scikit-learn-0.23.1: Successfully uninstalled scikit-learn-0.23.1 Successfully installed scikit-learn-0.24.2 Note: you may need to restart the kernel to use updated packages.
# from sklearn.preprocessing import CategoricalEncoder # sklearn 0.20버전에서만 사용가능
# cat_encoder = CategoricalEncoder()
# housing_cat_reshaped = housing_cat.values.reshape(-1, 1)
# housing_cat_1hot = cat_encoder.fit_transform(housing_cat_reshaped)
# housing_cat_1hot
간단하게 CategoricalEncoder를 사용할 수도 있지만, sklearn version 0.20에서 제공하므로, OneHotEncoder를 이용하도록 한다.
# from sklearn.preprocessing import OneHotEncoder
# cat_encoder = OneHotEncoder(categories='auto')
# housing_cat_reshaped = housing_cat.values.reshape(-1, 1)
# housing_cat_1hot = cat_encoder.fit_transform(housing_cat_reshaped)
# housing_cat_1hot
# housing_cat_1hot.toarray()
2.5.3 나만의 변환기¶
사이킷런이 유용한 변환기를 많이 제공하지만 특별한 정제 작업이나 어떤 특성들을 조합하는 등의 작업을 위해 자신만의 변환기를 만들어야할 때가 있다. 내가 만든 변환기를 (파이프라인과 같은) 사이킷런의 기능과 매끄럽게 연동하고 싶을 것이다. fit(), transform(), fit_transform() 메서드를 구현한 파이썬 클래스를 만들면 된다.
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% 확신이 없는 모든 데이터 준비 단계에 대해 하이퍼파라미터를 추가할 수 있습니다. 이런 데이터 준비 단계를 자동화할수록 더 많은 조합을 자동으로 시도해볼 수 있고 최상의 조합을 찾을 가능성을 매우 높여줍니다(그리고 시간도 많이 절약됩니다)
2.5.4 특성 스케일링¶
데이터에 적용할 가장 중요한 변환 중 하나가 특성 스케일링(feature scaling)이다.
모든 특성의 범위를 같도록 만들어주는 방법으로 min-max 스케일링과 표준화(standardization)이 널리 사용된다.
min-max 스케일링
- 데이터에서 최솟값을 뺀 후 최댓값과 최솟값의 차이로 나눈다.
- 0~1 범위 정규화(normalization)
- 사이킷런 MinMaxScaler 변환기
표준화(standardization)
- 평균을 뺀 후 표준편차로 나누어 결과 분포의 분산이 1이 되도록 한다.
- 상한과 하한이 없어 어떤 알고리즘에서는 문제가 될 수 있다. (신경망 : 0~1 기대)
- 이상치에 영향을 덜 받는다.
- 사이킷런 StandardScaler 변환기
훈련 데이터에서만 fit()메서드를 적용해야 하고, 훈련 세트와 테스트 세트에 대해 transform()메서드를 사용한다.
2.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()),
])
Pipeline은 연속된 단계를 나타내는 이름/추정기 쌍의 목록으로 입력을 받는다.
마지막 단계에는 변환기와 추정기를 모두 사용할 수 있고 그 외에는 모두 변환기여야 한다.
파이프라인 객체는 마지막 추정기와 동일한 메서드를 제공한다.
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는 나머지는 버리고 필요한 특성만을 선택하여 데이터프레임을 넘파이 배열로 바꾸는 식으로 데이터를 변환한다.
이를 이용해 데이터프레임을 받아 수치형만 다루는 파이프라인을 손쉽게 만들 수 있다.
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")),
('cat_encoder', OneHotEncoder(categories='auto')),
])
이제 생성된 두 파이프라인을 FeatureUnion을 통해 합쳐준다.
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
<16512x16 sparse matrix of type '<class 'numpy.float64'>' with 198144 stored elements in Compressed Sparse Row format>
housing_prepared.toarray()
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)
2.6 모델 선택과 훈련¶
2.6.1 훈련 세트에서 훈련하고 평가하기¶
from sklearn.linear_model import LinearRegression
lin_reg = LinearRegression()
lin_reg.fit(housing_prepared, housing_labels)
LinearRegression()
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.60458902 317768.8068993 210956.43338983 59218.98914406 189747.55850938] 레이블 [286600.0, 340600.0, 196900.0, 46300.0, 254500.0]
일부 데이터에 대해 회귀분석을 진행하고, 예측 오차를 측정했다. 모델이 과소적합되어있기 때문에 정확한 측정은 아니다.
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
회귀분석을 진행하고, 예측 오차를 측정했다. 모델이 과소적합되어있기 때문에 정확한 측정은 아니다.
from sklearn.tree import DecisionTreeRegressor
tree_reg = DecisionTreeRegressor()
tree_reg.fit(housing_prepared, housing_labels)
DecisionTreeRegressor()
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으로 과대적합이 된 것으로 보인다.
2.6.2 교차 검정을 사용한 평가¶
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)
트리 모델 기반으로 회귀 분석을 진행하였다.
사이킷런의 교차 검증 기능을 사용한다. 사이킷런의 교차 검증 기능은 scoring 매개변수에 (낮을수록 좋은) 비용 함수가 아니가 (클수록 좋은) 효용 함수를 기대합니다. 그래서 평균 제곱 오차(MSE)의 반댓값(즉, 음숫값)을 계산하는 neg_mean_squared_error 함수를 사용합니다. 이런 이유로 앞선 코드에서 제곱근을 계산하기 전에 -scores로 부호를 바꾼 것입니다.
- 회귀 모델에서 scoring 매개변수의 기본값은 0~1 사이의 값을 가지는 r2_score가 사용된다.
def display_scores(scores):
print("Scores:", scores)
print("Mean:", scores.mean())
print("Strandard deviation", scores.std())
display_scores(tree_rmse_scores)
Scores: [68983.46389768 68035.03930543 72177.14656599 67982.20638533 71392.68390392 75195.9673047 70836.33522371 71278.06391306 76778.50437553 70678.11751523] Mean: 71333.75283905835 Strandard deviation 2712.8476119444063
교차검증은 성능측정에는 유리하지만, 비용이 비싸서 언제나 쓸 수 있는 것은 아니다.
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.7384065 66960.1180739 70347.95253496 74739.57052051 68031.13390131 71193.84184183 64969.63056998 68281.61137362 71552.91570307 67665.10087687] Mean: 69052.4613802548 Strandard deviation 2731.674007630782
선형 회귀 모델과 비교했을 때, 결정 트리 모델이 과대적합되어 성능이 더 나쁘다는 것을 알 수 있다.
from sklearn.ensemble import RandomForestRegressor
forest_reg = RandomForestRegressor()
forest_reg.fit(housing_prepared, housing_labels)
RandomForestRegressor()
housing_predictions = forest_reg.predict(housing_prepared)
forest_mse = mean_squared_error(housing_labels, housing_predictions)
forest_rmse = np.sqrt(forest_mse)
forest_rmse
18716.857110739296
forest_scores = cross_val_score(forest_reg, housing_prepared, housing_labels,
scoring="neg_mean_squared_error", cv=10)
forest_rmse_scores = np.sqrt(-forest_scores)
display_scores(forest_rmse_scores)
Scores: [49379.90309076 47649.35306478 50043.74244753 52008.87139895 49356.55284207 52853.15731105 49011.32219583 48246.88963054 52745.12941753 50114.13558696] Mean: 50140.905698600305 Strandard deviation 1729.2532445246732
랜덤 포레스트는 시간이 오래걸리지만, 성능면에서는 훌륭함을 보여준다. 하지만 훈련 세트에 대한 점수가 검증 세트에 대한 점수보다 훨씬 낮으므로 이 모델도 여전히 훈련 세트에 과대적합되어 있다. 과대적합을 해결하는 방법은 1. 모델을 간단히 하거나, 2. 제한을 하거나(규제), 3. 더 많은 훈련 데이터를 모으는 것이다. 하지만 랜덤 포레스트를 더 깊이 들어가기 전에, 여러 종류의 머신러닝 알고리즘으로 하이퍼파라미터 조정에 너무 많은 시간을 들이지 않으면서 다양한 모델(다양한 커널의 서포트 벡터 머신, 신경망 등)을 시도해봐야 한다. 가능성 있는 2~5개 정도의 모델을 선정하는 것이 목적이다.
# from sklearn.externals import joblib
# joblib.dump(my_model, "my_model.pkl")
# my_model_loaded = joblib.load("my_model.pkl")
- 실험한 모델을 모두 저장해두면 필요할 때 쉽게 모델을 복원할 수 있습니다. 교차 검증 점수와 실제 예측값은 물론 하이퍼파라미터와 훈련된 모델 파라미터 모두 저장해야 합니다. 이렇게 하면 여러 모델의 점수와 모델이 만든 오차를 쉽게 비교할 수 있습니다. 파이썬의 pickle 패키지나 (넘파이 배열을 저장하는 데 아주 효율적인) sklearn.externals.joblib을 사용하여 사이킷런 모델을 간단하게 저장할 수 있습니다.
2.7 모델 세부 튜닝¶
가능성있는 모델들을 추렸다고 가정하자.
2.7.1 그리드 탐색¶
from sklearn.model_selection import GridSearchCV
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, estimator=RandomForestRegressor(), param_grid=[{'max_features': [2, 4, 6, 8], 'n_estimators': [3, 10, 30]}, {'bootstrap': [False], 'max_features': [2, 3, 4], 'n_estimators': [3, 10]}], return_train_score=True, scoring='neg_mean_squared_error')
가장 단순한 방법으로, 만족할 만한 하이퍼파라미터 조합을 찾을 때까지 수동으로 하이퍼파라미터를 조정하는 것인데, 이는 매우 지루한 작업이고 많은 경우의 수를 탐색하기에는 시간이 부족할 수 있다. 이를 돕는 것이 사이킷런의 GridSearchCV이다.
첫번째 dict에 있는 조합 12개 + 두번째 dict에 있는 조합 6개를 합쳐서 총 18의 조합을 탐색한하고, 각각 다섯 번 모델을 훈련시킨다. (5겹 교차검증을 사용하기 때문)
grid_search.best_params_
{'max_features': 8, 'n_estimators': 30}
총 90회 훈련해야하기 때문에 시간이 꽤 오래걸린다.
위 결과 8과 30이 나왔는데, 이는 탐색 범위의 최댓값이기 때문에 점수가 더 향상될 수 있으므로, 더 큰 값으로 다시 검색해봐야 한다.
grid_search.best_estimator_
RandomForestRegressor(max_features=8, n_estimators=30)
최적의 추정기에 직접 접근할 수도 있다.
GridSearchCV가 (기본값인) refit=True로 초기화되었다면 교차 검증으로 최적의 추정기를 찾은 다음 전체 훈련 세트로 다시 훈련시킨다. 일반적으로 데이터가 많을수록 성능이 향상되므로 좋은 방법이다.
cvres = grid_search.cv_results_
for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]):
print(np.sqrt(-mean_score), params)
63505.953131453134 {'max_features': 2, 'n_estimators': 3} 55238.32008888662 {'max_features': 2, 'n_estimators': 10} 52997.64528282196 {'max_features': 2, 'n_estimators': 30} 59806.93370027508 {'max_features': 4, 'n_estimators': 3} 53112.688114062286 {'max_features': 4, 'n_estimators': 10} 50476.1425472497 {'max_features': 4, 'n_estimators': 30} 58557.79289918644 {'max_features': 6, 'n_estimators': 3} 51584.719911805856 {'max_features': 6, 'n_estimators': 10} 50369.48637193193 {'max_features': 6, 'n_estimators': 30} 58728.718764791396 {'max_features': 8, 'n_estimators': 3} 52293.26725206924 {'max_features': 8, 'n_estimators': 10} 50078.668364602476 {'max_features': 8, 'n_estimators': 30} 63698.20206406498 {'bootstrap': False, 'max_features': 2, 'n_estimators': 3} 54617.052928241494 {'bootstrap': False, 'max_features': 2, 'n_estimators': 10} 61022.53229968242 {'bootstrap': False, 'max_features': 3, 'n_estimators': 3} 52916.113212730896 {'bootstrap': False, 'max_features': 3, 'n_estimators': 10} 58316.55371289239 {'bootstrap': False, 'max_features': 4, 'n_estimators': 3} 51665.66073442937 {'bootstrap': False, 'max_features': 4, 'n_estimators': 10}
평가 점수도 확인해볼 수 있다.
데이터 준비 단계를 하나의 하이퍼파라미터처럼 다룰 수도 있다. 예를 들면 그리드 탐색이 확실하지 않은 특성을 추가할지 말지 자동으로 정할 수 있습니다. (예를 들어 CombinedAttributes 변환기의 add_bedrooms_per_room 하이퍼파라미터를 사용하여 특성을 추가할지 결정합니다.) 비슷하게 이상치가 값이 빈 특성을 다루거나 특성 선택 등을 자동으로 처리하는 데 그리드 탐색을 사용합니다.
2.7.2 랜덤 탐색¶
그리드 탐색방법은 비교적 적은 수의 조합을 탐구할 때 괜찮다. 하지만 하이퍼파라미터 탐색 공간이 커지면 RandomizedSearchCV를 사용하는 것이 좋다. 모든 조합을 시도하는 대신 각 반복마다 하이퍼파라미터에 임의의 수를 대입하여 지정한 횟수만큼 평가하는 방식이다.
이 방식의 주요 장점은 다음 두 가지 이다.
- 랜덤 탐색을 1,000회 반복하도록 실행하면 하이퍼파라미터마다 각기 다른 1,000개의 값을 탐색한다.(그리드 탐색은 몇 개의 값만 탐색한다.)
- 단순히 반복 횟수를 조절하는 것 만으로 하이퍼파라미터 탐색에 투입한 컴퓨팅 자원을 제어할 수 있다.
2.7.3 앙상블 방법¶
모델을 세밀하게 튜닝하는 또 다른 방법은 최상의 모델을 연결해보는 것이다. (결정 트리의 앙상블인 랜덤 포레스트가 결정 트리 하나보다 더 성능이 좋은 것처럼)
2.7.4 최상의 모델과 오차 분석¶
feature_importances = grid_search.best_estimator_.feature_importances_
feature_importances
array([7.05078228e-02, 6.15657973e-02, 4.52529518e-02, 1.57663099e-02, 1.48345128e-02, 1.53809851e-02, 1.43623705e-02, 3.68203743e-01, 4.06542697e-02, 1.13338168e-01, 6.85368185e-02, 7.50527920e-03, 1.58675504e-01, 6.50752004e-05, 2.62190701e-03, 2.72848462e-03])
extra_attribs = ["rooms_per_hhold", "pop_per_hhold", "bedrooms_per_room"]
cat_one_hot_attribs = list(encoder.categories_[0])
attributes = num_attribs + extra_attribs + cat_one_hot_attribs
sorted(zip(feature_importances, attributes), reverse=True)
[(0.36820374300431064, 'median_income'), (0.15867550449148063, 1), (0.11333816815979839, 'pop_per_hhold'), (0.07050782279253998, 'longitude'), (0.06853681847137962, 'bedrooms_per_room'), (0.061565797281500835, 'latitude'), (0.04525295179226727, 'housing_median_age'), (0.040654269659206176, 'rooms_per_hhold'), (0.015766309911249977, 'total_rooms'), (0.015380985098621328, 'population'), (0.014834512798307033, 'total_bedrooms'), (0.014362370513138729, 'households'), (0.007505279198704017, 0), (0.0027284846219040107, 4), (0.002621907005181797, 3), (6.50752004096158e-05, 2)]
위 코드의 결과가 책과 다르지만, 위 정보를 바탕으로 덜 중요한 특성들을 제외할 수 있다 (예를 덜어 ocean_proximity카테고리 중 하나만 실제로 유용하므로 다른 카테고리는 제외할 수 있다.([0,4,3,2])
2.7.5 테스트 세트로 시스템 평가하기¶
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
47960.12448284642
어느 정도 모델을 튜닝하면 마침내 만족할 만한 모델을 얻게 된다. 이제 테스트 세트에서 최종 모델을 평가한다. 테스트 세트에서 예측 변수와 레이블을 얻은 후, full_pipeline을 사용해 데이터를 변환하고(fit_transform()이 아니라 transform()을 호출해야 한다), 테스트 세트에서 최종 모델을 평가한다.
하이퍼파라미터 튜닝을 많이 했다면 교차 검증을 사용해 측정한 것보다 조금 성능이 낮은 것이 보통이다. 이는 검증된 세트에서 최적화된 것이기 때문에 높은 일반화를 기대해서는 안되기 때문이고, 만족해야 한다.
2.8 론칭, 모니터링, 그리고 시스템 유지 보수¶
2.10 연습문제¶
1. 서포트 벡터 머신 회귀(sklearn.svm.SVR)를 kernel="linear"(하이퍼파라미터 C를 바꿔가며)나 kernel="rbf"(하이퍼파라미터 C와 gamma를 바꿔가며) 등의 다양한 하이퍼파라미터 설정으로 시도해보세요. 지금은 이 하이퍼파라미터가 무엇을 의미하는지 너무 신경 쓰지 마세요. 최상의 SVR모델은 무엇인가요?¶
from sklearn.svm import SVR
svm_reg = SVR
2. GridSearchCV를 RandomizedSearchCV로 바꿔보세요.¶
3. 가장 중요한 특성을 선택하는 변환기를 준비 파이프라인에 추가해보세요.¶
4. 전체 데이터 준비 과정과 최종 예측을 하나의 파이프라인으로 만들어보세요.¶
5. GridSearchCV를 사용해 준비 단계의 옵션을 자동으로 탐색해보세요.¶
'Data Analysis > Hands On ML' 카테고리의 다른 글
[Hands On ML] 6. 결정 트리 (0) | 2021.11.04 |
---|---|
[Hands On ML] 5. 서포트 벡터 머신 (0) | 2021.11.04 |
[Hands On ML] 4. 모델 훈련 (0) | 2021.11.04 |
[Hands On ML] 3. 분류 (0) | 2021.11.04 |
[Hands On ML] 1. 한눈에 보는 머신러닝 (0) | 2021.11.04 |