Deep Learning β€Ί Neural Network Foundations β€Ί Day 203

Day 203: Training MNIST from Scratch

Day 203 of 365 β€” Training MNIST from Scratch

Train a complete two-layer neural network on the classic MNIST benchmark in pure NumPy: parse image binary tensors, implement vectorized mini-batch SGD with Momentum, reach >= 95% test accuracy, and conduct error analysis on misclassified digits.

Course
Deep Learning
Category
Neural Network Foundations
Reading time
β‰ˆ 35 min
Practical time
β‰ˆ 50 min
Lesson duration
1h 25m
Last verified
2026-08-29

Hands-on lab for this lesson

Lab files on GitHub: https://github.com/ai-roadmap-365/ai-roadmap-365.github.io/tree/main/labs/sections/deep-learning/day-203-training-mnist-from-scratch

  1. Get the hands-on files. Clone the labs repository once (you can reuse this clone for every lesson). This works on macOS, Linux, and Windows (PowerShell or WSL):
    git clone https://github.com/ai-roadmap-365/ai-roadmap-365.github.io.git
    cd ai-roadmap-365.github.io
  2. Open this lesson's lab. Move into the directory for this specific day. Every lab lives at the same predictable path β€” section / subsection / week / day:
    cd labs/sections/deep-learning/day-203-training-mnist-from-scratch
  3. Read the lab guide. Open `README.md` in that directory. It lists the exact commands, what each does, the expected output, and how to check your work β€” read it before running anything.
  4. Run it and check your work. Follow the README's "How to run" section: run the example first to see the finished result, then complete the numbered exercises in `starter/`, then run the tests. The tests pass (exit 0) only when your work is correct.
    bash tests/run_tests.sh   # or the test command named in the lab README

You can also open the lab as a local page (works offline, shows the file tree and expected output).

Learning objectives

By the end of this lesson you will be able to:

Prerequisites

Why this matters

In 1998, Yann LeCun, Leon Bottou, Yoshua Bengio, and Patrick Haffner introduced the MNIST (Modified National Institute of Standards and Technology) database of handwritten digits. For over two decades, MNIST has served as the β€œHello World” benchmark of deep learning and computer vision.

Today, you will complete the foundational milestone of Week 29: building, training, and evaluating a complete two-layer neural network on the full MNIST dataset in pure NumPy from scratch β€” achieving over 95% test accuracy.

Training a neural network on 70,000 real-world handwritten images tests every mathematical concept you mastered this week:


The idea in plain language

Imagine teaching a postal sorting machine to read zip codes written by thousands of different people:

Your two-layer neural network solves this by learning 128 specialized visual feature detectors:

The final output layer combines all 128 feature detections into a calibrated probability distribution, voting on whether the image is digit 0, 1, 2, …, or 9.


Historical background

  1. 1998 (Yann LeCun et al.): Released the MNIST dataset, compiling 60,000 training images and 10,000 testing images from high school students and US Census Bureau employees.
  2. 1998 (Classical Baseline): LeCun demonstrated that a two-layer Multi-Layer Perceptron with 300 hidden units reached ~95.3% accuracy, while early LeNet-5 Convolutional Networks reached 99.05%.
  3. 2010 (Ciresan et al.): Demonstrated that standard deep Multi-Layer Perceptrons trained on GPUs with elastic distortions could achieve 99.65% accuracy, approaching human error rates (~99.8%).
  4. Today: MNIST serves as the definitive calibration dataset for validating custom neural network architectures and backpropagation engines.

What it is β€” and what it is not

What Training MNIST from Scratch IS:

What it is NOT:


Why it was created and what problems it solves

Synthetic 2D toy datasets (like Two Moons or Concentric Circles from Day 201) are excellent for visualizing decision boundaries, but they have only 2 input features and a few hundred samples.

Training on MNIST solves the challenge of scaling neural networks to real-world high-dimensional data:

Demonstrating that our pure NumPy engine converges on MNIST proves that the mathematical derivations from Days 197–202 scale to real computer vision problems.


How it works

Let us analyze the complete MNIST network architecture, parameter dimensions, weight initialization, training loop, and error analysis.

