# app.py import os import tempfile import uuid import gradio as gr import matplotlib.pyplot as plt import numpy as np import pandas as pd import torch from torch import nn from torch.utils.data import DataLoader, TensorDataset def _pick_device(device_choice: str) -> torch.device: if device_choice == "cuda": return torch.device("cuda" if torch.cuda.is_available() else "cpu") if device_choice == "cpu": return torch.device("cpu") # auto return torch.device("cuda" if torch.cuda.is_available() else "cpu") def make_synthetic_regression(n_samples: int, noise_std: float, seed: int): """ X shape: (n_samples, 10) y = X @ w_true + b_true + noise """ n_features = 10 g = torch.Generator().manual_seed(int(seed)) X = torch.randn(n_samples, n_features, generator=g) w_true = torch.randn(n_features, 1, generator=g) b_true = torch.randn(1, generator=g) noise = noise_std * torch.randn(n_samples, 1, generator=g) y = X @ w_true + b_true + noise # 80/20 split (shuffled) idx = torch.randperm(n_samples, generator=g) n_train = int(round(0.8 * n_samples)) train_idx = idx[:n_train] val_idx = idx[n_train:] X_train, y_train = X[train_idx], y[train_idx] X_val, y_val = X[val_idx], y[val_idx] # Full dataframe for CSV download cols = [f"x{i}" for i in range(n_features)] df = pd.DataFrame(X.numpy(), columns=cols) df["y"] = y.numpy().reshape(-1) split = np.array(["val"] * n_samples, dtype=object) split[train_idx.numpy()] = "train" df["split"] = split # Data preview: first 20 TRAIN rows df_train_preview = df[df["split"] == "train"].head(20).reset_index(drop=True) return (X_train, y_train, X_val, y_val, w_true, b_true, df, df_train_preview) def train_raw_pytorch_loop( X_train: torch.Tensor, y_train: torch.Tensor, X_val: torch.Tensor, y_val: torch.Tensor, lr: float, batch_size: int, epochs: int, seed: int, device: torch.device, ): # Ensure deterministic-ish behavior for model init torch.manual_seed(int(seed) + 12345) model = nn.Linear(10, 1).to(device) loss_fn = nn.MSELoss() optimizer = torch.optim.SGD(model.parameters(), lr=lr) train_loader = DataLoader( TensorDataset(X_train, y_train), batch_size=batch_size, shuffle=True, drop_last=False, ) val_loader = DataLoader( TensorDataset(X_val, y_val), batch_size=batch_size, shuffle=False, drop_last=False, ) train_losses = [] val_losses = [] for _epoch in range(epochs): # ---- TRAIN ---- model.train() running = 0.0 n_seen = 0 for xb, yb in train_loader: xb = xb.to(device) yb = yb.to(device) # Manual training loop steps: optimizer.zero_grad() # 1) zero_grad y_pred = model(xb) # 2) forward loss = loss_fn(y_pred, yb) # 3) loss loss.backward() # 4) backward optimizer.step() # 5) step bs = xb.shape[0] running += loss.item() * bs n_seen += bs train_losses.append(running / max(1, n_seen)) # ---- VAL ---- model.eval() running = 0.0 n_seen = 0 with torch.no_grad(): for xb, yb in val_loader: xb = xb.to(device) yb = yb.to(device) y_pred = model(xb) loss = loss_fn(y_pred, yb) bs = xb.shape[0] running += loss.item() * bs n_seen += bs val_losses.append(running / max(1, n_seen)) return model, train_losses, val_losses def build_weight_comparison(w_true: torch.Tensor, b_true: torch.Tensor, model: nn.Linear): w_learned = model.weight.detach().cpu().numpy().reshape(-1) b_learned = float(model.bias.detach().cpu().numpy().reshape(-1)[0]) w_true_np = w_true.detach().cpu().numpy().reshape(-1) b_true_np = float(b_true.detach().cpu().numpy().reshape(-1)[0]) rows = [] for i in range(10): rows.append( { "param": f"w[{i}] (x{i})", "true": float(w_true_np[i]), "learned": float(w_learned[i]), "abs_error": float(abs(w_true_np[i] - w_learned[i])), } ) rows.append( { "param": "bias (b)", "true": b_true_np, "learned": b_learned, "abs_error": float(abs(b_true_np - b_learned)), } ) return pd.DataFrame(rows) def make_loss_plot(train_losses, val_losses): fig, ax = plt.subplots() xs = np.arange(1, len(train_losses) + 1) ax.plot(xs, train_losses, label="train") ax.plot(xs, val_losses, label="val") ax.set_title("Raw PyTorch Training Loop (Linear Regression)") ax.set_xlabel("Epoch") ax.set_ylabel("MSE Loss") ax.legend() ax.grid(True, alpha=0.3) fig.tight_layout() return fig def run_experiment(n_samples, noise_std, lr, batch_size, epochs, seed, device_choice): # sanitize n_samples = int(n_samples) batch_size = int(batch_size) epochs = int(epochs) seed = int(seed) noise_std = float(noise_std) lr = float(lr) device = _pick_device(device_choice) X_train, y_train, X_val, y_val, w_true, b_true, df_full, df_train_preview = make_synthetic_regression( n_samples=n_samples, noise_std=noise_std, seed=seed, ) model, train_losses, val_losses = train_raw_pytorch_loop( X_train=X_train, y_train=y_train, X_val=X_val, y_val=y_val, lr=lr, batch_size=batch_size, epochs=epochs, seed=seed, device=device, ) fig = make_loss_plot(train_losses, val_losses) w_table = build_weight_comparison(w_true, b_true, model) # Save dataset CSV for download out_path = os.path.join( tempfile.gettempdir(), f"synthetic_regression_{uuid.uuid4().hex}.csv", ) df_full.to_csv(out_path, index=False) summary = ( "Raw PyTorch loop steps used each batch:\n" " optimizer.zero_grad() -> model(x) -> loss_fn(...) -> loss.backward() -> optimizer.step()\n\n" f"Device used: {device.type}\n" f"Samples: {n_samples} (train={int(round(0.8*n_samples))}, val={n_samples-int(round(0.8*n_samples))})\n" f"Noise std: {noise_std}\n" f"LR: {lr}, Batch size: {batch_size}, Epochs: {epochs}, Seed: {seed}\n\n" f"Final train loss: {train_losses[-1]:.6f}\n" f"Final val loss: {val_losses[-1]:.6f}\n" ) return fig, w_table, summary, df_train_preview, out_path def build_ui(): available_devices = ["auto", "cpu"] if torch.cuda.is_available(): available_devices.append("cuda") with gr.Blocks(title="Raw PyTorch Training Loop (Gradio)") as demo: gr.Markdown( """ # Raw PyTorch Training Loop (Linear Regression) This Space generates a fresh synthetic regression dataset each run and trains a `nn.Linear(10, 1)` model using a **manual** PyTorch training loop. """ ) with gr.Tabs(): with gr.Tab("Train & Results"): with gr.Row(): with gr.Column(scale=1): n_samples = gr.Slider( minimum=200, maximum=20000, value=2000, step=100, label="n_samples", ) noise_std = gr.Slider( minimum=0.0, maximum=5.0, value=1.0, step=0.05, label="noise_std", ) lr = gr.Number(value=0.01, label="lr (SGD learning rate)", precision=6) batch_size = gr.Slider( minimum=8, maximum=1024, value=64, step=8, label="batch_size", ) epochs = gr.Slider( minimum=1, maximum=200, value=20, step=1, label="epochs", ) seed = gr.Number(value=42, label="seed", precision=0) device_choice = gr.Dropdown( choices=available_devices, value="auto", label="device (cpu/cuda if available)", ) run_btn = gr.Button("Run training") with gr.Column(scale=2): loss_plot = gr.Plot(label="Loss curve (train vs val)") w_compare = gr.Dataframe( label="w_true vs w_learned (and bias)", interactive=False, wrap=True, ) summary = gr.Textbox( label="Summary", lines=10, interactive=False, ) dataset_file = gr.File( label="Download full dataset CSV (train+val): columns x0..x9, y, split", interactive=False, ) run_btn.click( fn=run_experiment, inputs=[n_samples, noise_std, lr, batch_size, epochs, seed, device_choice], outputs=[loss_plot, w_compare, summary, gr.State(), dataset_file], ) # We need the Data Preview tab to show first 20 training rows. # We'll store it in a hidden state then route it to the other tab via a small helper. train_preview_state = gr.State() def _capture_preview(fig, wtab, summ, preview_df, csv_path): return fig, wtab, summ, preview_df, csv_path, preview_df run_btn.click( fn=_capture_preview, inputs=[loss_plot, w_compare, summary, gr.State(), dataset_file], outputs=[loss_plot, w_compare, summary, gr.State(), dataset_file, train_preview_state], ) with gr.Tab("Data Preview"): gr.Markdown("### First 20 rows from the **training split**") preview_df = gr.Dataframe( label="Training rows (first 20)", interactive=False, wrap=True, ) # Update preview automatically after training run def _show_preview(df): if df is None: return pd.DataFrame(columns=[f"x{i}" for i in range(10)] + ["y", "split"]) return df demo.load(fn=_show_preview, inputs=[train_preview_state], outputs=[preview_df]) # Also allow a manual refresh button (handy on Spaces) refresh = gr.Button("Refresh preview") refresh.click(fn=_show_preview, inputs=[train_preview_state], outputs=[preview_df]) gr.Markdown( """ **Notes** - Dataset is regenerated each run (based on `seed`). - Train/val split is 80/20 and uses `DataLoader`. - Model: `nn.Linear(10,1)`, Loss: `nn.MSELoss()`, Optimizer: `torch.optim.SGD(lr=...)`. """ ) return demo if __name__ == "__main__": demo = build_ui() demo.queue() demo.launch()