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

Let’s visualize one example from each input class

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

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

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.

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}

Training the model

Plot training curves

Evaluate test accuracy

Examine failed cases

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.

Variations

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}

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}

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}

Helper functions