Pular para o conteúdo principal

Machine Learning no Cursor: TensorFlow.js, ONNX e Pipelines de ML Assistidos por IA

Machine Learning no Cursor

Machine learning traz inteligência preditiva para aplicativos, desde classificação de imagens até processamento de linguagem natural e sistemas de recomendação. Cursor, um editor IDE alimentado por IA, acelera cada fase do fluxo de trabalho de ML: pré-processamento de dados, design de modelo, loops de treinamento, métricas de avaliação e deployment. Este guia aborda TensorFlow.js para inferência e treinamento baseados em navegador, ONNX Runtime para execução de modelo multiplataforma, pipelines de pré-processamento de dados, estratégias de avaliação de modelo e geração de pipeline de ML assistida por IA que transforma descrições em linguagem natural em scripts de treinamento funcionais.

Pré-requisitos​

Antes de começar, certifique-se de que você tem o seguinte:

  • Cursor (versão mais recente recomendada) como seu editor IDE
  • Node.js 18 ou superior
  • Familiaridade básica com JavaScript, TypeScript e Python
  • Compreensão conceitual de redes neurais, funções de perda e gradiente descendente
dica

Se você é novo em machine learning, peça ao Cursor para explicar a diferença entre aprendizado supervisionado, não supervisionado e por reforço antes de escrever qualquer código. Entender em qual paradigma seu problema se encaixa determina toda a sua abordagem: rotulagem de dados, arquitetura de modelo e métricas de avaliação.

Entendendo o Fluxo de Trabalho de ML no Cursor​

Um projeto típico de machine learning segue um pipeline previsível. O Cursor ajuda você a implementar e conectar cada estágio:

  1. Coleta e pré-processamento de dados: Limpar dados brutos, lidar com valores ausentes, normalizar features e dividir em conjuntos de treino/validação/teste.
  2. Design de modelo: Escolher uma arquitetura (rede densa, CNN, RNN, transformer) e definir camadas.
  3. Treinamento: Executar o loop de treinamento com um otimizador e função de perda escolhidos, monitorar métricas e aplicar early stopping.
  4. Avaliação: Medir desempenho com precision, recall, F1-score, matrizes de confusão e outras métricas.
  5. Deployment: Exportar o modelo, converter para ONNX se necessário e executar inferência no navegador ou em um servidor.

Scaffold de Projeto​

Peça ao Cursor para fazer scaffold de um projeto TensorFlow.js:

Create a TypeScript project for machine learning with TensorFlow.js.
Set up Vite for the frontend, install @tensorflow/tfjs and
@tensorflow/tfjs-node, and create a folder structure for data,
models, training scripts, and evaluation utilities.

O Cursor gera:

my-ml-project/
data/
raw/
processed/
models/
src/
train.ts
evaluate.ts
predict.ts
src-tauri/ (optional, for desktop deployment)
package.json
tsconfig.json
aviso

Sempre use @tensorflow/tfjs-node (que se vincula à biblioteca C++ nativa do TensorFlow) para treinamento, não @tensorflow/tfjs puro (que roda em WebGL/WASM no navegador). Treinar no Node.js é 10x a 50x mais rápido que o treinamento baseado em navegador porque aproveita instruções SIMD e gerenciamento de memória nativa. TF.js no navegador é para inferência, não para treinamento pesado.

Pré-processamento de Dados​

O pré-processamento de dados consome a maior parte do tempo de desenvolvimento de ML. O Cursor pode gerar pipelines completos de pré-processamento que lidam com limpeza, normalização, codificação e divisão.

Pipeline de Pré-processamento​

// src/preprocessing.ts
import * as tf from '@tensorflow/tfjs-node';

export interface DatasetStats {
mean: number[];
std: number[];
min: number[];
max: number[];
}

