Deep Learning · Module 1 of 26 · 8 concepts · about 17 min

Deep Learning Fundamentals

What makes a model deep, what it learns instead of being told, and the five moving parts every training run has.

About Deep Learning: Deep learning uses neural networks, layers of simple maths stacked together, to learn things like recognising images or understanding text. It rests on a few ideas: a network makes a guess, measures how wrong it was, and adjusts itself a little to do better next time. You will begin with how a single network learns, then see the main designs built on it, for images, for sequences and the transformers behind today's language models. It finishes with the practical side: building networks in PyTorch, training them on a GPU, fixing them when they break and putting them to use.

At work: The ideas behind photo search, voice assistants, translation and chatbots: models that learn their own features from raw data.

Deep learning as function composition

What it is

A deep network is a parameterised function made by composing simple functions: input → layer → activation → layer → prediction. Each layer receives a representation, transforms it using learned weights, and passes a new representation onward. Training chooses the parameters that minimise a loss on examples; depth lets later layers build features from earlier ones, such as edges becoming shapes and then object parts. The same computational pattern can map pixels to labels, tokens to the next-token distribution, or noisy latents to an image.

Why it matters

This is the one picture behind image classifiers, language models, and diffusion models. Architecture changes which functions can be composed and which assumptions the model makes about the data; training changes the numerical parameters inside those functions. Keeping those two decisions separate makes debugging much clearer: a bad architecture limits what can be represented, while bad optimisation or data prevents a capable architecture from finding it.

In the real world: A photo-tagging model is a chain of simple steps that turns pixels into “red running shoe”, learned end to end from examples.

Visual example

Deep learning

A network turns examples into predictions through a chain of learned transformations.

A learned function

  1. input xnumbers representing one example
  2. layers + activationsprogressively transformed features
  3. prediction ŷa score, value, or token distribution
  4. loss L(ŷ, y)one number training will reduce

Only the loss compares the prediction with the answer.

# f(x) = layer3(activation(layer2(activation(layer1(x)))))
y_hat = model(x)
loss = loss_fn(y_hat, y)

Calling any large model 'deep learning'. A lookup table or a hand-written pipeline may be complex, but it does not learn a hierarchy of representations by optimising differentiable parameters.

Representation learning vs hand-engineered features

What it is

Classical machine learning asks a person to decide what the model gets to see: edge histograms for images, n-gram counts for text, ratios and lags for tabular data. Deep learning keeps the raw signal and learns the intermediate representations as part of fitting the loss, so the features are chosen by optimisation rather than by intuition. The cost is that those learned features need data and compute to discover, and are not labelled in human terms afterwards.

Why it matters

It decides where your effort goes. On a small tabular dataset, an afternoon of feature engineering plus gradient boosting usually beats a neural network, because there is not enough signal to learn the features from scratch. On pixels, audio, or text, the hand-engineered route stalls: nobody can write down the features that separate ten thousand object classes, and the learned ones win by a wide margin.

In the real world: Instead of engineers hand-coding “edges” and “corners” to spot scratched phones, the model learns what a scratch looks like.

Visual example

Representation learning

The same task, two places the intelligence can live: in the preprocessing, or in the parameters.

hand-engineered pipeline
stagechosen by
inputraw pixels—
featuresedges, hue, ratiosa person
modellinear / tree modelfitted
who chooses the features
learned representation
stagechosen by
inputraw pixels—
featuresconv layer activationsgradient descent
modeloutput headfitted

Why this matters

Only one row changes. Deep learning is not a different kind of fitting; it is the same fitting applied to the feature-construction step as well.

Common misconception

Wrong idea: a deep network has no features, it just reads the pixels.

Correct idea: it has features at every layer. They are tensors it learned rather than columns someone wrote, which is why they work well and read as nonsense.

import torch.nn as nn

# Hand-engineered: you compute the features, the model only weighs them.
# features = [edge_density(img), mean_hue(img), aspect_ratio(img)]
shallow = nn.Linear(3, 10)

