# app.py from flask import Flask, render_template_string, request, jsonify import numpy as np from sklearn.datasets import make_classification, make_regression from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.metrics import ( accuracy_score, precision_score, recall_score, f1_score, mean_squared_error, mean_absolute_error, r2_score ) import torch import torch.nn as nn import torch.optim as optim import time import logging import os app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) HTML_TEMPLATE = ''' ML Loss Function Playground

ML Loss Function Playground

Interactive Tool to Compare Loss Functions

Configure Experiment

Training... please wait

Loss Function Formulas

Mean Squared Error (MSE)
$$ L_{\\text{MSE}} = \\frac{1}{n} \\sum_{i=1}^{n} (y_i - \\hat{y}_i)^2 $$

Best for regression. Sensitive to outliers.

Mean Absolute Error (MAE)
$$ L_{\\text{MAE}} = \\frac{1}{n} \\sum_{i=1}^{n} |y_i - \\hat{y}_i| $$

Robust to outliers. Constant gradient.

Cross-Entropy Loss
$$ L_{\\text{CE}} = -\\sum_{c=1}^{C} y_c \\log(\\hat{p}_c) $$

For multi-class classification with softmax.

Sparse Categorical Cross-Entropy
$$ L = -\\log(\\hat{p}_{y}) $$

Same as CE, but label is integer index.

Binary Cross-Entropy (BCE)
$$ L = -[y \\log(\\sigma(z)) + (1-y) \\log(1-\\sigma(z))] $$

For binary classification with sigmoid.

Huber Loss
$$ L_{\\delta} = \\begin{cases} \\frac{1}{2}(y - \\hat{y})^2 & |y - \\hat{y}| \\leq \\delta \\\\ \\delta(|y - \\hat{y}| - \\frac{1}{2}\\delta) & \\text{otherwise} \\end{cases} $$

Combines MSE and MAE. Robust + smooth.

What You'll Learn

  • Compare loss functions on the same dataset
  • See convergence speed and stability
  • Experiment with noise and outliers
  • Try mismatched loss functions (educational!)
