본문 바로가기

문돌이 존버/데이터 분석

핸즈온 머신러닝2 복습하기(챕터 2: 머신러닝 프로젝트 처음부터 끝까지)

반응형

최근 핸즈온 머신러닝2를 구입해서 머신러닝 공부를 시작하고 있습니다. 앤드류 응 교수님의 코세라 강의를 비롯해서 김성훈 교수님의 모두를 위한 머신러닝/딥러닝 강의도 들었지만 오래되서 점차 기초 지식을 잊어버리는 듯한 느낌입니다. 핸즈온 머신러닝1도 사실 공부했었는데, 베이스가 없었던 때에 접했기 때문에 흐름만 읽는 수준이었습니다. 이제는 책을 찬찬히 살펴보며 이해를 하고, 때로는 의문을 가지며 꼼꼼하게 정리해야 하지 않을까 싶네요. 물론, 제가 배운 것을 정리하고 나중에 더 쉽게 기억하기 위해서 말이죠. 

그래서 핸즈온 머신러닝2 복습하기 시리즈는 오로지 제 자신을 위해 정리한 필기에 불과합니다. 여러분들께 소개하고자 하는 목표가 아니기 때문에 설명이 불친절할 수도 있습니다. 또 책의 내용을 그대로 가져왔다는 점 참고해주시기 바랍니다~ 혹시나 궁금한 사항이 있다면 댓글로 남겨주시기 바랍니다. 책에 자세한 내용을 설명하고 있다면 찾아서 알려드리겠습니다.

 


 

가상환경 설치하기

python3 -m pip install --user -U virtualenv
mkdir TEST
cd TEST
virtualenv env
pip3 install --upgrade jupyter matplotlib numpy pandas scipy scikit-learn
python3 -m ipykernel install --user --name=python3

코드를 통한 데이터 다운로드 방법

import os
import tarfile
import urllib

DOWNLOAD_ROOT = ""
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):
	os.makedirs(housing_path, exist_ok=True)
    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()

데이터 읽기

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)

데이터 세트 전처리 

설문조사기관에서 1000명에게 질문을 하려고 할 때, 1000명을 무작위로 뽑진 않는다. 전체 인구를 대표할 수 있는 1000명을 선택하기 위해 노력해야 하는 것이다. 예를 들어, 미국 인구의 51.3%가 여성이고, 48.7%가 남성이라면 샘플에서도 이 비율을 유지해야 한다. 즉 테스트 세트가 전체 인구를 대표하도록 각 계층에서 올바른 수의 샘플 추출이 필요. 

# 소득 카테고리 구분
housing["income_cat"] = pd.cut(housing["median_income"], 
			bins=[0., 1.5, 3.0, 4.5, 6., np.inf], 
            		lables=[1, 2, 3, 4, 5])

# 소득 카테고리를 기반으로 계층 샘플링 준비
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_test = housing.loc[train_index]
    strat_test_set = housing.loc[test_index]
    
# 테스트 세트에서 소득 카테고리 비율 확인    
strat_test_set["income_cat"].value_counts() / len(strat_test_set)

# income_cat 특성을 삭제하여 데이터를 원래 상태로
for set_ in (strat_train_set, strat_test_set):
	set_.drop("income_cat", axis=1, inplace=True)

데이터 시각화

# alpha: 투명도 조정
housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.1)

# 원의 반지름은 인구 크기 - 매개변수 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,
)
plt.legend()
# 숫자형 특성 사이에 산점도를 그려주는 함수
from pandas.plotting import scatter_matrix

# 디폴트는 모든 숫자형 특성을 반영하여 그래프를 그리는 것으로 특성이 많으면 attributes처럼 일부 선택
attributes = ["median_house_value", "median_income", "total_rooms", "housing_median_age"]
scatter_matrix(housing[attributes], figsize=(12, 8))

# 대각선 방향(왼쪽 위에서 오른쪽 아래)은 각 변수 자신에 대한 것으로 그냥 직선
# 판다스는 이곳에 디폴트로 각 특성의 히스토그램 출력
# diagonal 매개변수에 'kde'를 입력하면 커널 밀도 추정(Kernel Density Estimation) 출력