MNIST handwritten digit classification pipeline showing 28x28 image flattening 784 input neurons 128 hidden ReLU units and 10 Softmax output probabilities


1. Dataset Preprocessing and Dimensional Contracts

Each MNIST sample is a 28 x 28 grayscale image where pixel values are integers in [0, 255]:

Step 1: Pixel Normalization

Scale integer intensities to floating-point numbers in [0.0, 1.0]:

X_norm = X_raw.astype(np.float32) / 255.0

Step 2: 2D Spatial Flattening

Flatten each 28 x 28 matrix into a 784-dimensional column vector:

X in R^{784 x m}

where m is the mini-batch size (e.g. 64).

Step 3: One-Hot Target Encoding

Transform scalar class labels y in {0, 1, ..., 9} into 10-dimensional binary vectors:

Y in {0, 1}^{10 x m}

2. The Two-Layer Architecture: [784 -> 128 -> 10]

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   MNIST TWO-LAYER ARCHITECTURE MATRIX                  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Layer        β”‚ Input Shape      β”‚ Parameter Shape β”‚ Output Shape / Act β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Layer 0 (In) β”‚ (784, m)         β”‚ N/A             β”‚ A^[0] = X (784, m) β”‚
β”‚ Layer 1 (Hid)β”‚ (784, m)         β”‚ W1: (128, 784)  β”‚ Z1: (128, m)       β”‚
β”‚              β”‚                  β”‚ b1: (128, 1)    β”‚ A1 = ReLU(Z1)      β”‚
β”‚ Layer 2 (Out)β”‚ (128, m)         β”‚ W2: (10, 128)   β”‚ Z2: (10, m)        β”‚
β”‚              β”‚                  β”‚ b2: (10, 1)     β”‚ A2 = Softmax(Z2)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Total Trainable Parameters:


3. He Parameter Initialization

To prevent vanishing or exploding gradients across 100,000 parameters:

W1 = np.random.randn(128, 784) * np.sqrt(2.0 / 784.0)
b1 = np.zeros((128, 1))

W2 = np.random.randn(10, 128) * np.sqrt(2.0 / 128.0)
b2 = np.zeros((10, 1))

4. Vectorized Training Dynamics and Metrics

MNIST neural network training curves showing cross entropy loss decay and validation accuracy reaching 96 percent across training epochs

The Training Loop per Mini-Batch:

  1. Forward Pass:
    Z1 = np.dot(W1, X_batch) + b1
    A1 = np.maximum(0.0, Z1)
    Z2 = np.dot(W2, A1) + b2
    exp_Z2 = np.exp(Z2 - np.max(Z2, axis=0, keepdims=True))
    A2 = exp_Z2 / np.sum(exp_Z2, axis=0, keepdims=True)
  2. Loss Calculation:
    Loss = - (1 / m) * np.sum(Y_batch * np.log(A2 + 1e-15))
  3. Backpropagation:
    dZ2 = A2 - Y_batch
    dW2 = (1 / m) * np.dot(dZ2, A1.T)
    db2 = (1 / m) * np.sum(dZ2, axis=1, keepdims=True)
    
    dA1 = np.dot(W2.T, dZ2)
    dZ1 = dA1 * np.where(Z1 > 0.0, 1.0, 0.0)
    dW1 = (1 / m) * np.dot(dZ1, X_batch.T)
    db1 = (1 / m) * np.sum(dZ1, axis=1, keepdims=True)
  4. Momentum Optimization Step (beta = 0.9, alpha = 0.1):
    V_dW1 = 0.9 * V_dW1 + 0.1 * dW1
    W1 -= alpha * V_dW1
    # (repeated for b1, W2, b2)

5. Visualizing Learned First-Layer Feature Filters

Each of the 128 rows of W1 contains 784 numbers corresponding to the 784 input pixels. When we reshape row i of W1 back into a 28 x 28 image:


An everyday analogy

Think of a panel of 128 handwriting analysts inspecting a document:


Examples in practice

Let us inspect the complete MNIST pure NumPy training script:

import numpy as np
from typing import Tuple, Dict

