100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Deep Learning & Neural Networks
60 minadvanced

Practice — classify tabular data with Keras

What You'll Build

You will build a complete end-to-end tabular classification pipeline using Keras to predict IPL match outcomes — whether a team wins or loses given pre-match statistics. The project covers the full production workflow: loading and exploring a tabular dataset, preprocessing with normalisation and encoding, building a Keras Sequential model with BatchNorm and Dropout, training with EarlyStopping and ModelCheckpoint callbacks, evaluating performance with accuracy and ROC-AUC, plotting training curves, and generating a confusion matrix. By the end you will have a reusable tabular classification template applicable to any structured dataset — customer churn, credit risk, medical diagnosis — using IPL cricket as the concrete domain. This exercise consolidates every M2 concept: both Keras APIs, regularisation, callbacks, and GPU training patterns applied together in one coherent pipeline.

Analogy🏏Cricket
🏏 Think of it like cricket: Building a neural net from scratch is like a young cricketer learning the game by playing gully cricket with a tape-ball before ever stepping into a coaching academy. You don't use a bowling machine, you don't get video analysis, you don't have a structured training manual — you just play, make mistakes, adapt. Just as gully cricket teaches the core instincts of timing, footwork, and reading the ball that no academy drill can fully replicate, building a network from NumPy teaches the core mechanics of gradient flow, matrix shapes, and numerical stability that no framework hides. Just as every Indian international cricketer traces their instincts back to gully cricket roots, every deep learning practitioner benefits from having once implemented backpropagation themselves. The insight is that frameworks automate what you understand — building from scratch ensures you actually understand it.

Prerequisites

  • Keras Sequential API: building and compiling models with Dense, Dropout, BatchNormalization layers
  • Training callbacks: EarlyStopping with restore_best_weights=True, ModelCheckpoint with save_best_only=True
  • Regularisation: when to use Dropout (Dense layers) vs BatchNorm and appropriate rates
  • NumPy and pandas basics: DataFrame operations, train-test split, feature normalisation
  • Loss functions: binary_crossentropy for binary classification; understanding val_loss vs val_accuracy monitoring

Setup & Project Structure

This project uses pandas for data manipulation, scikit-learn for train-test splitting and evaluation metrics, and TensorFlow/Keras for the model. Install dependencies if not already present. The project has five logical sections: data generation and exploration, preprocessing pipeline, model building, training with callbacks, and evaluation with visualisation. All outputs (model file, training curves, confusion matrix) are saved to /tmp for the exercise.

Analogy🏏Cricket
🏏 Think of it like cricket: Before a net session you lay out the ground in clear zones — the bowling machine and pitch (data generation of synthetic IPL stats), the batsman's technique broken into stance, backlift, and follow-through (the NeuralNet forward, backward, and update methods), the throwdown routine that repeats and tracks improvement (the training loop with loss and accuracy monitoring), and finally a match simulation to test readiness (test-set evaluation). Just as a coach keeps the whole drill on one ground so a player can see how each phase connects, keeping all the code in one file lets you trace how a forward pass flows into a gradient and then a weight update. Just as you need only bat, ball, and pitch — not a full stadium — for a productive net, this exercise needs only NumPy, no heavy libraries. The payoff: a clean, well-sectioned practice structure means every mechanic is visible and debuggable, so you understand exactly why the network learns rather than treating it as a black box.
bash
# Install and import required libraries
# pip install tensorflow pandas scikit-learn matplotlib

import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, callbacks
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score
import matplotlib
matplotlib.use('Agg')  # non-interactive backend for scripts
import matplotlib.pyplot as plt

np.random.seed(42); tf.random.set_seed(42)
print('TF version:', tf.__version__)
print('GPU available:', bool(tf.config.list_physical_devices('GPU')))

# Project structure:
# ipl_classifier/
# ├── generate_data.py     — synthetic IPL match dataset
# ├── preprocess.py        — scaling, encoding, splitting
# ├── model.py             — Keras model builder
# ├── train.py             — training loop with callbacks
# └── evaluate.py          — metrics, confusion matrix, ROC curve

