아래와 같은 순서로 책을 보시면 파이썬 => 판다스 => 머신러닝, 딥러닝을 공부할 수 있습니다. 제가 정리한 글에 있습니다.
https://steemit.com/kr/@papasmf1/73cj22
핸즈온 머신러닝 책의 소스는 아래의 깃허브에 있습니다. 참고하실 수 있습니다.
https://github.com/rickiepark/handson-ml
제가 테스트하고 실습하는 환경은 맥에 아나콘다 최근 패키지를 설치해서 주피터랩으로 실행하고 있습니다.
2장 머신러닝 프로젝트 처음부터 끝까지
이 장에서는 StatLib저장소에 있는 캘리포니아 주택 가격 데이터셋을 사용합니다. 이 데이터셋은 1990년 캘리포니아 인구조사 데이터를 기반으로 합니다. 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
상관관계의 범위는 -1부터 1까지입니다. 1에 가까우면 강한 양의 상관관계를 가진다는 뜻입니다. 예를 들어 중간 주택 가격(median_house_value)는 중간 소득(median_income)이 올라갈 때 증가하는 경항이 있습니다.
특성 사이의 상관관계를 확인하는 다른 방법은 숫자형 특성 사이에 산점도를 그려주는 판다스의 scatter_matrix함수를 사용하는 것입니다.
from pandas.plotting import scatter_matrix
attributes = ["median_house_value", "median_income", "total_rooms",
"housing_median_age"]
scatter_matrix(housing[attributes], figsize=(12,9))
대각선 방향은 각 변수 자신에 대한 것이라 그냥 직선이 되므로 유용하지 않습니다. 그래서 판다스는 이곳에 각 특성의 히스토그램을 그립니다. 중간 주택 가격을 예측하는데 가장 유용할 것 같은 특성을 중간 소득이므로 상관관계 산점도를 확대해 보겠습니다.
housing.plot(kind="scatter", x="median_income", y="median_house_value",
alpha=0.1)
이 그래프는 상관관계가 매우 강합니다. 위쪽으로 향하는 경향을 볼 수 있으며 포인트들이 너무 널리 퍼져 있지 않습니다. 둘째 앞서 본 가격 제한 값이 $500,000에서 수평선으로 잘 보입니다.
특성 조합으로 실험
앞 절에서 데이터를 탐색하고 통찰을 얻는 여러 방법에 대한 아이디어를 얻었기 바립니다.
머신러닝 알고리즘용 데이터를 실제로 준비하기 전에 마지막으로 해볼 수 있는 것은 여러 특성의 조합을 시도해보는 것입니다. 예를 들어 특정 구역의 방 개수는 얼마나 많은 가구수가 있는지 모른다면 그다지 유용하지 않습니다.
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특성은 전체 방 갯수나 침대 갯수보다 중간 주택 가격과의 상관관계가 훨씬 높습니다. 확실히 침대방의 비율이 낮은 집은 더 비싼 경향이 있습니다. 가구당 방 개수도 구역 내 전체 방 갯수보다 더 유용합니다. 단연히 더 큰 집이 더 비쌉니다.
머신러닝 알고리즘을 위한 데이터 준비
이제 머신러닝 알고리즘을 위해 데이터를 준비해야 합니다.
어떤 데이터셋에 대해서도 데이터 변환을 손쉽게 반복할 수 있습니다.
향후 프로젝트에 사용할 수 있는 변환 라이브러리를 점진적으로 구축하게 됩니다.
실제 시스템에서 알고리즘에 새 데이터를 주입하기 전에 변환시키는 데 이 함수를 사용할 수 있습니다.
여러 가지 데이터 변환을 쉽게 시도해 볼 수 있고 어떤 조합이 가장 좋은지 확인하는데 편리합니다.
housing = strat_train_set.drop("median_house_value", axis=1)
housing_labels = strat_train_set["median_house_value"].copy()
데이터 정제
대부분의 머신러닝 알고리즘은 누락된 특성을 다루지 못하므로 이를 처리할 수 있는 함수를 몇개 만들겠습니다. 앞서 total_bedrooms특성에 값이 없는 경우를 보았는데 이를 고쳐보겠습니다. 방법은 세가지 입니다.
해당 구역을 제거합니다.
전체 특성을 삭제합니다.
어떤 값으로 채웁니다(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)
옵션 3을 선택하면 훈련 세트에서 중간값을 계산하고 누락된 값을 이 값으로 채워 넣어야 합니다. 하지만 계산된 중간값을 저장하는 것 또한 잊지 말아야 합니다. 나중에 시스템을 평가할 때 테스트 세트에 있는 누락된 값을 바꾸기 위해 필요하고 시스템이 실제 운영될 때 새로운 데이터에서 누락된 값을 바꿔야 하기 때문입니다.
사이킷런의 Imputer는 누락된 값을 손쉽게 다루도록 해줍니다.
from sklearn.preprocessing import Imputer
imputer = Imputer(strategy = "median")
중간값이 수치형 특성에서만 계산될 수 있기 때문에 텍스트 특성인 ocean_proximity를 제외한 데이터 복사본을 생성합니다.
housing_num = housing.drop("ocean_proximity", axis=1)
이제 imputer객체의 fit()메서드를 사용해 훈련 데이터에 적용할 수 있습니다.
imputer.fit(housing_num)
Imputer(axis=0, copy=True, missing_values='NaN', strategy='median', verbose=0)
Imputer는 각 특성의 중간값을 계산해서 그 결과를 객체의 statistics_속성에 저장합니다. Total_bedrooms특성에만 누락된 값이 있지만 나중에 시스템이 서비스될 때 새로운 데이터에서 어떤 값이 누락될지 확신할 수 없으므로 모든 수치형 특성에 imputer를 적용하는 것이 바람직합니다.
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))
사이킷런의 설계 철학
텍스트와 범주형 특성 다루기
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
대부분의 머신러닝 알고리즘은 숫자형을 다루므로 이 카테고리를 텍스트에서 숫자로 바꾸도록 하겠습니다. 이를 위해 각 카테고리를 다른 정수값으로 맵핑해주는 판다스의 factrize()메서드를 사용합니다.
housing_cat_encoded, housing_categories = housing_cat.factorize()
housing_cat_encoded[:10]
array([0, 0, 1, 2, 0, 2, 0, 2, 0, 0])
훨씬 나아졌네요. 이제 housing_cat_encoded는 완전히 숫자입니다. Factorize()메서드는 카테고리 리스트도 반환해줍니다.( 1H OCEAN은 0으로 맵핑되고, NEAR OCEAN은 1로 매핑되고 등등)
housing_categories
Index(['<1H OCEAN', 'NEAR OCEAN', 'INLAND', 'NEAR BAY', 'ISLAND'], dtype='object')
이 표현 방식의 문제는 머신러닝 알고리즘이 가까이 있는 두 값이 떨어져 있는 두 값보다 더 비슷하다고 생각한다는 점입니다. 실제로는 그렇지 않습니다. 이 문제는 일반적으로 카테고리별 이진 특성을 만들어 해결합니다. 카테고리가 ‘1H OCEAN’일 때 한 특성이 1이고(그 외 특성은 0), 카테고리가 ‘INLAND’일 때 다른 한 특성이 1이 되는(역시 그 외에는 0)식입니다. 한 특성만 1이고(핫) 나머지는 0이므로 이를 원-핫 인코딩 one-hot encoding이라고 부릅니다.
사이킷런은 숫자로 된 범주형 값을 원-핫 벡터로 바꿔주는 OneHotEncoder를 제공합니다. 카테고리들을 원-핫 벡터로 인코딩해보겠습니다.
from sklearn.preprocessing import OneHotEncoder
encoder = OneHotEncoder()
housing_cat_1hot = encoder.fit_transform(housing_cat_encoded.reshape(-1, 1))
housing_cat_1hot
/anaconda3/lib/python3.7/site-packages/sklearn/preprocessing/_encoders.py:368: FutureWarning: The handling of integer data will change in version 0.22. Currently, the categories are determined based on the range [0, max(values)], while in the future they will be determined based on the unique values.
If you want the future behaviour and silence this warning, you can specify "categories='auto'".
In case you used a LabelEncoder before this OneHotEncoder to convert the categories to integers, then you can now use the OneHotEncoder directly.
warnings.warn(msg, FutureWarning)
[48]:
<16512x5 sparse matrix of type '<class 'numpy.float64'>'
with 16512 stored elements in Compressed Sparse Row format>
fit_transform()메서드는 2차원 배열을 넣어줘야 하는데 housing_cat_encoded는 1차원 배열이므로 구조를 바꿔야 합니다. 또한 출력을 보면 넘파이 배열이 아니고 사이파이 희소 행렬입니다. 이는 수천개의 카테고리가 있는 범주형 특성일 경우 매우 효율적입니다. 이런 특성을 원-핫 인코딩하면 열이 수천 개인 행렬로 변하고 각 행은 1이 하나뿐이고 그 외에는 모두 0으로 채워져 있을 것입니다. 0을 모두 메모리에 저장하는 것은 낭비이므로 희소 행렬은 0이 아닌 원소의 위치만 저장합니다. 이 행렬을 거의 일반적인 2차원 배열처럼 사용할 수 있지만 밀집된 넘파이 배열로 바꾸려먼 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.]])
(아래의 코드가 에러가 나서 깃허브에서 다시 수정한 소스를 받아옴. 19/2/28일 체크)
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 <preprocessing_categorical_features>.
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
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
<16512x5 sparse matrix of type '<class 'numpy.float64'>'
with 16512 stored elements in Compressed Sparse Row format>
기본적으로 CategoricalEncoder는 희소 행렬을 출력하지만 밀집 행렬을 원할 경우에는 encoding매개변수를 “onehot-dense”로 지정할 수 있습니다.
cat_encoder = CategoricalEncoder(encoding="onehot-dense")
housing_cat_1hot = cat_encoder.fit_transform(housing_cat_reshaped)
housing_cat_1hot
array([[1., 0., 0., 0., 0.],
[1., 0., 0., 0., 0.],
[0., 0., 0., 0., 1.],
...,
[0., 1., 0., 0., 0.],
[1., 0., 0., 0., 0.],
[0., 0., 0., 1., 0.]])
cat_encoder.categories_
[array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'],
dtype=object)]
2.5.3 나만의 변환기
사이킷런은 상속이 아닌 덕 타이핑을 지원하므로 fit(), transform(0, 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[:, bedrooms_ix] / X[:, rooms_ix]
if self.add_bedrooms_per_room:
bedrooms_per_room = X[:, bedrooms_ix] / X[:, rooms_ix]
return np.c_[X, rooms_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)
특성 스케일링
데이터에 적용할 가장 중요한 변환 중 하나가 특성 스케일링feature scaling입니다. 몇 가지를 빼고는 머신러닝 알고리즘은 입력 숫자 특성들의 스케일이 많이 다르면 잘 작동하지 않습니다.
모든 특성의 범위를 같도록 만들어주는 방법으로 min-max 스케일링과 표준화가 널리 사용됩니다.
min-max스케일링은 매우 간단합니다. 많은 사람들이 이를 정규화nomalization이라고 부릅니다. 0~1범위에 들도록 값을 이동하고 스케일을 조정하면 되니다.
변환 파이프라인
앞서 보았듯이 변환 단계가 많으면 정확한 순서대로 실행되어야 합니다. 다행이 사이킷런에는 연속된 변환을 순서대로 처리할 수 있도록 도와주는 Pipeline클래스가 있습니다. 다음은 숫자 특성을 처리하는 간단한 파이프라인입니다.
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
num_pipeline = Pipeline([
('imputer', Imputer(strategy="median")),
('attribs_adder', CombinedAttributesAdder()),
('std_scaler', StandardScaler()),
])
housing_num_tr = num_pipeline.fit_transform(housing_num)
수치형 컬럼을 넘파이 배열로 추출하는 대신 판다스의 데이터프레임을 파이프라인에 직접 주입할 수 있다면 좋을 것입니다. 사이킷런이 판다스의 데이터프레임을 다룰 수는 없지만 이를 처리하는 변환기를 직접 만들 수는 있습니다.
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', Imputer(strategy="median")),
('attribs_adder', CombinedAttributesAdder()),
('std_scaler', StandardScaler()),
])
housing_num_tr = num_pipeline.fit_transform(housing_num)
수치형 컬럼을 넘파이 배열로 추출하는 대신 판다스의 데이터프레임을 파이프라인에 직접 주입할 수 있다면 좋을 것 입니다. 사이킷런이 판다스의 데이터프레임을 다룰 수는 없지만 이를 처리하는 변환기를 직접 만들 수는 있습니다.
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', Imputer(strategy="median")),
('attribs_adder', CombinedAttributesAdder()),
('std_scaler', StandardScaler()),
])
cat_pipeline = Pipeline([
('selector', DataFrameSelector(cat_attribs)),
('cat_encoder', CategoricalEncoder(encoding="onehot-dense")),
])
하지만 어떻게 이 두 파이프라인을 하나의 파이프라인으로 합칠 수 있을까요? 정답은 사이킷런의 FeatureUnion입니다. 변환기 목록을 전달하고 transform()메서드를 호출하면 각 변환기의 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, 15)
모델 선택과 훈련
드디어 마지막입니다.
훈련 세트에서 훈련하고 평가하기
이전 단계의 작업 덕분에 생각보다 훨씬 간단해졌습니다. 먼저 앞 장에서 했던 것처럼 선형 회귀 모델을 훈련시켜보겠습니다.
from sklearn.linear_model import LinearRegression
lin_reg = LinearRegression()
lin_reg.fit(housing_prepared, housing_labels)
이게 끝입니다! 완전히 작동하는 선형 회귀 모델을 만들었습니다.
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))
예측: [210528.01750105 317824.00920324 210815.82440675 59307.5245779
189946.78983305]
print("레이블:", list(some_labels))
레이블: [286600.0, 340600.0, 196900.0, 46300.0, 254500.0]
아주 정확한 예측은 아니지만(예를 들어 첫번째 예측은 40% 가까이 벗어났습니다.) 작동은 합니다. 사이킷런의 mean_square_errorgkatㅜ를 사용해 전체 훈련 세트에 해한 이 회귀 모델의 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
68635.59352516982
대부분의 구역의 중간 주택 가격은 120,000에서 265,000사이입니다. 그러므로 예측 오차가 68,628인 것은 매우 만족스럽지 못합니다. 이는 모델이 훈련 데이터에 과소적합된 사례입니다. 이런 상황은 특성들이 좋은 예측을 만들만큼 충분한 정보를 제공하지 못했거나 모델이 충분히 강력하지 못하다는 사실을 말해줍니다. 앞 장에서 보았듯이 과소적합을 해결하는 주요 방법은 더 강력한 모델을 선택하거나 훈련 알고리즘에 더 좋은 특성을 주입하거나 모델의 규제를 감소시키는 것입니다.
DecisionTreeRegressor를 훈련해 보겠습니다. 이 모델은 강력하고 데이터에서 복잡한 비선형 관계를 찾을 수 있습니다.
from sklearn.tree import DecisionTreeRegressor
tree_reg = DecisionTreeRegressor()
tree_reg.fit(housing_prepared, housing_labels)
모델을 훈련시켰으니 훈련 세트로 평가해보겠습니다.
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
오차가 전혀 없나요? 이 모델이 진짜 완벽할 수 있나요? 물론 이는 모델이 데이터에 너무 심하게 과대적합된 것으로 보입니다. 어떻게 확신할 수 있죠?
교차 검증을 사용한 평가
결정 트리 모델을 평가하는 방법을 생각해보겠습니다. 우선 train_test_split함수를 사용해 훈련 세트를 더 작은 훈련 세트와 검증 세트로 나누고, 더 작은 훈련 세트에서 모델을 훈련시키고 검증 세트로 모델을 평가하는 방법이 있습니다.
훌륭한 대안으로 사이킷런의 교차 검증 기능을 사용하는 방법이 있습니다. 다음 코드는 K-겹교차 검증을 수행합니다. 훈련 세트를 폴드라 불리는 10개의 서브셋으로 무작위로 분할합니다. 그런 다음 결정 트리 모델을 10번 훈련하고 평가하는데, 매번 다른 폴드를 선택해 평가에 사용하고 나머지 9개 폴드는 훈련에 사용합니다.
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_mse_scores = np.sqrt(-scores)
def display_scores(scores):
print("Scores:", scores)
print("Mean:", scores.mean())
print("Standard deviation:", scores.std())
display_scores(tree_rmse_scores)
Scores: [71156.05817196 64846.35193095 72724.18719344 71727.94841578
70547.89024209 74606.97294108 71840.42843526 69401.03252896
69633.37238457 67451.25666522]
Mean: 70393.54989093055
Standard deviation: 2620.220906554669
결정 트리 결과가 이전만큼 좋아 보이지 않습니다. 실제로 선형 회귀 모델보다 나쁩니다! 결정트리점수가 대략 평균 71,379에서 2,458사이입니다. 검증 세트를 하나만 사용했다면 이런 정보를 얻지 못했을 겁니다.
비교를 위해 선형 회귀 모델의 점수를 계산해 보겠습니다.
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: [66780.80311827 66939.08505782 70380.83790115 74469.61166289
68042.55318962 71196.31002382 64973.23018016 68285.42317144
70978.93569734 67670.63282586]
Mean: 68971.74228283842
Standard deviation: 2629.390921569475
확실히 결정 트리 모델이 과대적합되어 선형 회귀 모델보다 성능이 나쁩니다.
마지막으로 RandomForestRegressor모델을 하나 더 시도해보겠습니다. 랜덤 프레스트는 특성을 무작위로 선택해서 많은 결정 트리를 만들고 그 예측을 평균 내는 방식으로 동작합니다. 여러 다른 모델을 모아서 하나의 모델을 만드는 것을 앙상블 학습이라고 하며 머신러닝 알고리즘의 성능을 극대화하는 방법 중 하나입니다.
from sklearn.ensemble import RandomForestRegressor
forest_reg = RandomForestRegressor()
forest_reg.fit(housing_prepared, housing_labels)