Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Deep Neural Networks (from scratch)

Dataset: MNIST handwritten digits

This is a publicly-available handwritten digit dataset link. It’s also available in nearly every ML library

Inputs have size (70000, 784)
Targets have size (70000,)

Let’s visualize one example from each input class

<Figure size 640x480 with 10 Axes>

To visualize the distribution of possible inputs, let’s use PCA

<Figure size 640x480 with 1 Axes>

To prepare for our deep learning expedition, let’s get a training, validation, and test set

The training set has size (44800, 784), (44800, 10)
The validation set has size (11200, 784), (11200, 10)
The testing set has size (14000, 784), (14000, 10)

Finally, let’s establish a baseline. We are going to use mean-squared error loss here

L=∑i=1N(ypred−ytrue)2\begin{align} \mathcal{L} = \sum_{i=1}^N (y_{\text{pred}} - y_{\text{true}})^2 \end{align}

PS1 will cover cross entropy loss.

Logistic regression is as good a place as any to start. But this time we’ll use the sklearn implementation rather than our own. This uses a 2nd order L-BFGS solver which should in principle do a bit better than gradient descent.

/opt/conda/lib/python3.11/site-packages/sklearn/linear_model/_logistic.py:469: ConvergenceWarning: lbfgs failed to converge (status=1):
STOP: TOTAL NO. of ITERATIONS REACHED LIMIT.

Increase the number of iterations (max_iter) or scale the data as shown in:
    https://scikit-learn.org/stable/modules/preprocessing.html
