Skip to content

An educational Python project developed during the Neural Network And Deep Learning course, this repository features the implementation of a neural network from scratch, without external libraries, and includes fundamental neural network algorithms.

Notifications You must be signed in to change notification settings

Balzakrez/mnist-neural-network-from-scratch-2025

Repository files navigation

Neural Network for MNIST Classification

An educational implementation of a neural network from scratch, built to understand the fundamentals of deep learning algorithms.

A Python project developed during the Neural Network and Deep Learning course, featuring a complete implementation of a feedforward neural network without using deep learning frameworks. The network is trained and evaluated on the MNIST dataset, a standard benchmark for handwritten digit classification.


Features

Core Implementation

  • From-Scratch Neural Network: Fully custom implementation using only NumPy
  • Flexible Architecture: Support for multiple hidden layers with configurable sizes
  • Multiple Activation Functions: Sigmoid, Tanh, ReLU, and Identity
  • Backpropagation Algorithm: Manual gradient computation and weight updates
  • Cross-Entropy Loss: Optimized for multi-class classification with softmax

Training & Validation

  • Grid Search: Automated hyperparameter exploration across architectures, learning rates, and activations
  • Holdout Validation: Fast initial screening of configuration combinations
  • K-Fold Cross-Validation: Robust performance evaluation of top configurations
  • Early Stopping: Configurable patience to prevent overfitting
  • Incremental Saving: Results saved after each experiment (safe for interruptions)

Results Management

  • Multi-Format Export: JSON (structured), CSV (tabular), and Markdown (readable)
  • Comprehensive Metrics: Training/validation/test accuracy, loss, epochs trained, and timing
  • Statistical Summaries: Automatic performance statistics and ranking
  • Full Reproducibility: All hyperparameters and configurations logged

Visualization

  • Prediction Display: View model predictions on test samples
  • Comparison View: Side-by-side display of true vs predicted labels with images

Quick Start

Installation

# Clone the repository git clone https://github.com/Peppebalzanoo/neural-network-MNIST-2025.git cd neural-network-MNIST-2025 # Install dependencies pip install -r requirements.txt

Run Examples

Explore ready-to-use code examples in examples.py. Open the file and uncomment the example you want to run:

# Edit examples.py and uncomment one or more examples: if __name__ == "__main__": example_direct_training() # Option 1: Direct training # example_holdout_validation() # Option 2: Holdout validation # example_kfold_cross_validation() # Option 3: K-fold CV # example_custom_experiment() # Custom experiment with ExperimentRunner # example_visualize_predictions() # Visualize predictions # example_compare_activations() # Compare activation functions # example_compare_architectures() # Compare architectures

Then run:

python examples.py

Available Examples:

  • example_direct_training() - Manual control over training
  • example_holdout_validation() - Quick training with Trainer
  • example_kfold_cross_validation() - Robust evaluation with K-fold
  • example_custom_experiment() - Custom experiment with automatic result saving
  • example_visualize_predictions() - Visual predictions on test samples
  • example_compare_activations() - Compare sigmoid, tanh, and relu
  • example_compare_architectures() - Compare different network sizes

Run Full Experiments

Execute the complete hyperparameter search pipeline:

python main.py

This automatically:

  1. Loads and preprocesses the MNIST dataset
  2. Runs holdout validation on all configuration combinations
  3. Performs k-fold cross-validation on top 3 configurations
  4. Saves results to experiments_results.json, .csv, and RESULTS.md

Usage

Three approaches with increasing sophistication:

Which Method Should I Use?

Your Goal Method Speed Reliability
Quick experiments Holdout Validation Fast Medium
Final evaluation K-Fold CV Slow High
Custom training logic Manual Control Fast Depends

Option 1: Holdout Validation (Recommended)

Let the Trainer handle data splitting (recommended for quick experiments)

