프로젝트를 어떻게 구성할까?
프로젝트는 다음의 순서로 정의한다.
- 큰 그림을 봐라
- 데이터를 구해라
- 데이터를 분석해라
- 머신러닝 알고리즘을 정의하고 그에 맞는 데이터를 준비해라
- 모델을 선택하고 train해라
- 모델을 상세하게 조정해라
- 솔루션을 제시해라
- 시스템을 론칭하고 모니터링하고 유지보수해라
2장에서는 위의 과정들을 주택가격을 예측하는 예제를 이용해 설명하고 있다.
큰 그림을 보자
1.큰 그림의 시작은 문제의 정의이다.
조금더 구체화 하면 비지니스의 목적이 정확히 무엇인지다.
책의 예제에서는 이 모델의 출력(예측 가격)이 여러 가지 다른 신호와 함께 다른 머신러닝 시스템에 입력으로 사용되고 뒤따르는 시스템이 해당지역에 투자할 가치가 있는지 결정한다.
2.현재의 솔루션을 이해한다.
현재 상황은 구역 주택 가격을 전문가가 수동으로 추정하고 한팀이 한 구역에 관한 최신 정보를 모으고 있는데 중간 주택 가격을 얻을 수 없을 때는 복잡한 규칙을 사용하여 추정한다고 한다.
이는 비용과 시간이 많이 들고 추정한 결과의 정확도도 낮다.
3.가진 정보들을 조합하여 시스템을 설계한다.
이미 모여진 정보가 있다.(지도? 비지도?) -> 지도학습
값을 예측한다.(분류? 회귀?) -> 회귀
예측에 사용할 특성이 여러개 -> 다중회귀 (multiple regression)
각 구역마다 하나의 값을 예측(단변량, 다변량) -> 단변량 회귀 (univariate regression)
4.성능 측정 지표를 선택한다.
회귀문제의 성능 측정 지표에는 전형적으로 평균 제곱근 오차(Root Mean Square Error, RMSE)와 평균 절대 오차(Mean Absolute Error, MAE)가 있다.
각각의 식은 다음과 같다.
\(RMSE(X,h)= \sqrt{ \frac{1}{m}\sum_{i=1}^m (h(\mathbf{x^{(i)}})-y^{(i)})^2}\).
\(MAE(X,h)= \frac{1}{m}\sum_{i=1}^m \begin{vmatrix}h(\mathbf{x^{(i)}})-y^{(i)} \end{vmatrix}\).
둘 모두 예측값과 타깃값의 벡터 사이의 거리를 재는 방법이다. 거리 측정에는 여러 가지 방법 (혹은 Norm)이 가능하다.
- RMSE 계산은 우리에게 친숙한 유클리디안 노름(Euclidean norm), 유클리디안 거리이다. \(l_2\) norm이라고도 부르며 \(\begin{Vmatrix} \centerdot \end{Vmatrix}_2\)(또는 그냥 \(\begin{Vmatrix} \centerdot \end{Vmatrix}\))로 표시한다.
- MAE는 \(l_1\) norm 이라고도 부르며 \(\begin{Vmatrix} \centerdot \end{Vmatrix}_1\)로 표기하고 도시의 구획이 직각으로 나뉘어 있을 때 이 도시의 두 지점 사이의 거리를 측정하는 것과 같아 맨해튼 노름(Manhattan norm)이라고도 한다.
- norm은 일반적으로 \(l_k\)일 때 \(\begin{Vmatrix} \mathbf{v} \end{Vmatrix}_k = ( \begin{vmatrix}v_1 \end{vmatrix}^{k} + \begin{vmatrix} v_2 \end{vmatrix}^{k} + \cdots+ \begin{vmatrix}v_n \end{vmatrix}^{k})^{ \frac{1}{k} }\)이다.
- norm의 지수가 클수록 큰 값의 원소에 치우치며 작은 값은 무시된다. 그래서 RMSE가 이상치에 민감하다. 하지만 반대로 이상치가 적다면 RMSE가 잘 맞는다.
데이터를 구하자
우선 책과 최대한 동일한 환경을 만들자.
아나콘다를 설치하고 아나콘다 프롬프트 창에서 다음과 같이 입력한다.
개인적으로 문제가 제일 안일어났었던게 3.6.2 버전이라 선호한다. 요즘에는 3.7도 많이 안정되어있을 것이다.
conda create --name [환경이름] python==3.6.2
ex) conda create --name hands python==3.6.2
다음은 환경을 활성화 하고 필요한 라이브러리를 설치한다.
conda activate [환경이름]
pip install pands scipy scikit-learn matplotlib
근데 사실 환경을 만들 때 다음과 같이 써도 무방하다.
conda create --name [환경이름] python==3.6.2 pands scipy scikit-learn matplotlib
다음은 생성된 환경을 주피터 노트북에 연동할 거다.
먼저 ipykernel을 설치하고 등록을한다.
pip install ipykernel
python -m ipykernel install --user --name [환경이름] --display-name "[표기하고싶은이름]"
이제 주피터로 접속하면 아나콘다로 설치한 환경을 선택할 수 있다.
만약 주피터의 단축키를 알고싶다면 Help-Keyboard Shortcuts를 보자
1.데이터 다운받기
이제 데이터를 다운 받을 것이다. 주소가 책에 나온것과 약간 차이가 있으니 참고하자
import os
import tarfile
import urllib
import pandas as pd
DOWNLOAD_ROOT = "https://github.com/ageron/handson-ml2/raw/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):
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()
def load_housing_data(housing_path=HOUSING_PATH):
csv_path = os.path.join(housing_path,"housing.csv")
return pd.read_csv(csv_path)
if not os.path.exists("datasets/housing/housing.csv"):
print("download")
fetch_housing_data()
2.데이터 구조보기
pandas의 head() method를 이용해 처음 다섯 행을 볼 수 있다.
housing = load_housing_data()
housing.head()
특성은 longitude, latitude, housing_median_age, total_rooms, total_bedrooms, population, households, median_income, median_house_value, ocean_proximity 등 10개이다.
pandas의 info() method를 사용하면 데이터에 대한 간략한 설명과 특히 전체 행 수, 각 특성의 데이터 타입과 null이 아닌 값의 개수를 확인하는 데 유용하다.
예를들어 total_bedrooms는 20640의 데이터 중 20433만 이 값을 가지고 있다는 것이다.
ocean_proximity의 경우 Data type이 object로 나와 있는데 csv 파일에서 읽었기 때문에 텍스트의 성격을 가지고 있을 것이다. 이 안에 어떤 카테고리가 있고 각 카테고리마다 얼마나 많은 구역이 있는지 value_counts() method로 확인 할 수 있다.
또한, describe() method를 사용하면 숫자형 특성의 요약 정보를 볼 수 있다.
count는 null 아닌 값의 갯수, mean은 평균, min은 최소값, max는 최대값, std는 표준편차, 숫자%는 백분위수이다.
다음은 각 숫자형 특성을 히스토그램으로 그려볼 것이다.
수평축은 주어진 값의 범위를 보여주고 수직축은 속한 샘플 수를 나타낸다.
매직 명령은 matplotlib이 주피터 자체의 백엔드를 사용하도록 설정하고 이렇게 하면 그래프는 노트북 안에 그려지게 된다.
%matplotlib inline # 주피터 노트북의 매직 명령
import matplotlib.pyplot as plt
housing.hist(bins=50, figsize=(20,15))
plt.show()
히스토그램을 보면 다음과 같은 사실을 파악할 수 있다.
- Median income (중간 소득) 특성이 US 달러로 표현되어 있지 않다. 스케일을 조정하고 상한이 15(실제는 15.0001), 하한이 0.5 (실제로는 0.4999)가 되도록 만들어져있다. (예를들어 3은 실제로 약 30000달러) 우리는 앞으로도 이렇게 전처리된 데이터를 다루는 상황을 많이 접할 것이고 데이터가 어떻게 계산된 것인지 이해하고 있어야 한다.
- housing median age(중간 주택 연도)와 median house value(중간 주택 가격) 역시 최대값과 최소값을 한정했다. median house value는 y_label( target )이기 때문에 문제가 될 수 있다. 가격이 한계값을 넘어가지 않도록 모델이 학습될 수 있다는 것이다. 실무에서는 클라이언트 팀(이 시스템의 출력을 사용할 팀)과 함께 검토하는 것이 좋다. 만약 최대값이 넘어가더라도 정확한 예측값이 필요하다고 한다면 선택할 수 있는 방법은 두가지이다.
- 한계값 밖의 구역에 대한 정확한 label을 구한다.
- 훈련 세트에서 이런 구역을 제거한다.($500,000가 넘는 값에 대한 예측은 정확도가 매우 낮게 되므로 테스트 세트에서도 제거한다.)
- 특성들의 스케일이 서로 상이하다. 때문에 스케일링이라는 작업을 해야 한다.
- 데이터가 골고루 퍼져있지 않고 히스토그램에 꼬리가 생겼다
3.테스트 세트를 만들자
책의 저자는 테스트 세트를 미리 보는 것을 매우 반대한다. 이 테스트 세트로 일반화 오차를 추정하면 매우 낙관적인 추정이 되며 시스템을 론칭했을 때 기대한 성능이 나오지 않을 것이다. 이를 데이터 스누핑 편향(Data snooping bias)라고 한다.
테스트 세트를 생성하는 일은 이론적으로는 매우 간단하다. 그냥 무작위로 어떤 샘플을 선택해서 데이터 셋의 20% 정도(데이터가 클 수록 이 비율은 줄어든다)를 떼어놓으면 된다.
일반적으로 랜덤 함수를 사용하는데 실행될때마다 테스트 세트가 바뀌는 것을 방지하기 위해 시드(seed)를 사용하기도 한다. 하지만 이 방식은 업데이트된 데이터셋을 사용하려면 문제가 된다.
데이터셋의 업데이트 이후에도 안정적인 훈련/테스트 분할을 위한 일반적인 해결책은 샘플의 식별자를 사용해 테스트 세트로 보낼지 말지 정하는 것이다. 예를들어, 각 샘플마다 식별자의 해시값을 계산하여 해시 최대값의 20%보다 작거나 같은 샘플만 테스트 세트로 보내는 방식이다.
코드는 아래와 같다.
from zlib import crc32
import numpy as np
def test_set_check(identifier, test_ratio):
return crc32(np.int64(identifier)) & 0xffffffff < test_ratio * 2**32
def split_train_test_by_id(data,test_ratio,id_column):
ids = data[id_column]
in_test_set = ids.apply(lambda id_: test_set_check(id_,test_ratio))
return data.loc[~in_test_set], data.loc[in_test_set]
housing_with_id = housing.reset_index()
train_set,test_set = split_train_test_by_id(housing_with_id,0.2,"index")
사이킷런에서는 데이터셋을 여러 서브셋으로 나누는 다양한 방법을 제공한다.
아래의 함수에서는 난수 초기값을 지정(rondom_state 파라미터) 할 수 있고, 행의 개수가 같은 여러 개의 데이터셋을 넘겨서 같은 인덱스를 기반으로 나눌 수 있다(데이터 프레임이 레이블에 따라 여러개로 나뉘어 있을 때 유용하다).
from sklearn.model_selection import train_test_split
train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)
이러한 방식들은 데이터셋이 충분히 클때는 일반적으로 괜찮지만, 그렇지 않다면 샘플링 편향이 생길 가능성이 크다. 미국 인구의 51.3%가 여성이고 48.7%가 남성이라면 잘 구성된 설문조사는 샘플에서도 이 비율을 유지해야 한다. 이를 계층적 샘플링(stractified sampling)이라고 한다.
전체 인구는 계층(strata)라는 동질의 그룹으로 나뉘고, 테스트 세트가 전체 인구를 대표하도록 각 계층에서 올바른 수의 샘플을 추출한다.
전문가가 median_income이 median_house_value를 예측하는 데 매우 중요시 한다고 가정하자.
이 경우 테스트 세트가 전체 데이터셋에 있는 여러 소득 카테고리를 잘 대표해야 한다. 중간 소득이 연속적인 숫자형 특성이므로 소득에 대한 카테고리 특성을 만들어야 한다.
계층별로 데이터셋에 충분한 샘플 수가 있어야한다. 그렇지 않으면 계층의 중요도를 추정하는데 편향이 발생할 것이다. 즉, 너무 많은 계층올 나누면 안 되고 각 계층이 충분히 커야한다.
책의 예제에서는 카테고리 1은 0~1.5까지로 $15,000 단위로 나누었다.
여기까지 되었으면 사이킷런의 StratifiedShuffleSplit을 사용할 수 있다.
from sklearn.model_selection import StractifiedShuffleSplit
split = StractifiedShuffleSplit(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]
이전과 비슷한 비율로 테스트 세트가 뽑혔는지 확인해보자
다음의 코드를 입력하면 income_cat 특성을 삭제해서 데이터를 원래 상태로 되돌린다.
for set_ in (strat_train_set,strat_test_set):
set_.drop("income_cat",axis=1,inplace=True)
데이터를 이해해보자
1.지리적 데이터를 시각화해보자
이번에는 다뤄야할 데이터의 종류를 파악하기 위해 데이터를 살펴본다.
훈련 세트를 손상시키지 않기 위해 복사본을 만들어 사용할 것이다.
housing = strat_train_set.copy()
먼저 지리 정보(위도와 경도)를 이용해 모든 구역을 산점도로 만들어 데이터를 시각화 해보자
여기서 alpha 값을 설정하면 데이터 포인트가 밀집된 영역을 볼 수 있다.
이제 주택 가격을 나타낼 것이다. 원의 반지름은 구역의 인구를 나타내고(파라미터 s), 색상은 가격을 나타낸다(파라미터 c). 책의 예제에서는 미리 정의된 color map 중 파란색에서 빨간색 까지 범위를 가지는 jet을 사용한다(파라미터 cmap).
그림에서 주택 가격은 지역과 인구 밀도에 관련이 매우 크다는 것을 알 수 있다.
군집(Clustering) 알고리즘을 사용해 주요 군집을 찾고 군집의 중심까지의 거리를 재는 특성을 추가할 수도 있다.
2.상관관계 조사를 해보자
데이터셋이 너무 크지 않으므로 모든 특성 간의 표준 상관계수(standard correlation coefficient,혹은 Pearson의 r)를 corr() method를 이용해 쉽게 계산할 수 있다.
상관관계의 범위는 -1~1이고 1에 가까우면 강한 양의 상관관계를 가진다는 뜻이다. 예를들어 median_house_value는 median_income이 올라갈 때 증가하는 경향이 있다. 계수가 -1에 가까우면 강한 음의 상관관계가 있다는 것이고 마지막으로 계수가 0에 가까우면 선형적인 상관관계가 없다는 뜻이다.
하지만 상관관계는 선형적인 관계만 측정한다. 즉, 비선형 관계는 파악할 수 없다.
특성 사이의 상관관계를 파악하는 다른 방법으로는 숫자형 특성 사이에 산점도를 그려주는 pands의 scatter_matrix 함수를 사용하는 것이다.
아래는 median_house_value와 상관관계가 높아 보이는 특성에 대한 예이다.
왼쪽위에서 오른쪽 아래로는 변수 자신에 대한 것이라 그냥 직선이 된다.
이중 가장 상관관계가 강한 median_income만 봐보자
위 사진에서 두가지를 발견할 수 있다.
- 상관관계가 매우 강하다. 위쪽으로 향하는 경향을 볼 수 있으며 포인트들이 너무 널리 퍼져 있지 않다.
- 가격 제한값이 $500,000에서 수평선으로 잘 보인다. 그리고 부분적으로 $350,000와 $280,000에도 수평선이 보인다. 알고리즘이 데이터에서 이런 이상한 형태를 학습하지 않도록 해당 구역을 제거하는 것이 좋다.
3.특성 조합을 만들어보자
앞서 학습을 하기전 정제해야할 데이터들을 확인했고, 특성 사이에서 유용한 상관관계를 발견했다. 어떤 특성은 꼬리가 두꺼운 분포라서 데이터를 변형해야 한다.
학습전 마지막으로 해볼 수 있는 것은 여러 특성의 조합을 시도해보는 것이다. 예를들어 특정 구역의 방 개수는 가구 수가 얼마나 되는지 모른다면 그다지 유용하지 않다. 정확히 필요한 것은 가구당 방 개수이다. 침대수는 방당 침대수이다. 이를 이용하면 다음과 같은 새로운 특성을 만들어 볼 수 있다.
scatter_matrix를 다시 한번 보자
방당 침대의 수와 가격은 음의 상관관계가 조금 생겼다. 하지만 나머지 값은 이전과 크게 다르지 않다.
이 탐색 단계는 아직 완벽하지 않다.
머신러닝 알고리즘을 위한 데이터 준비
이 작업을 수동으로 하는 대신 함수를 만들어 자동화해야 하는 이유가 있다.
- 어떤 데이터셋에 대해서도 데이터 변환을 간편하게 반복할 수 있다.
- 향후 프로젝트에 사용할 수 있는 변환 라이브러리를 점진적으로 구축하게 된다.
- 실제 시스템에서 알고리즘에 새 데이터를 주입하기 전에 변환시키는 데 이 함수를 사용할 수 있다.
- 여러 가지 데이터 변환을 쉽게 시도해볼 수 있고 어떤 조합이 가장 좋은지 확인하는데 편하다.
먼저 훈련 세트로 복원하고 예측 변수와 타겟값에 같은 변형을 적용하지 않기 위해 분리한다.
housing = strat_train_set.drop("median_house_value", axis=1)
housing_labels = strat_train_set["median_house_value"].copy()
1.데이터를 정제하자
누락된 특성을 다루기 위해처리를 하는 함수를 만들것이다. 예를들어 total_bedrooms 특성에 값이 없는 경우가 있는데 이를 고치는 작업을 할 것이다.
방법은 아래와 같이 세가지가 있다.
- 해당 구역을 제거한다
- 전체 특성을 삭제한다
- 어떠한 값으로 채운다(0, 평균, 중간값 등)
데이터프레임의 dropna(), drop(), fillna() method를 이용해 이런 작업을 쉽게 할 수 있다.
print(housing.shape)
a=housing.dropna(subset=["total_bedrooms"]) # null값인 구역을 삭제한다.
b=housing.drop("total_bedrooms",axis=1) # 특성 자체를 삭제한다.
median = housing["total_bedrooms"].median() # 중간값계산.
housing["total_bedrooms"].fillna(median,inplace=True) # 빠진부분을 중간값으로 채워 넣는다.
print(a.shape)
print(b.shape)
이렇게 shape을 찍으면 이해가 좀더 빠를 것이다.
housing.shape = (16512, 9)
housing.dropna.shape = (16354, 9) 해당 특성에서 null 값인 행을 삭제했다.
housing.drop.shape = (16512, 8) 해당 특성을 삭제해서 열의 개수가 줄어들었다.
사이킷런의 SimpleImputer는 누락된 값을 쉽게 다룰 수 있게 해준다. 누락된 값을 특성의 중간값으로 대체한다고 지정하여 SimpleImputer의 객체를 생성한다.
from sklearn.impute import SimpleImputer
imputer=SimpleImputer(strategy="median")
먼저 ‘ocean_proximity’는 수치형 특성이 아니기 때문에 이 특성을 제외하고 복사를 한다.
housing_num = housing.drop("ocean_proximity",axis=1)
이제 imputer 객체의 fit() method를 사용해 훈련 데이터에 적용할 수 있다.
imputer는 각 특성의 중간값을 계산해서 그 결과를 객체의 statostocs_ 속성에 저장한다.
total_bedrooms 특성에만 누락된 값이 있지만 나중에 새로운 데이터에서도 값이 누락될 수 있으므로 모든 수치형 특성에 imputer를 적용하는 것이 좋다.
imputer의 statistics_에 저장된 값과 pandas의 median method를 비교하면 동일한 값임을 확인 할 수 있다.
imputer의 transform() method를 사용하면 numpy 배열로 바꿀 수도 있다. 또한 다시 pandas의 data frame으로 돌리는 것도 쉽게 가능하다.
2.텍스트와 범주형 특성을 다뤄보자
이번에는 수치형 특성이 아닌 ocean_proximity를 다뤄본다.
처음 10개 샘플에서 이 특성값을 확인해보자
이처럼 가능한 값이 제한된 개수로 나열되어 있고 각 값이 카테고리를 나타낼 때 범주형 특성이라고 한다.
대부분의 머신러닝 알고리즘은 숫자를 다루므로 카테고리들을 텍스트에서 숫자로 변환해보자.
사이킷런의 OrdinalEncoder class를 사용하면 쉽게 해결할 수 있다.
또한, categories_에서 카테고리 목록을 얻을 수 있다.
실제로 머신러닝 알고리즘에 이용하기 위해서는 숫자형으로 되어 있으면 안되고 카테고리별로 이진 특성을 만들어야 한다. 예를들어 카테고리가 5개 이고 INLAND는 1 index에 대응 하므로 이를 표현하면 [0,1,0,0,0]이 된다.
이러한 표현을 원-핫 인코딩(one-hot encoding)이라고 한다. 이따금 새로운 특성을 Dummy 특성이라고도 부른다.
사이킷런에서는 OneHotEncoder class를 사용하면 쉽게 구현할 수 있다.
출력에서 Sparse matrix임을 확인할 수 있는데 희소 행렬의 역할은 0을 모두 메모리에 저장하는 방식일 경우 카테고리의 수가 증가함에 따라 더욱더 낭비가 되므로 0이 아닌 원소의 위치만 저장한다.
넘파이 배열로 바꾸려면(이때는 dense 라는 표현을 쓰는 것 같다.) toarray() method를 호출하면 된다.
여기서도 categories_에 카테고리 리스트가 저장되어 있다.
3.변환기를 만들어보자
프로젝트를 진행하다보면 특별한 정제 작업이나 어떤 특성들을 조합하는 등 본인에게 맞는 변환기를 만들어야 할 때가 분명 존재한다. 그리고 사이킷런의 기능과 매끄럽게 연동하고 싶을 것이다.
사이킷런은 duck typing(상속이나 인터페이스 구현이 아닌, 객체의 속성 혹은 메서드가 객체의 유형을 결정하는 방식)을 지원하므로 fit(), transform(), fit_transform() 메서드를 구현한 파이썬 클래스를 만들면 된다. (참고로 fit_transform의 결과와 fit한 후 transform한 결과는 같지만 속도는 fit_transform이 더 빠르다.)
fit_transform()의 경우 TransformerMixin(이름에 Mixin이 있으면 객체의 기능을 확장하려는 목적으로 만들어진 클래스를 나타낸다)을 상속하면 자동으로 생성된다.
BaseEstimator를 상속하면 하이퍼파라미터튜닝에 필요한 get_params()와 set_params()를 추가로 얻을 수 있다.
이 두 기능은 사이킷런의 pipeline과 grid 탐색에 꼭 필요한 매서드이므로 모든 estimator와 transformer는 BaseEstimator를 상속해야 한다.
책에서는 다음과 같은 예제를 제공한다.
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):
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] # column 연결, concat(axis=1)로 생각해도 됨
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)
이러한 데이터 준비 단계를 자동화할수록 더 많은 조합을 자동으로 시도해볼 수 있고 최상의 조합을 찾을 가능성을 매우 높여준다.
4.특성을 스케일링해보자
특성 스케일링 (feature scaling)은 데이터에 적용할 가장 중요한 변환 중 하나이다.
머신러닝은 입력 숫자 특성들의 스케일이 많이 다르면 잘 작동하지 않기 때문이다. (타겟값에 대한 스케일링은 일반적으로 불필요하다.)
모든 특성의 범위를 같도록 만들어 주는 방법으로 Min-Max 스케일링(혹은 정규화(Normalization))과 표준화(Standardization)가 널리 사용된다.
Min-Max 스케일링은 0~1범위에 들도록 값을 이동하고 스케일을 조정하면 된다. 데이터에서 최소값을 뺀 후 최댓값과 최솟값의 차이로 나누면 가능하다. 사이킷런에는 이에 해당하는 MinMaxSaler 변환기를 제공한다. 0~1 사이를 원하지 않는다면 featrue_range 파라미터로 범위를 변경 할 수 있다.
표준화는 먼저 평균을 뺀 후(때문에 표준화를 하면 평균이 항상 0이다.) 표준편차로 나누어 결과 분포의 분산이 1이 되도록 한다. 표준화는 정규화와 다르게 범위의 상한과 하한이 없어 문제가 될 수 있지만 이상치(outlier)에 영향을 덜 받는다. 사이킷런에서는 StandardScaler 변환기를 사용하면 된다.
5.변환 Pipe-line
이 수 많은 변환 단계들은 정확한 순서대로 실행되어야 한다. 사이킷런에서는 연속된 변환을 순서대로 처리할 수 있도록 Pipeline class를 제공한다. 아래는 책에서 제공하는 숫자 특성을 처리하는 간단한 pipe-line 예제이다.
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)
pipeline은 연속된 단계를 나타내는 이름/추정기 쌍의 목록을 입력으로 받는다.
pipeline의 fit() method를 호출하면 모든 변환기의 fit_transform() method를 순서대로 호출하면서 한 단계의 출력을 다음 단계의 입력으로 전달한다. 마지막 단계에서는 fit() method만 호출한다. 따라서 마지막을 제외하면 모두 transformer여야 한다.(마지막은 estimator여도 된다.)
pipeline object는 마지막 estimator와 동일한 method를 제공한다. 위의 예제에서는 마지막 estimator가 transformer인 StandardScaler이므로 pipeline이 데이터에 대해 모든 변환을 순서대로 적용하는 transform() method를 가지고 있다.
지금까지 범주형 열과 수치형 열을 각각 다루었지만 하나의 transformer로 각 열마다 적절한 변환을 적용하여 모든 열을 처리하는 것이 더 편리할 것이다. 사이킷런 0.20 버전에서 이런 기능을 위해 ColumnTransformer가 추가되었다. 또한 이 클래스는 Pandas의 data frame과 잘 동작한다. 이 클래스를 사용해 주택 가격 데이터에 전체 변환을 적용한다.
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)
각 튜플은 이름, transformer, transformer가 적용될 열 이름(또는 index)의 list로 이루어진다.
각 변환기를 적절한 열에 적용하고 그 결과를 두 번째 축을 따라 연결한다.(때문에 변환기는 동일한 개수의 행을 반환해야 한다.)
OneHotEncoder는 희소 행렬을 반환하지만 ColumnTransformer는 최종 행렬의 밀집 정도를 추정한다(0이 아닌 원소의 비율을 추정한다). 밀집도가 임계값보다 낮으면 희소 행렬을 반환한다.
모델 선택과 훈련
1.훈련 세트에서 훈련하고 평가하기
먼저 선형 회귀 모델을 훈련 시켜보자.
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))
print("레이블:," ,list(some_labels))
아주 정확한 예측은 아니다. 이제 사이킷런의 mean_square_error 함수를 사용해 전체 훈련 세트에 대한 회귀 모델의 RMSE를 측정해보자
이 결과는 모델이 훈련 데이터에 Underfitting된 사례이다. 이 상황은 특성들이 좋은 예측을 만들 만큼 충분한 정보를 제공하지 못했거나 모델이 충분히 적절하지 못하다는 사실을 말해준다. 과소 적합을 해결하는 주요 방법은 더 강력한 모델을 선택하거나 훈련 알고리즘에 더 좋은 특성을 주입하거나 모델의 규제를 감소시키는 것이다. 이 모델은 규제를 사용하지 않았으므로 이 부분은 고려하지 않는다. 먼저 더 복잡한 모델을 시도해보자
이번에는 DecisionTreeRegressor를 훈련시켜보자.
결과가 0에 나오는 것은 사실 불가능에 가깝다. 이 결과는 모델이 훈련데이터에 Overfitting된 사례이다.
앞서 이야기 했듯이 모델이 론칭할 준비가 되기 전까지 test set을 하용하지 않을 것이다.
따라서 train set의 일부분으로 train을 하고 다른 일부분은 모델 검증에 사용해야 한다.
2.교차 검증을 사용한 평가
모델의 평가는 사이킷런의 k-겹 교차 ㄱ머증(k-fold cross-validation) 기능을 사용할 것이다.
훈련 세트를 폴드(fold)라 불리는 10개의 서브셋으로 무작위로 반할한다. 그런 다음 결정 트리 모델을 10번 훈련하고 평가하는데, 매번 다른 폴드를 선택해 평가에 사용하고 나머지 9개 폴드는 훈련에 사용한다.
10개의 평가 점수가 담긴 배열이 결과가 된다.
참고로 사이킷런의 교차 검증 기능은 scoring 파라미터에 낮을 수록 좋은 비용함수가 아니라 클수록 좋은 효용 함수를 입력해야 한다. 따라서 부호를 반대로 하는 과정이 있다.
다음은 결정트리와 선형 회귀 모델의 결과이다.
위의 결과를 비교해 보면 결정 트리 점수가 평균 70,552에서 2,537내의 차이를 보인다. (이 값은 달라질 수 있다.)
선형 회귀 모델은 평균 69,052에서 2,731내의 차이를 보인다.
결정트리는 과적합된 경우여서 현재는 선형 회귀 모델보다 성능이 나쁘다.
마지막으로 RandomForestRegressor 모델을 하나 더 시도해보자. 랜덤 포레스트는 특성을 무작위로 선택해서 많은 결정 트리를 만들고 그 예측을 평균 내는 방식으로 작동한다. 여러 다른 모델을 모아서 하나의 모델을 만드는 것을 앙상블(ensemble) 학습이라고 하며 머신러닝 알고리즘의 성능을 극대화하는 방법 중 하나이다.
결과는 이전보다 좋아 보이지만 여전히 훈련 세트에 대한 점수가 검증 세트에 대한 점수보다 훨씬 낮으므로 Overfitting되어 있다.(참고로 이전보다 오래걸린다.)
Overfitting을 해결하는 방법은 모델을 간단히 하거나, 규제를 하거나, 더많은 훈련데이터를 제공하는 것이다.
모델 세부 튜닝
이제 모델들을 세부 튜닝하는 방법 몇 개를 살펴보자
1.그리드 탐색
가장 단순한 방법은 괜찮은 하이퍼파라미터 조합을 찾을 때까지 수동으로 하이퍼파라미터를 조정하는 것이다. 많은 경우의 수를 탐색하기 때문에 시간이 많이 소요 될 수 있다.
사이킷런의 GridSearchCV를 사용할 수 있다. 탐색하고자 하는 하이퍼파라미터와 시도해볼 값을 지정하기만 하면 된다. 그러면 가능한 모든 하이퍼파라미터 조합에 대해 교차 검증을 사용해 평가하게 된다. 다음은 RandomForestRegressor에 대한 최적의 하이퍼파라미터 조합을 탐색하는 예제이다.
첫 번째 dict에 있는 n_estimators와 max_features 하이퍼 파라미터의 조합인 총 12개(n_estimatros의 length 3, max_features의 length 4의 곱)를 평가하고 두 번째는 bootstrap의 하이퍼파라미터를 False로 설정한 후 6개의 조합을 평가한다. cv=5로 설정되어 있어 5-겹 교차 검증이고 전체 훈련 횟수는 18 X 5 = 90이다. best_params_가 이 조합의 최적의 파라미터이다.
다음과 같이 최적의 estimator에 접근하거나 평가점수를 확인할 수 있다.
이때 RMSE 점수가 기존의 값보다 더 낮은 곳을 찾을 수 있다.
성공적으로 최적의 모델을 찾은 것이다.
추가적으로 그리드 탐색이 확실하지 않은 특성을 추가할지 말지 자동으로 정할 수 있다. 비슷하게 이상치나 값이 빈 특성을 다루거나 특성 선택 등을 자동으로 처리하는 데 그리드 탐색을 사용한다.
2.랜덤 탐색
그리드 탐색 방법은 비교적 적은 수의 조합을 탐구할 때 좋다.
하지만 하이퍼파라미터 탐색 공간이 커지면 랜덤 탐색을 사용하는 편이 더 좋다.(RandomizedSearchCV)
RandomizedSearchCV는 GridSearchCV와 거의 같은 방식으로 사용하지만 가능한 모든 조합을 시도하는 대신 각 반복마다 하이퍼파라미터에 임의의 수를 대입해 지정한 횟수만큼 평가한다.
이 방식의 장점은 다음 두 가지 이다.
- 랜덤 탐색을 1,000회 반복하도록 실행하면 하이퍼파라미터마다 각기 다른 1,000개의 값을 탐색한다.(그리드 탐색에서는 하이퍼파라미터마다 몇 개의 값만 탐색한다.)
- 단순히 반복 횟수를 조절하는 것만으로 하이퍼파라미터 탐색에 투입할 컴퓨팅 자원을 제어할수 있다.
3.최상의 모델과 오차 분석
정확한 예측을 만들기 위해 각 특성의 상대적인 중요도를 확인할 수 있다.
왼쪽이 중요도, 오른쪽이 그에 대응하는 특성 이름이다.
이 정보를 바탕으로 덜 중요한 특성들을 제외할 수도 있다.
예를들어 ocean_proximity의 카테고리 중 하나만 실제로 유용하므로 다른 카테고리는 제외할 수 있다.
4.테스트 세트로 시스템 평가하기
이제 최종 모델을 평가할 차례이다. 이 과정에서는 특별히 다른 점은 없고 테스트 세트에서 예측 변수와 레이블을 얻은 후 full_pipeline을 사용해 데이터를 변환하고 테스트 세트에서 최종 모델을 평가하면 된다.
이때 테스트 세트에서 훈련이 이루어지면 안되므로 fit_transform()이 아니라 transform()을 호출해야한다.
어떤 경우에는 이런 일반화 오차의 추정이 론칭을 결정하기에 충분하지 않을 것이다.
추정값이 얼마나 정확한지 확인하기 위해 scipy.stats.t.interval()을 사용한다.
이를 통해 일반화 오차의 95% 신뢰 구간(confidence interval)을 계산할 수 있다.
하이퍼파라미터 튜닝을 많이 했다면 교차 검증을 사용해 측정한 것보다 조금 성능이 낮은 것이 보통이다.
이 예제에서는 성능이 낮아지진 않았지만, 이 경우라도 테스트 세트에서 하이퍼파라미터를 튜닝하려 한다면 새로운 데이터에 일반화되기 어려운 모델이 만들어진다.
론칭 이후
모델이 잘 만들어지고 이를 배포했다고 끝나는게 아니다.
우리는 일정 간격으로 시스템의 실시간 성능을 체크하고 성능이 떨어졌을 때 알람을 통지할 수 있는 모니터링 코드를 작성해야 한다.
시간이 지나면서 모델이 낙후되는 경향이 있기 때문에 이러한 현상은 당연한 것이다.
우리는 어떠한 방식이든 모니터링 시스템을 준비해야 한다. 또한 모델이 실패했을 때 무엇을 할지 정의하고 어떻게 대비할지 관련 프로세스를 모두 준비해야 한다.
데이터가 계속 변화하면 데이터셋을 업데이트하고 모델을 정기적으로 다시 훈련해야 한다. 그리고 전체 과정에서 가능한 많은 것을 자동화해야 한다. 다음은 자동화할 수 있는 일부 작업이다.
- 정기적으로 새 데이터를 수집하고 레이블을 단다.(ex. 조사원 이용)
- 모델을 훈련하고 하이퍼파라미터를 자동으로 세부 튜닝하는 스크립트를 작성한다. 매일 또는 매주 자동으로 이 스크립트를 실행할 수도 있다.
- 업데이트된 테스트 세트에서 새로운 모델과 이전 모델을 평가하는 스크립트를 하나 더 작성한다. 성능이 감소하지 않으면 새로운 모델을 제품에 배포하고 성능이 나쁘다면 원인을 파악해야한다.
또한, 모델의 입력 데이터 품질을 평가해야 한다. 모델의 입력을 모니터링하면서 이러한 문제를 일찍 파악해야 한다.
예를 들어 점점 더 많은 입력에서 한 특성이 누락되거나 평균이나 표준편차가 훈련 세트와 멀어지거나 범주형 특성이 새로운 카테고리 값을 포함하는 경우 알람이 오도록 할 수 있다.
마지막으로 만든 모든 모델을 백업해야 한다. 새로운 모델이 어떤 이유로든 작동하지 않는 경우 이전 모델로 빠르게 롤백하기 위한 절차와 도구를 준비해야 한다. 백업을 가지고 있으면 새로운 모델과 이전 모델을 쉽게 비교할 수 있다.
비슷하게 새로운 버전의 데이터셋이 오염되었다면 롤백할 수 있도록 모든 버전의 데이터셋을 백업해야 한다.
Comments