Deep learning · Units II and III

From a single neuron to a bidirectional LSTM

Everything in your two units, in the order the syllabus lists it, with the three interactive pieces that are genuinely hard to picture from a textbook — activations and their gradients, the convolution window, and why momentum beats plain SGD.

Written to run offline. Nothing here needs the internet except the fonts.
Unit II12 periods
01

What a deep network actually computes

A deep neural network is a chain of alternating linear maps and non-linear squashes. One layer is:

z[l] = W[l] a[l-1] + b[l] (linear: a weighted sum) a[l] = g[l]( z[l] ) (non-linear: the activation)

with a[0] = x, the input. Stack L of these and the final a[L] is the prediction ŷ. That is the entire forward pass.

The one idea the whole unit rests on

Without the non-linear g, stacking layers is pointless. Two linear layers compose into W₂(W₁x + b₁) + b₂, which is just another linear map W′x + b′. A hundred linear layers still only draws a straight line. The activation function is what makes depth mean anything — every layer you add can then bend the decision surface again.

Two terms that get confused in vivas:

Parameter count for a dense layer is (inputs × units) + units. A 784→128 layer is 784×128 + 128 = 100,480 parameters — useful to be able to compute on the spot, because examiners ask.

02

Activation functions

Pick a function below. The solid line is g(x); the dashed line is its derivative g′(x) — and the derivative is the part that matters, because it is what gets multiplied down the chain during backpropagation.

Activation explorer

Watch the dashed line. Wherever it sits near zero, gradient stops flowing — that region is where learning dies.

FunctionRangeUse it forIts failure mode
Sigmoid
1/(1+e⁻ˣ)
(0, 1)Binary classification output onlySaturates at both ends; max derivative is 0.25, so gradients shrink by ≥4× per layer. Not zero-centred.
Tanh
(eˣ−e⁻ˣ)/(eˣ+e⁻ˣ)
(−1, 1)Hidden layers in RNNs; still standard inside LSTM/GRUAlso saturates, but is zero-centred, so it converges faster than sigmoid.
ReLU
max(0, x)
[0, ∞)Default for hidden layers in CNNs and MLPsDying ReLU — a unit stuck at negative input has derivative exactly 0 forever.
Leaky ReLU
max(0.01x, x)
(−∞, ∞)When you suspect dead unitsThe slope 0.01 is one more hyperparameter to guess.
ELU
x if x>0 else α(eˣ−1)
(−α, ∞)Smoother alternative to ReLU; pushes mean activation toward zeroThe exponential costs more compute than ReLU.
Softmax
eᶻᵢ/Σeᶻʲ
(0,1), sums to 1Multi-class output layer onlyNever use it in a hidden layer — it couples all units together.
The vanishing gradient problem, stated properly

Backprop multiplies derivatives layer by layer. With sigmoid, each factor is at most 0.25. Across 10 layers the gradient reaching layer 1 is scaled by at most 0.2510 ≈ 10−6 — the early layers effectively stop learning. ReLU fixes this because its derivative is exactly 1 for positive inputs, so the product does not decay. This single fact is why ReLU replaced sigmoid in hidden layers, and it is the most commonly asked "why" in this unit.

In your own CNN grid

Your MNIST/CIFAR configurations use activation="relu" throughout except two that switch to "elu". That is the comparison this section explains: ELU allows small negative outputs, which keeps the mean activation nearer zero and can train slightly faster on deeper stacks — at the cost of an exponential per unit. Your deeper 4- and 6-layer configs are exactly where a difference would show, if one shows at all.

03

Loss functions

The loss measures how wrong one prediction is. The cost is the loss averaged over the batch. Gradient descent minimises the cost.

For regression

MSE = (1/n) Σ (yᵢ − ŷᵢ)² penalises large errors quadratically MAE = (1/n) Σ |yᵢ − ŷᵢ| treats all errors linearly, robust to outliers Huber = ½(y−ŷ)² if |y−ŷ| ≤ δ δ|y−ŷ| − ½δ² otherwise

Huber is the compromise: quadratic near zero so it has a smooth, well-behaved gradient, linear far away so a single freak outlier cannot dominate the update. That is precisely why it is the standard choice for noisy real-world series.

