TOP
class="layout-aside-left paging-number">
본문 바로가기
[파이썬 Projects]/<파이썬 머신러닝>

[머신러닝] 회귀 - 캐글 주택 가격

by 기록자_Recordian 2024. 10. 23.
728x90
반응형
회귀 관련 내용

[머신러닝] 회귀(Regression)

[머신러닝]경사 하강법(GD, gradient descent)

[머신러닝] 회귀 - LinearRegression 클래스

[머신러닝] 회귀 - 다항 회귀와 과대(과소) 적합

[머신러닝] 회귀 - 규제 선형 모델: 릿지, 라쏘, 엘라스틱넷

[머신러닝] 로지스틱 회귀

[머신러닝] 회귀 트리


캐글 주택 가격 실습 (고급 회귀 기법)

 

이번에는 캐글에서 제공하는

캐글 주택 가격: 고급 회귀 기법(House Prices: Advanced Regression Techniques) 데이터 세트를 이용해 회귀 분석을 더 심층적으로 학습해 본다.

캐글(하단 링크)에 접속하여 해당 실습의 데이터 (train.csv)를 다운 받는다. (로그인 및 경쟁 규칙 동의 필요)

 

House Prices - Advanced Regression Techniques

Predict sales prices and practice feature engineering, RFs, and gradient boosting

www.kaggle.com

 

[데이터 필드]
데이터 설명 파일에서 찾을 수 있는 내용의 간략한 내용은 다음과 같다.

SalePrice - 부동산의 달러 판매 가격. 이것은 당신이 예측하려고 하는 대상 변수.
MSSubClass : 빌딩 클래스
MSZoning : 일반적인 구역 분류
LotFrontage : 부지에 연결된 거리의 선형 피트
LotArea : 부지 크기(제곱 피트)
도로 : 도로 접근 유형
골목길 : 골목길 접근 유형
LotShape : 부동산의 일반적인 모양
LandContour : 부동산의 평탄도
유틸리티 : 사용 가능한 유틸리티 유형
LotConfig : Lot 구성
LandSlope : 부지의 경사
이웃 : Ames 시 경계 내의 물리적 위치
조건1 : 주요 도로 또는 철도와의 근접성
조건2 : 주요 도로 또는 철도와의 근접성(두 번째 도로가 있는 경우)
BldgType : 주거 유형
HouseStyle : 주거 스타일
OverallQual : 전반적인 소재 및 마감 품질
OverallCond : 전체 상태 평가
YearBuilt : 원래 건설 날짜
YearRemodAdd : 리모델링 날짜
RoofStyle : 지붕의 종류
RoofMatl : 지붕재료
외부1 : 주택의 외부 덮개
외부2차 : 주택의 외부 덮개(자재가 두 개 이상인 경우)
MasVnrType : 석조 베니어 유형
MasVnrArea : 석조 베니어 면적(제곱 피트)
ExterQual : 외장재 품질
ExterCond : 외부 재료의 현재 상태
기초 : 기초의 종류
BsmtQual : 지하실의 높이
BsmtCond : 지하실의 일반적인 상태
BsmtExposure : 워크아웃 또는 정원 수준의 지하실 벽
BsmtFinType1 : 지하실 완성 구역의 품질
BsmtFinSF1 : 타입 1 완성된 평방 피트
BsmtFinType2 : 두 번째로 완성된 영역의 품질(존재하는 경우)
BsmtFinSF2 : Type 2 완성된 평방 피트
BsmtUnfSF : 지하실 면적의 미완성 평방 피트
TotalBsmtSF : 지하 공간의 총 제곱 피트
난방 : 난방 유형
HeatingQC : 난방 품질 및 상태
CentralAir : 중앙 에어컨
전기 : 전기 시스템
1stFlrSF : 1층 평방 피트
2ndFlrSF : 2층 평방 피트
LowQualFinSF : 낮은 품질의 완성된 평방 피트(모든 층)
GrLivArea : 지상(지상) 거주 공간 평방 피트
BsmtFullBath : 지하 전체 욕실
BsmtHalfBath : 지하 반 욕실
FullBath : 지상 위에 있는 완전한 욕실
HalfBath : 지상에 있는 반쪽 욕실
침실 : 지하층 위에 있는 침실 수
주방 : 주방의 수
KitchenQual : 주방 품질
TotRmsAbvGrd : 지상 위의 총 객실 수(욕실은 포함하지 않음)
기능적 : 가정 기능 평가
벽난로 : 벽난로 수
FireplaceQu : 벽난로 품질
GarageType : 차고 위치
GarageYrBlt : 차고가 지어진 연도
GarageFinish : 차고 내부 마감
GarageCars : 차량 수용 인원에 따른 차고 크기
차고 면적 : 차고의 크기(제곱 피트)
GarageQual : 차고 품질
GarageCond : 차고 상태
PavedDrive : 포장된 차도
WoodDeckSF : 목재 데크 면적 (제곱 피트)
OpenPorchSF : 평방 피트 단위의 오픈 포치 공간
EnclosedPorch : 평방 피트 단위의 밀폐된 현관 면적
3SsnPorch : 3시즌 포치 면적 (제곱 피트)
ScreenPorch : 스크린 포치 면적 (제곱 피트)
PoolArea : 풀 면적 (제곱 피트)
PoolQC : 풀 품질
울타리 : 울타리 품질
MiscFeature : 다른 카테고리에 포함되지 않은 기타 기능
MiscVal : 기타 기능의 $Value
MoSold : 판매된 달
YrSold : 판매된 연도
SaleType : 판매 유형
판매조건 : 판매조건


