Handwritten Devanagari character recognition. A convolutional network built from scratch in PyTorch classifies the 46 characters of the script, 36 consonants and 10 digits, from 32x32 grayscale images, and a Gradio interface lets you photograph or draw a character and see what the model makes of it.
Devanagari is the script behind Hindi, Marathi, Sanskrit and Nepali. Reading it reliably by machine is the first step towards digitising handwritten records, sorting post, and processing forms across India and Nepal.
96.80% test accuracy, trained on 100 images per class.
Measured on a held out test set of 4,600 images, 100 per class.
| Metric | Value |
|---|---|
| Test accuracy | 96.80% |
| Test loss | 0.1037 |
| Best validation accuracy | 96.52% |
| Correct predictions | 4,453 of 4,600 |
| Macro average F1 | 0.968 |
| Weighted average precision | 0.969 |
| Trainable parameters | 824,206 |
| Saved model size | 3.2 MB |
| Training time | 11.8 minutes, 25 epochs, CPU only |
The headline number is worth reading alongside the data budget. The full DHCD dataset offers roughly 2,000 training images per class. This run deliberately subsamples to 100 per class, about 5% of what was available, leaving 3,910 images for training and 690 for validation. Reaching 96.80% on that budget says more about the architecture and the augmentation than a number trained on the full set would.
Training ran on CPU at roughly 28 seconds per epoch. Early stopping was configured with a patience of 7 but never triggered, so all 25 epochs completed.
Per class accuracy is above 90% for 45 of the 46 characters. The failures cluster where the script itself is ambiguous.
| Character | Accuracy |
|---|---|
| kshya (क्ष) | 84.0% |
| jha (झ) | 90.0% |
| shha (ष) | 92.0% |
| pa (प) | 92.0% |
| bha (भ) | 93.0% |
The pattern is visual similarity rather than anything systematic: श against ष, ब against व, and conjuncts like क्ष that carry more stroke detail than a 32x32 grid comfortably holds. Variation in the shirorekha, the horizontal headline running across the top of each character, accounts for a further share of the confusions.
Three convolutional blocks, each with two convolutions so the network can build richer features before losing spatial resolution, followed by a fully connected classifier head.
Input 1 x 32 x 32
Block 1 Conv 1 -> 32, Conv 32 -> 32 BatchNorm, ReLU, MaxPool 2, Dropout2d 0.25 -> 32 x 16 x 16
Block 2 Conv 32 -> 64, Conv 64 -> 64 BatchNorm, ReLU, MaxPool 2, Dropout2d 0.25 -> 64 x 8 x 8
Block 3 Conv 64 -> 128, Conv 128 -> 128 BatchNorm, ReLU, MaxPool 2, Dropout2d 0.25 -> 128 x 4 x 4
Head Flatten 2048 -> Linear 256, BatchNorm1d, ReLU, Dropout 0.5 -> Linear 46
Every convolution uses a 3x3 kernel with padding 1, so spatial size is reduced only by pooling. Three blocks suit a 32x32 input: a fourth would shrink the feature map below a useful size, and two would not capture enough of the character's structure. Batch normalisation after every convolution keeps training stable at a learning rate of 1e-3. Dropout is the main regulariser, light and spatial inside the blocks at 0.25, heavier at 0.5 before the final layer where the parameter count is concentrated.
| Setting | Value |
|---|---|
| Loss | Cross entropy |
| Optimiser | Adam, lr 1e-3, weight decay 1e-4 |
| Scheduler | ReduceLROnPlateau, factor 0.5, patience 3 |
| Epochs | 25, early stopping patience 7 |
| Batch size | 64 |
| Train and validation split | 85 / 15, seed 42 |
Training images are augmented to match how people actually write: rotation up to 10 degrees, translation up to 10% of the frame, and scaling between 0.9x and 1.1x. Validation and test images get no augmentation, only resize and normalisation, so the numbers above reflect clean inputs. All images are converted to single channel grayscale, resized to 32x32, and normalised to the range [-1, 1].
The Gradio app has two tabs. Upload Image takes a photograph of a character written on paper. Draw lets you sketch one directly in the browser. Both return the predicted character with its confidence and a top-5 breakdown, which is the more honest view when the model is uncertain between visually close characters.
Photographs need more preprocessing than the dataset images do, and the pipeline handles it: grayscale conversion, colour inversion to match the dataset's white on black convention, contrast stretching, and an automatic crop to the character's bounding box before the resize. Skipping the contrast step was the single largest source of wrong predictions on real photographs.
The public Gradio share link from the original submission has expired, as those links last one week. Re-running the final notebook cell issues a new one.
The notebook is self contained and runs top to bottom. It expects the Devanagari Handwritten Character Dataset laid out as Train/ and Test/ folders of per class subfolders, which is how it ships.
Google Colab. Open devanagari_cnn.ipynb, run the upload cell, and pick the dataset zip. It unzips, locates the Train/ folder wherever it ended up nested, and sets the data path itself.
Kaggle or local. Uncomment the matching DATA_DIR line in the dataset setup cell and skip the upload.
A GPU is optional. The published run used CPU throughout and finished in under twelve minutes.
Note that one cell permanently deletes images to subsample the dataset to 100 per class. Run it against a copy, not your only download.
| PyTorch | Model definition, training loop, autograd, Adam, LR scheduling |
| torchvision | ImageFolder loading, augmentation and normalisation transforms |
| scikit-learn | Confusion matrix, per class precision, recall and F1 report |
| NumPy | Array handling, per class accuracy, confusion pair ranking |
| Matplotlib | Loss and accuracy curves, prediction grids, per class bar charts |
| Seaborn | Confusion matrix heatmap |
| Pillow | Photograph preprocessing: inversion, autocontrast, bounding box crop |
| Gradio | Two tab web interface with upload and draw input |
| Google Colab | Hosted runtime, file upload widget, public demo tunnel |
| Jupyter | Notebook format, narrative alongside executable cells |
Python 3 throughout. Matplotlib needs a Devanagari capable font to label plots in the native script; the notebook installs fonts-lohit-deva and falls back to fonts-indic, and transliterated names are used where a glyph is unavailable.
| File | What it is |
|---|---|
devanagari_cnn.ipynb |
The full project: data loading, model, training, evaluation, Gradio app |
devanagari_cnn.pdf |
Rendered copy of the notebook with outputs, readable without Jupyter |
gradio_demo.png |
The interface predicting फ (pha) at 93.7% confidence |
The trained weights, devanagari_cnn_best.pth at 3.2 MB, are written by the notebook's save cell rather than committed here.