Step 1 — Foundation

Step 1 generates a realistic synthetic IPL match dataset and builds the preprocessing pipeline. The dataset has 14 features per match: both teams' batting average, strike rate, 4s per over, 6s per over, bowling economy, wicket rate, powerplay run rate, and death overs economy. The label is binary: 1 = home team wins. Preprocessing normalises all features to mean=0 std=1 using StandardScaler (fit only on training data to prevent data leakage) and performs an 80/10/10 train/val/test split. Correct preprocessing is the foundation of any ML pipeline — leaking the test set's statistics into the scaler is one of the most common and consequential bugs in data science.

Analogy🏏Cricket
🏏 Think of it like cricket: Before the first ball is bowled, the groundsman prepares the pitch — the pitch condition is the foundation that determines what is possible. Bad pitch preparation (wrong initialisation) makes even the best bowlers and batsmen ineffective. Just as a pitch that is too green (over-seamed) or too dry (over-spun) constrains the entire match, bad weight initialisation constrains the entire training run. He initialisation is the 'neutral pitch' — well-prepared, giving both batting and bowling a fair contest, from which any outcome is possible.
python
import numpy as np, pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
np.random.seed(42)

def generate_ipl_dataset(n=5000):
    """Synthetic IPL match dataset with 14 features."""
    data = {}
    for team in ['home', 'away']:
        data[f'{team}_batting_avg']     = np.random.normal(32, 8, n)
        data[f'{team}_strike_rate']     = np.random.normal(138, 18, n)
        data[f'{team}_fours_per_over']  = np.random.normal(2.1, 0.6, n)
        data[f'{team}_sixes_per_over']  = np.random.normal(0.9, 0.4, n)
        data[f'{team}_bowling_economy'] = np.random.normal(8.2, 1.4, n)
        data[f'{team}_wicket_rate']     = np.random.normal(1.8, 0.5, n)
        data[f'{team}_powerplay_rr']    = np.random.normal(8.5, 1.3, n)
        data[f'{team}_death_economy']   = np.random.normal(10.1, 2.0, n)

    df = pd.DataFrame(data)
    feature_cols = list(df.columns)

    # Win label: home team wins if batting edge + bowling edge > noise threshold
    batting_edge = (df['home_batting_avg'] - df['away_batting_avg']) * 0.4 + \
                   (df['home_strike_rate'] - df['away_strike_rate']) * 0.02
    bowling_edge = (df['away_bowling_economy'] - df['home_bowling_economy']) * 0.5 + \
                   (df['home_wicket_rate'] - df['away_wicket_rate']) * 0.3
    raw_score = batting_edge + bowling_edge + np.random.randn(n) * 3
    df['home_wins'] = (raw_score > 0).astype(int)

    return df, feature_cols

df, feature_cols = generate_ipl_dataset(5000)
print(df.describe().T[['mean','std','min','max']].round(2))
print(f"\nWin rate: {df['home_wins'].mean():.2%}")
print(f"Features: {len(feature_cols)}, Samples: {len(df)}")

# ── Train / Val / Test split ───────────────────────────────────
X = df[feature_cols].values.astype(np.float32)
y = df['home_wins'].values.astype(np.float32)

X_trainval, X_test, y_trainval, y_test = train_test_split(X, y, test_size=0.10, random_state=42)
X_train,    X_val,  y_train,    y_val  = train_test_split(X_trainval, y_trainval, test_size=0.111, random_state=42)

# ── StandardScaler — fit ONLY on training data ────────────────
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train).astype(np.float32)   # fit + transform
X_val   = scaler.transform(X_val).astype(np.float32)         # transform only
X_test  = scaler.transform(X_test).astype(np.float32)        # transform only

print(f'\nTrain: {X_train.shape}, Val: {X_val.shape}, Test: {X_test.shape}')
print(f'Train mean (should be ~0): {X_train.mean():.4f}, std (should be ~1): {X_train.std():.4f}')

Step 2 — Core Logic

