Networks/Hands-On 머신러닝 정리

Hands-On Machine Learning 정리 - 머신러닝(Chapter 2: 프로젝트)

코딩하는 Español되기 2024. 10. 18. 20:00

Chap2 목차

1. 실제 데이터로 작업하기

    - 문제 정의

    - 성능 측정 지표 선택

2. 데이터 가져오기

    - 데이터 로드

3. 데이터 이해를 위한 탐색과 시각화

    - 지리적 데이터 시각화

    - 상관관계 조사

    - 특성조합으로 실행

4. 머신러닝 알고리즘을 위한 데이터 준비

    - 수치형 데이터

    - 텍스트와 범주형 특성

    - 변환기 및 특성 스케일링

    - 변환 파이프라인

5. 모델 선택과 훈련

    - 훈련 세트에서 훈련 & 평가

    - 교차 검증을 사용한 평가

    - 모델 저장

6. 모델 세부 튜닝

    - GridSearchCV(그리드 탐색)

    - RandomizedSearchCV(랜덤 탐색)

    - Emsemble(앙상블 방법)

    - 최상의 모델과 오차 분석

    - 테스트 세트로 시스템 평가하기

7. 론칭, 모니터링, 시스템 유지 보수

    - 론칭 방법

    - 모니터링

    - 백업


1. 실제 데이터로 작업하기

유명한 공개 데이터 저장소
- UC 어바인 머신러닝 저장소     
- 캐글 데이터셋
- AWS 데이터셋

메타 포털(공개 데이터 저장소 나열되어 있음)
- 데이터 포털
- 오픈 데이터 모니터
- 퀀들

인기 있는 공개 데이터 저장소가 나열된 다른 페이지
- 위키백과 머신러닝 데이터셋 목록
- Quora.com
- 데이터셋 서브레딧  

※ 해당 책에서는 StatLib 저장소에 있는 캘리포니아 주택 가격 데이터 셋을 사용함

1-1 문제 정의

- 해당 데이터의 경우 레이블된 훈련 샘플이 존재(= 기대 출력값; 구역의 중간 주택 가격) → 지도 학습 작업

- 값을 예측해야 함 + 예측에 사용할 특성이 여러 개(구역의 인구, 중간 소득 등) → 다중 회귀 문제

- If 각 구역마다 여러 값을 예측단변량 회귀 문제

  elseif 구역마다 여러 값을 예측다변량 회귀

- 데이터에 연속적인 흐름이 없음 So, 빠르게 변화하는 데이터에 적응할 필요 X and 데이터 적음 → 배치 학습

 

1-2 성능 측정 지표 선택

1-2-1 RMSE(Root Mean Square Error; 평균 제곱근 오차)

  : 회귀 문제의 전형적인 성능 지표로 오차가 커질수록 값이 커짐

    RMSE(X, h) = 가설 h를 사용하여 일련의 샘플을 평가하는 비용 함수

$$ RMSE(X,h) = \sqrt{\frac{1}{m}\sum_{i=1}^{m}(h(x^{(i)})- y^{(i)})^{2}} $$

m : RMSE를 측정할 데이터셋에 있는 샘플 수

$$ x^{(i)} : i 번째 샘플의 전체 특성값의 벡터 $$

$$ y^{(i)} : 해당 레이블(= 해당 샘플의 기대 출력값) $$

e.g. 첫 번째 구역의 경도(-118.29), 중간 소득($38,372), 주민(1,416명), 중간 주택 가격($156,400) 일 때

$$ X^{(1)} = \begin{pmatrix} -118.29 \\ 33.91 \\ 1,416 \\ 38,372 \end{pmatrix} $$

$$ y^{(1)} = 156,400 $$

 

X는 데이터셋에 있는 모든 샘플의 모든 특성값(레이블 제외)을 포함하는 행렬

샘플 하나가 하나의 행이어서 i번째 행은 x^(i)의 전치와 같고 아래와 같이 표기

$$ (x^{(i)})^{T} $$

※ 전치 : 열 벡터를 행벡터로 변환(반대로도 변환)

IF 첫 번째 구역이 앞의 예와 같다면 행렬 X는 아래와 같음

$$ X = \begin{pmatrix} (x^{(1)})^{T} \\ (x^{(2)})^{T} \\ ... \\ (x^{(1999)})^{T} \\ (x^{(2000)})^{T} \end{pmatrix}
=  \begin{pmatrix} -118.29 & 33.91 & 1,416 & 38,372 \\ ... & ... & ... & ... \\ \end{pmatrix} $$

 