from network import Network from loader import DataLoader from trainer import Trainer from error import cross_entropy from visualization import Visualizer from results_manager import ResultsManager import activation as act # Load data loader = DataLoader("./MNIST") X_train, Y_train, X_test, Y_test = loader.get_train_test_data() # Create and train network network = Network(784, [128], 10, activation_function=act.relu) history = Trainer.holdout_validation( loader, network, cross_entropy, epoch_number=100, eta=0.01, patience=10, train_ratio=0.8 ) # Evaluate on test set Z_test = network.forward_propagation(X_test) test_accuracy = network.get_accuracy(Z_test, Y_test) print(f"Test Accuracy: {test_accuracy:.2%}") # Save results (optional) result = { "hidden_neurons": [128], "eta": 0.01, "activation": "relu", "epoch_number": 100, "patience": 10, "method": "holdout", "test_accuracy": float(test_accuracy), "epochs_trained": history["epochs_trained"], "initial_valid_error": history["initial_valid_error"], "initial_valid_accuracy": history["initial_valid_accuracy"], "final_valid_error": history["final_valid_error"], "final_valid_accuracy": history["final_valid_accuracy"], "best_valid_error": history["best_valid_error"], "status": "success" } ResultsManager.append_result(result, "my_experiments.json") # Visualize predictions (optional) Visualizer.show_multiple_predictions(network, X_test, Y_test, num_samples=5)

Use when: Learning, prototyping, quick experiments
Limitation: Single validation split
Note: Results are NOT saved automatically. Use ResultsManager.append_result() to save.


Option 2: K-Fold Cross-Validation

Most robust evaluation (slower, recommended for final model selection)

from loader import DataLoader from trainer import Trainer from error import cross_entropy from visualization import Visualizer from results_manager import ResultsManager import activation as act # Load data loader = DataLoader("./MNIST") X_train, Y_train, X_test, Y_test = loader.get_train_test_data() # Train with K-fold cross-validation best_model, avg_history = Trainer.kfold_cross_validation( loader, 784, [128], 10, cross_entropy, k=5, epoch_number=200, eta=0.01, patience=10, activation_function=act.relu ) # Evaluate best model Z_test = best_model.forward_propagation(X_test) test_accuracy = best_model.get_accuracy(Z_test, Y_test) print(f"Test Accuracy: {test_accuracy:.2%}") print(f"Avg Validation Accuracy (K-fold): {avg_history['final_valid_accuracy']:.2%}") # Save results (optional) result = { "hidden_neurons": [128], "eta": 0.01, "activation": "relu", "epoch_number": 200, "patience": 10, "method": "k-fold", "k_folds": 5, "test_accuracy": float(test_accuracy), "epochs_trained": int(avg_history["epochs_trained"]), "initial_valid_error": avg_history["initial_valid_error"], "initial_valid_accuracy": avg_history["initial_valid_accuracy"], "final_valid_error": avg_history["final_valid_error"], "final_valid_accuracy": avg_history["final_valid_accuracy"], "best_valid_error": avg_history["best_valid_error"], "status": "success" } ResultsManager.append_result(result, "my_experiments.json") # Visualize predictions (optional) Visualizer.show_multiple_predictions(best_model, X_test, Y_test, num_samples=10)

Use when: Thesis results, final evaluation, comparing architectures
Note: Trains K models (5x slower than holdout). Results are NOT saved automatically.


Option 3: Manual Control (Advanced)

Manual control over data splitting and training

from network import Network from loader import DataLoader from error import cross_entropy from visualization import Visualizer from results_manager import ResultsManager import activation as act # Load and manually split data loader = DataLoader("./MNIST") X_train, Y_train, X_test, Y_test = loader.get_train_test_data() X_train_split, Y_train_split, X_valid, Y_valid = loader.split_data(0.8) # Create network network = Network(784, [128], 10, activation_function=act.relu) # Train with full control history = network.fit( X_train_split, Y_train_split, X_valid, Y_valid, error_function=cross_entropy, epoch_number=100, eta=0.01, patience=10 ) # Evaluate Z_test = network.forward_propagation(X_test) test_accuracy = network.get_accuracy(Z_test, Y_test) print(f"Test Accuracy: {test_accuracy:.2%}") print(f"Epochs trained: {history['epochs_trained']}") print(f"Final validation error: {history['final_valid_error']:.4f}") # Save results (optional) result = { "hidden_neurons": [128], "eta": 0.01, "activation": "relu", "epoch_number": 100, "patience": 10, "method": "manual", "test_accuracy": float(test_accuracy), "epochs_trained": history["epochs_trained"], "initial_valid_error": history["initial_valid_error"], "initial_valid_accuracy": history["initial_valid_accuracy"], "final_valid_error": history["final_valid_error"], "final_valid_accuracy": history["final_valid_accuracy"], "best_valid_error": history["best_valid_error"], "status": "success" } ResultsManager.append_result(result, "my_experiments.json") # Visualize predictions (optional) Visualizer.show_prediction(network, X_test, Y_test, random_seed=42)