For classification

Binary cross-entropy −[ y logŷ + (1−y) log(1−ŷ) ] Categorical cross-entropy − Σₖ yₖ log ŷₖ
Why not just use MSE for classification?

Two reasons, and examiners want both. (1) Gradient quality: pair sigmoid/softmax with MSE and the derivative contains a g′(z) factor that vanishes exactly when the prediction is confidently wrong — the network learns slowest when it most needs to learn. Cross-entropy is constructed so that factor cancels, leaving the clean gradient (ŷ − y). (2) Principle: cross-entropy is the negative log-likelihood of the correct class, so minimising it is maximum-likelihood estimation. MSE assumes Gaussian noise, which is the wrong model for a categorical label.

categorical vs sparse_categorical — the difference in your notebook

Your configs switch between "categorical_crossentropy" and "sparse_categorical_crossentropy". They compute the same quantity. The only difference is the label format: categorical needs one-hot labels ([0,0,1,0]), sparse takes the plain integer (2). Sparse saves memory on CIFAR-100. Getting this pairing wrong is the single most common silent bug in a Keras classifier — it will either crash on shape or train to nonsense.

04

Gradient-based learning and backpropagation

Training is one loop, repeated:

  1. Forward pass — push a batch through, get ŷ.
  2. Compute the loss — how wrong were we.
  3. Backward pass — get ∂L/∂W for every weight.
  4. Update — step every weight downhill.

Step 3 is backpropagation, and it is nothing but the chain rule applied efficiently. For a weight in layer l:

∂L/∂W[l] = ∂L/∂a[L] · ∂a[L]/∂z[L] · ... · ∂a[l]/∂z[l] · ∂z[l]/∂W[l]

The clever part is that the shared prefix of that product is reused for every layer, so the whole gradient costs about as much as one extra forward pass rather than one pass per weight. Define the error signal δ[l] = ∂L/∂z[l] and it becomes a clean two-line recursion:

δ[L] = ∇aL ⊙ g′(z[L]) at the output δ[l] = (W[l+1]T δ[l+1]) ⊙ g′(z[l]) propagate backwards ∂L/∂W[l] = δ[l] a[l-1]T ∂L/∂b[l] = δ[l]

Then the update, with learning rate η:

W ← W − η · ∂L/∂W

Notice g′(z) appearing in every backward step. That is the same factor from section 2 — the link between "which activation did I pick" and "does this network train at all" runs straight through this recursion.

05

DNNs for regression

Regression predicts a continuous number. Three design rules follow from that and they are not negotiable:

  1. Output layer: one unit, linear activation (i.e. none). Any squashing function would cap the range.
  2. Loss: MSE, MAE, or Huber.
  3. Scale your inputs. Features on wildly different scales produce an ill-conditioned loss surface, which is exactly the situation the optimizer demo in section 7 shows going badly.
from tensorflow.keras import layers, models

model = models.Sequential([
    layers.Input(shape=(n_features,)),
    layers.Dense(64, activation="relu"),
    layers.Dense(64, activation="relu"),
    layers.Dense(1)                      # linear: no activation
])
model.compile(optimizer="adam", loss="mse", metrics=["mae"])

Case study — house price prediction (Boston / California housing)

Inputs are a dozen numeric features: rooms, age, distance to employment centres, median income. Target is a price in lakhs or thousands of dollars. Standardise every input to zero mean and unit variance first. Report MAE rather than MSE when you present it, because MAE is in the same unit as the target and therefore means something to a reader — "off by ₹2.3 lakh on average" is interpretable; "MSE 5.29" is not.

The scaling trap

Fit the scaler on the training set only, then apply it to validation and test. Fitting it on everything leaks information about the test distribution into training, and your reported error becomes optimistic and wrong. This gets marked in lab vivas.

06

DNNs for classification

Classification predicts a class label. The design rules mirror regression exactly:

ProblemOutput layerLoss
Binary (2 classes)1 unit, sigmoidbinary_crossentropy
Multi-class, one labelk units, softmaxcategorical_crossentropy (one-hot) or sparse_categorical_crossentropy (integer)
Multi-label, several at oncek units, sigmoidbinary_crossentropy

