メインコンテンツまでスキップ

Cursor での機械学習:TensorFlow.js、ONNX、AI支援による ML パイプライン

Cursor での機械学習

機械学習は、画像分類から自然言語処理、推薦システムまで、アプリケーションに予測インテリジェンスをもたらします。Cursor は AI 駆動の IDE エディターとして、データ前処理、モデル設計、トレーニングループ、評価指標、デプロイという ML ワークフローのすべてのフェーズを加速します。本ガイドでは、ブラウザベースの推論とトレーニングのための TensorFlow.js、クロスプラットフォームのモデル実行のための ONNX Runtime、データ前処理パイプライン、モデル評価戦略、自然言語の記述を動作するトレーニングスクリプトに変える AI 支援による ML パイプライン生成をカバーします。

前提条件​

開始する前に、以下の条件を満たしていることを確認してください:

  • Cursor(最新バージョン推奨)を IDE エディターとして使用
  • Node.js 18 以上
  • JavaScript、TypeScript、Python の基本的な知識
  • ニューラルネットワーク、損失関数、勾配降下法の概念的理解
ヒント

機械学習を初めて使用する場合は、コードを書く前に Cursor に教師あり学習、教師なし学習、強化学習の違いを説明するよう依頼してください。問題がどのパラダイムに属するかを理解することが、データラベリング、モデルアーキテクチャ、評価指標という全体のアプローチを決定します。

Cursor での ML ワークフローの理解​

典型的な機械学習プロジェクトは予測可能なパイプラインに従います。Cursor は各ステージの実装と接続を支援します:

  1. データ収集と前処理:生データのクリーンアップ、欠損値の処理、特徴量の正規化、トレーニング/バリデーション/テストセットへの分割。
  2. モデル設計:アーキテクチャ(密結合ネットワーク、CNN、RNN、Transformer)の選択とレイヤーの定義。
  3. トレーニング:選択したオプティマイザーと損失関数でトレーニングループを実行し、指標を監視し、早期終了を適用。
  4. 評価:精度、再現率、F1スコア、混同行列などの指標でパフォーマンスを測定。
  5. デプロイ:モデルをエクスポートし、必要に応じて ONNX に変換し、ブラウザまたはサーバーで推論を実行。

プロジェクトスキャフォールディング​

Cursor に 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 が生成します:

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

トレーニングには純粋な @tensorflow/tfjs(ブラウザで WebGL/WASM 上で実行)ではなく、常に @tensorflow/tfjs-node(ネイティブ C++ TensorFlow ライブラリにバインド)を使用してください。Node.js 上のトレーニングは、SIMD 命令とネイティブメモリ管理を活用するため、ブラウザベースのトレーニングより10倍〜50倍高速です。ブラウザの TF.js は推論用であり、重いトレーニング用ではありません。

データ前処理​

データ前処理は ML 開発時間の大部分を消費します。Cursor はクリーンアップ、正規化、エンコーディング、分割を処理する完全な前処理パイプラインを生成できます。

前処理パイプライン​

// 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;
}
}
ヒント

Cursor に前処理パイプラインをテストするための合成データセットの生成を依頼してください。「1000行、8つの数値特徴量、2つのカテゴリ列、5%の欠損値を持つ合成 CSV を生成して」というプロンプトで、実際のデータセットを先に見つけてダウンロードすることなく、リアルなテストデータが得られます。

モデル設計とトレーニング​

TensorFlow.js はレイヤー化 API を使用してニューラルネットワークを構築します。Cursor は問題の記述に基づいて完全なモデルアーキテクチャを生成します。

分類モデルの構築​

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

コールバック付きトレーニングループ​

// 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)));
}
ヒント

トレーニングに時間がかかりすぎる場合は、Cursor にボトルネックのプロファイリングを依頼してください。「トレーニングスクリプトのパフォーマンス問題を分析して。テンソルのメモリリーク、不要な同期操作、データ読み込みのボトルネックをチェックして」というプロンプトで、未破棄のテンソルによるメモリ圧迫と減速などの問題を特定できます。

警告

