Download app.py from mnoorchenar/loss-function-playground: direct link, hf CLI and curl.
- Browser
- Download file 28.3 kB
-
https://huggingface.co/spaces/mnoorchenar/loss-function-playground/resolve/main/app.py
- Command line
-
hf download hf://spaces/mnoorchenar/loss-function-playground/app.py
-
curl -L -o app.py https://huggingface.co/spaces/mnoorchenar/loss-function-playground/resolve/main/app.py
28.3 kB
| # 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 = '''<!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>ML Loss Function Playground</title> | |
| <script src="https://cdn.plot.ly/plotly-latest.min.js"></script> | |
| <script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script> | |
| <script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { font-family: 'Inter', sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; } | |
| .header-banner { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 40px 20px; text-align: center; border-radius: 15px 15px 0 0; box-shadow: 0 4px 15px rgba(0,0,0,0.2); } | |
| .header-banner h1 { font-size: 2.5em; font-weight: 700; margin-bottom: 10px; } | |
| .container { max-width: 1600px; margin: 0 auto; background: white; border-radius: 15px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); overflow: hidden; } | |
| .tab-navigation { display: flex; background: #f8f9fa; border-bottom: 2px solid #e0e0e0; } | |
| .tab-button { flex: 1; padding: 20px; background: transparent; border: none; font-size: 1.1em; font-weight: 600; color: #666; cursor: pointer; transition: all 0.3s ease; border-bottom: 3px solid transparent; } | |
| .tab-button:hover { background: #fff; color: #667eea; } | |
| .tab-button.active { background: white; color: #667eea; border-bottom: 3px solid #667eea; } | |
| .tab-content { display: none; padding: 40px; } | |
| .tab-content.active { display: block; } | |
| .section-title { font-size: 1.8em; color: #333; margin: 30px 0 20px; padding-bottom: 10px; border-bottom: 3px solid #667eea; } | |
| .controls { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 25px; margin-bottom: 30px; } | |
| .control-group { background: #f8f9fa; padding: 20px; border-radius: 10px; border: 2px solid transparent; } | |
| .control-group:hover { border-color: #667eea; } | |
| label { font-weight: 600; margin-bottom: 8px; display: block; } | |
| input, select { width: 100%; padding: 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 15px; } | |
| input:focus, select:focus { border-color: #667eea; outline: none; } | |
| .checkbox-group { display: flex; flex-wrap: wrap; gap: 10px; background: #f8f9fa; padding: 10px; border-radius: 10px; justify-content: flex-start; } | |
| .checkbox-group label { display: flex; align-items: center; gap: 5px; cursor: pointer; padding: 5px; border-radius: 6px; transition: background 0.2s; white-space: nowrap; } | |
| .checkbox-group label:hover { background: #e0e0e0; } | |
| .checkbox-group input[type="checkbox"] { width: 16px; height: 16px; margin: 0; } | |
| button { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 18px 40px; border: none; border-radius: 10px; font-size: 18px; font-weight: 600; margin: 30px auto; display: block; width: fit-content; } | |
| button:disabled { background: #ccc; cursor: not-allowed; } | |
| #loading { display: none; text-align: center; padding: 40px; } | |
| .plot-container { margin: 40px 0; background: white; padding: 25px; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.08); } | |
| .summary-table { width: 100%; border-collapse: collapse; margin-top: 20px; } | |
| .summary-table th, .summary-table td { padding: 15px; text-align: left; border-bottom: 1px solid #eee; } | |
| .summary-table th { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; } | |
| .best-metric { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); color: white; font-weight: 600; padding: 8px; border-radius: 6px; } | |
| .loss-function-card { background: white; padding: 25px; border-radius: 12px; margin: 20px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.1); border-left: 5px solid #667eea; } | |
| .loss-function-card .formula { background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 15px 0; overflow-x: auto; font-family: 'Courier New', monospace; } | |
| .footer { background: #2d3436; color: white; padding: 30px; text-align: center; margin-top: 50px; border-radius: 0 0 15px 15px; } | |
| .footer a { color: #667eea; text-decoration: none; font-weight: 600; } | |
| .footer a:hover { color: #764ba2; text-decoration: underline; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header-banner"> | |
| <h1>ML Loss Function Playground</h1> | |
| <p>Interactive Tool to Compare Loss Functions</p> | |
| </div> | |
| <div class="tab-navigation"> | |
| <button class="tab-button active" onclick="switchTab('playground')">Playground</button> | |
| <button class="tab-button" onclick="switchTab('documentation')">Documentation & Formulas</button> | |
| </div> | |
| <!-- PLAYGROUND TAB --> | |
| <div id="playground-tab" class="tab-content active"> | |
| <h2 class="section-title">Configure Experiment</h2> | |
| <div class="controls"> | |
| <div class="control-group"> | |
| <label>Task Type:</label> | |
| <select id="task_type" onchange="updateLossOptions()"> | |
| <option value="regression">Regression</option> | |
| <option value="binary_classification">Binary Classification</option> | |
| <option value="multi_classification">Multi-class Classification</option> | |
| </select> | |
| </div> | |
| <div class="control-group" id="n_classes_group" style="display:none;"> | |
| <label>Number of Classes:</label> | |
| <input type="number" id="n_classes" value="3" min="3" max="10"> | |
| </div> | |
| <div class="control-group"> | |
| <label>Samples:</label> | |
| <input type="number" id="n_samples" value="1000" min="100" max="10000"> | |
| </div> | |
| <div class="control-group"> | |
| <label>Features:</label> | |
| <input type="number" id="n_features" value="10" min="2" max="50"> | |
| </div> | |
| <div class="control-group"> | |
| <label>Noise Level (%):</label> | |
| <input type="number" id="noise_level" value="10" min="0" max="100" step="0.1"> | |
| </div> | |
| <div class="control-group"> | |
| <label>Outlier %:</label> | |
| <input type="number" id="outlier_percentage" value="1" min="0" max="20" step="0.1"> | |
| </div> | |
| <div class="control-group"> | |
| <label>Outlier Magnitude:</label> | |
| <input type="number" id="outlier_magnitude" value="10" min="1" max="100"> | |
| </div> | |
| <div class="control-group"> | |
| <label>Hidden Layers:</label> | |
| <input type="number" id="n_hidden_layers" value="2" min="1" max="5"> | |
| </div> | |
| <div class="control-group"> | |
| <label>Neurons/Layer:</label> | |
| <input type="number" id="neurons_per_layer" value="32" min="8" max="128"> | |
| </div> | |
| <div class="control-group"> | |
| <label>Epochs:</label> | |
| <input type="number" id="epochs" value="100" min="10" max="500"> | |
| </div> | |
| </div> | |
| <div class="control-group"> | |
| <label>Loss Functions:</label> | |
| <div class="checkbox-group" id="loss_functions"> | |
| <label><input type="checkbox" value="mse"> MSE</label> | |
| <label><input type="checkbox" value="mae"> MAE</label> | |
| <label><input type="checkbox" value="cross_entropy" checked> Cross Entropy</label> | |
| <label><input type="checkbox" value="sparse_categorical_crossentropy"> Sparse Cat CE</label> | |
| <label><input type="checkbox" value="bce"> BCE</label> | |
| <label><input type="checkbox" value="huber"> Huber</label> | |
| </div> | |
| </div> | |
| <button onclick="trainModels()" id="trainBtn">Train Models</button> | |
| <div id="loading"><h3>Training... please wait</h3></div> | |
| <div id="results"></div> | |
| </div> | |
| <!-- DOCUMENTATION TAB --> | |
| <div id="documentation-tab" class="tab-content"> | |
| <h2 class="section-title">Loss Function Formulas</h2> | |
| <div class="loss-function-card"> | |
| <h5>Mean Squared Error (MSE)</h5> | |
| <div class="formula"> | |
| $$ L_{\\text{MSE}} = \\frac{1}{n} \\sum_{i=1}^{n} (y_i - \\hat{y}_i)^2 $$ | |
| </div> | |
| <p>Best for regression. Sensitive to outliers.</p> | |
| </div> | |
| <div class="loss-function-card"> | |
| <h5>Mean Absolute Error (MAE)</h5> | |
| <div class="formula"> | |
| $$ L_{\\text{MAE}} = \\frac{1}{n} \\sum_{i=1}^{n} |y_i - \\hat{y}_i| $$ | |
| </div> | |
| <p>Robust to outliers. Constant gradient.</p> | |
| </div> | |
| <div class="loss-function-card"> | |
| <h5>Cross-Entropy Loss</h5> | |
| <div class="formula"> | |
| $$ L_{\\text{CE}} = -\\sum_{c=1}^{C} y_c \\log(\\hat{p}_c) $$ | |
| </div> | |
| <p>For multi-class classification with softmax.</p> | |
| </div> | |
| <div class="loss-function-card"> | |
| <h5>Sparse Categorical Cross-Entropy</h5> | |
| <div class="formula"> | |
| $$ L = -\\log(\\hat{p}_{y}) $$ | |
| </div> | |
| <p>Same as CE, but label is integer index.</p> | |
| </div> | |
| <div class="loss-function-card"> | |
| <h5>Binary Cross-Entropy (BCE)</h5> | |
| <div class="formula"> | |
| $$ L = -[y \\log(\\sigma(z)) + (1-y) \\log(1-\\sigma(z))] $$ | |
| </div> | |
| <p>For binary classification with sigmoid.</p> | |
| </div> | |
| <div class="loss-function-card"> | |
| <h5>Huber Loss</h5> | |
| <div class="formula"> | |
| $$ 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} $$ | |
| </div> | |
| <p>Combines MSE and MAE. Robust + smooth.</p> | |
| </div> | |
| <!-- What You'll Learn --> | |
| <div class="info-section" style="margin-top: 40px; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white; padding: 25px; border-radius: 12px;"> | |
| <h3>What You'll Learn</h3> | |
| <ul style="list-style-type: disc; padding-left: 20px; margin-top: 10px;"> | |
| <li>Compare loss functions on the same dataset</li> | |
| <li>See convergence speed and stability</li> | |
| <li>Experiment with noise and outliers</li> | |
| <li>Try mismatched loss functions (educational!)</li> | |
| </ul> | |
| </div> | |
| </div> | |
| <!-- Footer --> | |
| <div class="footer"> | |
| <p> | |
| <strong>© 2025 Mohammad Noorchenarboo</strong> | | |
| <a href="https://www.linkedin.com/in/mnoorchenar" target="_blank">LinkedIn Profile</a> | |
| </p> | |
| <p style="margin-top: 10px; font-size: 0.85em;"> | |
| ⚖️ <strong>Copyright Notice:</strong> All rights reserved. Unauthorized copying, reproduction, or distribution of this application is strictly prohibited. | |
| </p> | |
| <p style="margin-top: 8px; font-size: 0.8em; opacity: 0.8;"> | |
| This application is provided for educational and research purposes only. | |
| </p> | |
| </div> | |
| </div> | |
| <script> | |
| function switchTab(tabName) { | |
| document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active')); | |
| document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active')); | |
| document.getElementById(tabName + '-tab').classList.add('active'); | |
| event.target.classList.add('active'); | |
| if (tabName === 'documentation' && window.MathJax) { | |
| MathJax.typesetPromise(); | |
| } | |
| } | |
| function updateLossOptions() { | |
| const taskType = document.getElementById('task_type').value; | |
| document.getElementById('n_classes_group').style.display = (taskType === 'multi_classification') ? 'block' : 'none'; | |
| const checkboxes = document.querySelectorAll('#loss_functions input'); | |
| checkboxes.forEach(cb => cb.checked = false); | |
| if (taskType === 'regression') { | |
| document.querySelector('input[value="mse"]').checked = true; | |
| } else { | |
| document.querySelector('input[value="cross_entropy"]').checked = true; | |
| if (taskType === 'binary_classification') { | |
| document.querySelector('input[value="bce"]').checked = true; | |
| } | |
| } | |
| } | |
| async function trainModels() { | |
| const config = { | |
| task_type: document.getElementById('task_type').value, | |
| n_samples: parseInt(document.getElementById('n_samples').value), | |
| n_features: parseInt(document.getElementById('n_features').value), | |
| noise_level: parseFloat(document.getElementById('noise_level').value), | |
| outlier_percentage: parseFloat(document.getElementById('outlier_percentage').value), | |
| outlier_magnitude: parseFloat(document.getElementById('outlier_magnitude').value), | |
| n_hidden_layers: parseInt(document.getElementById('n_hidden_layers').value), | |
| neurons_per_layer: parseInt(document.getElementById('neurons_per_layer').value), | |
| epochs: parseInt(document.getElementById('epochs').value), | |
| loss_functions: Array.from(document.querySelectorAll('#loss_functions input:checked')).map(cb => cb.value) | |
| }; | |
| if (config.task_type === 'multi_classification') { | |
| config.n_classes = parseInt(document.getElementById('n_classes').value); | |
| } | |
| if (config.loss_functions.length === 0) { | |
| alert('Please select at least one loss function'); | |
| return; | |
| } | |
| document.getElementById('trainBtn').disabled = true; | |
| document.getElementById('loading').style.display = 'block'; | |
| document.getElementById('results').innerHTML = ''; | |
| try { | |
| const response = await fetch('/train', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify(config) | |
| }); | |
| const data = await response.json(); | |
| if (data.error) throw new Error(data.error); | |
| displayResults(data, config.epochs, config.task_type); | |
| } catch (error) { | |
| document.getElementById('results').innerHTML = `<div class="error-message">Error: ${error.message}</div>`; | |
| } finally { | |
| document.getElementById('trainBtn').disabled = false; | |
| document.getElementById('loading').style.display = 'none'; | |
| } | |
| } | |
| function displayResults(data, epochs, taskType) { | |
| let html = '<h2 class="section-title">Results</h2>'; | |
| Object.keys(data).forEach(lossName => { | |
| html += ` | |
| <div class="plot-container"> | |
| <h3>${lossName.toUpperCase()} Loss</h3> | |
| <div id="plot_${lossName}"></div> | |
| </div> | |
| `; | |
| }); | |
| html += ` | |
| <div class="plot-container"> | |
| <h3>Total Training Time</h3> | |
| <div id="timePlot"></div> | |
| </div> | |
| <h2 class="section-title">Performance Metrics</h2> | |
| <table class="summary-table" id="summaryTable"></table> | |
| `; | |
| document.getElementById('results').innerHTML = html; | |
| Object.keys(data).forEach(lossName => { | |
| const d = data[lossName]; | |
| const x = Array.from({length: epochs}, (_, i) => i + 1); | |
| Plotly.newPlot(`plot_${lossName}`, [ | |
| { x, y: d.train_losses, name: 'Train Loss', line: {color: '#667eea', width: 2} }, | |
| { x, y: d.val_losses, name: 'Val Loss', line: {color: '#f5576c', width: 2, dash: 'dot'} } | |
| ], { | |
| title: `${lossName.toUpperCase()} Train and Val Loss`, | |
| xaxis: {title: 'Epoch'}, | |
| yaxis: {title: 'Loss', type: 'linear'}, // Changed to linear for better visibility of small values | |
| height: 400 | |
| }); | |
| }); | |
| const lossNames = Object.keys(data); | |
| const times = lossNames.map(name => data[name].total_time); | |
| Plotly.newPlot('timePlot', [{ | |
| x: lossNames, | |
| y: times, | |
| type: 'bar', | |
| marker: {color: '#667eea'} | |
| }], { | |
| title: 'Total Training Time per Loss Function', | |
| xaxis: {title: 'Loss Function'}, | |
| yaxis: {title: 'Time (seconds)'}, | |
| height: 400 | |
| }); | |
| let tableHTML = '<tr><th>Loss Function</th><th>Convergence Epoch</th><th>Total Time (s)</th>'; | |
| if (taskType === 'regression') { | |
| tableHTML += '<th>MSE</th><th>MAE</th><th>R²</th></tr>'; | |
| } else { | |
| tableHTML += '<th>Accuracy</th><th>Precision</th><th>Recall</th><th>F1 Score</th></tr>'; | |
| } | |
| Object.keys(data).forEach(lossName => { | |
| const result = data[lossName]; | |
| const metrics = result.metrics; | |
| tableHTML += `<tr> | |
| <td><strong>${lossName}</strong></td> | |
| <td>${result.convergence_epoch}</td> | |
| <td>${result.total_time.toFixed(3)}</td> | |
| `; | |
| if (taskType === 'regression') { | |
| tableHTML += ` | |
| <td>${metrics.mse.toFixed(4)}</td> | |
| <td>${metrics.mae.toFixed(4)}</td> | |
| <td>${metrics.r2.toFixed(4)}</td> | |
| `; | |
| } else { | |
| tableHTML += ` | |
| <td>${metrics.accuracy.toFixed(4)}</td> | |
| <td>${metrics.precision.toFixed(4)}</td> | |
| <td>${metrics.recall.toFixed(4)}</td> | |
| <td>${metrics.f1.toFixed(4)}</td> | |
| `; | |
| } | |
| tableHTML += '</tr>'; | |
| }); | |
| document.getElementById('summaryTable').innerHTML = tableHTML; | |
| } | |
| </script> | |
| </body> | |
| </html>''' | |
| 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 | |
| def index(): | |
| return render_template_string(HTML_TEMPLATE) | |
| def health(): | |
| return jsonify({'status': 'healthy'}), 200 | |
| 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) |