That last row is the one people get wrong. Softmax forces the outputs to sum to 1, so it cannot express "this image contains both a cat and a dog". Independent sigmoids can.

model = models.Sequential([
    layers.Input(shape=(784,)),
    layers.Dense(128, activation="relu"),
    layers.Dropout(0.3),
    layers.Dense(10, activation="softmax")     # k units, softmax
])
model.compile(optimizer="adam",
              loss="sparse_categorical_crossentropy",
              metrics=["accuracy"])

Case study — MNIST digits with a plain DNN

Flatten each 28×28 image to a 784-vector, two hidden layers, softmax over 10 classes. This reaches roughly 97–98% test accuracy. Worth knowing as a baseline, because Unit III's whole argument is that a CNN beats it while using fewer parameters — and the reason is that flattening throws away the fact that neighbouring pixels are related.

Accuracy is not always the metric

On a balanced set like MNIST, accuracy is fine. On an imbalanced one — say 2% of samples are the disease-positive class — a model predicting "negative" every single time scores 98% accuracy and is useless. Then you report precision, recall and F1, and you look at the confusion matrix. For a bioinformatics dataset this is nearly always the case, so expect it as a viva follow-up.

07

Optimization: SGD, momentum, adaptive learning rates

Plain gradient descent uses the whole dataset for one step — accurate but far too slow. The variants:

VariantBatch used per updateCharacter
Batch GDAll n samplesSmooth, slow, memory-hungry
Stochastic GD1 sampleVery noisy, but the noise can escape shallow local minima
Mini-batch SGD32–256 samplesThe practical default — what "SGD" means in every framework

Momentum

Plain SGD oscillates across a narrow valley instead of running along it. Momentum accumulates a velocity so that consistent directions build up and oscillating ones cancel:

v ← βv + (1−β) ∇W β typically 0.9 W ← W − ηv

The physical reading is a ball rolling downhill with inertia rather than a hiker recomputing the steepest direction at every footstep. Nesterov momentum refines it by evaluating the gradient at the point the velocity is about to carry you to — a look-ahead correction.

Adaptive learning rates

One global η is wrong when different parameters need different step sizes. These give every parameter its own:

AdaGrad divides η by √(sum of all past squared gradients) → rare features get big steps. But the sum only grows, so the rate decays to zero and learning stalls. RMSProp same idea with an exponentially decaying average instead of a raw sum, so it never stalls. Standard for RNNs. Adam RMSProp + momentum, with bias correction: m ← β₁m + (1−β₁)∇W (1st moment, the direction) v ← β₂v + (1−β₂)(∇W)² (2nd moment, the scale) m̂ = m/(1−β₁ᵗ) v̂ = v/(1−β₂ᵗ) W ← W − η · m̂/(√v̂ + ε) defaults β₁=0.9, β₂=0.999, ε=10⁻⁸, η=0.001

Optimizer race on an ill-conditioned bowl

The valley is 12× steeper across than along — the classic hard case. All three start at the same point with the same learning rate. Watch how plain SGD zig-zags while momentum straightens out.

Why your sunspots grid has one SGD run at lr = 1e-5

Eleven of your twelve RNN configs use Adam at 1e-3; config 12 switches to SGD(learning_rate=1e-5, momentum=0.9). That is not a typo — it is the point of the comparison. Adam's per-parameter scaling means its η is not comparable to SGD's, and recurrent networks have notoriously badly scaled gradients, so a raw SGD step that works at all has to be roughly a hundred times smaller. Running it is how you demonstrate that adaptivity is doing real work, not just cosmetic tuning.

Unit III12 periods
08

Why convolution exists

Feed a 224×224 colour image to a dense layer of 1000 units and you need 224×224×3×1000 ≈ 150 million weights in the first layer alone. It will not fit, will not train, and will not generalise. Convolution fixes this with three ideas:

A 3×3 kernel over 3 channels producing 32 feature maps costs 3×3×3×32 + 32 = 896 parameters — regardless of whether the image is 32×32 or 4000×4000.

