Aller au contenu principal

Apprentissage automatique dans Cursor : TensorFlow.js, ONNX et pipelines ML assistés par IA

Apprentissage automatique dans Cursor

L'apprentissage automatique apporte une intelligence prédictive aux applications, de la classification d'images au traitement du langage naturel et aux systèmes de recommandation. Cursor, un éditeur IDE alimenté par IA, accélère chaque phase du flux de travail ML : prétraitement des données, conception de modèles, boucles d'entraînement, métriques d'évaluation et déploiement. Ce guide couvre TensorFlow.js pour l'inférence et l'entraînement dans le navigateur, ONNX Runtime pour l'exécution de modèles multiplateformes, les pipelines de prétraitement des données, les stratégies d'évaluation de modèles, et la génération de pipelines ML assistée par IA qui transforme des descriptions en langage naturel en scripts d'entraînement fonctionnels.

Prérequis​

Avant de commencer, assurez-vous de disposer des éléments suivants :

  • Cursor (version la plus récente recommandée) comme éditeur IDE
  • Node.js 18 ou supérieur
  • Connaissance de base de JavaScript, TypeScript et Python
  • Une compréhension conceptuelle des réseaux de neurones, des fonctions de perte et de la descente de gradient
astuce

Si vous débutez en apprentissage automatique, demandez à Cursor d'expliquer la différence entre l'apprentissage supervisé, non supervisé et par renforcement avant d'écrire du code. Comprendre à quel paradigme appartient votre problème détermine toute votre approche : étiquetage des données, architecture du modèle et métriques d'évaluation.

Comprendre le flux de travail ML dans Cursor​

Un projet d'apprentissage automatique typique suit un pipeline prévisible. Cursor vous aide à implémenter et connecter chaque étape :

  1. Collecte et prétraitement des données : Nettoyer les données brutes, gérer les valeurs manquantes, normaliser les caractéristiques et diviser en ensembles d'entraînement/validation/test.
  2. Conception du modèle : Choisir une architecture (réseau dense, CNN, RNN, transformer) et définir les couches.
  3. Entraînement : Exécuter la boucle d'entraînement avec un optimiseur et une fonction de perte choisis, surveiller les métriques et appliquer l'arrêt anticipé.
  4. Évaluation : Mesurer les performances avec la précision, le rappel, le score F1, les matrices de confusion et d'autres métriques.
  5. Déploiement : Exporter le modèle, le convertir en ONNX si nécessaire et exécuter l'inférence dans le navigateur ou sur un serveur.

Échafaudage de projet​

Demandez à Cursor d'échafauder un projet 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.

Cursor génère :

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

Utilisez toujours @tensorflow/tfjs-node (qui se lie à la bibliothèque TensorFlow C++ native) pour l'entraînement, pas le @tensorflow/tfjs pur (qui s'exécute sur WebGL/WASM dans le navigateur). L'entraînement sur Node.js est 10 à 50 fois plus rapide que l'entraînement basé sur navigateur car il exploite les instructions SIMD et la gestion native de la mémoire. TF.js dans le navigateur est pour l'inférence, pas pour l'entraînement intensif.

Prétraitement des données​

Le prétraitement des données consomme la majorité du temps de développement ML. Cursor peut générer des pipelines de prétraitement complets qui gèrent le nettoyage, la normalisation, l'encodage et la division.

Pipeline de prétraitement​

// 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;
}
}
astuce

Demandez à Cursor de générer des jeux de données synthétiques pour tester votre pipeline de prétraitement. Une invite comme « Générez un CSV synthétique avec 1000 lignes, 8 caractéristiques numériques, 2 colonnes catégorielles et 5% de valeurs manquantes » vous donne des données de test réalistes sans avoir besoin de trouver et télécharger un vrai jeu de données.

Conception et entraînement de modèles​

TensorFlow.js utilise une API en couches pour construire des réseaux de neurones. Cursor génère des architectures de modèles complètes basées sur la description de votre problème.

Construction d'un modèle de classification​

// 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;
}

Boucle d'entraînement avec rappels​

// 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)));
}
astuce

Quand l'entraînement prend trop de temps, demandez à Cursor de profiler le goulot d'étranglement. Une invite comme « Analyse mon script d'entraînement pour les problèmes de performance. Vérifie les fuites de mémoire de tenseur, les opérations synchrones inutiles et les goulets d'étranglement de chargement de données » identifie des problèmes comme les tenseurs non disposés qui causent une pression mémoire et des ralentissements.

attention