''' class SimpleNN(nn.Module): def __init__(self, input_dim, output_dim, task_type, loss_name, n_hidden_layers, neurons_per_layer): super().__init__() self.task_type = task_type self.loss_name = loss_name layers = [nn.Linear(input_dim, neurons_per_layer), nn.ReLU()] for _ in range(n_hidden_layers - 1): layers.extend([nn.Linear(neurons_per_layer, neurons_per_layer), nn.ReLU()]) layers.append(nn.Linear(neurons_per_layer, output_dim)) self.model = nn.Sequential(*layers) def forward(self, x): return self.model(x) def get_loss_function(name): return { 'mse': nn.MSELoss(), 'mae': nn.L1Loss(), 'cross_entropy': nn.CrossEntropyLoss(), 'sparse_categorical_crossentropy': nn.CrossEntropyLoss(), 'bce': nn.BCEWithLogitsLoss(), 'huber': nn.SmoothL1Loss() }.get(name, nn.MSELoss()) def evaluate_regression(model, X, y): model.eval() with torch.no_grad(): pred = model(X).cpu().numpy() true = y.cpu().numpy() return { 'mse': mean_squared_error(true, pred), 'mae': mean_absolute_error(true, pred), 'r2': r2_score(true, pred) } def evaluate_classification(model, X, y): model.eval() with torch.no_grad(): out = model(X) if model.loss_name == 'bce' and model.task_type == 'binary_classification': pred = (torch.sigmoid(out) > 0.5).long() else: _, pred = torch.max(out, 1) true = y.cpu().numpy() pred = pred.cpu().numpy() return { 'accuracy': accuracy_score(true, pred), 'precision': precision_score(true, pred, average='macro', zero_division=0), 'recall': recall_score(true, pred, average='macro', zero_division=0), 'f1': f1_score(true, pred, average='macro', zero_division=0) } def add_outliers(X, y, n_samples, perc, mag): n = int(n_samples * perc / 100) if n == 0: return X, y idx = np.random.choice(n_samples, n, replace=False) X[idx] *= mag if y.ndim == 2: # regression y[idx] *= mag else: # classification if y.max() > 1: # multi-class y[idx] = np.random.randint(0, y.max() + 1, size=n) else: # binary y[idx] = 1 - y[idx] return X, y def clean_nans(d): if isinstance(d, list): return [0.0 if np.isnan(x) or np.isinf(x) else x for x in d] if isinstance(d, dict): return {k: clean_nans(v) for k, v in d.items()} return d @app.route('/') def index(): return render_template_string(HTML_TEMPLATE) @app.route('/health') def health(): return jsonify({'status': 'healthy'}), 200 @app.route('/train', methods=['POST']) def train(): try: cfg = request.get_json() if not cfg: return jsonify({'error': 'No data'}), 400 task = cfg['task_type'] n_samples = cfg['n_samples'] n_features = cfg['n_features'] noise = cfg['noise_level'] out_p = cfg['outlier_percentage'] out_m = cfg['outlier_magnitude'] layers = cfg['n_hidden_layers'] neurons = cfg['neurons_per_layer'] epochs = cfg['epochs'] losses = cfg['loss_functions'] n_classes = cfg.get('n_classes') # Generate data if task == 'regression': X, y = make_regression(n_samples=n_samples, n_features=n_features, noise=noise, random_state=42) y = y.reshape(-1, 1) output_dim = 1 else: flip = min(1.0, noise / 100) n_cls = 2 if task == 'binary_classification' else n_classes X, y = make_classification(n_samples=n_samples, n_features=n_features, n_classes=n_cls, n_informative=max(2, n_features//2), n_clusters_per_class=1, flip_y=flip, random_state=42) output_dim = n_cls X, y = add_outliers(X, y, n_samples, out_p, out_m) X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42) scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_val = scaler.transform(X_val) X_train_t = torch.FloatTensor(X_train) X_val_t = torch.FloatTensor(X_val) if task == 'regression': y_train_t = torch.FloatTensor(y_train) y_val_t = torch.FloatTensor(y_val) else: y_train_t = torch.LongTensor(y_train) y_val_t = torch.LongTensor(y_val) results = {} for loss_name in losses: out_dim = 1 if task == 'binary_classification' and loss_name == 'bce' else output_dim model = SimpleNN(n_features, out_dim, task, loss_name, layers, neurons) criterion = get_loss_function(loss_name) opt = optim.Adam(model.parameters(), lr=0.001) train_losses = [] val_losses = [] start = time.time() convergence_epoch = None for epoch in range(epochs): model.train() opt.zero_grad() outputs = model(X_train_t) loss = None try: if loss_name in ['cross_entropy', 'sparse_categorical_crossentropy'] and task != 'regression': loss = criterion(outputs, y_train_t) elif loss_name == 'bce' and task == 'binary_classification': loss = criterion(outputs, y_train_t.float().unsqueeze(1)) else: # Mismatched cases if task == 'regression': if loss_name in ['cross_entropy', 'sparse_categorical_crossentropy']: # For mismatched CE on regression, treat as binary for demo logits = torch.cat([outputs, -outputs], dim=1) # Fake 2 classes targets = (y_train_t > 0).long().squeeze(1) loss = criterion(logits, targets) elif loss_name == 'bce': loss = criterion(outputs, (y_train_t > 0).float()) else: loss = criterion(outputs, y_train_t) else: loss = criterion(outputs.mean(dim=1, keepdim=True), y_train_t.float().unsqueeze(1)) if loss is not None: loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) opt.step() except Exception as e: logger.warning(f"Loss computation or backward failed for {loss_name} on {task}: {str(e)}") loss = torch.tensor(float('nan')) model.eval() with torch.no_grad(): val_outputs = model(X_val_t) val_loss = None try: if loss_name in ['cross_entropy', 'sparse_categorical_crossentropy'] and task != 'regression': val_loss = criterion(val_outputs, y_val_t) elif loss_name == 'bce' and task == 'binary_classification': val_loss = criterion(val_outputs, y_val_t.float().unsqueeze(1)) else: if task == 'regression': if loss_name in ['cross_entropy', 'sparse_categorical_crossentropy']: logits = torch.cat([val_outputs, -val_outputs], dim=1) targets = (y_val_t > 0).long().squeeze(1) val_loss = criterion(logits, targets) elif loss_name == 'bce': val_loss = criterion(val_outputs, (y_val_t > 0).float()) else: val_loss = criterion(val_outputs, y_val_t) else: val_loss = criterion(val_outputs.mean(dim=1, keepdim=True), y_val_t.float().unsqueeze(1)) except Exception as e: logger.warning(f"Val loss failed for {loss_name} on {task}: {str(e)}") val_loss = torch.tensor(float('nan')) train_losses.append(float(loss.item()) if loss is not None and not torch.isnan(loss) else float('nan')) val_losses.append(float(val_loss.item()) if val_loss is not None and not torch.isnan(val_loss) else float('nan')) if epoch > 10 and convergence_epoch is None: recent_val = val_losses[-5:] if len(recent_val) == 5 and all(not np.isnan(r) for r in recent_val): if abs(recent_val[-1] - recent_val[0]) / (abs(recent_val[0]) + 1e-8) < 0.001: convergence_epoch = epoch + 1 total_time = time.time() - start if task == 'regression': metrics = evaluate_regression(model, X_val_t, y_val_t) else: metrics = evaluate_classification(model, X_val_t, y_val_t) results[loss_name] = { 'train_losses': train_losses, 'val_losses': val_losses, 'total_time': total_time, 'convergence_epoch': convergence_epoch or epochs, 'metrics': metrics } return jsonify(clean_nans(results)) except Exception as e: logger.error(f"Server error in train: {str(e)}") return jsonify({'error': str(e)}), 500 if __name__ == '__main__': port = int(os.environ.get('PORT', 7860)) app.run(host='0.0.0.0', port=port, debug=False, use_reloader=False, threaded=True)