09

The convolution operation and kernels

Slide a small kernel over the input; at each position compute the element-wise product and sum it. Click cells in the input below to redraw the shape, and change the kernel to see what each one detects.

Convolution playground

Click any input cell to toggle it. The highlighted 3×3 window shows which input patch produced the highlighted output cell — hover the output to move it.

Input 7×7
Kernel 3×3
Output 5×5

Output size — the formula you will be asked to apply

O = floor( (W − K + 2P) / S ) + 1 W = input width K = kernel size P = padding S = stride

Output size calculator

"same" padding means choosing P so the output matches the input size — for stride 1 that is P = (K−1)/2, so 1 for a 3×3 kernel and 2 for a 5×5. "valid" means no padding, and the map shrinks by K−1 each layer.

Kernel size: what your 3 vs 5 vs 7 comparison is really testing

Two stacked 3×3 layers see the same 5×5 region as one 5×5 layer, but use 2×(3×3)=18 weights per channel pair instead of 25, and apply a non-linearity in between — more expressive, fewer parameters. Three stacked 3×3s match a 7×7 at 27 weights versus 49. This is why modern architectures are almost entirely 3×3, and why your kernels=[7,7] and [7,7,7] configs are expected to underperform the deep 3×3 stacks at similar cost. Being able to state that trade-off is a guaranteed exam answer.

10

Pooling

Pooling downsamples a feature map by summarising each small window with a single number.

A 2×2 pool with stride 2 halves both spatial dimensions and so quarters the number of activations. It has no learnable parameters — a frequent one-mark question.

What pooling buys you: a wider receptive field for later layers, less computation, and a degree of translation invariance — shift the input by one pixel and a max-pooled output often does not change at all.

Equivariance vs invariance

These are examined as a pair and they are not the same. Convolution is equivariant: shift the input, the output shifts identically. Pooling adds invariance: shift the input slightly, the output does not change. Convolution tracks where; pooling lets the network stop caring exactly where.

11

Designing a CNN for image recognition

The standard pattern, repeated in blocks:

[ Conv → BatchNorm → Activation → (Conv → BN → Act) → MaxPool → Dropout ] × N → Flatten or GlobalAveragePooling → Dense → Dropout → Dense(k, softmax)

Filters double as spatial size halves — 32, 64, 128, 256 — keeping roughly constant computation per block while the features become more abstract: edges, then textures, then parts, then objects.

model = models.Sequential([
    layers.Input(shape=(32, 32, 3)),

    layers.Conv2D(32, (3,3), padding="same", use_bias=False),
    layers.BatchNormalization(),
    layers.Activation("relu"),
    layers.MaxPooling2D((2,2)),          # 32x32 -> 16x16
    layers.Dropout(0.2),

    layers.Conv2D(64, (3,3), padding="same", use_bias=False),
    layers.BatchNormalization(),
    layers.Activation("relu"),
    layers.MaxPooling2D((2,2)),          # 16x16 -> 8x8
    layers.Dropout(0.3),

    layers.Flatten(),
    layers.Dense(128), layers.BatchNormalization(), layers.Activation("relu"),
    layers.Dropout(0.4),
    layers.Dense(10, activation="softmax"),
])

The two regularisers in that stack

Reading your own results table

Your grid varies depth, kernel size, filter count, dropout, batch norm and activation. When you write it up, the defensible claims are comparisons between neighbouring rows that differ in one thing — that is how the grid was built. Expect: MNIST saturates around 99%+ so differences there are noise; CIFAR-10 is where depth and batch norm separate the configurations; CIFAR-100 is where every configuration struggles, because 100 classes over the same 50,000 images means only 500 examples per class. If your no-dropout, no-augmentation configs show training accuracy far above validation accuracy, that gap is the overfitting result — report it rather than hiding it.

12

Random and unsupervised features

This syllabus line asks a real question: must convolution kernels be learned by supervised backpropagation?

Why this matters in bioinformatics specifically

Labelled biological images are scarce and expensive — someone has to annotate them. Unlabelled ones are abundant. Unsupervised and self-supervised pretraining is how the field gets around that, and it is a strong point to make if this comes up as a discussion question.

