2. 머신러닝 프로젝트 처음부터 끝까지
핸즈온 머신러닝 2판에서 공부했던 내용을 정리하는 부분입니다.
부동산 회사에 막 고용된 데이터 과학자라고 가정 후 프로젝트 진행
- 설정
- 2.1 실제 데이터로 작업하기
- 2.2 큰 그림 보기
- 2.3 데이터 가져오기
- 2.4 데이터 이해를 위한 탐색과 시각화
- 2.5 머신러닝 알고리즘을 위한 데이터 준비
- 2.6 모델 선택과 훈련
- 2.7 모델 세부 튜닝
- 2.8 론칭, 모니터링, 시스템 유지 보수
- 추가 내용
- 연습문제 풀이
- 출처
설정
# 파이썬 ≥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)
# 그림을 저장할 위치
PROJECT_ROOT_DIR = "."
CHAPTER_ID = "end_to_end_project"
IMAGES_PATH = os.path.join(PROJECT_ROOT_DIR, "images", CHAPTER_ID)
os.makedirs(IMAGES_PATH, exist_ok=True)
def save_fig(fig_id, tight_layout=True, fig_extension="png", resolution=300):
path = os.path.join(IMAGES_PATH, fig_id + "." + fig_extension)
print("그림 저장:", fig_id)
if tight_layout:
plt.tight_layout()
plt.savefig(path, format=fig_extension, dpi=resolution)
2.1 실제 데이터로 작업하기
- 실제 데이터로 실험하는 것이 가장 좋다
- StatLib 저장소의 캘리포니아 주택 가격 데이터셋 사용
2.2 큰 그림 보기
- 캘리포니아 인구 조사 데이터로 주택 가격 모델 만들기
- 캘리포니아 블록 그룹마다 인구, 중간 소득, 중간 주택 가격을 담고 있다.
- 구역의 중간 주택 가격 예측
2.2.1 문제 정의
- 비즈니스의 목적을 정확히 아는 것이 중요
- 파이프라인
- 데이터 처리 ‘컴포넌트’들이 연속되어 있는 것
- 보통 컴포넌트들은 비동기적으로 동작(각 컴포넌트 완전히 독립)
- 지도학습(레이블된 훈련 샘플 존재), 회귀(다중 회귀, 단변량 회귀), 배치학습
2.2.2 성능 측정 지표 선택
- 평균 제곱근 오차(RMSE)
- 회귀 문제의 전형적인 성능 지표
- 유클리디안 노름(Euclidean norm), l2노름
- 평균 절대 오차(평균 절대 편차, MAE)
- 맨해튼 노름(Manhattan norm), l1노름
- 노름의 지수가 클수록 큰 값에 치우쳐진다.
- RMSE가 MAE보다 이상치에 민감하다.
- 이상치가 드물면 RMSE가 맞아 일반적으로 널리 사용
2.2.3 가정 검사
- 마지막으로 지금까지의 가정들을 나열하고 검사하는 것이 좋다.
2.3 데이터 가져오기
2.3.1 작업환경 만들기
- 아나콘다
python=3.7
,tensorflow-gpu=2.6.0
(gpu 사용)의test3.7
가상환경 생성
2.3.2 데이터 다운로드
housing.tgz
다운로드 및 추출 함수
import os
import tarfile
import urllib.request
DOWNLOAD_ROOT="https://raw.githubusercontent.com/rickiepark/handson-ml2/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()
fetch_housing_data()
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)
housing=load_housing_data()
housing.head()
longitude | latitude | housing_median_age | total_rooms | total_bedrooms | population | households | median_income | median_house_value | ocean_proximity | |
---|---|---|---|---|---|---|---|---|---|---|
0 | -122.23 | 37.88 | 41.0 | 880.0 | 129.0 | 322.0 | 126.0 | 8.3252 | 452600.0 | NEAR BAY |
1 | -122.22 | 37.86 | 21.0 | 7099.0 | 1106.0 | 2401.0 | 1138.0 | 8.3014 | 358500.0 | NEAR BAY |
2 | -122.24 | 37.85 | 52.0 | 1467.0 | 190.0 | 496.0 | 177.0 | 7.2574 | 352100.0 | NEAR BAY |
3 | -122.25 | 37.85 | 52.0 | 1274.0 | 235.0 | 558.0 | 219.0 | 5.6431 | 341300.0 | NEAR BAY |
4 | -122.25 | 37.85 | 52.0 | 1627.0 | 280.0 | 565.0 | 259.0 | 3.8462 | 342200.0 | NEAR BAY |
housing.columns
Index(['longitude', 'latitude', 'housing_median_age', 'total_rooms',
'total_bedrooms', 'population', 'households', 'median_income',
'median_house_value', 'ocean_proximity'],
dtype='object')
- 데이터의 특성(10개)
- [‘longitude’, ‘latitude’, ‘housing_median_age’, ‘total_rooms’, ‘total_bedrooms’, ‘population’, ‘households’, ‘median_income’, ‘median_house_value’, ‘ocean_proximity’]
housing.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 20640 entries, 0 to 20639
Data columns (total 10 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 longitude 20640 non-null float64
1 latitude 20640 non-null float64
2 housing_median_age 20640 non-null float64
3 total_rooms 20640 non-null float64
4 total_bedrooms 20433 non-null float64
5 population 20640 non-null float64
6 households 20640 non-null float64
7 median_income 20640 non-null float64
8 median_house_value 20640 non-null float64
9 ocean_proximity 20640 non-null object
dtypes: float64(9), object(1)
memory usage: 1.6+ MB
info()
: 데이터에 대한 간략한 설명, 전체 행 수, 각 특성의 데이터 타입, 널이 아닌 값의 개수 확인에 유용
housing['ocean_proximity'].value_counts()
<1H OCEAN 9136
INLAND 6551
NEAR OCEAN 2658
NEAR BAY 2290
ISLAND 5
Name: ocean_proximity, dtype: int64
housing.describe()
longitude | latitude | housing_median_age | total_rooms | total_bedrooms | population | households | median_income | median_house_value | |
---|---|---|---|---|---|---|---|---|---|
count | 20640.000000 | 20640.000000 | 20640.000000 | 20640.000000 | 20433.000000 | 20640.000000 | 20640.000000 | 20640.000000 | 20640.000000 |
mean | -119.569704 | 35.631861 | 28.639486 | 2635.763081 | 537.870553 | 1425.476744 | 499.539680 | 3.870671 | 206855.816909 |
std | 2.003532 | 2.135952 | 12.585558 | 2181.615252 | 421.385070 | 1132.462122 | 382.329753 | 1.899822 | 115395.615874 |
min | -124.350000 | 32.540000 | 1.000000 | 2.000000 | 1.000000 | 3.000000 | 1.000000 | 0.499900 | 14999.000000 |
25% | -121.800000 | 33.930000 | 18.000000 | 1447.750000 | 296.000000 | 787.000000 | 280.000000 | 2.563400 | 119600.000000 |
50% | -118.490000 | 34.260000 | 29.000000 | 2127.000000 | 435.000000 | 1166.000000 | 409.000000 | 3.534800 | 179700.000000 |
75% | -118.010000 | 37.710000 | 37.000000 | 3148.000000 | 647.000000 | 1725.000000 | 605.000000 | 4.743250 | 264725.000000 |
max | -114.310000 | 41.950000 | 52.000000 | 39320.000000 | 6445.000000 | 35682.000000 | 6082.000000 | 15.000100 | 500001.000000 |
describe()
: 숫자형 특성의 요약 정보 보여줌
%matplotlib inline
import matplotlib.pyplot as plt
housing.hist(bins=50, figsize=(20,15))
save_fig("attribute_histogram_plots")
plt.show()
그림 저장: attribute_histogram_plots
%matplotlib inline
: 주피터 자체 백엔드를 사용하도록 지정 →IPython kernel 4.4.0, matplotlib 1.5.0
이상부터는 자동으로 주피터 자체 백엔드로 설정- 몇가지 사항 확인
- 중간 소득(median income) 특성 US달러로 표현 X → 스케일 조정과 상한 15, 하한 3으로 조정(ex) 3은 실제로 30,000달러 의미)
- 중간 주택 연도(housing_median_age), 중간 주택 가격(median_house_value)의 최대,최소값도 한정 → 중간 주택 가격은 타켓 변수로 두가지 선택 방법이 필요
- 한계값 밖의 구역에 대한 정확한 레이블 구함
- 훈련 세트에서 이런 구역 제거($500,000가 넘는 값에 대한 예측은 평가 결과가 나쁘다고 보고 테스트 세트에서도 제거)
- 특성들의 스케일이 서로 많이 다르다
- 많은 히스토그램들의 꼬리가 두껍다 → 나중에 종 모양의 분포로 변형 필요
CAUTION
데이터를 깊게 들여다가 보기 전에 테스트 세트를 따로 두고 절대 참고하면 안됨
2.3.4 테스트 세트 만들기
- 데이터 스누핑 편향(data snooping): 테스트 세트로 파악한 패턴에 맞는 머신러닝 모델을 선택하여 기대한 성능이 나오지 않는 것
# 노트북의 실행 결과가 동일하도록
np.random.seed(42)
import numpy as np
# 예시로 만든 것입니다. 실전에서는 사이킷런의 train_test_split()를 사용하세요.
def split_train_test(data,test_ratio):
shuffle_indices=np.random.permutation(len(data))
test_set_size=int(len(data)*test_ratio)
test_indices=shuffle_indices[:test_set_size]
train_indices=shuffle_indices[test_set_size:]
return data.iloc[train_indices],data.iloc[test_indices]
train_set,test_set=split_train_test(housing,0.2)
len(train_set)
16512
len(test_set)
4128
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에서 모두 잘 동작합니다. 초판에서는 모든 해시 함수를 지원하는 다음 방식을 제안했지만 느리고 파이썬 2를 지원하지 않습니다.
import hashlib
def test_set_check(identifier, test_ratio, hash=hashlib.md5):
return hash(np.int64(identifier)).digest()[-1] < 256 * test_ratio
모든 해시 함수를 지원하고 파이썬 2와 파이썬 3에서 사용할 수 있는 함수를 원한다면 다음을 사용하세요.
def test_set_check(identifier, test_ratio, hash=hashlib.md5):
return bytearray(hash(np.int64(identifier)).digest())[-1] < 256 * test_ratio
housing_with_id = housing.reset_index() # `index` 열이 추가된 데이터프레임을 반환합니다
train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "index")
housing_with_id["id"] = housing["longitude"] * 1000 + housing["latitude"]
train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "id")
test_set.head()
index | longitude | latitude | housing_median_age | total_rooms | total_bedrooms | population | households | median_income | median_house_value | ocean_proximity | id | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
8 | 8 | -122.26 | 37.84 | 42.0 | 2555.0 | 665.0 | 1206.0 | 595.0 | 2.0804 | 226700.0 | NEAR BAY | -122222.16 |
10 | 10 | -122.26 | 37.85 | 52.0 | 2202.0 | 434.0 | 910.0 | 402.0 | 3.2031 | 281500.0 | NEAR BAY | -122222.15 |
11 | 11 | -122.26 | 37.85 | 52.0 | 3503.0 | 752.0 | 1504.0 | 734.0 | 3.2705 | 241800.0 | NEAR BAY | -122222.15 |
12 | 12 | -122.26 | 37.85 | 52.0 | 2491.0 | 474.0 | 1098.0 | 468.0 | 3.0750 | 213500.0 | NEAR BAY | -122222.15 |
13 | 13 | -122.26 | 37.84 | 52.0 | 696.0 | 191.0 | 345.0 | 174.0 | 2.6736 | 191300.0 | NEAR BAY | -122222.16 |
- 무작위 샘플링
- 사이킷런의
train_test_split
random_state
: 난수 초깃값 지정 매개변수- 행의 개수가 같은 여러 개의 데이터 셋을 넘겨서 인덱스 기반으로 나눌 수 있다. (데이터 프레임이 레이블에 따라 여러 개로 나누어져 있을 때 매우 유용)
- 사이킷런의
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()
longitude | latitude | housing_median_age | total_rooms | total_bedrooms | population | households | median_income | median_house_value | ocean_proximity | |
---|---|---|---|---|---|---|---|---|---|---|
20046 | -119.01 | 36.06 | 25.0 | 1505.0 | NaN | 1392.0 | 359.0 | 1.6812 | 47700.0 | INLAND |
3024 | -119.46 | 35.14 | 30.0 | 2943.0 | NaN | 1565.0 | 584.0 | 2.5313 | 45800.0 | INLAND |
15663 | -122.44 | 37.80 | 52.0 | 3830.0 | NaN | 1310.0 | 963.0 | 3.4801 | 500001.0 | NEAR BAY |
20484 | -118.72 | 34.28 | 17.0 | 3051.0 | NaN | 1705.0 | 495.0 | 5.7376 | 218600.0 | <1H OCEAN |
9814 | -121.93 | 36.62 | 34.0 | 2351.0 | NaN | 1063.0 | 428.0 | 3.7250 | 278000.0 | NEAR OCEAN |
- 계층적 샘플링: 계층이라는 동질의 그룹으로 나뉘고 테스트 세트가 전체를 대표하도록 그룹 별로 올바른 수의 샘플을 추출
- 중간 소득이 중간 주택 가격 예측의 중요 변수라고 가정
- 소득에 대한 카테고리 특성 생성
housing['median_income'].hist()
<AxesSubplot:>
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()
3 7236
2 6581
4 3639
5 2362
1 822
Name: income_cat, dtype: int64
housing['income_cat'].hist()
<AxesSubplot:>
- 사이킷런의
StratifiedShuffleSplit
을 사용하여 소득 카테고리 기반으로 계층 샘플링 StratifiedShuffleSplit
StratifiedKFold
의 계층 샘플링과ShuffleSplit
의 랜덤 샘플링을 합친 것- 매개변수
test_size
+train_size
의 합을 1이하로 지정 가능
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)
3 0.350533
2 0.318798
4 0.176357
5 0.114341
1 0.039971
Name: income_cat, dtype: float64
housing['income_cat'].value_counts()/len(housing)
3 0.350581
2 0.318847
4 0.176308
5 0.114438
1 0.039826
Name: income_cat, dtype: float64
- 계층 샘플링의 경우 전체 데이터셋의 소득 카테고리 비율과 거의 유사
- 일반 무작위 샘플링은 많이 다르다
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
Overall | Stratified | Random | Rand. %error | Strat. %error | |
---|---|---|---|---|---|
1 | 0.039826 | 0.039971 | 0.040213 | 0.973236 | 0.364964 |
2 | 0.318847 | 0.318798 | 0.324370 | 1.732260 | -0.015195 |
3 | 0.350581 | 0.350533 | 0.358527 | 2.266446 | -0.013820 |
4 | 0.176308 | 0.176357 | 0.167393 | -5.056334 | 0.027480 |
5 | 0.114438 | 0.114341 | 0.109496 | -4.318374 | -0.084674 |
- 판다스 데이터프레임
drop
메서드axis
0: 행 삭제, 1: 열 삭제inplace = True
설정 시 호출된 객체에 새로운 데이터프레임 재할당하고 아무런 값도 반환 X
for set_ in (strat_train_set,strat_test_set):
set_.drop("income_cat", axis=1,inplace=True)
2.4 데이터 이해를 위한 탐색과 시각화
- 훈련 세트에 대해서만 탐색
- 훈련 세트의 크기가 매우 크면 별도로 샘플링할 수도 있음
- 복사본 만들어 탐색
housing = strat_train_set.copy()
2.4.1 지리적 데이터 시각화
housing.plot(kind="scatter", x="longitude", y="latitude")
save_fig("bad_visualization_plot")
그림 저장: bad_visualization_plot
housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.1)
save_fig("better_visualization_plot")
그림 저장: better_visualization_plot
sharex=False
매개변수는 x-축의 값과 범례를 표시하지 못하는 버그를 수정합니다. 이는 임시 방편입니다(https://github.com/pandas-dev/pandas/issues/10611 참조). 수정 사항을 알려준 Wilmer Arellano에게 감사합니다.
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()
save_fig("housing_prices_scatterplot")
그림 저장: housing_prices_scatterplot
주택 가격은 지역과 인구 밀집에 관련이 매우 크다.
# 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))
Downloading california.png
('.\\images\\end_to_end_project\\california.png',
<http.client.HTTPMessage at 0x1af7990e508>)
import matplotlib.image as mpimg
california_img=mpimg.imread(os.path.join(images_path,filename))
ax=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=False)
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)
save_fig("california_housing_prices_plot")
plt.show()
그림 저장: california_housing_prices_plot
2.4.2 상관관계 조사
corr_matrix = housing.corr()
corr_matrix["median_house_value"].sort_values(ascending=False)
median_house_value 1.000000
median_income 0.687151
total_rooms 0.135140
housing_median_age 0.114146
households 0.064590
total_bedrooms 0.047781
population -0.026882
longitude -0.047466
latitude -0.142673
Name: median_house_value, dtype: float64
- 상관 계수
- 선형적인 상관관계만 측정(비선형적 관계 알 수 없다)
- 상관계수 수치와 기울기는 관련성이 없다
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")
그림 저장: scatter_matrix_plot
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")
그림 저장: income_vs_house_value_scatterplot
- 중간 주택 가격(median_house_value)와 중간 소득(median income)의 상과관계 산점도
- 상관관계 매우 강함
- 앞서 본
$
500,000 과$
450,000,$
350,000,$
280,000에서 수평선의 분포 보임 → 이런 이상한 형태를 학습하지 않도록 해당 구역을 제거하는 것이 좋다.
2.4.3 특성 조합으로 실험
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.687151
rooms_per_household 0.146255
total_rooms 0.135140
housing_median_age 0.114146
households 0.064590
total_bedrooms 0.047781
population_per_household -0.021991
population -0.026882
longitude -0.047466
latitude -0.142673
bedrooms_per_room -0.259952
Name: median_house_value, dtype: float64
housing.plot(kind="scatter",x="rooms_per_household",y="median_house_value",alpha=0.2)
plt.show()
housing.describe()
longitude | latitude | housing_median_age | total_rooms | total_bedrooms | population | households | median_income | median_house_value | rooms_per_household | bedrooms_per_room | population_per_household | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
count | 16512.000000 | 16512.000000 | 16512.000000 | 16512.000000 | 16354.000000 | 16512.000000 | 16512.000000 | 16512.000000 | 16512.000000 | 16512.000000 | 16354.000000 | 16512.000000 |
mean | -119.575635 | 35.639314 | 28.653404 | 2622.539789 | 534.914639 | 1419.687379 | 497.011810 | 3.875884 | 207005.322372 | 5.440406 | 0.212873 | 3.096469 |
std | 2.001828 | 2.137963 | 12.574819 | 2138.417080 | 412.665649 | 1115.663036 | 375.696156 | 1.904931 | 115701.297250 | 2.611696 | 0.057378 | 11.584825 |
min | -124.350000 | 32.540000 | 1.000000 | 6.000000 | 2.000000 | 3.000000 | 2.000000 | 0.499900 | 14999.000000 | 1.130435 | 0.100000 | 0.692308 |
25% | -121.800000 | 33.940000 | 18.000000 | 1443.000000 | 295.000000 | 784.000000 | 279.000000 | 2.566950 | 119800.000000 | 4.442168 | 0.175304 | 2.431352 |
50% | -118.510000 | 34.260000 | 29.000000 | 2119.000000 | 433.000000 | 1164.000000 | 408.000000 | 3.541550 | 179500.000000 | 5.232342 | 0.203027 | 2.817661 |
75% | -118.010000 | 37.720000 | 37.000000 | 3141.000000 | 644.000000 | 1719.000000 | 602.000000 | 4.745325 | 263900.000000 | 6.056361 | 0.239816 | 3.281420 |
max | -114.310000 | 41.950000 | 52.000000 | 39320.000000 | 6210.000000 | 35682.000000 | 5358.000000 | 15.000100 | 500001.000000 | 141.909091 | 1.000000 | 1243.333333 |
- 침실/방, 가구당 방 개수 특성들은 기존의 특성들보다 중간 주택 가격과의 상관관계가 높다.
- 특히 머신러닝 프로젝트에서는 빠른 프로토 타이핑과 반복적인 프로세스가 권장됨
2.5 머신러닝 알고리즘을 위한 데이터 준비
- 이 작업을 수동으로 하는 대신 자동화 함수를 생성해야하는 이유
- 어떤 데이터 셋에 대해서도 데이터 변환을 쉽게 반복 가능 (ex: 다음번에 새로운 데이터셋 사용할 때)
- 향후 프로젝트에 사용 가능한 변환 라이브러리 점진적 구축
- 실제 시스템에서 알고리즘에 새 데이터 주입 전 변환 시 사용 가능
- 여러가지 데이터 변화 쉽게 시도 가능하고, 어떤 조합이 가장 좋은지 확인하는데 편리
- 예측 변수, 레이블 분리
housing=strat_train_set.drop("median_house_value",axis=1)# 훈련 세트를 위해 레이블 제거
housing_labels=strat_train_set["median_house_value"].copy()
drop()
은 데이터 복사본을 만들어 strat_train_set에 영향을 주지 않음
2.5.1 데이터 정제
- 누락된 값 처리
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)
- 각 옵션을 설명하기 위해 주택 데이터셋의 복사본을 만듭니다. 이 때 적어도 하나의 열이 비어 있는 행만 고릅니다. 이렇게 하면 각 옵션의 정확한 동작을 눈으로 쉽게 확인할 수 있습니다.
sample_incomplete_rows=housing[housing.isnull().any(axis=1)].head()
sample_incomplete_rows
longitude | latitude | housing_median_age | total_rooms | total_bedrooms | population | households | median_income | ocean_proximity | |
---|---|---|---|---|---|---|---|---|---|
1606 | -122.08 | 37.88 | 26.0 | 2947.0 | NaN | 825.0 | 626.0 | 2.9330 | NEAR BAY |
10915 | -117.87 | 33.73 | 45.0 | 2264.0 | NaN | 1970.0 | 499.0 | 3.4193 | <1H OCEAN |
19150 | -122.70 | 38.35 | 14.0 | 2313.0 | NaN | 954.0 | 397.0 | 3.7813 | <1H OCEAN |
4186 | -118.23 | 34.13 | 48.0 | 1308.0 | NaN | 835.0 | 294.0 | 4.2891 | <1H OCEAN |
16885 | -122.40 | 37.58 | 26.0 | 3281.0 | NaN | 1145.0 | 480.0 | 6.3580 | NEAR OCEAN |
sample_incomplete_rows.dropna(subset=["total_bedrooms"]) # 옵션 1
longitude | latitude | housing_median_age | total_rooms | total_bedrooms | population | households | median_income | ocean_proximity |
---|
sample_incomplete_rows.drop("total_bedrooms",axis=1) # 옵션 2
longitude | latitude | housing_median_age | total_rooms | population | households | median_income | ocean_proximity | |
---|---|---|---|---|---|---|---|---|
1606 | -122.08 | 37.88 | 26.0 | 2947.0 | 825.0 | 626.0 | 2.9330 | NEAR BAY |
10915 | -117.87 | 33.73 | 45.0 | 2264.0 | 1970.0 | 499.0 | 3.4193 | <1H OCEAN |
19150 | -122.70 | 38.35 | 14.0 | 2313.0 | 954.0 | 397.0 | 3.7813 | <1H OCEAN |
4186 | -118.23 | 34.13 | 48.0 | 1308.0 | 835.0 | 294.0 | 4.2891 | <1H OCEAN |
16885 | -122.40 | 37.58 | 26.0 | 3281.0 | 1145.0 | 480.0 | 6.3580 | NEAR OCEAN |
median = housing["total_bedrooms"].median()
sample_incomplete_rows["total_bedrooms"].fillna(median, inplace=True) # 옵션 3
sample_incomplete_rows
longitude | latitude | housing_median_age | total_rooms | total_bedrooms | population | households | median_income | ocean_proximity | |
---|---|---|---|---|---|---|---|---|---|
1606 | -122.08 | 37.88 | 26.0 | 2947.0 | 433.0 | 825.0 | 626.0 | 2.9330 | NEAR BAY |
10915 | -117.87 | 33.73 | 45.0 | 2264.0 | 433.0 | 1970.0 | 499.0 | 3.4193 | <1H OCEAN |
19150 | -122.70 | 38.35 | 14.0 | 2313.0 | 433.0 | 954.0 | 397.0 | 3.7813 | <1H OCEAN |
4186 | -118.23 | 34.13 | 48.0 | 1308.0 | 433.0 | 835.0 | 294.0 | 4.2891 | <1H OCEAN |
16885 | -122.40 | 37.58 | 26.0 | 3281.0 | 433.0 | 1145.0 | 480.0 | 6.3580 | NEAR OCEAN |
from sklearn.impute import SimpleImputer
imputer=SimpleImputer(strategy="median")
중간값이 수치형 특성에서만 계산될 수 있기 때문에 텍스트 특성을 삭제합니다:
housing_num=housing.drop("ocean_proximity",axis=1)
imputer.fit(housing_num)
SimpleImputer(strategy='median')
imputer.statistics_ # 각 특성의 중간값
array([-118.51 , 34.26 , 29. , 2119. , 433. ,
1164. , 408. , 3.54155])
각 특성의 중간 값이 수동으로 계산한 것과 같은지 확인해 보세요:
housing_num.median().values
array([-118.51 , 34.26 , 29. , 2119. , 433. ,
1164. , 408. , 3.54155])
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]
longitude | latitude | housing_median_age | total_rooms | total_bedrooms | population | households | median_income | |
---|---|---|---|---|---|---|---|---|
1606 | -122.08 | 37.88 | 26.0 | 2947.0 | 433.0 | 825.0 | 626.0 | 2.9330 |
10915 | -117.87 | 33.73 | 45.0 | 2264.0 | 433.0 | 1970.0 | 499.0 | 3.4193 |
19150 | -122.70 | 38.35 | 14.0 | 2313.0 | 433.0 | 954.0 | 397.0 | 3.7813 |
4186 | -118.23 | 34.13 | 48.0 | 1308.0 | 433.0 | 835.0 | 294.0 | 4.2891 |
16885 | -122.40 | 37.58 | 26.0 | 3281.0 | 433.0 | 1145.0 | 480.0 | 6.3580 |
imputer.strategy
'median'
housing_tr.head()
longitude | latitude | housing_median_age | total_rooms | total_bedrooms | population | households | median_income | |
---|---|---|---|---|---|---|---|---|
12655 | -121.46 | 38.52 | 29.0 | 3873.0 | 797.0 | 2237.0 | 706.0 | 2.1736 |
15502 | -117.23 | 33.09 | 7.0 | 5320.0 | 855.0 | 2015.0 | 768.0 | 6.3373 |
2908 | -119.04 | 35.37 | 44.0 | 1618.0 | 310.0 | 667.0 | 300.0 | 2.8750 |
14053 | -117.13 | 32.75 | 24.0 | 1877.0 | 519.0 | 898.0 | 483.0 | 2.2264 |
20496 | -118.70 | 34.28 | 27.0 | 3536.0 | 646.0 | 1837.0 | 580.0 | 4.4964 |
- 사이킷런의
SimpleImputer
- 각 특성의 중간 값 계산해서 객체의
statistics
속성에 저장 - 모든 수치형 특성에
imputer
적용하는것이 바람직(새로운 데이터에서 어떤 값이 누락될지 모르기 때문)
- 각 특성의 중간 값 계산해서 객체의
- 사이킷런의 설계 철학
- 일관성: 모든 객체가 일관되고 단순한 인터페이스 공유
- 추정기(estimator)
- 데이터셋 기반으로 일련의 모델 파라미터들을 추정하는 객체 ex)
imputer
fit()
메서드에 의해 추정 수행되고 하나의 매개변수로 하나의 데이터셋만 전달(지도 학습: 매개변수 두개, 두번째 데이터셋은 레이블)
- 데이터셋 기반으로 일련의 모델 파라미터들을 추정하는 객체 ex)
- 변환기(transformer)
- (
imputer
같이) 데이터셋을 변환하는 추정기 - 데이터셋을 매개변수로 전달받은
transform()
메서드가 변환 수행 fit_transform
:fit()
과transform()
을 연달아 호출하는 것과 동일
- (
- 예측기(predictor)
- 일부 추정기는 주어진 데이터셋에 대해 예측을 만들 수 있다 ex)
LinearRegression
모델 → 예측기 predict()
: 새로운 데이터셋을 받아 이에 상응하는 예측값을 반환score()
: 테스트 세트를 사용해 예측의 품질을 측정함
- 일부 추정기는 주어진 데이터셋에 대해 예측을 만들 수 있다 ex)
- 추정기(estimator)
- 검사 가능
- 모든 추정기의 하이퍼파라미터는 공개 인스턴스 변수로 직접 접근 가능 ex)
imputer.strategy
- 모든 추정기의 학습된 모델파라미터 접미사로 밑줄을 붙여서 공개 인스턴스 변수로 제공 ex)
imputer.statistics_
- 모든 추정기의 하이퍼파라미터는 공개 인스턴스 변수로 직접 접근 가능 ex)
- 클래스 남용 방지: 데이터셋을 넘파이 배열, 사이파이 희소 행렬로 표현
- 조합성: 기존의 구성요소 최대한 재사용 ex) 여러 변환기를 연결한 다음 마지막에 추정기 하나를 배치한
Pipeline
추정기 - 합리적인 기본 값: 대부분의 매개변수에 합리적인 기본값 지정 → 일단 돌아가는 기본 시스템 빠르게 만들 수 있다.
- 일관성: 모든 객체가 일관되고 단순한 인터페이스 공유
2.5.2 텍스트와 범주형 특성 다루기
이제 범주형 입력 특성인 ocean_proximity
을 전처리합니다:
housing_cat=housing[["ocean_proximity"]]
housing_cat.head(10)
ocean_proximity | |
---|---|
12655 | INLAND |
15502 | NEAR OCEAN |
2908 | INLAND |
14053 | NEAR OCEAN |
20496 | <1H OCEAN |
1481 | NEAR BAY |
18125 | <1H OCEAN |
5830 | <1H OCEAN |
17989 | <1H OCEAN |
4861 | <1H OCEAN |
OrdinalEncoder
- 범주형 특성 값을 숫자로 변환
- 문제: 머신러닝 알고리즘이 가까운 값을 더 비슷하다고 생각
from sklearn.preprocessing import OrdinalEncoder
ordinal_encoder=OrdinalEncoder()
housing_cat_encoded=ordinal_encoder.fit_transform(housing_cat)
housing_cat_encoded[:10]
array([[1.],
[4.],
[1.],
[4.],
[0.],
[3.],
[0.],
[0.],
[0.],
[0.]])
ordinal_encoder.categories_ # 카테고리 목록
[array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'],
dtype=object)]
OneHotEncoder
- 한 특성만 1, 나머지는 모두 0 (더미 특성: 새로운 특성)
- 사이파이 희소 행렬로 출력 →
numpy
배열로 바꾸려면toarray()
메서드 사용 - 0이 아닌 원소의 위치만 저장하므로 카테고리의 수가 많을 때 매우 효율적
from sklearn.preprocessing import OneHotEncoder
cat_encoder=OneHotEncoder()
housing_cat_1hot=cat_encoder.fit_transform(housing_cat)
housing_cat_1hot
<16512x5 sparse matrix of type '<class 'numpy.float64'>'
with 16512 stored elements in Compressed Sparse Row format>
housing_cat_1hot.toarray()
array([[0., 1., 0., 0., 0.],
[0., 0., 0., 0., 1.],
[0., 1., 0., 0., 0.],
...,
[1., 0., 0., 0., 0.],
[1., 0., 0., 0., 0.],
[0., 1., 0., 0., 0.]])
또는 OneHotEncoder
를 만들 때 sparse=False
로 지정할 수 있습니다:
cat_encoder=OneHotEncoder(sparse=False)
housing_cat_1hot=cat_encoder.fit_transform(housing_cat)
housing_cat_1hot
array([[0., 1., 0., 0., 0.],
[0., 0., 0., 0., 1.],
[0., 1., 0., 0., 0.],
...,
[1., 0., 0., 0., 0.],
[1., 0., 0., 0., 0.],
[0., 1., 0., 0., 0.]])
cat_encoder.categories_
[array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'],
dtype=object)]
OrdinalEncoder
,OneHotEncoder
둘다 인코더의categories_
인스턴스 변수를 사용해 카테고리 리스트를 얻을 수 있다.
Tip
- 카테고리 특성이 많으면 원-핫 인코딩은 많은 수의 입력 특성 생성 → 훈련을 느리고 성능 감소
- 범주형 입력 값 이 특성과 관련된 숫자형 특성으로 변경 ex)
ocean proximity
특성 해안까지의 거리로 변경- 각 카테고리 임베딩으로 변환(훈련동안 각 카테고리의 표현이 학습) → 표현학습의 한 예시
- 표현학습
- 각 카테고리를 임베딩으로 바꿔 훈련하는 동안 각 카테고리의 표현 학습
- 임베딩: 학습 가능한 저차원의 벡터로 변경한 결과 혹은 그 과정 전체
2.5.3 나만의 변환기
- 덕타이핑: 상속이나 인터페이스 구현이 아닌 객체의 속성이나 메서드가 객체의 유형을 결정하는 방식
- 사이킷런은 덕타이핑 지원 ->
fit()
,transform()
,fit_transform()
메서드를 구현한 파이썬 클래스 만들면 됨 TransformerMixin
상속하면fit_transform()
자동 생성,BaseEstimator
상속하면 하이퍼파라미터 튜닝에 필요한get_params()
,set_params()
메서드 얻음
앞서말한 조합 특성 추가 간단한 변환기
from sklearn.base import TransformerMixin, BaseEstimator
#열 인덱스
rooms_ix, bedrooms_ix, population_ix, households_ix=3,4,5,6
class CombinedAttributesAdder(TransformerMixin,BaseEstimator):
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())
책에서는 간단하게 인덱스 (3, 4, 5, 6)을 하드코딩했지만 다음처럼 동적으로 처리하는 것이 더 좋습니다:
col_names="total_rooms","total_bedrooms","population","households"
rooms_ix, bedrooms_ix, population_ix, households_ix=[housing.columns.get_loc(c) for c in col_names]# 열 인덱스 구하기
또한 housing_extra_attribs
는 넘파이 배열이기 때문에 열 이름이 없습니다(안타깝지만 사이킷런을 사용할 때 생기는 문제입니다). DataFrame
으로 복원하려면 다음과 같이 할 수 있습니다:
housing_extra_attribs=pd.DataFrame(
housing_extra_attribs,
columns=list(housing.columns)+["rooms_per_household", "population_per_household"],
index=housing.index)
housing_extra_attribs.head()
longitude | latitude | housing_median_age | total_rooms | total_bedrooms | population | households | median_income | ocean_proximity | rooms_per_household | population_per_household | |
---|---|---|---|---|---|---|---|---|---|---|---|
12655 | -121.46 | 38.52 | 29.0 | 3873.0 | 797.0 | 2237.0 | 706.0 | 2.1736 | INLAND | 5.485836 | 3.168555 |
15502 | -117.23 | 33.09 | 7.0 | 5320.0 | 855.0 | 2015.0 | 768.0 | 6.3373 | NEAR OCEAN | 6.927083 | 2.623698 |
2908 | -119.04 | 35.37 | 44.0 | 1618.0 | 310.0 | 667.0 | 300.0 | 2.875 | INLAND | 5.393333 | 2.223333 |
14053 | -117.13 | 32.75 | 24.0 | 1877.0 | 519.0 | 898.0 | 483.0 | 2.2264 | NEAR OCEAN | 3.886128 | 1.859213 |
20496 | -118.7 | 34.28 | 27.0 | 3536.0 | 646.0 | 1837.0 | 580.0 | 4.4964 | <1H OCEAN | 6.096552 | 3.167241 |
2.5.4 특성 스케일링
- 타깃값에 대한 스케일링 일반적으로 불필요
- min-max 스케일링(정규화로 많이 불림)
- x-min/max-min (0~1 범위로 값을 조정)
MinMaxScaler
변환기 제공feature_range
매개변수로 범위 변경 가능
- 표준화
- x-average/std (평균:0, 분산:1)
- 범위의 상한과 하한이 없어 문제가 될 수 있다. ex) 인공신경망은 입력값의 기대값이 0~1사이
- 이상치에 영향을 덜 받음
StandardScaler
변환기 제공
CAUTION
훈련데이터에 대해서만fit()
한 후, 훈련 데이터 와 테스트 데이터에 대해transform()
메서드 사용
2.5.5 변환 파이프라인
Pipeline
- 연속된 단계를 나타내는 이름/추정기 쌍을 입력받음
- 마지막 단계는 변환기와 추정기 모두 사용 가능 (나머지는 변환기) → 즉, (
fit
+transform
) orfit_transform
가져야함
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
num_pipeline=Pipeline([
('imputer', SimpleImputer(strategy="median")),
('attribs_adder', CombinedAttributesAdder()),
('std_scaler', StandardScaler()),
])
housing_tr=num_pipeline.fit_transform(housing_num)
housing_tr
array([[-0.94135046, 1.34743822, 0.02756357, ..., 0.01739526,
0.00622264, -0.12112176],
[ 1.17178212, -1.19243966, -1.72201763, ..., 0.56925554,
-0.04081077, -0.81086696],
[ 0.26758118, -0.1259716 , 1.22045984, ..., -0.01802432,
-0.07537122, -0.33827252],
...,
[-1.5707942 , 1.31001828, 1.53856552, ..., -0.5092404 ,
-0.03743619, 0.32286937],
[-1.56080303, 1.2492109 , -1.1653327 , ..., 0.32814891,
-0.05915604, -0.45702273],
[-1.28105026, 2.02567448, -0.13148926, ..., 0.01407228,
0.00657083, -0.12169672]])
ColumnTransformer
- 하나의 변환기로 각 열마다 적절한 변환을 적용하여 모든 열을 처리
- 단계
- 수치형, 범주형 열 이름의 리스트 생성
ColumnTransformer
객체에 이름, 변환기, 변환기가 적용될 열 이름(또는 인덱스) 튜플의 리스트 입력- 각 변환기를 적절한 열에 적용(반환하는 행의 개수 같아야함)
OneHotEncoder
는 희소 행렬,num_pipeline
은 밀집 행렬 반환 → 최종 행렬 밀집도 임계값 기준으로 결과 반환(기본적으로sparse_threshold=0.3
)- 여기서는 밀집 행렬 반환
TIP
drop
: 삭제하는 열,passthrough
: 변환을 적용하지 않을 열- 나열되지 않은 열을 기본적으로 삭제
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
array([[-0.94135046, 1.34743822, 0.02756357, ..., 0. ,
0. , 0. ],
[ 1.17178212, -1.19243966, -1.72201763, ..., 0. ,
0. , 1. ],
[ 0.26758118, -0.1259716 , 1.22045984, ..., 0. ,
0. , 0. ],
...,
[-1.5707942 , 1.31001828, 1.53856552, ..., 0. ,
0. , 0. ],
[-1.56080303, 1.2492109 , -1.1653327 , ..., 0. ,
0. , 0. ],
[-1.28105026, 2.02567448, -0.13148926, ..., 0. ,
0. , 0. ]])
housing_prepared.shape
(16512, 16)
다음은 (판다스 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)),
])
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
array([[-0.94135046, 1.34743822, 0.02756357, ..., 0. ,
0. , 0. ],
[ 1.17178212, -1.19243966, -1.72201763, ..., 0. ,
0. , 1. ],
[ 0.26758118, -0.1259716 , 1.22045984, ..., 0. ,
0. , 0. ],
...,
[-1.5707942 , 1.31001828, 1.53856552, ..., 0. ,
0. , 0. ],
[-1.56080303, 1.2492109 , -1.1653327 , ..., 0. ,
0. , 0. ],
[-1.28105026, 2.02567448, -0.13148926, ..., 0. ,
0. , 0. ]])
ColumnTransformer
의 결과와 동일합니다:
np.allclose(housing_prepared, old_housing_prepared)
True
2.6 모델 선택과 훈련
2.6.1 훈련 세트에서 훈련하고 평가하기
from sklearn.linear_model import LinearRegression
lin_reg=LinearRegression()
lin_reg.fit(housing_prepared,housing_labels)
LinearRegression()
# 훈련 샘플 몇 개를 사용해 전체 파이프라인을 적용해 보겠습니다
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))
예측: [ 85657.90192014 305492.60737488 152056.46122456 186095.70946094
244550.67966089]
실제 값과 비교
print("레이블: ", list(some_labels))
레이블: [72100.0, 279600.0, 82700.0, 112500.0, 238300.0]
some_data_prepared
array([[-0.94135046, 1.34743822, 0.02756357, 0.58477745, 0.64037127,
0.73260236, 0.55628602, -0.8936472 , 0.01739526, 0.00622264,
-0.12112176, 0. , 1. , 0. , 0. ,
0. ],
[ 1.17178212, -1.19243966, -1.72201763, 1.26146668, 0.78156132,
0.53361152, 0.72131799, 1.292168 , 0.56925554, -0.04081077,
-0.81086696, 0. , 0. , 0. , 0. ,
1. ],
[ 0.26758118, -0.1259716 , 1.22045984, -0.46977281, -0.54513828,
-0.67467519, -0.52440722, -0.52543365, -0.01802432, -0.07537122,
-0.33827252, 0. , 1. , 0. , 0. ,
0. ],
[ 1.22173797, -1.35147437, -0.37006852, -0.34865152, -0.03636724,
-0.46761716, -0.03729672, -0.86592882, -0.59513997, -0.10680295,
0.96120521, 0. , 0. , 0. , 0. ,
1. ],
[ 0.43743108, -0.63581817, -0.13148926, 0.42717947, 0.27279028,
0.37406031, 0.22089846, 0.32575178, 0.2512412 , 0.00610923,
-0.47451338, 1. , 0. , 0. , 0. ,
0. ]])
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
68627.87390018745
노트: 사이킷런 0.22 버전부터는 squared=False
매개변수로 mean_squared_error()
함수를 호출하면 RMSE를 바로 얻을 수 있습니다.
from sklearn.metrics import mean_absolute_error
lin_mae = mean_absolute_error(housing_labels, housing_predictions)
lin_mae
49438.66860915801
- 선형 회귀 모델 → 과소적합
- 더 복잡한 모델, 더 좋은 특성 주입
- 더 복잡한 모델 선택 →
DecisionTreeRegressor
from sklearn.tree import DecisionTreeRegressor
tree_reg = DecisionTreeRegressor(random_state=42)
tree_reg.fit(housing_prepared, housing_labels)
DecisionTreeRegressor(random_state=42)
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
- 결정 트리 → 과대적합
- 훈련 세트의 일부분으로 훈련하고 다른 일부분은 검증으로 사용해야함
2.6.2 교차 검증을 사용한 평가
- k-fold cross-validation
- 훈련 세트를 k개의 폴드(fold)로 분할하여 매번 다른 폴드를 평가에 사용 (k번 훈련하고 평가)
- 사이킷런 교차검증 기능
scoring
매개변수에 효용함수 기대(클수록 좋은) → 그래서neg_mean_squared_error
,-scores
사용
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)
점수: [72831.45749112 69973.18438322 69528.56551415 72517.78229792
69145.50006909 79094.74123727 68960.045444 73344.50225684
69826.02473916 71077.09753998]
평균: 71629.89009727491
표준 편차: 2914.035468468928
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)
점수: [71762.76364394 64114.99166359 67771.17124356 68635.19072082
66846.14089488 72528.03725385 73997.08050233 68802.33629334
66443.28836884 70139.79923956]
평균: 69104.07998247063
표준 편차: 2880.328209818068
- 앙상블 학습: 여러 다른 모델을 모아서 하나의 모델을 만드는 것
from sklearn.ensemble import RandomForestRegressor
forest_reg = RandomForestRegressor(n_estimators=100, random_state=42)
forest_reg.fit(housing_prepared, housing_labels)
RandomForestRegressor(random_state=42)
노트: 사이킷런 0.22 버전에서 n_estimators
의 기본값이 100으로 바뀌기 때문에 향후를 위해 n_estimators=100
로 지정합니다(책에는 등장하지 않습니다).
housing_predictions = forest_reg.predict(housing_prepared)
forest_mse = mean_squared_error(housing_labels, housing_predictions)
forest_rmse = np.sqrt(forest_mse)
forest_rmse
18650.698705770003
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)
점수: [51559.63379638 48737.57100062 47210.51269766 51875.21247297
47577.50470123 51863.27467888 52746.34645573 50065.1762751
48664.66818196 54055.90894609]
평균: 50435.58092066179
표준 편차: 2203.3381412764606
훈련 세트에 대한 점수가 검증 세트에 대한 점수보다 훨씬 낮으므로 훈련 세트에 과대적합 되어있다.
과대적합 줄이는 방법: 모델 간단히, 규제 , 더 많은 훈련 데이터 수집
scores = cross_val_score(lin_reg, housing_prepared, housing_labels, scoring="neg_mean_squared_error", cv=10)
pd.Series(np.sqrt(-scores)).describe()
count 10.000000
mean 69104.079982
std 3036.132517
min 64114.991664
25% 67077.398482
50% 68718.763507
75% 71357.022543
max 73997.080502
dtype: float64
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
111095.06635291968
TIP
- 교차 검증 점수와 실제 예측값은 물론 하이퍼파라미터와 훈련된 모델 파라미터 모두 저장해야함
- 파이썬의
pickle
,joblib를
사용하여 사이킷런 모델을 간단하게 저장 가능
import joblib
joblib.dump(my_model, "my_model.pkl")
#나중에...
my_model_loaded=joblib.load("my_model.pkl")
2.7 모델 세부 튜닝
2.7.1 그리드 탐색
GridSearchCV
- 탐색할 하이퍼파라미터와 시도해볼 값 지정 → 모든 하이퍼파라미터에 대해 교차 검증을 통한 평가
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)
GridSearchCV(cv=5, estimator=RandomForestRegressor(random_state=42),
param_grid=[{'max_features': [2, 4, 6, 8],
'n_estimators': [3, 10, 30]},
{'bootstrap': [False], 'max_features': [2, 3, 4],
'n_estimators': [3, 10]}],
return_train_score=True, scoring='neg_mean_squared_error')
최상의 파라미터 조합은 다음과 같습니다:
grid_search.best_params_
{'max_features': 8, 'n_estimators': 30}
grid_search.best_estimator_
RandomForestRegressor(max_features=8, n_estimators=30, random_state=42)
그리드서치에서 테스트한 하이퍼파라미터 조합의 점수를 확인합니다:
cvres = grid_search.cv_results_
for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]):
print(np.sqrt(-mean_score), params)
63895.161577951665 {'max_features': 2, 'n_estimators': 3}
54916.32386349543 {'max_features': 2, 'n_estimators': 10}
52885.86715332332 {'max_features': 2, 'n_estimators': 30}
60075.3680329983 {'max_features': 4, 'n_estimators': 3}
52495.01284985185 {'max_features': 4, 'n_estimators': 10}
50187.24324926565 {'max_features': 4, 'n_estimators': 30}
58064.73529982314 {'max_features': 6, 'n_estimators': 3}
51519.32062366315 {'max_features': 6, 'n_estimators': 10}
49969.80441627874 {'max_features': 6, 'n_estimators': 30}
58895.824998155826 {'max_features': 8, 'n_estimators': 3}
52459.79624724529 {'max_features': 8, 'n_estimators': 10}
49898.98913455217 {'max_features': 8, 'n_estimators': 30}
62381.765106921855 {'bootstrap': False, 'max_features': 2, 'n_estimators': 3}
54476.57050944266 {'bootstrap': False, 'max_features': 2, 'n_estimators': 10}
59974.60028085155 {'bootstrap': False, 'max_features': 3, 'n_estimators': 3}
52754.5632813202 {'bootstrap': False, 'max_features': 3, 'n_estimators': 10}
57831.136061214274 {'bootstrap': False, 'max_features': 4, 'n_estimators': 3}
51278.37877140253 {'bootstrap': False, 'max_features': 4, 'n_estimators': 10}
pd.DataFrame(grid_search.cv_results_)
mean_fit_time | std_fit_time | mean_score_time | std_score_time | param_max_features | param_n_estimators | param_bootstrap | params | split0_test_score | split1_test_score | ... | mean_test_score | std_test_score | rank_test_score | split0_train_score | split1_train_score | split2_train_score | split3_train_score | split4_train_score | mean_train_score | std_train_score | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0.049811 | 0.000980 | 0.002801 | 4.001381e-04 | 2 | 3 | NaN | {'max_features': 2, 'n_estimators': 3} | -4.119912e+09 | -3.723465e+09 | ... | -4.082592e+09 | 1.867375e+08 | 18 | -1.155630e+09 | -1.089726e+09 | -1.153843e+09 | -1.118149e+09 | -1.093446e+09 | -1.122159e+09 | 2.834288e+07 |
1 | 0.166234 | 0.001834 | 0.007215 | 3.941191e-04 | 2 | 10 | NaN | {'max_features': 2, 'n_estimators': 10} | -2.973521e+09 | -2.810319e+09 | ... | -3.015803e+09 | 1.139808e+08 | 11 | -5.982947e+08 | -5.904781e+08 | -6.123850e+08 | -5.727681e+08 | -5.905210e+08 | -5.928894e+08 | 1.284978e+07 |
2 | 0.496511 | 0.001855 | 0.021005 | 6.843901e-07 | 2 | 30 | NaN | {'max_features': 2, 'n_estimators': 30} | -2.801229e+09 | -2.671474e+09 | ... | -2.796915e+09 | 7.980892e+07 | 9 | -4.412567e+08 | -4.326398e+08 | -4.553722e+08 | -4.320746e+08 | -4.311606e+08 | -4.385008e+08 | 9.184397e+06 |
3 | 0.082218 | 0.001327 | 0.002402 | 4.920901e-04 | 4 | 3 | NaN | {'max_features': 4, 'n_estimators': 3} | -3.528743e+09 | -3.490303e+09 | ... | -3.609050e+09 | 1.375683e+08 | 16 | -9.782368e+08 | -9.806455e+08 | -1.003780e+09 | -1.016515e+09 | -1.011270e+09 | -9.980896e+08 | 1.577372e+07 |
4 | 0.268059 | 0.001264 | 0.007202 | 3.999001e-04 | 4 | 10 | NaN | {'max_features': 4, 'n_estimators': 10} | -2.742620e+09 | -2.609311e+09 | ... | -2.755726e+09 | 1.182604e+08 | 7 | -5.063215e+08 | -5.257983e+08 | -5.081984e+08 | -5.174405e+08 | -5.282066e+08 | -5.171931e+08 | 8.882622e+06 |
5 | 0.800979 | 0.002558 | 0.021205 | 4.003287e-04 | 4 | 30 | NaN | {'max_features': 4, 'n_estimators': 30} | -2.522176e+09 | -2.440241e+09 | ... | -2.518759e+09 | 8.488084e+07 | 3 | -3.776568e+08 | -3.902106e+08 | -3.885042e+08 | -3.830866e+08 | -3.894779e+08 | -3.857872e+08 | 4.774229e+06 |
6 | 0.107624 | 0.000800 | 0.002201 | 3.998997e-04 | 6 | 3 | NaN | {'max_features': 6, 'n_estimators': 3} | -3.362127e+09 | -3.311863e+09 | ... | -3.371513e+09 | 1.378086e+08 | 13 | -8.909397e+08 | -9.583733e+08 | -9.000201e+08 | -8.964731e+08 | -9.151927e+08 | -9.121998e+08 | 2.444837e+07 |
7 | 0.361481 | 0.001960 | 0.007402 | 4.901351e-04 | 6 | 10 | NaN | {'max_features': 6, 'n_estimators': 10} | -2.622099e+09 | -2.669655e+09 | ... | -2.654240e+09 | 6.967978e+07 | 5 | -4.939906e+08 | -5.145996e+08 | -5.023512e+08 | -4.959467e+08 | -5.147087e+08 | -5.043194e+08 | 8.880106e+06 |
8 | 1.104447 | 0.002227 | 0.021005 | 2.431402e-07 | 6 | 30 | NaN | {'max_features': 6, 'n_estimators': 30} | -2.446142e+09 | -2.446594e+09 | ... | -2.496981e+09 | 7.357046e+07 | 2 | -3.760968e+08 | -3.876636e+08 | -3.875307e+08 | -3.760938e+08 | -3.861056e+08 | -3.826981e+08 | 5.418747e+06 |
9 | 0.141231 | 0.002482 | 0.002601 | 4.901155e-04 | 8 | 3 | NaN | {'max_features': 8, 'n_estimators': 3} | -3.590333e+09 | -3.232664e+09 | ... | -3.468718e+09 | 1.293758e+08 | 14 | -9.505012e+08 | -9.166119e+08 | -9.033910e+08 | -9.070642e+08 | -9.459386e+08 | -9.247014e+08 | 1.973471e+07 |
10 | 0.472305 | 0.003060 | 0.007002 | 3.234067e-07 | 8 | 10 | NaN | {'max_features': 8, 'n_estimators': 10} | -2.721311e+09 | -2.675886e+09 | ... | -2.752030e+09 | 6.258030e+07 | 6 | -4.998373e+08 | -4.997970e+08 | -5.099880e+08 | -5.047868e+08 | -5.348043e+08 | -5.098427e+08 | 1.303601e+07 |
11 | 1.421118 | 0.012175 | 0.021004 | 3.989506e-07 | 8 | 30 | NaN | {'max_features': 8, 'n_estimators': 30} | -2.492636e+09 | -2.444818e+09 | ... | -2.489909e+09 | 7.086483e+07 | 1 | -3.801679e+08 | -3.832972e+08 | -3.823818e+08 | -3.778452e+08 | -3.817589e+08 | -3.810902e+08 | 1.916605e+06 |
12 | 0.079017 | 0.000895 | 0.003001 | 5.722046e-07 | 2 | 3 | False | {'bootstrap': False, 'max_features': 2, 'n_est... | -4.020842e+09 | -3.951861e+09 | ... | -3.891485e+09 | 8.648595e+07 | 17 | -0.000000e+00 | -4.306828e+01 | -1.051392e+04 | -0.000000e+00 | -0.000000e+00 | -2.111398e+03 | 4.201294e+03 |
13 | 0.261658 | 0.002060 | 0.009002 | 5.560829e-07 | 2 | 10 | False | {'bootstrap': False, 'max_features': 2, 'n_est... | -2.901352e+09 | -3.036875e+09 | ... | -2.967697e+09 | 4.582448e+07 | 10 | -0.000000e+00 | -3.876145e+00 | -9.462528e+02 | -0.000000e+00 | -0.000000e+00 | -1.900258e+02 | 3.781165e+02 |
14 | 0.103025 | 0.001789 | 0.003001 | 4.909339e-07 | 3 | 3 | False | {'bootstrap': False, 'max_features': 3, 'n_est... | -3.687132e+09 | -3.446245e+09 | ... | -3.596953e+09 | 8.011960e+07 | 15 | -0.000000e+00 | -0.000000e+00 | -0.000000e+00 | -0.000000e+00 | -0.000000e+00 | 0.000000e+00 | 0.000000e+00 |
15 | 0.340272 | 0.002565 | 0.008602 | 4.900183e-04 | 3 | 10 | False | {'bootstrap': False, 'max_features': 3, 'n_est... | -2.837028e+09 | -2.619558e+09 | ... | -2.783044e+09 | 8.862580e+07 | 8 | -0.000000e+00 | -0.000000e+00 | -0.000000e+00 | -0.000000e+00 | -0.000000e+00 | 0.000000e+00 | 0.000000e+00 |
16 | 0.130395 | 0.001772 | 0.003001 | 6.323347e-04 | 4 | 3 | False | {'bootstrap': False, 'max_features': 4, 'n_est... | -3.549428e+09 | -3.318176e+09 | ... | -3.344440e+09 | 1.099355e+08 | 12 | -0.000000e+00 | -0.000000e+00 | -0.000000e+00 | -0.000000e+00 | -0.000000e+00 | 0.000000e+00 | 0.000000e+00 |
17 | 0.427296 | 0.004113 | 0.008802 | 4.005432e-04 | 4 | 10 | False | {'bootstrap': False, 'max_features': 4, 'n_est... | -2.692499e+09 | -2.542704e+09 | ... | -2.629472e+09 | 8.510266e+07 | 4 | -0.000000e+00 | -0.000000e+00 | -0.000000e+00 | -0.000000e+00 | -0.000000e+00 | 0.000000e+00 | 0.000000e+00 |
18 rows × 23 columns
TIP
어떤 하이퍼파라미터 값 지정할지 모르겠다 → 10의 거듭 제곱 수로 시도 (더 세밀하게 → 더 작은 값 지정)
- (첫번째 dict 12번 + 두번째 dict 6번) X 5번의 교차 검증 = 90 → 전체 훈련 횟수
TIP
탐색의 최대 값들이 최적의 조합이면 더 큰 값으로 검색해야함 (계속해서 점수 향상될 수 있음)
NOTE
GridSearchCV
가refit=True
로 초기화 되어있으면 교차검증으로 최적의 추정기 찾고 전체 훈련 세트로 다시 훈련 (일반적으로 데이터 많을수록 성능 향상되므로 좋은 방법)
Tip
- 데이터 준비 단계를 하나의 파라미터처럼 다룰 수 있다.
- ex) 그리드 탐색이 확실하지 않은 특성을 추가할지 말지 자동으로 정할 수 있음
- 이상치나 값이 빈 특성을 다루거나 특성 선택 등을 자동으로 처리할 때 그리드 탐색 사용
- 데이터 준비 단계와 모델을 연결한 파이프라인을 그리드 탐색에 적용할 때 데이터 준비 단계를 캐싱하면 탐색 시간을 줄일 수 있다.
2.7.2 랜덤 탐색
- 그리드 탐색 → 비교적 적은 수의 조합 탐구할 때 좋음
- 하이퍼 파라미터 탐색공간이 커질 때
RandomizedSearchCV
사용 - 각 반복마다 하이퍼파라미터에 임의의 수를 대입하여 지정한 횟수만큼 평가
- 장점
- 랜덤 탐색 1000회 반복 → 하이퍼파라미터마다 각기 다른 1000개의 값 탐색 (그리드 탐색: 하이퍼파라미터마다 몇개의 값만 탐색)
- 단순 반복 횟수 조절로 컴퓨팅 자원 제어 가능
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint
params_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=params_distribs,
n_iter=10, cv=5, scoring='neg_mean_squared_error', random_state=42)
rnd_search.fit(housing_prepared,housing_labels)
RandomizedSearchCV(cv=5, estimator=RandomForestRegressor(random_state=42),
param_distributions={'max_features': <scipy.stats._distn_infrastructure.rv_frozen object at 0x000001AF011F3C48>,
'n_estimators': <scipy.stats._distn_infrastructure.rv_frozen object at 0x000001AF011F3F48>},
random_state=42, scoring='neg_mean_squared_error')
cvres=rnd_search.cv_results_
for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]):
print(np.sqrt(-mean_score), params)
49117.55344336652 {'max_features': 7, 'n_estimators': 180}
51450.63202856348 {'max_features': 5, 'n_estimators': 15}
50692.53588182537 {'max_features': 3, 'n_estimators': 72}
50783.614493515 {'max_features': 5, 'n_estimators': 21}
49162.89877456354 {'max_features': 7, 'n_estimators': 122}
50655.798471042704 {'max_features': 3, 'n_estimators': 75}
50513.856319990606 {'max_features': 3, 'n_estimators': 88}
49521.17201976928 {'max_features': 5, 'n_estimators': 100}
50302.90440763418 {'max_features': 3, 'n_estimators': 150}
65167.02018649492 {'max_features': 5, 'n_estimators': 2}
2.7.3 앙상블 방법
- 모델의 그룹이 최상의 단일 모델보다 더 나은 성능을 발휘할 때가 많다. (특히 개개의 모델이 각기 다른 형태의 오차를 만들 때)
2.7.4 최상의 모델과 오차 분석
feature_importances=grid_search.best_estimator_.feature_importances_
feature_importances
array([6.96542523e-02, 6.04213840e-02, 4.21882202e-02, 1.52450557e-02,
1.55545295e-02, 1.58491147e-02, 1.49346552e-02, 3.79009225e-01,
5.47789150e-02, 1.07031322e-01, 4.82031213e-02, 6.79266007e-03,
1.65706303e-01, 7.83480660e-05, 1.52473276e-03, 3.02816106e-03])
extra_attribs = ["rooms_per_hhold", "pop_per_hhold", "bedrooms_per_room"]
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)
[(0.3790092248170967, 'median_income'),
(0.16570630316895876, 'INLAND'),
(0.10703132208204355, 'pop_per_hhold'),
(0.06965425227942929, 'longitude'),
(0.0604213840080722, 'latitude'),
(0.054778915018283726, 'rooms_per_hhold'),
(0.048203121338269206, 'bedrooms_per_room'),
(0.04218822024391753, 'housing_median_age'),
(0.015849114744428634, 'population'),
(0.015554529490469328, 'total_bedrooms'),
(0.01524505568840977, 'total_rooms'),
(0.014934655161887772, 'households'),
(0.006792660074259966, '<1H OCEAN'),
(0.0030281610628962747, 'NEAR OCEAN'),
(0.0015247327555504937, 'NEAR BAY'),
(7.834806602687504e-05, 'ISLAND')]
- 최상의 모델을 분석하면 문제에 대한 좋은 통찰을 얻는 경우가 많다. ex)
RandomForestRegressor
각 특성의 상대적 중요도 - 이 정보로 덜 중요한 특성 제외 가능
- 시스템이 특정 오차 발생 시 왜 그런 문제가 생겼는지 이해하고 문제 해결 방법 찾아야 함
2.7.5 테스트 세트로 시스템 평가하기
- 테스트 세트에서 레이블, 예측 변수 얻은 후
full_pipeline
으로 데이터 변환 (테스트 세트 학습하면 안되므로fit_transform()
이 아닌transform()
호출)
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
47873.26095812988
테스트 RMSE에 대한 95% 신뢰 구간을 계산할 수 있습니다:
- 일반화 오차의 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)))
array([45893.36082829, 49774.46796717])
- 하이퍼파라미터 튜닝을 많이 했다면 교차 검증을 사용해 측정한 것보다 조금 성능이 낮은 것이 보통
- 검증 데이터에서 좋은 성능을 내도록 세밀하게 튜닝했기 때문에 새로운 데이터셋에 잘 작동하지 않을 수 있음
- 이런 경우가 발생하더라도 테스트 세트에서 하이퍼파라미터 튜닝 시도 X → 그렇게 향상된 성능은 새로운 데이터에 일반화 되기 어렵다.
2.8 론칭, 모니터링, 시스템 유지 보수
- 모델 상용 환경에 배포
- 전체 전처리 파이프라인과 예측 파이프라인이 포함된 훈련된 사이킷런 모델을 저장(
joblib
사용) → 이 훈련된 모델 상용 환경에 로드하고predict()
메서드로 예측 - 웹 애플리케이션이 REST API를 통해 질의할 수 있는 전용 웹 서비스로 모델을 감싼다.
- 주 애플리케이션을 건드리지 않고 모델을 새 버전 업그레이드하기 쉽다
- 웹 애플리케이션에서 웹 서비스로 오는 요청 로드 밸런싱 가능 → 규모 확장 쉽다
- 웹 애플리케이션 파이썬 아닌 다른 언어로 작성 가능
- REST API: 표준 HTTP 메서드를 사용해 자원에 대한 읽기, 수정, 생성, 삭제를 수행하며 입력과 출력으로 JSON을 사용하는 HTTP 기반 API
- 구글 클라우드 AI 플랫폼과 같은 클라우드에 배포
- 모델 저장하고 구글 클라우드 스토리지(GCS)에 업로드 → 구글 클라우드 AI 플랫폼으로 이동하여 새로운 모델 버전 만들고 GCS파일 지정
- 로드 밸런싱과 자동확장 처리하는 간단한 웹서비스 만든다
- 웹사이트에서 웹 서비스 사용 가능
- 전체 전처리 파이프라인과 예측 파이프라인이 포함된 훈련된 사이킷런 모델을 저장(
- 실시간 성능 체크와 성능 저하 시 알람 통지 모니터링 코드 작성 필요
- 시스템의 고장난 컴포넌트로 인한 갑작스런 성능 저하, 긴 시간 동안 눈에 띄지 않게 성능 서서히 감소 → 감지 해야한다
CAUTION
데이터의 변화와 트렌드에 의해 훈련된 모델은 정기적으로 재훈련할 필요가 있다.
- 모델의 실전 성능 모니터링 해야 한다
- 하위 시스템의 지표로 모델 성능 추정 ex) 추천 시스템
- 모델이 예측한 샘플을 사람이 평가
- 데이터가 계속 변화하면 데이터셋 업데이트하고 정기적으로 재훈련 → 전체 과정에서 많은 부분 자동화 필요
- 자동화 가능 일부 작업
- 정기적으로 새로운 데이터 수집하고 레이블 달기 ex) 조사원
- 모델 훈련하고 하이퍼파라미터 자동으로 세부 튜닝하는 스크립트 작성(작업에 따라 매일 또는 매주 작성)
- 업데이트된 테스트 세트에서 새로운 모델과 이전 모델을 평가하는 스크립트 하나 더 작성 → 성능이 감소하지 않으면 새로운 모델 배포(성능이 나쁘면 왜 그런지 조사)
- 자동화 가능 일부 작업
- 모델의 입력 데이터 품질 평가 필요
- 만든 모델을 백업해야한다
- 새로운 모델이 이상 작동하는 경우 빠르게 이전 모델로 롤백하기 위한 절차와 도구 필요
- 백업을 가지고 있으면 이전 모델과 새 모델을 쉽게 비교
- 비슷하게 데이터셋도 백업 필요
- Tip: 데이터 일부분에 대해 모델이 잘 작동하는지 평가하기 위해 테스트 세트를 여러 서브셋으로 나누는 경우 있다
- ex) 가장 최근 데이터 담은 서브셋 or 특별한 종류의 입력 위한 서브셋
추가 내용
전처리와 예측을 포함한 전체 파이프라인
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)
array([ 85657.90192014, 305492.60737488, 152056.46122456, 186095.70946094,
244550.67966089])
joblib를 사용한 모델 저장
my_model= full_pipeline_with_predictor
import joblib
joblib.dump(my_model, "my_model.pk") # DIFF
# ...
my_model_loaded = joblib.load("my_model.pkl") # DIFF
RandomizedSearchCV
를 위한 Scipy 분포 함수
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
모델은 무엇인가요?
경고: 사용하는 하드웨어에 따라 다음 셀을 실행하는데 30분 또는 그 이상 걸릴 수 있습니다.
from sklearn.svm import SVR
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]},
]
svr_reg = SVR()
grid_search=GridSearchCV(svr_reg, param_grid=param_grid, cv=5, scoring='neg_mean_squared_error', verbose=2)
grid_search.fit(housing_prepared, housing_labels) # 탐색 과정 출력 생략
Fitting 5 folds for each of 50 candidates, totalling 250 fits
[CV] END ..............................C=10.0, kernel=linear; total time= 6.7s
[CV] END ..............................C=10.0, kernel=linear; total time= 6.7s
[CV] END ..............................C=10.0, kernel=linear; total time= 6.7s
.
.
.
GridSearchCV(cv=5, estimator=SVR(),
param_grid=[{'C': [10.0, 30.0, 100.0, 300.0, 1000.0, 3000.0,
10000.0, 30000.0],
'kernel': ['linear']},
{'C': [1.0, 3.0, 10.0, 30.0, 100.0, 300.0, 1000.0],
'gamma': [0.01, 0.03, 0.1, 0.3, 1.0, 3.0],
'kernel': ['rbf']}],
scoring='neg_mean_squared_error', verbose=2)
최상 모델의 (5-폴드 교차 검증으로 평가한) 점수는 다음과 같다:
negative_mse = grid_search_search.best_score_
rmse = np.sqrt(-negative_mse)
rmse
70286.61838178603
RandomForestRegressor
보다 훨씬 좋지 않음 => 최상의 하이퍼파라미터를 확인:
grid_search.best_params_
{'C': 30000.0, 'kernel': 'linear'}
- 선형 커널이 RBF 커널보다 성능이 나은 것 같다.
C
는 테스트한 것 중에 최대값이 선택됨 => 따라서 (작은 값들은 지우고) 더 큰 값의C
로 그리드서치를 다시 실행해 보아야 한다.(아마도 더 큰 값의C
에서 성능이 높아질 것이다)
2.GridSearchCV
를 RandomizedSearchCV
로 바꿔보세요.
경고: 사용하는 하드웨어에 따라 다음 셀을 실행하는데 45분 또는 그 이상 걸릴 수 있습니다.
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, random_state=42)
rnd_search.fit(housing_prepared, housing_labels)# 탐색 과정 출력 생략
Fitting 5 folds for each of 50 candidates, totalling 250 fits
[CV] END C=629.782329591372, gamma=3.010121430917521, kernel=linear; total time= 6.9s
[CV] END C=629.782329591372, gamma=3.010121430917521, kernel=linear; total time= 6.7s
[CV] END C=629.782329591372, gamma=3.010121430917521, kernel=linear; total time= 6.9s
.
.
.
RandomizedSearchCV(cv=5, estimator=SVR(), n_iter=50,
param_distributions={'C': <scipy.stats._distn_infrastructure.rv_frozen object at 0x00000242513FEFC8>,
'gamma': <scipy.stats._distn_infrastructure.rv_frozen object at 0x000002421647B088>,
'kernel': ['linear', 'rbf']},
random_state=42, scoring='neg_mean_squared_error',
verbose=2)
최상 모델의 (5-폴드 교차 검증으로 평가한) 점수는 다음과 같다:
negative_mse=rnd_search.best_score_
rmse=np.sqrt(-negative_mse)
rmse
54751.69009256622
이제 RandomForestRegressor
의 성능에 훨씬 가까워졌다(하지만 아직 차이가 남). 최상의 하이퍼파라미터를 확인:
rnd_search.best_params_
{'C': 157055.10989448498, 'gamma': 0.26497040005002437, 'kernel': 'rbf'}
이번에는 RBF 커널에 대해 최적의 하이퍼파라미터 조합을 찾았다. 보통 랜덤서치가 같은 시간안에 그리드서치보다 더 좋은 하이퍼파라미터를 찾는다.
여기서 사용된 scale=1.0
인 지수 분포를 살펴보겠다. 일부 샘플은 1.0보다 아주 크거나 작다. 하지만 로그 분포를 보면 대부분의 값이 exp(-2)와 exp(+2), 즉 0.1과 7.4 사이에 집중되어 있음을 알 수 있다.
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
에 사용된 분포는 매우 다르다. 주어진 범위안에서 균등 분포로 샘플링된다. 그래서 오른쪽 로그 분포가 거의 일정하게 나타난다. 이런 분포는 원하는 스케일이 정확이 무엇인지 모를 때 사용하면 좋다:
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.가장 중요한 특성을 선택하는 변환기를 준비 파이프라인에 추가해보세요.
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()
메서드에서 직접 계산할 수도 있지만 (캐싱을 사용하지 않을 경우) 그리드서치나 랜덤서치의 모든 하이퍼파라미터 조합에 대해 계산이 일어나기 때문에 매우 느려집니다.
선택할 특성의 개수를 지정:
k = 5
최상의 k개 특성의 인덱스를 확인:
top_k_feature_indices = indices_of_top_k(feature_importances, k)
top_k_feature_indices
array([ 0, 1, 7, 9, 12], dtype=int64)
np.array(attributes)[top_k_feature_indices]
array(['longitude', 'latitude', 'median_income', 'pop_per_hhold',
'INLAND'], dtype='<U18')
최상의 k개 특성이 맞는지 다시 확인:
sorted(zip(feature_importances, attributes), reverse=True)[:k]
[(0.3790092248170967, 'median_income'),
(0.16570630316895876, 'INLAND'),
(0.10703132208204355, 'pop_per_hhold'),
(0.06965425227942929, 'longitude'),
(0.0604213840080722, 'latitude')]
이제 이전에 정의한 준비 파이프라인과 특성 선택기를 추가한 새로운 파이프라인을 만듦:
preparation_and_feature_selection_pipeline = Pipeline([
('preparation', full_pipeline),
('feature_selection', TopFeatureSelector(feature_importances, k))
])
housing_prepared_top_k_features = preparation_and_feature_selection_pipeline.fit_transform(housing)
처음 3개 샘플의 특성을 확인:
housing_prepared_top_k_features[0:3]
array([[-0.94135046, 1.34743822, -0.8936472 , 0.00622264, 1. ],
[ 1.17178212, -1.19243966, 1.292168 , -0.04081077, 0. ],
[ 0.26758118, -0.1259716 , -0.52543365, -0.07537122, 1. ]])
최상의 k개 특성이 맞는지 다시 확인:
housing_prepared[0:3, top_k_feature_indices]
array([[-0.94135046, 1.34743822, -0.8936472 , 0.00622264, 1. ],
[ 1.17178212, -1.19243966, 1.292168 , -0.04081077, 0. ],
[ 0.26758118, -0.1259716 , -0.52543365, -0.07537122, 1. ]])
4.전체 데이터 준비 과정과 최종 예측을 하나의 파이프라인으로 만들어보세요.
rnd_search.best_params_={'C': 157055.10989448498, 'gamma': 0.26497040005002437, 'kernel': 'rbf'}
rnd_search.best_params_
{'C': 157055.10989448498, 'gamma': 0.26497040005002437, 'kernel': 'rbf'}
prepare_select_and_predict_pipeline = Pipeline([
('preparation', full_pipeline),
('feature_selection', TopFeatureSelector(feature_importances, k)),
('svm_reg', SVR(**rnd_search.best_params_))
])
prepare_select_and_predict_pipeline.fit(housing, housing_labels)
Pipeline(steps=[('preparation',
ColumnTransformer(transformers=[('num',
Pipeline(steps=[('imputer',
SimpleImputer(strategy='median')),
('attribs_adder',
CombinedAttributesAdder()),
('std_scaler',
StandardScaler())]),
['longitude', 'latitude',
'housing_median_age',
'total_rooms',
'total_bedrooms',
'population', 'households',
'median_income']),
('cat', OneHotEncoder(...
TopFeatureSelector(feature_importances=array([6.96542523e-02, 6.04213840e-02, 4.21882202e-02, 1.52450557e-02,
1.55545295e-02, 1.58491147e-02, 1.49346552e-02, 3.79009225e-01,
5.47789150e-02, 1.07031322e-01, 4.82031213e-02, 6.79266007e-03,
1.65706303e-01, 7.83480660e-05, 1.52473276e-03, 3.02816106e-03]),
k=5)),
('svm_reg',
SVR(C=157055.10989448498, gamma=0.26497040005002437))])
몇 개의 샘플에 전체 파이프라인을 적용:
some_data=housing.iloc[0:4]
some_labels=housing_labels.iloc[0:4]
print("Predictions:\t", prepare_select_and_predict_pipeline.predict(some_data))
print("Labels:\t\t", list(some_labels))
Predictions: [ 83384.49158095 299407.90439234 92272.03345144 150173.16199041]
Labels: [72100.0, 279600.0, 82700.0, 112500.0]
전체 파이프라인이 잘 작동하는 것 같다. 물론 예측 성능이 아주 좋지는 않는다. SVR
보다 RandomForestRegressor
가 더 나은 것 같다.
5.GridSearchCV
를 사용해 준비 단계의 옵션을 자동으로 탐색해보세요.
경고: 사용하는 하드웨어에 따라 다음 셀을 실행하는데 45분 또는 그 이상 걸릴 수 있습니다.
노트: 아래 코드에서 훈련 도중 경고를 피하기 위해 OneHotEncoder
의 handle_unknown
하이퍼파라미터를 'ignore'
로 지정했습니다. 그렇지 않으면 OneHotEncoder
는 기본적으로 handle_unkown='error'
를 사용하기 때문에 데이터를 변활할 때 훈련할 때 없던 범주가 있으면 에러를 냅니다. 기본값을 사용하면 훈련 세트에 모든 카테고리가 들어 있지 않은 폴드를 평가할 때 GridSearchCV
가 에러를 일으킵니다. 'ISLAND'
범주에는 샘플이 하나이기 때문에 일어날 가능성이 높습니다. 일부 폴드에서는 테스트 세트 안에 포함될 수 있습니다. 따라서 이런 폴드는 GridSearchCV
에서 무시하여 피하는 것이 좋습니다.
full_pipeline.named_transformers_["cat"].handle_unknown = 'ignore'
param_grid = [{
'preparation__num__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)
grid_search_prep.fit(housing, housing_labels) # 경고 메시지, 탐색 과정 삭제
Fitting 5 folds for each of 48 candidates, totalling 240 fits
.
.
.
GridSearchCV(cv=5,
estimator=Pipeline(steps=[('preparation',
ColumnTransformer(transformers=[('num',
Pipeline(steps=[('imputer',
SimpleImputer(strategy='median')),
('attribs_adder',
CombinedAttributesAdder()),
('std_scaler',
StandardScaler())]),
['longitude',
'latitude',
'housing_median_age',
'total_rooms',
'total_bedrooms',
'population',
'households',
'median_inc...
5.47789150e-02, 1.07031322e-01, 4.82031213e-02, 6.79266007e-03,
1.65706303e-01, 7.83480660e-05, 1.52473276e-03, 3.02816106e-03]),
k=5)),
('svm_reg',
SVR(C=157055.10989448498,
gamma=0.26497040005002437))]),
param_grid=[{'feature_selection__k': [1, 2, 3, 4, 5, 6, 7, 8, 9,
10, 11, 12, 13, 14, 15, 16],
'preparation__num__imputer__strategy': ['mean',
'median',
'most_frequent']}],
scoring='neg_mean_squared_error', verbose=2)
grid_search_prep.best_params_
{'feature_selection__k': 1, 'preparation__num__imputer__strategy': 'mean'}
최상의 Imputer
정책은 most_frequent
이고 거의 모든 특성이 유용(16개 중 15개). 마지막 특성(ISLAND
)은 잡음이 추가될 뿐이다.
출처
- 핸즈온 머신러닝 2판