02
Neural Network Image Classifier
Trains a small multi-layer perceptron on the 8x8 digits dataset with mini-batch Adam, then reports test accuracy, precision, recall, F1 and a confusion matrix.
Features
- Model:
Linear,ReLU,Linearin aSequential.src/models.tsalso builds GELU and LeakyReLU variants - Data pipeline:
loadDigits,trainTestSplit,StandardScaler - Training on plain tensors. The model has trainable parameters and grad mode is on, so
model.forward(x)returns aGradTensorthat tracks the weights, andloss.backward()fills the gradients. Noparameter(...)wrapper is needed for the data - Evaluation under
noGrad(), where the output is a plainTensor - Training loss and accuracy curves as SVG
What you will learn
- Model:
Linear,ReLU,Linearin aSequential.src/models.tsalso builds GELU and LeakyReLU variants - Data pipeline:
loadDigits,trainTestSplit,StandardScaler - Training on plain tensors. The model has trainable parameters and grad mode is on, so
model.forward(x)returns aGradTensorthat tracks the weights, andloss.backward()fills the gradients. Noparameter(...)wrapper is needed for the data - Evaluation under
noGrad(), where the output is a plainTensor - Use deepbox/nn for
Sequential,Linear,ReLU,GELU,LeakyReLU,crossEntropyLoss. - Use deepbox/optim for
Adam.
Project layout
02-neural-image-classifier/
├── index.ts # Main entry point
├── README.md # This file
├── output/ # Generated SVGs
└── src/
├── models.ts # MLP model definitions
└── trainer.ts # Optional helpers (not imported by index.ts): trainStep, evaluateModel, EarlyStopping, LR schedulesSource
/** * Neural Network Image Classifier * * A demonstration of neural network training for image classification * using the Deepbox Framework's deep learning modules. * * Deepbox Modules Used: * - deepbox/nn: Neural network layers, Sequential, loss functions * - deepbox/ndarray: GradTensor, autograd, tensor operations * - deepbox/datasets: loadDigits dataset * - deepbox/metrics: Classification metrics * - deepbox/preprocess: train/test split */ import { existsSync, mkdirSync, writeFileSync } from "node:fs";import { loadDigits } from "deepbox/datasets";import { confusionMatrix, f1Score, precision, recall } from "deepbox/metrics";import { type AnyTensor, GradTensor, noGrad, type Tensor, tensor } from "deepbox/ndarray";import { crossEntropyLoss } from "deepbox/nn";import { Adam } from "deepbox/optim";import { Figure } from "deepbox/plot";import { StandardScaler, trainTestSplit } from "deepbox/preprocess";import { setSeed } from "deepbox/random"; import { createModel, createSimpleMLP, getModelSummary } from "./src/models"; // ============================================================================// Configuration// ============================================================================ const OUTPUT_DIR = "docs/projects/02-neural-image-classifier/output";const NUM_CLASSES = 10;const LEARNING_RATE = 0.001;const NUM_EPOCHS = 40;const BATCH_SIZE = 32; // ============================================================================// Main Execution// ============================================================================ console.log("═".repeat(70));console.log(" NEURAL NETWORK IMAGE CLASSIFIER");console.log(" Built with Deepbox: TypeScript toolkit for AI & numerical computing");console.log("═".repeat(70)); setSeed(42); // Create output directoryif (!existsSync(OUTPUT_DIR)) { mkdirSync(OUTPUT_DIR, { recursive: true });} // ============================================================================// Step 1: Load and Prepare Data// ============================================================================ console.log("\nSTEP 1: Loading Digits Dataset");console.log("─".repeat(70)); const digits = loadDigits();console.log(`Loaded digits dataset`);console.log(` Samples: ${digits.data.shape[0]}`);console.log(` Features: ${digits.data.shape[1]} (8x8 pixel images)`);console.log(` Classes: ${NUM_CLASSES} (digits 0-9)`); // Split dataconst [XTrain, XTest, yTrain, yTest] = trainTestSplit(digits.data, digits.target, { testSize: 0.2, randomState: 42, shuffle: true,}); console.log(`\nSplit data:`);console.log(` Training samples: ${XTrain.shape[0]}`);console.log(` Test samples: ${XTest.shape[0]}`); // Scale featuresconst scaler = new StandardScaler();scaler.fit(XTrain);const XTrainScaled = scaler.transform(XTrain);const XTestScaled = scaler.transform(XTest); console.log(`Scaled features using StandardScaler`); const trainSize = XTrainScaled.shape[0];const testSize = XTestScaled.shape[0];const numFeatures = XTrainScaled.shape[1]; // ============================================================================// Step 2: Build Neural Network Model// ============================================================================ console.log("\nSTEP 2: Building Neural Network");console.log("─".repeat(70)); // Create a simple MLPconst model = createSimpleMLP(numFeatures, 128, NUM_CLASSES); console.log("\nModel Architecture:");console.log(model.toString()); const summary = getModelSummary(model);console.log(`\nModel Summary:`);console.log(` Total Layers: ${summary.numLayers}`);console.log(` Total Parameters: ${summary.numParameters}`); // ============================================================================// Step 3: Training Setup// ============================================================================ console.log("\nSTEP 3: Training Setup");console.log("─".repeat(70)); const modelParams = Array.from(model.parameters());console.log(` Trainable parameter tensors: ${modelParams.length}`); const optimizer = new Adam(model.parameters(), { lr: LEARNING_RATE });console.log(` Optimizer: Adam (lr=${LEARNING_RATE})`);console.log(` Epochs: ${NUM_EPOCHS}`);console.log(` Batch Size: ${BATCH_SIZE}`); // ============================================================================// Step 4: Training Loop// ============================================================================ console.log("\nSTEP 4: Training Neural Network");console.log("─".repeat(70)); // Training historyconst history: { epochs: number[]; trainLoss: number[]; trainAcc: number[] } = { epochs: [], trainLoss: [], trainAcc: [],}; // Copy the tensors into plain arrays so mini-batches can be picked by indexfunction extractData(X: Tensor, y: Tensor): { XArr: number[][]; yArr: number[] } { return { XArr: X.toArray() as number[][], yArr: (y.toArray() as number[]).map(Number), };} // Number of rows where the arg-max of the logits equals the labelfunction countCorrect(logits: AnyTensor, labels: number[]): number { const predicted = logits.argmax(1).toArray() as number[]; return predicted.reduce((n, p, i) => n + (p === labels[i] ? 1 : 0), 0);} // Training with mini-batchesconsole.log("\nTraining Progress:");console.log("─".repeat(50)); const { XArr: XTrainArr, yArr: yTrainArr } = extractData(XTrainScaled, yTrain);const createEpochOrder = (epoch: number): number[] => { const order = Array.from({ length: trainSize }, (_, index) => index); let state = 42 + epoch * 9973; for (let i = order.length - 1; i > 0; i--) { state = (state * 1664525 + 1013904223) >>> 0; const j = state % (i + 1); const current = order[i]; order[i] = order[j] ?? current; order[j] = current ?? order[j] ?? 0; } return order;}; // One pass over the shuffled training set per epochfor (let epoch = 0; epoch < NUM_EPOCHS; epoch++) { model.train(true); let epochLoss = 0; let epochCorrect = 0; const epochOrder = createEpochOrder(epoch); const numBatches = Math.ceil(epochOrder.length / BATCH_SIZE); // Process in batches for (let batch = 0; batch < numBatches; batch++) { const startIdx = batch * BATCH_SIZE; const endIdx = Math.min(startIdx + BATCH_SIZE, epochOrder.length); // Extract batch const batchIndices = epochOrder.slice(startIdx, endIdx); const XBatch = batchIndices.map((index) => XTrainArr[index] ?? []); const yBatch = batchIndices.map((index) => yTrainArr[index] ?? 0); // Plain tensors are enough: the model has trainable parameters and grad mode is on, // so forward() returns a GradTensor that tracks the weights. const input = tensor(XBatch, { dtype: "float32" }); const targets = tensor(yBatch, { dtype: "int32" }); optimizer.zeroGrad(); const output = model.forward(input); if (!(output instanceof GradTensor)) { throw new Error("Expected a GradTensor: is grad mode off?"); } const loss = crossEntropyLoss(output, targets); loss.backward(); optimizer.step(); epochLoss += Number(loss.item()); epochCorrect += countCorrect(output, yBatch); } const avgLoss = epochLoss / numBatches; const trainAcc = epochCorrect / trainSize; history.epochs.push(epoch + 1); history.trainLoss.push(avgLoss); history.trainAcc.push(trainAcc); if ((epoch + 1) % 5 === 0 || epoch === 0) { console.log( ` Epoch ${String(epoch + 1).padStart(2)}: Loss = ${avgLoss.toFixed(4)}, ` + `Accuracy = ${(trainAcc * 100).toFixed(2)}%` ); }} console.log("─".repeat(50));console.log("Training Complete!"); // ============================================================================// Step 5: Model Evaluation// ============================================================================ console.log("\nSTEP 5: Model Evaluation");console.log("─".repeat(70)); model.train(false); // Evaluate on test setconst { XArr: XTestArr, yArr: yTestArr } = extractData(XTestScaled, yTest);const testInput = tensor(XTestArr, { dtype: "float32" });// noGrad() skips graph tracking, so the logits come back as a plain Tensorconst testOutput = noGrad(() => model.forward(testInput));if (testOutput instanceof GradTensor) { throw new Error("Expected a plain Tensor inside noGrad()");} // Predicted class is the arg-max of each row of logitsconst predictions = testOutput.argmax(1).toArray() as number[];const testCorrect = countCorrect(testOutput, yTestArr); const testAccuracy = testCorrect / testSize;const predTensor = tensor(predictions);const yTestTensor = tensor(yTestArr); console.log("\nTest Set Performance:");console.log(` Accuracy: ${(testAccuracy * 100).toFixed(2)}%`);console.log( ` Precision: ${(Number(precision(yTestTensor, predTensor, "macro")) * 100).toFixed(2)}%`);console.log(` Recall: ${(Number(recall(yTestTensor, predTensor, "macro")) * 100).toFixed(2)}%`);console.log( ` F1 Score: ${(Number(f1Score(yTestTensor, predTensor, "macro")) * 100).toFixed(2)}%`); // Confusion matrixconsole.log("\nConfusion Matrix:");const cm = confusionMatrix(yTestTensor, predTensor);const cmData = cm.toArray() as number[][]; // Print confusion matrixconsole.log(" Predicted");console.log( ` ${Array.from({ length: NUM_CLASSES }, (_, i) => i.toString().padStart(3)).join("")}`);console.log(` +${"-".repeat(NUM_CLASSES * 3 + 1)}`);for (let i = 0; i < NUM_CLASSES; i++) { let row = ` ${i} |`; for (let j = 0; j < NUM_CLASSES; j++) { row += String(cmData[i][j]).padStart(3); } console.log(row);} // ============================================================================// Step 6: Architecture Overview// ============================================================================ console.log("\nSTEP 6: Architecture Overview");console.log("─".repeat(70)); const architectureConfigs = [ { name: "simple", label: "Simple + ReLU" }, { name: "gelu", label: "GELU" }, { name: "leaky", label: "LeakyReLU" },] as const; for (const config of architectureConfigs) { const candidate = createModel({ inputSize: numFeatures, hiddenSize: 128, numClasses: NUM_CLASSES, architecture: config.name, }); const candidateSummary = getModelSummary(candidate); console.log( ` ${config.label.padEnd(16)}: ${candidateSummary.numLayers} layers, ${candidateSummary.numParameters} parameters` );} // ============================================================================// Step 7: Visualizations// ============================================================================ console.log("\nSTEP 7: Generating Visualizations");console.log("─".repeat(70)); // Learning curve plottry { const fig = new Figure({ width: 800, height: 400 }); const ax = fig.addAxes(); ax.plot(tensor(history.epochs), tensor(history.trainLoss), { color: "#2196F3", linewidth: 2, }); ax.setTitle("Training Loss Curve"); ax.setXLabel("Epoch"); ax.setYLabel("Loss"); const svg = fig.renderSVG(); writeFileSync(`${OUTPUT_DIR}/loss-curve.svg`, svg.svg); console.log(` Saved: ${OUTPUT_DIR}/loss-curve.svg`);} catch (e) { console.log(` Warning: could not generate loss curve: ${e}`);} // Accuracy curvetry { const fig = new Figure({ width: 800, height: 400 }); const ax = fig.addAxes(); ax.plot(tensor(history.epochs), tensor(history.trainAcc.map((a) => a * 100)), { color: "#4CAF50", linewidth: 2, }); ax.setTitle("Training Accuracy Curve"); ax.setXLabel("Epoch"); ax.setYLabel("Accuracy (%)"); const svg = fig.renderSVG(); writeFileSync(`${OUTPUT_DIR}/accuracy-curve.svg`, svg.svg); console.log(` Saved: ${OUTPUT_DIR}/accuracy-curve.svg`);} catch (e) { console.log(` Warning: could not generate accuracy curve: ${e}`);} // ============================================================================// Step 8: Summary// ============================================================================ console.log(`\n${"═".repeat(70)}`);console.log(" TRAINING COMPLETE - SUMMARY");console.log("═".repeat(70)); console.log("\nResults Summary:\n");console.log(" Dataset:");console.log(` • ${trainSize} training samples`);console.log(` • ${testSize} test samples`);console.log(` • ${numFeatures} features per sample`); console.log("\n Model:");console.log(` • Architecture: Simple MLP`);console.log(` • Hidden Size: 128`);console.log(` • Parameters: ${summary.numParameters}`); console.log("\n Training:");console.log(` • Epochs: ${NUM_EPOCHS}`);console.log(` • Final Loss: ${history.trainLoss[history.trainLoss.length - 1].toFixed(4)}`);console.log( ` • Final Train Acc: ${(history.trainAcc[history.trainAcc.length - 1] * 100).toFixed(2)}%`); console.log("\n Evaluation:");console.log(` • Test Accuracy: ${(testAccuracy * 100).toFixed(2)}%`); console.log("\nOutput Files:");console.log(` • ${OUTPUT_DIR}/loss-curve.svg`);console.log(` • ${OUTPUT_DIR}/accuracy-curve.svg`); console.log(`\n${"═".repeat(70)}`);console.log(" Neural Network Image Classifier Complete!");console.log("═".repeat(70));Output
Training progress with loss and accuracy every 5 epochs
Test metrics and a confusion matrix
`output/loss-curve.svg`
`output/accuracy-curve.svg`