export class DataPreprocessor {
private stats: DatasetStats | null = null;

/**
* Compute normalization statistics from training data.
* Must be called before normalize() and applied identically
* to training, validation, and test sets.
*/
computeStats(data: number[][]): void {
const tensor = tf.tensor2d(data);
const mean = tensor.mean(0).dataSync();
const std = tensor.sub(tensor.mean(0)).square().mean(0).sqrt().dataSync();
const min = tensor.min(0).dataSync();
const max = tensor.max(0).dataSync();

this.stats = {
mean: Array.from(mean),
std: Array.from(std).map((s, i) => (s < 1e-8 ? 1 : s)), // Avoid division by zero
min: Array.from(min),
max: Array.from(max),
};

tensor.dispose();
}

/**
* Apply Z-score normalization: (x - mean) / std
*/
normalize(data: number[][]): number[][] {
if (!this.stats) throw new Error('Call computeStats() first');
return data.map((row) =>
row.map((val, i) => (val - this.stats!.mean[i]) / this.stats!.std[i])
);
}

/**
* One-hot encode categorical labels
*/
encodeLabels(labels: number[], numClasses: number): number[][] {
return labels.map((label) => {
const encoded = new Array(numClasses).fill(0);
encoded[label] = 1;
return encoded;
});
}

/**
* Split data into train, validation, and test sets
*/
split(
data: number[][],
labels: number[][],
trainRatio = 0.7,
valRatio = 0.15
): { trainX: number[][]; trainY: number[]; valX: number[][]; valY: number[]; testX: number[][]; testY: number[] } {
const n = data.length;
const indices = Array.from({ length: n }, (_, i) => i);
// Fisher-Yates shuffle
for (let i = n - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[indices[i], indices[j]] = [indices[j], indices[i]];
}
const trainEnd = Math.floor(n * trainRatio);
const valEnd = trainEnd + Math.floor(n * valRatio);
const pick = (idx: number[]) => ({ x: idx.map((i) => data[i]), y: idx.map((i) => labels[i]) });
const train = pick(indices.slice(0, trainEnd));
const val = pick(indices.slice(trainEnd, valEnd));
const test = pick(indices.slice(valEnd));
return { trainX: train.x, trainY: train.y, valX: val.x, valY: val.y, testX: test.x, testY: test.y };
}

/**
* Handle missing values by imputing the column mean
*/
imputeMissing(data: (number | null)[][]): number[][] {
const numCols = data[0].length;
const colSums = new Array(numCols).fill(0);
const colCounts = new Array(numCols).fill(0);

data.forEach((row) => {
row.forEach((val, col) => {
if (val !== null && !isNaN(val)) { colSums[col] += val; colCounts[col]++; }
});
});

const colMeans = colSums.map((sum, i) => (colCounts[i] > 0 ? sum / colCounts[i] : 0));

return data.map((row) => row.map((val, col) => (val === null || isNaN(val) ? colMeans[col] : val)));
}

getStats(): DatasetStats | null {
return this.stats;
}
}
dica

Peça ao Cursor para gerar datasets sintéticos para testar seu pipeline de pré-processamento. Um prompt como "Gere um CSV sintético com 1000 linhas, 8 features numéricas, 2 colunas categóricas e 5% de valores ausentes" fornece dados de teste realistas sem precisar encontrar e baixar um dataset real primeiro.

Design de Modelo e Treinamento​

O TensorFlow.js usa uma API em camadas para construir redes neurais. O Cursor gera arquiteturas de modelo completas com base na sua descrição do problema.

Construindo um Modelo de Classificação​

// src/models/classifier.ts
import * as tf from '@tensorflow/tfjs-node';

export interface ModelConfig {
inputSize: number;
numClasses: number;
hiddenLayers: number[];
learningRate: number;
dropoutRate: number;
}

export function buildClassifier(config: ModelConfig): tf.LayersModel {
const model = tf.sequential();

// Input layer
model.add(
tf.layers.dense({
inputShape: [config.inputSize],
units: config.hiddenLayers[0],
activation: 'relu',
kernelInitializer: 'heNormal',
})
);

model.add(tf.layers.batchNormalization());
model.add(tf.layers.dropout({ rate: config.dropoutRate }));

// Hidden layers
for (let i = 1; i < config.hiddenLayers.length; i++) {
model.add(
tf.layers.dense({
units: config.hiddenLayers[i],
activation: 'relu',
kernelInitializer: 'heNormal',
})
);
model.add(tf.layers.batchNormalization());
model.add(tf.layers.dropout({ rate: config.dropoutRate }));
}

// Output layer
model.add(
tf.layers.dense({
units: config.numClasses,
activation: 'softmax',
})
);

model.compile({
optimizer: tf.train.adam(config.learningRate),
loss: 'categoricalCrossentropy',
metrics: ['accuracy'],
});

return model;
}

