[ 핸즈 온 머신러닝 2판 ] pandas, sklearn을 통한 모델 학습과 튜닝은 어떻게 하는 것일까? (3)
어제보다 나은 사람이 되기

걱정보단 실행을, 그러나 계획적으로

Box World 자세히보기

AI/Hands-On Machine Learning 2판

[ 핸즈 온 머신러닝 2판 ] pandas, sklearn을 통한 모델 학습과 튜닝은 어떻게 하는 것일까? (3)

Box형 2020. 7. 16. 14:35
반응형

 이전 2개의 포스팅에 결쳐 우리는 지금까지 문제를 정의하고 데이터를 읽어들여 탐색하였습니다. 그리고 데이터를 training set과 test set으로 나누고 학습을 위한 머신러닝 알고리즘에 주입할 데이터를 자동으로 전처리하고 정제하는 파이프라인까지 만들어 보았습니다.

 이번 포스팅에서는 머신러닝 모델을 선택하고 훈련시켜 세부적으로 튜닝하는 법까지 다뤄보겠습니다.


box-world.tistory.com/42

 

[ 핸즈 온 머신러닝 2판 ] pandas, sklearn을 통한 데이터 전처리는 어떻게 하는걸까? (1)

발견에는 항상 뜻밖의 재미가 있다 - 제프 베조스(Amazon CEO) - Chapter 2 이번 포스팅을 시작으로 3번에 걸쳐 하나의 머신러닝 프로젝트가 어떻게 구성되고 진행되는지 알아보겠습니다. 우선 주요 단

box-world.tistory.com

box-world.tistory.com/43

 

[ 핸즈온 머신러닝 2판 ] pandas, sklearn을 통한 데이터 전처리는 어떻게 하는걸까? (2)

 저번 포스팅에서는 캘리포니아 주택 가격 데이터셋을 가지고 pandas, sklearn을 이용하여 데이터의 특성을 탐색하고, 모델 학습을 위해 test set을 분리하는 다양한 방법에 대해 알아보았습니다.  ��

box-world.tistory.com


2.6 모델 선택과 훈련

2.6.1 training set에서의 훈련 및 평가

 우선 가장 대표적인 모델인 linear regression을 훈련시켜보겠습니다.

from sklearn.linear_model import LinearRegression

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

 이제 우리는 linear regression 모델을 만들었으니, training set의 일부 샘플을 넣어보겠습니다.

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

###결과값###
>>
예측:  [210644.60459286 317768.80697211 210956.43331178  59218.98886849
 189747.55849879]
print("레이블: ", list(some_labels))

###결과값###
>>
레이블:  [286600.0, 340600.0, 196900.0, 46300.0, 254500.0]

 아주 정확한 예측은 아니지만, 어느정도 작동하는 것을 볼 수 있습니다. 이제 sklearn의 mean_sqaure_error 함수를 이용하여 전체 training set에 대한 이 linear regression 모델의 rmse를 측정해보겠습니다.

 대부분 구역의 median house value가 $120000~$265000 사이인 것을 감안하면, $68628의 오차는 그리 좋은 편은 아닌 것 같습니다. 이러한 결과는 모델이 과소 적합(Underfit) 되었기 때문이며 이는 데이터가 부족하거나, 모델이 강력하지 못한 탓입니다. 우선 좀 더 복잡한 모델을 시도해서 어떻게 되는지 확인해보겠습니다.

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

 DecisionTreeRegressor를 훈련시켜보겠습니다. 이 모델은 강력하며, 데이터에서 복잡한 비선형관계를 찾을 수 있습니다.(DecisionTree에 대해서는 추후에 설명드리겠습니다.)