h : 시스템의 예측 함수(가설; hypothesis)

    ● 시스템이 하나의 샘플 특성 벡터(아래) 를 받으면 그 샘플에 대한 예측값을 출력

$$ X^{(i)} $$

$$ \hat{y}^{(i)} = h(x^{(i)}) $$

    ● IF 시스템이 첫 번째 구역의 중간 주택 가격을 $158,400으로 예측한다면,

$$ \hat{y}^{(i)} = h(x^{(i)}) = 158,400 $$

    ● 구역에 대한 예측 오차

$$ \hat{y}^{(i)} - y^{(1)} = 2,000 $$

 

하지만 이상치로 보이는 구역이 많다고 하면, 평균 절대 오차(Mean absolute Error; 평균 절대 편차)를 고려할 수 있음

 

1-2-2 MAE(평균 절대 오차) : 모두 예측값의 벡터와 타깃값의 벡터 사이의 거리를 재는 방법(RMSE도 동일)

$$ MAE(X, h) = \frac{1}{m}\sum_{i=1}^{m}|h(x^{(i)})-y^{(i)})| $$

 

거리 측정에는 여러 가지 방법(= norm)이 가능

    RMSE(제곱항을 합한 것의 계산) 계산은 유클리디안 노름(L2 노름)라고도 하며 아래와 같이 표시

$$ \begin{Vmatrix} . \end{Vmatrix}_2   또는   \begin{Vmatrix} . \end{Vmatrix} $$

    ● 맨해튼 노름 : 절댓값의 합을 계산하는 것은 L1 노름에 해당하며 이는 도시 구획이 직각으로 나뉘어 있을 때

                             도시의 두 지점 사이의 거리를 측정하는 것과 같음

$$ \begin{Vmatrix} . \end{Vmatrix} $$

    ● 일반적으로 원소가 n개인 벡터 v의 L_k 노름은 아래와 같이 정의

$$ \begin{Vmatrix} V \end{Vmatrix}_k = (|v_o|^{k} + |v_1|^{k} + ... + |v_n|^{k} )^{(\frac{1}{k})} $$

    ● L_0은 단순히 벡터에 있는 0이 아닌 원소의 수이고, L_무한대는 벡터에서 가장 큰 절댓값이 됨

    ● 노름의 지수가 클수록 큰 값의 원소에 치우치며 작은 값은 무시하게 됨.

   So, RMSE가 MAE보다 조금 더 이상치에 민감 But, 이상치가 매우 드물면(like 종 모양 분포의 양 끝단) RMSE가 더 적합

 

2. 데이터 가져오기

[Python 코드]

더보기

[Import]

# 파이썬 ≥3.5 필수
import sys
assert sys.version_info >= (3, 5)

# 사이킷런 ≥0.20 필수
import sklearn
assert sklearn.__version__ >= "0.20"

# 공통 모듈 임포트
import numpy as np
import os

# 깔금한 그래프 출력을 위해
%matplotlib inline
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.rc('axes', labelsize=14)
mpl.rc('xtick', labelsize=12)
mpl.rc('ytick', labelsize=12)

 

[데이터 다운로드]

import os
import tarfile
import urllib.request
import pandas as pd

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):
    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()

def load_housing_data(housing_path=HOUSING_PATH):
    csv_path = os.path.join(housing_path, "housing.csv")
    return pd.read_csv(csv_path)

 

[데이터 구조 보기]

housing = load_housing_data()
housing.head()

- 데이터에 대한 간략한 설명과 전체 행 수, 각 특성의 데이터 타입, 널이 아닌 값의 개수 등을 확인 가능

housing.info()

- 범주형인 "ocean_proximity" 열의 카테고리와 구역 수[value_counts()]를 확인

housing["ocean_proximity"].value_counts()

- describe메소드 : 숫자형 특성의 요약 정보를 출력

housing.describe()
%matplotlib inline
import matplotlib.pyplot as plt
housing.hist(bins=50, figsize=(20,15))
save_fig("attribute_histogram_plots")
plt.show()

※ 히스토그램에서 확인 가능한 결과

[중요] 데이터를 살펴보기 전에 테스트 세트를 따로 두고 절대 보면 안 됨!!!

    1. 중간 소득 특성(median_income) 이 US 달러로 표현되지 않음 → IF 3이라는 값 = 약 30,000 달러

    2. 중간 주택 연도(housing_median_age)와 중간 주택 가격(median_house_value) 역시 최댓값과 최솟값이 한정

         - 중간 주택 가격은 타깃 속성(레이블)으로 사용되기 때문에 심각한 문제가 될 수 있음

           If $500,000을 넘어가더라도 정확한 예측값이 필요하다?

              방법 1) 한곗값 밖의 구역에 대한 정확한 레이블 구하기

              방법 2) 훈련 세트, 테스트 세트에서 이런 구역을 제거

    3. 특성들의 스케일이 서로 너무 다름 → 스케일링은 나중에 다룰 예정

    4. 많은 히스토그램의 꼬리가 두꺼움 → 가운데 왼쪽보다 오른쪽으로 더 멀리 뻗어 있음 → 정규화가 필요

 