# Learned: the raw pixels go in and the intermediate features are parameters.
deep = nn.Sequential(
    nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.ReLU(),
    nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(),
    nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(64, 10),
)

Reading 'no feature engineering needed' as 'no data work needed'. Deep learning moves the work from inventing features to curating data: labels, coverage, deduplication, augmentation and normalisation all still decide the result.

Parameters vs hyperparameters in a network

What it is

Parameters are the tensors gradient descent writes to: every weight matrix and bias vector, plus embedding tables and normalisation scales. Hyperparameters are everything you fix before the run and the optimiser never touches: depth, width, activation choice, learning rate, batch size, weight decay, dropout probability, number of epochs. model.parameters() enumerates exactly the first group, which is why the optimiser is constructed from it.

Why it matters

The division tells you which knob to turn when a run disappoints. A model that cannot fit its own training batch usually has a hyperparameter problem — too little capacity, a learning rate that cannot move, a broken architecture. A model that fits training data and fails on validation data has enough parameters and the wrong hyperparameters around them. Confusing the two produces the classic wasted week: training longer to fix a choice that training cannot change.

In the real world: The model learns millions of weights; the team chooses the number of layers and the learning rate, which decide cost and quality.

Visual example

Deep learning

Two groups of numbers with completely different owners: one is written by the optimiser, one by you.

Parameter names

(what model.parameters() yields)

Learned entries

(9610 numbers in total)

nn.Sequential(Linear(64, 128), ReLU(), Linear(128, 10))
shapeentrieswritten by
0.weight(128, 64)8192optimizer.step()
0.bias(128,)128optimizer.step()
2.weight(10, 128)1280optimizer.step()
2.bias(10,)10optimizer.step()

The optimiser can change every number in the entries column and none of the words 128, 64 or 10.

Where the hyperparameters went

hidden=128 → chose the 128 in every shape above

in=64, out=10 → fixed by the data, not learned

lr, batch size, epochs → appear nowhere in this table

import torch.nn as nn

hidden, lr = 128, 3e-4          # hyperparameters: chosen, never learned
model = nn.Sequential(nn.Linear(64, hidden), nn.ReLU(), nn.Linear(hidden, 10))

total = sum(p.numel() for p in model.parameters())
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(total, trainable)         # 9610 9610 — every entry is learned

Tuning hyperparameters against the test set. Depth, learning rate and epoch count are fitted to whatever data you compare them on, so a test set used for selection stops being an estimate of unseen performance.

Why depth helps: composing features

What it is

Each layer can only apply one transformation, but a stack can apply a transformation to a transformation. In a vision model that shows up as a legible hierarchy — oriented edges, then corners and textures, then parts like wheels or eyes, then whole objects — where every level is built from combinations of the level below. The same compositional argument holds for language: characters into subwords, subwords into phrases, phrases into the referents a sentence is about.

Why it matters

Depth buys reuse. An 'eye' detector built from twenty edge detectors costs twenty weights plus a combination; a shallow model that must recognise an eye directly from pixels needs a separate template for every position, scale and lighting condition it will meet. That is why adding layers often beats adding width at equal parameter count, and why very wide shallow networks are both expensive and disappointing.

In the real world: Early layers spot edges, later ones spot shapes, the last ones spot “damaged package”: how a warehouse camera learns.

Visual example

Depth vs width

At a fixed parameter budget, spending it on layers beats spending it on a single huge layer.

deep (8 layers)shallow (1 hidden layer)
024681000.10.20.30.40.5parameters (millions)validation errorwidening stops paying

Illustrative curves for one image task; the gap is the compositional saving, not a law of nature.

import torch, torch.nn as nn