데이터 정제 방법

# 옵션1
housing.dropna(subset=["total_bedrooms"])

# 옵션2
housing.drop("total_bedrooms", axis=1) 

# 옵션3
median = housing["total_bedrooms"].median() 
housing["total_bedrooms"].fillna(median, inplace=True)

# 사이킷런 SimpleImputer 활용
from sklearn.impute import SimpleImputer

imputer = SimpleImputer(strategy="median")
housing_num = housing.drop("ocean_proximity", axis=1) # 텍스트 특성 제외
imputer.fit(housing_num)
# imputer.statistics_ <= imputer는 계산한 중간값을 객체의 statistics_ 속성에 저장
housing_num.median().values
X = imputer.transform(housing_num) # 훈련 세트에서 누락된 값을 학습한 중간값으로 대체
housing_tr = pd.DataFrame(X, columns=housing_num.columns, index=housing_num.index)

텍스트(범주형 특성)를 숫자로 변환

from sklearn.preprocessing import OrdinalEncoder
ordinal_encoder = OrdinalEncoder()
housing_cat_encoded = ordinal_encoder.fit_transform(housing_cat)
housing_cat_encoded[:10]
ordinal_encoder.categories_ # 인스턴스 변수를 사용해 카테고리 목록 얻기

위의 문제점은 머신러닝 알고리즘이 가까이 있는 두 값이 떨어져 있는 두 값보다 더 비슷하다고 생각한다는 점이다. 이를 해결하기 위해 한 특성만 1이고 나머지는 0인 원-핫 인코딩을 사용해야 한다. 

from sklearn.preprocessing import OneHotEncoder
cat_encoder = OneHotEncoder()
housing_cat_1hot = cat_encoder.fit_transform(housing_cat)
housing_cat_1hot

출력은 넘파이 배열이 아닌 사이파이(SciPy) 희소 행렬이다. 이는 수천 개의 카테고리가 있는 범주형 특성일 경우 매우 효율적이다. 이를 원-핫 인코딩하면 0으로 가득찬 행렬이 나오겠지만 희소 행렬은 0이 아닌 원소의 위치만 저장한다. 어쨋거나 이 행렬을 (밀집된) 넘파이 배열로 바꾸려면 toarray() 메서드를 호출하면 된다.

housing_cat_1hot.toarray()

여기서 잠깐 *args*kwargs에 대해 알아보고 넘어가자. 

*args = *arguments의 줄임말 
- 여러 개의 인자를 함수로 받고자 할 때 사용하며 타입은 튜플  
- 일반 변수보다 뒤에 위치해야 함

*kwargs = *keyword arguments의 줄임말
- (키워드 = 특정 값) 형태로 함수 호출하여 딕셔너리 타입으로 출력
- 일반 변수, *args 뒤에 위치해야 함

나만의 변환기 설정

get_params()set_params() 함수는 사이킷런의 파이프라인과 그리드 탐색에 꼭 필요한 메서드로 모든 추정기와 변환기는 BaseEstimator를 상속해야 한다. 이 두 메서드는 생성자에 명시된 매개변수만을 참조하므로 *args나 **kwargs는 사용할 수 없다. TransformerMixin을 상속하면 자동으로 fit(), transform(), fit_transform() 메서드를 생성한다. 

from sklearn.base import BaseEstimator, TransformerMixin

rooms_ix, bedrooms_ix, population_ix, households_ix = 3, 4, 5, 6

class CombiendAttributesAdder(BaseEstimator, TransformerMixin):
	def __init__(self, add_bedrooms_per_room = True) # args나 kwargs가 아님
    	self.add_bedrooms_per_room = add_bedrooms_per_room
    def fit(self, X, y=None):
    	return self
    def transform(self, X):
    	rooms_per_household = X[:, rooms_ix] / X[:, households_ix]
        population_per_household = X[:, population_ix] / X[:, households_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)

변환 파이프라인 설정

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)