Use when: Custom data splits, data augmentation, multi-stage training, debugging
Note: Results are NOT saved automatically. Use ResultsManager.append_result() to save.

Common Use Cases:

# 1. Custom data split (first 5000 samples only) X_train_custom = X_train[:5000] Y_train_custom = Y_train[:5000] network.fit(X_train_custom, Y_train_custom, X_valid, Y_valid, cross_entropy, epoch_number=100, eta=0.01, patience=10) # 2. Multi-stage training with different learning rates network.fit(X_train, Y_train, X_valid, Y_valid, cross_entropy, epoch_number=50, eta=0.1, patience=5) # Stage 1: high LR network.fit(X_train, Y_train, X_valid, Y_valid, cross_entropy, epoch_number=50, eta=0.01, patience=5) # Stage 2: low LR # 3. Debugging with small subset X_small = X_train[:1000] Y_small = Y_train[:1000] network.fit(X_small, Y_small, X_valid[:200], Y_valid[:200], cross_entropy, epoch_number=10, eta=0.01, patience=3)

Saving Results

Results are saved in JSON format by default, with optional CSV and Markdown export.

Save Single Experiment

from results_manager import ResultsManager # After training, create result dictionary result = { "hidden_neurons": [128], "eta": 0.01, "activation": "relu", "test_accuracy": 0.9651, # ... other fields } # Save (appends to existing file or creates new) ResultsManager.append_result(result, "my_experiments.json")

Save Multiple Experiments

from results_manager import ResultsManager # Collect results in a list results = [] # Experiment 1 result1 = {...} # Your result dictionary results.append(result1) # Experiment 2 result2 = {...} # Your result dictionary results.append(result2) # Save all at once ResultsManager.save_json(results, "comparison.json") ResultsManager.save_csv(results, "comparison.csv") ResultsManager.generate_markdown_report(results, "COMPARISON.md")

Automatic Saving

When using ExperimentRunner or main.py, results are saved automatically:

from experiments import ExperimentRunner from loader import DataLoader from error import cross_entropy import activation as act loader = DataLoader("./MNIST") X_train, Y_train, X_test, Y_test = loader.get_train_test_data() runner = ExperimentRunner(loader, X_test, Y_test) # Results are automatically saved to experiments_results.json result = runner.run_single_experiment( input_neurons=784, hidden_neurons=[128], output_neurons=10, eta=0.1, epoch_number=100, patience=10, error_function=cross_entropy, activation_function=act.relu, activation_name="relu", use_kfold=False )

Visualizing Results

After training, you can visualize predictions:

from visualization import Visualizer # Show a single prediction with specific seed Visualizer.show_prediction(network, X_test, Y_test, random_seed=42) # Show multiple predictions in a grid Visualizer.show_multiple_predictions(network, X_test, Y_test, num_samples=10)

To view experiment results after running main.py:

# View JSON results cat experiments_results.json # View CSV results (can open in Excel/LibreOffice) cat experiments_results.csv # View Markdown report (human-readable) cat RESULTS.md

Or load results programmatically:

import json # Load experiment results with open('experiments_results.json', 'r') as f: data = json.load(f) # Access experiments experiments = data['experiments'] # Find best configuration best = max(experiments, key=lambda x: x['test_accuracy']) print(f"Best configuration:") print(f" Architecture: {best['hidden_neurons']}") print(f" Activation: {best['activation']}") print(f" Learning rate: {best['eta']}") print(f" Test accuracy: {best['test_accuracy']:.2%}")

Typical Workflow