데이터 사전 처리(Preprocessing)

 

- 데이터 로드 및 확인

필요한 모듈과 데이터를 로딩하여 개략적으로 데이터를 확인해 본다.

import warnings
warnings.filterwarnings('ignore')
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline

house_df_org = pd.read_csv("../Data/05. train_House.csv")
house_df = house_df_org.copy() # 많은 데이터 가공이 예상되므로 원본 보관
house_df.head()

Target 값은 맨 마지막 칼럼인 SalePrice 이다.

 

- 데이터 세트의 전체 크기와 칼럼 타입, Null이 있는 칼럼과 그 건수를 내림차순으로 출력

print('데이터 세트의 Shape:', house_df.shape)
print('\n전체 피처의 type \n', house_df.dtypes.value_counts())
isnull_series = house_df.isnull().sum()
print('\nNull 칼럼과 그 건수:\n', isnull_series[isnull_series > 0].sort_values(ascending=False))

 

  • 데이터 세트는 1450개의 레코드와 81개의 피처로 구성
  • 피처의 타입은 숫자형, 문자형
  • Target을 제외한 80개의 피처 중 43개가 문자형이며, 나머지가 숫자형
  • 데이터 양에 비해 Null 값이 많은 피처는 드롭 필요

 

 

 

 

 

 

 

 

 

 

 

 

 

- 회귀 모델 적용 전 타깃 값의 분포도 확인 (정규 분포 여부)

plt.title('Original Sale Price Histogram')
plt.xticks(rotation=45)
sns.histplot(house_df['SalePrice'], kde=True)
plt.show()

 

▶ 데이터 값의 분포가 중심에서 왼쪽으로 치우친 형태로, 정규 분포에서 벗어나 있다.

 

- 정규 분포 형태로 변환하기 위한 로그 변환(log transformation) 적용

  • 넘파이의 log1p()를 이용해 로그 변환한 결괏값을 기반으로 학습
  • 예측 시에는 다시 결괏값은 expm1()으로 추후에 환원
plt.title('Log Transformed Sale Price Histogram')
log_SalePrice = np.log1p(house_df['SalePrice'])
sns.histplot(log_SalePrice, kde=True)
plt.show()

- 로그 변환 + Null 피처의 전처리 수행

  • SalePrice를 로그 변환한 뒤 DataFrame에 반영
  • Null 값이 1000개가 넘는 피처 PoolQC, MiscFeature, Alley, Fence 드롭
  • Id는 단순 식별자 이므로 삭제
  • LotFrontage는 평균값으로 대체
  • 나머지 Null 피처는 Null 값이 많지 않으므로 숫자형의 경우 평균값으로 대체  ▶ DataFrame 객체의 mean() 메서드는 자동으로 숫자형 칼럼만 추출해 칼럼별 평균값을 Series 객체로 반환
# SalePrice 로그 변환
original_SalePrice = house_df['SalePrice']
house_df['SalePrice'] = np.log1p(house_df['SalePrice'])

# Null 전처리
house_df.drop(['Id', 'PoolQC', 'MiscFeature', 'Alley', 'Fence', 'FireplaceQu'], axis=1, inplace=True)
# 드롭하지 않는 숫자형 Null 칼럼은 평균값으로 대체
# 숫자형 칼럼만 선택하여 Null 값 대체
house_df.fillna(house_df.select_dtypes(include=['number']).mean(), inplace=True)

# Null 값이 있는 피처명과 타입 추출
null_column_count = house_df.isnull().sum()[house_df.isnull().sum() > 0]
print('## Null 피처의 Type: \n', house_df.dtypes[null_column_count.index])

▶ 문자형 피처를 제외하고는 Null 값이 없다.

 

- 문자형 피처 모두 원-핫 인코딩을 변환 

판다스의 get_dummies() 를 이용하여 원-핫 인코딩을 이용하는데, 원-핫 인코딩을 적용하면 칼럼 수가 증가한다.