[테스트 세트 나누기]

import numpy as np
from sklearn.model_selection import train_test_split

train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)

test_set.head()
housing["median_income"].hist()
housing["income_cat"] = pd.cut(housing["median_income"],
                               bins=[0., 1.5, 3.0, 4.5, 6., np.inf],
                               labels=[1, 2, 3, 4, 5])
                               
housing["income_cat"].value_counts()
housing["income_cat"].hist()

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]
    
strat_test_set["income_cat"].value_counts() / len(strat_test_set)
housing["income_cat"].value_counts() / len(housing)
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

compare_props

 - Income_cat 특성을 삭제하여 원래 상태로 바꾸기

for set_ in (strat_train_set, strat_test_set):
    set_.drop("income_cat", axis=1, inplace=True)

3. 데이터 이해를 위한 탐색과 시각화

3-1 지리적 데이터 시각화

[python 코드와 해석]

더보기

- 훈련 세트의 데이터가 자기에 그대로 사용 & 훈련 세트 손상 방지를 위해 복사본을 만들어 사용

housing = strat_train_set.copy()

- 모든 구역을 산점도로 만들어 시각화 : 지리 정보(위도/경도)가 있기 때문

housing.plot(kind="scatter", x="longitude", y="latitude")
# 저장을 원한다면
# save_fig("bad_visualization_plot")

- alpha 옵션을 0.1로 주어 포인트가 밀집된 영역을 보여주기

housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.1)
# 저장을 원한다면
# save_fig("better_visualization_plot")
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()

- 원의 반지름 : 구역의 인구(매개변수 s)

- 색상 : 가격(매개변수 c)

- 파란색(낮은 가격) → 빨간색(높은 가격) 범위를 가지는 jet 사용(매개변수 cmap)

주택 가격 = 지역(e.g. 바다와 인접한 곳)과 인구 밀도와 관련이 크다

 

- 캘리포니아 지도에 결합하여 더 확실하게 확인하기

# Download the California image
images_path = os.path.join(PROJECT_ROOT_DIR, "images", "end_to_end_project")
os.makedirs(images_path, exist_ok=True)
DOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml2/master/"
filename = "california.png"
print("Downloading", filename)
url = DOWNLOAD_ROOT + "images/end_to_end_project/" + filename
urllib.request.urlretrieve(url, os.path.join(images_path, filename))
import matplotlib.image as mpimg
california_img=mpimg.imread(os.path.join(images_path, filename))
ax = housing.plot(kind="scatter", x="longitude", y="latitude", figsize=(10,7),
                  s=housing['population']/100, label="Population",
                  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,
           cmap=plt.get_cmap("jet"))
plt.ylabel("Latitude", fontsize=14)
plt.xlabel("Longitude", fontsize=14)

prices = housing["median_house_value"]
tick_values = np.linspace(prices.min(), prices.max(), 11)
cbar = plt.colorbar(ticks=tick_values/prices.max())
cbar.ax.set_yticklabels(["$%dk"%(round(v/1000)) for v in tick_values], fontsize=14)
cbar.set_label('Median House Value', fontsize=16)

plt.legend(fontsize=16)
plt.show()

3-2 상관관계 조사

데이터 셋이 크지 않기에 모든 특성 간의 표준 상관계수(Standard Correlation Coefficient; 피어슨의 r)를 계산

※ 상관계수는 선형적인 상관관계만 측정 So, 비선형적인 관계는 잡을 수 없음.

아래 그림에서 마지막 줄에 있는 그래프들은 두 축이 완전히 독립적이지 않음에도 상관계수가 0 (= 비선형관계의 예)

두 번째 줄은 상관 계수가 1 | -1인 경우로 상관계수는 기울기와 상관없음

[Python 코드]

더보기
corr_matrix = housing.corr()
corr_matrix["median_house_value"].sort_values(ascending=False)

- median_house_value는 median_income이 올라갈 때 증가하는 경향이 있음

- median_house_value는 위도(latitude)와 약한 음의 상관관계(= 북쪽으로 갈수록 주택 가격이 조금씩 내려감)

- 특성 사이의 상관 계수를 확인하는 다른 방법으로 숫자형 특성 사이에 산점도를 그려주는 scatter_matrix 사용

