A neural network trained on 60,000 handwritten digits that recognizes 0–9 with 99% accuracy — and a script to test it on your own handwritten images.
MNIST is built into Keras — 70,000 images of handwritten digits, already split into train and test sets.
import tensorflow as tf
import matplotlib.pyplot as plt
# Load MNIST — downloads automatically on first run (~11MB)
(X_train, y_train), (X_test, y_test) = tf.keras.datasets.mnist.load_data()
print(f'Training images: {X_train.shape}') # (60000, 28, 28)
print(f'Test images: {X_test.shape}') # (10000, 28, 28)
print(f'Pixel range: {X_train.min()} to {X_train.max()}')
# Show a few samples
fig, axes = plt.subplots(1, 5, figsize=(10, 2))
for i, ax in enumerate(axes):
ax.imshow(X_train[i], cmap='gray')
ax.set_title(f'Label: {y_train[i]}')
ax.axis('off')
plt.tight_layout()
plt.savefig('sample_digits.png')
Training images: (60000, 28, 28) Test images: (10000, 28, 28) Pixel range: 0 to 255
Normalize pixel values to 0–1 and flatten each 28×28 image into a 784-element vector.
# Normalize: scale pixels from 0-255 to 0-1
X_train = X_train / 255.0
X_test = X_test / 255.0
# Flatten: reshape (60000, 28, 28) → (60000, 784)
X_train_flat = X_train.reshape(-1, 784)
X_test_flat = X_test.reshape(-1, 784)
print(f'Training shape after flatten: {X_train_flat.shape}')
Training shape after flatten: (60000, 784)
A simple 3-layer network (784 → 128 → 64 → 10) is enough to hit 98%+ accuracy on MNIST.
from tensorflow import keras
model = keras.Sequential([
keras.layers.Dense(128, activation='relu', input_shape=(784,)),
keras.layers.Dropout(0.2),
keras.layers.Dense(64, activation='relu'),
keras.layers.Dropout(0.2),
keras.layers.Dense(10, activation='softmax') # 10 outputs = digits 0-9
])
model.compile(
optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy']
)
model.summary()
# Train — takes about 1-2 minutes on CPU
history = model.fit(
X_train_flat, y_train,
epochs=10,
batch_size=128,
validation_split=0.1,
verbose=1
)
Epoch 1/10 - loss: 0.2584 - accuracy: 0.9242 Epoch 5/10 - loss: 0.0789 - accuracy: 0.9763 Epoch 10/10 - loss: 0.0512 - accuracy: 0.9846
Check accuracy on data the model has never seen before.
test_loss, test_accuracy = model.evaluate(X_test_flat, y_test, verbose=0)
print(f'Test accuracy: {test_accuracy:.4f}')
# Show some predictions
import numpy as np
predictions = model.predict(X_test_flat[:5])
predicted_digits = np.argmax(predictions, axis=1)
for i in range(5):
print(f'Image {i}: True={y_test[i]}, Predicted={predicted_digits[i]}, Confidence={predictions[i][predicted_digits[i]]:.1%}')
Test accuracy: 0.9821 Image 0: True=7, Predicted=7, Confidence=99.9% Image 1: True=2, Predicted=2, Confidence=99.7% Image 2: True=1, Predicted=1, Confidence=99.8% Image 3: True=0, Predicted=0, Confidence=99.9% Image 4: True=4, Predicted=4, Confidence=98.4%
Save the trained model and write a script to predict digits from your own drawn images.
# Save the model
model.save('digit_recognizer.keras')
print('Model saved!')
# To predict your own image:
# 1. Draw a digit on white paper and photograph it
# 2. Or draw in MS Paint on a white background and save as PNG
# 3. Run this script:
from PIL import Image
import numpy as np
import tensorflow as tf
model = tf.keras.models.load_model('digit_recognizer.keras')
img = Image.open('my_digit.png').convert('L') # convert to grayscale
img = img.resize((28, 28)) # resize to 28x28
img_array = np.array(img) / 255.0 # normalize
img_flat = img_array.reshape(1, 784) # flatten
# MNIST uses white digits on black — invert if your image is black on white
img_flat = 1 - img_flat
prediction = model.predict(img_flat)
digit = np.argmax(prediction)
confidence = prediction[0][digit]
print(f'Predicted digit: {digit} ({confidence:.1%} confidence)')
Model saved! Predicted digit: 5 (96.3% confidence)
You trained a neural network from scratch to 98% accuracy on 10,000 test images. MNIST is the "Hello World" of deep learning — the same architecture principles (layers, activation functions, dropout, softmax output) apply to any image classification task.
Spotted a bug, broken code, or something that doesn't look right? Tell us what's off and we'll fix it.