Ensemble Techniques: Combining Models for Superior Predictive Power
Ensemble Learning: From Boosting to Bagging - A Comprehensive Guide for Improved Predictive Models.
Machine learning is an iterative process, and whether it's adding new features to your data, filling missing values with different techniques, adding new data points, monitoring your predictions, or hyper-parameter tuning; there is this constant search for improvement to machine learning models.
This article like many other machine-learning tutorials, talks about how you can improve your machine-learning model but with a focus on Ensembling machine-learning models, Ensembling can be described as combining different machine-learning models in an attempt to achieve a new model or prediction pattern that combines the strength of the base-models and reduces their flaws. The "in an attempt" used in describing ensembling is very deliberate and will be explained later on.
In this article, I will be discussing different methods of ensembling machine learning models using a machine learning competition I participated in as a case study, and also comparing the results of each ensembling method.
Prerequisite
To understand what will be done in this article, you'll need to have:
Intermediate knowledge of Python.
Basic understanding of Numpy and Pandas.
Basic understanding of simple machine learning algorithms like Linear Regression and Decision Trees
Jupyter Notebook installed in your system or a Google Collaboratory (Google Collab) account.
Dataset used
One of the first problems you would face when trying to build a machine learning model is data. You'll mostly find yourself trying to answer the question "What data should I use to solve XYZ problem?".
For this article, you'll be using data from Kaggle's Regression with a Crab Age Dataset competition. The competition is aimed at predicting the Age of a crab using the physical features of the crab such as weight, height and length of the crab. The evaluation metric from the competition is the Mean Absolute Error. Three CSV files were provided from the competition board; a train (to build your model on), a test (data to predict on) and a sample submission file (showing what your submission should be like). From the problem statement and evaluation metric, this is a regression problem.
Kaggle competitions typically have two scores, a public score ( 20% of the test data) that you can see during the competition and a private score (80% of the test data) that shows after the competition ends, the final leaderboard is calculated using the private scores.
Libraries used
The libraries you'll need to have installed to be able to run the code in this tutorial are: Numpy, Pandas, Scikit-learn, CatBoost, LightGBM and XGBoost, if some of the libraries look strange to you, don't worry at all, you'll see what they do later on.
Data cleaning
Now that you have data, the next step is to clean and prepare the dataset for model building!
Install the necessary libraries.
If you use google collab (that's what I use), all the libraries except Catboost are pre-installed. You can easily install Catboost by running
pip install catboost
.If you use any other text editor or Python notebook. You can install all the libraries by running the below line on your terminal:
pip install numpy pandas sklearn catboost lightboost xgboost
Import the necessary libraries.
For data cleaning, the basic libraries you'll need are Numpy and Pandas.
import numpy as np import pandas as pd
Read the train and test files
train=pd.read_csv("train.csv",index_col=0) test=pd.read_csv("test.csv",index_col=0) print(train.head()) print(test.head())
The first column of the train and test CSV file is the id which according to the competition helps to identify the data points, so the
index_col=0
parameter helps you to set it (the 1st column, with index 0) as the index.Check for missing values
Missing values pose problems when building machine learning models. Most machine learning models can't handle data with missing values and will return an error when you try to build models on such data. Another problem they pose is loss of information. When values are missing, the quality of the data and information that can be gotten by the predictive model reduces.
#check for missing values print(train.isna().sum()) print(test.isna().sum())
Thankfully, the data has no missing which makes things a lot easier and you don't have to worry about filling in missing values.
Separate the target column and drop it from the train dataframe
target=train["Age"] train.drop("Age",axis=1,inplace=True)
Check for categorical columns
Further, most models can't handle non-numerical features and it'd be better if you can spot them and convert them to numerical features to make things easier.
#Check for categorical columns print(train.dtypes) print(test.dtypes)
From the results, you can see that only the Sex column has a non-numerical datatype ("object").
Check the unique values in the Sex column and convert them to numerical values.
#Check the unique values in the Sex column print(train["Sex"].value_counts()) #convert to numerical train["Sex"]=train["Sex"].map({"M":0,"I":1,"F":2}) test["Sex"]=test["Sex"].map({"M":0,"I":1,"F":2})
All the important data cleaning and handling required have been done, and the data has no missing values or non-numerical features.
Note: You can still do a lot more to improve the data. For instance, you can create new features, scale the data and many more. However, since this tutorial is centered around ensembling machine learning models, we will pay less attention to that.
Split data
To build your model, you'll need 3 different DataFrames - a train (to train the model), validation (to check your model's performance) and test (data to predict on) DataFrame. Although the train and test data have been given already, there is no validation data. No worries though. You can easily get the validation data by splitting the train data into two; X (which becomes the new train dataframe) and val (the validation dataframe). The split can be done using sklearn's train_test_split
function.
The target column is also split into y (target for X dataframe ) and y_val (target for val dataframe).
#splitting the train data into [X and val]
from sklearn.model_selection import train_test_split
X,val,y,y_val=train_test_split(train,target,test_size=0.15,random_state=0)
Model Training
To train the model you'll be using 3 different regressors; CatBoostRegressor (from the catboost package), LGBMRegressor (from lightboost/lightgbm package) and XGBRegressor(from xgboost package). These regressors are some of the best performing for tabular data and are based on an ensembling technique called Boosting.
Boosting
Boosting algorithms are machine learning algorithms that start with weak learners (weak models) and iteratively improve the models by correcting mistakes made by the previous models. This iterative process helps the ensemble improve its predictive ability.
The final predictions by boosting algorithms are made by combining the predictions by all the learners using a weighted voting system, weak learners are assigned lower weights while stronger (more accurate) learners are assigned higher weights.
Catboost, lightboost and xgboost are boosting algorithms based on Decision Trees, which means they are made up of multiple decision trees under the hood, although the iterative process and the method they used to improve the weak learners (individual decision trees) are quite different.
Building the models.
You'll build a CatBoostRegressor, LGBMRegressor and XGBRegressor and also create a submission file for each of the models.
CatBoost
Import the regressor
#import the regressor from catboost import CatBoostRegressor
Create the model
#create the model model_catboost=CatBoostRegressor(verbose=0,random_state=0)
Fit the model on the X,y data
#fit the model on the X,y data model_catboost.fit(X,y)
Make predictions on the X,val and test dataframe.
#predictions on the X,val and test dataframe pred_x_catboost=model_catboost.predict(X) pred_val_catboost=model_catboost.predict(val) pred_test_catboost=model_catboost.predict(test)
Check the mean_absolute_errors of the X and validation datasets.
#import mean_absolute_error from sklearn.metrics import mean_absolute_error #check the mean absolute error of the validation and X dataframe print(mean_absolute_error(pred_val_catboost,y_val,)) print(mean_absolute_error(pred_x_catboost,y))
Create a submission file
sub_catboost=pd.DataFrame({"id":test.index,"Age":pred_test_catboost}).set_index("id") sub_catboost.to_csv("submission_catboost.csv")
LightGBM
You just need to repeat what was done on the CatBoostRegressor.
model_lightgbm=LGBMRegressor(random_state=0)
model_lightgbm.fit(X,y)
pred_x_lightgbm=model_lightgbm.predict(X)
pred_val_lightgbm=model_lightgbm.predict(val)
pred_test_lightgbm=model_lightgbm.predict(test)
print(mean_absolute_error(pred_val_lightgbm,y_val,))
print(mean_absolute_error(pred_x_lightgbm,y))
#creating a submision file
sub_lightboost=pd.DataFrame({"id":test.index,"Age":pred_test_lightgbm}).set_index("id")
sub_lightboost.to_csv("submission_lgb.csv")
XGBoost
model_xgboost=XGBRegressor(random_state=0)
model_xgboost.fit(X,y)
pred_x_xgboost=model_xgboost.predict(X)
pred_val_xgboost=model_xgboost.predict(val)
pred_test_xgboost=model_xgboost.predict(test)
print(mean_absolute_error(pred_val_xgboost,y_val))
print(mean_absolute_error(pred_x_xgboost,y))
#creating a submision file
sub_xgboost=pd.DataFrame({"id":test.index,"Age":pred_test_xgboost}).set_index("id")
sub_xgboost.to_csv("submission_xgb.csv")
Voting
Voting is an ensembling technique, it involves taking in different models and creating predictions by making a vote from predictions made by each model. By voting, it means finding the mode of the predictions (for classification problems) or average (for regression problems). The different models could also be given weights, models with higher weights will contribute more to the voting process and are more important.
Voting
For this problem, you can easily implement voting by averaging the three models (catboost regressor, lgbm regressor and xgboost regressor).
mean_predictions=(pred_test_catboost+pred_test_lightgbm+pred_test_xgboost)/3
#create a submission file
sub_mode=pd.DataFrame({"id":test.index,"Age":mean_predictions}).set_index("id")
sub_mode.to_csv("sub_mean.csv")
You can also implement voting by using scikit-learns' VotingRegressor
(regression) and VotingClassifier
(classification).
#import voting regressor
from sklearn.ensemble import VotingRegressor
#sub_models
estimators=[("catboost",model_catboost),
("lightboost",model_lightgbm),
("xgboost",model_xgboost)]
#voting regressor
model_voting=VotingRegressor(estimators=estimators)
model_voting.fit(X,y)
pred_x_voting=model_voting.predict(X)
pred_val_voting=model_voting.predict(val)
pred_test_voting=model_voting.predict(test)
print(mean_absolute_error(pred_val_voting,y_val))
print(mean_absolute_error(pred_x_voting,y))
The VotingRegressor also has a weights
parameter, an array of shape n_regressors
, that weighs the occurrences of predicted values before averaging, if no weights are given, then each model has the same weights and importance.
Create a submission file:
sub_voting=pd.DataFrame({"id":test.index,"Age":pred_test_voting}).set_index("id")
sub_voting.to_csv("submission_voting.csv")
Stacking
Stacking is an ensembling technique that takes in different models (base models) and creates a new model (meta-model) that makes predictions using the output from the base models. Stacking tries to take voting a step further by using a machine learning model (instead of simple averaging done in voting) to learn how best to combine the base models' predictions to improve overall predictive performance.
Stacking tries to leverage the strengths of the base models and compensate for their weaknesses by combining the predictions from each model in a sophisticated manner, aiming to identify more complex patterns in the data.
Simple implementation of stacking
Build a dataframe of the model's predictions
X_predictions_dataframe=pd.DataFrame({"catboost":pred_x_catboost, "lightboost":pred_x_lightgbm, "xgboost":pred_x_xgboost}) test_predictions_dataframe=pd.DataFrame({"catboost":pred_test_catboost, "lightboost":pred_test_lightgbm, "xgboost":pred_test_xgboost}) val_predictions_dataframe=pd.DataFrame({"catboost":pred_val_catboost, "lightboost":pred_val_lightgbm, "xgboost":pred_val_xgboost})
Build the meta model
#import linear regressor from sklearn.linear_model import LinearRegression final_model=LinearRegression() final_model.fit(X_predictions_dataframe,y) pred_x_final=final_model.predict(X_predictions_dataframe) pred_val_final=final_model.predict(val_predictions_dataframe) pred_test_final=final_model.predict(test_predictions_dataframe) print(mean_absolute_error(pred_val_final,y_val)) print(mean_absolute_error(pred_x_final,y))
Create a submission file
sub_final=pd.DataFrame({"id":test.index,"Age":pred_test_final}).set_index("id") sub_final.to_csv("submission_final.csv")
Sklearn's StackingRegressor
Sklearn has a StackingRegressor
class that accepts a list of base estimators and a final estimator (meta-model to use on the base estimators).
from sklearn.ensemble import StackingRegressor
model_stack=StackingRegressor(estimators=estimators,final_estimator=final_model)
model_stack.fit(X,y)
pred_x_stack=model_stack.predict(X)
pred_val_stack=model_stack.predict(val)
pred_test_stack=model_stack.predict(test)
print(mean_absolute_error(pred_val_stack,y_val))
print(mean_absolute_error(pred_x_stack,y))
#creating a submission file
sub_stack=pd.DataFrame({"id":test.index,"Age":pred_test_stack}).set_index("id")
sub_stack.to_csv("submission_stack.csv")
Stacking is prone to overfitting, so the StackingRegressor tries to reduce overfitting by using cross-validation to fit the final estimator on the predictions from the base models.
Bagging
Bagging (full-form Bootstrap Aggregation) is an ensemble method that improves predictive performance by combining models trained on different subsamples of the data and combining them to obtain more rounded results.
When building models, you typically split the train data into two (X and validation), by training the model on X alone, you've lost all the patterns that can be learned from the validation data, with bagging, you can split your data into multiple folds, validate the model on one fold and build the model on the others, do this until you've used all the folds as validations. So, if your data was split into 10 folds, you can build models on 10 subsamples of the data and combine their predictions.
Manual implementation
from sklearn.model_selection import KFold
#cross_validator to splite the data into folds
folds=KFold(n_splits=8,shuffle=True,random_state=0)
#a dataframe to store the predictions made by each fold
predictions_df=pd.DataFrame()
#list to save the mean absolute errors from validatingon each folds
mae_val=[]
mae_X=[]
#a simple catboost regressor
model=CatBoostRegressor(verbose=0,random_state=0)
#train model, make predictions and check the validation accuracy on each fold
for i,(train_index,test_index) in enumerate(folds.split(train,target)):
train_fold=train.iloc[train_index]
val_fold=train.iloc[test_index]
y_fold=target.iloc[train_index]
y_val_fold=target.iloc[test_index]
model.fit(train_fold,y_fold)
print(i)
prediction=model.predict(test)
predictions_df[i]=prediction
mae_val.append(mean_absolute_error(model.predict(val_fold),y_val_fold))
mae_X.append(mean_absolute_error(model.predict(train_fold),y_fold))
print(mae_val)
print(mae_X)
Up next is to make predictions by combining the results from all the folds, this can be done by finding the mean.
predictions=predictions_df.mean(axis=1)
sub_kfold_mean=pd.DataFrame({"id":test.index,"Age":predictions}).set_index("id")
sub_kfold_mean.to_csv("submission_kfold_mean.csv")
SKlearn's BaggingRegressor
from sklearn.ensemble import BaggingRegressor
model_bagging=BaggingRegressor(estimator=CatBoostRegressor(random_state=0,verbose=0),n_estimators=8,max_samples=0.8)
model_bagging.fit(train,target)
pred_test_bagging=model_bagging.predict(test)
sub_bagging=pd.DataFrame({"id":test.index,"Age":pred_test_bagging}).set_index("id")
sub_bagging.to_csv("submission_bagging.csv")
A good question that you may have at this point is what is the need for manual implementation since there is the BaggingRegressor
class? Well, the answer is flexibility, you'd have a lot of flexibility and freedom if you implement it manually, for example, you can choose to use the median (even minimum or maximum) of the predictions instead of the mean if you want to reduce the effect of outliers.
predictions=predictions_df.median(axis=1)
sub_kfold_median=pd.DataFrame({"id":test.index,"Age":predictions}).set_index("id")
sub_kfold_median.to_csv("submission_kfold_median.csv")
Effects of ensembling on the model's performance?
Submitting the submission files to the competition's page gives the following scores.
From the results, the ensembled models generally outperform the individual models (except for the stacking implementation by hand which overfits). The mean of the bagging implementation by hand performs best (in terms of private score).
What is the best ensembling method?
You never really know until you try, the method that gives you the best performance for problem A might not give the best performance for problem B, so it's important to try first, you can use the performance on validation data to get a closer view of how the ensembling method will perform on new data.
In some cases, not all the ensembling approaches improve the performance of individual models, hence the reason for the "in an attempt" used in describing ensembling earlier. Ensembling won't always lead to an increase in performance, although it does a lot of times.
The full source code can be found here.
Is ensembling worth it?
Ensembling generally comes with benefits like improvements in performance, reduction in model bias and robustness to noisy data. In some cases, single well-tuned models can achieve satisfactory performance without the added complexity of an ensemble.
Ensembling also has some downsides like additional resources for training and prediction, an increase in computation cost, and a reduction in model explainability. So to answer, the question, it depends on your project, if you prioritize improvement in performance over model explainability or computation cost and resources, then ensembling is perfectly fine.
Conclusion
Four main methods of ensembling machine learning models were discussed in this article, you are not limited to these methods but by your creativity. Any creative ways you can decide to combine the results of models to make new predictions counts as ensembling.
Thank you!