13

Recurrent units and backpropagation through time

A CNN sees a fixed-size input all at once. Sequences — text, speech, a time series — have variable length and order that carries meaning. An RNN handles this by keeping a hidden state that it carries forward:

hₜ = tanh( Wₕₕ hₜ₋₁ + Wₓₕ xₜ + b ) yₜ = Wₕₖ hₜ + bₖ

hₜ is a summary of everything seen up to time t. Crucially the same W matrices are reused at every timestep — parameter sharing across time, exactly as convolution shares across space.

Training unrolls the loop into a deep feedforward network, one layer per timestep, and backpropagates through it. That is backpropagation through time (BPTT). And it inherits a serious problem: the gradient across k steps contains Wₕₕ raised to the power k.

Why a simple RNN cannot hold long-range information

If the dominant eigenvalue of Wₕₕ is below 1, the gradient vanishes exponentially and the network cannot learn dependencies more than roughly 10 steps back. If it is above 1, the gradient explodes into NaNs. Exploding gradients have a cheap fix — gradient clipping, rescale any gradient whose norm exceeds a threshold. Vanishing gradients do not; they need an architectural change, and that change is the LSTM.

14

LSTM — Long Short-Term Memory

The LSTM adds a second state: a cell state Cₜ that runs straight through the unit with only element-wise multiplication and addition on its path. No repeated matrix multiplication means no exponential decay — gradients can flow across hundreds of steps. Three gates, each a sigmoid producing values in (0,1), decide what passes.

LSTM cell at time t C(t-1) C(t) × + fₜ σ iₜ σ gₜ tanh oₜ σ forget input candidate output × h(t) tanh h(t-1), x(t)
fₜ = σ( W₌·[hₜ₋₁, xₜ] + b₌ ) forget gate what to erase from memory iₜ = σ( Wᵢ·[hₜ₋₁, xₜ] + bᵢ ) input gate how much new info to admit gₜ = tanh( W_C·[hₜ₋₁, xₜ] + b_C ) candidate the new info itself oₜ = σ( Wₒ·[hₜ₋₁, xₜ] + bₒ ) output gate how much memory to expose Cₜ = fₜ ⊙ Cₜ₋₁ + iₜ ⊙ gₜ update the cell state hₜ = oₜ ⊙ tanh( Cₜ ) produce the hidden state
The sentence to write in an exam

"The cell state is modified only by element-wise multiplication by the forget gate and element-wise addition of the gated candidate. Because there is no repeated matrix multiplication along that path, the gradient does not decay exponentially, so the LSTM can learn dependencies over hundreds of timesteps where a simple RNN manages about ten."

One practical note: initialise the forget-gate bias to 1 so the cell remembers by default at the start of training. Keras does this for you via unit_forget_bias=True.

15

GRU — Gated Recurrent Unit

The GRU is a simplification of the LSTM. Two changes: there is no separate cell state — hₜ does both jobs — and the forget and input gates are merged into a single update gate, since deciding how much to keep already decides how much to admit.

zₜ = σ( Wᶻ·[hₜ₋₁, xₜ] ) update gate keep vs replace rₜ = σ( Wʳ·[hₜ₋₁, xₜ] ) reset gate how much past to use h̃ₜ = tanh( W·[ rₜ ⊙ hₜ₋₁ , xₜ ] ) candidate state hₜ = (1 − zₜ) ⊙ hₜ₋₁ + zₜ ⊙ h̃ₜ the single interpolation
LSTMGRU
Gates3 (forget, input, output)2 (update, reset)
States2 (cell C and hidden h)1 (h only)
Parameters4 weight blocks3 blocks — about 25% fewer
SpeedSlowerFaster to train
When to preferLong sequences, plenty of dataSmaller datasets, tighter compute

Empirically they perform comparably on most tasks, and there is no reliable rule for which wins — which is exactly why configs 1, 2 and 3 of your sunspots grid swap SimpleRNN → LSTM → GRU with everything else held fixed. That three-row comparison is the experiment; the honest write-up says which won on your data and does not over-generalise from it.

16

Recurrent layers and time-series forecasting

