Mathew K Analytics

Lesson 53 · Data Science Projects

Logistic Regression for Handwritten Digit Recognition Using PyTorch: Step-by-Step Tutorial

Introduction to deep learning for digit recognition Why digit recognition matters in the real world How PyTorch and logistic regression help solve this…

⬇ Download notebookOpen in Colab ↗

What you'll learn

Data

No separate download needed — the notebook creates or downloads everything it uses.

📓 Full notebook

Download .ipynb

Identifying Handwritten Digits with Logistic Regression in PyTorch#

  • Introduction to deep learning for digit recognition

  • Why digit recognition matters in the real world

  • How PyTorch and logistic regression help solve this problem

  • What you will learn step by step

About the Dataset#

  • We use the Digits dataset from scikit learn
  • It contains 8x8 images of handwritten digits 0 through 9
  • Each image is labeled with the correct digit
  • This is a simple version of the classic MNIST problem
  • Great for learning and quick experiments
# Always suppress warnings for cleaner output
import warnings; warnings.filterwarnings("ignore")
import numpy as np
np.random.seed(42)
# Data setup
from sklearn.datasets import load_digits
digits = load_digits()
X, y = digits.data, digits.target
print(X.shape)
print(y.shape)
(1797, 64)
(1797,)
# Let us print and view the first image
import matplotlib.pyplot as plt
plt.gray()
plt.matshow(digits.images[0])
plt.title(f"True Label: {digits.target[0]}")
plt.show()
<Figure size 640x480 with 0 Axes>
No description has been provided for this image

What is Logistic Regression?#

  • A simple classification algorithm for dividing data

  • Works by estimating class probabilities

  • For digits, it predicts which number best fits the input image

  • Despite its name, logistic regression is used for classification

# Let us check the distribution of digit labels
import pandas as pd
import seaborn as sns
sns.set_style("whitegrid")
df_labels = pd.DataFrame({'label': y})
sns.countplot(x='label', data=df_labels, palette='viridis')
plt.title('Number of Samples per Digit')
plt.show()
No description has been provided for this image
# Prepare data for PyTorch: train test split and normalization
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
print(X_train_scaled.shape, X_test_scaled.shape)
(1437, 64) (360, 64)

Introduction to PyTorch#

  • PyTorch is a popular deep learning library
  • It makes building neural networks easy and flexible
  • We use tensors, which are special multi-dimensional arrays
  • PyTorch is especially strong for experiments and rapid ideas
# Convert data to torch tensors and wrap in datasets
import torch
from torch.utils.data import TensorDataset, DataLoader
X_train_tensor = torch.tensor(X_train_scaled, dtype=torch.float32)
X_test_tensor = torch.tensor(X_test_scaled, dtype=torch.float32)
y_train_tensor = torch.tensor(y_train, dtype=torch.long)
y_test_tensor = torch.tensor(y_test, dtype=torch.long)
train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
test_dataset = TensorDataset(X_test_tensor, y_test_tensor)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=32)

PyTorch Logistic Regression Model Structure#

  • One fully connected layer is enough for logistic regression

  • The input layer size matches our number of features (64)

  • The output layer has 10 units (one per digit class)

  • We will not use any hidden layers for now

  • This shows how simple models can be powerful

# Define the logistic regression model in PyTorch
import torch.nn as nn
class LogisticRegressionModel(nn.Module):
    def __init__(self):
        super(LogisticRegressionModel, self).__init__()
        self.linear = nn.Linear(64, 10)
    def forward(self, x):
        outputs = self.linear(x)
        return outputs
model = LogisticRegressionModel()
# Set up loss function and optimizer
import torch.optim as optim
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.1)

Training the Model#

  • We go through the data in batches for better memory use
  • On each batch, the model makes predictions and measures loss
  • We adjust the model's weights to reduce that loss
  • One full pass through the data is called an epoch
  • More epochs give the model more chances to improve
# Run the training loop
epochs = 20
for epoch in range(epochs):
    model.train()
    total_loss = 0
    for batch_x, batch_y in train_loader:
        optimizer.zero_grad()
        outputs = model(batch_x)
        loss = criterion(outputs, batch_y)
        loss.backward()
        optimizer.step()
        total_loss += loss.item() * batch_x.size(0)
    avg_loss = total_loss / len(train_loader.dataset)
    if (epoch + 1) % 5 == 0 or epoch == 0:
        print(f"Epoch {epoch+1}, Loss: {avg_loss:.4f}")
Epoch 1, Loss: 0.9942
Epoch 5, Loss: 0.2020
Epoch 10, Loss: 0.1344
Epoch 15, Loss: 0.1065
Epoch 20, Loss: 0.0899
# Evaluate test accuracy after training
model.eval()
correct = 0
total = 0
with torch.no_grad():
    for batch_x, batch_y in test_loader:
        outputs = model(batch_x)
        _, predicted = torch.max(outputs, 1)
        correct += (predicted == batch_y).sum().item()
        total += batch_y.size(0)
accuracy = correct / total * 100
print(f"Test Accuracy: {accuracy:.2f}%")
Test Accuracy: 96.67%
# Show predictions for some test images
import numpy as np
examples = np.random.choice(len(X_test_tensor), 8, replace=False)
model.eval()
with torch.no_grad():
    for idx in examples:
        img = X_test_tensor[idx].numpy().reshape(8, 8)
        out = model(X_test_tensor[idx].unsqueeze(0))
        pred = out.argmax(dim=1).item()
        plt.imshow(img, cmap='gray')
        plt.title(f"Predicted: {pred}, True: {y_test[idx]}")
        plt.axis('off')
        plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
# Look at a confusion matrix for test results
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
import matplotlib.pyplot as plt
all_preds = []
all_true = []
model.eval()
with torch.no_grad():
    for batch_x, batch_y in test_loader:
        outputs = model(batch_x)
        _, predicted = torch.max(outputs, 1)
        all_preds.extend(predicted.numpy())
        all_true.extend(batch_y.numpy())
cm = confusion_matrix(all_true, all_preds)
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=np.arange(10))
disp.plot(cmap='Blues')
plt.title('Confusion Matrix: Test Set')
plt.show()
No description has been provided for this image

Try it Yourself: Predict Your Own Digit Image#

  • Convert a simple 8x8 sketch of a digit into a numpy array

  • Follow the same pre processing and use the model for prediction

  • See how well the model handles new handwritten styles

  • Practicing with your own samples is a great way to learn

  • Post your results or questions in the YouTube comments below!

# Optional: Type a digit image as numbers for prediction
import numpy as np
user_pixels = []
print("Enter 64 pixel values (0 16) for an 8x8 image, separated by space:")
for i in range(8):
    row_values = input(f"Row {i+1}/8: ").split()
    user_pixels.extend([int(x) for x in row_values])
user_img = np.array(user_pixels).reshape(1, -1)
user_img_scaled = scaler.transform(user_img)
user_tensor = torch.tensor(user_img_scaled, dtype=torch.float32)
with torch.no_grad():
    output = model(user_tensor)
    predicted = output.argmax(dim=1).item()
print(f"Predicted digit: {predicted}")
Enter 64 pixel values (0 16) for an 8x8 image, separated by space:
Predicted digit: 4

Where to Go from Here?#

  • Try increasing epochs to see how accuracy improves
  • Replace logistic regression with deeper layers for even better results
  • Try with the larger MNIST dataset next
  • Play with optimizers and learning rates
  • Share your experiments, and subscribe for more tutorials!

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.