# 문자형 피처들을 모두 One Hot Encoding
print('get_dummies() 수행 전 데이터 Shape:', house_df.shape)
house_df_ohe = pd.get_dummies(house_df)
print('get_dummies() 수행 후 데이터 Shape:', house_df_ohe.shape)

null_column_count = house_df_ohe.isnull().sum()[house_df_ohe.isnull().sum() > 0]
print('## Null 피처의 Type:\n', house_df_ohe.dtypes[null_column_count.index])


선형 회귀 모델 학습/예측/평가

 

- 데이터 세트의 기본적인 가공을 마치고 회귀 모델을 생성해 학습한 후 에측 결과 평가

  • 데이터 세트를 학습과 테스트 데이터 세트로 분할해 사이킷런의 LinearRegression, Ridge, Lasso 를 이용해 선형 계열의 회귀 모델을 만든다.
  • 여러 모델의 로그 변환된 RMSE를 측정할 것이므로 이를 계산하는 함수 get_rmse() 를 먼저 생성한다.
  • get_rmses(models)는 get_rmse()를 이용해 여러 모델의 RMSE 값을 반환한다
def get_rmse(model):
    pred = model.predict(X_test)
    mse = mean_squared_error(y_test, pred)
    rmse = np.sqrt(mse)
    print(model.__class__.__name__, '로그 변환된 RMSE:', np.round(rmse, 3))
    return rmse

def get_rmses(models):
    rmses = []
    for model in models:
        rmse = get_rmse(model)
        rmses.append(rmse)
    return rmses

 

from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error

y_target = house_df_ohe['SalePrice']
X_features = house_df_ohe.drop('SalePrice', axis=1, inplace=False)
X_train, X_test, y_train, y_test = train_test_split(X_features, y_target, test_size=0.2, random_state=156)

# LinearRegression, Ridge, Lasso 학습, 예측 평가
lr_reg = LinearRegression()
lr_reg.fit(X_train, y_train)
ridge_reg = Ridge()
ridge_reg.fit(X_train, y_train)
lasso_reg = Lasso()
lasso_reg.fit(X_train, y_train)

models = [lr_reg, ridge_reg, lasso_reg]
get_rmses(models)

▶ 라쏘의 경우 회귀 성능이 타 방식보다 많이 떨어지는 결과가 나왔기 때문에 최적 하이퍼 파리미터 튜닝이 필요해 보인다.

 

- 피처별 회귀 계수 시각화

  • 모델별로 어떠한 피처의 회귀 계수로 구성되는지 확인하기 위해 피처별 회귀 계수를 시각화한다.
  • 회귀 계수 값의 상위 10개, 하위 10개의 피처명과 그 회귀 계수 값을 가지는 판다스 Series 객체를 반환하는 get_top_bottom_coef(model, n=10) 함수 생성
  • 시각화를 위한 함수로 visualize_coefficient(models) 생성: list 객체로 모델을 입력받아 모델별로 회귀 계수 상위, 하위 각 10개 씩 추출해 가로 막대 그래프 형태로 출력
  • fig.tight_layout(): tight_layout을 이용하여 subplot들이 겹치지 않도록 최소한의 여백을 만들어주는 역할
def get_top_bottom_coef(model, n=10):
    # coef_ 속성을 기반으로 Series 객체 생성. index는 칼럼명
    coef = pd.Series(model.coef_, index=X_features.columns)

    # 상위, 하위 각 10개 씩 회귀 계수 추출해 반환
    coef_high = coef.sort_values(ascending=False).head(n)
    coef_low = coef.sort_values(ascending=False).tail(n)
    return coef_high, coef_low

def visualize_coefficient(models):
    # 3개 회귀 모델의 시각화를 위해 3개의 칼럼을 가지는 subplot 생성
    fig, axs = plt.subplots(figsize=(24, 10), nrows=1, ncols=3)
    fig.tight_layout() # subplot 겹치지 않도록 설정
    # 입력 인자로 받은 list 객체인 models 에서 차례로 model을 추출해 회귀 계수 시각화
    for i_num, model in enumerate(models):
        # 상위 10개, 하위 10개 회귀 계수 구하고, 판다스 concat으로 연결
        coef_high, coef_low = get_top_bottom_coef(model)
        coef_concat = pd.concat([coef_high, coef_low])
        # ax subplot에 barchar 로 표현. 한 화면에 표현하기 위해 tick label 위치와 font 크기 조정
        axs[i_num].set_title(model.__class__.__name__+' Coeffiecients', size=25)
        axs[i_num].tick_params(axis='y', direction='in', pad=-120)
        for label in (axs[i_num].get_xticklabels() + axs[i_num].get_yticklabels()):
            label.set_fontsize(22)
        sns.barplot(x=coef_concat.values, y=coef_concat.index, ax=axs[i_num])