숫자형 특성 11개이므로 121(11^2) 개만큼 그래프를 모두 나타낼 수 없기에

중간 주택가격과 상관관계가 높은 특성 몇 개만 산점도로 추출

from pandas.plotting import scatter_matrix

attributes = ["median_house_value", "median_income", "total_rooms",
              "housing_median_age"]
scatter_matrix(housing[attributes], figsize=(12, 8))

- median_house_value를 예측하는데 가장 유용할 것 같은 특성은 median_income. So 상관관계와 산점도를 확대

housing.plot(kind="scatter", x="median_income", y="median_house_value",
             alpha=0.1)
plt.axis([0, 16, 0, 550000])

※ 확인 가능한 결과

    1. 상관관계가 매우 강함(위쪽으로 향하는 경향 + 포인트들이 널리 퍼져있지 않음)

    2. 가격 제한값이 $ 500,000에서 수평선으로 잘 보임(직선에 가까운 형태)

    3. $ 450,000 근처에 수평선이 보이고 $ 350,000와 $ 280,000에도 있고 아래 조금 더 보임

        → 데이터에서 이상한 형태를 학습하지 않도록 제거해 주는 것이 좋음

- 조합으로 feature 생성: 가구당 방, 침실당 방, 가구당 인구

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)

housing.plot(kind="scatter", x="rooms_per_household", y="median_house_value",
             alpha=0.2)
plt.show()

4. 머신러닝 알고리즘을 위한 데이터 준비

※ 머신러닝 알고리즘을 위한 데이터 준비를 자동화해야 하는 이유

    ● 어떤 데이터셋에 대해서도 데이터 변환을 손쉽게 반복 가능

    ●  향후 프로젝트에 사용할 수 있는 변환 라이브러리를 점진적으로 구축하게 됨

    ●  실제 시스템에서 알고리즘에 새 데이터를 주입하기 전에 변환시키는 데 사용 가능

    ●  여러 가지 데이터 변환 시도와 어떤 조합이 가장 좋은 지 확인하기 편리 

○ 수치형 데이터에서 누락된 값을 다루기 위해 SimpleImputer 사용

○ 범주형 데이터에서 텍스트를 수치형으로 변환하기 위해 OrdinalEncoder & OneHotEncoder 클래스 사용

 

4-1 수치형 데이터 Python 코드

더보기

- 훈련 세트로 복원하고, 예측 변수와 타깃값에 같은 변형을 적용하지 않기 위해 예측 변수와 레이블 분리

housing = strat_train_set.drop("median_house_value", axis=1) # 훈련 세트를 위해 레이블 삭제
housing_labels = strat_train_set["median_house_value"].copy()

- 현재 total_bedrooms 특성에 값이 없는 경우가 있었음

    [해결방법] 

    1. 해당 구역 제거 : dropna()

    2. 전체 특성 제거 : drop()

    3. 다른 값으로 채우기(e.g. 0, 평균, 중간값 등) : 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)

 IF Choose Option3 → 훈련 세트에서 중간값 계산 후 누락된 값을 이 값으로 채워야 함

또한, 계산한 중간 값을 저장해서 나중에 평가 시 테스트 세트에 있는 누락된 값과 시스템이 실제 운영될 때 새로운 데이터에 있는 누락된 값을 채워 넣는 데 필요

 

[옵션 3을 사용]

- 누락된 값을 손쉽게 다루기 위해 SimpleImputer 객체 생성

- 중간값은 수치형 특성에서만 계산 가능하기에 텍스트 특성인 ocean_proximity를 제외한 데이터 복사본 생성

from sklearn.impute import SimpleImputer
imputer = SimpleImputer(strategy="median")

housing_num = housing.drop("ocean_proximity", axis=1)

- Inputer 객체의 fit() 메소드를 사용해 훈련 데이터에 적용

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)
housing_tr.loc[sample_incomplete_rows.index.values]

4-2 텍스트와 범주형 특성 Python코드

더보기

- 범주형 데이터인 ocean_proximity를 전처리

from sklearn.preprocessing import OrdinalEncoder

ordinal_encoder = OrdinalEncoder()
housing_cat_encoded = ordinal_encoder.fit_transform(housing_cat)
housing_cat_encoded[:10]

- categories_ 인스턴스 변수를 사용해 카테고리 목록 반환

  : 범주형 특성마다 카테고리들의 1D 배열을 담은 리스트가 반환

    ※ 해당 표현 방식의 문제 : 가까이 있는 두 값이 멀리 있는 두 값보다 비슷하다고 생각

    아래의 경우 0과 1 보다는 0과 4가 더 비슷함

    So, 카테고리별 이진 특성을 만들어 해결 가능(원-핫 인코딩: 한 특성만 1이고 나머지는 모두 0인 것)