from sklearn.tree import DecisionTreeRegressor

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

 이제 모델을 평가해보겠습니다. 0.0이라는 것은 오차가 없다는 뜻인데, 모델이 완벽할 리는 없고 아마 데이터가 심각하게 과대 적합(Overfit) 되었을 확률이 큽니다. 하지만 이 또한 확신할 수 없습니다. 따라서 우리는 training set에서 일부를 교차 검증(cross-validation) 데이터로 분리시켜 모델을 평가하는데 사용해야 합니다.

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 Cross-Validation을 사용한 평가

 DecisionTreeRegressor 모델을 이어서 계속 보겠습니다. 우선 이전에 정의했던 train_test_split 함수를 사용하여 training set을 더 작은 traing set과 cv set으로 나누고, training set에서는 모델 훈련을, cv set에서는 모델 평가가 이루어지게 하면 됩니다.

 혹은 훌륭한 대안으로 sklearn의 k-fold cross-validation 기능을 사용할 수 있습니다. 이는 training set를 fold라 불리는 10개의 subset으로 무작위 분할합니다. 그 후 DecisionTree 모델을 10번 훈련하고 평가하는데, 이때 매번 다른 하나의 fold를 사용하여 평가하고 나머지 9개는 훈련에 사용됩니다. 그리고 10개의 평가 점수가 담긴 배열이 결과가 됩니다.

 np.sqrt()에 -scores가 들어간 것은 cross_val_score() 메서드의 scoring 매개변수는 낮을수록 좋은 loss function이 아니라, 클수록 좋은 utility function을 기대합니다. 따라서 MSE의 반대 즉 음숫값을 계산하는 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)

 아래 결과를 살펴보면 DecisionTreeRegressor의 결과가 이전만큼 좋아보이지 않습니다. 심지어 linear regression 모델보다 안좋습니다. 이를 통해 Cross-validation으로는 모델의 성능 추정뿐 아니라 이 추정이 얼마나 정확한지(표준편차)를 측정할 수 있습니다.

 DecisionTree 점수가 71407에서 ±2439 사이입니다. cv set을 하나만 사용했다면 이를 알 수 없었을 것입니다. 그러나 모델을 여러 번 훈련시키는 것은 cost가 높으므로 이 점을 고려하며 사용해야겠습니다.

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

display_scores(tree_rmse_scores)

###결과값###
>>
점수: [69649.64460859 66090.16419858 70329.66447084 69160.83207592
 70549.37962702 73640.27705273 70815.59582659 70998.36764945
 76652.63720653 68576.3158628 ]
평균: 70646.28785790454
표준편차: 2711.925409096817

 비교를 위해 linear regression 모델의 점수를 계산해보겠습니다. 확실히 DecisionTreeRegressor가 Overfit되어 성능이 나쁘다는 것을 알 수 있는 대목입니다.

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)

###결과값###
>>
점수: [66782.73843989 66960.118071   70347.95244419 74739.57052552
 68031.13388938 71193.84183426 64969.63056405 68281.61137997
 71552.91566558 67665.10082067]
평균: 69052.46136345083
표준편차: 2731.674001798344

 마지막으로 RandomForestRegressor 모델을 하나 더 시도해보겠습니다. 이는 feature를 무작위로 선택하여 많은 DecisionTree를 만들고 그 예측들을 평균 내는 방식으로 작동합니다.

 이렇게 여러 모델을 만들어 하나의 모델을 만드는 것을 앙상블 학습(Ensemble Learning)이라고 하며 머신러닝 알고리즘의 성능을 극대화하는 방식 중 하나입니다.

from sklearn.ensemble import RandomForestRegressor

forest_reg = RandomForestRegressor()
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

###결과값###
>>
18790.050174547516

 훨씬 훌륭해보입니다. 그러나 training set에 대한 점수가 cv set에 대한 점수보다 훨씬 낮다는 것은 training set에 여전히 Overfit되어 잇따는 의미입니다. 이러한 Overfit을 해결하려면 모델을 간단히 하거나, 더 많은 데이터를 모아야 합니다.

scores = cross_val_score(forest_reg, housing_prepared, housing_labels,
                         scoring = "neg_mean_squared_error", cv=10)
forest_rmse_scores = np.sqrt(-scores)
display_scores(forest_rmse_scores)

###결과값###
>>
점수: [49621.96770814 47698.25757407 50071.21751987 52311.76799284
 49529.02895964 53216.72302009 48755.50575797 47792.20315641
 53017.97804452 49841.16154041]
평균: 50185.58112739592
표준편차: 1912.3687121407233

 특히 모델 선정 시 중요한 것은 하나의 모델을 오래 붙잡고 하이퍼파라미터 튜닝에 시간을 소요하기 전 2~5개 정도의 모델을 선정한 후 다양한 모델을 만들어보고 큰 그림을 그리는 것이 좋습니다.


 다음은 실험한 sklearn 모델을 쉽게 저장할 수 있는 파이썬 패키지입니다.

import joblib

##모델 저장 시
joblib.dump(모델명,"my_model.pkl")

##모델 복원 시
my_model_loaded = joblib.load("my_model.pkl")

2.7 모델 세부 튜닝

 이제 쓸만한 모델을 대충 추렸다고 가정하고, 이 모델들을 각각 세부 튜닝해보겠습니다.