# 앞 예제에서 학습한 lr_reg, ridge_reg, lasso_reg 모델의 회귀 계수 시각화
models = [lr_reg, ridge_reg, lasso_reg]
visualize_coefficient(models)

▶ LinearRegression과 Ridge의 경우는 회귀 계수가 유사한 형태로 분포돼 있으나, Lasso 는 전체적으로 회귀 계수 값이 작다. 

 

- Cross_val_score()를 이용하여 교차 검증 폴드 세트로 분할한 뒤 평균 RMSE 측정

학습/테스트 데이터 세트를 분할하지 않고 전체 데이터 세트를 5개의 교차 검증 폴드 세트로 분할해 평균 RMSE 측정

from sklearn.model_selection import cross_val_score

def get_avg_rmse_cv(models):

    for model in models:
        # 분할하지 않고 전체 데이터로 cross_val_score() 수행. 모델별 CV RMSE 값과 평균 RMSE 출력
        rmse_list = np.sqrt(-cross_val_score(model, X_features, y_target,
                                             scoring="neg_mean_squared_error", cv=5))
        rmse_avg = np.mean(rmse_list)
        print('\n{0} CV RMSE 값 리스트: {1}'.format(model.__class__.__name__, np.round(rmse_list, 3)))
        print('{0} CV 평균 RMSE 값: {1}'.format(model.__class__.__name__, np.round(rmse_avg, 3)))

# 앞 예제에서 학습한  ridge_reg, lasso_reg 모델의 CV RMSE 값 출력
models = [ridge_reg, lasso_reg]
get_avg_rmse_cv(models)

▶ 5개의 폴드 세트로 학습한 후 평가해도 여전히 Lasso의 성능이 떨어진다.

 

- 릿지와 라쏘 모델에 대해 alpha 하이퍼 파라미터를 변화시키면서 최적 값 도출

print_best_params(model, params) 함수를 이용해 릿지 모델과 라쏘 모델의 최적화 alpha 값을 추출.

이 함수는 모델과 하이퍼 파라미터 딕셔너리 객체를 받아 최적화 작업의 결과를 표시하는 함수이다.

from sklearn.model_selection import GridSearchCV

def print_best_params(model, params):
    grid_model = GridSearchCV(model, param_grid=params,
                              scoring="neg_mean_squared_error", cv=5)
    grid_model.fit(X_features, y_target)
    rmse = np.sqrt(-1 * grid_model.best_score_)
    print(' {0} 5 CV 시 최적 평균 RMSE 값:{1}, 최적 alpha:{2}'.format(model.__class__.__name__,
                                                               np.round(rmse, 4), grid_model.best_params_))

ridge_params = { 'alpha': [0.05, 0,1, 1, 5, 8, 10, 12, 15, 20]}
lasso_params = { 'alpha': [0.001, 0.005, 0.008, 0.05, 0.03, 0.1, 0.5, 1, 5, 10]}
print_best_params(ridge_reg, ridge_params)
print_best_params(lasso_reg, lasso_params)

 

- 선형 모델에 최적 alpha 값을 설정한 뒤, 분할된 학습/테스트 데이터를 이용해 모델의 학습/예측/평가를 수행하고, 모델별 회귀 계수 시각화

상단의 최적화 alpha 값으로 학습 데이터로 학습하고, 테스트 데이터로 예측 및 평가 수행

lr_reg = LinearRegression()
lr_reg.fit(X_train, y_train)
ridge_reg = Ridge(alpha=12)
ridge_reg.fit(X_train, y_train)
lasso_reg = Lasso(alpha=0.001)
lasso_reg.fit(X_train, y_train)

# 모든 모델의 RMSE 출력
models = [lr_reg, ridge_reg, lasso_reg]
get_rmses(models)

# 모든 모델의 회귀 계수 시각화
models = [lr_reg, ridge_reg, lasso_reg]
visualize_coefficient(models)

▶ alpha 값 최적화 후 테스트 데이터 세트의 예측 성능이 더 좋아졌다.


데이터 세트 가공을 통한 모델 튜닝 1 - 데이터 분포도 파악

 

- 피처 데이터 세트의 데이터 분포도를 살펴본다.

  • 피처 데이터 세트의 경우 지나치게 왜곡된 피처가 존재할 경우 회귀 예측 성능을 저하시킬 수 있다.
  • 모든 숫자형 피처의 데이터 분포도를 확인해 분포도 왜곡정도 파악
  • 사이파이 stats 모듈의 skew() 함수를 이용해 칼럼의 데이터 세트의 왜곡된 정도를 쉽게 추출
  • 일반적으로 skew() 함수의 반환 값이 1 이상인 경우 왜곡 정도가 높다고 판단 (상황에 따라 편차 있음). 따라서 1 이상의 값을 반환하는 피처만 추출해 왜곡 정도를 완화하기 위해 로그 변환 적용
  • 숫자형 피처의 칼럼 index 객체를 추출해 구한 숫자형 칼럼 데이터 세트의 apply lambda 식 skew() 호출해 숫자형 피처의 왜곡 정도 구함.