ordinal_encoder.categories_

- 원-핫 인코딩 적용

○ 출력을 확인하면 넘파이 배열이 아닌 사이파이 희소 행렬

    → 수천 개의 카테고리가 있는 범주형 특성일 경우 효과적

from sklearn.preprocessing import OneHotEncoder

cat_encoder = OneHotEncoder()
housing_cat_1hot = cat_encoder.fit_transform(housing_cat)
housing_cat_1hot

- 밀집된 넘파이 배열로 바꾸기 위해 toarray() 메소드를 호출

housing_cat_1hot.toarray()

※ IF 카테고리 특성이 담을 수 있는 카테고리 수가 多 → 원-핫 인코딩하면 많은 수의 입력 특성 생성

   이런 경우 범주형 입력값을 특성과 관련된 숫자형 특성으로 바꾸고자 할 것

   (e.g. ocean_proximity → 해안까지 거리)

   아니면 각 카테고리를 임베딩(학습 가능한 저 차원 벡터로 바꾸기)할 수 있음

   훈련하는 동안 각 카테고리의 표현이 학습됨 → 표현 학습(representation Learning의 한 예, 13장 | 17장)

4-3 나만의 변환기 및 특성 스케일링

○ 타깃값에 대한 스케일링은 일반적으로 불필요

min-max 스케일링(= 정규화) | 표준화를 통해 모든 특성의 범위를 같도록 만들어지는 방법으로 스케일링

○ Sklearn의 MinMaxScaler을 사용(IF 0 ~ 1 사이 값을 원하지 않는다면 feature_range 매개변수로 범위 조정 가능)

표준화 : 평균을 뺀 후(So, 항상 평균이 0이 됨) 표준 편차로 나누어 결과 분포의 분산이 1이 되도록 함

    But min-max 스케일링과 달리 표준화는 범위의 상한, 하한이 없어 신경망과 같이 0 ~ 1 값을 기대하는 알고리즘에서는

   문제가 될 수 있음 하지만 이상치에 영향을 덜 받음

○ (중요) 모든 변환기에서 스케일링은 전체 데이터가 아닌 훈련 데이터에 대해서만 fit() 메소드를 진행해야 함. 그런 다음

   훈련 세트와 테스트 세트(그리고 새로운 데이터)에 대해 transform() 메소드를 사용할 것

더보기

[예시]

- add_bedrooms_per_room 하이퍼파라미터 하나를 가지며 기본값을 True로 지정

from sklearn.base import BaseEstimator, TransformerMixin

# 열 인덱스
rooms_ix, bedrooms_ix, population_ix, households_ix = 3, 4, 5, 6

class CombinedAttributesAdder(BaseEstimator, TransformerMixin):
    def __init__(self, add_bedrooms_per_room=True): # *args 또는 **kargs 없음
        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.to_numpy())

4-4 변환 파이프라인

- ColumnTransformer 사용법

- DataFrameSelector 변환기와 FeatureUnion를 사용한 예전 방식

더보기

- 수치형 특성을 전처리하는 간단한 파이프라인

Input : 연속된 단계를 나타내는 이름/추정기 쌍의 목록

Last Step : 변환기와 추정기를 모두 사용 가능

Else Step : 모두 변환기여야 함(= fit_transform() 메소드가 있어야함)

fit() 매소드를 호출 → 모든 변환기의 fit_transform() 메소드를 순서대로 호출하면서 한 단계의 출력을 다음 단계 입력으로 전달 → 마지막에는 fit() 메서드만 호출

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)

housing_num_tr

 

- 사이킷런 0.20 버전에서 ColumnTransformer에 
  하나의 변환기로 각 열마다 적절한 변환을 적용해 모든 열을 처리하기 위한 기능을 추가

○ 해당 클래스(ColumnTransformer)를 사용하여 주택 가격 데이터의 전체 변환을 적용하는 코드

    1. 수치형 열 이름의 리스트, 범주형 열 이름의 리스트 생성

    2. ColumnTransformer 객체 생성 → 튜플의 리스트를 받음

    3. 수치형 열 : 앞서 정의한 num_pipeline을 사용해 변환 → 밀집 행렬 반환

        범주형 열 : OneHotEncoder로 변환 → 희소 행렬 반환

    4. 마지막으로 ColumnTransformer를 주택 데이터에 적용

    5. 희소/밀집행렬이 섞여 있기 때문에 ColumnTransformer을 통해 최종 행렬의 밀집 정도 추정

         IF 밀집도가 임곗값(default: sparse_threshold=0.3) 보다 낮으면 희소 행렬 반환

         이 예에서는 밀집 행렬 반환

 