# 1. Quick test with Holdout network = Network(784, [128], 10, activation_function=act.relu) history = Trainer.holdout_validation(loader, network, cross_entropy, epoch_number=50, eta=0.01, patience=5) # 2. Grid search to find best hyperparameters # Edit config.py, then run: # python main.py # 3. Final validation with K-Fold using best hyperparameters best_model, avg_history = Trainer.kfold_cross_validation( loader, 784, [128], 10, cross_entropy, k=5, epoch_number=200, eta=0.1, patience=10, # Use best eta from step 2 activation_function=act.relu ) # 4. Save final results result = { "hidden_neurons": [128], "eta": 0.1, "activation": "relu", "test_accuracy": float(best_model.get_accuracy(Z_test, Y_test)), # ... other metrics } ResultsManager.append_result(result, "final_results.json")

Customizing Experiments

Configure hyperparameters in config.py to run custom experiments with main.py:

Example 1: Testing Different Architectures

# In config.py, modify: HIDDEN_CONFIGURATIONS = [ [50], # Single layer with 50 neurons [100], # Single layer with 100 neurons [128], # Single layer with 128 neurons [100, 50], # Two layers: 100 -> 50 [128, 64], # Two layers: 128 -> 64 [128, 64, 32], # Three layers: 128 -> 64 -> 32 ]

Then run:

python main.py

This will test all 6 architectures with all combinations of learning rates and activation functions.

Example 2: Fine-Tuning Learning Rates

# In config.py, modify: LEARNING_RATES = [0.01, 0.05, 0.1, 0.5, 1.0] # Test 5 different learning rates # Use only one architecture to speed up HIDDEN_CONFIGURATIONS = [[100]] # Use only best activation from previous experiments ACTIVATION_FUNCTIONS = [ (act.relu, "relu"), ]

Result: Tests 5 learning rates × 1 architecture × 1 activation = 5 experiments

Example 3: Comparing Activation Functions

# In config.py, modify: ACTIVATION_FUNCTIONS = [ (act.sigmoid, "sigmoid"), (act.tanh, "tanh"), (act.relu, "relu"), (act.identity, "identity"), # Add identity for comparison ] # Use fixed architecture and learning rate HIDDEN_CONFIGURATIONS = [[128]] LEARNING_RATES = [0.1]

Result: Tests 4 activation functions on the same architecture

Example 4: Long Training Experiments

# In config.py, modify: EPOCH_NUMBER = 2000 # Train longer PATIENCE = 20 # More patience for early stopping K_FOLDS = 10 # More folds for robust evaluation TOP_N_FOR_KFOLD = 5 # Evaluate top 5 configurations with k-fold

Example 5: Quick Testing Setup

# In config.py, modify for fast iteration: HIDDEN_CONFIGURATIONS = [[50]] # Single small architecture LEARNING_RATES = [0.1] # Single learning rate ACTIVATION_FUNCTIONS = [(act.relu, "relu")] # Single activation EPOCH_NUMBER = 50 # Few epochs PATIENCE = 5 # Early stopping TOP_N_FOR_KFOLD = 1 # Only best config to k-fold

Advanced Examples

Example 1: Single Custom Experiment

Run a specific configuration and save results:

from experiments import ExperimentRunner from loader import DataLoader from error import cross_entropy import activation as act # Setup loader = DataLoader("./MNIST") X_train, Y_train, X_test, Y_test = loader.get_train_test_data() runner = ExperimentRunner(loader, X_test, Y_test) # Run single experiment (automatically saved to experiments_results.json) result = runner.run_single_experiment( input_neurons=784, hidden_neurons=[100, 50], # Two hidden layers output_neurons=10, eta=0.1, # Learning rate epoch_number=500, patience=10, error_function=cross_entropy, activation_function=act.tanh, activation_name="tanh", use_kfold=False # Use holdout instead of k-fold ) # Print results print(f"Test Accuracy: {result['test_accuracy']:.2%}") print(f"Validation Accuracy: {result['final_valid_accuracy']:.2%}") print(f"Epochs Trained: {result['epochs_trained']}") print("✓ Results automatically saved to experiments_results.json")

Example 2: Compare Two Specific Architectures