Step 2 builds the Keras model and configures the training callbacks. The model uses the production-grade pattern: Dense → BatchNorm → Activation → Dropout, repeated across three hidden layers with a funnel architecture (128 → 64 → 32 neurons). The callback suite applies EarlyStopping, ModelCheckpoint, and ReduceLROnPlateau — the standard three-callback combination for any supervised learning task. The model is compiled with AdamW (weight decay regularisation) and binary cross-entropy. Architecture choices are documented inline so you can experiment with different widths, depths, and dropout rates.

Analogy🏏Cricket
🏏 Think of it like cricket: The NeuralNet class is the team management system — it tracks every player's contribution (forward pass caching), attributes match outcomes to specific decisions (backward pass gradients), and adjusts each player's role for the next match (weight update). Just as the team manager must record which batsman faced which bowler and how many runs were scored (cache activations) before attributing success or failure, the neural net must cache z and a at every layer before computing gradients. The momentum in SGD is the team's institutional memory — great past performances inform future selection policy, not overriding current evidence but weighted alongside it.
python
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, callbacks

def build_ipl_classifier(input_dim, dropout_rates=(0.4, 0.3, 0.2)):
    """
    Tabular classifier with BatchNorm + Dropout regularisation.
    Architecture: 128  64  32  1 (funnel)
    """
    model = keras.Sequential([
        # Layer 1: wide feature extraction
        layers.Dense(128, input_shape=(input_dim,), kernel_initializer='he_normal'),
        layers.BatchNormalization(),
        layers.Activation('relu'),
        layers.Dropout(dropout_rates[0]),

        # Layer 2: feature combination
        layers.Dense(64, kernel_initializer='he_normal'),
        layers.BatchNormalization(),
        layers.Activation('relu'),
        layers.Dropout(dropout_rates[1]),

        # Layer 3: abstract representation
        layers.Dense(32, kernel_initializer='he_normal'),
        layers.BatchNormalization(),
        layers.Activation('relu'),
        layers.Dropout(dropout_rates[2]),

        # Output: win probability
        layers.Dense(1, activation='sigmoid', dtype='float32')
    ], name='ipl_win_classifier')

    model.compile(
        optimizer=keras.optimizers.AdamW(learning_rate=1e-3, weight_decay=1e-4),
        loss='binary_crossentropy',
        metrics=['accuracy', keras.metrics.AUC(name='auc')]
    )
    return model

model = build_ipl_classifier(input_dim=16)  # 16 features (8 per team)
model.summary()
print(f'Total params: {model.count_params():,}')

# ── Callbacks ─────────────────────────────────────────────────
cb_early = callbacks.EarlyStopping(
    monitor='val_loss', patience=15,
    restore_best_weights=True, verbose=1
)
cb_ckpt = callbacks.ModelCheckpoint(
    '/tmp/ipl_classifier_best.keras',
    monitor='val_auc', mode='max',
    save_best_only=True, verbose=0
)
cb_lr = callbacks.ReduceLROnPlateau(
    monitor='val_loss', factor=0.5, patience=7,
    min_lr=1e-6, verbose=1
)
print('Model and callbacks ready')

Step 3 — Integration & Enhancement

Step 3 runs the full training loop and plots the training curves. We build a tf.data pipeline for efficiency, train with all three callbacks active, and plot both loss and AUC curves for training and validation sets. The training curve plot is the most diagnostic tool available — it immediately reveals overfitting (diverging train/val curves), underfitting (both curves plateau at poor performance), or good training (both curves improve together then val stabilises). We also run a hyperparameter comparison between two dropout configurations to demonstrate how the gap between train and val curves changes with regularisation strength.

Analogy🏏Cricket
🏏 Think of it like cricket: Mini-batch training is like a cricket coaching session where the coach evaluates the batsman against a random set of 32 deliveries from the session's total pool, not all 500 deliveries at once. Evaluating all 500 deliveries before giving any feedback (full-batch) is slow and ignores the fact that early corrections from the first 32 balls can already improve performance on balls 33–64. Mini-batch feedback (stochastic gradients) allows the batsman to improve continuously throughout the session, converging to good technique faster than waiting for the end-of-session review. The randomness (shuffle each epoch) ensures the coach doesn't accidentally train the batsman to handle only one type of delivery sequence.
python
import numpy as np, tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, callbacks
import matplotlib.pyplot as plt

