Artificial Intelligence 14 min read

Can JavaScript Power Handwritten Digit Recognition? Build a k‑NN Classifier from Scratch

This article walks you through using JavaScript to implement a simple k‑nearest neighbours classifier for the MNIST handwritten digit dataset, covering data representation, preparation, algorithm implementation, testing, performance analysis, and practical deployment considerations.

Taobao Frontend Technology
Taobao Frontend Technology
Taobao Frontend Technology
Can JavaScript Power Handwritten Digit Recognition? Build a k‑NN Classifier from Scratch

Introduction

Is JavaScript suitable for machine learning? Every developer should understand the thinking and methods behind machine learning and consider how it will affect their work; algorithmic ability may become a standard skill for engineers.

Hello Word of Machine Learning

The first "Hello World" in machine learning is recognizing handwritten digits, using the classic MNIST dataset. A web page can provide a canvas for users to draw a digit (0‑9) and display the predicted result.

Data Representation and Collection

Programs must translate an image into numeric pixel values between 0 and 1, where values closer to 1 represent darker pixels. The MNIST dataset provides 28 × 28 pixel images, each represented as a flat array of 784 normalized values.

Each training example consists of the pixel array (input) and a label (output). The output is encoded with a one‑hot vector of length 10, where the position of the value 1 indicates the digit.

<code>0 0 0.3 0 1 0 ... n (=28) | 4
1 0 0.1 0 0 ... n (=28) | 6
....
n (=1000)
</code>

In practice we use the publicly available MNIST dataset, which already provides the data in this format.

Preparing Data

We use the

mnist

NPM package to load the dataset and convert the binary files into JavaScript arrays.

<code>const mnist = require('mnist');
const set = mnist.set(8000, 2000); // 8000 training, 2000 test
const trainingSet = set.training;
const testSet = set.test;
const trainingImages = [];
const labels = [];
trainingSet.forEach(({input, output}) => {
  const number = output.indexOf(Math.max(...output));
  trainingImages.push(input);
  labels.push(number);
});
</code>

Choosing an Algorithm

Given the numeric representation, the task becomes a multi‑class classification problem. We choose the k‑nearest neighbours (k‑NN) algorithm because it is simple, intuitive, and works well for small datasets.

k‑NN Overview

In a 2‑D example, a new point is classified by the majority label among its k closest points (using Euclidean distance). The same principle extends to the 784‑dimensional MNIST vectors.

Euclidean distance between two vectors

a

and

b

is computed as the square root of the sum of squared differences of each component.

<code>function classify(x, trainingData, labels, k) {
  const distances = [];
  trainingData.forEach(element => {
    let distance = 0;
    element.forEach((value, index) => {
      const diff = x[index] - value;
      distance += diff * diff;
    });
    distances.push(Math.sqrt(distance));
  });
  const sortedDistIndices = distances
    .map((value, index) => ({value, index}))
    .sort((a, b) => a.value - b.value);
  const classCount = {};
  for (let i = 0; i < k; i++) {
    const voteLabel = labels[sortedDistIndices[i].index];
    classCount[voteLabel] = (classCount[voteLabel] || 0) + 1;
  }
  let predictedClass = '';
  let topCount = 0;
  for (const voteLabel in classCount) {
    if (classCount[voteLabel] > topCount) {
      predictedClass = voteLabel;
      topCount = classCount[voteLabel];
    }
  }
  return predictedClass;
}
</code>

Testing the Algorithm

We split the data 80 % for training and 20 % for testing, then run the classifier on each test sample and count misclassifications.

<code>let errorCount = 0;
const startTime = Date.now();
testSet.forEach(({input, output}, key) => {
  const number = output.indexOf(Math.max(...output));
  const predicted = classify(input, trainingImages, labels, 3);
  const result = predicted == number;
  if (!result) errorCount++;
});
console.log(`Total errors: ${errorCount}`);
console.log(`Error rate: ${errorCount / testSet.length}`);
console.log(`Time spent: ${(Date.now() - startTime) / 1000}s`);
</code>

With 8 000 training samples and 2 000 test samples the error rate is about 5 % and the whole run takes roughly 325 seconds, which is far from production‑ready.

Deploying the Algorithm

After validation, the classifier can be integrated with a hand‑writing canvas: capture the pixel data, feed it to

classify

, and display the predicted digit.

<code>const input = [0, 0.3, 1, 1, 0, 0, 0.2, ...]; // pixel array from canvas
const predicted = classify(input, trainingImages, labels, 3);
console.log(`Predicted digit: ${predicted}`);
</code>

Further Thoughts

Are there alternative ways to represent handwritten digits besides raw pixel intensities?

Does increasing the training set size always improve performance, or are there diminishing returns?

How can we tune algorithm parameters (k, distance metric, data preprocessing) to achieve better accuracy and speed?

References

Keras.js – MNIST example

MNIST dataset

mnist – NPM package

k‑NN algorithm

Image credit: https://unsplash.com/photos/OFpzBycm3u0 by @jens johnsson
JavaScriptMachine LearningMNISThandwritten digit recognitionk-NN
Taobao Frontend Technology
Written by

Taobao Frontend Technology

The frontend landscape is constantly evolving, with rapid innovations across familiar languages. Like us, your understanding of the frontend is continually refreshed. Join us on Taobao, a vibrant, all‑encompassing platform, to uncover limitless potential.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.