Loop de Treinamento com Callbacks​

// src/train.ts
import * as tf from '@tensorflow/tfjs-node';
import { buildClassifier, ModelConfig } from './models/classifier';
import { DataPreprocessor } from './preprocessing';
import * as fs from 'fs/promises';
import * as path from 'path';

interface TrainingResult {
finalAccuracy: number;
finalLoss: number;
epochsRun: number;
modelPath: string;
}

export async function trainModel(
dataPath: string,
config: ModelConfig,
outputDir: string
): Promise<TrainingResult> {
// Load and preprocess data
const rawData = await loadCSV(dataPath);
const features = rawData.map((row) => row.slice(0, config.inputSize));
const labels = rawData.map((row) => row[config.inputSize]);

const preprocessor = new DataPreprocessor();
preprocessor.computeStats(features);
const normalizedFeatures = preprocessor.normalize(features);

const encodedLabels = preprocessor.encodeLabels(labels, config.numClasses);

const split = preprocessor.split(normalizedFeatures, labels);

const trainXTensor = tf.tensor2d(split.trainX);
const trainYTensor = tf.tensor2d(
preprocessor.encodeLabels(split.trainY, config.numClasses)
);
const valXTensor = tf.tensor2d(split.valX);
const valYTensor = tf.tensor2d(
preprocessor.encodeLabels(split.valY, config.numClasses)
);

// Build model
const model = buildClassifier(config);

// Train with callbacks
const history = await model.fit(trainXTensor, trainYTensor, {
epochs: 100,
batchSize: 32,
validationData: [valXTensor, valYTensor],
shuffle: true,
callbacks: [
tf.callbacks.earlyStopping({ monitor: 'val_loss', patience: 10, minDelta: 0.001, restoreBestWeight: true }),
{
onEpochEnd: async (epoch, logs) => {
if (epoch > 0 && epoch % 20 === 0) {
console.log(`Epoch ${epoch}: Consider reducing learning rate`);
}
console.log(`Epoch ${epoch + 1}: loss=${logs?.loss?.toFixed(4)}, acc=${logs?.acc?.toFixed(4)}, val_loss=${logs?.val_loss?.toFixed(4)}, val_acc=${logs?.val_acc?.toFixed(4)}`);
},
},
],
});

// Save model and preprocessor stats
const modelPath = path.join(outputDir, 'model');
await model.save(`file://${modelPath}`);
await fs.writeFile(path.join(outputDir, 'preprocessor-stats.json'), JSON.stringify(preprocessor.getStats(), null, 2));

// Cleanup tensors
[trainXTensor, trainYTensor, valXTensor, valYTensor].forEach((t) => t.dispose());

const lastEpoch = history.epoch.length;
const finalAccuracy = history.history.val_acc?.[lastEpoch - 1] ?? 0;
const finalLoss = history.history.val_loss?.[lastEpoch - 1] ?? 0;

console.log(`\nTraining complete after ${lastEpoch} epochs`);
console.log(`Best validation accuracy: ${finalAccuracy.toFixed(4)}`);
console.log(`Model saved to: ${modelPath}`);

return { finalAccuracy, finalLoss, epochsRun: lastEpoch, modelPath };
}

async function loadCSV(filePath: string): Promise<number[][]> {
const lines = (await fs.readFile(filePath, 'utf-8')).trim().split('\n');
const header = lines[0].split(',').length;
return lines.slice(1)
.map((line) => line.split(',').map((v) => parseFloat(v.trim())))
.filter((row) => row.length === header && row.every((v) => !isNaN(v)));
}
dica

Quando o treinamento demora muito, peça ao Cursor para profilear o gargalo. Um prompt como "Analise meu script de treinamento quanto a problemas de performance. Verifique vazamentos de memória de tensor, operações síncronas desnecessárias e gargalos de carregamento de dados" identifica problemas como tensores não descartados que causam pressão de memória e lentidão.

