Spaces:
Running
Running
Download predict.js from mrfakename/kumo-tabular-webgpu: direct link, hf CLI and curl.
- Browser
- Download file 6.5 kB
-
https://huggingface.co/spaces/mrfakename/kumo-tabular-webgpu/resolve/main/predict.js
- Command line
-
hf download hf://spaces/mrfakename/kumo-tabular-webgpu/predict.js
-
curl -L -o predict.js https://huggingface.co/spaces/mrfakename/kumo-tabular-webgpu/resolve/main/predict.js
6.5 kB
| // Runtime-agnostic prediction pipeline: `ort` is onnxruntime-web (browser) or | |
| // onnxruntime-node (tests); `getSession(task)` returns an InferenceSession. | |
| import { | |
| prepareFeatures, buildMembers, prepareClassification, prepareRegression, | |
| trimmedMean, softmax, makeRng, isMissing, parseNumber, isNumericColumn, | |
| } from "./kumo.js"; | |
| export const LIMITS = { context: 5000, query: 20000, classes: 10, chunk: 256 }; | |
| export async function predict({ ort, getSession, ctx, qry, target, task, numEstimators, seed = 0, onProgress = () => {} }) { | |
| if (!ctx.columns.includes(target)) throw new Error(`Target "${target}" not found`); | |
| const features = ctx.columns.filter((c) => c !== target); | |
| if (!features.length) throw new Error("Need at least one feature column besides the target"); | |
| const labeled = ctx.rows.filter((r) => !isMissing(r[target])); | |
| const unlabeled = ctx.rows.filter((r) => isMissing(r[target])); | |
| const queryRows = (qry ? qry.rows : []).concat(unlabeled); | |
| if (!queryRows.length) throw new Error("Nothing to predict: add a query CSV or leave the target blank on some rows"); | |
| if (labeled.length < 2) throw new Error("Need at least 2 labeled context rows"); | |
| if (labeled.length > LIMITS.context) throw new Error(`Browser demo limit: ${LIMITS.context} labeled context rows`); | |
| if (queryRows.length > LIMITS.query) throw new Error(`Browser demo limit: ${LIMITS.query} query rows`); | |
| const yVals = labeled.map((r) => r[target]); | |
| if (task === "auto") { | |
| const numeric = isNumericColumn(yVals); | |
| task = numeric && new Set(yVals.map(Number)).size > 10 ? "regression" : "classification"; | |
| } | |
| if (task === "regression" && yVals.some((v) => !Number.isFinite(parseNumber(v)))) throw new Error("Regression needs a numeric target"); | |
| const E = numEstimators; | |
| const rng = makeRng(seed + 1); | |
| // RNG order mirrors sdm: target first, then feature flips, then column shuffle. | |
| const tgt = task === "classification" ? prepareClassification(yVals, rng, E) : prepareRegression(yVals, rng, E); | |
| if (task === "classification") { | |
| if (tgt.K < 2) throw new Error("Classification needs at least 2 distinct target values"); | |
| if (tgt.K > LIMITS.classes) throw new Error(`Browser demo supports up to ${LIMITS.classes} classes (got ${tgt.K})`); | |
| } | |
| onProgress("Preprocessing…", 0); | |
| const prep = prepareFeatures(features, labeled, queryRows); | |
| if (!prep.columns.length) throw new Error("All feature columns are constant in the context rows"); | |
| const members = buildMembers(prep, E, rng); | |
| const session = await getSession(task); | |
| const n = prep.n, nq = queryRows.length, chunk = LIMITS.chunk; | |
| const nChunks = Math.ceil(nq / chunk); | |
| const outW = task === "classification" ? tgt.K : 999; | |
| const perMember = members.map(() => new Float64Array(nq * outW)); | |
| let step = 0; | |
| for (let m = 0; m < E; m++) { | |
| const { x, F, mask } = members[m]; | |
| const yT = task === "classification" | |
| ? new ort.Tensor("int64", tgt.yFor(m), [n]) | |
| : new ort.Tensor("float32", tgt.yFor(m), [n]); | |
| const maskT = new ort.Tensor("bool", mask, [F]); | |
| for (let c = 0; c < nChunks; c++) { | |
| const q0 = c * chunk, q1 = Math.min(nq, q0 + chunk), R = n + (q1 - q0); | |
| const xb = new Float32Array(R * F); | |
| xb.set(x.subarray(0, n * F), 0); | |
| xb.set(x.subarray((n + q0) * F, (n + q1) * F), n * F); | |
| const out = await session.run({ x: new ort.Tensor("float32", xb, [R, F]), y: yT, categorical_mask: maskT }); | |
| const o = out.logits.data, W = out.logits.dims[1]; | |
| for (let i = 0; i < q1 - q0; i++) { | |
| const row = o.subarray(i * W, (i + 1) * W); | |
| const v = task === "classification" ? tgt.unshift(m, row) : tgt.invert(m, row); | |
| perMember[m].set(v.subarray(0, outW), (q0 + i) * outW); | |
| } | |
| step++; | |
| onProgress(`Estimator ${m + 1}/${E} · chunk ${c + 1}/${nChunks}`, step / (E * nChunks)); | |
| } | |
| } | |
| // Post-process | |
| const columns = [], rows = []; | |
| const truthPresent = queryRows.some((r) => target in r && !isMissing(r[target])); | |
| let metrics = ""; | |
| if (task === "classification") { | |
| const names = tgt.classes; | |
| columns.push(`predicted_${target}`, ...(truthPresent ? [`true_${target}`] : []), "confidence", ...features, ...names.map((c) => `p(${c})`)); | |
| let correct = 0, scored = 0; | |
| for (let i = 0; i < nq; i++) { | |
| const avg = Array.from({ length: tgt.K }, (_, k) => members.reduce((s, _m, m) => s + perMember[m][i * tgt.K + k], 0) / E); | |
| const p = softmax(avg); | |
| let best = 0; for (let k = 1; k < p.length; k++) if (p[k] > p[best]) best = k; | |
| const row = { [`predicted_${target}`]: names[best], confidence: +p[best].toFixed(4) }; | |
| if (truthPresent) { | |
| const t = queryRows[i][target] ?? ""; | |
| row[`true_${target}`] = t; | |
| if (!isMissing(t)) { scored++; if (String(t) === names[best] || (Number(t) === Number(names[best]) && t !== "")) correct++; } | |
| } | |
| for (const f of features) row[f] = queryRows[i][f] ?? ""; | |
| names.forEach((c, k) => (row[`p(${c})`] = +p[k].toFixed(4))); | |
| rows.push(row); | |
| } | |
| if (scored) metrics = `accuracy ${(correct / scored).toFixed(3)} on ${scored} labeled query rows`; | |
| } else { | |
| columns.push(`predicted_${target}`, ...(truthPresent ? [`true_${target}`] : []), "p10", "p90", ...features); | |
| const pick = (i, qi) => trimmedMean(members.map((_m, m) => perMember[m][i * 999 + qi])); | |
| let se = 0, ae = 0, cov = 0, scored = 0; const ts = []; | |
| for (let i = 0; i < nq; i++) { | |
| const med = pick(i, 499), p10 = pick(i, 99), p90 = pick(i, 899); | |
| const row = { [`predicted_${target}`]: +med.toPrecision(6), p10: +p10.toPrecision(6), p90: +p90.toPrecision(6) }; | |
| if (truthPresent) { | |
| const t = queryRows[i][target] ?? ""; | |
| row[`true_${target}`] = t; | |
| const tv = parseNumber(t); | |
| if (Number.isFinite(tv)) { scored++; se += (med - tv) ** 2; ae += Math.abs(med - tv); cov += tv >= p10 && tv <= p90; ts.push([tv, med]); } | |
| } | |
| for (const f of features) row[f] = queryRows[i][f] ?? ""; | |
| rows.push(row); | |
| } | |
| if (scored) { | |
| const mt = ts.reduce((a, [t]) => a + t, 0) / scored, sst = ts.reduce((a, [t]) => a + (t - mt) ** 2, 0); | |
| metrics = `RMSE ${Math.sqrt(se / scored).toFixed(3)} · MAE ${(ae / scored).toFixed(3)} · R² ${(1 - se / sst).toFixed(3)} · 80% interval coverage ${(cov / scored).toFixed(2)} on ${scored} rows`; | |
| } | |
| } | |
| return { task, columns, rows, metrics, nContext: n, nQuery: nq, nFeatures: prep.columns.length }; | |
| } | |