Tabular (scikit-learn no navegador)
Modelo de scikit-learn respondendo no navegador, sem servidor de
inferência e sem enviar os dados do usuário pra lugar nenhum. O subpath
tempest-react-sdk/tabular roda o .onnx que o
tempest-fastapi-sdk
exporta — mesmo arquivo, mesmo contrato, no cliente em vez de num
dispositivo.
import { TabularPredictor } from "tempest-react-sdk/tabular";
const predictor = await TabularPredictor.create("/models/classifier.onnx");
const { labels, probabilities } = await predictor.predict([[5.1, 3.5, 1.4, 0.2]]);
console.log(labels[0], probabilities[0]);
0 [0.6662, 0.1061, 0.2277]
Antes de tudo: de onde vem o modelo
Este módulo não treina nada. Ele executa um modelo que já foi treinado — em Python, pela sua equipe de dados — e exportado para um formato que o navegador consegue ler.
O caminho inteiro tem duas metades:
flowchart LR
A[Python: modelo treinado] -->|edge_pipeline| B[pasta com o modelo]
B -->|você publica junto do app| C[public/models/risco/]
C -->|loadEdgePackage| D[React: predição no navegador]
Metade 1 — Python, uma vez por versão do modelo (roda como está, o dataset vem no scikit-learn):
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier
from tempest_fastapi_sdk.modelops import edge_pipeline
data = load_iris()
model = RandomForestClassifier(n_estimators=20, max_depth=4, random_state=0)
model.fit(data.data, data.target)
edge_pipeline(
model,
data.data,
"public/models/flores", # dentro do seu app React
name="flores",
labels=data.target,
feature_names=list(data.feature_names),
compact=True, # a versão que dispensa runtime
)
Isso escreve uma pasta, não um arquivo:
public/models/flores/
├── flores.onnx o modelo em ONNX
├── flores.tmc o mesmo modelo, formato compacto
├── baseline.json referência dos dados de treino
└── manifest.json o que tem aí dentro
Metade 2 — React, no navegador. Se o pacote tem a versão compacta
(compact=True acima), não há nada a instalar além do próprio SDK:
import { loadEdgePackage } from "tempest-react-sdk/tabular";
const pkg = await loadEdgePackage("/models/flores/");
console.log(pkg.featureNames);
// ["sepal length (cm)", "sepal width (cm)", "petal length (cm)", "petal width (cm)"]
const { labels, probabilities } = await pkg.predictor.predict([[5.1, 3.5, 1.4, 0.2]]);
console.log(labels[0], probabilities[0]);
A ordem das colunas vem junto
pkg.featureNames diz em que ordem os valores têm que entrar. Use isso
para montar a linha a partir do seu formulário — modelo alimentado com
os números certos na ordem errada responde com confiança e errado, e
não existe checagem em runtime que pegue.
Só tenho um .onnx solto, sem pasta
Funciona também — TabularPredictor.create("/models/classifier.onnx").
Você perde o que o manifesto carrega (ordem das colunas, nomes das
classes, versão), e passa a precisar do onnxruntime-web.
Três rotas, você escolhe
Rodar sklearn no navegador tem um custo que não é o modelo. Medido:
| Tamanho | |
|---|---|
Runtime onnxruntime-web (.wasm) |
25,6 MB — 6,0 MB gzipped |
| Floresta de 12 árvores em ONNX | 20 KB |
| A mesma em formato compacto | 9,6 KB |
| O leitor compacto (código do SDK) | 1,49 KB brotli |
O modelo é ruído. O runtime é a conta. Daí existirem três caminhos, e o certo depender do que o seu app já carrega.
A — Sem runtime (CompactPredictor)
# Python, no build
package = edge_pipeline(model, X_train, "dist/risk", labels=y_train, compact=True)
import { loadEdgePackage } from "tempest-react-sdk/tabular";
const pkg = await loadEdgePackage("/models/risk/");
console.log(pkg.runtime); // "compact"
Sem WebAssembly, sem onnxruntime-web, sem peer dependency. Modelo linear é
produto escalar; árvore é comparação encadeada — isso cabe em 1,49 KB de
JavaScript (medido, brotli).
Cobre linear (logística, linear, ridge, SGD, SVC linear), árvore,
floresta, extra-trees, regressores dos mesmos, e StandardScaler/
MinMaxScaler dentro de Pipeline.
Não cobre gradient boosting, MLP, ou qualquer transformação que não seja
(x - offset) / escala — e recusa exportar em vez de aproximar.
Verificado contra o scikit-learn, não contra a minha ideia do formato
O exportador compara o arquivo escrito com as predições do próprio estimador e se recusa a gravar se discordarem. Do lado do navegador, os testes rodam contra fixtures geradas pelo Python junto das saídas do scikit-learn — 7 famílias, rótulos idênticos e probabilidades batendo em 5 casas.
O arquivo é dado, nunca código: nada de JavaScript gerado, nada de
eval, nada que uma CSP estrita proíba.
Sem onnxruntime-web instalado, mesmo assim funciona
Verificado empacotando o SDK e instalando num projeto vazio, sem o peer:
o barril importa, CompactPredictor prediz e bate com o scikit-learn.
Pedir a rota ONNX ali dá erro nomeando o npm install onnxruntime-web e
a alternativa compacta.
Isso quebrou na v0.33.0 — o módulo de assets importava o runtime no topo,
então quem só queria a rota A precisava instalar 25,6 MB de wasm assim
mesmo. Corrigido na v0.33.1: o runtime entra por import() dinâmico, só
quando um modelo ONNX é carregado. Tem guard de teste travando isso.
B — Runtime mínimo (.ort + build próprio)
import { configureOrtAssets, TabularPredictor } from "tempest-react-sdk/tabular";
configureOrtAssets("/ort-minimal/");
const predictor = await TabularPredictor.create("/models/classifier.ort");
O export_onnx_to_ort do tempest-fastapi-sdk gera o .ort e o
required_operators.config, que é o que permite compilar um ONNX Runtime só
com os operadores do seu modelo. Aponte configureOrtAssets para esse build.
O .ort sozinho não economiza — ele aumenta
Medido: 526 B de ONNX viram 2.360 B de .ort; uma floresta de 266 KB
vira 650 KB. O .ort é formato de carregamento, não de compressão.
Quem encolhe é o runtime compilado sob medida, e isso custa
compilar o ORT do zero (Docker, horas) e manter esse build. O bundle
padrão lê .ort normalmente — testado — então dá para preparar a rota
antes de ter o build.
C — ONNX padrão (TabularPredictor)
const pkg = await loadEdgePackage("/models/risk/", { runtime: "onnx" });
O caminho de sempre. Custo marginal zero se o app já carrega
onnxruntime-web — por exemplo se usa tempest-react-sdk/vision. Aí o
runtime já foi pago e o ONNX cobre qualquer estimador.
Medido no navegador, com o dist construído
Chromium, mesma floresta de 10 árvores servida nas duas rotas, wasm servido localmente (ou seja, sem latência de rede — o piso):
| Compacta | ONNX | |
|---|---|---|
| Carga até poder responder | 6,0 ms | 579,6 ms |
| Predição (lote de 3 linhas) | 0,0035 ms | 0,0575 ms |
.wasm baixado |
nenhum | 25,6 MB |
A carga é 97x mais rápida, e a predição 16x — o leitor não aloca tensor nem atravessa a fronteira do WebAssembly, que nesse tamanho de modelo é o custo inteiro.
A suíte e2e/tabular.spec.ts prova as três coisas em Chromium de verdade:
que a rota compacta não busca nenhum .wasm (lendo a timeline de recursos
da própria página), que ela responde igual ao scikit-learn nas 7 famílias, e
que continua respondendo com o fetch derrubado.
Como decidir
| Situação | Rota |
|---|---|
| PWA só com modelo tabular | A — 6 MB gzipped a menos |
| App já roda visão/ONNX | C — custo marginal zero, cobertura total |
| Precisa de gradient boosting, MLP, pipeline complexo | C |
| Precisa de cobertura ampla e binário pequeno | B |
Modelo muda toda semana e o time só publica .pkl |
Qualquer uma — a esteira é a mesma |
A escolha não vaza para o seu código
As duas rotas devolvem o mesmo objeto: predict(rows) com labels,
probabilities, numRows e ms — inclusive o tipo do rótulo
(0, não "0", quando o scikit-learn usou inteiro). Trocar de rota é
mudar uma opção, não reescrever a tela.
const pkg = await loadEdgePackage("/models/risk/", { runtime: "auto" });
"auto" pega a compacta quando o pacote tem, e ONNX quando não tem.
Pedir "compact" num pacote que não a carrega dá erro dizendo isso —
em vez de baixar 25 MB de WebAssembly em silêncio.
Quando isso vale a pena
| Vale | Não vale |
|---|---|
| Score que precisa aparecer enquanto o usuário digita | Modelo que muda de hora em hora |
| Dado sensível que não deve sair do dispositivo | Modelo grande (deep learning pesado) |
| App que precisa funcionar sem rede | Predição que exige dados que só o servidor tem |
Latência medida no Chromium: 0,2 ms para um lote de 3 linhas numa regressão logística; ~0,05 ms por linha numa floresta de 10 árvores. A conta não é sobre computação — é sobre a viagem de rede que deixa de existir.
O que você economiza é a viagem, não o cálculo
Medido no lado do servidor com o tempest-fastapi-sdk: uma predição de
uma linha custa 0,0075 ms de inferência dentro de 1,22 ms de HTTP —
160x mais transporte que modelo, e isso com cliente em processo, sem
rede.
No navegador esse 1,22 ms simplesmente não existe: a mesma predição sai em ~0,05 ms, local. Por isso o critério da tabela acima é sobre frescor do modelo e tamanho, não sobre velocidade de cálculo — a velocidade você já ganhou tirando a rede do caminho.
Num componente React
O hook cuida do que todo componente erra igual: carregamento assíncrono, cancelamento quando o componente desmonta antes de terminar, e liberação da sessão.
import { useState } from "react";
import { useTabularPredictor } from "tempest-react-sdk/tabular";
function RiskWidget() {
const { predict, isReady, error } = useTabularPredictor("/models/risk-v3.onnx");
const [score, setScore] = useState<number | null>(null);
async function onScore(features: number[]) {
const { probabilities } = await predict([features]);
setScore(probabilities[0]?.[1] ?? null);
}
if (error) return <p>Modelo indisponível: {error.message}</p>;
return (
<button disabled={!isReady} onClick={() => onScore([1, 2, 3, 4])}>
{isReady ? "Calcular score" : "Carregando modelo..."}
</button>
);
}
Versione o nome do arquivo
classifier-v3.onnx, não classifier.onnx. O cache do navegador não
tem como saber que o conteúdo mudou, e um modelo velho servido do cache
é o tipo de bug que ninguém liga ao deploy da semana passada. Com o
pacote (loadEdgePackage) isso vem resolvido: a version do manifesto
é derivada do conteúdo.
Exportar com skl2onnx na mão quebra no navegador
Medido: o default do skl2onnx deixa o ZipMap ligado, e a saída de
probabilidade vira uma sequência de mapas. O ONNX Runtime Web recusa
valores que não são tensor — Reading data from non-tensor typed value is
not supported — e a predição morre em runtime, não no build.
Use export_sklearn_to_onnx (ou edge_pipeline), que desliga o ZipMap.
Se o .onnx veio de outra origem, o módulo detecta e o erro diz o que
fazer em vez de repetir a mensagem do runtime.
Pacote de borda: o manifesto que vem do Python
Do lado do Python, edge_pipeline publica um diretório, não um arquivo:
dist/risk/
├── risk.onnx o grafo
├── risk.onnx.gz o mesmo, ~10% do tamanho
├── baseline.json referência de deriva
└── manifest.json o contrato
Sirva esse diretório como asset estático e o navegador lê o mesmo contrato:
import { loadEdgePackage } from "tempest-react-sdk/tabular";
const pkg = await loadEdgePackage("/models/risk/");
console.log(pkg.featureNames); // ["age", "income", "tenure", "score"]
console.log(pkg.classes); // ["0", "1", "2"]
const { probabilities } = await pkg.predictor.predict([[41, 5200, 3, 0.82]]);
console.log(pkg.explain(probabilities[0]!));
[{ name: "2", score: 0.7484 }, { name: "0", score: 0.1564 }, { name: "1", score: 0.0952 }]
A ordem das colunas é o campo que salva você
Modelo alimentado com as features certas na ordem errada responde com confiança e errado. Não existe checagem em runtime que pegue isso — o tensor tem a largura certa, os números são plausíveis, e a resposta é lixo.
featureNames vem do treino, gravado pelo edge_pipeline. Use-o para
montar a linha a partir do seu formulário, em vez de confiar que a ordem
do <form> bate com a do DataFrame de seis meses atrás.
Modelo que nasceu de um .pkl
Se o pacote foi gerado por edge_pipeline_from_pickle, o manifesto traz
source — nome, SHA-256 e a versão do scikit-learn que converteu:
const manifest = await fetchEdgeManifest("/models/risk/");
console.log(manifest.source?.file, manifest.source?.sha256.slice(0, 12));
O .pkl não viaja para o navegador, e não é limitação: pickle é
programa Python, não dado. O que viaja é o ONNX mais o carimbo de qual
arquivo o produziu — o suficiente para rastrear um modelo rodando numa
aba de volta até a esteira, seis meses depois.
Checar versão sem baixar o modelo
import { fetchEdgeManifest } from "tempest-react-sdk/tabular";
const manifest = await fetchEdgeManifest("/models/risk/");
if (manifest.version !== localStorage.getItem("risk-version")) {
// modelo novo publicado — vale baixar
}
O manifesto tem algumas centenas de bytes. A version é derivada do
conteúdo, então republicar os mesmos bytes não parece versão nova.
Compatibilidade é explícita
O schema_version é conferido. Pacote escrito por um SDK mais novo do
que este leitor entende é recusado, com a instrução de atualizar —
ler assim mesmo arriscaria interpretar errado justamente o campo de
ordem das colunas. Campos desconhecidos, ao contrário, são ignorados: um
acréscimo compatível não quebra nada.
Instalação
onnxruntime-web é peer dependency opcional: só quem usa esse subpath
instala.
npm install onnxruntime-web
Importe o pacote padrão, nunca onnxruntime-web/webgpu
Medido no Chromium: o build WebGPU carrega um binário WebAssembly sem o
domínio ai.onnx.ml, e a sessão nem chega a abrir —
No Op registered for TreeEnsembleClassifier.
Modelos de scikit-learn são feitos desses operadores
(TreeEnsembleClassifier, LinearClassifier, Scaler), então não há
velocidade sobrando pra buscar na GPU: o backend wasm é o único que os
implementa, e é o default do módulo. Quando o erro acontece, ele vira
UnsupportedGraphError com a instrução de trocar o import.
Offline de verdade
São duas coisas que precisam estar no dispositivo, e esquecer a segunda é o erro clássico.
O modelo
Fica em Cache Storage na primeira visita:
import { fetchModelBytes, isModelCached } from "tempest-react-sdk/tabular";
const bytes = await fetchModelBytes("/models/classifier-v3.onnx");
const predictor = await TabularPredictor.create(bytes);
console.log(await isModelCached("/models/classifier-v3.onnx")); // true
Cache-first, não network-first: um arquivo de modelo é imutável para uma dada
versão, então revalidar a cada carga gasta uma ida à rede pra não aprender
nada. Publique versão nova sob URL nova (ou passe revalidate: true).
O hook faz isso sozinho quando a fonte é uma URL — cache: false desliga. O
nome do Cache Storage usado é DEFAULT_MODEL_CACHE, exportado para um botão de
"limpar modelos baixados" apagar exatamente o que o SDK criou.
O runtime
import { configureOrtAssets, ortAssetUrls } from "tempest-react-sdk/tabular";
configureOrtAssets("/ort/");
O ONNX Runtime Web não embute o WebAssembly — nem nos builds .bundle
Medido: servindo o app sem os .wasm ao lado, a criação da sessão falha
com Aborted(both async and sync fetching of the wasm failed) — uma
mensagem que não diz qual arquivo faltou. No Chromium o arquivo buscado
foi ort-wasm-simd-threaded.jsep.wasm.
Copie os binários de node_modules/onnxruntime-web/dist/ para o seu
diretório público no build, e precache junto:
Os .wasm moram em public/, então o Vite não os emite no bundle e eles
não entram sozinhos no manifest de precache. Liste-os em additionalUrls,
no vite.config.ts:
import { defineConfig } from "vite";
import { tempestPwaManifest } from "tempest-react-sdk/vite";
import { ortAssetUrls } from "tempest-react-sdk/tabular";
export default defineConfig({
plugins: [tempestPwaManifest({ additionalUrls: ortAssetUrls("/ort/") })],
});
O service worker então precacheia tudo pelo manifest, sem lista repetida:
import { installPrecache } from "tempest-react-sdk/sw";
installPrecache();
Qual binário é buscado depende do suporte a threads e SIMD do navegador,
então um app que precisa funcionar em todo lugar leva todos —
ORT_WASM_ASSETS tem a lista.
Detalhes que o módulo resolve por você
Rótulo int64 chega como bigint
O ONNX Runtime Web devolve o tensor de rótulo como BigInt64Array. Quem
compara label === 1 recebe false em silêncio, e JSON.stringify
lança. O módulo converte para number — índice de classe nunca chega
perto de Number.MAX_SAFE_INTEGER.
Qual saída é qual
Classificador devolve label + probabilities; regressor devolve um
único variable. Indexar por posição funciona até o dia em que você
publica o outro tipo. predictor.info diz o que foi carregado:
console.log(predictor.info);
// { inputName: "input", numFeatures: 4, isClassifier: true, ... }
Linha de largura errada falha antes do runtime
FeatureShapeError nomeando a expectativa (o modelo espera 4 features
por linha, recebeu 2), em vez de um erro opaco vindo do WebAssembly.
Lote irregular também: o erro diz qual linha.
Erros
Todos herdam de TabularError, então dá pra pegar a família inteira. O
name é literal — o minificador renomeia classes, e um build real reportava
error.name === "t" antes disso ser corrigido.
| Erro | Quando |
|---|---|
UnsupportedGraphError |
Runtime sem os operadores ai.onnx.ml (build WebGPU) |
ModelLoadError |
Bytes não viraram sessão |
ModelFetchError |
Offline e nada em cache — problema de deploy, não de modelo |
FeatureShapeError |
Lote vazio, irregular ou da largura errada |
InferenceError |
Rodou mas a saída não é legível (export com ZipMap) |
CompactFormatError |
Pacote em formato compacto cujo cabeçalho o leitor não reconhece |
API
| Símbolo | O que é |
|---|---|
TabularPredictor.create(source, options?) |
Carrega um modelo (URL ou bytes) |
predictor.predict(rows) |
Prediz um lote; devolve labels, probabilities, ms |
predictor.info |
Entrada, nº de features, saídas, providers em uso |
predictor.dispose() |
Libera a sessão |
useTabularPredictor(source, options?) |
Hook com status/isReady/predict/reload |
loadEdgePackage(directoryUrl, options?) |
Carrega um pacote publicado pelo edge_pipeline |
configureOrtAssets(path) / configuredOrtAssetPath() |
Define de onde os .wasm do ORT são servidos, e lê de volta o que está valendo |
fetchEdgeManifest(directoryUrl) |
Só o manifesto — versão, colunas, classes |
fetchModelBytes(url, options?) |
Bytes do cache, com rede como fallback |
isModelCached(url) / cacheModelBytes / clearModelCache |
Gestão do cache |
configureOrtAssets(basePath) / ortAssetUrls(basePath) / ORT_WASM_ASSETS |
Assets do runtime |
DEFAULT_TABULAR_PROVIDERS |
["wasm"], pelo motivo acima |
Recapitulando
- Exporte com
export_sklearn_to_onnx— ZipMap ligado não roda no navegador. - Importe
onnxruntime-web, não o subpath/webgpu. - Sirva o
.onnxcomo asset versionado; o cache cuida do resto. - Copie e precache os
.wasm, senão "offline" só funciona online. - Use o hook em componente; a classe direto em worker ou fora do React.