aviso

Tensores do TensorFlow.js não são coletados pelo garbage collector. Cada tensor que você cria permanece na memória até você chamar .dispose() ou envolvê-lo em tf.tidy(). Em um loop de treinamento, criar tensores sem descarte faz a memória crescer até o processo travar. Sempre descarte tensores após o uso e use tf.tidy() para computações de curta duração.

Avaliação de Modelo​

Avaliação vai além da acurácia. O Cursor gera código de avaliação abrangente que computa múltiplas métricas e visualiza resultados.

Métricas de Avaliação​

// src/evaluate.ts
import * as tf from '@tensorflow/tfjs-node';
import { DataPreprocessor } from './preprocessing';

export interface EvaluationResult {
accuracy: number;
precision: number;
recall: number;
f1Score: number;
confusionMatrix: number[][];
perClassMetrics: Array<{
class: number;
precision: number;
recall: number;
f1: number;
support: number;
}>;
}

export async function evaluateModel(
model: tf.LayersModel,
testX: number[][],
testY: number[],
numClasses: number,
preprocessor: DataPreprocessor
): Promise<EvaluationResult> {
const inputTensor = tf.tensor2d(preprocessor.normalize(testX));
const predictions = model.predict(inputTensor) as tf.Tensor;
const predicted = predictions.argMax(1).dataSync();
const actual = tf.tensor1d(testY).dataSync();

// Build confusion matrix
const confusionMatrix: number[][] = Array.from({ length: numClasses }, () => new Array(numClasses).fill(0));
for (let i = 0; i < predicted.length; i++) confusionMatrix[actual[i]][predicted[i]]++;

// Per-class metrics
const perClassMetrics: EvaluationResult['perClassMetrics'] = [];
let totalCorrect = 0;
let totalSamples = 0;

for (let c = 0; c < numClasses; c++) {
const tp = confusionMatrix[c][c];
const fp = confusionMatrix.reduce((sum, row, idx) => sum + (idx !== c ? row[c] : 0), 0);
const fn = confusionMatrix[c].reduce((sum, val, idx) => sum + (idx !== c ? val : 0), 0);
const support = confusionMatrix[c].reduce((a, b) => a + b, 0);

const precision = tp + fp > 0 ? tp / (tp + fp) : 0;
const recall = tp + fn > 0 ? tp / (tp + fn) : 0;
const f1 = precision + recall > 0 ? (2 * precision * recall) / (precision + recall) : 0;

perClassMetrics.push({ class: c, precision, recall, f1, support });
totalCorrect += tp;
totalSamples += support;
}

// Macro-averaged metrics
const macroPrecision = perClassMetrics.reduce((sum, m) => sum + m.precision, 0) / numClasses;
const macroRecall = perClassMetrics.reduce((sum, m) => sum + m.recall, 0) / numClasses;
const macroF1 = perClassMetrics.reduce((sum, m) => sum + m.f1, 0) / numClasses;

inputTensor.dispose();
predictions.dispose();

return {
accuracy: totalCorrect / totalSamples,
precision: macroPrecision,
recall: macroRecall,
f1Score: macroF1,
confusionMatrix,
perClassMetrics,
};
}

export function printEvaluation(result: EvaluationResult): void {
console.log('\n=== Model Evaluation ===\n');
console.log(`Accuracy: ${(result.accuracy * 100).toFixed(2)}%`);
console.log(`Precision: ${(result.precision * 100).toFixed(2)}%`);
console.log(`Recall: ${(result.recall * 100).toFixed(2)}%`);
console.log(`F1-Score: ${(result.f1Score * 100).toFixed(2)}%`);

console.log('\nPer-Class Metrics:');
console.log('Class | Precision | Recall | F1 | Support');
result.perClassMetrics.forEach((m) => {
console.log(` ${m.class} | ${(m.precision * 100).toFixed(1)}% | ${(m.recall * 100).toFixed(1)}% | ${(m.f1 * 100).toFixed(1)}% | ${m.support}`);
});

console.log('\nConfusion Matrix:');
result.confusionMatrix.forEach((row, i) => {
console.log(` ${i}: ` + row.join(' '));
});
}
dica