from experiments import ExperimentRunner from loader import DataLoader from error import cross_entropy import activation as act loader = DataLoader("./MNIST") X_train, Y_train, X_test, Y_test = loader.get_train_test_data() runner = ExperimentRunner(loader, X_test, Y_test) # Test shallow network result_shallow = runner.run_single_experiment( input_neurons=784, hidden_neurons=[256], output_neurons=10, eta=0.1, epoch_number=200, patience=10, error_function=cross_entropy, activation_function=act.relu, activation_name="relu", use_kfold=False ) # Test deep network result_deep = runner.run_single_experiment( input_neurons=784, hidden_neurons=[128, 64, 32], output_neurons=10, eta=0.1, epoch_number=200, patience=10, error_function=cross_entropy, activation_function=act.relu, activation_name="relu", use_kfold=False ) # Compare print(f"Shallow [256]: {result_shallow['test_accuracy']:.2%}") print(f"Deep [128,64,32]: {result_deep['test_accuracy']:.2%}") print("✓ Both results saved to experiments_results.json")

Example 3: Manual Comparison with Custom Saving

from network import Network from loader import DataLoader from trainer import Trainer from error import cross_entropy from results_manager import ResultsManager import activation as act loader = DataLoader("./MNIST") X_train, Y_train, X_test, Y_test = loader.get_train_test_data() # Collect results results = [] # Test different architectures for hidden in [[50], [100], [128, 64]]: network = Network(784, hidden, 10, activation_function=act.relu) history = Trainer.holdout_validation( loader, network, cross_entropy, epoch_number=100, eta=0.1, patience=10 ) Z_test = network.forward_propagation(X_test) test_acc = network.get_accuracy(Z_test, Y_test) result = { "hidden_neurons": hidden, "eta": 0.1, "activation": "relu", "epoch_number": 100, "patience": 10, "method": "holdout", "test_accuracy": float(test_acc), "epochs_trained": history["epochs_trained"], "final_valid_accuracy": history["final_valid_accuracy"], "status": "success" } results.append(result) print(f"Architecture {hidden}: {test_acc:.2%}") # Save all results at once ResultsManager.save_json(results, "architecture_comparison.json") ResultsManager.save_csv(results, "architecture_comparison.csv") ResultsManager.generate_markdown_report(results, "ARCHITECTURE_COMPARISON.md") print("\n✓ Results saved in JSON, CSV, and Markdown formats")

Example 4: Experiment with K-Fold Validation

from experiments import ExperimentRunner from loader import DataLoader from error import cross_entropy import activation as act loader = DataLoader("./MNIST") X_train, Y_train, X_test, Y_test = loader.get_train_test_data() runner = ExperimentRunner(loader, X_test, Y_test) # Run with K-fold cross-validation (automatically saved) result = runner.run_single_experiment( input_neurons=784, hidden_neurons=[128], output_neurons=10, eta=0.1, epoch_number=200, patience=10, error_function=cross_entropy, activation_function=act.relu, activation_name="relu", use_kfold=True, # Enable K-fold k=5 # 5 folds ) # K-fold provides averaged metrics print(f"Test Accuracy: {result['test_accuracy']:.2%}") print(f"Avg Validation Accuracy (5 folds): {result['final_valid_accuracy']:.2%}") print("✓ Results automatically saved to experiments_results.json")

Example 5: Visualize During Training

from network import Network from loader import DataLoader from trainer import Trainer from error import cross_entropy from visualization import Visualizer from results_manager import ResultsManager import activation as act loader = DataLoader("./MNIST") X_train, Y_train, X_test, Y_test = loader.get_train_test_data() # Train network network = Network(784, [128], 10, activation_function=act.relu) history = Trainer.holdout_validation( loader, network, cross_entropy, epoch_number=100, eta=0.1, patience=10 ) # Test and save Z_test = network.forward_propagation(X_test) test_accuracy = network.get_accuracy(Z_test, Y_test) print(f"Test Accuracy: {test_accuracy:.2%}") # Save result result = { "hidden_neurons": [128], "eta": 0.1, "activation": "relu", "test_accuracy": float(test_accuracy), "final_valid_accuracy": history["final_valid_accuracy"], "epochs_trained": history["epochs_trained"], "status": "success" } ResultsManager.append_result(result, "training_with_viz.json") # Show predictions print("\nVisualizing predictions...") Visualizer.show_multiple_predictions(network, X_test, Y_test, num_samples=10)

