본문 바로가기
  • 조금 느려도, 꾸준히
Artificial Intelligence/Machine Learning

핸즈온 머신러닝(3) - 머신러닝 프로젝트 6[마무리]

by chan 2020. 1. 21.
반응형

# 모델 선택과 훈련

 지금까지 문제를 정의한 후 데이터를 읽어 들이고 탐색하였으며, 훈련 세트와 테스트 세트로 나누고 머신러닝 알고리즘에 주입할 데이터를 자동으로 정제하고 준비하기 위해 변환 파이프라인을 작성하였다. 이번 포스트에서는 머신러닝 모델을 선택하고 훈련시키는 과정에 대해 다룰 것이다.

 

# 훈련 세트에서 훈련하고 평가하기

 이전 포스트에서 했던 것처럼 선형 회귀 모델(Linear Regression)을 훈련시켜 본다.

from sklearn.linear_model import LinearRegression

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

# LinearRegression(copy_X=True, fit_intercept=True, n_jobs=None, normalize=False)

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

# 예측: [210644.60459286 317768.80697211 210956.43331178  59218.98886849 189747.55849879]
 
 print("레이블:", list(some_labels))
 
 # 레이블: [286600.0, 340600.0, 196900.0, 46300.0, 254500.0]

 정확하지 않지만 작동하는것을 볼 수 있다. 사이킷런의 mean_square_error 함수를 사용해 전체 훈련 세트에 대한 이 회귀 모델의 RMSE를 측정해보도록 한다.

 *앞 포스트에서 다뤘던 RMSE의 수식을 다시 첨부해보면 다음과 같다.

RMSE 수식 모델

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

 예측 오차가 68.628$ 인 것은 사실 만족스럽지 못한 결과이다. 이는 모델이 훈련 데이터에 과소적합된 사례이며, 이런 상황은 특성들이 좋은 예측을 만들만큼 충분한 정보를 제공하지 못했거나 모델이 충분히 강력하지 못하다는 사실을 말해준다. 

 더 강력하고 데이터에서 복잡한 비선형 관계를 찾을 수 있는 DecisionTreeRegressor(결정 트리 모델)를 훈련시켜 결과를 살펴보도록 한다.

from sklearn.tree import DecisionTreeRegressor

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

"""
DecisionTreeRegressor(criterion='mse', max_depth=None, max_features=None,
                      max_leaf_nodes=None, min_impurity_decrease=0.0,
                      min_impurity_split=None, min_samples_leaf=1,
                      min_samples_split=2, min_weight_fraction_leaf=0.0,
                      presort=False, random_state=42, splitter='best')
"""

 훈련 세트로 평가해본 결과는 다음과 같다.

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

오차가 전혀 없는 것으로 나타난다. 사실 이 모델은 데이터에 너무 심하게 과대적합되어 나타난 결과이다. 우리는 확신이 드는 모델이 론칭할 준비가 되기 전까지 테스트 세트는 가급적 사용을 삼가고 훈련 세트의 일부분으로 훈련을 하고 다른 일부분은 모델 검증에 사용해야 한다.

 

 

# 교차 검증을 사용한 평가

 결정 트리 모델을 평가하기 위해, train_test_split 함수를 사용해 훈련세트를 나누어 더 작은 훈련 세트에서 모델을 훈련시키고 검증 세트로 모델을 평가하는 방법이 있다. 

 

 또다른 대안으로 사이킷런의 교차 검증 기능을 사용할 수 있다. 다음은 K-겹 교차 검증 을 수행하는 코드인데, 훈련 세트를 폴드(fold)라 불리는 10개의 서브셋으로 무작위로 분할하여, 결정 트리 모델을 10번 훈련 하고 평가하는데 매번 다른 폴드를 선택해 평가에 사용하고 나머지 9개의 폴드를 훈련에 사용한다. 10개의 평가 점수가 담긴 배열이 결과값으로 도출된다.

from sklearn.model_selection import cross_val_score

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

 사이킷런의 교차검증 기능은 scoring 매개변수로 비용함수가 아닌 효용함수(클수록 좋은)를 기대한다. 따라서 평균제곱오차의 반댓값을 계산하는 neg_mean_squared_error 함수를 사용하고, 제곱근을 계산하기 전에 -scores로 부호를 바꿔준다.

 