Peça ao Cursor para gerar um visualizador de matriz de confusão baseado em texto e também um relatório HTML. Um prompt como "Crie um template HTML que renderiza os resultados de avaliação como um relatório estilizado com um heatmap da matriz de confusão, barras de métricas por classe e uma tabela resumo" fornece relatórios de avaliação compartilháveis para stakeholders.

ONNX Runtime para Inferência Multiplataforma​

ONNX (Open Neural Network Exchange) é um formato padrão para representar modelos de ML que roda em muitas plataformas. Converta seu modelo TensorFlow.js para ONNX e execute inferência no navegador, em dispositivos móveis ou em dispositivos edge.

Convertendo para ONNX​

# scripts/convert_to_onnx.py
# Run this script to convert a saved TF model to ONNX format
import tensorflow as tf
import tf2onnx
import onnxruntime as ort
import numpy as np

# Load the saved TensorFlow model
model = tf.keras.models.load_model('models/saved_model')

# Convert to ONNX
spec = (tf.TensorSpec((None, 8), tf.float32, name='input'),)
onnx_model, _ = tf2onnx.convert.from_keras(model, input_signature=spec, opset=15)

# Save the ONNX model
with open('models/model.onnx', 'wb') as f:
f.write(onnx_model.SerializeToString())

# Verify with ONNX Runtime
session = ort.InferenceSession('models/model.onnx')
print(f"ONNX model inputs: {session.get_inputs()[0].name}")
print(f"ONNX model outputs: {[o.name for o in session.get_outputs()]}")

# Quick sanity check with dummy data
dummy_input = np.random.randn(1, 8).astype(np.float32)
result = session.run(None, {'input': dummy_input})
print(f"Test inference output shape: {result[0].shape}")

Executando Inferência ONNX no Navegador​

// src/predict.ts
import * as ort from 'onnxruntime-web';

export interface PredictionResult {
predictedClass: number;
confidence: number;
probabilities: number[];
}

export class OnnxPredictor {
private session: ort.InferenceSession | null = null;
private inputName: string = '';
private outputName: string = '';

async loadModel(modelPath: string): Promise<void> {
this.session = await ort.InferenceSession.create(modelPath, {
executionProviders: ['wasm'],
graphOptimizationLevel: 'all',
});

this.inputName = this.session.inputNames[0];
this.outputName = this.session.outputNames[0];
}

async predict(features: number[], mean: number[], std: number[]): Promise<PredictionResult> {
if (!this.session) throw new Error('Model not loaded. Call loadModel() first.');

// Normalize input
const normalized = features.map((val, i) => (val - mean[i]) / std[i]);

// Create ONNX tensor
const input = new ort.Tensor('float32', Float32Array.from(normalized), [1, normalized.length]);

// Run inference
const outputs = await this.session.run({ [this.inputName]: input });
const output = outputs[this.outputName];

// Convert to probabilities
const probabilities = Array.from(output.data as Float32Array);
const maxIndex = probabilities.indexOf(Math.max(...probabilities));

return {
predictedClass: maxIndex,
confidence: probabilities[maxIndex],
probabilities,
};
}
}
dica

Ao carregar modelos ONNX no navegador, use onnxruntime-web com o provider de execução wasm. O provider webgl é mais rápido, mas tem problemas de compatibilidade com algumas arquiteturas de modelo. Peça ao Cursor para fazer benchmark de ambos os providers no seu modelo específico e recomendar o melhor.

aviso

Arquivos de modelo ONNX podem ser grandes (10MB a 500MB). Carregá-los no navegador bloqueia a thread de UI. Sempre use um Web Worker para inferência ONNX para manter seu aplicativo responsivo. Peça ao Cursor para envolver o predictor em um Worker com API de passagem de mensagens.

Geração de Pipeline de ML Assistida por IA​

O recurso de ML mais poderoso do Cursor é gerar pipelines completos de treinamento a partir de uma descrição em linguagem natural. Descreva seu problema e o Cursor produz a stack completa de código.