데이터에 적용할 가장 중요한 변환 중 하나는 특성 스케일링(feature scaling)이다. 보통 타깃값에 대한 스케일링은 불필요하다. 전체 데이터가 아닌 훈련 데이터에만 fit() 메서드를 적용하고, 훈련 세트와 테스트 세트에 대해 tranform() 메서드를 사용한다. 

모든 특성의 범위를 같도록 만들어주는 방법으로 min-max 스케일링과 표준화가 있다. min-max는 정규화(regularization)이라고도 부르며 0~1 범위에 들도록 값을 이동하고 스케일을 조정하면 된다. 데이터에서 최솟값을 뺀 후 최댓값과 최솟값의 차이로 나누면 가능하다. 이를 위해선 사이킷런의 MinMaxScaler 변환기를 사용하면 된다.

표준화란 먼저 평균을 뺀 후 표준편차로 나누어 결과 분포의 분산이 1이 되도록 하는 것이다. min-max 스케일링과는 달리 표준화는 범위의 상한과 하한이 없어 문제가 될 수도 있다. 하지만 표준화는 이상치에 영향을 덜 받는다는 장점이 있다. 사이킷런의 StandardScaler 변환기를 사용하면 된다. 

하나의 변화기로 각 열마다 적절한 변환을 적용하여 모든 열을 처리할 수 있도록 사이킷런의 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(), cat_atrribs),
])

housing_prepared = full_pipeline.fit_transform(housing)

OneHotEncoder는 희소 행렬을 반환하지만 num_pipeline은 밀집 행렬은 반환한다. 이렇게 섞여 있을 때 ColumnTransformer는 최종 행렬의 밀집 정도를 추정한 후, 임곗값(기본적으로 sparse_threshold=0.3)보다 낮으면 희소 행렬을 반환한다. 

모델 선택과 훈련

1. 선형 회귀 모델

from sklearn.linear_model import LinearRegression

lin_reg = LinearRegression()
lin_reg.fit(housing_prepared, housing_labels)

2. 결정 트리

from sklearn.tree import DecisionTreeRegressor

tree_reg = DecisionTreeRegressor()
tree_reg.fit(housing_prepared, housing_labels)

3. 랜덤 포레스트

from sklearn.ensemble import RandomForestRegressor
forest_reg = RandomForestRegressor()
forest_reg.fit(housing_prepared, housing_labels)

RMSE 측정

from sklearn.metrics import mean_sqaured_error
housing_predictions = lin_reg.predict(housing_prepared) # tree_reg.predict
model_mse = mean_squared_error(housing_labels, housing_predictions)
model_rmse = np.sqrt(model_mse)

교차 검증을 사용한 평가

사이킷런의 k-겹 교차 검증(k-fold cross-validation) 기능을 사용한다. 결정 트리 모델을 10번 훈련하고 평가하는데, 매번 다른 폴드를 선택해 평가에 사용하고 나머지 9개 폴드는 훈련에 사용한다. 

from sklearn.model_selection import cross_val_score
scores = corss_val_score(tree_reg, housing_prepared, housing_labels, 
					scoring="neg_mean_squared_error", cv=10)

# 부호 주의
tree_rmse_scores = np.sqrt(-scores)

평균 제곱 오차가 작을수록 좋은 비용 함수이므로 부호가 반대가 되어야 scoring 매개변수 정의에 맞다. scoring 매개변수는 클수록 좋은 효용 함수를 기대하기 때문이다. 

메서드 정리

info(): # 전체 행 수, 각 특성의 데이터 타입, 널이 아닌 값의 개수 확인
value_counts(): # 카테고리 종류와 각 카테고리마다의 값 확인
describe(): # 숫자형 특성의 요약 정보(null 값 제외)
hist(): # 모든 숫자형 특성에 대한 히스토그램 출력(컴퓨터 그래픽 백엔드를 위한 주피터의 매직 명령 %matplotlib inline 필요)
pd.cut(): # 특정 데이터에 대해 범위를 지정하고 그에 따라 카테고리화
corr(): # 표준 상관계수 계산(피어슨의 r이라고도 부름) 
dropna(), drop(), fillna(): # 해당 구역 제거, 전체 특성 삭제, 어떤 값으로 채우기
728x90
반응형