class MNISTClassifier:
    def __init__(self, hidden_dim: int = 128, lr: float = 0.1, momentum: float = 0.9):
        self.hidden_dim = hidden_dim
        self.lr = lr
        self.beta = momentum

        # He initialization
        np.random.seed(42)
        self.W1 = np.random.randn(hidden_dim, 784) * np.sqrt(2.0 / 784.0)
        self.b1 = np.zeros((hidden_dim, 1))
        self.W2 = np.random.randn(10, hidden_dim) * np.sqrt(2.0 / hidden_dim)
        self.b2 = np.zeros((10, 1))

        # Velocities
        self.V_dW1 = np.zeros_like(self.W1)
        self.V_db1 = np.zeros_like(self.b1)
        self.V_dW2 = np.zeros_like(self.W2)
        self.V_db2 = np.zeros_like(self.b2)

    def forward(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
        Z1 = np.dot(self.W1, X) + self.b1
        A1 = np.maximum(0.0, Z1)

        Z2 = np.dot(self.W2, A1) + self.b2
        exp_Z2 = np.exp(Z2 - np.max(Z2, axis=0, keepdims=True))
        A2 = exp_Z2 / np.sum(exp_Z2, axis=0, keepdims=True)

        return Z1, A1, Z2, A2

    def train_epoch(self, X: np.ndarray, Y: np.ndarray, batch_size: int = 64) -> float:
        m = X.shape[1]
        p = np.random.permutation(m)
        X_shuf = X[:, p]
        Y_shuf = Y[:, p]

        num_batches = int(np.ceil(m / batch_size))
        total_loss = 0.0

        for b in range(num_batches):
            start = b * batch_size
            end = min(start + batch_size, m)
            X_b = X_shuf[:, start:end]
            Y_b = Y_shuf[:, start:end]
            bs = end - start

            Z1, A1, Z2, A2 = self.forward(X_b)
            loss = - (1.0 / bs) * np.sum(Y_b * np.log(A2 + 1e-15))
            total_loss += loss * bs

            # Backward pass
            dZ2 = A2 - Y_b
            dW2 = (1.0 / bs) * np.dot(dZ2, A1.T)
            db2 = (1.0 / bs) * np.sum(dZ2, axis=1, keepdims=True)

            dA1 = np.dot(self.W2.T, dZ2)
            dZ1 = dA1 * np.where(Z1 > 0.0, 1.0, 0.0)
            dW1 = (1.0 / bs) * np.dot(dZ1, X_b.T)
            db1 = (1.0 / bs) * np.sum(dZ1, axis=1, keepdims=True)

            # Momentum updates
            self.V_dW2 = self.beta * self.V_dW2 + (1.0 - self.beta) * dW2
            self.V_db2 = self.beta * self.V_db2 + (1.0 - self.beta) * db2
            self.V_dW1 = self.beta * self.V_dW1 + (1.0 - self.beta) * dW1
            self.V_db1 = self.beta * self.V_db1 + (1.0 - self.beta) * db1

            self.W2 -= self.lr * self.V_dW2
            self.b2 -= self.lr * self.V_db2
            self.W1 -= self.lr * self.V_dW1
            self.b1 -= self.lr * self.V_db1

        return total_loss / m

    def evaluate(self, X: np.ndarray, y_labels: np.ndarray) -> Tuple[float, float]:
        _, _, _, A2 = self.forward(X)
        preds = np.argmax(A2, axis=0)
        acc = float(np.mean(preds == y_labels))
        # One-hot
        m = X.shape[1]
        Y = np.zeros((10, m))
        for i in range(m):
            Y[y_labels[i], i] = 1.0
        loss = float(- (1.0 / m) * np.sum(Y * np.log(A2 + 1e-15)))
        return loss, acc

Implications: security, privacy, performance, scalability, and cost

  1. CPU Memory Footprint & Cache Locality:
    • Full MNIST dataset (60,000 images x 784 floats x 4 bytes) occupies only ~188 MB of RAM, allowing the entire dataset to reside in system memory for fast zero-disk-I/O training epochs.
  2. Computational Scaling to Laptop CPU:
    • 20 epochs of mini-batch training with batch size 64 on 60,000 images takes less than 15 seconds on a modern multi-core laptop CPU, providing rapid experimental feedback without cloud GPU costs.

Alternatives: free, open source, and commercial

ArchitectureParametersMNIST AccuracyTraining Time (CPU)
Pure NumPy 2-Layer MLP (Our Model)101,77096.4%~12 seconds
PyTorch 3-Layer MLP200,00098.1%~8 seconds
LeNet-5 Convolutional Net (CNN)60,00099.2%~45 seconds
ResNet-18 Vision Backbone11,000,00099.7%~5 minutes

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 MNIST CLASSIFIER COMPLEXITY COMPARISON                 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Model Type         β”‚ Receptive Field       β”‚ Accuracy on MNIST Test    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Logistic Regressionβ”‚ Global Linear Weight  β”‚ 92.5%                     β”‚
β”‚ 2-Layer MLP (NumPy)β”‚ 128 Non-Linear Stems  β”‚ 96.4%                     β”‚
β”‚ 3-Layer MLP (ReLU) β”‚ Hierarchical Stems    β”‚ 98.1%                     β”‚
β”‚ Convolutional CNN  β”‚ Spatial 2D Convolutionsβ”‚ 99.2%                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

When to use it β€” and when not to

When to USE a Dense MLP for Vision:

When NOT to use it:


Knowledge check

  1. Why are raw MNIST pixel values divided by 255.0 before forward propagation?
  2. What are the exact dimensions of weight matrix W^[1] and weight matrix W^[2] in the [784, 128, 10] architecture?
  3. Why does initial cross-entropy loss start near 2.302 before training begins?
  4. What do the reshaped 28 x 28 weights of hidden neurons in layer 1 represent visually?
  5. Which digit pairs in MNIST exhibit the highest classification confusion, and why?

Hands-on exercise

In this lab, you will build and train a complete MNISTClassifier in pure NumPy: load preprocessed MNIST digits, execute 15 training epochs with mini-batch SGD and Momentum, track loss decay below 0.15, reach $\ge 95.0%$ test accuracy, compute the confusion matrix, and analyze misclassified digit samples.

Expected output

[MNIST Digit Classification in Pure NumPy]
Loading MNIST Dataset (60,000 Train Images, 10,000 Test Images):
  Image Tensor Shape: (784, 60000), Pixel Range: [0.0, 1.0]
Initializing 2-Layer Network Architecture [784 -> 128 (ReLU) -> 10 (Softmax)]
Training Network for 15 Epochs (Batch Size = 64, Learning Rate = 0.1, Momentum = 0.9):
  Epoch  1/15: Train Loss = 0.3842, Test Accuracy = 91.2%
  Epoch  5/15: Train Loss = 0.1415, Test Accuracy = 95.4%
  Epoch 10/15: Train Loss = 0.0892, Test Accuracy = 96.1%
  Epoch 15/15: Train Loss = 0.0581, Test Accuracy = 96.5% [PASSED >= 95% TARGET]
Confusion Matrix Top Error Pairs:
  Actual 4 -> Predicted 9: 18 errors
  Actual 7 -> Predicted 1: 14 errors
  Actual 3 -> Predicted 5: 12 errors
Test Suite: 3 passed in 0.20s

Validate your work

Run the automated test runner:

./tests/run_tests.sh

Troubleshooting

Common mistakes


Practice assignment

  1. Implement L2 Weight Regularization (lambda = 0.001) and measure its impact on the test generalization gap.
  2. Render an image grid displaying the top 16 most confidently misclassified digits.

Extension challenge

Implement a Three-Layer MLP Architecture [784 -> 256 -> 64 -> 10]:

Quiz

Q1. Why are raw pixel values in MNIST (which range as unsigned 8-bit integers from 0 to 255) normalized by dividing by 255.0 to [0.0, 1.0] before feeding into the neural network?

  1. Normalization keeps input features in a small numerical range, preventing large pre-activation values Z^[1] that would saturate activations and cause numerical gradient instability
  2. To convert the images from color to black and white
  3. To compress the image file size on disk
  4. To make the image transparent
Show answer

Answer: A. Normalization keeps input features in a small numerical range, preventing large pre-activation values Z^[1] that would saturate activations and cause numerical gradient instability

Unscaled pixel inputs (up to 255) multiplied by weights would produce huge linear sums Z^[1], causing exploding gradients or saturation. Scaling to [0, 1] ensures stable, well-conditioned optimization.

Q2. In the [784, 128, 10] MNIST network architecture, how many total trainable parameters (weights + biases) exist across both layers?

  1. 101,770 parameters (Layer 1: 128 * 784 = 100,352 weights + 128 biases; Layer 2: 10 * 128 = 1,280 weights + 10 biases)
  2. 784 parameters
  3. 10,000 parameters
  4. 1,000,000 parameters
Show answer

Answer: A. 101,770 parameters (Layer 1: 128 * 784 = 100,352 weights + 128 biases; Layer 2: 10 * 128 = 1,280 weights + 10 biases)

Layer 1 has 100,352 weights and 128 biases (100,480 total). Layer 2 has 1,280 weights and 10 biases (1,290 total). Sum = 100,480 + 1,290 = 101,770 parameters.

Q3. What theoretical value is the initial cross-entropy loss expected to be on epoch 0 before training begins when weights are randomly initialized?

  1. Approximately ln(10) ~ 2.3026, because random weights assign equal 10% probability (0.10) to all 10 digit classes: -ln(0.10) = 2.3026
  2. 0.0000
  3. 100.00
  4. 1.0000
Show answer

Answer: A. Approximately ln(10) ~ 2.3026, because random weights assign equal 10% probability (0.10) to all 10 digit classes: -ln(0.10) = 2.3026

Prior to learning, a symmetric classifier assigns uniform probability 1/K = 1/10 to each class. The initial loss is -ln(1/10) = ln(10) = 2.3026. Verifying this at epoch 0 is a standard sanity check.

Q4. What do the 128 rows of the first-layer weight matrix W^[1] (each containing 784 numbers) represent visually when reshaped back into 28x28 images?

  1. They represent learned spatial feature detectors (receptive fields) that fire when specific visual patterns β€” like horizontal crossbars, loops, diagonals, or corners β€” appear in the input digit
  2. They represent corrupted noise images
  3. They represent JPEG compression artifacts
  4. They represent random Gaussian noise that never changes
Show answer

Answer: A. They represent learned spatial feature detectors (receptive fields) that fire when specific visual patterns β€” like horizontal crossbars, loops, diagonals, or corners β€” appear in the input digit

Each hidden neuron acts as a specialized template matcher: its 784 weights form a spatial filter tuned to detect specific pen stroke features across the 28x28 canvas.

Q5. When analyzing the confusion matrix of a trained MNIST model, which pairs of digits are most commonly confused by the network, and why?

  1. Digits (4 and 9), (3 and 5), or (7 and 1), because human handwriting variations in these digit pairs share nearly identical pen stroke geometry
  2. Digits 0 and 1, because they look identical
  3. Digits 8 and 8
  4. No digits are ever confused
Show answer

Answer: A. Digits (4 and 9), (3 and 5), or (7 and 1), because human handwriting variations in these digit pairs share nearly identical pen stroke geometry

Handwritten 4s with closed tops resemble 9s, and sloppy 7s resemble 1s. Inspecting these confusion clusters provides insight into dataset ambiguity.

Glossary

MNIST Benchmark
The canonical machine learning dataset consisting of 70,000 28x28 grayscale images of handwritten digits from 0 to 9.
Feature Flattening
Transforming a multi-dimensional spatial grid (e.g., 28x28 pixels) into a 1D vector of length 784.
Pixel Normalization
Rescaling raw integer pixel intensities from [0, 255] to floating-point values in [0.0, 1.0].
Receptive Field Filter
Visualizing a neuron 784 weights as a 28x28 spatial image to observe the learned visual pattern it detects.
Confusion Matrix
A K x K contingency table recording actual versus predicted digit class frequencies to identify systematic classification errors.
Two-Layer Multi-Layer Perceptron
A neural network architecture with one hidden layer and one output layer, capable of learning non-linear classification boundaries.
One-Hot Encoding
Representing a categorical digit target k in {0..9} as a 10-dimensional binary vector with a 1 at index k and 0s elsewhere.
Error Analysis
The practice of manually inspecting misclassified samples to diagnose dataset noise, label ambiguity, or model blind spots.

Sources and further reading


Kept in this browser, no account needed. Your progress page turns the whole record into one link you can bookmark or open on another device.