TensorFlow.js テンソルはガベージコレクションされません。作成したすべてのテンソルは .dispose() を呼び出すか tf.tidy() でラップするまでメモリに残ります。トレーニングループでテンソルを破棄せずに作成すると、プロセスがクラッシュするまでメモリが成長し続けます。使用後は常にテンソルを破棄し、短命の計算には tf.tidy() を使用してください。

モデル評価​

評価は精度にとどまりません。Cursor は複数の指標を計算し結果を可視化する包括的な評価コードを生成します。

評価指標​

// 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(' '));
});
}
ヒント

Cursor にテキストベースの混同行列ビジュアライザーと HTML レポートの生成を依頼してください。「評価結果を、混同行列ヒートマップ、クラス別指標バー、サマリーテーブルを含むスタイル付きレポートとしてレンダリングする HTML テンプレートを作成して」というプロンプトで、ステークホルダー向けの共有可能な評価レポートが得られます。

クロスプラットフォーム推論のための ONNX Runtime​

ONNX(Open Neural Network Exchange)は、多くのプラットフォームで実行される ML モデルを表現する標準フォーマットです。TensorFlow.js モデルを ONNX に変換し、ブラウザ、モバイル、またはエッジデバイスで推論を実行します。

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

ブラウザでの ONNX 推論​

// 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,
};
}
}
ヒント

ブラウザで ONNX モデルを読み込む際は、wasm 実行プロバイダーで onnxruntime-web を使用してください。webgl プロバイダーは高速ですが、一部のモデルアーキテクチャで互換性の問題があります。Cursor に特定のモデルで両方のプロバイダーをベンチマークし、最適なものを推奨するよう依頼してください。

警告

ONNX モデルファイルは大きくなる可能性があります(10MB〜500MB)。ブラウザで読み込むと UI スレッドがブロックされます。アプリケーションの応答性を保つため、ONNX 推論には常に Web Worker を使用してください。Cursor にメッセージパッシング API を備えた Worker で予測器をラップするよう依頼してください。

AI 支援による ML パイプライン生成​

Cursor の最も強力な ML 機能は、自然言語の記述からトレーニングパイプライン全体を生成することです。問題を記述すると、Cursor は完全なコードスタックを生成します。

完全なパイプラインの生成​

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 は複数のファイルにまたがる完全なパイプラインを、接続された実行可能な状態で生成します。これは手動で500行以上のボイラープレートコードを書くことに相当します。

ヒント

Cursor に特定の部分の修正を依頼してパイプラインを反復してください。例えば:「各 Dense レイヤーの前に BatchNorm を使用するようにモデルアーキテクチャを変更して」や「チャーンクラスがデータの20%しかないためクラス重みバランシングを追加して」。このインクリメンタルなアプローチにより、パイプライン全体を書き直すことなく迅速に実験できます。

モデルデプロイパターン​

ONNX モデルファイルは大きくなる可能性があります(10MB〜500MB)。ブラウザで読み込むと UI スレッドがブロックされます。推論には常に Web Worker を使用してアプリケーションの応答性を保ってください。

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

メインスレッドでの Worker 使用​

// 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 });
});
}
}
ヒント

Cursor に、Worker で ONNX モデルを読み込み、フォームでユーザー入力を受け付け、推論を実行し、信頼度バー付きで予測結果を表示する完全な React コンポーネントの生成を依頼してください。このエンドツーエンドのパターンは、モデルファイルからユーザー向け UI までの完全なデプロイワークフローを示します。

まとめ​

Cursor での機械学習は、完全な ML ライフサイクルを単一の AI 駆動 IDE エディターにもたらします。本ガイドのパターンに従うことで、構造化されたデータ前処理、適切にアーキテクチャされた TensorFlow.js モデル、包括的な評価指標、ONNX クロスプラットフォームデプロイ、AI 支援によるパイプライン生成により、機械学習プロジェクトをこれまで以上に迅速にコンセプトから本番環境へと移行できます。テンソルメモリを慎重に管理し、精度だけでなく複数の指標で常に評価し、Cursor にボイラープレートを生成させてモデルアーキテクチャとデータ品質に集中できるようにしてください。