Gerando um Pipeline Completo​

Create a complete machine learning pipeline for customer churn prediction:
1. Load a CSV with columns: tenure, monthlyCharges, totalCharges, contractType, internetService, churn
2. Handle missing values in totalCharges (impute median)
3. One-hot encode contractType and internetService
4. Normalize all numeric features with Z-score
5. Split 70/15/15 train/val/test
6. Build a dense neural network with 3 hidden layers (64, 32, 16) and dropout
7. Train with Adam optimizer, binary crossentropy loss, early stopping
8. Evaluate with accuracy, precision, recall, F1, and confusion matrix
9. Save the model and preprocessor stats
10. Export to ONNX format

Write all code in TypeScript using @tensorflow/tfjs-node.

O Cursor gera o pipeline completo em múltiplos arquivos, conectado e pronto para executar. Isso equivale a escrever mais de 500 linhas de código boilerplate manualmente.

dica

Itere no seu pipeline pedindo ao Cursor para modificar partes específicas. Por exemplo: "Mude a arquitetura do modelo para usar BatchNorm antes de cada camada Dense" ou "Adicione balanceamento de peso de classe porque a classe churn é apenas 20% dos dados." Essa abordagem incremental permite experimentar rapidamente sem reescrever todo o pipeline.

Padrões de Deployment de Modelo​

Arquivos de modelo ONNX podem ser grandes (10MB a 500MB). Carregá-los no navegador bloqueia a thread de UI. Sempre use um Web Worker para inferência para manter o aplicativo responsivo.

Inferência no Navegador com Web Worker​

// src/worker/inference-worker.ts
import { OnnxPredictor } from '../lib/predict';

let predictor: OnnxPredictor | null = null;

self.onmessage = async (event: MessageEvent) => {
const { type, modelPath, features, stats } = event.data;
if (type === 'load' && modelPath) {
predictor = new OnnxPredictor();
await predictor.loadModel(modelPath);
self.postMessage({ type: 'loaded' });
} else if (type === 'predict' && features && stats && predictor) {
const result = await predictor.predict(features, stats.mean, stats.std);
self.postMessage({ type: 'result', result });
}
};

Usando o Worker na Thread Principal​

// src/lib/inference-client.ts
import { PredictionResult } from './predict';

export class InferenceClient {
private worker = new Worker(new URL('../worker/inference-worker.ts', import.meta.url));

async loadModel(modelPath: string): Promise<void> {
return new Promise((resolve) => {
const handler = () => { this.worker.removeEventListener('message', handler); resolve(); };
this.worker.addEventListener('message', handler);
this.worker.postMessage({ type: 'load', modelPath });
});
}

async predict(features: number[], stats: { mean: number[]; std: number[] }): Promise<PredictionResult> {
return new Promise((resolve) => {
const handler = (event: MessageEvent) => {
if (event.data.type === 'result') {
this.worker.removeEventListener('message', handler);
resolve(event.data.result);
}
};
this.worker.addEventListener('message', handler);
this.worker.postMessage({ type: 'predict', features, stats });
});
}
}
dica

Peça ao Cursor para gerar um componente React completo que carrega o modelo ONNX em um Worker, aceita entrada do usuário através de um formulário, executa inferência e exibe a previsão com uma barra de confiança. Esse padrão end-to-end demonstra o fluxo de trabalho completo de deployment, do arquivo de modelo à UI voltada para o usuário.

Resumo​

Machine learning no Cursor traz o ciclo de vida completo de ML para um único editor IDE alimentado por IA. Seguindo os padrões deste guia — pré-processamento de dados estruturado, modelos TensorFlow.js bem arquitetados, métricas de avaliação abrangentes, deployment ONNX multiplataforma e geração de pipeline assistida por IA — você pode levar um projeto de machine learning do conceito à produção mais rápido do que nunca. Lembre-se de gerenciar a memória de tensores com cuidado, sempre avaliar com múltiplas métricas além da acurácia e deixar o Cursor gerar seu boilerplate para que você possa focar na arquitetura do modelo e qualidade dos dados.