※ 튜플에 변환기 사용 대신 삭제하고 싶은 열이 있다면? drop 문자열로 지정

    변환 지정을 원하지 않을 열이 있다면? passthrough로 지정

    기본적으로 나머지열(나열되지 않은 열)은 삭제 But IF 다르게 처리하고 싶다면?

    remainder 하이퍼파라미터에 어떤 변환기 지정 가능

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_attribs),
    ])

housing_prepared = full_pipeline.fit_transform(housing)

housing_prepared

 

[판다스 DataFrame 열 일부를 선택하기 위해 DataFrameSelector 변환기와 FeatureUnion을 사용하는 옛날 방법]

- 하나의 큰 파이프라인에 이들을 모두 결합해 수치형, 범주형 특성을 전처리

from sklearn.base import BaseEstimator, TransformerMixin

# 수치형 열과 범주형 열을 선택하기 위한 클래스
class OldDataFrameSelector(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
        
num_attribs = list(housing_num)
cat_attribs = ["ocean_proximity"]

old_num_pipeline = Pipeline([
        ('selector', OldDataFrameSelector(num_attribs)),
        ('imputer', SimpleImputer(strategy="median")),
        ('attribs_adder', CombinedAttributesAdder()),
        ('std_scaler', StandardScaler()),
    ])

old_cat_pipeline = Pipeline([
        ('selector', OldDataFrameSelector(cat_attribs)),
        ('cat_encoder', OneHotEncoder(sparse=False)),
    ])

- ColumnTransformer의 결과(좌)와 동일(우)

from sklearn.pipeline import FeatureUnion

old_full_pipeline = FeatureUnion(transformer_list=[
        ("num_pipeline", old_num_pipeline),
        ("cat_pipeline", old_cat_pipeline),
    ])
    
old_housing_prepared = old_full_pipeline.fit_transform(housing)
old_housing_prepared

5. 모델 선택과 훈련

5-1 훈련 세트에서 훈련하고 평가

    - 선형 회귀 모델

    - DecisionTreeRegressor

    - RandomForestRegressor

더보기

- 선형 회귀 모델을 훈련

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))

 

- 전체 훈련 세트에 대한 회귀 모델의 RMSE 측정 (mean_square_error 함수를 사용)

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.19819848922

 

[해석]

대부분 구역의 중간 주택 가격은 $ 120,000 ~ $ 265,000 사이 But 예측 오차가 $ 68,628 인건 별로 좋은 예측값 X

→ 모델이 훈련 데이터에 과소적합된 사례

(모델이 강력하지 못하거나 좋은 예측을 할 만큼 충분한 정보 제공을 하지 못한 예)

방법 : 강력한 모델 선택 | 규제(규제 사용 안 했으므로 제외) | 특성 추가(e.g. 로그 스케일된 인구)

 

- DecisionTreeRegressor 훈련 : 복잡한 비선형 관계 찾을 수 있음(6장)

from sklearn.tree import DecisionTreeRegressor

tree_reg = DecisionTreeRegressor(random_state=42)
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 ← 과대적합된 것으로 예상 가능

 

5-2 교차 검증을 사용한 평가

더보기

- 사이킷런 k-fold cross-validation 기능 사용

  [5-fold 예제 사진]

    [코드]

    1. IF cv = 10 → 훈련 세트를 폴드(10개의 서브셋)로 무작위 분할

    2. 결정 트리 모델을 10번 훈련하고 평가

        → 매번 다른 폴드를 선택해 평가에 사용 & 나머지 9개 폴드는 훈련에 사용

    3. 10개의 평가 점수가 담긴 배열이 결과가 됨

from sklearn.model_selection import cross_val_score

scores = cross_val_score(tree_reg, housing_prepared, housing_labels,
                         scoring="neg_mean_squared_error", cv=10)
tree_rmse_scores = np.sqrt(-scores)

def display_scores(scores):
    print("점수:", scores)
    print("평균:", scores.mean())
    print("표준 편차:", scores.std())

display_scores(tree_rmse_scores)

※ scoring 매개변수에(낮을수록 좋은) 비용함수가 아니라 (클수록 좋은) 효용 함수를 기대

    So, 평균 제곱 오차(MSE)의 반댓값(음숫값)을 계산하는 neg_mean_squared_error 함수를 사용

    [MAE : 평균 절대오차, RMSE : 평균 제곱근 오차]

    그래서 코드에서 np.sqrt(-scores)에서 '-'를 넣어 부호를 변경해 줌

 

