""" Overfitting Explorer — Before/After Comparison with Gradio ========================================================== Train a baseline (overfitting) model and a regularized model on Fashion-MNIST side by side. Choose which regularization techniques to apply and see the difference in accuracy/loss curves. """ # ── 1. Import Libraries ───────────────────────────────────────────────────── import os os.environ["KERAS_BACKEND"] = "torch" import keras import numpy as np import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import gradio as gr print(f"Keras version: {keras.__version__}") print(f"Backend: {keras.backend.backend()}") # ── 2. Download and Prepare Fashion-MNIST ──────────────────────────────────── (x_train_raw, y_train_raw), (x_test_raw, y_test_raw) = keras.datasets.fashion_mnist.load_data() CLASS_NAMES = [ "T-shirt/top", "Trouser", "Pullover", "Dress", "Coat", "Sandal", "Shirt", "Sneaker", "Bag", "Ankle boot", ] # Normalize to [0, 1] x_train_all = x_train_raw.astype("float32") / 255.0 x_test_all = x_test_raw.astype("float32") / 255.0 # Train/val split y_train = y_train_raw[:50000] y_val = y_train_raw[50000:] y_test = y_test_raw # Flat versions for dense models x_train_flat = x_train_all[:50000].reshape(-1, 784) x_val_flat = x_train_all[50000:].reshape(-1, 784) x_test_flat = x_test_all.reshape(-1, 784) # Image versions for CNN/augmentation models x_train_img = x_train_all[:50000].reshape(-1, 28, 28, 1) x_val_img = x_train_all[50000:].reshape(-1, 28, 28, 1) x_test_img = x_test_all.reshape(-1, 28, 28, 1) print(f"Training set: {x_train_flat.shape} (flat), {x_train_img.shape} (image)") print(f"Validation set: {x_val_flat.shape}") print(f"Test set: {x_test_flat.shape}") # ── 3. Gradio Progress Callback ───────────────────────────────────────────── class ProgressCallback(keras.callbacks.Callback): """Keras callback that updates a Gradio progress bar.""" def __init__(self, progress, total_epochs, label=""): super().__init__() self.progress = progress self.total_epochs = total_epochs self.label = label def on_epoch_end(self, epoch, logs=None): logs = logs or {} self.progress( (epoch + 1) / self.total_epochs, desc=( f"{self.label} — Epoch {epoch + 1}/{self.total_epochs} | " f"loss: {logs.get('loss', 0):.4f} | " f"val_loss: {logs.get('val_loss', 0):.4f} | " f"val_acc: {logs.get('val_accuracy', 0):.4f}" ), ) # ── 4. Model Builders ─────────────────────────────────────────────────────── LAYER_UNITS = [512, 512, 256, 256, 128] def build_baseline(): """Large dense network with NO regularization — will overfit.""" model = keras.Sequential( [keras.layers.Input(shape=(784,))] + [keras.layers.Dense(u, activation="relu") for u in LAYER_UNITS] + [keras.layers.Dense(10, activation="softmax")] ) model.compile( optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"], ) return model def build_regularized(use_dropout, use_batchnorm, use_augmentation, use_l1, use_l2, dropout_rate, l1_factor, l2_factor): """Build a model with selected regularization techniques.""" if use_l1 and use_l2: reg = keras.regularizers.l1_l2(l1=l1_factor, l2=l2_factor) elif use_l1: reg = keras.regularizers.l1(l1_factor) elif use_l2: reg = keras.regularizers.l2(l2_factor) else: reg = None if use_augmentation: # CNN model to support spatial data augmentation layers = [keras.layers.Input(shape=(28, 28, 1))] # Data augmentation (only active during training) layers.append(keras.layers.RandomFlip("horizontal")) layers.append(keras.layers.RandomRotation(0.1)) # Conv block 1 layers.append(keras.layers.Conv2D(32, (3, 3), padding="same", kernel_regularizer=reg)) if use_batchnorm: layers.append(keras.layers.BatchNormalization()) layers.append(keras.layers.Activation("relu")) layers.append(keras.layers.MaxPooling2D((2, 2))) if use_dropout: layers.append(keras.layers.Dropout(dropout_rate * 0.5)) # Conv block 2 layers.append(keras.layers.Conv2D(64, (3, 3), padding="same", kernel_regularizer=reg)) if use_batchnorm: layers.append(keras.layers.BatchNormalization()) layers.append(keras.layers.Activation("relu")) layers.append(keras.layers.MaxPooling2D((2, 2))) if use_dropout: layers.append(keras.layers.Dropout(dropout_rate * 0.5)) # Dense head layers.append(keras.layers.Flatten()) layers.append(keras.layers.Dense(256, kernel_regularizer=reg)) if use_batchnorm: layers.append(keras.layers.BatchNormalization()) layers.append(keras.layers.Activation("relu")) if use_dropout: layers.append(keras.layers.Dropout(dropout_rate)) layers.append(keras.layers.Dense(128, kernel_regularizer=reg)) if use_batchnorm: layers.append(keras.layers.BatchNormalization()) layers.append(keras.layers.Activation("relu")) if use_dropout: layers.append(keras.layers.Dropout(dropout_rate * 0.8)) layers.append(keras.layers.Dense(10, activation="softmax")) else: # Dense model (same architecture as baseline) layers = [keras.layers.Input(shape=(784,))] for units in LAYER_UNITS: layers.append(keras.layers.Dense(units, kernel_regularizer=reg)) if use_batchnorm: layers.append(keras.layers.BatchNormalization()) layers.append(keras.layers.Activation("relu")) if use_dropout: layers.append(keras.layers.Dropout(dropout_rate)) layers.append(keras.layers.Dense(10, activation="softmax")) model = keras.Sequential(layers) model.compile( optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"], ) return model # ── 5. Training and Comparison ────────────────────────────────────────────── def train_and_compare(use_dropout, use_batchnorm, use_augmentation, use_l1, use_l2, use_early_stopping, dropout_rate, l1_factor, l2_factor, epochs, progress=gr.Progress()): """Train baseline and regularized models, return comparison plots.""" epochs = int(epochs) # ── Train baseline (always dense, no regularization) ──────────────── progress(0, desc="Training baseline model (no regularization)...") baseline_model = build_baseline() baseline_cb = ProgressCallback(progress, epochs, label="Baseline") baseline_history = baseline_model.fit( x_train_flat, y_train, epochs=epochs, batch_size=128, validation_data=(x_val_flat, y_val), callbacks=[baseline_cb], verbose=0, ) # ── Train regularized ─────────────────────────────────────────────── progress(0, desc="Training regularized model...") reg_model = build_regularized( use_dropout, use_batchnorm, use_augmentation, use_l1, use_l2, dropout_rate, l1_factor, l2_factor, ) # Choose data format based on augmentation rx_train = x_train_img if use_augmentation else x_train_flat rx_val = x_val_img if use_augmentation else x_val_flat rx_test = x_test_img if use_augmentation else x_test_flat reg_callbacks = [ProgressCallback(progress, epochs, label="Regularized")] reg_epochs = epochs if use_early_stopping: reg_callbacks.append(keras.callbacks.EarlyStopping( monitor="val_loss", patience=5, restore_best_weights=True, verbose=0, )) reg_epochs = epochs + 20 # allow extra room for early stopping reg_history = reg_model.fit( rx_train, y_train, epochs=reg_epochs, batch_size=128, validation_data=(rx_val, y_val), callbacks=reg_callbacks, verbose=0, ) # ── Evaluate on test set ──────────────────────────────────────────── progress(1.0, desc="Evaluating on test set...") b_loss, b_acc = baseline_model.evaluate(x_test_flat, y_test, verbose=0) r_loss, r_acc = reg_model.evaluate(rx_test, y_test, verbose=0) bh = baseline_history.history rh = reg_history.history # ── Plot 1: Accuracy and loss comparison ──────────────────────────── fig1, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5)) b_ep = range(1, len(bh["loss"]) + 1) r_ep = range(1, len(rh["loss"]) + 1) ax1.plot(b_ep, bh["accuracy"], "b--", label="Baseline Train", linewidth=1.5) ax1.plot(b_ep, bh["val_accuracy"], "r--", label="Baseline Val", linewidth=1.5) ax1.plot(r_ep, rh["accuracy"], "b-", label="Regularized Train", linewidth=2) ax1.plot(r_ep, rh["val_accuracy"], "r-", label="Regularized Val", linewidth=2) ax1.set_title("Accuracy: Baseline vs Regularized", fontsize=13) ax1.set_xlabel("Epoch") ax1.set_ylabel("Accuracy") ax1.legend() ax1.grid(True, alpha=0.3) ax1.set_ylim([0.7, 1.0]) ax2.plot(b_ep, bh["loss"], "b--", label="Baseline Train", linewidth=1.5) ax2.plot(b_ep, bh["val_loss"], "r--", label="Baseline Val", linewidth=1.5) ax2.plot(r_ep, rh["loss"], "b-", label="Regularized Train", linewidth=2) ax2.plot(r_ep, rh["val_loss"], "r-", label="Regularized Val", linewidth=2) ax2.set_title("Loss: Baseline vs Regularized", fontsize=13) ax2.set_xlabel("Epoch") ax2.set_ylabel("Loss") ax2.legend() ax2.grid(True, alpha=0.3) fig1.suptitle("Before/After Overfitting Comparison", fontsize=15, fontweight="bold") plt.tight_layout() # ── Plot 2: Individual curves side by side ────────────────────────── fig2, axes = plt.subplots(1, 2, figsize=(14, 5)) # Baseline ax = axes[0] ax.plot(b_ep, bh["accuracy"], "b-", label="Train Acc", linewidth=2) ax.plot(b_ep, bh["val_accuracy"], "r-", label="Val Acc", linewidth=2) b_gap = bh["accuracy"][-1] - bh["val_accuracy"][-1] ax.set_title(f"Baseline (No Regularization)\nGap: {b_gap:.4f}", fontsize=12, fontweight="bold") ax.set_xlabel("Epoch") ax.set_ylabel("Accuracy") ax.legend() ax.grid(True, alpha=0.3) ax.set_ylim([0.7, 1.0]) # Regularized ax = axes[1] ax.plot(r_ep, rh["accuracy"], "b-", label="Train Acc", linewidth=2) ax.plot(r_ep, rh["val_accuracy"], "r-", label="Val Acc", linewidth=2) r_gap = rh["accuracy"][-1] - rh["val_accuracy"][-1] ax.set_title(f"Regularized\nGap: {r_gap:.4f}", fontsize=12, fontweight="bold") ax.set_xlabel("Epoch") ax.set_ylabel("Accuracy") ax.legend() ax.grid(True, alpha=0.3) ax.set_ylim([0.7, 1.0]) plt.tight_layout() # ── Build techniques list ─────────────────────────────────────────── techniques = [] if use_dropout: techniques.append(f"Dropout (rate={dropout_rate})") if use_batchnorm: techniques.append("Batch Normalization") if use_augmentation: techniques.append("Data Augmentation (RandomFlip + RandomRotation)") if use_l1: techniques.append(f"L1 Regularization (factor={l1_factor})") if use_l2: techniques.append(f"L2 Regularization (factor={l2_factor})") if use_early_stopping: techniques.append(f"Early Stopping (stopped at epoch {len(rh['loss'])})") if not techniques: techniques.append("None selected — regularized model is same as baseline") # ── Architecture info ─────────────────────────────────────────────── if use_augmentation: arch_info = " Baseline: Dense NN (784 → 512 → 512 → 256 → 256 → 128 → 10)\n Regularized: CNN + Dense (28x28x1 → Conv32 → Conv64 → 256 → 128 → 10)" else: arch_info = f" Both models: Dense NN (784 → {' → '.join(str(u) for u in LAYER_UNITS)} → 10)" # ── Summary text ──────────────────────────────────────────────────── summary = ( f"MODEL ARCHITECTURE\n" f"{'─' * 45}\n" f"{arch_info}\n" f"\n" f"TECHNIQUES APPLIED\n" f"{'─' * 45}\n" + "\n".join(f" - {t}" for t in techniques) + "\n" f"\n" f"BASELINE (Before)\n" f"{'─' * 45}\n" f" Test Accuracy: {b_acc:.4f} ({b_acc * 100:.2f}%)\n" f" Test Loss: {b_loss:.4f}\n" f" Train Accuracy: {bh['accuracy'][-1]:.4f}\n" f" Val Accuracy: {bh['val_accuracy'][-1]:.4f}\n" f" Overfit Gap: {b_gap:.4f}\n" f"\n" f"REGULARIZED (After)\n" f"{'─' * 45}\n" f" Test Accuracy: {r_acc:.4f} ({r_acc * 100:.2f}%)\n" f" Test Loss: {r_loss:.4f}\n" f" Train Accuracy: {rh['accuracy'][-1]:.4f}\n" f" Val Accuracy: {rh['val_accuracy'][-1]:.4f}\n" f" Overfit Gap: {r_gap:.4f}\n" f"\n" f"IMPROVEMENT\n" f"{'─' * 45}\n" f" Test Acc Change: {(r_acc - b_acc) * 100:+.2f}%\n" f" Gap Reduction: {(b_gap - r_gap) * 100:+.2f}%\n" ) return fig1, fig2, summary # ── 6. Gradio Interface ───────────────────────────────────────────────────── demo = gr.Interface( fn=train_and_compare, inputs=[ gr.Checkbox(value=True, label="Dropout"), gr.Checkbox(value=True, label="Batch Normalization"), gr.Checkbox(value=False, label="Data Augmentation (switches to CNN)"), gr.Checkbox(value=False, label="L1 Regularization (sparsity)"), gr.Checkbox(value=True, label="L2 Regularization (weight decay)"), gr.Checkbox(value=True, label="Early Stopping"), gr.Slider(minimum=0.1, maximum=0.7, value=0.5, step=0.05, label="Dropout Rate"), gr.Slider(minimum=0.000001, maximum=0.001, value=0.00001, step=0.000001, label="L1 Factor"), gr.Slider(minimum=0.00001, maximum=0.01, value=0.0001, step=0.00001, label="L2 Factor"), gr.Slider(minimum=5, maximum=40, value=20, step=5, label="Epochs (Baseline)"), ], outputs=[ gr.Plot(label="Before/After Comparison"), gr.Plot(label="Individual Accuracy Curves"), gr.Textbox(label="Results Summary", lines=28), ], flagging_mode="never", title="Overfitting Explorer — Fashion-MNIST", description=( "Compare a baseline dense NN (no regularization) with a regularized model " "on Fashion-MNIST. Toggle regularization techniques on/off to see how " "each one affects overfitting. Enabling Data Augmentation switches the " "regularized model to a CNN architecture to support spatial transforms." ), ) if __name__ == "__main__": demo.launch()