2.7.1 그리드 탐색(Grid Search)

 가장 단순한 방법은 만족할 만한 하이퍼 파라미터 조합을 찾을 때까지 수동으로 조정하는 것입니다.

 sklearn의 GridSearchCV를 사용하면, 탐색하고자 하는 하이퍼파라미터와 시도해볼 값을 지정만 하면, 가능한 모든 하이퍼파라미터 조합에 대한 Cross-Validation을 사용해 평가합니다. 다음은 RandomForestRegressor에 대한 코드입니다. (보통 어떤 하이퍼파라미터 값을 지정해야 할지 모르겠다면 연속된 10의 거듭제곱(10, 100, 1000...)을 시도해보는 것이 좋습니다.)

  param_grid에 따라 sklearn이 n_estimators와 max_features의 3 * 4 조합을 평가한 후, 두 번째 dict에 있는 2 * 3 조합을 시도합니다. 이를 모두 합하면 총 18개 조합을 탐색하고 cv = 5에 의해 각각 5번 모델을 훈련시킵니다. 즉 18 * 5 = 90번을 훈련시킵니다.

from sklearn.model_selection import GridSearchCV

param_grid = [
              {'n_estimators' : [3, 10, 30], 'max_features': [2, 4, 6, 8]},
              {'bootstrap':[False],'n_estimators':[3,10], 'max_features':[2,3,4]},
]

forest_reg = RandomForestRegressor()

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_

###결과값###
>>
{'max_features': 8, 'n_estimators': 30}

 최적의 estimator에 직접 접근할 수도 있습니다.

grid_search.best_estimator_

###결과값###
>>
RandomForestRegressor(bootstrap=True, ccp_alpha=0.0, criterion='mse',
                      max_depth=None, max_features=8, max_leaf_nodes=None,
                      max_samples=None, min_impurity_decrease=0.0,
                      min_impurity_split=None, min_samples_leaf=1,
                      min_samples_split=2, min_weight_fraction_leaf=0.0,
                      n_estimators=30, n_jobs=None, oob_score=False,
                      random_state=None, verbose=0, warm_start=False)

 그리고 18개의 조합에 대한 평가 점수도 확인할 수 있습니다. 어쨌든 우리는 이제 최적의 모델을 찾았습니다.

cvres = grid_search.cv_results_
for mean_score, params in zip(cvres["mean_test_score"],cvres["params"]):
print(np.sqrt(-mean_score),params)
  
###결과값###
>>
62600.75458160353 {'max_features': 2, 'n_estimators': 3}
55591.35252026881 {'max_features': 2, 'n_estimators': 10}
52846.87575009015 {'max_features': 2, 'n_estimators': 30}
59730.917651981195 {'max_features': 4, 'n_estimators': 3}
52997.7576498801 {'max_features': 4, 'n_estimators': 10}
50563.70361731654 {'max_features': 4, 'n_estimators': 30}
59290.062986907295 {'max_features': 6, 'n_estimators': 3}
51769.540932619544 {'max_features': 6, 'n_estimators': 10}
50116.10761043424 {'max_features': 6, 'n_estimators': 30}
59543.28349500444 {'max_features': 8, 'n_estimators': 3}
51715.32187214485 {'max_features': 8, 'n_estimators': 10}
49920.36274608907 {'max_features': 8, 'n_estimators': 30}
62010.100776085696 {'bootstrap': False, 'max_features': 2, 'n_estimators': 3}
54309.058249698515 {'bootstrap': False, 'max_features': 2, 'n_estimators': 10}
59237.80817701035 {'bootstrap': False, 'max_features': 3, 'n_estimators': 3}
52482.66370840826 {'bootstrap': False, 'max_features': 3, 'n_estimators': 10}
58842.04223735213 {'bootstrap': False, 'max_features': 4, 'n_estimators': 3}
51687.84566902621 {'bootstrap': False, 'max_features': 4, 'n_estimators': 10}

2.7.2 Random Search

 Grid Search는 위에서 본 적은 수의 조합을 탐구할 때 괜찮습니다. 그러나 탐색의 조합 수가 커지면 RandomizedSearchCV를 사용하는 편이 더 좋습니다. RandomizedSearchCV는 GridSearchCV와 거의 같은 방식으로 사용하지만, 모든 조합을 시도하는 대신 각 반복마다 하이퍼파라미터에 임의의 수를 대입하여 지정한 횟수만큼 평가합니다.

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)

###결과값###
>>
RandomizedSearchCV(cv=5, error_score=nan,
                   estimator=RandomForestRegressor(bootstrap=True,
                                                   ccp_alpha=0.0,
                                                   criterion='mse',
                                                   max_depth=None,
                                                   max_features='auto',
                                                   max_leaf_nodes=None,
                                                   max_samples=None,
                                                   min_impurity_decrease=0.0,
                                                   min_impurity_split=None,
                                                   min_samples_leaf=1,
                                                   min_samples_split=2,
                                                   min_weight_fraction_leaf=0.0,
                                                   n_estimators=100,
                                                   n_jobs=None, oob_score=Fals...
                                                   warm_start=False),
                   iid='deprecated', n_iter=10, n_jobs=None,
                   param_distributions={'max_features': <scipy.stats._distn_infrastructure.rv_frozen object at 0x7f20324dfeb8>,
                                        'n_estimators': <scipy.stats._distn_infrastructure.rv_frozen object at 0x7f20324df668>},
                   pre_dispatch='2*n_jobs', random_state=42, refit=True,
                   return_train_score=False, scoring='neg_mean_squared_error',
                   verbose=0)