검증 결과 선형 회귀 모델보다 나쁜 성능(아래의 사진)을 보여줌

교차 검증으로 모델 성능 추정과 추정의 정확도(표준편차)를 측정 가능함

 

[선형 회귀 모델 교차검증 결과]

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)

 

[RandomForestRegressor 모델(7장) 사용]

- 랜덤 포레스트가 좋은 성능을 보였지만, 훈련 세트에 대한 점수가 검증 세트에 대한 점수보다 훨씬 낮음

   → 여전히 훈련세트에 과대적합된 상태

   [방법] 모델을 간단히 | 규제 | 훈련 데이터 더 쓰기

from sklearn.ensemble import RandomForestRegressor

forest_reg = RandomForestRegressor(n_estimators=100, random_state=42)
forest_reg.fit(housing_prepared, housing_labels)

housing_predictions = forest_reg.predict(housing_prepared)
forest_mse = mean_squared_error(housing_labels, housing_predictions)
forest_rmse = np.sqrt(forest_mse)
forest_rmse #18603.515021376355
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)

5-3 모델 저장

실험한 모델을 모두 저장하여 나중에 쉽게 복원할 수 있게 해야 함. (여러 모델의 점수와 모델이 만든 오차 비교가 쉬움)

So, 교차 검증 점수, 실제 예측값, 하이퍼파라미터, 훈련된 모델 파라미터 등 모두 저장(joblib | pickle 패키지)

import joblib

# 저장
joblib.dump(my_model, "my_model.pkl")

# 저장된 모델 사용
my_model_loaded = joblib.load("my_model.pkl")

6. 모델 세부 튜닝

6-1 GridSearchCV(그리드 탐색) : 가능한 모든 하이퍼파라미터 조합에 대해 교차 검증을 사용해 평가

더보기

- RandomForestRegressor의 최적의 하이퍼파라미터 조합 탐색

    ● param_grid 설정에 따라 사이킷런이 먼저 첫 번째 dict에 있는 n_estimators와 max_features의 조합인 12개 평가

    ● dict에 있는 하이퍼파라미터 조합 : 6(2 X 3) 개를 시도

    ● bootstrap 하이퍼파라미터를 True가 아닌 False로 설정

    ● 즉, 하이퍼파라미터 값의 18(12 + 6) 개의 조합을 탐색하고 각각 cv=5(5번) 모델을 훈련

    ● 결과로 최적의 조합을 얻을 수 있음( 'max_features': 8, 'n_estimators' : 30}

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)
grid_search.fit(housing_prepared, housing_labels)
grid_search.best_params_
grid_search.best_estimator_

 

[최적의 추정기에 직접 접근하는 방법]

- 테스트한 하이퍼파라미터 조합의 점수 확인

cvres = grid_search.cv_results_
for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]):
    print(np.sqrt(-mean_score), params)

6-2 RandomizedSearchCV(랜덤 탐색) : 하이퍼파라미터 탐색 공간이 커지면 랜덤 서치가 더 적합

    ● 장점 : IF 랜덤 탐색을 1,000회 실행 → 하이퍼파라미터마다 각기 다른 1,000개의 값을 탐색

                 (그리드 탐색에서는 하이퍼파라미터마다 몇 개의 값만 탐색)

                단순히 반복 횟수 조절만으로 하이퍼파라미터 탐색에 투입할 컴퓨팅 자원 제어 가능

더보기
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)
rnd_search.fit(housing_prepared, housing_labels)
cvres = rnd_search.cv_results_
for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]):
    print(np.sqrt(-mean_score), params)

6-3 Emsemble(앙상블 방법) : 최상의 모델을 연결해 보기(7장)

6-4 최상의 모델과 오차 분석

    ● 최상의 모델을 분석하면 문제에 대한 좋은 통찰을 얻을 수 있음

        e.g. RandomForestRegressor가 정확한 예측을 만들기 위한 각 특성의 상대적 중요도를 알려줌

더보기
feature_importances = grid_search.best_estimator_.feature_importances_
feature_importances

- 중요도에 대응하는 특성 이름을 표시

  : 아래 정보를 바탕으로 덜 중요한 특성을 제외시킬 수 있음

    (e.g. ocean_proximity 카테고리 중 하나만 실제로 유용하기에 다른 카테고리 제외 가능)

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"]
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)

6-5 테스트 세트로 시스템 평가하기

- 테스트 세트에서 예측 변수와 레이블 받기 → full_pipeline을 사용해 데이터 변환 → 테스트 세트에서 최종 모델 평가