x = torch.randn(8, 3, 32, 32)
stack = nn.Sequential(
    nn.Conv2d(3, 16, 3, padding=1), nn.ReLU(),     # edges
    nn.Conv2d(16, 32, 3, stride=2, padding=1), nn.ReLU(),  # textures, corners
    nn.Conv2d(32, 64, 3, stride=2, padding=1), nn.ReLU(),  # parts
)
for layer in stack:
    x = layer(x)
    if isinstance(layer, nn.Conv2d):
        print(tuple(x.shape))   # (8, 16, 32, 32) -> (8, 32, 16, 16) -> (8, 64, 8, 8)

Reading 'deeper is better' as unconditional. Past a point, depth without residual connections or normalisation trains worse than a shallower net — not because it cannot represent the function, but because the gradient cannot reach the early layers (modules 7 and 10).

Universal approximation, and why it does not settle anything

What it is

The universal approximation theorem says a network with one hidden layer, a non-polynomial activation and enough units can approximate any continuous function on a bounded region to any accuracy you like. It is a statement about existence: some setting of the weights is close enough. It says nothing about how many units 'enough' is, whether gradient descent can find that setting, or how many examples it would take to pin it down.

Why it matters

It is the most-cited and least-useful theorem in the field, and knowing why protects you from two bad arguments. The first is 'one hidden layer suffices, so depth is fashion' — true in principle, false in the width and sample size required. The second is 'the network can represent it, so training failure means bad data' — representability and learnability are separate questions, and almost every real failure is the second one.

In the real world: In theory a network can fit anything; in practice the business still needs enough data and the right design.

import torch, torch.nn as nn

# Both can represent a sawtooth well. Only one learns it from 2000 samples.
wide = nn.Sequential(nn.Linear(1, 4096), nn.ReLU(), nn.Linear(4096, 1))
deep = nn.Sequential(
    nn.Linear(1, 64), nn.ReLU(),
    nn.Linear(64, 64), nn.ReLU(),
    nn.Linear(64, 64), nn.ReLU(),
    nn.Linear(64, 1),
)
print(sum(p.numel() for p in wide.parameters()))   # 12289
print(sum(p.numel() for p in deep.parameters()))   # 8513

Using the theorem to justify an architecture. 'A big enough MLP can approximate anything' is true of a lookup table too; what distinguishes an architecture is which functions it finds *easily* from finite data, not which it could express.

Tensors: shape, dtype and device

What it is

A tensor is the NumPy ndarray mental model — a dense, regularly shaped block of numbers of one dtype — plus two things that matter in deep learning: a device saying which processor's memory holds it, and optionally an autograd history saying which operations produced it. Shape is the contract between layers, dtype decides precision and memory, device decides whether an operation is legal at all. Leading dimensions are conventionally batch-first in PyTorch: (batch, features), (batch, channels, height, width), (batch, tokens, model_dim).

Why it matters

Nearly every error in a deep learning session is one of these three fields being wrong, not the mathematics. A shape mismatch stops the run loudly; a dtype mismatch silently loses precision or refuses to index; a device mismatch raises the single most common runtime error in PyTorch. Reading a tensor as shape/dtype/device rather than as a list of numbers is what makes those errors immediately diagnosable.

In the real world: A batch of 64 product images on the GPU in 16-bit numbers: getting shape, type and device right is daily work.

Visual example

Tensor anatomy

One batch, read as a table: the leading axis is examples, the rest is one example's shape.

Batch dimension

(axis 0 — never mixed between examples)

One example's features

(axes 1.. — what a layer transforms)

x — shape (2, 3), dtype float32, device cpu
f₀f₁f₂
example 00.42-1.30.07
example 1-0.850.311.22

x.shape

(2, 3) — the layer contract

x[0]

one example, shape (3,)

A layer declared nn.Linear(3, 8) maps this to (2, 8) — the batch axis passes through untouched.

The three fields that cause the errors

shape → (2, 3): decides which Linear it fits

dtype → float32: decides precision and memory

device → cpu: an op needs both operands on one device

import torch

x = torch.randn(32, 3, 224, 224)        # a batch of 32 RGB images
print(x.shape, x.dtype, x.device)        # torch.Size([32, 3, 224, 224]) torch.float32 cpu
print(x.numel() * x.element_size())      # 19267584 bytes ≈ 19 MB