※ 숫자형 피처에서 원-핫 인코딩된 카테고리 숫자형 피처 제외 필요하기 때문에 인코딩이 적용된 house_df_ohe가 아닌 인코딩이 적용되지 않은 house_df 로 skew() 함수 적용.

from scipy.stats import skew

# object 가 아닌 숫자형 피처의 칼럼 index 추출
features_index = house_df.dtypes[house_df.dtypes != 'object'].index
# house_df에 칼럼 index를 []로 입력하면 해당하는 칼럼 데이터 세트 반환
skew_features = house_df[features_index].apply(lambda x: skew(x))
# skew(왜곡) 정도가 1 이상인 칼럼만 추출
skew_features_top = skew_features[skew_features > 1]
print(skew_features_top.sort_values(ascending=False))

# 추출된 왜곡 정도가 높은 피처를 로그 변환
house_df[skew_features_top.index] = np.log1p(house_df[skew_features_top.index])

 

house_df의 피처를 일부 로그 변환했으므로 다시 원-핫 인코딩을 적용한 house_df_ohe 만들고, 이에 기반한 피처 데이터 세트와 타깃 데이터 세트, 학습/테스트 데이터 세트를 모두 다시 만든다.

그리고 이 데이터 세트들을 앞에서 만든 print_best_params() 함수를 이용해 최적 alpha 값과 RMSE 를 출력한다.

# 왜곡 정도가 높은 피처를 로그 변환했으므로 다시 원-핫 인코딩을 적용하고 피처/타깃 데이터 세트 생성
house_df_ohe = pd.get_dummies(house_df)
y_target = house_df_ohe['SalePrice']
X_features = house_df_ohe.drop('SalePrice', axis=1, inplace=False)
X_train, X_test, y_train, y_test = train_test_split(X_features, y_target, test_size=0.2, random_state=156)

# 피처를 로그 변환 후 다시 최적 하이퍼 파리미터와 RMSE 출력
ridge_params = { 'alpha': [0.05, 0,1, 1, 5, 8, 10, 12, 15, 20]}
lasso_params = { 'alpha': [0.001, 0.005, 0.008, 0.05, 0.03, 0.1, 0.5, 1, 5, 10]}
print_best_params(ridge_reg, ridge_params)
print_best_params(lasso_reg, lasso_params)

▶ 두 모델 모두 피처의 로그 변환 이전과 비교해 5폴드 교차 검증의 평균 RMSE 값이 향상되었다.

 

선형 모델에 최적 alpha 값을 설정한 뒤, 분할된 학습/테스트 데이터를 이용해 모델의 학습/예측/평가를 수행하고, 모델별 회귀 계수 시각화 재수행

lr_reg = LinearRegression()
lr_reg.fit(X_train, y_train)
ridge_reg = Ridge(alpha=10)
ridge_reg.fit(X_train, y_train)
lasso_reg = Lasso(alpha=0.001)
lasso_reg.fit(X_train, y_train)

# 모든 모델의 RMSE 출력
models = [lr_reg, ridge_reg, lasso_reg]
get_rmses(models)

# 모든 모델의 회귀 계수 시각화
models = [lr_reg, ridge_reg, lasso_reg]
visualize_coefficient(models)

▶ 시각화 결과를 보면 세 모델 모두 GrLivArea(주거 공간 크기)가 회귀 계수가 가장 높은 피처가 됐다.

이는 주거 공간의 크기가 주택 가격에 미치는 영향이 제일 높을 것이라는 결과가 도출된 것이다.


데이터 세트 가공을 통한 모델 튜닝 2 - 이상치 데이터 처리

 

회귀 계수가 높은 피처, 즉 예측에 많은 영향을 미치는 중요 피처의 이상치 데이터 처리가 중요하다.

먼저 가장 큰 회귀 계수를 가지는 GrLivArea(주거 공간 크기) 피처의 데이터 분포를 살펴본다.

 

- 원본 데이터 세트인 house_df_org 에서 GrLivArea 와 타깃 값인 SalePrice의 관계 시각화

plt.scatter(x = house_df_org['GrLivArea'], y = house_df_org['SalePrice'])
plt.ylabel('SalePrice', fontsize=15)
plt.xlabel('GrLivArea', fontsize=15)
plt.show()

▶ 주거 공간이 큰 집일수록 가격이 비싸기 때문에 두 피처는 양의 상관도가 매우 높음을 알 수 있다.