(중요) 테스트 세트는 훈련하면 안 되므로 fit_transform()이 아닌 transform()을 호출해야 함!! 

- scipy.stats.t.interval() : 일반화 오차의 95% 신뢰 구간 계산

더보기
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 # 47,730.0 출력

어떤 경우에는 이런 일반화 오차의 추정이 론칭을 결정하기에 충분하지 않을 것

IF 현재 제품 시스템에 있는 모델보다 불과 0.1% 높은 경우?? 추정값의 정확도를 알고자 할 것

이를 위해 scipy.stats.t.interval()를 사용해 일반화 오차의 95% 신뢰 구간을 계산 가능

from scipy import stats

confidence = 0.95
squared_errors = (final_predictions - y_test) ** 2
np.sqrt(stats.t.interval(confidence, len(squared_errors) - 1,
                         loc=squared_errors.mean(),
                         scale=stats.sem(squared_errors)))

7. 론칭, 모니터링, 시스템 유지보수

7-1 론칭 방법

    ● 설루션 론칭 허가 → 제품 시스템에 적용하기 위한 준비(문서 & 테스트 케이스 작성) → 모델을 상용환경에 배포

    [공통]

    전체 파이프라인과 예측 파이프라인이 포함된 훈련된 사이킷런 모델을 저장 → 훈련된 모델을 상용환경에 로드

    웹서비스의 경우 예측하기 버튼 클릭 → 웹서버로 쿼리 전송 → 웹 애플리케이션으로 전달 → predict() 호출(예측 생성)

    [방법 1] 

    웹 애플리케이션이 RESTAPI를 통해 질의 가능한 전용 웹서비스로 감싸기

    - 장점 : 주 애플리케이션을 안 건드리고 새 버전으로 업그레이드하기 쉬움

                필요한 만큼 웹 서비스를 시작 & 웹 애플리케이션에서 웹 서비스로 오는 요청을 로드밸런싱 가능(확장 쉬움)

   [방법 2] 구글 클라우드 AI 플랫폼(ML Engine)과 같은 클라우드에 배포하기

   모델을 구글 클라우드 스토리지(GCS)에 업로드

    → 구글 클라우드 AI 플랫폼에서 새로운 모델 버전 생성 후 GCS 파일을 지정

 

7-2 모니터링

실시간으로 시스템 성능 체크 및 성능이 떨어졌을 때 알람을 통지할 수 있는 모니터링 코드 작성

why? 시스템 고장 같은 갑작스러운 성능 감소와 기술 발전으로 서서히 성능이 감소하는 상황

그렇기 위해서 모델의 실전 성능 모니터링이 필수

    [방법] 하위 시스템의 지표로 모델 성능 추정 가능

    e.g. 모델이 추천 시스템의 일부 → 사용자가 관심 가질만한 제품을 추천하고 매일 추천 상품의 판매량 모니터링

어떻게든 모니터링 시스템이 필요하고 모델이 실패했을 경우 무엇을 하고 대비 방법 등 관련 프로세스 준비가 필수

이러한 작업은 모델 작성 및 훈련하는 것보다 일이 많은 경우가 多

전체 과정에서 가능한 많은 것을 자동화해서 시간을 단축해야 함

    [자동화 가능한 일부 작업]

    ● 정기적으로 새로운 데이터 수집하고 레이블 달기

    ● 모델 훈련하고 하이퍼파라미터를 자동으로 세부 튜닝하는 스크립트 작성

    ● 작업에 따라 매일 | 매주 자동으로 스크립트 실행

    ● 업데이트된 테스트 세트에서 새로운 모델과 이전 모델을 평가하는 스크립트

       → 성능이 감소하지 않으면 새로운 모델을 제품에 배포

 

7-3 백업 : 롤백하기 위한 절차와 도구 준비 필요

    ※ 데이터 일부분에 대해 모델이 얼마나 잘 작동하는지 평가를 위해 테스트 세트를 여러 서브셋으로 나누는 경우가 있음

    ※ e.g. 가장 최근 데이터를 담은 서브셋을 만들거나 특별한 종류의 입력을 위한 서브셋이 필요할 수 있음

 

2장에서 실제 데이터로 실무에서 사용할 수 있는 작업 방법과 시각화, 탐색 등에 관한 방법과 모니터링, 시스템 유지 보수의 중요성을 강조하였습니다.

그중에서 모델 작성과 파악도 중요하지만 모니터링 및 시스템 유지 보수, 자동화의 가장 중요함을 특히 강조하고 있었습니다. 

이후 모델을 만들게 되면 유지 보수 및 모니터링하는 스크립트를 작성해 봐야겠습니다.