An RNN layer either returns its output at every timestep or only the last one. This is the single detail that breaks most student implementations:

return_sequencesOutput shapeUse when
False (default)(batch, units)It is the last recurrent layer, feeding a Dense head
True(batch, timesteps, units)Another recurrent layer follows, or you need an output per step

Stacking rule: every recurrent layer except the last needs return_sequences=True.

Turning a series into a supervised problem

A forecasting model needs (X, y) pairs, so you slide a window across the series: the previous w values are the input, the next value is the target. Window 30 on monthly data means "predict from the last 30 months".

def windowed_dataset(series, window_size, batch_size):
    ds = tf.data.Dataset.from_tensor_slices(series)
    ds = ds.window(window_size + 1, shift=1, drop_remainder=True)
    ds = ds.flat_map(lambda w: w.batch(window_size + 1))
    ds = ds.map(lambda w: (w[:-1], w[-1]))        # inputs, label
    return ds.batch(batch_size).prefetch(1)
Never shuffle a time series before splitting

Split chronologically — train on the earliest portion, validate on the most recent. A random split lets the model see the future while training, which is data leakage; your validation error will look excellent and the model will be worthless. Shuffling within the already-split training windows is fine and helps optimisation; shuffling before the split is the error.

Your sunspots design, decoded

Train on months 0–3000, validate on the last 235 — correctly chronological. Then the interesting choices:

  • Window 30 → 60 → 132. Not arbitrary. The solar cycle is about 11 years = 132 months, so window 132 is the one that lets the model see a full cycle. Whether that actually beats 60 is the empirical question worth reporting.
  • Huber instead of MSE from config 4 on — sunspot peaks are extreme, and MSE lets those few outliers dominate the gradient.
  • A Conv1D front-end from config 7 on. A 1-D convolution smooths and extracts local shape before the recurrent layer ever sees it, which usually helps and trains faster.
  • Metric mae alongside the loss, because MAE is in sunspot units and is what you quote in a conclusion.
17

Bidirectional RNNs

A standard RNN reads left to right, so at time t it knows only the past. A bidirectional RNN runs two independent recurrent layers — one forward, one backward over the reversed sequence — and concatenates their hidden states:

hₜ = [ h⃗ₜ ; h⃖ₜ ] forward state concatenated with backward state → output dimension doubles
layers.Bidirectional(layers.LSTM(64, return_sequences=True))

Every output now depends on the entire sequence, past and future. That resolves ambiguities a forward-only model cannot: in "the bank of the river", the disambiguating word arrives afterwards.

The limitation that decides whether you may use it

A bidirectional model needs the whole sequence before it can produce any output. So it is excellent for offline, complete sequences — named-entity recognition, sentiment classification, protein secondary-structure prediction from a full sequence, sentence translation. It is unusable for real-time forecasting, because the "future" it wants to read is the thing you are trying to predict. For genuine live prediction it is cheating; for a retrospective study it is not.

This is worth being careful about in your own sunspots work: config 9 is bidirectional, and it may well score best on the leaderboard. If it does, the correct write-up says it is not a valid forecaster despite the number, because within each 60-month window it is reading months that would not have happened yet at prediction time. Saying that yourself is a stronger result than the score.

In bioinformatics the offline case is the common one: a BiLSTM over a protein sequence is standard for secondary-structure and binding-site prediction, precisely because the whole sequence is already known.

18

What to memorise for the exam

Formulas that must be reproducible from memory

  1. Conv output size: O = floor((W − K + 2P)/S) + 1
  2. Conv parameter count: (K × K × C_in × C_out) + C_out
  3. Dense parameter count: (inputs × units) + units
  4. The four LSTM gate equations plus the two state updates
  5. The two GRU gates plus the interpolation hₜ = (1−z)⊙hₜ₋₁ + z⊙h̃ₜ
  6. Adam's m, v, bias correction, and update
  7. Cross-entropy, both binary and categorical

Questions that come up almost every year

If you have one evening

Work through section 9's output-size calculator until the arithmetic is automatic, then write out the LSTM equations from memory three times. Those two things between them cover more marks than anything else in these two units.