Please also refer to the documentation for alternative solver options:
    https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression
  n_iter_i = _check_optimize_result(
Train MSE = 0.226	Train Accuracy = 0.844
Test MSE = 0.223	Test Accuracy = 0.847

Multi-layer perceptron class definition

Let’s define a 2-layer MLP with sigmoid activations

zi(1)=∑j=1Nixjwji(1)+bi(1)i=1,…,NhLayer 1 preactivationai(1)=σ(zi(1))i=1,…,NhLayer 1 activationzi(2)=∑j=1Nhaj(1)wji(2)+bi(2)i=1,…,NoLayer 2 preactivationai(2)=σ(zi(2))i=1,…,NoLayer 2 activation\begin{align} z^{(1)}_i &= \sum_{j=1}^{N_i} x_j w^{(1)}_{ji} + b^{(1)}_i & i = 1, \dots, N_h & & \mbox{Layer 1 preactivation} \\ a^{(1)}_i &= \sigma(z^{(1)}_i ) & i = 1, \dots, N_h & & \mbox{Layer 1 activation} \\ \\ z^{(2)}_i &= \sum_{j=1}^{N_h} a^{(1)}_j w^{(2)}_{ji} + b^{(2)}_i & i = 1, \dots, N_o & & \mbox{Layer 2 preactivation} \\ a^{(2)}_i &= \sigma(z^{(2)}_i ) & i = 1, \dots, N_o & & \mbox{Layer 2 activation} \end{align}
Initial Val MSE: 2.610, Val Accuracy: 0.125

Training the model

(32, 784) (32, 10)
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...

Plot training curves

<Figure size 500x250 with 2 Axes>

Evaluate test accuracy

Test MSE = 0.117	Test Accuracy = 0.932

Examine failed cases

<Figure size 1000x400 with 10 Axes>

What does the model learn?

Each hidden layer neuron is sensitive to a particular pixel configuration. We can visualize this to get an understanding for the input representations that the model produces.

(784, 64)
<Figure size 1500x1500 with 64 Axes>

Variations

To aid in the examples below, it’s useful to have these helper functions

Helper functions

ReLU MLP

Consider an MLP whose hidden layer uses the ReLU function instead of sigmoid. ReLU is defined as

ReLU(z)={zif z≥00if z<0\begin{align} \text{ReLU}(z) = \begin{cases} z & \text{if } z \ge 0 \\ 0 & \text{if } z < 0 \end{cases} \end{align}

[Question:] What is its derivative?

With this our network is now defined as

zi(1)=∑j=1Nixjwji(1)+bi(1)i=1,…,NhLayer 1 preactivationai(1)=ReLU(zi(1))i=1,…,NhLayer 1 activationzi(2)=∑j=1Nhaj(1)wji(2)+bi(2)i=1,…,NoLayer 2 preactivationai(2)=σ(zi(2))i=1,…,NoLayer 2 activation\begin{align} z^{(1)}_i &= \sum_{j=1}^{N_i} x_j w^{(1)}_{ji} + b^{(1)}_i & i = 1, \dots, N_h & & \mbox{Layer 1 preactivation} \\ a^{(1)}_i &= \text{ReLU}(z^{(1)}_i ) & i = 1, \dots, N_h & & \mbox{Layer 1 activation} \\ \\ z^{(2)}_i &= \sum_{j=1}^{N_h} a^{(1)}_j w^{(2)}_{ji} + b^{(2)}_i & i = 1, \dots, N_o & & \mbox{Layer 2 preactivation} \\ a^{(2)}_i &= \sigma(z^{(2)}_i ) & i = 1, \dots, N_o & & \mbox{Layer 2 activation} \end{align}
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Test MSE = 0.108	Test Accuracy = 0.938

3-layer MLP

zi(1)=∑j=1Nixjwji(1)+bi(1)i=1,…,NhLayer 1 preactivationai(1)=σ(zi(1))i=1,…,NhLayer 1 activationzi(2)=∑j=1Nhaj(1)wji(2)+bi(2)i=1,…,NhLayer 2 preactivationai(2)=σ(zi(2))i=1,…,NhLayer 2 activationzi(3)=∑j=1Nhaj(2)wji(3)+bi(3)i=1,…,NoLayer 3 preactivationai(3)=σ(zi(3))i=1,…,NoLayer 3 activation\begin{align} z^{(1)}_i &= \sum_{j=1}^{N_i} x_j w^{(1)}_{ji} + b^{(1)}_i & i = 1, \dots, N_h & & \mbox{Layer 1 preactivation} \\ a^{(1)}_i &= \sigma(z^{(1)}_i ) & i = 1, \dots, N_h & & \mbox{Layer 1 activation} \\ \\ z^{(2)}_i &= \sum_{j=1}^{N_h} a^{(1)}_j w^{(2)}_{ji} + b^{(2)}_i & i = 1, \dots, N_h & & \mbox{Layer 2 preactivation} \\ a^{(2)}_i &= \sigma(z^{(2)}_i ) & i = 1, \dots, N_h & & \mbox{Layer 2 activation} \\ \\ z^{(3)}_i &= \sum_{j=1}^{N_h} a^{(2)}_j w^{(3)}_{ji} + b^{(3)}_i & i = 1, \dots, N_o & & \mbox{Layer 3 preactivation} \\ a^{(3)}_i &= \sigma(z^{(3)}_i ) & i = 1, \dots, N_o & & \mbox{Layer 3 activation} \end{align}
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Test MSE = 0.063	Test Accuracy = 0.961

N-layer MLP

It would be nice to have a framework to build arbitrarily deep multi-layer perceptrons without having to type so much, right? There is a compositional structure to these, so we should be able to loop some of the computation. For example, each layer generally computes

zi(ℓ)=∑j=1Ni(ℓ)aj(ℓ)wji(ℓ)+bi(ℓ)i=1,…,No(ℓ)ai(ℓ)=f(ℓ)(zi(ℓ))i=1,…,No(ℓ)\begin{align} z^{(\ell)}_i &= \sum_{j=1}^{N_i^{(\ell)}} a^{(\ell)}_j w^{(\ell)}_{ji} + b^{(\ell)}_i & & i = 1, \dots, N^{(\ell)}_o\\ a^{(\ell)}_i &= f^{(\ell)}(z^{(\ell)}_i) & & i = 1, \dots, N^{(\ell)}_o\\ \end{align}