Les tenseurs TensorFlow.js ne sont pas gérés par le ramassage-miettes. Chaque tenseur que vous créez reste en mémoire jusqu'à ce que vous appeliez .dispose() ou l'enveloppiez dans tf.tidy(). Dans une boucle d'entraînement, créer des tenseurs sans disposition fait croître la mémoire jusqu'à ce que le processus plante. Disposez toujours les tenseurs après usage et utilisez tf.tidy() pour les calculs de courte durée.

Évaluation des modèles​

L'évaluation va au-delà de la précision. Cursor génère du code d'évaluation complet qui calcule plusieurs métriques et visualise les résultats.

Métriques d'évaluation​

// 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(' '));
});
}
astuce

Demandez à Cursor de générer un visualiseur de matrice de confusion textuel et aussi un rapport HTML. Une invite comme « Créez un modèle HTML qui rend les résultats d'évaluation sous forme de rapport stylisé avec une carte thermique de matrice de confusion, des barres de métriques par classe et un tableau récapitulatif » vous donne des rapports d'évaluation partageables pour les parties prenantes.

ONNX Runtime pour l'inférence multiplateforme​

ONNX (Open Neural Network Exchange) est un format standard pour représenter les modèles ML qui s'exécute sur de nombreuses plateformes. Convertissez votre modèle TensorFlow.js en ONNX et exécutez l'inférence dans le navigateur, sur mobile ou sur des appareils de périphérie.

Conversion en 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}")

Exécution de l'inférence ONNX dans le navigateur​

// 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,
};
}
}
astuce

Lors du chargement de modèles ONNX dans le navigateur, utilisez onnxruntime-web avec le fournisseur d'exécution wasm. Le fournisseur webgl est plus rapide mais a des problèmes de compatibilité avec certaines architectures de modèles. Demandez à Cursor de benchmarking les deux fournisseurs sur votre modèle spécifique et de recommander le meilleur.

attention

Les fichiers de modèles ONNX peuvent être volumineux (10 Mo à 500 Mo). Les charger dans le navigateur bloque le thread UI. Utilisez toujours un Web Worker pour l'inférence ONNX pour maintenir votre application réactive. Demandez à Cursor d'envelopper le prédicteur dans un Worker avec une API de passage de messages.

Génération de pipelines ML assistée par IA​

La fonctionnalité ML la plus puissante de Cursor est la génération de pipelines d'entraînement entiers à partir d'une description en langage naturel. Décrivez votre problème et Cursor produit la pile de code complète.

Génération d'un pipeline complet​

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.

Cursor génère le pipeline complet dans plusieurs fichiers, connectés et prêts à exécuter. Cela équivaut à écrire manuellement plus de 500 lignes de code de boilerplate.

astuce

Itérez sur votre pipeline en demandant à Cursor de modifier des parties spécifiques. Par exemple : « Change l'architecture du modèle pour utiliser BatchNorm avant chaque couche Dense » ou « Ajoute l'équilibrage des poids de classe car la classe de churn ne représente que 20% des données. » Cette approche incrémentale vous permet d'expérimenter rapidement sans réécrire tout le pipeline.

Modèles de déploiement​

Les fichiers de modèles ONNX peuvent être volumineux (10 Mo à 500 Mo). Les charger dans le navigateur bloque le thread UI. Utilisez toujours un Web Worker pour l'inférence afin de maintenir l'application réactive.

Inférence dans le navigateur avec un 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 });
}
};

Utilisation du Worker dans le 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 });
});
}
}
astuce

Demandez à Cursor de générer un composant React complet qui charge le modèle ONNX dans un Worker, accepte les entrées utilisateur via un formulaire, exécute l'inférence et affiche la prédiction avec une barre de confiance. Ce modèle de bout en bout démontre le flux de déploiement complet du fichier de modèle à l'interface utilisateur.

Résumé​

L'apprentissage automatique dans Cursor apporte le cycle de vie ML complet dans un éditeur IDE alimenté par IA. En suivant les modèles de ce guide — prétraitement structuré des données, modèles TensorFlow.js bien architecturés, métriques d'évaluation complètes, déploiement multiplateforme ONNX et génération de pipelines assistée par IA — vous pouvez faire passer un projet d'apprentissage automatique du concept à la production plus rapidement que jamais. N'oubliez pas de gérer la mémoire des tenseurs avec soin, d'évaluer toujours avec plusieurs métriques au-delà de la précision, et de laisser Cursor générer votre code de boilerplate afin que vous puissiez vous concentrer sur l'architecture du modèle et la qualité des données.