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…
- CourseData Science Projects
- Lesson53 of 33
- Video24 min
- FormatJupyter notebook · 13 code cells
What you'll learn
Data
No separate download needed — the notebook creates or downloads everything it uses.
📓 Full notebook
Download .ipynbIdentifying 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)
# 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()
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()
# 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)
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}")
# 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}%")
# 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()
# 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()
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}")
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.



