Neural Networks 
Trying to recreate a brain from scratch
What a neural network is
Recognise a handwritten digit.
if rules existsData: MNIST — 60,000 labelled training images, 10,000 for testing.
after 3Blue1Brown, But what is a neural network?
784 → 16 → 16 → 10, as in 3Blue1Brown chapter 1
\[ \mathbf{a}^{(1)} = \sigma\!\left(W_0\,\mathbf{a}^{(0)} + \mathbf{b}_0\right) \]
forward propagation — activations in one layer determine the next
The hope is that layers correspond to levels of abstraction:
A 9 is a loop up top plus a line down the right; an 8 is two loops.
Keep this hope in mind. We will come back and check whether it is true.
after 3Blue1Brown, BreakUpMacroPatterns / BreakUpMicroPatterns
\[ a = \sigma\!\left(w_1 a_1 + w_2 a_2 + \cdots + w_n a_n + b\right) \]
after 3Blue1Brown, IntroduceWeights / IncludeBias
\[ a = \sigma\!\left(w_1 a_1 + w_2 a_2 + \cdots + w_n a_n + b\right) \]
The weights and biases are the only things we get to change. Everything we call “learning” is choosing numbers for them.
there are 784 weights into each hidden neuron — one per pixel — so you can draw them
\[\sigma(x) = \frac{1}{1 + e^{-x}}\]
\[\mathrm{ReLU}(x) = \max(0,\, x) \vphantom{\frac{1}{1 + e^{-x}}}\]
after 3Blue1Brown, IntroduceSigmoid / IntroduceReLU
Without \(\sigma\), a layer computes \(Wa + b\). Stack two of them: \[ W_2(W_1 a + b_1) + b_2 = (W_2 W_1)\, a + (W_2 b_1 + b_2) \]
Still linear. A hundred layers without an activation function collapse into a single linear model — you have re-invented OLS, expensively.
Collect the weights of a layer into a matrix \(W\), the activations and biases into vectors: \[ \mathbf{a}^{(l+1)} = \sigma\!\left(W_l\, \mathbf{a}^{(l)} + \mathbf{b}_l\right) \]
This is the whole forward pass. It is why neural network code is short and why GPUs are involved: a layer is one matrix multiplication.
Counting parameters for 784 → 16 → 16 → 10:
\[ \underbrace{784\cdot 16 + 16 \cdot 16 + 16 \cdot 10}_{\text{12,960 weights}} \;+\; \underbrace{16 + 16 + 10}_{\text{42 biases}} \;=\; \mathbf{13{,}002} \]
How the 13,002 numbers get chosen
Start with random weights and biases. The network will be terrible.
We need to tell it how terrible, with a single number.
For one training image, the cost is \[ C_0 = \sum_{j=0}^{n_L - 1} \left(a^{(L)}_j - y_j\right)^2 \] where \(y_j\) is what we wanted: \(1\) for the correct digit, \(0\) for the other nine.
\[ C = \frac{1}{n}\sum_{k=0}^{n-1} C_k \] the average cost over all \(n\) training examples.
Now look at what kind of object \(C\) is:
Learning = minimising \(C\). Which is now an ordinary, if enormous, calculus problem.
after 3Blue1Brown, SingleVariableCostFunction
Solving \(\frac{dC}{dw} = 0\) is hopeless for a function this complicated.
So do the stupid thing instead: start anywhere, look at the slope, take a step downhill, repeat. \[ w \leftarrow w - \eta \, \frac{dC}{dw} \]
after 3Blue1Brown, TwoVariableInputSpace / LocalVsGlobal
For a function of many variables, the gradient \(\nabla C\) is the vector of all partial derivatives. It points in the direction of steepest increase.
So \(-\nabla C\) points downhill. The algorithm is one line:
In 13,002 dimensions you cannot picture the surface — but “compute the gradient, step against it” needs no picture.
after 3Blue1Brown, ShowFullCostFunctionGradient
\(-\nabla C\) has 13,002 components, one per parameter. Each tells you two things:
A component of \(+0.82\) against one of \(+0.03\) says: changing the first weight buys you 27 times more than changing the second.
Two things worth noticing:
This network gets ~96% of unseen digits right. Tuned a little, ~98%.
So the second layer must be detecting loops and edges, as we hoped?
Look at the weights again: mostly noise with a loose pattern in the middle. It found some structure in the training set that happens to work. Not our structure.
Worse: feed it random noise and it will confidently name a digit. It has not learned to recognise digits — it has learned to sort the 60,000 images it was shown.
This gap between “high test accuracy” and “learned the concept” is the single most important caveat when you report results from a black box.
Where the gradient actually comes from
Gradient descent needs \(\nabla C\): 13,002 partial derivatives, recomputed at every single step.
Backpropagation is the algorithm that computes them all in one backward sweep, at roughly the cost of one forward pass.
The intuition first, the calculus after.
after 3Blue1Brown, WalkThroughTwoExample
Take \(a^{(L)} = \sigma\!\left(w_1 a_1 + \cdots + w_n a_n + b\right)\) and ask how to increase it:
Only 1 and 2 are ours to change. Number 3 is a request, passed back to the previous layer.
Point 2 is Hebbian theory in miniature: neurons that fire together, wire together. The biggest weight increases go to the connections that were already most active.
The neuron for “2” wants the previous layer to change. So do the other nine output neurons — and they disagree.
Add up all ten requests. That gives a desired nudge for every neuron in layer \(L-1\).
Now recurse: apply the same three-way logic one layer back. And again. All the way to the input.
Do this for every training example, average the results, and you have \(-\nabla C\) — one vote per example, per parameter.
That means a full forward and backward pass over all 60,000 images for one step of gradient descent. And you need thousands of steps.
So: shuffle the data, chop it into mini-batches of, say, 100, and take a step using each batch.
Each step is now a bad estimate of the true gradient — but you get 600 of them for the price of one. This is stochastic gradient descent.
after 3Blue1Brown, OrganizeDataIntoMiniBatches
Knobs, and an example
60,000 examples with batch size 100 → 600 iterations per epoch.
Watch the test cost, not the training cost. Stop when it turns around.
Weights start small and random. A feature measured in the thousands and one measured in \([0,1]\) do not get a fair hearing.
Min-max, to \([0,1]\): \[ x' = \frac{x - x_{\min}}{x_{\max} - x_{\min}} \]
Or standardise, to mean \(0\) and standard deviation \(1\): \[ x' = \frac{x - \mu}{\sigma} \]
Fit the scaling on the training data only, then apply it to the test data. Otherwise you have leaked information.
Predict median house values, neuralnet against glm:
library(neuralnet); library(MASS)
set.seed(3456)
# 1. scale everything into [0, 1] -- neural nets need it, OLS does not care
mins <- apply(Boston, 2, min); maxs <- apply(Boston, 2, max)
scaled <- as.data.frame(scale(Boston, center = mins, scale = maxs - mins))
# 2. split
train_i <- sample(1:nrow(Boston), round(0.75 * nrow(Boston)))
train <- scaled[train_i, ]; test <- scaled[-train_i, ]# same data, same scaling, so RMSE is comparable
pred_net <- predict(net, test[, setdiff(names(test), "medv")])
pred_lm <- predict(glm(medv ~ ., data = train), test)
c(net = caret::RMSE(pred_net, test$medv),
lm = caret::RMSE(pred_lm, test$medv))Full script: res/nn-code.R
Fit one, then ask whether OLS would have done as well. Often it will.
target_low: at least 3 months of unemployment in the next 6 monthstarget_high: at least 6 months of unemployment in the next 2 yearsOR
Either way the model is yours to pick: a neural network, or anything else that does the job.
Due date: January 8th ### Sources and further reading {.smaller}
The visual material on these slides
src/nn-figures.pyBooks and papers