그러나, 빨간색 네모로 표시된 2개의 데이터는 공간이 넓은데도 불구하고 가격이 낮다.

따라서, GrLivArea는 4,000 평방미터 이상임에도 가격이 500,000달러 이하인 데이터는 모두 이상치로 간주하고 삭제한다.

 

- 이상치 데이터 삭제

  • 데이터 변환이 모두 완료된 house_df_ohe 에서 대상 데이터를 필터링
  • GrLivArea, SalePrice 모두 로그 변환 됐으므로 이를 반영한 조건을 생성한 뒤, 불린 인덱싱으로 대상을 찾음
  • 찾은 데이터의 DataFrame 인덱스와 drop()을 이용해 해당 데이터 삭제
# GrLivArea와 SalePrice 모두 로그 변환됐으므로 이를 반영한 조건 생성
cond1 = house_df_ohe['GrLivArea'] > np.log1p(4000)
cond2 = house_df_ohe['SalePrice'] < np.log1p(500000)
outlier_index = house_df_ohe[cond1 & cond2].index

print('이상치 레코드 index: ', outlier_index.values)
print('이상치 삭제 전 house_df_ohe shape:', house_df_ohe.shape)

# DataFrame의 인덱스를 이용해 이상치 레코드 삭제
house_df_ohe.drop(outlier_index, axis=0, inplace=True)
print('이상치 삭제 후 house_df_ohe shape:', house_df_ohe.shape)

▶ 두 개의 이상치 데이터를 찾아 삭제하였다.

 

- 업데이트 된 house_df_ohe를 기반으로 피처 데이터 세트와 타깃 데이터 세트를 다시 생성하고 print_best_params() 함수를 이용해 릿지와 라쏘 모델의 최적화를 수행하고 결과 출력

y_target = house_df_ohe['SalePrice']
X_features = house_df_ohe.drop('SalePrice', axis=1, inplace=False)
X_train, X_test, y_train, y_test = train_test_split(X_features, y_target, test_size=0.2, random_state=156)

ridge_params = { 'alpha': [0.05, 0,1, 1, 5, 8, 10, 12, 15, 20]}
lasso_params = { 'alpha': [0.001, 0.005, 0.008, 0.05, 0.03, 0.1, 0.5, 1, 5, 10]}
print_best_params(ridge_reg, ridge_params)
print_best_params(lasso_reg, lasso_params)

▶ 두 개의 이상치 데이터를 찾아 삭제하였는데, 예측 수치가 매우 크게 향상 되었다.

 

- 이상치가 제거된 데이터 세트를 기반으로 분할된 데이터 세트의 RMSE 수치 및 회귀 계수 시각화

lr_reg = LinearRegression()
lr_reg.fit(X_train, y_train)
ridge_reg = Ridge(alpha=10)
ridge_reg.fit(X_train, y_train)
lasso_reg = Lasso(alpha=0.001)
lasso_reg.fit(X_train, y_train)

# 모든 모델의 RMSE 출력
models = [lr_reg, ridge_reg, lasso_reg]
get_rmses(models)

# 모든 모델의 회귀 계수 시각화
models = [lr_reg, ridge_reg, lasso_reg]
visualize_coefficient(models)

 


회귀 트리 모델 학습/예측/평가

 

- 회귀 트리 모델을 이용해 회귀 모델 생성

  • XGBoost: XGBRegressor 클래스
  • LightGBM: LGBMRegressor 클래스

둘 모두 수행 시간이 오래 걸릴 수 있는 관계로 하이퍼 파라미터 설정을 미리 적용한 상태로 5 폴드 세트에 대한 평균 RMSE 값 계산

from xgboost import XGBRegressor

xgb_params = {'n_estimators':[1000]}
xgb_reg = XGBRegressor(n_estimators=1000, learning_rate=0.05, colsample_bytree=0.5, subsample=0.8)
print_best_params(xgb_reg, xgb_params)

from lightgbm import LGBMRegressor

lgbm_params = {'n_estimators':[1000]}
lgbm_reg = LGBMRegressor(n_estimators=1000, learning_rate=0.05, num_leaves=4,
                         colsample_bytree=0.4, subsample=0.6, reg_lambda=10, n_jobs=-1)
print_best_params(lgbm_reg, lgbm_params)

 

- 트리 모델의 피처 중요도 시각화

XGBRegressor와 LGBMRegressor의 피처 중요도를 상위 20개만 추출하여 시각화

xgb_reg = XGBRegressor()
xgb_reg.fit(X_train, y_train)
lgbm_reg = LGBMRegressor()
lgbm_reg.fit(X_train, y_train)

# XGBRegressor 및 LGBMRegressor 모델 학습
xgb_reg = XGBRegressor()
xgb_reg.fit(X_train, y_train)