# Paste data generation + preprocessing from Step 1 here
# Then run:

def plot_training_curves(history, title='IPL Classifier Training'):
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))

    # Loss curves
    ax1.plot(history.history['loss'],     label='Train Loss', color='#1f77b4')
    ax1.plot(history.history['val_loss'], label='Val Loss',   color='#ff7f0e', linestyle='--')
    ax1.set_title(f'{title} — Loss'); ax1.set_xlabel('Epoch'); ax1.set_ylabel('BCE Loss')
    ax1.legend(); ax1.grid(True, alpha=0.3)

    # AUC curves
    ax2.plot(history.history['auc'],     label='Train AUC', color='#2ca02c')
    ax2.plot(history.history['val_auc'], label='Val AUC',   color='#d62728', linestyle='--')
    ax2.set_title(f'{title} — AUC'); ax2.set_xlabel('Epoch'); ax2.set_ylabel('ROC-AUC')
    ax2.legend(); ax2.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.savefig(f'/tmp/{title.replace(" ","_")}.png', dpi=150, bbox_inches='tight')
    plt.close()
    print(f'Saved training curve: /tmp/{title.replace(" ","_")}.png')

# Main training run
BATCH_SIZE = 128
train_ds = (tf.data.Dataset.from_tensor_slices((X_train, y_train))
            .shuffle(4000).batch(BATCH_SIZE).prefetch(tf.data.AUTOTUNE))
val_ds   = (tf.data.Dataset.from_tensor_slices((X_val, y_val))
            .batch(BATCH_SIZE).prefetch(tf.data.AUTOTUNE))

model = build_ipl_classifier(input_dim=X_train.shape[1])
history = model.fit(
    train_ds, epochs=200,
    validation_data=val_ds,
    callbacks=[cb_early, cb_ckpt, cb_lr],
    verbose=1
)
plot_training_curves(history, 'IPL Classifier')
print(f'Stopped at epoch {len(history.history["loss"])}')

# ── Hyperparameter comparison: low vs high dropout ─────────────
results = {}
for dropout in [(0.1,0.1,0.05), (0.5,0.4,0.3)]:
    m = build_ipl_classifier(X_train.shape[1], dropout_rates=dropout)
    h = m.fit(train_ds, epochs=50, validation_data=val_ds,
              callbacks=[keras.callbacks.EarlyStopping('val_loss', patience=10,
                          restore_best_weights=True)],
              verbose=0)
    train_auc = max(h.history['auc'])
    val_auc   = max(h.history['val_auc'])
    results[str(dropout)] = {'train_auc': train_auc, 'val_auc': val_auc, 'gap': train_auc-val_auc}
    print(f'Dropout {dropout}: TrainAUC={train_auc:.3f} ValAUC={val_auc:.3f} Gap={train_auc-val_auc:.3f}')

Step 4 — Testing & Verification

Step 4 evaluates on the held-out test set and generates a confusion matrix and classification report. The test set is evaluated only once — multiple evaluations on the test set constitute implicit hyperparameter tuning on it, which defeats its purpose as an unbiased estimate of generalisation. We compute accuracy, AUC, precision, recall, and F1-score, and visualise the confusion matrix to see which error type dominates (false positives vs false negatives).

