본문 바로가기
머신러닝

머신러닝 - 스태킹 앙상블과 그 변형에 대해

by Tiabet 2024. 1. 7.

이번엔 스태킹 앙상블을 적용하는 방법에 대해 살펴보고자 한다.

 

스태킹 앙상블은 메인스트림이 된 배깅, 부스팅과 같은 앙상블 모델하고는 느낌이 조금 다르다. 같이 앙상블이라고 불리긴 하지만, 약간 이런 느낌?

수제작.

보팅이야 다수결의 원리라는 워낙 간단명료한 기법이라 설명이 필요없고, 스태킹의 개념이 조금 복잡하기도, 다양하기도 한 감이 있다. 그래서 짧게 정리해보고자 한다.

 

참고자료 : https://www.yes24.com/Product/Goods/69752484

 

파이썬 머신러닝 완벽 가이드 - 예스24

자세한 이론 설명과 파이썬 실습을 통해 머신러닝을 완벽하게 배울 수 있다!『파이썬 머신러닝 완벽 가이드』는 이론 위주의 머신러닝 책에서 탈피해 다양한 실전 예제를 직접 구현해 보면서

www.yes24.com

 

스태킹 앙상블

출처 : Medium -  Stacking to Improve Model Performance: A Comprehensive Guide on Ensemble Learning in Python

 

스태킹 앙상블은 모델을 섞는 것은 똑같으나 그 방법이 보팅 앙상블과는 조금 다르다. 보팅 앙상블이 여러 모델의 예측값을 평균 내는 방식으로 섞는다면, 스태킹은 차곡차곡 쌓아서 하나의 새로운 데이터셋을 만든다. 이 새로운 데이터셋의 하나의 Train Dataset이 되어, 궁극적으로 결과를 추론할 메타 모델에 사용된다. 메타 모델은 새로운 데이터셋을 갖고 학습, 추론을 진행하여 최종 결과를 얻어낸다.

 

참고한 책에 코드가 있긴 한데, ChatGPT로 새롭게 코드를 생성해봤다.

 

from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
from sklearn.datasets import make_classification

# Generate a sample dataset - 예시로 사용할 데이터셋이다.
X, y = make_classification(n_samples=1000, n_features=20, n_informative=10, n_classes=2, random_state=42)

# Split the data into training and testing sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Base models - 베이스 모델로는 랜덤포레스트와 그래디언트부스팅을 사용했다.
rf_model = RandomForestClassifier(n_estimators=100, random_state=42)
gb_model = GradientBoostingClassifier(n_estimators=100, random_state=42)

# Train base models - train dataset으로 베이스 모델들을 학습한 뒤
rf_model.fit(X_train, y_train)
gb_model.fit(X_train, y_train)

# Make predictions using base models - 예측을 진행한다.
rf_pred = rf_model.predict(X_test)
gb_pred = gb_model.predict(X_test)

# Create a new feature matrix with the predictions of the base models
# 예측 결과들을 stacking하여 새로운 데이터셋을 만든다. 이건 메타모델의 Train Dataset이 된다.
stacked_features_train = []
stacked_features_train.append(rf_pred)
stacked_features_train.append(gb_pred)
stacked_features_train = np.vstack(stacked_features_train).T

# Meta-model (Logistic Regression in this case) - 메타모델을 생성하고
meta_model = LogisticRegression()

# Train the meta-model using the predictions of the base models
# 생성했던 Train Dataset으로 학습을 진행한다.
meta_model.fit(stacked_features_train, y_test)

# Make predictions on the test set using the base models
# Test Dataset도 만들어준다.
rf_pred_test = rf_model.predict(X_test)
gb_pred_test = gb_model.predict(X_test)

# Create a new feature matrix with the test set predictions of the base models
stacked_features_test = []
stacked_features_test.append(rf_pred_test)
stacked_features_test.append(gb_pred_test)
stacked_features_test = np.vstack(stacked_features_test).T

# Make predictions using the meta-model - 메타모델로 예측을 진행한다.
ensemble_pred = meta_model.predict(stacked_features_test)

# Evaluate the accuracy of the ensemble model
ensemble_accuracy = accuracy_score(y_test, ensemble_pred)
print(f"Ensemble Accuracy: {ensemble_accuracy}")

 

그런데 이런 스태킹 앙상블에 문제가 하나 있다. 위 코드에서 stacked_features_train과 stacked_features_test는 사실 같은 데이터셋이다. 즉 메타모델은 배운 데이터셋 그대로 예측을 진행하는 것이다. validation set을 중간에 하나 껴서 이런 과적합 문제를 해결하려고 시도할 수 있지만, 널리 알려진 방식은 CV 기반의 스태킹 앙상블이다.

 

