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.
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
mnistNPM 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
aand
bis 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
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.