Download backend.py from elvis-hf/ensemble: direct link, hf CLI and curl.
- Browser
- Download file 6.21 kB
-
https://huggingface.co/spaces/elvis-hf/ensemble/resolve/main/backend.py
- Command line
-
hf download hf://spaces/elvis-hf/ensemble/backend.py
-
curl -L -o backend.py https://huggingface.co/spaces/elvis-hf/ensemble/resolve/main/backend.py
6.21 kB
| """Incremental multiclass scikit-learn ensembles in Pyodide.""" | |
| import inspect | |
| import json | |
| import warnings | |
| import numpy as np | |
| from sklearn.tree import DecisionTreeClassifier | |
| from sklearn.ensemble import RandomForestClassifier, ExtraTreesClassifier | |
| from sklearn.linear_model import LogisticRegression, SGDClassifier, Perceptron, RidgeClassifier | |
| from sklearn.svm import SVC, LinearSVC | |
| from sklearn.naive_bayes import GaussianNB | |
| from sklearn.neighbors import KNeighborsClassifier | |
| from sklearn.discriminant_analysis import LinearDiscriminantAnalysis, QuadraticDiscriminantAnalysis | |
| from sklearn.neural_network import MLPClassifier | |
| CLASSIFIERS = {cls.__name__: cls for cls in ( | |
| DecisionTreeClassifier, LogisticRegression, SGDClassifier, Perceptron, | |
| RidgeClassifier, SVC, LinearSVC, GaussianNB, KNeighborsClassifier, | |
| LinearDiscriminantAnalysis, QuadraticDiscriminantAnalysis, MLPClassifier, | |
| )} | |
| FORESTS = {"random_forest": RandomForestClassifier, "extra_trees": ExtraTreesClassifier} | |
| GRID_SIZE = 160 | |
| axis = np.linspace(0, 1, GRID_SIZE) | |
| gx, gy = np.meshgrid(axis, axis) | |
| GRID = np.column_stack((gx.ravel(), gy.ravel())) | |
| history = [] | |
| configuration = None | |
| def catalog(): | |
| return [{"name": name, "weighted": "sample_weight" in inspect.signature(cls.fit).parameters} | |
| for name, cls in CLASSIFIERS.items()] | |
| def train(request): | |
| global configuration, history | |
| seed = request.get("seed", 42) | |
| if type(seed) is not int or not 0 <= seed <= 2**32 - 1: | |
| raise ValueError("Random seed must be an integer from 0 to 4294967295.") | |
| params = request.get("params", {}) | |
| if not isinstance(params, dict): | |
| raise ValueError('Arguments must be a JSON object, for example {"max_depth": 2}.') | |
| method = request["method"] | |
| forest = method in FORESTS | |
| boosting = method == "boosting" | |
| if method not in (*FORESTS, "boosting", "bagging"): | |
| raise ValueError("Unknown ensemble method.") | |
| if forest and any(k in params for k in ("n_estimators", "warm_start")): | |
| raise ValueError("Use Add/Remove to set the number of trees; omit n_estimators and warm_start.") | |
| cls = FORESTS[method] if forest else CLASSIFIERS[request["classifier"]] | |
| estimator = cls(**params) | |
| if boosting and "sample_weight" not in inspect.signature(cls.fit).parameters: | |
| raise ValueError(f"{cls.__name__} does not support sample_weight. Select bagging or another classifier.") | |
| X = np.array([[p["x"], p["y"]] for p in request["points"]], dtype=float) | |
| y = np.array([p["label"] for p in request["points"]]) | |
| classes = np.unique(y) | |
| if len(y) > 1500 or len(classes) < 2 or not set(classes).issubset({-1, 1, 2}): | |
| raise ValueError("Draw at least two classes, with at most 1,500 points.") | |
| key = json.dumps({**{k: request[k] for k in ("points", "classifier", "params", "method")}, "seed": seed}, sort_keys=True) | |
| if key != configuration: | |
| history, configuration = [], key | |
| count = request["count"] | |
| if not 0 <= count <= len(history) or count >= 100: | |
| raise ValueError("Ensemble state changed. Reset the dataset and try again.") | |
| history = history[:count] | |
| # Each model has a repeatable stream, including after Remove then Add. | |
| rng = np.random.default_rng(np.random.SeedSequence([seed, count])) | |
| if not forest and "random_state" in estimator.get_params() and params.get("random_state") is None: | |
| estimator.set_params(random_state=int(rng.integers(2**31 - 1))) | |
| with warnings.catch_warnings(record=True) as caught: | |
| warnings.simplefilter("always") | |
| if forest: | |
| options = dict(params) | |
| if options.get("random_state") is None: | |
| options["random_state"] = seed | |
| estimator = cls(n_estimators=count + 1, warm_start=True, **options) | |
| estimator.estimators_ = [m["estimator"] for m in history] | |
| estimator.fit(X, y) | |
| learner = estimator.estimators_[-1] | |
| training_probabilities = learner.predict_proba(X) | |
| grid_probabilities = learner.predict_proba(GRID) | |
| predictions = classes[training_probabilities.argmax(axis=1)] | |
| grid = classes[grid_probabilities.argmax(axis=1)] | |
| alpha, perfect = 1.0, False | |
| else: | |
| # SAMME: only mistakes increase a sample's log weight. | |
| logs = sum((m["alpha"] * (m["predictions"] != y) for m in history), np.zeros(len(y))) | |
| weights = np.exp(logs - logs.max()) | |
| weights /= weights.sum() | |
| if boosting: | |
| estimator.fit(X, y, sample_weight=weights) | |
| else: | |
| indices = rng.integers(len(y), size=len(y)) | |
| estimator.fit(X[indices], y[indices]) | |
| learner = estimator | |
| predictions = learner.predict(X) | |
| error = float(weights @ (predictions != y)) | |
| if boosting and error >= 1 - 1 / len(classes) - 1e-12: | |
| raise ValueError("This learner cannot beat chance with the current weights. Try different classifier arguments or another classifier.") | |
| clipped = max(error, 1e-12) | |
| alpha = float(np.log((1 - clipped) / clipped) + np.log(len(classes) - 1)) if boosting else 1.0 | |
| perfect = boosting and error < 1e-12 | |
| grid = learner.predict(GRID) | |
| result = {"classes": classes.astype(int).tolist(), "forest": forest, "alpha": alpha, | |
| "perfect": perfect, "predictions": predictions.astype(int).tolist(), | |
| "tree": {"size": GRID_SIZE, "grid": grid.astype(int).tolist()}, | |
| "warning": " ".join(dict.fromkeys(str(w.message) for w in caught))} | |
| if forest: | |
| result["trainingProbabilities"] = training_probabilities.tolist() | |
| result["tree"]["probabilities"] = grid_probabilities.tolist() | |
| # Commit only after all prediction and serialization work succeeds. | |
| json.dumps(result, allow_nan=False) | |
| history.append({"estimator": learner, "alpha": alpha, "predictions": predictions}) | |
| return result | |
| def handle_request(payload): | |
| try: | |
| return json.dumps({"model": train(json.loads(payload))}, allow_nan=False) | |
| except Exception as exc: | |
| return json.dumps({"error": str(exc)}) | |