CV 기반의 스태킹 앙상블

CV기반의 스태킹 앙상블은 개념은 어렵지 않은데 설명이 너무 길어질 것 같아서 간략하게 끝내려고 한다. 스태킹 앙상블이 과적합이 발생하는 이유는 메타 모델이 학습할 때 원본 Test Dataset을 참고하면서 추론도 Test Dataset에 대해 진행하기 때문이다. 이를 해결하기 위해 CV 기반의 스태킹 앙상블은 학습과 예측을 위한 Dataset을 따로 생성한다. 여기서 사용되는 것이 Cross Validation의 개념이다. 

 

원래 스태킹 앙상블은 원본 Train Dataset으로 Model Fitting, Fit된 모델로 Test Dataset에 대해 예측하여 메타 모델의 Train Dataset 생성을 따른다. CV 기반의 스태킹 앙상블은 TrainDataset을 N개의 폴드로 분리, N-1개의 폴드로 Model Fitting, Fit된 모델로 나머지 1개의 폴드에 예측하여 메타모델의 Train Dataset 생성을 진행한다. 그리고 Test Dataset에도 추론을 한 번씩 진행한다. 이를 N개의 폴드에 대해 반복하면 1개의 베이스모델에 대해 Train Dataset이 완성되고, N개의 Test Dataset 추론값이 생긴다. Test Dataset은 N개를 평균내어 하나로 합쳐주는 것이 일반적이다.

 

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

책에 소개된 코드는 위와 같다. 원본 TrainDataset을 K-Fold로 분할, 모델 N개에 대해 만들어진 TrainDataset을 하나로 합쳐주는 것이 핵심이다.

 

출처 : https://rasbt.github.io/mlxtend/user_guide/classifier/StackingCVClassifier/

 

추가적인 스태킹 기법

 

마지막으로 언급할 내용은 Nerual Network를 이용하는 스태킹 기법이다. 원래 정형 데이터셋에 대해선 NN 보다 LGBM , XGB, CatBoost 같은 머신러닝 모델들이 강한 면모를 보인다는 것이 정설이다. 그러나 여러 모델을 사용해서 아예 새로운 메타모델의 Train Dataset을 만드는 기존의 스태킹과는 달리, LGBM으로 예측한 값을 원본 Train Dataset에 이어 붙여서 NN을 학습시키면 NN의 성능이 더 뛰어나진다는 말이 있다. 

 

# Example code for base model (LGBM)
import lightgbm as lgb
from sklearn.model_selection import KFold

# Assuming X_train, y_train are your training features and labels
kf = KFold(n_splits=5, random_state=42, shuffle=True)


kf = KFold(n_splits=5, random_state=42, shuffle=True)
lgbm_predictions = pd.Series(index=X_train.index)  # Create a Series to store predictions with original indices

for train_index, val_index in kf.split(X_train):
    X_train_fold, X_val_fold = X_train.iloc[train_index], X_train.iloc[val_index]
    y_train_fold, y_val_fold = y_train.iloc[train_index], y_train.iloc[val_index]

    lgbm_model = lgb.LGBMRegressor()
    lgbm_model.fit(X_train_fold, y_train_fold)

    lgbm_fold_predictions = lgbm_model.predict(X_val_fold)
    lgbm_predictions.iloc[val_index] = lgbm_fold_predictions

# Example code for final model (Neural Network using TensorFlow/Keras)
X_train_nn = pd.concat([X_train, lgbm_predictions], axis=1)

# Example code for final model (Neural Network using TensorFlow/Keras)
from tensorflow import keras
from tensorflow.keras import layers

# Define your neural network architecture
model = keras.Sequential([
    layers.Dense(64, activation='relu', input_dim=X_train_nn.shape[1]),
    layers.Dense(1, activation='linear')
])

# Compile the model
model.compile(optimizer='adam', loss='mean_squared_error')

# Train the model
model.fit(X_train_nn, y_train, epochs=10, batch_size=32, validation_split=0.2)

 

코드로 구현하면 대략 이정도 되는 것 같다. 이때 과적합을 방지하기 위해 CV 기반 스태킹 앙상블에서 사용했던 것과 비슷하게 Out-of-Fold Prediction을 진행한다.

 

실제로 어느 정도 효과가 뛰어난지는 모르겠는데, 캐글에서 이런 방식으로 문제를 풀어나가는 사람들이 종종 있다. 입상할 정도로 성능이 우수한지는 확인이 안 된다.