Spaces:
Running
Running
File size: 5,225 Bytes
cf8ab7a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | """Write src/sklearn_params.json: every setting of each model in models.json,
read from scikit-learn's parameter constraints and docstrings.
Run it with the scikit-learn version Pyodide ships (see the PYODIDE_SKLEARN
note in the README), from the backend folder:
uv run --isolated --no-project --python 3.12 --with "scikit-learn==1.4.2" \\
--with "numpy<2" python scripts/generate_sklearn_params.py
"""
import json
import re
import sys
from numbers import Integral
from pathlib import Path
import sklearn
from sklearn.utils._param_validation import Hidden, Interval, StrOptions
SRC = Path(__file__).resolve().parent.parent / "src"
sys.path.insert(0, str(SRC))
from logic import ESTIMATORS, MODEL_SPECS # noqa: E402
# Settings that don't change what is learned, or are gone in newer scikit-learn
EXCLUDED = {"n_jobs", "verbose", "warm_start", "multi_class"}
# "precomputed" needs a matrix students can't give from the page
EXCLUDED_OPTIONS = {"precomputed"}
def describe(constraints: list) -> dict | None:
"""The control for a setting, or None if students can't set it from the page
(e.g. it only takes callables, dicts or arrays)."""
strings, none, boolean, intervals = [], False, False, []
for c in constraints:
if c is None:
none = True
elif c == "boolean":
boolean = True
elif c == "random_state":
# A seed; scikit-learn also takes a RandomState object
intervals.append(Interval(Integral, 0, 2**32 - 1, closed="both"))
none = True
elif isinstance(c, StrOptions):
strings += sorted(c.options - c.deprecated - EXCLUDED_OPTIONS)
elif isinstance(c, Interval):
intervals.append(c)
elif isinstance(c, Hidden):
continue
# Anything else (callable, dict, array-like, ...) has no control
if boolean and not (strings or none or intervals):
return {"kind": "boolean"}
number = None
if intervals:
integer = all(i.type is Integral for i in intervals)
number = {"integer": integer}
lows = [i for i in intervals if i.left is not None]
highs = [i for i in intervals if i.right is not None]
# Unbounded if any interval is; open if the bounding interval is
if len(lows) == len(intervals):
low = min(lows, key=lambda i: i.left)
number["min"] = low.left
if low.closed in ("right", "neither"):
number["minExclusive"] = True
if len(highs) == len(intervals):
high = max(highs, key=lambda i: i.right)
number["max"] = high.right
if high.closed in ("left", "neither"):
number["maxExclusive"] = True
if number and not strings:
spec = {"kind": "integer" if number.pop("integer") else "number", **number}
if none:
spec["nullable"] = "None"
return spec
if strings and not number and not none:
return {"kind": "choice", "options": [{"value": s, "label": s} for s in strings]}
options = [{"value": s, "label": s} for s in strings] + ([{"value": None, "label": "None"}] if none else [])
if len(options) + (number is not None) < 2:
return None
spec = {"kind": "mixed", "options": options}
if number:
spec["number"] = number
return spec
def descriptions(estimator) -> dict[str, str]:
"""First sentence of each setting's description in the docstring."""
doc = estimator.__doc__ or ""
section = doc.split("Parameters\n ----------\n", 1)[-1]
section = re.split(r"\n \w[\w ]*\n -{3,}\n", section, maxsplit=1)[0]
result = {}
for match in re.finditer(r"^ (\w+) : .*\n((?:(?: {8}.*)?\n)+)", section, re.M):
paragraph = " ".join(match.group(2).strip().split("\n\n")[0].split())
sentence = re.split(r"(?<=\.)\s(?=[A-Z])", paragraph, maxsplit=1)[0]
result[match.group(1)] = clean_rst(sentence)
return result
def clean_rst(text: str) -> str:
text = re.sub(r":\w+:`~?([^`<]*?)\s*(?:<[^>]*>)?`", lambda m: m.group(1).split(".")[-1], text)
return text.replace("``", "").replace("`", "")
def generate() -> dict:
models = {}
for spec in MODEL_SPECS.values():
estimator = ESTIMATORS[spec["estimator"]]
defaults = estimator().get_params()
texts = descriptions(estimator)
params = []
for name, constraints in estimator._parameter_constraints.items():
if name in EXCLUDED:
continue
control = describe(constraints)
if control is None:
continue
params.append({
"name": name,
"label": name if len(name) <= 2 else name.replace("_", " ").capitalize(),
"description": texts.get(name, ""),
**control,
"default": defaults[name],
})
models[spec["id"]] = params
return {"sklearn_version": sklearn.__version__, "models": models}
if __name__ == "__main__":
out = SRC / "sklearn_params.json"
out.write_text(json.dumps(generate(), indent=2) + "\n")
print(f"Wrote {out} (scikit-learn {sklearn.__version__})")
|