결과를 살펴보면 다음과 같다.

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

display_scores(tree_rmse_scores)

"""
점수: [70194.33680785 66855.16363941 72432.58244769 70758.73896782
 71115.88230639 75585.14172901 70262.86139133 70273.6325285
 75366.87952553 71231.65726027]
평균: 71407.68766037929
표준편차: 2439.4345041191004
"""

 다음은 선형 회귀 모델의 점수이다.

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.674001798349
"""

 결정 트리 결과는 실제로 선형 회귀 모델보다 성능이 좋지 않았다. 

 교차 검증으로 모델의 성능을 추정하는 것뿐만 아니라 이 추정이 얼마나 정확한지 표준편차를 이용하여 측정할 수 있다.

 또한 교차 검증은 모델을 여러 번 훈련시켜야 하기 때문에 비용이 비싸므로 언제나 교차 검증을 쓸 수 있는 것은 아니다.

 

마지막으로 RandomForestRegressor 모델을 시도해 보도록 한다. 랜덤 포레스트는 특성을 무작위로 선택해서 많은 결정 트리를 만들고 그 예측을 평균 내는 방식으로 작동한다. 여러 다른 모델을 모아 하나의 모델을 만드는 것을 앙상블 학습이라고 하며 머신러닝 알고리즘의 성능을 극대화 하는 방법 중 하나이다. 코드는 다른 모델들과 기본적으로 비슷하다.

from sklearn.ensemble import RandomForestRegressor

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

housing_predictions = forest_reg.predict(housing_prepared)
forest_mse = mean_squared_error(housing_labels, housing_predictions)
forest_rmse = np.sqrt(forest_mse)
forest_rmse #21933.31414779769

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)

"""
점수: [51646.44545909 48940.60114882 53050.86323649 54408.98730149
 50922.14870785 56482.50703987 51864.52025526 49760.85037653
 55434.21627933 53326.10093303]
