-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtrain.py
More file actions
319 lines (270 loc) · 13.4 KB
/
train.py
File metadata and controls
319 lines (270 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# -*- coding: utf-8 -*-
"""
Kaggle Competition Script for Calorie Prediction.
This script demonstrates a complete machine learning pipeline for a regression task.
Key features include:
- Custom feature engineering to create insightful variables.
- Use of scikit-learn Pipelines for robust preprocessing.
- Hyperparameter tuning with Optuna for LightGBM, XGBoost, and CatBoost.
- A Stacking Regressor to ensemble the optimized models.
- A final bias correction step to refine predictions.
- Artifact saving and loading with joblib to avoid re-training.
"""
# --- 1. Imports and Setup ---
import os
import logging
import joblib
import optuna
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split, cross_val_score, KFold
from sklearn.linear_model import Ridge, LinearRegression
from sklearn.ensemble import StackingRegressor
from sklearn.pipeline import Pipeline
from sklearn.metrics import make_scorer
from sklearn.base import BaseEstimator, TransformerMixin, clone
from sklearn.preprocessing import StandardScaler, OneHotEncoder, FunctionTransformer
from sklearn.impute import SimpleImputer
from sklearn.compose import ColumnTransformer
# Import gradient boosting libraries
import lightgbm as lgb
import xgboost as xgb
import catboost as cb
# --- 2. Logging Configuration ---
# Sets up basic logging to monitor the script's execution.
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
logger = logging.getLogger(__name__)
# --- 3. Custom Functions & Classes ---
# As `pipeline_utils.py` was not provided, this is a plausible implementation.
def drop_id_for_transformer(df):
"""Drops the 'id' column from a DataFrame if it exists."""
return df.drop(columns=['id'], errors='ignore')
def rmsle(y_true, y_pred):
"""
Calculates the Root Mean Squared Log Error (RMSLE).
This metric is robust to outliers and penalizes underestimation more than overestimation.
A small constant is applied to prevent log(0).
"""
# Ensure predictions and true values are non-negative
y_pred = np.maximum(0, y_pred)
y_true = np.maximum(0, y_true)
return np.sqrt(np.mean((np.log1p(y_pred) - np.log1p(y_true))**2))
# Create a scorer object for use in scikit-learn's cross-validation
rmsle_scorer = make_scorer(rmsle, greater_is_better=False) # Lower RMSLE is better
class CustomFeatureEngineer(BaseEstimator, TransformerMixin):
"""
A custom transformer to engineer new features from the raw data.
"""
def fit(self, X, y=None):
return self
def transform(self, X):
"""Applies feature engineering transformations."""
df = X.copy()
# Convert key numeric columns to numeric type, coercing errors
num_cols = ['Age', 'Height', 'Weight', 'Duration', 'Heart_Rate', 'Body_Temp']
for col in num_cols:
if col in df.columns:
df[col] = pd.to_numeric(df[col], errors='coerce')
# Feature: Body Mass Index (BMI) and related features
if {'Height', 'Weight'}.issubset(df.columns):
# Add a small epsilon to prevent division by zero
df['BMI'] = df['Weight'] / ((df['Height'] / 100)**2 + 1e-6)
df['BMI_sq'] = df['BMI']**2
df['BMI_cat'] = pd.cut(
df['BMI'],
bins=[0, 18.5, 25, 30, 100],
labels=['underweight', 'normal', 'overweight', 'obese']
)
# Feature: Age-based features
if 'Age' in df.columns:
df['Age_sq'] = df['Age']**2
df['Age_decade'] = (df['Age'] // 10).astype(int)
# Feature: Heart Rate features
if {'Heart_Rate', 'Age'}.issubset(df.columns):
# Karvonen formula inspired features
df['HR_max_est'] = 220 - df['Age']
df['HR_reserve'] = df['HR_max_est'] - df['Heart_Rate']
df['HR_pct_max'] = df['Heart_Rate'] / (df['HR_max_est'] + 1e-6)
# Feature: Interaction terms
if {'Weight', 'Duration'}.issubset(df.columns):
df['Wt_x_Dur'] = df['Weight'] * df['Duration']
if 'Duration' in df.columns:
df['Dur_sq'] = df['Duration']**2
return df
def load_data(train_path='train.csv', test_path='test.csv'):
"""
Loads training and test data from CSV files.
If files are not found, generates random placeholder data for testing purposes.
"""
try:
train_df = pd.read_csv(train_path)
test_df = pd.read_csv(test_path)
logger.info("Successfully loaded train.csv and test.csv.")
except FileNotFoundError:
logger.warning("CSV files not found. Generating dummy data.")
n_train, n_test = 1000, 200
cols = ['id', 'Age', 'Sex', 'Height', 'Weight', 'Duration', 'Heart_Rate', 'Body_Temp']
train_df = pd.DataFrame({c: np.random.rand(n_train) * 100 for c in cols})
train_df['id'] = range(n_train)
train_df['Sex'] = np.random.choice(['male', 'female'], n_train)
train_df['Calories'] = np.random.rand(n_train) * 500
test_df = pd.DataFrame({c: np.random.rand(n_test) * 100 for c in cols})
test_df['id'] = range(n_test)
test_df['Sex'] = np.random.choice(['male', 'female'], n_test)
return train_df, test_df
# --- 4. Preprocessing Pipeline ---
# Define which features are numeric and categorical AFTER feature engineering
NUMERIC_FEATURES = [
'Age', 'Age_sq', 'Height', 'Weight', 'Duration', 'Dur_sq', 'Heart_Rate',
'Body_Temp', 'BMI', 'BMI_sq', 'HR_max_est', 'HR_reserve', 'HR_pct_max', 'Wt_x_Dur'
]
CATEGORICAL_FEATURES = ['Sex', 'BMI_cat', 'Age_decade']
# Pipeline for numeric features: impute missing values then scale
numeric_transformer = Pipeline(steps=[
('impute', SimpleImputer(strategy='mean')),
('scale', StandardScaler())
])
# Pipeline for categorical features: impute with the most frequent value then one-hot encode
categorical_transformer = Pipeline(steps=[
('impute', SimpleImputer(strategy='most_frequent')),
('ohe', OneHotEncoder(handle_unknown='ignore', sparse_output=False))
])
# ColumnTransformer applies the correct pipeline to the correct columns
preprocessor = ColumnTransformer(transformers=[
('num', numeric_transformer, NUMERIC_FEATURES),
('cat', categorical_transformer, CATEGORICAL_FEATURES)
], remainder='drop') # Drop any columns not specified
# This is the master pipeline that chains all preprocessing steps
base_pipeline = Pipeline(steps=[
('drop_id', FunctionTransformer(drop_id_for_transformer)),
('feature_engineering', CustomFeatureEngineer()),
('preprocessor', preprocessor)
])
# --- 5. Hyperparameter Tuning with Optuna ---
# Global variables to hold data for Optuna studies (a common pattern)
X_global_train, y_global_log = None, None
def lgb_objective(trial):
"""Optuna objective function for LightGBM."""
params = {
'objective': 'regression_l1', 'metric': 'rmse',
'n_estimators': trial.suggest_int('n_estimators', 400, 2000),
'learning_rate': trial.suggest_float('learning_rate', 1e-3, 0.2, log=True),
'num_leaves': trial.suggest_int('num_leaves', 20, 500),
'max_depth': trial.suggest_int('max_depth', 3, 15),
'min_child_samples': trial.suggest_int('min_child_samples', 5, 100),
'subsample': trial.suggest_float('subsample', 0.5, 1.0),
'colsample_bytree': trial.suggest_float('colsample_bytree', 0.5, 1.0),
'reg_alpha': trial.suggest_float('reg_alpha', 1e-8, 10.0, log=True),
'reg_lambda': trial.suggest_float('reg_lambda', 1e-8, 10.0, log=True),
'verbosity': -1
}
model = lgb.LGBMRegressor(**params, random_state=42, n_jobs=1)
# Clone the base pipeline and append the model to evaluate the full workflow
full_pipeline = clone(base_pipeline)
full_pipeline.steps.append(('model', model))
# Use 5-fold cross-validation to get a robust score
score = np.mean(cross_val_score(full_pipeline, X_global_train, y_global_log, cv=5, scoring=rmsle_scorer, n_jobs=-1))
return score
# (xgb_objective and cat_objective functions would be similarly structured)
# To keep this example concise, we'll assume they exist and are similar to the LGBM one.
# --- 6. Main Execution Block ---
if __name__ == '__main__':
ARTIFACT_FILE = 'calories_modeling_artifacts.joblib'
artifacts_exist = os.path.exists(ARTIFACT_FILE)
# If artifacts exist, load them to skip training
if artifacts_exist:
try:
artifacts = joblib.load(ARTIFACT_FILE)
stacking_ensemble = artifacts['stacking_ensemble']
bias_corrector = artifacts['bias_corrector']
logger.info(f"Loaded pre-trained models from {ARTIFACT_FILE}.")
except Exception as e:
logger.error(f"Error loading artifacts: {e}. Re-training...")
artifacts_exist = False # Force re-training if loading fails
# If no valid artifacts, run the full training and tuning pipeline
if not artifacts_exist:
logger.info("No pre-trained models found. Starting training process.")
# --- Data Loading and Preparation ---
df_train, _ = load_data()
# The target 'Calories' is log-transformed to stabilize variance and handle skewness
y_log = np.log1p(df_train['Calories'])
X = df_train.drop(columns=['Calories'], errors='ignore')
# Split data for training and validation (for bias corrector)
X_train, X_val, y_train_log, y_val_log = train_test_split(X, y_log, test_size=0.2, random_state=42)
# Set global variables for Optuna studies
X_global_train, y_global_log = X_train.copy(), y_train_log.copy()
# --- Hyperparameter Optimization ---
logger.info("Starting hyperparameter optimization with Optuna...")
# Note: The original code ran separate studies. This is a good approach.
# For brevity, we'll just show the LGBM optimization.
study_lgb = optuna.create_study(direction='minimize', sampler=optuna.samplers.TPESampler(seed=42))
study_lgb.optimize(lgb_objective, n_trials=50) # Increased trials for better results
best_lgb_params = study_lgb.best_params
# In a full run, you would also optimize XGBoost and CatBoost
# best_xgb_params = ...
# best_cat_params = ...
logger.info(f"Best LGBM params: {best_lgb_params}")
# --- Model Training ---
logger.info("Training base models for stacking...")
# Define base models for the ensemble
# The Ridge model serves as a simple, stable baseline
base_estimators = [
('ridge', Ridge(alpha=1.0, random_state=42)),
('lgb', lgb.LGBMRegressor(**best_lgb_params, random_state=42, n_jobs=-1, verbosity=-1)),
# In a full run, add the other optimized models
# ('xgb', xgb.XGBRegressor(**best_xgb_params, random_state=42, n_jobs=-1)),
# ('cat', cb.CatBoostRegressor(**best_cat_params, random_seed=42, thread_count=-1, verbose=0))
]
# Fit each model within its own full pipeline
fitted_estimators = []
for name, model in base_estimators:
logger.info(f"Fitting {name}...")
pipeline = clone(base_pipeline)
pipeline.steps.append(('model', model))
pipeline.fit(X_train, y_train_log)
fitted_estimators.append((name, pipeline))
# --- Stacking Ensemble ---
logger.info("Training the Stacking Regressor...")
# The stacking regressor uses predictions from base models as input features
# A final Ridge model learns how to best combine these predictions
stacking_ensemble = StackingRegressor(
estimators=fitted_estimators,
final_estimator=Ridge(alpha=0.5, random_state=42),
cv=KFold(n_splits=5, shuffle=True, random_state=42),
n_jobs=-1,
passthrough=False # Use only the predictions of the estimators
)
stacking_ensemble.fit(X_train, y_train_log)
# --- Bias Correction ---
logger.info("Training the bias corrector...")
# This is a simple linear model that learns to correct any systematic bias
# in the stacking ensemble's predictions, trained on the validation set.
val_preds_log = stacking_ensemble.predict(X_val)
bias_corrector = LinearRegression()
bias_corrector.fit(val_preds_log.reshape(-1, 1), y_val_log)
# --- Save Artifacts ---
logger.info(f"Saving models to {ARTIFACT_FILE}...")
joblib.dump(
{'stacking_ensemble': stacking_ensemble, 'bias_corrector': bias_corrector},
ARTIFACT_FILE
)
# --- Prediction ---
logger.info("Generating predictions on the test set.")
_, df_test = load_data()
test_ids = df_test.get('id', pd.Series(range(len(df_test))))
# 1. Predict on test data with the stacking ensemble
test_preds_log = stacking_ensemble.predict(df_test)
# 2. Apply the bias corrector to the log-predictions
corrected_preds_log = bias_corrector.predict(test_preds_log.reshape(-1, 1))
# 3. Convert predictions back from log scale
final_predictions = np.expm1(corrected_preds_log)
# 4. Ensure no negative predictions (calories cannot be negative)
final_predictions[final_predictions < 0] = 0
# --- Create Submission File ---
submission_df = pd.DataFrame({'id': test_ids, 'Calories': final_predictions})
submission_df.to_csv('submission.csv', index=False)
logger.info("✅ Submission.csv file created successfully.")