cvres = rnd_search.cv_results_
for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]):
    print(np.sqrt(-mean_score), params)
    
###결과값###
>>
49150.70756927707 {'max_features': 7, 'n_estimators': 180}
51389.889203389284 {'max_features': 5, 'n_estimators': 15}
50796.155224308866 {'max_features': 3, 'n_estimators': 72}
50835.13360315349 {'max_features': 5, 'n_estimators': 21}
49280.9449827171 {'max_features': 7, 'n_estimators': 122}
50774.90662363929 {'max_features': 3, 'n_estimators': 75}
50682.78888164288 {'max_features': 3, 'n_estimators': 88}
49608.99608105296 {'max_features': 5, 'n_estimators': 100}
50473.61930350219 {'max_features': 3, 'n_estimators': 150}
64429.84143294435 {'max_features': 5, 'n_estimators': 2}

2.7.3 앙상블 방법

 모델 튜닝의 또 다른 방법은 최상의 모델을 연결해보는 것입니다. 다시 말해서 DecisionTree의 앙상블 모델인 RandomForest가 성능이 더 좋은 것처럼 모델의 그룹이 최상의 단일 모델보다 더 나은 성능을 발휘할 때가 많습니다. 특히 개개의 모델이 각기 다른 형태의 오차를 만들 때 더욱 그렇습니다.


2.7.4 최상의 모델과 오차 분석

 최상의 모델을 분석하면 문제에 대한 좋은 통찰을 얻는 경우가 많습니다. 예를 들어 RandomForestregressor가 정확한 예측을 만들기 위한 각 feature의 상대적인 중요도를 알려줍니다.

feature_importances = grid_search.best_estimator_.feature_importances_
feature_importances

###결과값###
>>
array([7.08371932e-02, 6.56756347e-02, 4.27773453e-02, 1.52918532e-02,
       1.38124536e-02, 1.50121082e-02, 1.44797217e-02, 3.56732982e-01,
       5.52871544e-02, 1.15384814e-01, 6.37338038e-02, 6.96180139e-03,
       1.58719639e-01, 6.57055394e-05, 2.13890180e-03, 3.08888805e-03])

 가독성을 위해 각 중요도에 대응하는 feature를 함께 출력해보겠습니다. 아래 정보를 바탕으로 덜 중요한 특성들은 제외할 수도 있습니다.

 이렇게 시스템이 특정 오차를 만들었다면 feature를 추가하거나, 제거하거나, 이상치(Anomaly data)를 지우는 등 문제를 해결하는 방법을 찾으려 해야합니다.

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.35673298193099007, 'median_income'),
 (0.1587196393569354, 'INLAND'),
 (0.11538481389295871, 'pop_per_hhold'),
 (0.07083719317436987, 'longitude'),
 (0.06567563468470206, 'latitude'),
 (0.06373380379050103, 'bedrooms_per_room'),
 (0.05528715436665197, 'rooms_per_hhold'),
 (0.04277734534225705, 'housing_median_age'),
 (0.015291853205156254, 'total_rooms'),
 (0.015012108190849801, 'population'),
 (0.014479721702475657, 'households'),
 (0.013812453588641672, 'total_bedrooms'),
 (0.006961801391129748, '<1H OCEAN'),
 (0.0030888880452384668, 'NEAR OCEAN'),
 (0.002138901797746015, 'NEAR BAY'),
 (6.570553939631248e-05, 'ISLAND')]

2.7.5 Test set로 시스템 평가하기

 어느 정도 모델 튜닝을 통해 만족할 만한 모델을 얻었으니 이제는 test set을 이용하여 최종 모델을 평가해보겠습니다.

 우선 test set에서 예측 변수와 label을 얻은 후 이전에 만들었던 full_pipeline을 사용해 데이터를 변환합니다. 이때 test set에서 training하면 안되기 때문에 fit_transform()이 아닌 transform()입니다. 그리고 test set에서 최종 모델을 평가합니다.

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)

###결과값###
>>
47730.0

 이렇게 우리는 3번의 포스팅에 걸쳐 하나의 머신러닝 프로젝트 안에서 데이터를 가져와 특징을 파악하고, 전처리하여 모델 훈련 후 튜닝까지 하는 법을 대략적으로 살펴보았습니다.

긴 글 읽어주셔서 감사합니다. 오늘도 행복한 하루 보내시길 바랍니다:)

반응형