From 97abbc318aa38d982e91a8b6b00050a3f8c15796 Mon Sep 17 00:00:00 2001 From: Iskken Date: Wed, 8 Jul 2026 02:57:09 +0200 Subject: [PATCH] Add interactive webapp demo for GlassBoxML algorithms - FastAPI server (webapp/app.py) with GET / and POST /run endpoints - Single-page HTML frontend (webapp/static/index.html) using Chart.js: scatter + regression line for LinearRegression, scatter + decision boundary for LogisticRegression, scatter by class for DecisionTree - webapp/requirements.txt (fastapi, uvicorn) - Add DecisionTree.predict(X) convenience method; also convert split() return values to numpy arrays so downstream code gets consistent types Co-Authored-By: Claude Sonnet 4.6 --- glassboxml/models/decision_tree.py | 12 +- webapp/app.py | 132 ++++++++++++++ webapp/requirements.txt | 2 + webapp/static/index.html | 268 +++++++++++++++++++++++++++++ 4 files changed, 412 insertions(+), 2 deletions(-) create mode 100644 webapp/app.py create mode 100644 webapp/requirements.txt create mode 100644 webapp/static/index.html diff --git a/glassboxml/models/decision_tree.py b/glassboxml/models/decision_tree.py index dc9c80f..2930c88 100644 --- a/glassboxml/models/decision_tree.py +++ b/glassboxml/models/decision_tree.py @@ -56,7 +56,12 @@ def split(self, X, y, b_f, b_th): right_data.append(X[i]) y_right.append(y[i]) - return left_data, right_data, y_left, y_right + return ( + np.array(left_data), + np.array(right_data), + np.array(y_left), + np.array(y_right) + ) def calculate_gini(self, y): _, counts = np.unique(y, return_counts=True) @@ -96,10 +101,13 @@ def find_best_split(self, X, y): return best_feature, best_threshold + def predict(self, X): + return np.array([self.predict_sample(x, self.root) for x in X]) + def predict_sample(self, x, node): if isinstance(node, Leaf): return node.value - + if x[node.best_feature] <= node.best_threshold: return self.predict_sample(x, node.left_child) else: diff --git a/webapp/app.py b/webapp/app.py new file mode 100644 index 0000000..baf3e83 --- /dev/null +++ b/webapp/app.py @@ -0,0 +1,132 @@ +from pathlib import Path + +import numpy as np +from fastapi import FastAPI +from fastapi.responses import FileResponse +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel + +from glassboxml.data.generators import ( + generate_classification_dataset, + generate_regression_dataset, +) +from glassboxml.metrics.regression import rmse as rmse_metric +from glassboxml.models.decision_tree import DecisionTree +from glassboxml.models.linear_regression import LinearRegression +from glassboxml.models.logistic_regression import LogisticRegression + +STATIC = Path(__file__).parent / "static" + +app = FastAPI(title="GlassBoxML Demo") +app.mount("/static", StaticFiles(directory=str(STATIC)), name="static") + + +class RunRequest(BaseModel): + algorithm: str + n_samples: int = 200 + noise_std: float = 0.5 + epochs: int = 500 + learning_rate: float = 0.1 + max_depth: int = 3 + + +@app.get("/") +def index(): + return FileResponse(str(STATIC / "index.html")) + + +@app.post("/run") +def run(req: RunRequest): + if req.algorithm == "linear_regression": + return _linear_regression(req) + if req.algorithm == "logistic_regression": + return _logistic_regression(req) + if req.algorithm == "decision_tree": + return _decision_tree(req) + return {"error": f"Unknown algorithm: {req.algorithm}"} + + +def _linear_regression(req: RunRequest): + X, y, _, _ = generate_regression_dataset( + w_true=[2.5], b_true=1.0, + n_samples=req.n_samples, noise_std=req.noise_std, + ) + model = LinearRegression() + model.fit_gradient_descent(X, y, epochs=req.epochs, learning_rate=req.learning_rate) + + y_pred = model.predict(X) + error = float(rmse_metric(y, y_pred)) + + x_line = np.linspace(float(X[:, 0].min()), float(X[:, 0].max()), 60) + y_line = float(model.w[0]) * x_line + float(model.b) + + return { + "scatter": {"x": X[:, 0].tolist(), "y": y.tolist()}, + "line": {"x": x_line.tolist(), "y": y_line.tolist()}, + "metrics": { + "rmse": round(error, 4), + "learned w": round(float(model.w[0]), 4), + "learned b": round(float(model.b), 4), + "true w": 2.5, + "true b": 1.0, + }, + } + + +def _logistic_regression(req: RunRequest): + X, y, _, _ = generate_classification_dataset( + w_true=[1.5, -2.0], b_true=0.5, + n_samples=req.n_samples, noise_std=req.noise_std, + ) + model = LogisticRegression() + model.fit(X, y, epochs=req.epochs, learning_rate=req.learning_rate) + + y_pred = model.predict(X) + accuracy = float(np.mean(y_pred == y)) + + # Decision boundary in 2D: w0*x0 + w1*x1 + b = 0 => x1 = -(w0*x0 + b) / w1 + x0_range = np.linspace(float(X[:, 0].min()), float(X[:, 0].max()), 60) + if abs(float(model.w[1])) > 1e-10: + x1_boundary = -(float(model.w[0]) * x0_range + float(model.b)) / float(model.w[1]) + else: + x1_boundary = np.zeros_like(x0_range) + + return { + "scatter": { + "x1": X[:, 0].tolist(), + "x2": X[:, 1].tolist(), + "labels": y.tolist(), + }, + "boundary": {"x1": x0_range.tolist(), "x2": x1_boundary.tolist()}, + "metrics": { + "accuracy": f"{round(accuracy * 100, 1)}%", + "learned w1": round(float(model.w[0]), 4), + "learned w2": round(float(model.w[1]), 4), + "learned b": round(float(model.b), 4), + }, + } + + +def _decision_tree(req: RunRequest): + X, y, _, _ = generate_classification_dataset( + w_true=[1.5, -2.0], b_true=0.5, + n_samples=req.n_samples, noise_std=req.noise_std, + ) + model = DecisionTree(max_depth=req.max_depth) + model.fit(X, y) + + y_pred = model.predict(X) + accuracy = float(np.mean(y_pred == y)) + + return { + "scatter": { + "x1": X[:, 0].tolist(), + "x2": X[:, 1].tolist(), + "labels": y.tolist(), + "predicted": y_pred.tolist(), + }, + "metrics": { + "accuracy": f"{round(accuracy * 100, 1)}%", + "max_depth": req.max_depth, + }, + } diff --git a/webapp/requirements.txt b/webapp/requirements.txt new file mode 100644 index 0000000..fc7b393 --- /dev/null +++ b/webapp/requirements.txt @@ -0,0 +1,2 @@ +fastapi>=0.100.0 +uvicorn[standard]>=0.23.0 diff --git a/webapp/static/index.html b/webapp/static/index.html new file mode 100644 index 0000000..52aa96a --- /dev/null +++ b/webapp/static/index.html @@ -0,0 +1,268 @@ + + + + + + GlassBoxML Demo + + + + +
+

GlassBoxML — Interactive Demo

+

Train hand-built ML algorithms on synthetic data and inspect the result.

+ +
+ + + +
+ + +
+
+ +
+
+
+
+
+ + + +