Analogy🏏Cricket
🏏 Think of it like cricket: Verifying the network is a batsman's fitness test with three non-negotiable checks. First, form must steadily improve — the loss must fall monotonically over the first 200 epochs, just as a player's error count should drop session after session, not swing wildly. Second, real skill must beat luck — test accuracy above 70% when random guessing is 50%, exactly as a batsman must clearly outscore a tail-ender swinging blindly to prove genuine technique. Third, every part of the technique must be engaged — non-zero gradient norms confirm the learning signal actually flows through all three layers, like checking footwork, backlift, and follow-through are each working rather than one part being frozen. The gold standard is the numerical gradient check: nudge a weight a tiny amount and confirm the loss changes as predicted, just as a coach films from two angles to confirm a fix is real, not imagined. The payoff: these checks prove your from-scratch network is genuinely learning and correctly wired, not just accidentally producing a decent number.
bash
# Evaluation on held-out test set (evaluate only ONCE)
test_ds = tf.data.Dataset.from_tensor_slices((X_test, y_test)).batch(256).prefetch(2)

# Load best checkpoint
best_model = keras.models.load_model('/tmp/ipl_classifier_best.keras')

# Predict probabilities
y_prob = best_model.predict(X_test, verbose=0).flatten()
y_pred = (y_prob > 0.5).astype(int)

# Metrics
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score
import matplotlib.pyplot as plt, seaborn

print('=== TEST SET EVALUATION ===')
print(f'Accuracy: {(y_pred == y_test.astype(int)).mean():.2%}')
print(f'ROC-AUC:  {roc_auc_score(y_test, y_prob):.4f}')
print('\nClassification Report:')
print(classification_report(y_test, y_pred, target_names=['Away Win','Home Win']))

# Confusion matrix
cm = confusion_matrix(y_test, y_pred)
fig, ax = plt.subplots(figsize=(6,5))
im = ax.imshow(cm, cmap='Blues')
ax.set_xticks([0,1]); ax.set_yticks([0,1])
ax.set_xticklabels(['Away Win','Home Win'])
ax.set_yticklabels(['Away Win','Home Win'])
for i in range(2):
    for j in range(2):
        ax.text(j, i, cm[i,j], ha='center', va='center', fontsize=14, fontweight='bold')
ax.set_xlabel('Predicted'); ax.set_ylabel('Actual')
ax.set_title('IPL Win Predictor — Confusion Matrix')
plt.colorbar(im, ax=ax)
plt.tight_layout()
plt.savefig('/tmp/confusion_matrix.png', dpi=150, bbox_inches='tight')
print('Confusion matrix saved to /tmp/confusion_matrix.png')
# Expected: >72% accuracy, >0.78 AUC on this synthetic dataset

Warning: Never evaluate on the test set more than once. If you tune hyperparameters (dropout rate, learning rate, model depth) by checking test set performance after each change, the test set becomes part of your training process — it is no longer an unbiased estimate of generalisation. All hyperparameter decisions must be made using only train and validation metrics. The test set is evaluated exactly once, at the end, to report the final unbiased performance.

Extension Challenge: (1) Add a second output head to predict the winning margin (regression, MSE loss) alongside the win/loss classification — this requires the Functional API and a multi-output model. (2) Replace the StandardScaler with a Keras Normalization layer (layers.Normalization()) that is fitted during the training step — this way normalisation is part of the model and you do not need to preprocess at inference time. (3) Implement k-fold cross-validation (k=5) using sklearn's StratifiedKFold to get a more reliable estimate of generalisation performance than a single train/val/test split.

  • Fit the StandardScaler only on training data and transform val/test — fitting on all data leaks test distribution into preprocessing, inflating measured performance.
  • The production Keras tabular pattern is: Dense → BatchNorm → Activation('relu') → Dropout, repeated with decreasing width (funnel architecture).
  • Monitor val_auc (not val_accuracy) in callbacks for classification — AUC is a continuous metric that captures probability calibration improvements that accuracy's rounding misses.
  • Use AdamW (not Adam) for tabular classification — the decoupled weight decay provides consistent L2 regularisation across all parameter groups.
  • Evaluate the test set exactly once after all hyperparameter decisions are made using train/val metrics — multiple test evaluations constitute implicit overfitting to the test set.
  • Training curves (loss + AUC for train and val) are the primary diagnostic tool — diverging curves mean overfitting, both plateauing low means underfitting, both improving then val stabilising means good training.
Lesson 12 of 35
0% complete