평균: 52583.72407377466
표준편차: 2298.353351147122
"""

앞의 두 모델보다 훨씬 낫지만, 이 모델 역시 훈련 세트에 대한 점수(rmse)가 검증세트에 대한 점수보다 훨씬 낮으므로 여전히 훈련 세트에 과대적합되어있는것을 볼 수 있다.

 

좋은 결과를 얻기 위해서 여러 종류의 머신러닝 알고리즘으로 하이퍼파라미터 조정에 너무 많은 시간을 들이지 않으면서 다양한 모델(다양한 커널의 서포트 벡터 머신, 신경망 등)을 시도해 봐야 한다.

 

* 실험한 모델을 모두 저장해두면 필요할 때 쉽게 모델을 복원할 수 있다. 파이썬의 pickle 패키지나 sklearn.externals.joblib을 사용하여 사이킷런 모델을 간단하게 저장할 수 있다.

 

 

# 모델 세부 튜닝

 가능성 있는 모델들을 추렸다면, 이 모델들을 세부 튜닝하는 방법이 몇가지 존재한다.

 

1. 그리드 탐색

 사이킷런의 GridSearchCV를 사용하여 탐색하고자 하는 하이퍼파라미터와 시도해볼 값을 지정하면 가능한 모든 하이퍼파라미터 조합에 대해 교차 검증을 사용해 평가할 수 있다. 다음은 RandomForestRegressor에 대한 최적의 하이퍼파라미터 조합을 탐색하는 코드이다.

 

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, n_jobs=-1)
grid_search.fit(housing_prepared, housing_labels)

best_params_ 메서드를 이용해 최적의 하이퍼파라미터 조합을 얻을 수 있다.

grid_search.best_params_

# {'max_features': 8, 'n_estimators': 30} 

 * 8과 30은 탐색 범위의 최댓값이기 때문에 계속 점수가 향상될 가능성이 있다. 이때 더 큰 값으로 다시 검색해야 한다.

 

grid_search.best_estimator_를 이용해 최적의 추정기에 직접 접근할 수도 있으며, grid_search.cv_results_ 의 ['mean_test_score'], ['params'] 항목을 이용해 평가 점수도 확인할 수 있다.

 

 

2. 랜덤 탐색

 하이퍼파라미터 탐색 공간이 커지면 그리드 탐색 방법보다 RandomizedSearchCV를 사용하는 편이 좋다. 랜덤 탐색의 주요 장점은 1) 반복횟수를 늘리면 그만큼 하이퍼파라미터가 각기 다른 개수의 값을 탐색한다 2) 반복 횟수를 조절하는 것만으로 하이퍼파라미터 탐색에 투입할 컴퓨팅 자원을 제어할 수 있다.

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, n_jobs=-1)
rnd_search.fit(housing_prepared, housing_labels)

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

 

3. 앙상블 방법

 최상의 모델을 연결해보는 것 또한 모델을 세밀하게 튜닝하는 또 다른 방법이 될 수 있다. 특히 개개의 모델이 각기 다른 형태의 오차를 만들 때 효과적이다.

 

 

4. 최상의 모델과 오차 분석

 최상의 모델을 분석하면 문제에 대한 좋은 통찰을 얻는 경우가 많다. 

 아래 코드는 랜덤포레스트 모델이 정확한 예측을 만들기 위한 각 특성의 상대적인 중요도를 나타낸 것이다.

feature_importances = grid_search.best_estimator_.feature_importances_
extra_attribs = ["rooms_per_hhold", "pop_per_hhold", "bedrooms_per_room"]
# cat_encoder = cat_pipeline.named_steps["cat_encoder"]
cat_encoder = full_pipeline.named_transformers_["cat_encoder"]
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.36615898061813423, 'median_income'),
 (0.16478099356159054, 'INLAND'),
 (0.10879295677551575, 'pop_per_hhold'),
 (0.07334423551601243, 'longitude'),
 (0.06290907048262032, 'latitude'),
 (0.056419179181954014, 'rooms_per_hhold'),
 (0.053351077347675815, 'bedrooms_per_room'),
 (0.04114379847872964, 'housing_median_age'),
 (0.014874280890402769, 'population'),
 (0.014672685420543239, 'total_rooms'),
 (0.014257599323407808, 'households'),
 (0.014106483453584104, 'total_bedrooms'),
 (0.010311488326303788, '<1H OCEAN'),
 (0.0028564746373201584, 'NEAR OCEAN'),
 (0.0019604155994780706, 'NEAR BAY'),
 (6.0280386727366e-05, 'ISLAND')]
"""

 

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

 어느 정도 모델을 튜닝하면 테스트 세트에서 최종 모델을 평가할 차례가 된다. 테스트 세트에서 예측 변수와 레이블을 얻은 후 full_pipeline을 사용해 데이터를 변환하고(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) # 47730.22690385927

하이퍼파라미터 튜닝을 많이 했다면 교차 검증을 사용해 측정한 것보다 조금 성능이 낮을 수 있다. 이런 경우가 생기더라도 테스트 세트에서 성능 수치를 좋게 하기 위해 하이퍼파라미터를 튜닝하려고 시도해서는 안된다. 그렇게 향상된 성능은 새로운 데이터에 일반화되기 어렵기 때문이다.

 

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

 제품 시스템에 적용하기 위해서 입력 데이터 소스를 시스템에 연결하고 테스트 코드를 작성해야 한다.

 

 또한 일정 간격으로 시스템의 실시간 성능을 체크하고 성능이 떨어졌을 때 알람을 통지할 수 있는 모니터링 코드를 작성해야 한다. 

 

 시스템의 성능을 평가하기 위해 시스템의 예측을 샘플링해서 평가해야 한다. 일반적으로 이런 과정은 사람의 분석이 필요하기 때문에 사람의 분석 파이프라인과 시스템을 연결해야 한다.

 

 시스템의 입력 데이터 품질 역시 평가해야 한다. 시스템 입력을 모니터링 하면 품질 낮은 시그널로 인한 성능 감소를 더 빨리 잡아낼 수 있다. (특히 온라인 학습 시스템에서 입력 모니터링이 중요하다)

 

새로운 데이터를 사용해 정기적으로 모델을 훈련시켜야 한다. 가능하면 이 과정을 자동화해야 한다. 온라인 학습 시스템이라면 일정 간격으로 시스템의 상태를 스냅샷으로 저장하여 이전 상태로 쉽게 되돌릴 수 있도록 해야 한다.

 

반응형

댓글