#!/usr/bin/env python # coding: utf-8 # In[1]: get_ipython().run_line_magic('load_ext', 'watermark') get_ipython().run_line_magic('watermark', '-v -p numpy,scipy,sklearn,pandas,matplotlib') # **2장 – 머신러닝 프로젝트의 처음부터 끝까지** # # *머신러닝 주택 회사에 오신 것을 환영합니다! 여러분이 해야 할 일은 캘리포니아 인구조사 데이터를 사용해 이 지역의 주택 가격 모델을 만드는 것입니다.* # # *이 노트북은 2장에 있는 모든 샘플 코드와 연습문제 해답을 가지고 있습니다.* # **노트**: 이 주피터 노트북의 결과가 책에 있는 것과 조금 다를 수 있습니다. 대부분은 훈련 알고리즘들이 가지고 있는 무작위성 때문입니다. 가능하면 노트북의 결과를 동일하게 유지하려고 하지만 모든 플랫폼에서 동일한 출력을 낸다고 보장하긴 어렵습니다. 어떤 데이터 구조(가령 딕셔너리)는 아이템의 순서가 일정하지 않습니다. 마지막으로 몇 가지 사소한 버그 수정(해당 부분에 설명을 추가했습니다) 때문에 결과가 조금 달라졌습니다. 하지만 책에서 제시한 설명은 유효합니다. # # 설정 # 파이썬 2와 3을 모두 지원합니다. 공통 모듈을 임포트하고 맷플롯립 그림이 노트북 안에 포함되도록 설정하고 생성한 그림을 저장하기 위한 함수를 준비합니다: # In[2]: # 파이썬 2와 파이썬 3 지원 from __future__ import division, print_function, unicode_literals # 공통 import numpy as np import os # 일관된 출력을 위해 유사난수 초기화 np.random.seed(42) # 맷플롯립 설정 get_ipython().run_line_magic('matplotlib', 'inline') import matplotlib import matplotlib.pyplot as plt plt.rcParams['axes.labelsize'] = 14 plt.rcParams['xtick.labelsize'] = 12 plt.rcParams['ytick.labelsize'] = 12 # 한글출력 matplotlib.rc('font', family='NanumBarunGothic') plt.rcParams['axes.unicode_minus'] = False # 그림을 저장할 폴드 PROJECT_ROOT_DIR = "." CHAPTER_ID = "end_to_end_project" IMAGES_PATH = os.path.join(PROJECT_ROOT_DIR, "images", CHAPTER_ID) def save_fig(fig_id, tight_layout=True, fig_extension="png", resolution=300): path = os.path.join(IMAGES_PATH, fig_id + "." + fig_extension) if tight_layout: plt.tight_layout() plt.savefig(path, format=fig_extension, dpi=resolution) # # 데이터 다운로드 # In[3]: 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() # In[4]: fetch_housing_data() # In[5]: 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) # In[6]: housing = load_housing_data() housing.head() # In[7]: housing.info() # In[8]: housing["ocean_proximity"].value_counts() # In[9]: housing.describe() # In[10]: get_ipython().run_line_magic('matplotlib', 'inline') import matplotlib.pyplot as plt housing.hist(bins=50, figsize=(20,15)) save_fig("attribute_histogram_plots") plt.show() # In[11]: # 일관된 출력을 위해 유사난수 초기화 np.random.seed(42) # In[12]: import numpy as np # 예시를 위해서 만든 것입니다. 사이킷런에는 train_test_split() 함수가 있습니다. 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] # In[13]: train_set, test_set = split_train_test(housing, 0.2) print(len(train_set), "train +", len(test_set), "test") # In[14]: 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] # 위의 `test_set_check()` 함수는 파이썬 2와 파이썬 3에서 모두 작동되고 다음의 hashlib를 사용한 구현보다 훨씬 빠릅니다. # In[15]: import hashlib def test_set_check(identifier, test_ratio, hash=hashlib.md5): return bytearray(hash(np.int64(identifier)).digest())[-1] < 256 * test_ratio # In[16]: # 이 버전의 test_set_check() 함수가 파이썬 2도 지원합니다. def test_set_check(identifier, test_ratio, hash=hashlib.md5): return bytearray(hash(np.int64(identifier)).digest())[-1] < 256 * test_ratio # In[17]: housing_with_id = housing.reset_index() # `index` 열이 추가된 데이터프레임이 반환됩니다. train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "index") # In[18]: housing_with_id["id"] = housing["longitude"] * 1000 + housing["latitude"] train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "id") # In[19]: test_set.head() # In[20]: from sklearn.model_selection import train_test_split train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42) # In[21]: test_set.head() # In[22]: housing["median_income"].hist() # In[23]: # 소득 카테고리 개수를 제한하기 위해 1.5로 나눕니다. housing["income_cat"] = np.ceil(housing["median_income"] / 1.5) # 5 이상은 5로 레이블합니다. housing["income_cat"].where(housing["income_cat"] < 5, 5.0, inplace=True) # In[24]: housing["income_cat"].value_counts() # In[25]: housing["income_cat"].hist() save_fig('income_category_hist') # In[26]: 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] # In[27]: strat_test_set["income_cat"].value_counts() / len(strat_test_set) # In[28]: housing["income_cat"].value_counts() / len(housing) # In[29]: def income_cat_proportions(data): return data["income_cat"].value_counts() / len(data) train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42) compare_props = pd.DataFrame({ "Overall": income_cat_proportions(housing), "Stratified": income_cat_proportions(strat_test_set), "Random": income_cat_proportions(test_set), }).sort_index() compare_props["Rand. %error"] = 100 * compare_props["Random"] / compare_props["Overall"] - 100 compare_props["Strat. %error"] = 100 * compare_props["Stratified"] / compare_props["Overall"] - 100 # In[30]: compare_props # In[31]: for set_ in (strat_train_set, strat_test_set): set_.drop("income_cat", axis=1, inplace=True) # # 데이터 이해를 위한 탐색과 시각화 # In[32]: housing = strat_train_set.copy() # In[33]: ax = housing.plot(kind="scatter", x="longitude", y="latitude") ax.set(xlabel='경도', ylabel='위도') save_fig("bad_visualization_plot") # In[34]: ax = housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.1) ax.set(xlabel='경도', ylabel='위도') save_fig("better_visualization_plot") # `sharex=False` 매개변수는 x-축의 값과 범례를 표시하지 못하는 버그를 수정합니다. 이는 임시 방편입니다(https://github.com/pandas-dev/pandas/issues/10611 참조). 수정 사항을 알려준 Wilmer Arellano에게 감사합니다. # In[35]: ax = housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.4, s=housing["population"]/100, label="인구", figsize=(10,7), c="median_house_value", cmap=plt.get_cmap("jet"), colorbar=True, sharex=False) ax.set(xlabel='경도', ylabel='위도') plt.legend() save_fig("housing_prices_scatterplot") # In[36]: import matplotlib.image as mpimg california_img=mpimg.imread(PROJECT_ROOT_DIR + '/images/end_to_end_project/california.png') ax = housing.plot(kind="scatter", x="longitude", y="latitude", figsize=(10,7), s=housing['population']/100, label="인구", c="median_house_value", cmap=plt.get_cmap("jet"), colorbar=False, alpha=0.4, ) plt.imshow(california_img, extent=[-124.55, -113.80, 32.45, 42.05], alpha=0.5) plt.ylabel("위도", fontsize=14) plt.xlabel("경도", fontsize=14) prices = housing["median_house_value"] tick_values = np.linspace(prices.min(), prices.max(), 11) cbar = plt.colorbar() cbar.ax.set_yticklabels(["$%dk"%(round(v/1000)) for v in tick_values], fontsize=14) cbar.set_label('중간 주택 가격', fontsize=16) plt.legend(fontsize=16) save_fig("california_housing_prices_plot") plt.show() # In[37]: corr_matrix = housing.corr() # In[38]: corr_matrix["median_house_value"].sort_values(ascending=False) # In[39]: from pandas.plotting import scatter_matrix attributes = ["median_house_value", "median_income", "total_rooms", "housing_median_age"] scatter_matrix(housing[attributes], figsize=(12, 8)) save_fig("scatter_matrix_plot") # In[40]: housing.plot(kind="scatter", x="median_income", y="median_house_value", alpha=0.1) plt.axis([0, 16, 0, 550000]) save_fig("income_vs_house_value_scatterplot") # In[41]: 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"] # In[42]: corr_matrix = housing.corr() corr_matrix["median_house_value"].sort_values(ascending=False) # In[43]: housing.plot(kind="scatter", x="rooms_per_household", y="median_house_value", alpha=0.2) plt.axis([0, 5, 0, 520000]) plt.show() # In[44]: housing.describe() # # 머신러닝 알고리즘을 위한 데이터 준비 # In[45]: housing = strat_train_set.drop("median_house_value", axis=1) # 훈련 세트를 위해 레이블 삭제 housing_labels = strat_train_set["median_house_value"].copy() # In[46]: sample_incomplete_rows = housing[housing.isnull().any(axis=1)].head() sample_incomplete_rows # In[47]: sample_incomplete_rows.dropna(subset=["total_bedrooms"]) # 옵션 1 # In[48]: sample_incomplete_rows.drop("total_bedrooms", axis=1) # 옵션 2 # In[49]: median = housing["total_bedrooms"].median() sample_incomplete_rows["total_bedrooms"].fillna(median, inplace=True) # 옵션 3 sample_incomplete_rows # `sklearn.preprocessing.Imputer` 클래스는 사이킷런 0.20 버전에서 사용 중지 경고가 발생하고 0.22 버전에서 삭제될 예정입니다. 대신 추가된 `sklearn.impute.SimpleImputer` 클래스를 사용합니다. # In[50]: #from sklearn.preprocessing import Imputer from sklearn.impute import SimpleImputer imputer = SimpleImputer(strategy="median") # 중간값이 수치형 특성에서만 계산될 수 있기 때문에 텍스트 특성을 삭제합니다: # In[51]: housing_num = housing.drop('ocean_proximity', axis=1) # 다른 방법: housing_num = housing.select_dtypes(include=[np.number]) # In[52]: imputer.fit(housing_num) # In[53]: imputer.statistics_ # 각 특성의 중간 값이 수동으로 계산한 것과 같은지 확인해 보세요: # In[54]: housing_num.median().values # 훈련 세트 변환: # In[55]: X = imputer.transform(housing_num) # In[56]: housing_tr = pd.DataFrame(X, columns=housing_num.columns, index = list(housing.index.values)) # In[57]: housing_tr.loc[sample_incomplete_rows.index.values] # In[58]: imputer.strategy # In[59]: housing_tr = pd.DataFrame(X, columns=housing_num.columns) housing_tr.head() # 이제 범주형 입력 특성인 `ocean_proximity`을 전처리합니다: # ### 책에 실린 방법 # In[60]: housing_cat = housing['ocean_proximity'] housing_cat.head(10) # 판다스의 `factorize()` 메소드는 문자열 범주형 특성을 머신러닝 알고리즘이 다루기 쉬운 숫자 범주형 특성으로 변환시켜 줍니다: # In[61]: housing_cat_encoded, housing_categories = housing_cat.factorize() housing_cat_encoded[:10] # In[62]: housing_categories # `OneHotEncoder`를 사용하여 범주형 값을 원-핫 벡터로 변경합니다: # 사이킷런 0.20 버전에서 OneHotEncoder의 동작 방식이 변경되었습니다. 종전에는 0~최댓값 사이의 정수를 카테고리로 인식했지만 앞으로는 정수나 문자열에 상관없이 고유한 값만을 카테고리로 인식합니다. 경고 메세지를 피하기 위해 `categories` 매개변수를 `auto`로 설정합니다. # In[63]: from sklearn.preprocessing import OneHotEncoder encoder = OneHotEncoder(categories='auto') housing_cat_1hot = encoder.fit_transform(housing_cat_encoded.reshape(-1,1)) housing_cat_1hot # `OneHotEncoder`는 기본적으로 희소 행렬을 반환합니다. 필요하면 밀집 배열로 변환할 수 있습니다: # In[64]: housing_cat_1hot.toarray() # In[65]: # [PR #9151](https://github.com/scikit-learn/scikit-learn/pull/9151)에서 가져온 CategoricalEncoder 클래스의 정의. # 이 클래스는 사이킷런 0.20에 포함될 예정입니다. 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): """Encode categorical features as a numeric array. The input to this transformer should be a matrix of integers or strings, denoting the values taken on by categorical (discrete) features. The features can be encoded using a one-hot aka one-of-K scheme (``encoding='onehot'``, the default) or converted to ordinal integers (``encoding='ordinal'``). This encoding is needed for feeding categorical data to many scikit-learn estimators, notably linear models and SVMs with the standard kernels. Read more in the :ref:`User Guide `. Parameters ---------- encoding : str, 'onehot', 'onehot-dense' or 'ordinal' The type of encoding to use (default is 'onehot'): - 'onehot': encode the features using a one-hot aka one-of-K scheme (or also called 'dummy' encoding). This creates a binary column for each category and returns a sparse matrix. - 'onehot-dense': the same as 'onehot' but returns a dense array instead of a sparse matrix. - 'ordinal': encode the features as ordinal integers. This results in a single column of integers (0 to n_categories - 1) per feature. categories : 'auto' or a list of lists/arrays of values. Categories (unique values) per feature: - 'auto' : Determine categories automatically from the training data. - list : ``categories[i]`` holds the categories expected in the ith column. The passed categories are sorted before encoding the data (used categories can be found in the ``categories_`` attribute). dtype : number type, default np.float64 Desired dtype of output. handle_unknown : 'error' (default) or 'ignore' Whether to raise an error or ignore if a unknown categorical feature is present during transform (default is to raise). When this is parameter is set to 'ignore' and an unknown category is encountered during transform, the resulting one-hot encoded columns for this feature will be all zeros. Ignoring unknown categories is not supported for ``encoding='ordinal'``. Attributes ---------- categories_ : list of arrays The categories of each feature determined during fitting. When categories were specified manually, this holds the sorted categories (in order corresponding with output of `transform`). Examples -------- Given a dataset with three features and two samples, we let the encoder find the maximum value per feature and transform the data to a binary one-hot encoding. >>> from sklearn.preprocessing import CategoricalEncoder >>> enc = CategoricalEncoder(handle_unknown='ignore') >>> enc.fit([[0, 0, 3], [1, 1, 0], [0, 2, 1], [1, 0, 2]]) ... # doctest: +ELLIPSIS CategoricalEncoder(categories='auto', dtype=<... 'numpy.float64'>, encoding='onehot', handle_unknown='ignore') >>> enc.transform([[0, 1, 1], [1, 0, 4]]).toarray() array([[ 1., 0., 0., 1., 0., 0., 1., 0., 0.], [ 0., 1., 1., 0., 0., 0., 0., 0., 0.]]) See also -------- sklearn.preprocessing.OneHotEncoder : performs a one-hot encoding of integer ordinal features. The ``OneHotEncoder assumes`` that input features take on values in the range ``[0, max(feature)]`` instead of using the unique values. sklearn.feature_extraction.DictVectorizer : performs a one-hot encoding of dictionary items (also handles string-valued features). sklearn.feature_extraction.FeatureHasher : performs an approximate one-hot encoding of dictionary items or strings. """ 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): """Fit the CategoricalEncoder to X. Parameters ---------- X : array-like, shape [n_samples, n_feature] The data to determine the categories of each feature. Returns ------- self """ 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): """Transform X using one-hot encoding. Parameters ---------- X : array-like, shape [n_samples, n_features] The data to encode. Returns ------- X_out : sparse matrix or a 2-d array Transformed input. """ 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: # Set the problematic rows to an acceptable value and # continue `The rows are marked `X_mask` and will be # removed later. 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 # `CategoricalEncoder`는 하나 이상의 특성을 가진 2D 배열을 기대합니다. 따라서 `housing_cat`을 2D 배열로 바꾸어 주어야 합니다: # In[66]: #from sklearn.preprocessing import CategoricalEncoder # Scikit-Learn 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 # 사이킷런 0.20 개발 브랜치에 있던 `CategoricalEncoder`는 새로운 `OneHotEncoder`와 `OrdinalEncoder`로 나뉘었습니다. `OneHotEncoder`로 문자열로 된 범주형 변수도 변환할 수 있습니다: # In[67]: 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 # 기본 인코딩은 원-핫 벡터이고 희소 행렬로 반환됩니다. `toarray()` 메소드를 사용하여 밀집 배열로 바꿀 수 있습니다: # In[68]: housing_cat_1hot.toarray() # 또는 encoding 매개변수를 `"onehot-dense"`로 지정하여 희소 행렬대신 밀집 행렬을 얻을 수 있습니다. 0.20 버전의 `OneHotEncoder`는 `sparse=Fasle` 옵션을 주어 밀집 행렬을 얻을 수 있습니다: # In[69]: # cat_encoder = CategoricalEncoder(encoding="onehot-dense") cat_encoder = OneHotEncoder(categories='auto', sparse=False) housing_cat_1hot = cat_encoder.fit_transform(housing_cat_reshaped) housing_cat_1hot # In[70]: cat_encoder.categories_ # ### future_encoders.py를 사용한 새로운 방법 # In[71]: housing_cat = housing[['ocean_proximity']] housing_cat.head(10) # 주의: 번역서는 판다스의 `Series.factorize()` 메서드를 사용하여 문자열 범주형 특성을 정수로 인코딩합니다. 사이킷런 0.20에 추가될 `OrdinalEncoder` 클래스(PR #10521)는 입력 특성(레이블 `y`가 아니라 `X`)을 위해 설계되었고 파이프라인(나중에 이 노트북에서 나옵니다)과 잘 작동되기 때문에 더 좋은 방법입니다. 지금은 `future_encoders.py` 파일에서 임포트하지만 사이킷런 0.20 버전이 릴리스되면 `sklearn.preprocessing`에서 바로 임포팅할 수 있습니다. # # 0.20 버전 릴리스에 맞추어 `sklearn.preprocessing`에서 임포트합니다. # In[72]: # from future_encoders import OrdinalEncoder from sklearn.preprocessing import OrdinalEncoder # In[73]: ordinal_encoder = OrdinalEncoder() housing_cat_encoded = ordinal_encoder.fit_transform(housing_cat) housing_cat_encoded[:10] # In[74]: ordinal_encoder.categories_ # 주의: 번역서는 `CategoricalEncoder`를 사용하여 각 범주형 값을 원-핫 벡터로 변경합니다. 이 클래스는 `OrdinalEncoder`와 새로운 `OneHotEncoder`로 리팩토링되었습니다. 지금은 `OneHotEncoder`가 정수형 범주 입력만 다룰 수 있지만 사이킷런 0.20에서는 문자열 범주 입력도 다룰 수 있을 것입니다(PR #10521). 지금은 `future_encoders.py` 파일에서 임포트하지만 사이킷런 0.20 버전이 릴리스되면 `sklearn.preprocessing`에서 바로 임포팅할 수 있습니다. # # 0.20 버전 릴리스에 맞추어 `sklearn.preprocessing`에서 임포트합니다(사실 우리는 이미 위에서 0.20 버전의 `OneHotEncoder`를 사용했습니다). # In[75]: # from future_encoders import OneHotEncoder from sklearn.preprocessing import OneHotEncoder cat_encoder = OneHotEncoder(categories='auto') housing_cat_1hot = cat_encoder.fit_transform(housing_cat) housing_cat_1hot # 기본적으로 `OneHotEncoder` 클래스는 희소 행렬을 반환하지만 필요하면 `toarray()` 메서드를 호출하여 밀집 배열로 바꿀 수 있습니다: # In[76]: housing_cat_1hot.toarray() # 또는 `OneHotEncoder` 객체를 만들 때 `sparse=False`로 지정하면 됩니다: # In[77]: cat_encoder = OneHotEncoder(categories='auto', sparse=False) housing_cat_1hot = cat_encoder.fit_transform(housing_cat) housing_cat_1hot # In[78]: cat_encoder.categories_ # ### 다시 책의 내용이 이어집니다 # 추가 특성을 위해 나만의 변환기를 만들겠습니다: # In[79]: 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): # no *args or **kargs self.add_bedrooms_per_room = add_bedrooms_per_room def fit(self, X, y=None): return self # nothing else to do 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) # In[80]: housing_extra_attribs = pd.DataFrame( housing_extra_attribs, columns=list(housing.columns)+["rooms_per_household", "population_per_household"]) housing_extra_attribs.head() # 수치 특성을 전처리하기 위한 파이프라인을 만듭니다(0.20 버전에 새로 추가된 `SimpleImputer` 클래스로 변경합니다): # In[81]: 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) # In[82]: housing_num_tr # ### future_encoders.py를 사용한 방법 ========================== # 사이킷런의 0.20 버전에 포함될 `ColumnTransformer`를 사용하면 책의 예제에서처럼 `DataFrameSelector`와 `FeatureUnion`을 사용하지 않고 간단히 전체 파이프라인을 만들 수 있습니다. 아직 사이킷런 0.20 버전이 릴리스되기 전이므로 여기서는 future_encoders.py에 `ColumnTransformer`를 넣어 놓고 사용합니다. # # 사이킷런 0.20 버전에 추가된 `sklearn.compose.ColumnTransformer`로 코드를 변경합니다. # In[83]: # from future_encoders import ColumnTransformer from sklearn.compose import ColumnTransformer num_attribs = list(housing_num) cat_attribs = ["ocean_proximity"] full_pipeline = ColumnTransformer([ ("num", num_pipeline, num_attribs), ("cat", OneHotEncoder(categories='auto'), cat_attribs), ]) housing_prepared = full_pipeline.fit_transform(housing) housing_prepared # ### ==================================================== # 판단스 DataFrame 컬럼의 일부를 선택하는 변환기를 만듭니다: # In[84]: from sklearn.base import BaseEstimator, TransformerMixin # 사이킷런이 DataFrame을 바로 사용하지 못하므로 # 수치형이나 범주형 컬럼을 선택하는 클래스를 만듭니다. 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 # 하나의 큰 파이프라인에 이들을 모두 결합하여 수치형과 범주형 특성을 전처리합니다: # 0.20 버전에 추가된 `SimpleImputer`를 사용합니다. # In[85]: 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")), ]) # ### future_encoders.py를 사용한 방법 ========================== # In[86]: cat_pipeline = Pipeline([ ('selector', DataFrameSelector(cat_attribs)), ('cat_encoder', OneHotEncoder(categories='auto', sparse=False)), ]) # ### ==================================================== # 사이킷런 0.20 버전에 추가된 `ColumnTransformer`로 만든 `full_pipline`을 사용합니다: # In[87]: # from sklearn.pipeline import FeatureUnion # full_pipeline = FeatureUnion(transformer_list=[ # ("num_pipeline", num_pipeline), # ("cat_pipeline", cat_pipeline), # ]) full_pipeline = ColumnTransformer([ ("num_pipeline", num_pipeline, num_attribs), ("cat_encoder", OneHotEncoder(categories='auto'), cat_attribs), ]) # In[88]: housing_prepared = full_pipeline.fit_transform(housing) housing_prepared # In[89]: housing_prepared.shape # # 모델 선택과 훈련 # In[90]: from sklearn.linear_model import LinearRegression lin_reg = LinearRegression() lin_reg.fit(housing_prepared, housing_labels) # In[91]: # 훈련 샘플 몇 개를 사용해 전체 파이프라인을 적용해 보겠습니다. 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)) # 실제 값과 비교합니다: # In[92]: print("레이블:", list(some_labels)) # In[93]: some_data_prepared # In[94]: 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 # In[95]: from sklearn.metrics import mean_absolute_error lin_mae = mean_absolute_error(housing_labels, housing_predictions) lin_mae # In[96]: from sklearn.tree import DecisionTreeRegressor tree_reg = DecisionTreeRegressor(random_state=42) tree_reg.fit(housing_prepared, housing_labels) # In[97]: housing_predictions = tree_reg.predict(housing_prepared) tree_mse = mean_squared_error(housing_labels, housing_predictions) tree_rmse = np.sqrt(tree_mse) tree_rmse # # 모델 세부 튜닝 # In[98]: 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) # In[99]: def display_scores(scores): print("점수:", scores) print("평균:", scores.mean()) print("표준편차:", scores.std()) display_scores(tree_rmse_scores) # In[100]: 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) # 사이킷런 0.22 버전에서 랜덤 포레스트의 `n_estimator` 기본값이 10에서 100으로 변경됩니다. 0.20 버전에서 `n_estimator` 값을 지정하지 않을 경우 이에 대한 경고 메세지가 나옵니다. 경고 메세지를 피하기 위해 명시적으로 `n_estimator`를 10으로 설정합니다. # In[101]: from sklearn.ensemble import RandomForestRegressor forest_reg = RandomForestRegressor(n_estimators=10, random_state=42) forest_reg.fit(housing_prepared, housing_labels) # In[102]: housing_predictions = forest_reg.predict(housing_prepared) forest_mse = mean_squared_error(housing_labels, housing_predictions) forest_rmse = np.sqrt(forest_mse) forest_rmse # In[103]: from sklearn.model_selection import cross_val_score 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) # In[104]: scores = cross_val_score(lin_reg, housing_prepared, housing_labels, scoring="neg_mean_squared_error", cv=10) pd.Series(np.sqrt(-scores)).describe() # In[105]: from sklearn.svm import SVR svm_reg = SVR(kernel="linear") svm_reg.fit(housing_prepared, housing_labels) housing_predictions = svm_reg.predict(housing_prepared) svm_mse = mean_squared_error(housing_labels, housing_predictions) svm_rmse = np.sqrt(svm_mse) svm_rmse # In[106]: from sklearn.model_selection import GridSearchCV param_grid = [ # 하이퍼파라미터 12(=3×4)개의 조합을 시도합니다. {'n_estimators': [3, 10, 30], 'max_features': [2, 4, 6, 8]}, # bootstrap은 False로 하고 6(=2×3)개의 조합을 시도합니다. {'bootstrap': [False], 'n_estimators': [3, 10], 'max_features': [2, 3, 4]}, ] forest_reg = RandomForestRegressor(random_state=42) # 다섯 폴드에서 훈련하면 총 (12+6)*5=90번의 훈련이 일어납니다. grid_search = GridSearchCV(forest_reg, param_grid, cv=5, scoring='neg_mean_squared_error', return_train_score=True, n_jobs=-1) grid_search.fit(housing_prepared, housing_labels) # 최상의 파라미터 조합: # In[107]: grid_search.best_params_ # In[108]: grid_search.best_estimator_ # 그리드서치에서 테스트한 하이퍼파라미터 조합의 점수를 확인합니다: # In[109]: cvres = grid_search.cv_results_ for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]): print(np.sqrt(-mean_score), params) # In[110]: pd.DataFrame(grid_search.cv_results_) # In[111]: from sklearn.model_selection import RandomizedSearchCV from scipy.stats import randint param_distribs = { 'n_estimators': randint(low=1, high=200), 'max_features': randint(low=1, high=8), } forest_reg = RandomForestRegressor(random_state=42) rnd_search = RandomizedSearchCV(forest_reg, param_distributions=param_distribs, n_iter=10, cv=5, scoring='neg_mean_squared_error', random_state=42, n_jobs=-1) rnd_search.fit(housing_prepared, housing_labels) # In[112]: cvres = rnd_search.cv_results_ for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]): print(np.sqrt(-mean_score), params) # In[113]: feature_importances = grid_search.best_estimator_.feature_importances_ feature_importances # 사이킷런 0.20 버전의 `ColumnTransformer`를 사용했기 때문에 `full_pipeline`에서 `cat_encoder`를 가져옵니다. 즉 `cat_pipeline`을 사용하지 않았습니다: # In[114]: extra_attribs = ["rooms_per_hhold", "pop_per_hhold", "bedrooms_per_room"] # cat_encoder = cat_pipeline.named_steps["cat_encoder"] cat_encoder = full_pipeline.named_transformers_["cat_encoder"] cat_one_hot_attribs = list(cat_encoder.categories_[0]) attributes = num_attribs + extra_attribs + cat_one_hot_attribs sorted(zip(feature_importances, attributes), reverse=True) # In[115]: 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) # In[116]: final_rmse # 테스트 RMSE에 대한 95% 신뢰 구간을 계산할 수 있습니다: # In[117]: from scipy import stats # In[118]: confidence = 0.95 squared_errors = (final_predictions - y_test) ** 2 mean = squared_errors.mean() m = len(squared_errors) np.sqrt(stats.t.interval(confidence, m - 1, loc=np.mean(squared_errors), scale=stats.sem(squared_errors))) # 다음과 같이 수동으로 계산할 수도 있습니다: # In[119]: tscore = stats.t.ppf((1 + confidence) / 2, df=m - 1) tmargin = tscore * squared_errors.std(ddof=1) / np.sqrt(m) np.sqrt(mean - tmargin), np.sqrt(mean + tmargin) # 또는 t 점수 대신 z 점수를 사용할 수도 있습니다: # In[120]: zscore = stats.norm.ppf((1 + confidence) / 2) zmargin = zscore * squared_errors.std(ddof=1) / np.sqrt(m) np.sqrt(mean - zmargin), np.sqrt(mean + zmargin) # # 추가 내용 # ## 전처리와 예측을 포함한 파이프라인 # In[121]: full_pipeline_with_predictor = Pipeline([ ("preparation", full_pipeline), ("linear", LinearRegression()) ]) full_pipeline_with_predictor.fit(housing, housing_labels) full_pipeline_with_predictor.predict(some_data) # ## joblib을 사용한 모델 저장 # In[122]: my_model = full_pipeline_with_predictor # In[123]: from sklearn.externals import joblib joblib.dump(my_model, "my_model.pkl") # DIFF #... my_model_loaded = joblib.load("my_model.pkl") # DIFF # ## `RandomizedSearchCV`을 위한 Scipy 분포 함수 # In[124]: from scipy.stats import geom, expon geom_distrib=geom(0.5).rvs(10000, random_state=42) expon_distrib=expon(scale=1).rvs(10000, random_state=42) plt.hist(geom_distrib, bins=50) plt.show() plt.hist(expon_distrib, bins=50) plt.show() # # 연습문제 해답 # ## 1. # 질문: 서포트 벡터 머신 회귀(sklearn.svm.SVR)를 kernel=“linear”(하이퍼파라미터 C를 바꿔가며)나 kernel=“rbf”(하이퍼파라미터 C와 gamma를 바꿔가며) 등의 다양한 하이퍼파라미터 설정으로 시도해보세요. 지금은 이 하이퍼파라미터가 무엇을 의미하는지 너무 신경 쓰지 마세요. 최상의 SVR 모델은 무엇인가요? # In[127]: from sklearn.model_selection import GridSearchCV param_grid = [ {'kernel': ['linear'], 'C': [10., 30., 100., 300., 1000., 3000., 10000., 30000.0]}, {'kernel': ['rbf'], 'C': [1.0, 3.0, 10., 30., 100., 300., 1000.0], 'gamma': [0.01, 0.03, 0.1, 0.3, 1.0, 3.0]}, ] svm_reg = SVR() grid_search = GridSearchCV(svm_reg, param_grid, cv=5, scoring='neg_mean_squared_error', verbose=2, n_jobs=1) grid_search.fit(housing_prepared, housing_labels) # 최상 모델의 (5-폴드 교차 검증으로 평가한) 점수는 다음과 같습니다: # In[128]: negative_mse = grid_search.best_score_ rmse = np.sqrt(-negative_mse) rmse # 이는 `RandomForestRegressor`보다 훨씬 좋지 않네요. 최상의 하이퍼파라미터를 확인해 보겠습니다: # In[129]: grid_search.best_params_ # 선형 커널이 RBF 커널보다 성능이 나은 것 같습니다. `C`는 테스트한 것 중에 최대값이 선택되었습니다. 따라서 (작은 값들은 지우고) 더 큰 값의 `C`로 그리드서치를 다시 실행해 보아야 합니다. 아마도 더 큰 값의 `C`에서 성능이 높아질 것입니다. # ## 2. # 질문: GridSearchCV를 RandomizedSearchCV로 바꿔보세요. # In[132]: from sklearn.model_selection import RandomizedSearchCV from scipy.stats import expon, reciprocal # expon(), reciprocal()와 다른 확률 분포 함수에 대해서는 # https://docs.scipy.org/doc/scipy/reference/stats.html를 참고하세요. # 노트: kernel 매개변수가 "linear"일 때는 gamma가 무시됩니다. param_distribs = { 'kernel': ['linear', 'rbf'], 'C': reciprocal(20, 200000), 'gamma': expon(scale=1.0), } svm_reg = SVR() rnd_search = RandomizedSearchCV(svm_reg, param_distributions=param_distribs, n_iter=50, cv=5, scoring='neg_mean_squared_error', verbose=2, n_jobs=1, random_state=42) rnd_search.fit(housing_prepared, housing_labels) # 최상 모델의 (5-폴드 교차 검증으로 평가한) 점수는 다음과 같습니다: # In[133]: negative_mse = rnd_search.best_score_ rmse = np.sqrt(-negative_mse) rmse # 이제 `RandomForestRegressor`의 성능에 훨씬 가까워졌습니다(하지만 아직 차이가 납니다). 최상의 하이퍼파라미터를 확인해 보겠습니다: # In[134]: rnd_search.best_params_ # 이번에는 RBF 커널에 대해 최적의 하이퍼파라미터 조합을 찾았습니다. 보통 랜덤서치가 같은 시간안에 그리드서치보다 더 좋은 하이퍼파라미터를 찾습니다. # 여기서 사용된 `scale=1.0`인 지수 분포를 살펴보겠습니다. 일부 샘플은 1.0보다 아주 크거나 작습니다. 하지만 로그 분포를 보면 대부분의 값이 exp(-2)와 exp(+2), 즉 0.1과 7.4 사이에 집중되어 있음을 알 수 있습니다. # In[135]: expon_distrib = expon(scale=1.) samples = expon_distrib.rvs(10000, random_state=42) plt.figure(figsize=(10, 4)) plt.subplot(121) plt.title("Exponential distribution (scale=1.0)") plt.hist(samples, bins=50) plt.subplot(122) plt.title("Log of this distribution") plt.hist(np.log(samples), bins=50) plt.show() # `C`에 사용된 분포는 매우 다릅니다. 주어진 범위안에서 균등 분포로 샘플링됩니다. 그래서 오른쪽 로그 분포가 거의 일정하게 나타납니다. 이런 분포는 원하는 스케일이 정확이 무엇인지 모를 때 사용하면 좋습니다: # In[136]: reciprocal_distrib = reciprocal(20, 200000) samples = reciprocal_distrib.rvs(10000, random_state=42) plt.figure(figsize=(10, 4)) plt.subplot(121) plt.title("Reciprocal distribution (scale=1.0)") plt.hist(samples, bins=50) plt.subplot(122) plt.title("Log of this distribution") plt.hist(np.log(samples), bins=50) plt.show() # reciprocal() 함수는 하이퍼파라미터의 스케일에 대해 전혀 감을 잡을 수 없을 때 사용합니다(오른쪽 그래프에서 볼 수 있듯이 주어진 범위안에서 모든 값이 균등합니다). 반면 지수 분포는 하이퍼파라미터의 스케일을 (어느정도) 알고 있을 때 사용하는 것이 좋습니다. # ## 3. # 질문: 가장 중요한 특성을 선택하는 변환기를 준비 파이프라인에 추가해보세요. # In[137]: from sklearn.base import BaseEstimator, TransformerMixin def indices_of_top_k(arr, k): return np.sort(np.argpartition(np.array(arr), -k)[-k:]) class TopFeatureSelector(BaseEstimator, TransformerMixin): def __init__(self, feature_importances, k): self.feature_importances = feature_importances self.k = k def fit(self, X, y=None): self.feature_indices_ = indices_of_top_k(self.feature_importances, self.k) return self def transform(self, X): return X[:, self.feature_indices_] # 노트: 이 특성 선택 클래스는 이미 어떤 식으로든 특성 중요도를 계산했다고 가정합니다(가령 `RandomForestRegressor`을 사용하여). `TopFeatureSelector`의 `fit()` 메서드에서 직접 계산할 수도 있지만 (캐싱을 사용하지 않을 경우) 이렇게 하면 그리드서치나 랜덤서치의 모든 하이퍼파라미터 조합에 대해 계산이 일어나기 때문에 매우 느려집니다. # 선택할 특성의 개수를 지정합니다: # In[138]: k = 5 # 최상의 k개 특성의 인덱스를 확인해 보겠습니다: # In[139]: top_k_feature_indices = indices_of_top_k(feature_importances, k) top_k_feature_indices # In[140]: np.array(attributes)[top_k_feature_indices] # 최상의 k개 특성이 맞는지 다시 확인합니다: # In[141]: sorted(zip(feature_importances, attributes), reverse=True)[:k] # 좋습니다. 이제 이전에 정의한 준비 파이프라인과 특성 선택기를 추가한 새로운 파이프라인을 만듭니다: # In[142]: preparation_and_feature_selection_pipeline = Pipeline([ ('preparation', full_pipeline), ('feature_selection', TopFeatureSelector(feature_importances, k)) ]) # In[143]: housing_prepared_top_k_features = preparation_and_feature_selection_pipeline.fit_transform(housing) # 처음 3개 샘플의 특성을 확인해 보겠습니다: # In[144]: housing_prepared_top_k_features[0:3] # 최상의 k개 특성이 맞는지 다시 확인합니다: # In[145]: housing_prepared[0:3, top_k_feature_indices] # 성공입니다! :) # ## 4. # 질문: 전체 데이터 준비 과정과 최종 예측을 하나의 파이프라인으로 만들어보세요. # In[146]: prepare_select_and_predict_pipeline = Pipeline([ ('preparation', full_pipeline), ('feature_selection', TopFeatureSelector(feature_importances, k)), ('svm_reg', SVR(**rnd_search.best_params_)) ]) # In[147]: prepare_select_and_predict_pipeline.fit(housing, housing_labels) # 몇 개의 샘플에 전체 파이프라인을 적용해 보겠습니다: # In[148]: some_data = housing.iloc[:4] some_labels = housing_labels.iloc[:4] print("예측:\t", prepare_select_and_predict_pipeline.predict(some_data)) print("레이블:\t\t", list(some_labels)) # 전체 파이프라인이 잘 작동하는 것 같습니다. 물론 예측 성능이 아주 좋지는 않습니다. `SVR`보다 `RandomForestRegressor`가 더 나은 것 같습니다. # ## 5. # 질문: `GridSearchCV`를 사용해 준비 단계의 옵션을 자동으로 탐색해보세요. # # 사이킷런 0.20 버전에서 `GridSearchCV`의 `n_jobs` 매개변수를 -1로 했을 때 에러가 발생하는 경우가 있습니다(https://github.com/scikit-learn/scikit-learn/issues/12250). 에러가 해결되기 전까지 매개변수를 1로 설정합니다. # In[149]: param_grid = [ {'preparation__num_pipeline__imputer__strategy': ['mean', 'median', 'most_frequent'], 'feature_selection__k': list(range(1, len(feature_importances) + 1))} ] grid_search_prep = GridSearchCV(prepare_select_and_predict_pipeline, param_grid, cv=5, scoring='neg_mean_squared_error', verbose=2, n_jobs=1) grid_search_prep.fit(housing, housing_labels) # In[150]: grid_search_prep.best_params_ # 최상의 Imputer 정책은 `most_frequent`이고 거의 모든 특성이 유용합니다(16개 중 15개). 마지막 특성(`ISLAND`)은 잡음이 추가될 뿐입니다. # 축하합니다! 이제 머신러닝에 대해 꽤 많은 것을 알게 되었습니다. :)