lgbm_reg = LGBMRegressor()
lgbm_reg.fit(X_train, y_train)

# 피처 중요도를 데이터프레임으로 변환
xgb_importance = pd.DataFrame({
    'Feature': X_train.columns,
    'Importance': xgb_reg.feature_importances_
}).sort_values(by='Importance', ascending=False)

lgbm_importance = pd.DataFrame({
    'Feature': X_train.columns,
    'Importance': lgbm_reg.feature_importances_
}).sort_values(by='Importance', ascending=False)

# 상위 20개 추출
xgb_top = xgb_importance.nlargest(20, 'Importance')
lgbm_top = lgbm_importance.nlargest(20, 'Importance')

# 서브플롯 생성
fig, axes = plt.subplots(1, 2, figsize=(14, 7))

# XGBRegressor 피처 중요도 시각화 (상위 20개)
sns.barplot(x='Importance', y='Feature', data=xgb_top, ax=axes[0])
axes[0].set_title('XGBRegressor Feature Importance (Top 20)')

# LGBMRegressor 피처 중요도 시각화 (상위 20개)
sns.barplot(x='Importance', y='Feature', data=lgbm_top, ax=axes[1])
axes[1].set_title('LGBMRegressor Feature Importance (Top 20)')

# 그래프 레이아웃 조정 및 표시
plt.tight_layout()
plt.show()


회귀 모델의 예측 결과 혼합을 통한 최종 예측

 

- 개별 회귀 모델의 예측 결괏값을 혼합해 이를 기반으로 최종 회귀 값 예측

최종 혼합 모델, 개별 모델의 RMSE 값을 출력하는 get_rmse_pred() 함수를 생성하고 각 모델의 예측값을 계산한 뒤 개별 모델과 최종 혼합 모델의 RMSE 계산

 

1) 앞에서 구한 릿지 모델과 라쏘 모델을 서로 혼합하되, 가중치는 릿지에 0.4, 라쏘에 0.6을 곱한 뒤 더함. (가중치를 정하는 특별한 기준은 없다)

def get_rmse_pred(preds):
    for key in preds.keys():
        pred_value = preds[key]
        mse = mean_squared_error(y_test, pred_value)
        rmse = np.sqrt(mse)
        print('{0} 모델의 RMSE: {1}'.format(key, rmse))

# 개별 모델의 학습
ridge_reg = Ridge(alpha=8)
ridge_reg.fit(X_train, y_train)
lasso_reg = Lasso(alpha=0.001)
lasso_reg.fit(X_train, y_train)

# 개별 모델 예측
ridge_pred = ridge_reg.predict(X_test)
lasso_pred = lasso_reg.predict(X_test)

# 개별 모델 예측값 혼합으로 최종 예측값 도출
pred = 0.4 * ridge_pred + 0.6 * lasso_pred
preds = {'최종 혼합': pred,
         'Ridge': ridge_pred,
         'Lasso': lasso_pred}

# 최종 혼합 모델, 개별 모델의 RMSE 값
get_rmse_pred(preds)

 

2) XGBoost와 LightGBM 혼합

xgb_reg = XGBRegressor(n_estimators=1000, learning_rate=0.05,
                       colsample_bytree=0.5, subsample=0.8)
lgbm_reg = LGBMRegressor(n_estimators=1000, learning_rate=0.05, num_leaves=4,
                         colsample_bytree=0.4, subsample=0.6, reg_lambda=10, n_jobs=-1)
xgb_reg.fit(X_train, y_train)
lgbm_reg.fit(X_train, y_train)
xgb_pred = xgb_reg.predict(X_test)
lgbm_pred = lgbm_reg.predict(X_test)
pred = 0.5 * xgb_pred + 0.5 * lgbm_pred
preds = {'최종 혼합': pred,
         'XGB': xgb_pred,
         'LGBM': lgbm_pred}

get_rmse_pred(preds)


스태킹 앙상블 모델을 통한 회귀 예측

 

스태킹 모델은 두 종류의 모델이 필요하다

  • 개별적인 기반 모델
  • 최종 메타 모델: 개별 기반 모델의 예측 데이터를 학습 데이터로 만들어서 학습

스태킹 모델의 핵심은 여러 개별 모델의 예측 데이터를 각각 스태킹 형태로 결합해 최종 메타 모델의 학습용 피처 데이터 세트와 테스트용 피처 데이터 세트를 만드는 것이다.

 

 

[파이썬 머신러닝] 스태킹 앙상블