labels = torch.randint(0, 10, (32,))     # int64 class IDs, one per example
x = x.to("cuda") if torch.cuda.is_available() else x

Indexing with float tensors or feeding float targets to nn.CrossEntropyLoss. Class IDs must be int64; a .float() somewhere upstream turns a clear type error into a confusing one several frames away.

The five moving parts of a training run

What it is

Every training script, from a tutorial MLP to a frontier language model, is the same five objects: the data (a Dataset plus a DataLoader that batches and shuffles it), the model (an nn.Module holding the parameters), the loss (a function turning prediction and target into one scalar), the optimizer (the rule that turns gradients into parameter changes), and the loop that runs forward, loss, backward, step over and over. Anything else — schedulers, checkpoints, logging, mixed precision — hangs off one of those five.

Why it matters

It is the map for this whole topic, and the first question to ask of a failing run: which of the five is wrong? A loss that never moves is usually the loop (a missing backward, a missing step, a detached tensor). A loss that moves and validation that does not is usually the data. A loss that goes to NaN is usually the optimizer's learning rate or the loss's numerics. Naming the five keeps debugging a search over five suspects instead of a vague feeling.

In the real world: Data, model, loss, optimiser and loop: when a defect-detection model underperforms, the fault is in one of these five.

Visual example

Deep learning

Every line of a training script belongs to one of five objects — and each gets a module of its own.

The five parts, and which module teaches each

  1. data → batchesshapes and batching (modules 1, 4)
  2. model → predictionneurons, activations, forward pass (2-4)
  3. loss → one scalarwhat is being minimised (5)
  4. backward → gradientsdescent and the chain rule (6-7)
  5. optimizer → new weightsupdate rules and schedules (8-9)

The rest of this topic is these five steps, one at a time.

import torch, torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset

loader = DataLoader(TensorDataset(X, y), batch_size=32, shuffle=True)   # 1. data
model = nn.Sequential(nn.Linear(64, 128), nn.ReLU(), nn.Linear(128, 10))  # 2. model
loss_fn = nn.CrossEntropyLoss()                                         # 3. loss
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)              # 4. optimizer

for xb, yb in loader:                                                   # 5. the loop
    optimizer.zero_grad()
    loss = loss_fn(model(xb), yb)
    loss.backward()
    optimizer.step()

Building the optimizer before the model is finished — or from the wrong object. torch.optim.AdamW(model.parameters()) captures the parameters that exist at that moment; a layer added afterwards, or a model.to(device) that replaces tensors, leaves the optimizer updating nothing.

When depth is worth it, and when it is not

What it is

Deep learning wins where the input is high-dimensional, raw and structured — pixels, waveforms, tokens, molecules — and where there is enough labelled or self-supervised data to discover the structure. It reliably loses on small, heterogeneous, well-understood tabular data, where gradient boosting on engineered features remains the strong baseline. Between those poles the honest answer comes from an experiment, not from a preference.

Why it matters

Choosing the family is a bigger decision than any hyperparameter inside it, and it is made before you can measure anything. Getting it wrong costs weeks: a neural network on 800 rows of tabular data will underperform a tree ensemble you could have fitted in a minute, and a gradient-boosted model on raw pixels will underperform a small CNN by a margin no tuning closes.

In the real world: Images, audio and text usually need deep models; a spreadsheet of customer features often doesn't, and gradient boosting wins.

# The comparison worth running before committing to a deep model.
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.metrics import accuracy_score

baseline = HistGradientBoostingClassifier().fit(X_train, y_train)
print(accuracy_score(y_val, baseline.predict(X_val)))   # the number to beat

# Only then: train the network and compare on the same split.

Skipping the non-deep baseline because the project is 'a deep learning project'. Without it you cannot tell whether the network learned something or merely learned the majority class — and you have no evidence that the extra complexity earned its cost.