Project Structure

neural-network-MNIST-2025/ ├── main.py # Main experiment pipeline (auto-saves results) ├── examples.py # Ready-to-run usage examples ├── config.py # Hyperparameter configuration ├── network.py # Neural network core implementation ├── trainer.py # Training strategies (holdout, k-fold) ├── experiments.py # Grid search and experiment management ├── loader.py # MNIST data loading and preprocessing ├── activation.py # Activation functions ├── error.py # Loss functions ├── results_manager.py # Results export (JSON, CSV, Markdown) ├── visualization.py # Prediction visualization ├── requirements.txt # Python dependencies └── MNIST/ # Dataset directory ├── mnist_train.csv └── mnist_test.csv 

Experiment Results

Complete results from the latest experimental run (2025-11-13):

Note: Additional experiments with different architectures are in progress and will be added to this section.

Top Performing Configurations

Rank Architecture Activation η (LR) Test Acc Valid Acc Epochs Valid Error
1 [100] ReLU 0.5 96.51% 96.38% 1000 0.1244
2 [50] ReLU 0.5 96.34% 95.62% 1000 0.1581
3 [100] Tanh 0.5 96.09% 95.86% 1000 0.1474
4 [50] Tanh 0.5 96.00% 95.49% 1000 0.1507
5 [100] Sigmoid 1.0 11.35% 11.22% 11 2.3469

Full Results Summary

Architecture: [50] Hidden Neurons

Activation η Test Acc Valid Acc Epochs Initial Error Final Error Status
Sigmoid 0.1 11.35% 10.81% 19 2.3026 2.3019 Failed
Tanh 0.1 92.02% 91.58% 1000 2.3026 0.2976 Success
ReLU 0.1 91.52% 91.03% 1000 2.3026 0.3087 Success
Sigmoid 0.5 92.75% 92.38% 1000 2.3026 0.2592 Success
Tanh 0.5 96.00% 95.49% 1000 2.3026 0.1507 Success
ReLU 0.5 96.34% 95.62% 1000 2.3026 0.1581 Success
Sigmoid 1.0 94.50% 94.07% 1000 2.3026 0.2107 Success
Tanh 1.0 92.13% 91.82% 145 2.3026 0.2733 Early Stop
ReLU 1.0 39.75% 40.21% 27 2.3026 2.0620 Unstable

Architecture: [100] Hidden Neurons

Activation η Test Acc Valid Acc Epochs Initial Error Final Error Status
Sigmoid 0.1 11.35% 10.93% 14 2.3025 2.3018 Failed
Tanh 0.1 91.97% 91.41% 1000 2.3026 0.2951 Success
ReLU 0.1 91.64% 91.18% 1000 2.3026 0.3161 Success
Sigmoid 0.5 92.61% 92.29% 1000 2.3026 0.2709 Success
Tanh 0.5 96.09% 95.86% 1000 2.3026 0.1474 Success
ReLU 0.5 96.51% 96.38% 1000 2.3026 0.1244 Success
Sigmoid 1.0 11.35% 11.22% 11 2.3026 2.3469 Failed
Tanh 1.0 33.89% 33.73% 26 2.3026 2.9349 Unstable
ReLU 1.0 39.71% 39.53% 26 2.3026 1.6202 Unstable

Output Files

Generated after running experiments:

  • experiments_results.json: Structured results with full details (default format)
  • experiments_results.csv: Tabular format for analysis (open in Excel)
  • RESULTS.md: Human-readable markdown report

Result Saving Summary:

Method Auto-Save? How to Save Manually
main.py Yes N/A
ExperimentRunner.run_single_experiment() Yes N/A
Trainer.holdout_validation() No Use ResultsManager.append_result()
Trainer.kfold_cross_validation() No Use ResultsManager.append_result()
Network.fit() No Use ResultsManager.append_result()

Requirements

  • Python: 3.7+
  • NumPy: Numerical computations
  • Matplotlib: Visualization

See requirements.txt for specific versions.


License

This project is licensed under the MIT License - see the LICENSE file for details.

About

An educational Python project developed during the Neural Network And Deep Learning course, this repository features the implementation of a neural network from scratch, without external libraries, and includes fundamental neural network algorithms.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages