Day 4 of My End-to-End ML Project
(The Algorithm Zoo and Its Keeper)
Where 8 algorithms battle in hyperparameter arenas, and only the fittest survive to become artifacts
Yesterday I built the data-pipeline . Today I unleashed the algorithm zoo—eight regression models competing in hyperparameter arenas to earn their place as the production-ready predictor. No more “let’s try Random Forest and call it a day.” This is systematic model selection with production-grade rigor.
Here’s how I engineered two critical components that transform raw features into a battle-tested, serialized model artifact.
utils.py: The Project’s Swiss Army Knife
This file is the unsung hero of production ML—containing utilities that prevent catastrophic failures most tutorials ignore. Three components deserve special attention:
1. The CatBoostRegressorWrapper: Taming the Wild Beast
CatBoost is a powerhouse algorithm, but it refuses to play nice with scikit-learn’s GridSearchCV out of the box. Why? Parameter naming mismatches and non-standard API conventions. My wrapper solves this with surgical precision:
# src/utils.py (critical section)
class CatBoostRegressorWrapper(BaseEstimator, RegressorMixin):
"""
Fully sklearn-compatible wrapper for CatBoostRegressor (supports GridSearchCV)
"""
def __init__(self, iterations=1000, learning_rate=0.03, depth=6,
l2_leaf_reg=3.0, random_strength=1.0, verbose=False, **kwargs):
# Store ALL tunable parameters as direct attributes (required for sklearn)
self.iterations = iterations
self.learning_rate = learning_rate
self.depth = depth
# ... other parameters ...
self.model = None # Will be initialized in fit()
def fit(self, X, y, **fit_params):
# Rebuild model with CURRENT parameters (critical for GridSearchCV iterations)
params = {
'iterations': self.iterations,
'learning_rate': self.learning_rate,
'depth': self.depth,
# ... all parameters ...
}
self.model = catboost.CatBoostRegressor(**params)
self.model.fit(X, y, **fit_params)
return self
# Required sklearn interface methods
def predict(self, X): ...
def get_params(self, deep=True): ... # Returns ALL parameters for GridSearchCV
def set_params(self, **params): ... # Updates parameters dynamically during search
Why this wrapper is non-negotiable:
| Without Wrapper | With Wrapper |
|—————–|————–|
| GridSearchCV crashes with TypeError | Seamless hyperparameter tuning |
| Manual parameter tuning (error-prone) | Automated search across 100+ configurations |
| Inconsistent API across algorithms | Unified interface for all 8 models |
| Result: CatBoost excluded from competition | Result: Fair competition with all algorithms |
Production insight: In real-world ML, algorithm compatibility often matters more than raw performance. A 92% accurate model that breaks your pipeline is worthless. An 89% model that integrates cleanly wins every time.
2. save_object()/load_object(): The Artifact Serialization Layer
Why dill instead of pickle? Because production models contain complex objects (custom wrappers, lambda functions, nested pipelines) that pickle chokes on:
def save_object(file_path, obj):
try:
dir_path = os.path.dirname(file_path)
os.makedirs(dir_path, exist_ok=True)
with open(file_path, "wb") as file_obj:
dill.dump(obj, file_obj) # Handles complex objects pickle can't serialize
except Exception as e:
raise CustomException(e, sys)
→ This single function becomes the bridge between training and inference. Without robust serialization, your beautifully trained model becomes a paperweight the moment you try to deploy it.
3. evaluate_models(): The Hyperparameter Arena
This function isn’t just “train and score”—it’s a leakage-proof evaluation chamber:
def evaluate_models(X_train, y_train, X_test, y_test, models, params):
report = {}
for model_name in models:
model = models[model_name]
param_grid = params[model_name]
# 1. Hyperparameter tuning on TRAINING data ONLY (critical!)
gs = GridSearchCV(model, param_grid, cv=3)
gs.fit(X_train, y_train)
# 2. Apply BEST parameters to model
model.set_params(**gs.best_params_)
# 3. FINAL training on FULL training set (with best params)
model.fit(X_train, y_train)
# 4. Evaluation on HELD-OUT test set (never seen during tuning!)
y_test_pred = model.predict(X_test)
test_score = r2_score(y_test, y_test_pred)
report[model_name] = test_score
return report
The leakage-proof sequence:
Raw Data → Train/Test Split → [TRAIN SET ONLY] → Hyperparameter Tuning → Best Params →
Final Training → [TEST SET ONLY] → Final Evaluation
→ Violate this sequence (e.g., tune on full data), and your reported accuracy becomes fiction.
model_trainer.py: The Algorithm Gladiator
This component orchestrates the entire model selection tournament—from algorithm registration to champion coronation.
The Tournament Structure
# src/components/model_trainer.py (annotated)
models = {
"Random Forest": RandomForestRegressor(),
"Decision Tree": DecisionTreeRegressor(),
"Gradient Boosting": GradientBoostingRegressor(),
"Linear Regression": LinearRegression(),
"K-Neighbors Regressor": KNeighborsRegressor(),
"XGBRegressor": XGBRegressor(),
"CatBoosting Regressor": CatBoostRegressorWrapper(verbose=False), # Wrapped!
"AdaBoost Regressor": AdaBoostRegressor()
}
params = {
"Random Forest": {'n_estimators': [8,16,32,64,128,256]},
"Gradient Boosting": {
'learning_rate': [.1,.01,.05,.001],
'n_estimators': [8,16,32,64,128,256]
},
# ... hyperparameter grids for all 8 models ...
}
The Champion Selection Protocol
- Hyperparameter Arena: Each model battles through 3-fold cross-validation across its parameter grid
- Final Showdown: Best-configured models train on full training set
- Judgment Day: Held-out test set evaluates true generalization ability
- Quality Gate: Reject ALL models if best R² < 0.6 (prevents garbage-in-garbage-out deployments)
- Coronation: Champion serialized to
artifacts/model.pkl
# Critical quality gate
if best_model_score < 0.6:
raise CustomException("No best model found.", sys) # Prevents bad deployments
# Champion serialization
save_object(
file_path=self.model_trainer_config.trained_model_file_path,
obj=best_model # The battle-tested champion
)
Why This Beats “Just Use XGBoost”
| Approach | Risk | Production Impact |
|---|---|---|
| Single algorithm selection | Algorithm bias; misses better performers | Stuck with suboptimal model for months |
| No hyperparameter tuning | 15-30% performance gap vs tuned version | Wasted compute/resources |
| No quality gate | Deploys garbage models silently | Business decisions based on broken predictions |
| This tournament approach | Compute cost during training | Optimal model + confidence in deployment |
Integration: How Day 4 Connects to Previous Days
Today’s components consume outputs from Day 3 and feed into tomorrow’s pipeline:
Day 3 Output (artifacts/) Day 4 Input Day 4 Output (artifacts/)
─────────────────────────── ─────────────────── ───────────────────────────────
train.csv │ │
test.csv │ data_ingestion.py │
↓ ↓
train_arr.npy ────────────→ model_trainer.py ───→ model.pkl
test_arr.npy ────────────→ (consumes arrays) preprocessor.pkl (from Day 3)
preprocessor.pkl ──────────→ │
↓
Day 5: train_pipeline.py
(orchestrates entire flow)
Critical integration points:
model_trainer.pyconsumestrain_arr.npy/test_arr.npyfrom Day 3’s transformation- Uses Day 2’s
CustomExceptionfor failure handling with full context - Logs every tournament stage via Day 2’s
logger.py:[2026-01-30 14:22:18] 47 ModelTrainer - INFO - Split training and test input data. [2026-01-30 14:23:45] 89 ModelTrainer - INFO - Best found model on both training and testing dataset. - Saves champion model to
artifacts/model.pkl—ready for Day 5’s inference pipeline
Validation: Proving the Tournament Works
Before trusting this with business-critical predictions, I validated the full flow:
# Standalone test of model trainer
python -c "
from src.components.data_ingestion import DataIngestion
from src.components.data_transformation import DataTransformation
from src.components.model_trainer import ModelTrainer
# Reproduce full pipeline flow
ingestion = DataIngestion()
train_path, test_path = ingestion.initiate_data_ingestion()
transformation = DataTransformation()
train_arr, test_arr, _ = transformation.initiate_data_transformation(train_path, test_path)
trainer = ModelTrainer()
r2 = trainer.initiate_model_trainer(train_arr, test_arr)
print(f'✓ Tournament complete! Best model R²: {r2:.4f}')
"
# Output:
# ✓ Tournament complete! Best model R²: 0.8865
# Best Model: Linear Regression, Score: 0.8865
This validation proves:
- End-to-end pipeline executes without errors
- Hyperparameter tuning completes successfully
- Quality gate passes (R² > 0.6)
- Champion model serialized to
artifacts/model.pkl
Real-World Lesson: The Hyperparameter Tuning Trap
During implementation, I almost made a catastrophic mistake:
# DANGEROUS APPROACH (causes data leakage!)
gs = GridSearchCV(model, params, cv=3)
gs.fit(X_train, y_train) # Tuning uses full training set
best_model = gs.best_estimator_ # Already trained on full data!
# Then evaluating on test set? NO! This leaks validation info into training.
The fix: Explicit refit after parameter selection:
gs.fit(X_train, y_train) # Tuning phase (uses CV splits internally)
model.set_params(**gs.best_params_) # Apply best params
model.fit(X_train, y_train) # FINAL training on FULL training set
# NOW evaluate on held-out test set → leakage-proof
→ This subtle distinction separates valid evaluation from self-deception. In production ML, leakage isn’t just inaccurate—it’s dangerous (leads to bad business decisions).
Day 4 Checklist Complete
utils.pyimplemented with:- CatBoostRegressorWrapper (GridSearchCV compatible)
save_object()/load_object()usingdill- Leakage-proof
evaluate_models()with hyperparameter tuning
model_trainer.pyimplemented with:- 8-algorithm tournament structure
- Hyperparameter grids for all models
- Quality gate (R² threshold)
- Champion serialization to
artifacts/model.pkl
- Full integration with Day 2-3 components (logging, exceptions, artifacts)
- Standalone validation test proving tournament execution
- Artifact directory now contains:
artifacts/model.pkl(champion model)artifacts/preprocessor.pkl(from Day 3)artifacts/train.csv/test.csv(from Day 3)
Tomorrow’s Mission (Day 5): The Grand Orchestration
Tomorrow I’ll build the conductor that unifies all components into a single-command pipeline:
- Implement
train_pipeline.py:class TrainPipeline: def run(self): # Orchestrate entire flow in 5 lines: train_path, test_path = DataIngestion().initiate_data_ingestion() train_arr, test_arr, _ = DataTransformation().initiate_data_transformation(train_path, test_path) r2 = ModelTrainer().initiate_model_trainer(train_arr, test_arr) return r2 - Create
train.pyas the single entry point:python train.py # Triggers full pipeline: raw data → model.pkl - Critical integration: Add error boundaries between components so one failure doesn’t corrupt artifacts
The pipeline will finally become one command away from production readiness.
Why Day 4 > “Just Train a Model” for Business Impact
| Activity | Short-Term “Cost” | Long-Term Business Impact |
|---|---|---|
| Train single model quickly | “Shipped fast!” | Model degrades silently; business decisions based on stale predictions |
| Algorithm tournament with quality gates | “Took extra day” | Confidence in predictions → Better decisions → Revenue impact |
The brutal truth: In production ML, model selection isn’t a technical detail—it’s a risk management strategy. Deploying an unvetted model is like launching a rocket without testing its engines. Today’s tournament isn’t overhead—it’s insurance against catastrophic failure.
Follow my end-to-end ML journey: GitHub Repo Link
Day 4 complete. Algorithm tournament concluded.
Champion crowned. Tomorrow: the grand orchestration.
Key Takeaways:
- Custom wrappers (like CatBoostRegressorWrapper) are non-negotiable for algorithm interoperability in production
dill>picklefor serializing complex ML objects—prevents “works in training, fails in inference” disasters- Hyperparameter tuning sequence matters: leakage-proof evaluation requires strict train/tune/test separation
- Quality gates (R² threshold) prevent garbage models from reaching production
- Production ML isn’t about finding the “best” algorithm—it’s about systematically eliminating bad ones
What’s your model selection strategy? Do you run algorithm tournaments or default to your favorite model? Share your war stories below!