시작에 앞서해당 내용은 ' 권철민 지음. 위키북스' 를 참고하여 작성되었습니다. 보다 자세한 내용은 해당 서적에 상세히 나와있으니 서적을 참고해 주시기 바랍니다.스태킹 모델 스태킹(Stacking

puppy-foot-it.tistory.com

 

- get_stacking_base_datasets() 함수를 모델별로 적용해 메타 모델이 사용할 학습 피처 데이터 세트와 테스트 피처 데이터 세트 추출

적용 모델: 릿지, 라쏘, XGBoost, LightGBM

from sklearn.model_selection import KFold
from sklearn.metrics import mean_absolute_error

# 개별 기반 모델에서 최종 메타 모델이 사용할 학습 및 테스트용 데이터 생성 위한 함수
def get_stacking_base_datasets(model, X_train_n, y_train_n, X_test_n, n_folds):
    # 지정된 n_folds 값으로 KFold 생성
    kf = KFold(n_splits=n_folds, shuffle=False)
    # 추추 메타 모델이 사용할 학습 데이터 반환을 위한 넘파이 배열 초기화
    train_fold_pred = np.zeros((X_train_n.shape[0], 1))
    test_pred = np.zeros((X_test_n.shape[0], n_folds))
    print(model.__class__.__name__, 'model 시작')

    for folder_counter, (train_index, valid_index) in enumerate(kf.split(X_train_n)):
        # 입력된 학습 데이터에서 기반 모델이 학습/예측할 폴드 데이터 세트 추출
        print('\t 폴드 세트: ', folder_counter, '시작')
        X_tr = X_train_n[train_index]
        y_tr = y_train_n[train_index]
        X_te = X_train_n[valid_index]

        # 폴드 세트 내부에서 다시 만들어진 학습 데이터로 기반 모델의 학습 수행
        model.fit(X_tr, y_tr)
        # 폴드 세트 내부에서 다시 만들어진 검증 데이터로 기반 모델 예측 후 데이터 저장
        train_fold_pred[valid_index, :] = model.predict(X_te).reshape(-1, 1)
        # 입력된 원본 테스트 데이터를 폴드 세트 내 학습된 기반 모델에서 예측 후 데이터 저장
        test_pred[:, folder_counter] = model.predict(X_test_n)

    # 폴드 세투 내에서 원본 테스트 데이터를 예측한 데이터를 평균하여 테스트 데이터로 생성
    test_pred_mean = np.mean(test_pred, axis=1).reshape(-1, 1)

    # train_fold_pred는 최종 메타 모델이 사용하는 학습 데이터, test_pred_mean은 테스트 데이터
    return train_fold_pred, test_pred_mean

# get_stacking_base_datasets()는 넘파이 ndarray를 인자로 사용하므로 DataFrame을 넘파이로 변환
X_train_n = X_train.values
X_test_n = X_test.values
y_train_n = y_train.values

# 각 개별 기반(Base) 모델이 생성한 학습용/테스트용 데이터 반환
ridge_train, ridge_test = get_stacking_base_datasets(ridge_reg, X_train_n, y_train_n, X_test_n, 5)
lasso_train, lasso_test = get_stacking_base_datasets(lasso_reg, X_train_n, y_train_n, X_test_n, 5)
xgb_train, xgb_test = get_stacking_base_datasets(xgb_reg, X_train_n, y_train_n, X_test_n, 5)
lgbm_train, lgbm_test = get_stacking_base_datasets(lgbm_reg, X_train_n, y_train_n, X_test_n, 5)

 

- 학습용 피처 데이터와 테스트용 피처 데이터 세트를 결합해 최종 메타 모델에 적용

메타 모델은 별도의 라쏘 모델을 이용하며, 최종적으로 예측 및 RMSE 측정

# 개별 모델이 반환한 학습 및 테스트용 데이터 세트를 스태킹 형태로 결합
Stack_final_X_train = np.concatenate((ridge_train, lasso_train, xgb_train, lgbm_train), axis=1)
Stack_final_X_test = np.concatenate((ridge_test, lasso_test, xgb_test, lgbm_test), axis=1)

# 최종 메타 모델은 라쏘 모델 적용
meta_model_lasso = Lasso(alpha=0.005)

# 개별 모델 예측값을 기반으로 새롭게 만들어진 학습/테스트 데이터로 메타 모델 예측 및 RMSE 측정
meta_model_lasso.fit(Stack_final_X_train, y_train)
final = meta_model_lasso.predict(Stack_final_X_test)
mse = mean_squared_error(y_test, final)
rmse = np.sqrt(mse)
print('스태킹 회귀 모델의 최종 RMSE 값은:', rmse)

최종적으로 스태킹 회귀 모델을 적용한 결과, 테스트 데이터 세트에서 RMSE 값이 0.0979로 현재까지 가장 좋은 성능 평가를 보여준다.

스태킹 모델은 분류 뿐만 아니라 회귀에서 특히 효과적으로 사용될 수 있는 모델이다.


다음 내용

 

 


[출처]

파이썬 머신러닝 완벽 가이드

728x90
반응형