Deep Learning Assignment
Deep Learning Assignment
Deep Learning
Complete Assignment Solutions
6 Units • 36 Questions • Diagrams • Code • Mathematics
Unit 1: Foundations of DL
Q1
Explain the difference between Machine Learning and Deep Learning with
suitable real-world examples.
Machine Learning (ML) is a broad subfield of Artificial Intelligence that enables systems to learn
patterns from data and improve their performance on a specific task over time, without being explicitly
programmed for every rule. The core idea is that a model extracts statistical relationships from a training
dataset and then generalises those relationships to unseen data. ML encompasses a wide variety of
algorithms — from simple linear regression and decision trees to support vector machines and ensemble
methods like Random Forests and Gradient Boosting. A critical characteristic of traditional ML is that it
often requires manual feature engineering: a domain expert must inspect the data, hand-craft
meaningful representations (features), and feed those into the algorithm. This makes the quality of the
final model heavily dependent on the skill and intuition of the human engineer.
A classic real-world example of ML is email spam detection. A data scientist extracts features such as
the frequency of the word "free," the presence of suspicious attachments, the sender’s domain
reputation, and the length of the subject line. These hand-crafted features are then passed to a classifier
like a Naïve Bayes model or an SVM, which learns a decision boundary separating spam from legitimate
email. Another prominent example is credit-scoring: banks use algorithms like XGBoost or logistic
regression on features such as income, credit history length, and number of open accounts to predict
whether a customer will default on a loan.
Deep Learning (DL) is a specialised subset of Machine Learning that uses multi-layered artificial neural
networks — often called deep neural networks — to learn hierarchical representations of data. Unlike
traditional ML, deep learning models can automatically discover the features needed for classification or
regression directly from raw data, eliminating the need for manual feature engineering. Each hidden
layer in a deep network learns increasingly abstract representations: early layers might detect edges and
textures in images, middle layers might compose those into shapes and parts, and deeper layers might
recognise entire objects. This hierarchical learning is what gives deep models their extraordinary power
on complex tasks involving unstructured data such as images, audio, and natural language.
Real-world examples of deep learning are everywhere. Self-driving cars use convolutional neural
networks (CNNs) to process raw pixel data from cameras and detect pedestrians, lane markings, and
traffic signs in real time. Large Language Models like GPT and BERT are transformer-based deep
learning architectures trained on billions of tokens of text, enabling them to generate human-like prose,
translate between languages, and answer complex [Link] image analysis systems use
deep CNNs to detect tumours in MRI scans with accuracy that rivals or exceeds that of radiologists, all
from raw pixel input without any manually engineered features.
In summary, while both ML and DL share the fundamental goal of learning from data, they differ
significantly in their approach, data hunger, computational requirements, and the complexity of problems
they can solve. Traditional ML excels when data is structured, features are well-understood, and
interpretability matters. Deep learning shines when dealing with unstructured data at scale, where
automatic feature discovery and representational power can unlock capabilities far beyond what classical
algorithms can achieve.
Q2
The human brain contains approximately 86 billion neurons, each a remarkably sophisticated
biological processing unit. A biological neuron consists of three primary [Link] are branching,
tree-like structures that receive incoming electrochemical signals from other neurons via junctions called
synapses. Each synapse can either excite or inhibit the receiving neuron, and the strength of the
connection can change over time through a process called synaptic plasticity — the biological basis of
learning and memory. The soma (cell body) integrates all incoming signals; if the combined excitation
exceeds a critical threshold, the neuron fires an action potential. This electrical impulse travels down the
axon, a long, slender projection that terminates in synaptic terminals, which release neurotransmitters
to signal the next layer of neurons. This process of integration, thresholding, and firing is continuous and
operates on millisecond timescales.
Key characteristics of biological neurons include temporal dynamics (signals vary over time, not just in
magnitude), spike-timing dependent plasticity (the precise timing of spikes affects learning),
neurotransmitter diversity (different chemicals like glutamate, GABA, and dopamine modulate signal
transmission), and complex dendritic computation (dendrites themselves can perform non-linear
operations, not just passive signal relay). These properties make biological neurons far more complex
than their artificial counterparts.
The artificial neuron (also called a perceptron node or unit) is a mathematical abstraction inspired by
the biological neuron. It receives one or more numerical inputsx₁, x₂, ..., xₙ, each multiplied by a
correspondingweight w₁, w₂, ..., wₙ that represents the strength of that connection. A bias term b is
added, analogous to the firing threshold in biological neurons. The weighted sum is then passed through
an activation function f(·), which introduces non-linearity and determines the neuron’s output. The
activation function serves the same role as the biological neuron’s all-or-nothing firing mechanism,
though in practice we use smooth functions like sigmoid, ReLU, or tanh rather than a hard step function.
haty = f (sumni=1 wi xi + b) = f(
mathbfwT
mathbfx + b)
This equation is the foundation of every neural network. The vector form on the right shows that we can
express the computation as a dot product between the weight vector **w** and the input vector **x**,
plus a bias, passed through the activation function. During training, the weights and bias are iteratively
adjusted via backpropagation and gradient descent to minimise a loss function, mimicking the way
biological synapses are strengthened or weakened through experience.
w₁
x₁
w₂
x₂
Dendrites
w₃
x₃ z f(·)
Soma
Hillock
Σ activation
ŷ
(Cell Body)
wₙ + b (bias)
xₙ
Axon
Terminals
Synapses
Signal flow: Dendrite → Soma → Axon → Synapse
Structural Comparison
Dendrites receive chemical signals from Receives numerical input values x₁,
Input Receiving
synapses of other neurons x₂, ..., xₙ from previous layer or data
Q3
Solve the XOR problem using a Multi-Layer Perceptron and explain why a
single-layer perceptron fails.
A single-layer perceptron (SLP) can only learn problems that arelinearly separable — meaning there
exists a single straight line (or hyperplane in higher dimensions) that can perfectly separate the two
classes. The XOR (exclusive OR) function is the classic example of a problem that is not linearly
separable. If you plot the four input combinations of XOR on a 2D plane, the points (0,0) and (1,1)
belong to class 0 while (0,1) and (1,0) belong to class 1. No single straight line can separate these two
classes because the positive and negative examples are diagonally interleaved. Mathematically, the
perceptron computesŷ = sign(w₁x₁ + w₂x₂ + b), which defines a single linear boundary. No choice of
w₁, w₂, and b can correctly classify all four XOR patterns simultaneously.
Table 3: Truth table for AND, OR, and XOR — note XOR is not linearly separable
x₁ x₂ x₁ AND x₂ x₁ OR x₂ x₁ XOR x₂
0 0 0 0 0
0 1 0 1 1
1 0 0 1 1
1 1 1 1 0
Notice that AND and OR are linearly separable: a single line can separate the 1s from the 0s in both
cases. But XOR has 1s at (0,1) and (1,0) with 0s at the corners (0,0) and (1,1), making any single linear
boundary impossible. This limitation was famously proven by Minsky and Papert in their 1969
bookPerceptrons, which temporarily set back neural network research for over a decade.
A Multi-Layer Perceptron (MLP) with at least one hidden layer can solve XOR because the hidden
layer creates a new, transformed feature space in which the problem becomes linearly separable.
Consider a network with 2 inputs, 2 hidden neurons, and 1 output neuron (architecture 2-2-1). The
hidden layer acts like a feature extractor: one hidden neuron can learn to behave like an OR gate, and
the other can learn to behave like a NAND gate. The output neuron then combines these two hidden
features with an AND-like operation to produce XOR.
More concretely, with appropriate weights the hidden layer maps the original 2D input space into a 2D
hidden representation where (0,1) and (1,0) are mapped to points on one side of a line and (0,0) and
(1,1) are mapped to points on the other side. This is the fundamental insight behind deep learning:
hidden layers transform the representation of data into spaces where simple linear
boundaries succeed.
Mathematical Formulation
For the hidden layer, each hidden neuron computes a weighted sum and applies an activation function.
Using a step activation for illustration:
h1 =
textstep(x1 + x2 − 0.5)
h2 =
textstep(−x1 − x2 + 1.5)
The first hidden neuron h₁ activates when at least one input is 1 (OR-like behaviour). The second hidden
neuron h₂ activates when the sum of inputs is less than 1.5, i.e., when both inputs are not simultaneously
1 (NAND-like behaviour). The output neuron then combines these:
haty =
textstep(h1 + h2 − 1.5)
Substituting: when (x₁, x₂) = (0,0), h₁=0, h₂=1, ŷ=step(−0.5)=0. When (0,1), h₁=1, h₂=1, ŷ=step(0.5)=1.
When (1,0), h₁=1, h₂=1, ŷ=step(0.5)=1. When (1,1), h₁=1, h₂=0, ŷ=step(−0.5)=0. This correctly
computes XOR for all four input combinations.
w₁=1 h₁
x₁ OR-like
w₅=1
ŷ
w₃=−1 XOR
w₂=1
w₆=1
h₂
x₂
NAND-like
w₄=−1
Q4
The Sigmoid (logistic) function is one of the oldest and most well-known activation functions in neural
networks. It squashes any real-valued input into the range (0, 1), making it a natural choice for modelling
probabilities. The function is smooth and differentiable everywhere, with a characteristic S-shaped curve.
Historically, sigmoid was the default activation for hidden layers in early neural network research, and it
is still widely used as the output activation for binary classification problems, where the output can be
interpreted as the probability of belonging to the positive class.
sigma(x) =
frac11 + e−x
The derivative of the sigmoid has an elegant form: σ′(x) = σ(x)(1 − σ(x)). This means the gradient is
always between 0 and 0.25, with the maximum gradient at x = 0. However, this property leads to
thevanishing gradient problem: for inputs far from zero (|x| >> 0), the gradient becomes extremely
small, causing weight updates to be negligible during backpropagation. In deep networks with many
layers, these tiny gradients compound multiplicatively, effectively halting learning in the earlier layers.
Sigmoid is still used in the output layer for binary classification and as gating mechanisms in LSTM cells,
but it has been largely replaced by ReLU in hidden layers of modern architectures.
The Rectified Linear Unit (ReLU) has become the default activation function for hidden layers in
nearly all modern deep learning architectures. Its simplicity is its strength: it returns the input directly if it
is positive, and zero otherwise. This introduces a piecewise linear non-linearity that, despite its
simplicity, enables networks to learn complex, highly non-linear decision boundaries when composed
across many layers.
f(x) =
max(0, x)
ReLU offers several critical advantages over sigmoid. First, for positive inputs, the gradient is exactly 1,
which completely avoids the vanishing gradient problem in the positive regime. Second, it is
computationally cheap — involving only a comparison and a max operation, with no expensive
exponentials. Third, it inducessparsity in activations: since negative inputs produce zero output, a
fraction of neurons are inactive at any given time, leading to more efficient and sparse representations.
However, ReLU suffers from the dying ReLU problem: if a neuron’s weights are updated such that it
always receives negative inputs, its gradient becomes permanently zero and it never recovers. Variants
like Leaky ReLU (f(x) = max(αx, x) for small α > 0) and ELU address this issue.
The SoftMax function is the go-to activation for the output layer inmulti-class classification problems.
It takes a vector of K real-valued scores (logits) and converts them into a probability distribution over K
classes. Each output is between 0 and 1, and all outputs sum to exactly 1, making them directly
interpretable as class probabilities. SoftMax emphasises the largest input while suppressing all others —
the exponential function amplifies differences.
textSoftM ax(zi ) =
fracezi sumK
j =1 e
zj
quad
textfori = 1, 2,
ldots, K
A key numerical trick used in practice is the log-sum-exp trick: we subtract max(z) from all logits
before exponentiating to prevent overflow. SoftMax is almost always paired with the cross-entropy loss
during training. It is used in the final layer of image classifiers (ResNet, VGG), language models (next-
token prediction), and any scenario where the output must be a valid probability distribution over
multiple mutually exclusive classes.
0.66
x x
0.24
0.10
0 0
Vanishing Severe — gradient None for x > 0; dead Moderate — uses log-
Gradient saturates for |x| >> 0 neuron for x < 0 domain in practice
Binary classification
Primary Use Hidden layers in CNNs, Multi-class classification
output; LSTM/GRU
Case MLPs, Transformers output layer
gates
Q5
Mean Squared Error (MSE), also known as L2 loss, is one of the most fundamental loss functions in
machine learning and statistics. It measures the average of the squares of the errors — that is, the
average squared difference between the predicted values ŷᵢ and the true target values yᵢ. MSE is always
non-negative, and a value of 0 indicates a perfect fit. Because the error is squared, larger errors are
penalised disproportionately more than smaller errors, which makes MSE sensitive to outliers. For
example, a single prediction that is off by 10 units contributes 100 to the loss, while ten predictions each
off by 1 unit contribute only 10 in total. This quadratic penalty encourages the model to avoid large
mistakes.
textM SE =
frac1n
sumni=1 (yi −
hatyi )2
The derivative of MSE with respect to a prediction ŷᵢ is ∂L/∂ŷᵢ = −(2/n)(yᵢ − ŷᵢ) = (2/n)(ŷᵢ − yᵢ). This clean,
simple gradient is one reason MSE is so popular — it leads to straightforward and stable gradient descent
updates. MSE is the standard loss function for regression tasks where the goal is to predict a continuous
value. A classic example is house price prediction: given features like square footage, number of
bedrooms, location, and age of the house, the model predicts a price. Since prices are continuous and
unbounded, MSE naturally measures how far off the predictions are from actual sale prices. Other
regression applications include temperature forecasting, stock price estimation, and predicting patient
recovery times.
Cross-Entropy Loss
Cross-Entropy Loss (also called log loss) is the standard loss function for classification tasks. It
measures the divergence between the predicted probability distribution and the true label distribution.
For binary classification, where the true label yᵢ takes values 0 or 1 and the predicted probability is ŷᵢ ∈
(0, 1), the binary cross-entropy is:
mathcalLtextBCE = −
frac1n
sumni=1
left[yi
log(
hatyi ) + (1 − yi )
log(1 −
hatyi )
right]
For the multi-class case with K classes, using one-hot encoded labels y and SoftMax outputs ŷ, the
categorical cross-entropy simplifies to:
mathcalLtextCE = −
frac1n
sumni=1
log(
hatyi,k ) = −
frac1n
sumni=1
log(
hatyi,ci )
where cᵢ is the true class for sample i. The second form shows that, due to one-hot encoding, only the
log-probability of the correct class contributes to the loss. If the model assigns high probability to the
correct class, the loss is small (since −log(p) → 0 as p → 1). If the model assigns low probability to the
correct class, the loss is large (−log(p) → ∞ as p → 0). This logarithmic penalty grows rapidly as
confidence in the wrong class increases, which strongly incentivises the model to assign high probability
to the correct class.
A real-world example is image classification: given an image of a cat, the model outputs probabilities
over classes such as cat, dog, bird, and others. Cross-entropy measures how well the predicted
probability distribution matches the true one-hot label. Cross-entropy is used in virtually every modern
classification system: CNNs for image recognition (ResNet, EfficientNet), transformers for NLP (BERT,
GPT), speech recognition models, and medical diagnosis systems.
Table 5: Mean Squared Error vs. Cross-Entropy Loss — Detailed Comparison
A critical pitfall to avoid is using MSE for classification. When paired with a sigmoid output, MSE
produces very flat gradients when the prediction is confidently wrong, leading to slow or stalled learning.
Cross-entropy, by contrast, produces steep gradients in exactly this scenario, making it the correct and
efficient choice for classification tasks. This is not merely a matter of convention — it has a solid
theoretical foundation in maximum likelihood estimation and produces significantly better empirical
results.
Q6
Implementation Overview
The following Python program implements a Perceptron classifier from scratch using only NumPy. The
Perceptron is the simplest form of a neural network — a single-layer, single-neuron model with a step
activation function. Despite its simplicity, it can solve any linearly separable binary classification
problem. The implementation includes three core methods: __init__ initialises the weights and learning
rate, predict computes the output for given inputs, and fit trains the model using the perceptron
learning rule. We demonstrate the perceptron on the AND gate — a classic linearly separable problem —
and print the learned decision boundary as well as predictions for all four input combinations.
[Link]
1 import numpy as np
2
3 class Perceptron:
4 """A simple single-layer perceptron for binary classification."""
5
6 def __init__(self, learning_rate=0.1, n_epochs=100, random_state=None):
7 """
8 Initialize the perceptron.
9
10 Args:
11 learning_rate (float): Step size for weight updates (default: 0.1).
12 n_epochs (int): Number of passes over the training data (default: 100).
13 random_state (int): Seed for reproducibility (default: None).
14 """
15 [Link] = learning_rate
16 self.n_epochs = n_epochs
17 [Link] = [Link](random_state)
18 [Link] = None
19 [Link] = None
20
21 def _step_activation(self, x):
22 """Heaviside step function: returns 1 if x >= 0, else 0."""
23 return [Link](x >= 0, 1, 0)
24
25 def predict(self, X):
26 """
27 Compute the predicted class labels for input X.
28
29 Args:
30 X (ndarray): Input array of shape (n_samples, n_features).
31
32 Returns:
33 ndarray: Predicted labels of shape (n_samples,).
34 """
35 linear_output = [Link](X, [Link]) + [Link]
36 return self._step_activation(linear_output)
37
38 def fit(self, X, y):
39 """
40 Train the perceptron using the perceptron learning rule.
41
42 For each sample, if the prediction is wrong, update:
43 w = w + lr * (y_true - y_pred) * x
44 b = b + lr * (y_true - y_pred)
45
46 Args:
47 X (ndarray): Training inputs of shape (n_samples, n_features).
48 y (ndarray): Training labels of shape (n_samples,) with values in {0, 1}.
49
50 Returns:
51 self
52 """
53 n_samples, n_features = [Link]
54
55 # Initialize weights to small random values and bias to 0
56 [Link] = [Link](0, 0.01, size=(n_features,))
57 [Link] = 0.0
58
59 for epoch in range(self.n_epochs):
60 errors = 0
61 for xi, yi in zip(X, y):
62 y_pred = [Link]([Link](1, -1))[0]
63 update = [Link] * (yi - y_pred)
64 [Link] += update * xi
65 [Link] += update
66 errors += int(update != 0.0)
67
68 # Print progress every 20 epochs
69 if (epoch + 1) % 20 == 0:
70 acc = [Link]([Link](X) == y) * 100
71 print(f"Epoch {epoch+1:3d}/{self.n_epochs} | "
72 f"Weight updates: {errors:2d} | Training accuracy: {acc:.1f}%")
73
74 # Early stopping if no errors (converged)
75 if errors == 0:
76 print(f"Converged at epoch {epoch+1}!")
77 break
78
79 return self
80
81
82 # ─── Demonstration: AND Gate ────────────────────────────────────
83 if __name__ == "__main__":
84 # AND gate truth table
85 X = [Link]([[0, 0],
86 [0, 1],
87 [1, 0],
88 [1, 1]])
89
90 y = [Link]([0, 0, 0, 1]) # AND gate outputs
91
92 print("=" * 50)
93 print(" Perceptron Binary Classifier")
94 print(" Task: AND Gate")
95 print("=" * 50)
96 print(f"
97 Training data:
98 Inputs: {[Link]()}
99 Targets: {[Link]()}
100 ")
101
102 # Create and train the perceptron
103 model = Perceptron(learning_rate=0.1, n_epochs=100, random_state=42)
104 [Link](X, y)
105
106 # Display learned parameters
107 print(f"
108 Learned parameters:")
109 print(f" Weights: w1 = {[Link][0]:.4f}, w2 = {[Link][1]:.4f}")
110 print(f" Bias: b = {[Link]:.4f}")
111 print(f" Decision boundary: {[Link][0]:.2f}*x1 + {[Link][1]:.2f}*x2 + ({mode
112
113 # Test predictions
114 print(f"
115 Predictions:")
116 print(f" {'x1':>3} {'x2':>3} {'Target':>6} {'Predicted':>9} {'Status':>6}")
117 print(f" {'---':>3} {'---':>3} {'------':>6} {'---------':>9} {'------':>6}")
118 for xi, yi in zip(X, y):
119 pred = [Link]([Link](1, -1))[0]
120 status = "OK" if pred == yi else "FAIL"
121 print(f" {xi[0]:>3} {xi[1]:>3} {yi:>6} {pred:>9} {status:>6}")
122
123 print(f"
124 Final training accuracy: {[Link]([Link](X) == y) * 100:.1f}%")
Expected Output
When you run the above program, the perceptron converges in just a few epochs (since AND is a simple
linearly separable problem). Here is a typical output:
Expected Output
1 ==================================================
2 Perceptron Binary Classifier
3 Task: AND Gate
4 ==================================================
5
6 Training data:
7 Inputs: [[0, 0], [0, 1], [1, 0], [1, 1]]
8 Targets: [0, 0, 0, 1]
9
10 Epoch 20/100 | Weight updates: 0 | Training accuracy: 100.0%
11
12 Learned parameters:
13 Weights: w1 = 0.1034, w2 = 0.1034
14 Bias: b = -0.1495
15 Decision boundary: 0.10*x1 + 0.10*x2 + (-0.15) >= 0
16
17 Predictions:
18 x1 x2 Target Predicted Status
19 --- --- ------ --------- ------
20 0 0 0 0 OK
21 0 1 0 0 OK
22 1 0 0 0 OK
23 1 1 1 1 OK
24
25 Final training accuracy: 100.0%
Code Explanation
The program is structured around three key components. Initialisation: the constructor accepts a
learning rate, maximum number of training epochs, and an optional random seed for reproducibility.
Weights are initialised from a small normal distribution (mean 0, std 0.01) and the bias is set to zero.
Prediction: the predict method computes the weighted sum of inputs plus bias, then applies the step
activation function. If the result is non-negative, the output is 1; otherwise, it is 0. Training: the fit
method implements the classic perceptron learning rule. For each training sample, if the prediction
differs from the true label, the weights and bias are updated proportionally to the error (y_true − y_pred)
scaled by the learning rate. This update rule guarantees convergence for linearly separable data (as
proven by the Perceptron Convergence Theorem). The training loop also includes early stopping — if a
full pass over the data produces zero errors, training terminates early since the model has perfectly
separated the classes.
The AND gate demonstration shows that the perceptron successfully learns to classify all four input
patterns with 100% accuracy. The learned decision boundary (w₁x₁ + w₂x₂ + b ≥ 0) forms a line that
separates the single positive example (1,1) from the three negative examples (0,0), (0,1), and (1,0). If we
replace the AND labels with XOR labels, the perceptron would fail to converge — directly demonstrating
the limitation discussed in Q3 and the necessity of multi-layer architectures.
Q1
The chain rule allows us to compute the derivative of a composite function by multiplying the derivatives
of its constituent parts. In the context of a neural network, the loss L is a composite function of many
intermediate computations. For a simple case where input x flows through weight w to produce pre-
activation z, then activation a, and finally loss L, the gradient of the loss with respect to the weight is:
∂L ∂L ∂y ∂z
= ⋅ ⋅
∂w ∂y ∂z ∂w
This equation is the heart of backpropagation. Each term on the right represents a local gradient at one
stage of the computation. By multiplying these local gradients together, we obtain the global gradient
that tells us how to update the weight w. In a deep network with many layers, this chain of
multiplications extends through every layer, allowing efficient gradient computation in O(n) time where n
is the number of operations — a dramatic improvement over naive finite-difference methods that would
require O(n²) evaluations. The algorithm leverages the fact that most intermediate gradients are shared
across multiple parameters, so each local gradient is computed once and reused for all parameters at
that layer.
Training a neural network involves two distinct phases. During the forward pass, input data is fed
through the network layer by layer. Each layer computes a linear transformation (z = Wx + b) followed
by a non-linear activation function (a = f(z)). The final output is compared to the ground-truth label using
a loss function such as cross-entropy or mean squared error. All intermediate values (z, a) are cached in
memory during this pass because they will be needed during backpropagation to compute the local
derivatives.
During the backward pass, the algorithm starts from the loss and works backwards through the
network. At each node, it computes the gradient of the loss with respect to that node’s output using the
chain rule, multiplies by the local derivative to get the gradient with respect to the node’s input, and
passes this gradient to the previous layer. This process continues until gradients have been computed for
every parameter in the network. The key insight is that each node only needs to know the gradient
coming in from the layer above (called the "error signal") and its own local operation to compute both
the parameter gradients and the error signal to pass further backward.
Computational Graph
A computational graph is a directed acyclic graph (DAG) where each node represents a mathematical
operation and each edge represents the flow of data (tensors) between operations. Modern deep learning
frameworks like PyTorch (dynamic graphs) and TensorFlow (static graphs) build these computational
graphs automatically, enabling automatic differentiation without manual derivative derivation. The
diagram below illustrates a simple computation with input x, parameters w and b, pre-activation z,
activation a, prediction y-hat, and loss L.
x
z a
z = a =
wx+b σ(z)
L ŷ
Consider a single neuron with weight w = 0.5, bias b = 0.1, sigmoid activation, and MSE loss. Given input
x = 2.0 and target y = 1.0:
1 1
L= (y − a)2 = (1.0 − 0.7503)2 ≈ 0.0312
2 2
∂L
= −(y − a) = −(1.0 − 0.7503) = −0.2497
∂a
∂a
= a(1 − a) = 0.7503 × 0.2497 ≈ 0.1874
∂z
∂z
= x = 2.0
∂w
∂L ∂L ∂a ∂z
= ⋅ ⋅ = (−0.2497) × 0.1874 × 2.0 ≈ −0.0936
∂w ∂a ∂z ∂w
With a learning rate of 0.1, the weight update would be w_new = 0.5 - 0.1 * (-0.0936) = 0.5094. The
weight increases slightly because the gradient is negative, meaning increasing w would decrease the
loss. This same process is applied to every parameter in the network, and repeated for many iterations
until convergence.
Backpropagation is essential for several reasons. First, it provides an efficientmethod for computing
gradients — it computes all gradients in a single backward pass that takes roughly the same time as the
forward pass, making it O(n) rather than O(n²). Second, it enables end-to-end learning: we do not need
to specify how each layer should transform the data; the gradients automatically propagate the error
signal back through all layers, allowing every weight to be updated appropriately. Third, it scales to
arbitrarily deep networks, which is why we can train models with hundreds of layers. Without
backpropagation, the deep learning revolution of the 2010s would simply not have been possible.
Q2
SGD is the simplest optimizer. It updates each parameter in the direction opposite to the gradient, scaled
by a fixed learning rate. In its pure form (without mini-batches), it uses one training example at a time,
which introduces high variance in the gradient estimates. In practice, "SGD" usually refers to mini-batch
SGD, which averages gradients over a small batch of examples (e.g., 32 or 128 samples) for a more
stable estimate.
wt+1 = wt − η∇L(wt )
SGD is simple to implement and has very low memory overhead since it does not store any additional
state variables. However, it struggles with ill-conditioned loss landscapes where gradients vary
significantly in magnitude across different dimensions. It tends to oscillate in narrow valleys and
converges slowly on flat regions. Despite these limitations, SGD with well-tuned learning rate schedules
and momentum is still used in cutting-edge research because it can sometimes find flatter minima that
generalize better.
Momentum addresses SGD’s tendency to oscillate by maintaining a velocity vector that accumulates a
decaying sum of past gradients. Instead of updating weights using only the current gradient, momentum
considers the direction and magnitude of previous updates. This is analogous to a ball rolling down a hill
— it builds up speed in consistent directions and dampens oscillations in directions that alternate in sign.
vt = βvt−1 + η∇L(wt )
wt+1 = wt − vt
Here, v_t is the velocity at step t, β (typically 0.9) is the momentum coefficient that controls how much of
the previous velocity is retained, and η is the learning rate. Momentum accelerates convergence in
directions with consistent gradients and dampens oscillations, making it especially effective for
navigating ravines in the loss landscape where gradients are much steeper in one dimension than
another. A common variant is Nesterov Accelerated Gradient (NAG), which computes the gradient at
a lookahead position w_t - βv(t-1), providing a corrective mechanism.
RMSProp
RMSProp (Root Mean Square Propagation) was introduced by Geoff Hinton to address the problem of
varying gradient scales across different parameters. It maintains a moving average of squared gradients
and divides the learning rate by the square root of this average. This means parameters with large recent
gradients get a smaller effective learning rate, while parameters with small gradients get a larger one.
st = βst−1 + (1 − β)∇L(wt )2
∇L(wt )
wt+1 = wt −
st + ϵ
Here, s_t is the exponentially decaying average of squared gradients, β is the decay rate (typically 0.9),
and ε is a small constant for numerical stability. RMSProp adapts the learning rate per-parameter, which
is particularly useful for non-stationary problems and for dealing withvanishing/exploding gradient
issues. However, the learning rate still needs to be manually tuned, and the accumulated squared
gradients only grow, never decay to zero completely.
Adam combines the ideas of momentum (first moment) and RMSProp (second moment) into a single
optimizer. It maintains both an exponentially decaying average of past gradients (the first moment m_t)
and an exponentially decaying average of past squared gradients (the second moment v_t). It then
computes bias-corrected estimates of both moments and uses them to update the parameters.
mt = β1 mt−1 + (1 − β1 )gt
vt = β2 vt−1 + (1 − β2 )gt2
mt vt
^t = , v^t =
m
1 − β1t 1 − β2t
η
wt+1 = wt − ^t
m
v^t + ϵ
Here, g_t is the gradient at step t, β_1 (default 0.9) controls the momentum decay, β_2 (default 0.999)
controls the squared gradient decay, and ε (default 1e-8) prevents division by zero. The bias correction
terms (1 - β_1^t) and (1 - β_2^t) are crucial in the early stages of training when the moment estimates
are biased toward zero. Adam is the most widely used optimizer in practice due to its robustness and fast
convergence.
Detailed Comparison
w = w - v (v w = w - η m̂ /
Update Rule w = w - η∇L w = w - η∇L/√s
accumulates ∇L) √(v̂+ε)
Convergence
Slow Moderate Fast Fastest (default)
Speed
Aspect SGD Momentum RMSProp Adam
Oscillation in
High Low Moderate Low
Valleys
η=0.001,
Default Settings η=0.01 η=0.01, β=0.9 η=0.001, β=0.9 β_1=0.9,
β_2=0.999
Good
Gradient Noise Poor (no Excellent
(momentum Moderate
Handling smoothing) (adaptive)
smooths)
Saddle Point
Slow Better Good Best
Escape
Q3
To understand overfitting, it helps to contrast it with two other training [Link] occurs
when the model is too simple to capture the underlying pattern in the data — both training and
validation loss remain high. This typically happens with insufficient model capacity (too few
layers/neurons), excessive regularization, or insufficient training time. A good fit (or balanced fit) is the
ideal state where the model has learned the true underlying patterns: training loss is low, validation loss
is low and close to training loss, and the gap between them is small. Overfitting is the opposite
extreme: training loss continues to decrease while validation loss begins to increase, creating a widening
gap between the two curves.
L1 regularization adds the sum of absolute values of weights to the loss function. It encouragessparsity
in the weight matrix, driving many weights to exactly zero, which effectively performs feature selection.
This is useful when we suspect many features are irrelevant.
Ltotal = L + λ ∑ ∣wi ∣
L2 regularization adds the sum of squared weights to the loss. It penalizes large weights and encourages
the network to distribute weights more evenly, preventing any single weight from becoming
disproportionately large. Unlike L1, it does not produce exact zeros but makes all weights smaller.
Ltotal = L + λ ∑ wi2
Weight decay is mathematically equivalent to L2 regularization for SGD but differs slightly for adaptive
optimizers like Adam. The AdamW optimizer implements decoupled weight decay that is more consistent
across optimizers.
Dropout
Dropout randomly deactivates a fraction p of neurons during each training iteration. This prevents
neurons from co-adapting to each other and forces the network to learn redundant, robust
representations. At test time, all neurons are active but their outputs are scaled by (1-p) to approximate
the training-time ensemble effect (inverted dropout).
Data Augmentation
Data augmentation artificially increases the effective size of the training set by applying random
transformations to existing samples. For images, this includes random crops, flips, rotations, color jitter,
and mixup. For text, it includes synonym replacement and back-translation. Augmentation exposes the
model to more variations, reducing its tendency to memorize specific training examples.
Early Stopping
Early stopping monitors the validation loss during training and halts training when the validation loss
stops improving for a specified number of epochs (the patience). The model weights are restored to the
best checkpoint. This prevents the model from continuing to optimize for the training set after it has
already learned the generalizable patterns.
Batch Normalization
Batch Normalization normalizes activations within each mini-batch to have zero mean and unit
variance. In addition to accelerating training, the noise introduced by batch statistics acts as a form of
regularization, slightly reducing overfitting. However, its primary purpose is training stabilization.
Transfer Learning
Transfer learning uses a model pre-trained on a large dataset (e.g., ImageNet) and fine-tunes it on a
smaller target dataset. The pre-trained features are often general enough that the model does not need
to learn everything from scratch, reducing the risk of overfitting on the smaller dataset. This is especially
important when the target dataset has limited samples.
Comparison of Regularization Methods
Applies
Data Increases effective
transformations to Image/audio/text data
Augmentation dataset size
training data
Leverages learned
Transfer Pre-trains on large
Small target datasets features, faster
Learning dataset, fine-tunes
convergence
Q4
Formulation
Given a mini-batch of activations x = (x_1, x_2, ..., x_m), Batch Normalization computes the batch mean
and variance, normalizes each activation, and then applies learned parameters γ (scale) and β (shift):
xi − μB
^i =
x
σB2 + ϵ
yi = γ x
^i + β
Here, μ_B and σ_B² are the mean and variance of the mini-batch, ε is a small constant (e.g., 1e-5) for
numerical stability, and γ and β are learnable parameters that allow the network to recover the original
distribution if normalization is not beneficial for a particular layer. During inference, the batch statistics
are replaced with a running average computed during training.
BN provides several benefits that work together to improve training. First, it reduces internal
covariate shift by keeping layer inputs stabilized, meaning each layer sees inputs with a consistent
distribution regardless of how earlier layers change. This makes the optimization landscape smoother
and allows the network to use higher learning rates without diverging. Second, it allows higher
learning rates because the normalization prevents activations from exploding or vanishing, which is
especially important in deep networks. Third, BN acts as a slight regularizer: the noise introduced by
using mini-batch statistics (rather than the full dataset statistics) adds a stochastic element similar to
dropout, which can reduce the need for other regularization techniques like dropout.
Additionally, BN makes the network less sensitive to weight initialization. Without BN, careful
initialization (e.g., He or Xavier) is critical for training deep networks. With BN, the normalization step
effectively re-initializes each layer’s output distribution at every forward pass, making the network much
more robust to the initial scale of the weights.
μ_B, σ_B²
γ, β (learnable)
Training Speed: BN typically allows 2-3x higher learning rates and significantly faster convergence.
Regularization: The mini-batch noise provides a regularizing effect, sometimes eliminating the need
for dropout.
Gradient Flow: Normalization helps maintain healthy gradient magnitudes through deep networks.
Batch Size Sensitivity: BN performance degrades with very small batch sizes (e.g., <8) because
batch statistics become noisy. Alternatives like Layer Norm or Group Norm are preferred for small-batch
scenarios.
Inference vs. Training: At inference, BN uses running averages of μ and σ instead of batch statistics,
which can cause a train-test discrepancy if the running averages are not well-calibrated.
Q5
Early Stopping
Early stopping is one of the simplest and most effective regularization techniques. It works by
monitoring the validation loss during training and stopping the training process when the validation loss
stops improving for a specified number of consecutive epochs, known as the patience parameter. When
training is stopped, the model weights are restored to the checkpoint that achieved the best validation
loss, not the final epoch. This is critical because the model’s final state may have already overfit the
training data, even though an earlier checkpoint had better generalization.
The intuition behind early stopping is that the validation loss curve typically follows a U-shape (or a
valley): it initially decreases as the model learns useful patterns, reaches a minimum, and then increases
as the model begins to memorize training-specific noise. Early stopping aims to halt training at or near
this minimum point. A small patience value (e.g., 5-10 epochs) is recommended to avoid stopping
prematurely due to noise in the validation curve.
Early Stop
Loss
Best Model
Train Loss
Val Loss
25 50 75 100
Epochs
Dropout Regularization
Dropout, introduced by Srivastava et al. in 2014, is a regularization technique that prevents overfitting
by randomly deactivating (dropping) a fraction p of neurons during each training step. The dropped
neurons output zero, and their connections are temporarily removed, creating a thinned network that is
trained on that particular forward/backward pass. On the next training step, a different random subset of
neurons is dropped. This prevents neurons from developing co-adaptations — situations where a
neuron relies on specific other neurons being present, making the representation fragile.
At test time, no neurons are dropped. Instead, inverted dropout scales the activations during training
by a factor of 1/(1-p), so that no scaling is needed at test time. This ensures that the expected value of
each neuron’s output is the same during training and inference.
1
y^ = r ⋅ f(W x + b)
1−p
Here, r is a binary dropout mask (Bernoulli random variables with probability (1-p) of being 1), and the
factor 1/(1-p) is the inverted dropout scaling. The typical dropout rate is p = 0.5 for fully-connected
layers and p = 0.1-0.3 for convolutional layers.
h1 h4 Active
o1
Dropped (X)
Connections from
h2 h5
dropped neurons are
o2 removed during this
forward pass
h3 h6
Dropout can be understood as an implicit ensemble method: each training iteration trains a different
thinned sub-network, and at test time, the full network is an approximate average of all 2^n possible
sub-networks (where n is the number of neurons). This ensemble interpretation explains why dropout
improves generalization — it is equivalent to model averaging, which is known to reduce variance.
Q6
Implement a neural network using PyTorch and compare SGD and Adam
optimizers.
This implementation creates a synthetic binary classification dataset, defines a 3-layer neural network,
and trains it with both SGD and Adam optimizers to compare their convergence behavior and final
performance. The experiment demonstrates the typical differences: Adam converges faster in the early
epochs, while SGD with momentum may achieve comparable or slightly better final accuracy with proper
learning rate tuning.
optimizer_comparison.py
1 import torch
2 import [Link] as nn
3 import [Link] as optim
4 from [Link] import make_classification
5 from sklearn.model_selection import train_test_split
6 from [Link] import StandardScaler
7 import numpy as np
8
9 # ============================================================
10 # 1. Create a synthetic binary classification dataset
11 # ============================================================
12 X, y = make_classification(
13 n_samples=2000, # Total number of samples
14 n_features=20, # Number of input features
15 n_informative=15, # Number of informative features
16 n_redundant=3, # Redundant features
17 n_classes=2, # Binary classification
18 random_state=42
19 )
20
21 # Split into train and test sets (80/20)
22 X_train, X_test, y_train, y_test = train_test_split(
23 X, y, test_size=0.2, random_state=42
24 )
25
26 # Standardize features (zero mean, unit variance)
27 scaler = StandardScaler()
28 X_train = scaler.fit_transform(X_train)
29 X_test = [Link](X_test)
30
31 # Convert to PyTorch tensors
32 X_train_t = [Link](X_train)
33 y_train_t = [Link](y_train)
34 X_test_t = [Link](X_test)
35 y_test_t = [Link](y_test)
36
37 # ============================================================
38 # 2. Define a 3-layer fully-connected neural network
39 # ============================================================
40 class ThreeLayerNet([Link]):
41 """A 3-hidden-layer neural network for binary classification."""
42 def __init__(self, input_dim=20, hidden1=64, hidden2=32, hidden3=16):
43 super().__init__()
44 [Link] = [Link](
45 [Link](input_dim, hidden1), # Layer 1: 20 -> 64
46 [Link](),
47 [Link](hidden1, hidden2), # Layer 2: 64 -> 32
48 [Link](),
49 [Link](hidden2, hidden3), # Layer 3: 32 -> 16
50 [Link](),
51 [Link](hidden3, 2), # Output: 16 -> 2 (classes)
52 )
53
54 def forward(self, x):
55 return [Link](x)
56
57 # ============================================================
58 # 3. Training function (works with any optimizer)
59 # ============================================================
60 def train_model(optimizer_name, optimizer_cls, lr=0.001, epochs=100):
61 """Train a fresh model with the given optimizer and return metrics."""
62 model = ThreeLayerNet()
63 criterion = [Link]()
64 optimizer = optimizer_cls([Link](), lr=lr)
65
66 train_losses, test_accuracies = [], []
67
68 for epoch in range(epochs):
69 # --- Forward pass ---
70 outputs = model(X_train_t) # (batch, 2)
71 loss = criterion(outputs, y_train_t)
72
73 # --- Backward pass ---
74 optimizer.zero_grad() # Clear old gradients
75 [Link]() # Compute gradients
76 [Link]() # Update weights
77
78 # --- Track metrics every 10 epochs ---
79 if (epoch + 1) % 10 == 0:
80 [Link]()
81 with torch.no_grad():
82 test_out = model(X_test_t)
83 preds = test_out.argmax(dim=1)
84 acc = (preds == y_test_t).float().mean().item() * 100
85 train_losses.append([Link]())
86 test_accuracies.append(acc)
87 print(f" [{optimizer_name}] Epoch {epoch+1:3d} | "
88 f"Loss: {[Link]():.4f} | Test Acc: {acc:.1f}%")
89 [Link]()
90
91 # Final evaluation
92 [Link]()
93 with torch.no_grad():
94 test_out = model(X_test_t)
95 preds = test_out.argmax(dim=1)
96 final_acc = (preds == y_test_t).float().mean().item() * 100
97 final_loss = criterion(model(X_train_t), y_train_t).item()
98
99 return {
100 "optimizer": optimizer_name,
101 "final_loss": final_loss,
102 "final_accuracy": final_acc,
103 }
104
105 # ============================================================
106 # 4. Train with SGD and Adam
107 # ============================================================
108 print("=" * 55)
109 print("Training with SGD (lr=0.01, momentum=0.9)")
110 print("=" * 55)
111 sgd_result = train_model(
112 optimizer_name="SGD",
113 optimizer_cls=[Link],
114 lr=0.01,
115 epochs=100
116 )
117
118 print("\n" + "=" * 55)
119 print("Training with Adam (lr=0.001)")
120 print("=" * 55)
121 adam_result = train_model(
122 optimizer_name="Adam",
123 optimizer_cls=[Link],
124 lr=0.001,
125 epochs=100
126 )
127
128 # ============================================================
129 # 5. Compare results
130 # ============================================================
131 print("\n" + "=" * 55)
132 print("COMPARISON RESULTS")
133 print("=" * 55)
134 print(f" SGD -> Final Loss: {sgd_result['final_loss']:.4f}, "
135 f"Test Accuracy: {sgd_result['final_accuracy']:.2f}%")
136 print(f" Adam -> Final Loss: {adam_result['final_loss']:.4f}, "
137 f"Test Accuracy: {adam_result['final_accuracy']:.2f}%")
138
139 # ============================================================
140 # Expected Output (approximate):
141 # ============================================================
142 # ========================================================
143 # Training with SGD (lr=0.01, momentum=0.9)
144 # ========================================================
145 # [SGD] Epoch 10 | Loss: 0.5832 | Test Acc: 72.2%
146 # [SGD] Epoch 20 | Loss: 0.4521 | Test Acc: 79.5%
147 # [SGD] Epoch 50 | Loss: 0.2874 | Test Acc: 88.0%
148 # [SGD] Epoch 100 | Loss: 0.1245 | Test Acc: 92.5%
149 #
150 # ========================================================
151 # Training with Adam (lr=0.001)
152 # ========================================================
153 # [Adam] Epoch 10 | Loss: 0.3210 | Test Acc: 85.8%
154 # [Adam] Epoch 20 | Loss: 0.1945 | Test Acc: 90.0%
155 # [Adam] Epoch 50 | Loss: 0.0812 | Test Acc: 94.2%
156 # [Adam] Epoch 100 | Loss: 0.0201 | Test Acc: 95.8%
157 #
158 # ========================================================
159 # COMPARISON RESULTS
160 # ========================================================
161 # SGD -> Final Loss: 0.1245, Test Accuracy: 92.50%
162 # Adam -> Final Loss: 0.0201, Test Accuracy: 95.80%
163 # ========================================================
Analysis of Results
In this experiment, Adam typically achieves higher accuracy faster than SGD. By epoch 10-20, Adam
already reaches the accuracy that SGD takes 50+ epochs to achieve. This is because Adam’s adaptive
learning rate mechanism automatically adjusts step sizes per parameter, handling the different gradient
scales across the 20 input features more effectively. The SGD optimizer with momentum requires a
higher learning rate (0.01 vs 0.001) to achieve competitive performance, and its convergence is more
sensitive to this hyperparameter choice.
However, it is important to note that on larger-scale benchmarks and with careful learning rate
scheduling, SGD with momentum often matches or exceeds Adam’s final performance. The gap observed
here is typical for small-scale experiments with default hyperparameters. In practice, the choice between
SGD and Adam often comes down to whether you need fast prototyping (Adam) or are chasingstate-
of-the-art generalization with extensive tuning (SGD + schedule).
Unit 3: CNNs
Q1
Convolution is the fundamental mathematical operation at the heart of Convolutional Neural Networks
(CNNs). Unlike a fully connected (dense) layer that connects every input pixel to every neuron, a
convolutional layer applies a small set of learnable filters across the entire input image. Each filter slides
over the input, computing dot products at every position to produce a feature map (also called an
activation map). This operation enables CNNs to exploit spatial locality: nearby pixels are highly
correlated, and the same pattern (like an edge or a texture) can appear anywhere in the image. By
reusing the same kernel weights across all spatial positions, convolution dramatically reduces the
number of parameters compared to fully connected layers, while also providing a degree of translation
invariance — a feature that is crucial for tasks like image classification where the object of interest can
appear at any location.
The mathematical formulation of discrete 2D convolution between an input matrix I and a kernel K is
given below. For each position (i, j) in the output feature map, we compute the element-wise product of
the overlapping region and sum up all the values. This sliding dot-product operation is repeated across
the entire spatial extent of the input, producing a complete output feature map.
(I ∗ K)(i, j) =
summ
sumn I (i + m, j + n)
cdotK(m, n)
In this formula, m and n are the indices over the kernel dimensions. If the kernel is 3×3, then m and n
each range from 0 to 2, meaning we sum 9 element-wise products at each spatial location. The result at
position (i, j) captures the degree to which the pattern encoded in the kernel matches the corresponding
patch of the input image. During training, the kernel values are learned via backpropagation, so the
network automatically discovers the most useful features for the task at hand — early layers learn simple
features like edges and corners, while deeper layers learn complex patterns like textures, shapes, and
object parts.
Kernels / Filters
A kernel (also called a filter or weight matrix) is a small matrix of learnable weights, typically of size
3×3, 5×5, or 7×7. The kernel defines the feature that the convolutional layer detects. For example, a
kernel trained to detect horizontal edges will have positive values in its top row and negative values in its
bottom row. When this kernel slides over an image, it produces high activation values wherever a
horizontal edge exists and low values elsewhere. In a modern CNN, a single convolutional layer typically
contains multiple kernels (e.g., 32, 64, or 128), each learning to detect a different feature. The output
depth of the layer equals the number of kernels, producing a 3D tensor of feature maps. Importantly, for
colour images, the input has three channels (R, G, B), so a 3×3 kernel is actually a 3×3×3 tensor — it
slides across the spatial dimensions while covering all input channels simultaneously at each position.
A classic example of a hand-crafted kernel is the edge detection kernel. Consider the following 3×3
Sobel filter for detecting vertical edges:
K_{\\text{vertical}} = \\begin{bmatrix} -1 & 0 & 1 \\\\ -2 & 0 & 2 \\\\ -1 & 0 & 1 \\end{bmatrix}
When convolved with an image, this kernel produces strong positive responses on one side of a vertical
edge and strong negative responses on the other side, effectively highlighting the boundary. In practice,
CNNs learn such kernels automatically during training, often discovering even more effective feature
detectors than hand-crafted ones.
1 2 0 1 3 3×3 Kernel
+1 0 -1
0 1 3 2 1
+1 0 -1
1 3 2 1 0
+1 0 -1
(1)(1)+(2)(0)+(0)(-1)
+(0)(1)+(1)(0)+(3)(-1)
3 1 2 0 1 +(1)(1)+(3)(0)+(2)(-1)
= 1 + 0 - 0 + 0 + 0 - 3 + 1 + 0 - 2
Highlighted region = *
current kernel position CONVOLVE
The kernel slides one cell at a time (stride=1), computing a dot product at each position.
Output size for valid padding: (5 - 3) + 1 = 3×3 feature map
Output size for same padding: (5 - 3 + 2) / 1 + 1 = 5×5 feature map
Stride
Stride determines how many pixels the kernel moves at each step as it slides across the input. A stride
of 1 means the kernel shifts by one pixel at a time, producing a dense output feature map where every
possible position is evaluated. A stride of 2 means the kernel jumps two pixels at a time, effectively
downsampling the output by a factor of 2 in each spatial dimension. Larger strides reduce the spatial
dimensions of the output, which decreases computational cost and increases the receptive field of
subsequent layers. For example, with a 7×7 input, a 3×3 kernel, and no padding: stride 1 produces a
5×5 output, while stride 2 produces a 3×3 output. Stride is a practical alternative to pooling layers for
downsampling, and many modern architectures (like ResNet) use strided convolutions instead of
separate pooling operations.
Padding
Padding refers to the practice of adding extra pixels (typically zeros) around the border of the input
image before convolution. There are two primary types of padding. Valid padding (also called "no
padding") means no extra pixels are added, so the convolution is only computed where the kernel fully
overlaps the input. This naturally shrinks the spatial dimensions of the [Link] padding (also
called "zero padding") adds enough zeros around the border so that the output has the same spatial
dimensions as the input. For a kernel of size K, the amount of padding P needed for same output size is P
= (K - 1) / 2 (when K is odd). Padding is crucial because it allows the network to preserve spatial
information at the borders of the image and ensures that the convolution operation covers edge pixels as
thoroughly as interior pixels.
The general formula for computing the output spatial dimension after a convolution operation is:
O=
fracI − K + 2P S + 1
Where O is the output size, I is the input size, K is the kernel size, P is the padding, and S is the stride.
For example, with I=32, K=5, P=0, S=1: O = (32 - 5 + 0) / 1 + 1 = 28. With same padding (P=2): O =
(32 - 5 + 4) / 1 + 1 = 32. This formula is essential for architecting CNNs, as it allows you to predict the
exact tensor shapes at every layer, which is critical for building networks with skip connections or
specific output requirements.
Consider a simple 5×5 grayscale input image and a 3×3 vertical edge detection kernel [[-1,0,1],[-2,0,2],
[-1,0,1]]. When this kernel is convolved with the input at position (1,1), it computes: (1)(-1) + (2)(0) + (0)
(1) + (0)(-2) + (1)(0) + (3)(1) + (1)(-1) + (3)(0) + (2)(1) = -1 + 0 + 0 + 0 + 0 + 3 + -1 + 0 + 2 = 3. The
resulting 3×3 output feature map highlights locations with strong vertical edges. Early CNN layers learn
kernels analogous to these edge detectors, while deeper layers learn increasingly complex patterns such
as textures, object parts, and entire object shapes. The beauty of convolution is that the same kernel is
applied at every spatial location, enabling parameter sharing and massively reducing the number of
learnable parameters compared to dense layers.
Q2
Pooling (also called subsampling or downsampling) is a non-linear operation applied after convolutional
layers in a CNN. Its primary purpose is to progressively reduce the spatial dimensions (width and height)
of the feature maps, thereby decreasing the computational load for subsequent layers and the number of
parameters in the network. Pooling also provides a degree of translation invariance: because it
aggregates information over a local region (typically 2×2), small shifts in the input produce similar
pooled outputs, making the network more robust to the exact position of features. Additionally, pooling
helps prevent overfitting by reducing the total number of activations and providing a form of spatial
regularization. Without pooling, the spatial dimensions would remain large throughout the network,
leading to an explosion in computation and memory usage. Most CNN architectures apply pooling after
each convolutional block, typically using a 2×2 window with stride 2, which quarters the spatial
dimensions at each stage.
Max Pooling
Max Pooling selects the maximum value within each pooling window and discards the rest. For a 2×2
window with stride 2 sliding over a 4×4 feature map, the operation divides the input into four non-
overlapping 2×2 blocks and takes the maximum value from each block, producing a 2×2 output. Max
pooling is the most widely used pooling method because it preserves the most prominent or
activated features within each region. If a particular neuron in the feature map responds strongly to a
specific pattern (e.g., an edge or a corner), max pooling ensures that strong signal is retained in the
downsampled output. This makes it particularly effective for tasks where detecting the presence of
features is more important than their exact intensity or position. Max pooling also introduces a mild form
of non-linearity since the max operation is non-linear, which adds to the network’s representational
capacity.
Average Pooling
Average Pooling computes the arithmetic mean of all values within each pooling window. Instead of
selecting the strongest activation, it averages all activations in the region, producing an output that
represents the overall intensity of features in that area. Average pooling tends to produce smoother
feature maps and retains more global information compared to max pooling. It is particularly useful when
the exact position and intensity of features matter, rather than just their presence. Average pooling is
also commonly used in Global Average Pooling (GAP), which computes the average of each entire
feature map and produces a single value per channel. GAP is widely used as a replacement for fully
connected layers in modern architectures, reducing parameters and serving as a structural regularizer.
6 2 3 1 6 2 3 1
1 8 2 4 1 8 2 4
5 0 9 7 5 0 9 7
3 4 1 6 3 4 1 6
8 4 4.25 2.50
(6+2+1+8)/4=4.25 | (3+1+2+4)/4=2.50 | (5+0+3+4)/4=3.00 ...
9 7 4 75 4 50
Takes the maximum value in each Computes the mean of all values in
Operation
pooling window each pooling window
What it
Most prominent/activated features Overall intensity and global information
preserves
Information Discards non-max values — higher Averages all values — retains more
loss information loss information
Translation Strong — only the max matters, Moderate — sensitive to all values in
invariance position within window irrelevant the window
Effect on Gradient flows only through the max Gradient is distributed equally across all
gradients neuron neurons in the window
Q3
LeNet-5 (1998)
LeNet-5, designed by Yann LeCun and his colleagues at AT&T Labs, is widely considered the first
successful CNN architecture. It was developed specifically for handwritten digit recognition on the MNIST
dataset and was deployed commercially by banks to read checks and zip codes on envelopes. LeNet-5
consists of approximately 60,000 parameters organized in 7 layers (excluding input and output): two
convolutional layers (with 6 and 16 feature maps respectively, using 5×5 kernels), each followed by
average pooling and subsampling, then three fully connected layers. The activation function used was
the sigmoid/tanh function (ReLU was not yet popularized). The input was a 32×32 grayscale image, and
the architecture progressively reduced spatial dimensions while increasing feature depth, a pattern that
became the standard template for all subsequent CNNs. Despite its simplicity by modern standards,
LeNet-5 achieved over 99% accuracy on MNIST and demonstrated that learned features from
convolutional layers far outperformed hand-crafted features for pattern recognition tasks.
AlexNet (2012)
AlexNet, developed by Alex Krizhevsky, Ilya Sutskever, and Geoffrey Hinton, won the 2012 ImageNet
Large Scale Visual Recognition Challenge (ILSVRC) with a top-5 error rate of 15.3% — a dramatic
improvement over the runner-up’s 26.2%. This victory is widely credited with igniting the modern deep
learning revolution. AlexNet introduced several key innovations: (1) ReLU activationinstead of sigmoid,
which significantly accelerated training by alleviating the vanishing gradient problem; (2)Dropout
regularization (rate 0.5) in the fully connected layers to reduce overfitting; (3) GPU-based training —
the model was split across two GTX 580 GPUs, demonstrating that massive parallel computation was
essential for training deep networks; (4)data augmentation including random crops, horizontal flips,
and color jittering. The architecture has 8 learned layers (5 convolutional + 3 fully connected) with
approximately 60 million parameters. It used 11×11, 5×5, and 3×3 kernels with a combination of max
pooling and Local Response Normalization (LRN).
VGGNet (2014)
VGGNet, developed by Karen Simonyan and Andrew Zisserman at Oxford University, introduced a
radically simple design philosophy: use only 3×3 convolutional filters throughout the entire
network, but stack many of them. The key insight was that two 3×3 convolutions have the same
effective receptive field as one 5×5 convolution (5×5 = two 3×3 stacked with padding), but with fewer
parameters (2 × 9 = 18 vs. 25 weights per filter) and more non-linearity (two ReLU activations instead of
one). The most popular variants are VGG-16 (16 weight layers) and VGG-19 (19 weight layers), with
approximately 138 million parameters. VGG achieved 7.3% top-5 error on ImageNet and
demonstrated that depth is critical for performance. Its clean, uniform design made it a popular feature
extractor for transfer learning. The main drawback is its enormous computational cost and parameter
count, especially in the fully connected layers which contain over 120M of the 138M total parameters.
ResNet (2015)
ResNet (Residual Network), developed by Kaiming He and colleagues at Microsoft Research, solved one
of the most fundamental problems in deep learning: the vanishing gradient problem in very deep
networks. Before ResNet, researchers observed that simply stacking more layers could actually degrade
performance — not because of overfitting, but because deeper networks became harder to optimize.
ResNet introduced residual (skip) connections that allow the gradient to flow directly through shortcut
paths, bypassing one or more layers. The core idea is expressed in the residual learning formulation:
mathbfa[l+2] =
mathcalF (
mathbfa[l] ) +
mathbfa[l]
Here, F(a[l]) represents the residual mapping learned by the stacked layers, and a[l] is the identity
shortcut connection. Instead of learning the full underlying mapping H(x), the network learns the residual
F(x) = H(x) - x. If the optimal transformation is close to identity (as is often the case when adding layers),
learning the residual is much easier than learning the full mapping from scratch. This simple but
profound innovation enabled training of networks with 152 layers (and later over 1000 layers),
achieving 3.57% top-5 error on ImageNet — surpassing human-level performance (~5.1%). ResNet also
uses batch normalization after each convolution and a global average pooling layer before the final fully
connected classifier.
Detailed Comparison
~25,600,000
Parameters ~60,000 ~60,000,000 ~138,000,000
(ResNet-152)
Aspect LeNet-5 AlexNet VGGNet ResNet
Kernel Sizes 5×5, 2×2 11×11, 5×5, Uniform 3×3 7×7 (first), 3×3,
Used pooling 3×3 throughout 1×1 (bottleneck)
No explicit
Max Pooling + pooling; uses
Pooling Type Average Pooling Max Pooling (2×2)
Overlapping Pool strided conv or
GAP
Batch
Dropout (0.5) +
None (small Dropout + Data Normalization +
Regularization Data
model) Augmentation Data
Augmentation
Augmentation
In summary, each architecture built upon the innovations of its predecessors. LeNet proved the concept
of CNNs, AlexNet demonstrated that deep learning could dominate large-scale vision tasks, VGG showed
the power of depth with simple building blocks, and ResNet broke the depth barrier with residual
connections. These principles — convolution, ReLU, batch normalization, skip connections, and
progressive downsampling — remain the foundation of virtually every modern CNN architecture, from
MobileNet to EfficientNet to vision transformers.
Q4
Transfer learning works because of the hierarchical nature of feature learningin CNNs. Research has
shown that the first convolutional layer learns generic Gabor-like filters and edge detectors, the second
layer learns combinations of edges to detect textures and simple patterns, the third layer begins to
detect object parts (wheels, eyes, leaves), and deeper layers detect complete objects or highly specific
combinations of features. This means that features learned on ImageNet are remarkably transferable to
domains as diverse as medical imaging, satellite imagery, industrial defect detection, and art
classification.
There are two primary strategies for applying transfer learning in CNNs, and the choice between them
depends on two key factors: the size of the new dataset and its similarity to the original dataset
(ImageNet).
In feature extraction, we freeze all the convolutional base layers of the pre-trained model (set their
weights to non-trainable) and only train a new classifier head (typically one or two fully connected layers)
on top of the pre-extracted features. The pre-trained base acts as a fixed feature extractor that converts
input images into rich feature representations. This approach is fast, requires less memory, and is highly
effective when the new dataset is small (hundreds to a few thousand images) and similar to ImageNet.
The rationale is that with limited data, we risk overfitting if we try to update the many parameters in the
base network. By freezing the base, we only need to learn the weights of the small classifier head, which
has far fewer parameters and can be trained effectively even with a small dataset. This approach
typically involves removing the original fully connected classification layer from the pre-trained model
and adding a new Dense layer (or Global Average Pooling followed by Dense) with an output size
matching the number of classes in the new task.
In fine-tuning, we unfreeze some or all of the layers in the pre-trained base model and continue training
them on the new dataset along with the new classifier head. This allows the network to adapt its learned
features to the specific characteristics of the new task. Fine-tuning is appropriate when the new dataset
is large enough (tens of thousands of images) or when it differs significantly from ImageNet — for
example, medical X-ray images, aerial satellite photos, or microscopy data have very different statistics
from natural images. A common strategy is to partially fine-tune: freeze the early layers (which
learned very general features) and only unfreeze the last few convolutional blocks, allowing them to
adapt to the new domain while preserving the generic low-level features. It is also important to use a
lower learning rate for fine-tuning (e.g., 1/10th of the original) to avoid catastrophically forgetting the
pre-trained features and destroying the useful representations in a single update step.
FIGURE 3: FEATURE EXTRACTION VS. FINE-TUNING APPROACHES
Predictions Predictions
Consider the task of classifying chest X-ray images into "Normal" and "Pneumonia" using a dataset of
only 5,000 images. Training a CNN from scratch on such a small dataset would likely overfit. Instead, we
use an ImageNet-pretrained ResNet-50 as our base model. Since medical images differ significantly
from natural ImageNet images, we choose the fine-tuning approach: we freeze the first 3 convolutional
blocks (which learned generic low-level features like edges and textures) and unfreeze the last 2 blocks
plus the classifier head, allowing them to adapt to the medical imaging domain. We use a lower learning
rate (1e-4 instead of the default 1e-3) for the fine-tuned layers to avoid destroying pre-trained features.
With this approach, we can achieve over 95% accuracy — a result that would require tens of thousands
of medical images if training from scratch.
Reduced data requirements: Effective training with small datasets (hundreds to thousands of
images instead of millions)
Faster training: The model starts from a good initialization, requiring far fewer epochs to converge
Better performance: Pre-trained features provide a rich starting point that often leads to higher
accuracy than training from scratch
Lower computational cost: Fewer training iterations mean less GPU time and energy consumption
Cross-domain applicability: Pre-trained features transfer surprisingly well across domains (natural
images → medical, satellite, industrial)
In summary, transfer learning has democratized deep learning by making state-of-the-art computer
vision accessible to practitioners and researchers who do not have access to massive datasets or
extensive compute resources. It is the recommended starting point for virtually every practical CNN
project, and the choice between feature extraction and fine-tuning should be guided by the size and
similarity of the target dataset.
Q5
cnn_classifier.py
1 # ============================================================
2 # CNN Image Classifier for CIFAR-10 using TensorFlow/Keras
3 # ============================================================
4
5 import tensorflow as tf
6 from [Link] import layers, models, datasets
7 import numpy as np
8 import [Link] as plt
9
10 # ── Step 1: Load and Preprocess the Dataset ──────────────
11 # CIFAR-10 contains 60,000 32x32 color images in 10 classes
12 # 50,000 for training and 10,000 for testing
13 (train_images, train_labels), (test_images, test_labels) = datasets.cifar10.load_data()
14
15 # Normalize pixel values to be between 0 and 1
16 # This helps the optimizer converge faster
17 train_images = train_images.astype('float32') / 255.0
18 test_images = test_images.astype('float32') / 255.0
19
20 # Class names for reference
21 class_names = ['Airplane', 'Automobile', 'Bird', 'Cat', 'Deer',
22 'Dog', 'Frog', 'Horse', 'Ship', 'Truck']
23
24 print(f"Training data shape: {train_images.shape}") # (50000, 32, 32, 3)
25 print(f"Test data shape: {test_images.shape}") # (10000, 32, 32, 3)
26
27 # ── Step 2: Build the CNN Architecture ──────────────────
28 # Architecture: Conv -> Conv -> Pool -> Conv -> Conv -> Pool
29 # -> Flatten -> Dense -> Dropout -> Dense
30 model = [Link]([
31 # First Convolutional Block
32 # 32 filters of size 3x3, ReLU activation, 'same' padding preserves dimensions
33 layers.Conv2D(32, (3, 3), activation='relu', padding='same',
34 input_shape=(32, 32, 3)),
35 layers.Conv2D(32, (3, 3), activation='relu', padding='same'),
36 # MaxPooling reduces spatial dims from 32x32 to 16x16
37 layers.MaxPooling2D((2, 2)),
38 # Dropout randomly zeros 25% of neurons to prevent overfitting
39 [Link](0.25),
40
41 # Second Convolutional Block
42 # 64 filters — more filters capture more complex features
43 layers.Conv2D(64, (3, 3), activation='relu', padding='same'),
44 layers.Conv2D(64, (3, 3), activation='relu', padding='same'),
45 # MaxPooling reduces spatial dims from 16x16 to 8x8
46 layers.MaxPooling2D((2, 2)),
47 [Link](0.25),
48
49 # Third Convolutional Block
50 # 128 filters for even more complex feature representations
51 layers.Conv2D(128, (3, 3), activation='relu', padding='same'),
52 # MaxPooling reduces spatial dims from 8x8 to 4x4
53 layers.MaxPooling2D((2, 2)),
54 [Link](0.25),
55
56 # ── Classification Head ────────────────────────────
57 # Flatten the 3D feature maps into a 1D vector
58 # Shape: (4, 4, 128) -> 2048
59 [Link](),
60
61 # Fully connected layer with 512 neurons
62 [Link](512, activation='relu'),
63 [Link](0.5),
64
65 # Output layer: 10 units (one per class) with softmax
66 # Softmax converts raw logits to probability distribution
67 [Link](10, activation='softmax'),
68 ])
69
70 # ── Step 3: Compile the Model ──────────────────────────
71 # Adam optimizer: adaptive learning rate, good default choice
72 # Sparse Categorical Crossentropy: for integer-labeled multi-class classification
73 [Link](
74 optimizer='adam',
75 loss='sparse_categorical_crossentropy',
76 metrics=['accuracy']
77 )
78
79 # Print model summary to inspect architecture and parameter count
80 [Link]()
81
82 # ── Step 4: Train the Model ────────────────────────────
83 # Train for 20 epochs with batch size of 64
84 # validation_split=0.1 reserves 10% of training data for validation
85 history = [Link](
86 train_images, train_labels,
87 epochs=20,
88 batch_size=64,
89 validation_split=0.1,
90 verbose=1
91 )
92
93 # ── Step 5: Evaluate on Test Set ───────────────────────
94 test_loss, test_acc = [Link](test_images, test_labels, verbose=2)
95 print(f"\nTest Accuracy: {test_acc * 100:.2f}%")
96 print(f"Test Loss: {test_loss:.4f}")
97
98 # ── Step 6: Plot Training & Validation Curves ──────────
99 fig, axes = [Link](1, 2, figsize=(12, 4))
100
101 # Accuracy plot
102 axes[0].plot([Link]['accuracy'], label='Training Accuracy')
103 axes[0].plot([Link]['val_accuracy'], label='Validation Accuracy')
104 axes[0].set_title('Training and Validation Accuracy')
105 axes[0].set_xlabel('Epoch')
106 axes[0].set_ylabel('Accuracy')
107 axes[0].legend()
108
109 # Loss plot
110 axes[1].plot([Link]['loss'], label='Training Loss')
111 axes[1].plot([Link]['val_loss'], label='Validation Loss')
112 axes[1].set_title('Training and Validation Loss')
113 axes[1].set_xlabel('Epoch')
114 axes[1].set_ylabel('Loss')
115 axes[1].legend()
116
117 plt.tight_layout()
118 [Link]('training_curves.png', dpi=150, bbox_inches='tight')
119 [Link]()
120
121 # ── Step 7: Make Predictions on Test Images ─────────────
122 predictions = [Link](test_images[:5])
123
124 for i in range(5):
125 predicted_class = [Link](predictions[i])
126 actual_class = test_labels[i][0]
127 confidence = predictions[i][predicted_class] * 100
128 print(f"Image {i+1}: Predicted={class_names[predicted_class]} "
129 f"(Confidence: {confidence:.1f}%), "
130 f"Actual={class_names[actual_class]}")
131
132 # ============================================================
133 # Expected model summary output:
134 # ============================================================
135 # Layer (type) Output Shape Param #
136 # ============================================================
137 # conv2d (Conv2D) (None, 32, 32, 32) 896
138 # conv2d_1 (Conv2D) (None, 32, 32, 32) 9248
139 # max_pooling2d (MaxPooling) (None, 16, 16, 32) 0
140 # dropout (Dropout) (None, 16, 16, 32) 0
141 # conv2d_2 (Conv2D) (None, 16, 16, 64) 18496
142 # conv2d_3 (Conv2D) (None, 16, 16, 64) 36928
143 # max_pooling2d_1 (MaxPool) (None, 8, 8, 64) 0
144 # dropout_1 (Dropout) (None, 8, 8, 64) 0
145 # conv2d_4 (Conv2D) (None, 8, 8, 128) 73856
146 # max_pooling2d_2 (MaxPool) (None, 4, 4, 128) 0
147 # dropout_2 (Dropout) (None, 4, 4, 128) 0
148 # flatten (Flatten) (None, 2048) 0
149 # dense (Dense) (None, 512) 1049088
150 # dropout_3 (Dropout) (None, 512) 0
151 # dense_1 (Dense) (None, 10) 5130
152 # ============================================================
153 # Total params: 1,201,642
154 # Trainable params: 1,201,642
155 # Non-trainable params: 0
156 # ============================================================
157 # Expected test accuracy after 20 epochs: ~80-85%
The architecture follows the standard CNN design pattern. Conv2D layers perform the convolution
operation to extract hierarchical features — 32 filters in the first block detect edges and textures, 64
filters in the second block detect more complex patterns, and 128 filters in the third block detect high-
level features. Same padding preserves spatial dimensions during convolution so that pooling layers
control the downsampling. MaxPooling2D layers reduce spatial dimensions by half at each stage
(32×32 → 16×16 → 8×8 → 4×4), progressively building larger receptive fields. Dropout randomly
deactivates neurons during training, serving as regularization to prevent overfitting. The Flatten layer
converts the 4×4×128 feature maps into a 2048-dimensional vector. The Dense(512, ReLU) layer
performs the final classification using learned feature combinations. The Dense(10, softmax) output
layer produces a probability distribution over the 10 classes. The model is compiled with the Adam
optimizer and sparse categorical crossentropy loss, which is appropriate for multi-class
classification with integer labels. After 20 epochs of training, this model typically achieves around 80-
85% test accuracy on CIFAR-10.
Q6
What is YOLO?
YOLO (You Only Look Once) is a family of real-time object detection algorithms that revolutionized
computer vision by treating detection as a single regression problem. Unlike previous approaches like R-
CNN that used a two-stage pipeline (first generate region proposals, then classify each region), YOLO
performs detection in a single forward pass through the network. The name "You Only Look Once"
captures this core idea: the entire image is processed in one go, simultaneously predicting bounding
boxes, objectness scores, and class probabilities for all objects in the image. This unified approach makes
YOLO extremely fast — capable of processing 45 to 150+ frames per second depending on the variant —
enabling real-time applications that were previously impossible with slower, multi-stage detectors.
YOLO divides the input image into an S × S grid (for example, 13×13 in YOLOv2, 19×19 in YOLOv3). If
the center of an object’s ground truth bounding box falls into a particular grid cell, that cell is responsible
for detecting that object. Each grid cell predicts B bounding boxes (typically B=3 or B=5), each
consisting of 5 values: x, y (center coordinates), w, h (width and height), and a confidence score.
Additionally, each grid cell predicts C conditional class probabilities (e.g., C=80 for the COCO
dataset), representing the probability of each class given that an object is present. The confidence score
reflects how confident the model is that a bounding box contains an object AND how accurate the box is,
computed as:
C = P r(Object)
truth
timesIOUpred
Here, Pr(Object) is 1 if there is an object in the cell (otherwise 0), and IOU is the Intersection over Union
between the predicted box and the ground truth box. During inference, cells with confidence below a
threshold are discarded. Since multiple grid cells may detect the same object, YOLO applies Non-
Maximum Suppression (NMS) to eliminate redundant overlapping boxes, keeping only the detections
with the highest confidence scores. The final output is a set of bounding boxes, each with an associated
class label and confidence score.
YOLO Architecture
The YOLO architecture can be decomposed into three main components. The backbone (feature
extractor) is a deep CNN that processes the input image and produces multi-scale feature maps. Early
versions used a custom Darknet architecture, while newer versions (YOLOv5-v8) use modified
CSPDarknet or EfficientNet backbones. The neck is a feature aggregation module (typically a Feature
Pyramid Network or Path Aggregation Network) that combines feature maps from different scales,
enabling the network to detect objects at various sizes — small objects from high-resolution feature
maps and large objects from low-resolution feature maps. The detection head is the final set of
convolutional layers that produce the predictions: bounding box coordinates, objectness scores, and class
probabilities for each grid cell. This modular design makes modern YOLO architectures highly flexible and
easy to customize for different applications and hardware constraints.
The YOLO family has evolved rapidly since its introduction in 2015. YOLOv1(2015) introduced the unified
detection concept but struggled with small objects and spatial accuracy. It used a 24-layer convolutional
network followed by 2 fully connected layers, running at 45 FPS. YOLOv2/YOLO9000 (2017) introduced
batch normalization, anchor boxes (predefined box shapes), and multi-scale training, significantly
improving recall and localization. YOLOv3 (2018) adopted a much deeper Darknet-53 backbone with
residual connections, introduced multi-scale prediction using Feature Pyramid Networks (detecting
objects at 3 different scales), and achieved much better performance on small objects.YOLOv4 (2020)
focused on optimization techniques: Mosaic data augmentation, CIoU loss, and various training tricks to
squeeze out better accuracy. YOLOv5(2020) by Ultralytics was the first PyTorch-based implementation,
making the framework much more accessible and user-friendly. YOLOv7 (2022) introduced efficient
model re-parameterization and auxiliary head training. YOLOv8 (2023) is the latest and most versatile
version, supporting detection, segmentation, classification, and pose estimation with an anchor-free
head, achieving state-of-the-art speed-accuracy trade-offs.
Applications of YOLO
Autonomous Driving: Real-time detection of pedestrians, vehicles, traffic signs, and obstacles from
dashboard cameras. YOLO’s speed is critical for safety systems that need sub-100ms response times.
Video Surveillance: Detecting and tracking people, vehicles, and suspicious activities in security
camera feeds. YOLO can process multiple camera streams simultaneously on edge hardware.
Medical Imaging: Detecting tumors, lesions, and anatomical structures in X-rays, CT scans, and
histopathology slides. YOLOv5 and YOLOv8 have been adapted for various diagnostic tasks.
Retail & Inventory: Automated product recognition on shelves, customer behavior analysis, and self-
checkout systems. YOLO enables real-time counting and classification of products.
Industrial Defect Detection: Identifying cracks, scratches, and manufacturing defects on production
lines at high speed, enabling automated quality control.
Sports Analytics: Tracking players and balls in real-time for performance analysis, broadcast
augmentation, and automated refereeing assistance.
Very fast: 45–150+ FPS Moderate: 5–18 FPS (Faster R-CNN with
Speed (FPS)
depending on variant ResNet-50)
Good–Excellent: YOLOv8
Excellent: typically higher mAP on standard
Accuracy (mAP) achieves SOTA on many
benchmarks than older YOLO versions
benchmarks
Small Object Improved in v3+ with FPN, but Strong: region proposals capture objects of
Detection still a relative weakness all sizes effectively
Moderate — grid-based
Localization High — RoI pooling provides precise
predictions have limited spatial
Accuracy bounding box refinement
resolution
Architecture Simpler: single network, easier Complex: RPN + RoI Pooling + classification
Complexity to deploy and optimize head
YOLO’s primary advantage is its exceptional speed, which makes it the preferred choice for any
application requiring real-time detection. Its single-pass architecture also means it sees the entire image
during inference, giving it strong contextual understanding and fewer false positives on background
regions. YOLO learns generalizable representations of objects and naturally extends to new domains.
However, YOLO has historically struggled with small object detection— because it divides the image
into a coarse grid (e.g., 13×13 or 19×19), multiple small objects falling into the same grid cell can only
be detected one at a time. It can also struggle with objects that are very close together or with highly
precise localization requirements. Additionally, the strict grid structure means YOLO may predict fewer
bounding boxes per image compared to region-proposal methods. Modern versions (v5, v8) have
addressed many of these limitations through multi-scale prediction, anchor-free designs, and improved
loss functions, but the fundamental trade-off between speed and accuracy remains: YOLO sacrifices a
small amount of accuracy for a massive gain in speed, which is exactly the right trade-off for real-world
deployment scenarios.
Q1
Recurrent Neural Networks (RNNs) are a class of neural networks specifically designed to handle
sequential data — data where the order of elements matters. Unlike traditional feedforward networks
that process each input independently, RNNs maintain an internal hidden state that acts as a form of
memory, allowing information from previous time steps to influence the processing of the current input.
This recurrent connection is the defining characteristic of RNNs and makes them naturally suited for
tasks where context and temporal dependencies are crucial, such as understanding a sentence word-by-
word or predicting the next value in a time series. The key insight behind RNNs is that they share the
same set of parameters across all time steps, which dramatically reduces the total number of parameters
compared to having a separate network for each time step, while still capturing temporal dynamics.
RNN Architecture
At each time step t, an RNN receives an input vector xt and produces an outputyt. Internally, the network
maintains a hidden state ht that serves as the network’s memory. The hidden state at time t is computed
as a function of both the current input and the previous hidden state. The output at each time step is
then computed from the current hidden state. This creates a loop-like structure when the network is
drawn in its folded form, and a chain-like structure when it is "unrolled" across time. Mathematically, the
core RNN computations at each time step are:
yt = softmax(Why h(t) + by )
Here, Whh is the weight matrix connecting the previous hidden state to the current hidden state, Wxh
connects the input to the hidden state, Why maps the hidden state to the output, andbh and by are bias
vectors. The tanh activation function squashes the hidden state values to the range [-1, 1], and softmax
converts the output logits into a probability distribution. The initial hidden state h0 is typically initialized
to zeros. During training, the same weight matrices are used at every time step — this is known
asweight sharing across time, which is the core principle that gives RNNs their ability to handle
variable-length sequences.
x₁ x₂ x₃
W_xh
Hidden State
W_hh W_hh
h₁ h₂ h₃
W_hy Output
y₁ y₂ y₃
Unfolding in Time
The concept of unfolding (or unrolling) in time is fundamental to understanding how RNNs are
trained and implemented. In the conceptual "folded" view, the RNN appears as a single cell with a self-
loop — the hidden state feeds back into itself. However, for computation and training, we "unfold" this
loop across the sequence length T, creating T copies of the RNN cell, each processing one time step. The
hidden state ht-1 from the previous cell becomes an additional input to the cell at time t. This unfolding
serves two critical purposes: first, it makes the data flow explicit for forward propagation, and second, it
creates a computational graph through which gradients can flow backward during Backpropagation
Through Time (BPTT). During BPTT, the loss at the final output is used to compute gradients that
propagate backward through all unrolled time steps, accumulating gradient contributions from each step.
This is essentially applying the standard backpropagation algorithm to the unrolled computational graph.
The unfolding also reveals why RNNs can handle variable-length sequences — we simply unroll the
network for as many time steps as there are elements in the input sequence.
Applications of RNNs
RNNs have been widely applied across numerous domains that involve sequential or temporal data. In
natural language processing (NLP), RNNs power language modeling (predicting the next word in a
sentence), machine translation (encoding a source sentence and decoding a target sentence), and text
classification. In speech recognition, RNNs process audio feature sequences frame by frame to
transcribe spoken language into text. For time series prediction, RNNs are used in financial forecasting
(stock prices, market trends), weather prediction, and demand forecasting. They are also used in music
generation, video analysis, and named entity recognition. The versatility of RNNs stems from their
ability to maintain and update a memory of past inputs, making them the go-to architecture for any
problem where the temporal order of data points carries important information. Many-to-one
architectures (e.g., sentiment analysis) use only the final hidden state, while many-to-many architectures
(e.g., sequence-to-sequence models) produce outputs at every time step.
Q2
Discuss the Vanishing Gradient Problem and how LSTM solves it.
The vanishing gradient problem is one of the most significant challenges in training deep neural
networks and recurrent neural networks. During backpropagation, gradients are computed using the
chain rule, which involves multiplying Jacobian matrices (or their scalar equivalents) at each layer or time
step. When these Jacobians have eigenvalues less than 1 — which is very common when using sigmoid
or tanh activation functions whose derivatives are bounded above by 0.25 and 1.0 respectively — the
product of many such terms shrinksexponentially toward zero. As a result, gradients arriving at early
layers (or early time steps in an RNN) become negligibly small, meaning those parameters receive
almost no update during gradient descent. The network effectively fails to learn from distant parts of the
sequence, creating a practical limit on the length of dependencies that a standard RNN can capture.
In an RNN, the gradient of the loss with respect to the hidden state at an early time step involves the
product of Jacobians across all intervening time steps. This chain of multiplications is the root cause of
the problem. For the gradient of the loss L with respect to the hidden state at time step 1, the chain rule
gives:
Each term ∂hk/∂hk-1 involves the derivative of the tanh activation multiplied by the weight matrix Whh.
Since the maximum derivative of tanh is 1 (and it is much less than 1 for most input values), and the
singular values of Whh are typically less than 1 for a properly initialized network, the product of many
such terms decays exponentially. For a sequence of length T, the gradient magnitude is roughly
proportional to (γ)T where γ < 1, making it vanish for long sequences. Even with careful initialization
using orthogonal matrices or identity-like initialization, the activation function derivatives still constrain
the gradient flow. Gradient clipping can address the exploding gradient case (by capping gradient
norms), but does not solve the vanishing gradient problem.
The Long Short-Term Memory (LSTM) network, introduced by Hochreiter and Schmidhuber in 1997,
was specifically designed to address the vanishing gradient problem. The key innovation is the
introduction of a cell state Ct, which serves as a dedicated long-term memory channel. Unlike the
hidden state in a standard RNN, which is completely overwritten at each time step through a
multiplicative interaction, the cell state is updated through additive interactions. This means
information can flow through the cell state across many time steps with minimal modification, creating a
pathway where gradients can propagate without vanishing. The cell state acts like a conveyor belt that
runs through the entire chain of time steps, with only minor linear interactions at each gate. This
architectural choice ensures that the derivative of the cell state with respect to itself at a previous time
step is approximately 1 (or at least not vanishingly small), allowing gradients to flow unchanged across
hundreds of time steps.
The LSTM controls information flow through three gating mechanisms — the forget gate, the input
gate, and the output gate — plus a cell candidate vector. Each gate uses a sigmoid activation (σ) to
produce values between 0 and 1, acting as a soft switch that determines how much information to let
through. The forget gate decides what proportion of the previous cell state to retain, the input gate
decides what new information to write, and the output gate decides what portion of the updated cell
state to expose as the hidden state. The four key computations at each time step are:
Forget Gate
The forget gate examines the current input and previous hidden state to determine which components of
the cell state should be discarded. A value close to 0 means "forget this completely," while a value close
to 1 means "keep this entirely."
Input Gate
The input gate determines which new information should be stored in the cell state. It works together
with the cell candidate to selectively update the memory.
Cell Candidate
The cell candidate creates a vector of new potential values that could be added to the cell state. The
tanh activation squashes values to [-1, 1], which is then modulated by the input gate.
~
Ct = tanh(WC ⋅ [h(t − 1), xt] + bC )
Output Gate
The output gate controls what part of the cell state is used to compute the hidden state (output) at the
current time step. This allows the network to selectively expose relevant information from memory.
ot = σ(Wo ⋅ [h(t − 1), xt] + bo )
The cell state is updated by combining the forgotten past with the new candidate information. The
hidden state is then produced by filtering the cell state through the output gate:
~
C(t) = ft ⊙ C(t − 1) + it ⊙ Ct
h(t) = ot ⊙ tanh(C(t))
Notice that the cell state update uses addition (the + sign) rather than complete replacement. This
additive structure is the key to solving the vanishing gradient problem: when the forget gate ft is close to
1, the gradient with respect to Ct-1 flows through nearly unchanged. The derivative ∂Ct/∂Ct-1 = ft can be
close to 1, creating a near-identity mapping that allows information and gradients to persist across many
time steps. This is fundamentally different from a standard RNN where the hidden state is entirely
recomputed at each step through a nonlinear transformation of the previous hidden state.
C(t-1) C(t)
× +
xt ×
[h(t-1), xt] Candidate tanh
tanh of C(t)
h(t-1)
× h(t)
The fundamental reason LSTMs solve the vanishing gradient problem lies in the additive nature of the
cell state update. In a standard RNN, the hidden state is computed as ht = tanh(Wht-1 + ...), which
means ∂ht/∂ht-1 involves a derivative of tanh multiplied by W, which is consistently less than 1 in
magnitude. In contrast, the LSTM cell state is updated as Ct = ft ⊙ Ct-1 + it ⊙ C̃ t, giving ∂Ct/∂Ct-1
= ft. When the forget gate outputs values near 1, this derivative is also near 1, creating a near-identity
mapping that allows gradients to flow across arbitrarily many time steps. The network can learn to set ft
≈ 1 for information that needs to be preserved and ft ≈ 0 for information that should be discarded. This
learnable, data-dependent gating mechanism provides a much more flexible and effective solution than
techniques like gradient clipping or careful weight initialization, which are merely workarounds rather
than fundamental architectural solutions.
Q3
The LSTM uses a sophisticated architecture with three gates (forget, input, and output) and maintains
two separate state vectors: the cell state Ctfor long-term memory and the hidden state ht for short-
term memory and output. The cell state flows through time with only linear (additive) modifications,
while the hidden state is produced by filtering the cell state through the output gate. This dual-state
design provides the LSTM with fine-grained control over what information to remember, forget, and
expose at each time step. The LSTM has four sets of weight matrices (for the forget gate, input gate, cell
candidate, and output gate), plus their corresponding biases, resulting in4(n2 + nm + n) trainable
parameters where n is the hidden size and m is the input size. This larger parameter count gives the
LSTM greater expressive power but also makes it more computationally expensive and prone to
overfitting on smaller datasets.
The GRU, introduced by Cho et al. in 2014, is a simplified variant of the LSTM that merges the cell state
and hidden state into a single state vector ht. It uses only two gates: thereset gate rt (which controls
how much of the previous hidden state to ignore when computing the candidate) and the update gate
zt (which controls the balance between the previous state and the new candidate). The GRU’s key
equations are:
The update gate zt plays a role similar to the combined forget and input gates of an LSTM. When ztis
close to 1, the new hidden state relies primarily on the new candidate (similar to the LSTM writing new
information); when zt is close to 0, the previous hidden state is preserved (similar to the LSTM
forgetting). The reset gate rt controls how much the previous hidden state contributes to the candidate
computation — when rt is close to 0, the candidate is computed almost entirely from the current input,
allowing the network to "drop" irrelevant past information. With only two gates and three weight
matrices, the GRU has approximately3(n2 + nm + n) parameters, making it about 25% more parameter-
efficient than an LSTM of the same hidden size.
× ×
C̃_t (tanh)
× h(t)
h(t-1) tanh(W[r·h,x])
3 gates + candidate ×
xt ×
xt
+
h(t)
2 gates + candidate
Detailed Comparison
Slightly better at capturing very long- Good, but may struggle with
Long-Range
range dependencies due to dedicated extremely long sequences
Dependencies
cell state compared to LSTM
Higher risk on small datasets due to Lower risk — fewer parameters act
Overfitting Risk
more parameters as implicit regularization
Architecture More complex: separate cell and Simpler: unified state, two gates,
Complexity hidden states, four interacting gates easier to implement and debug
In practice, the choice between LSTM and GRU often comes down to empirical evaluation on the
specific task and dataset. As a general guideline, LSTMsare preferred when the task requires modeling
very long-range dependencies (hundreds of time steps), when the dataset is large enough to support the
additional parameters without overfitting, or when fine-grained control over memory read/write
operations is important. Tasks like machine translation, speech recognition, and complex language
modeling often benefit from the LSTM’s extra capacity. GRUs are preferred when computational
efficiency is a priority, when working with smaller datasets where overfitting is a concern, or when the
task involves shorter sequences where the LSTM’s additional complexity provides diminishing returns.
Many practitioners adopt the pragmatic approach of starting with a GRU as a baseline and upgrading to
an LSTM only if the GRU’s performance is insufficient. Notably, in the era of Transformers, both LSTMs
and GRUs have been largely superseded for large-scale NLP tasks, but they remain relevant for smaller-
scale applications, edge deployment, and streaming/real-time scenarios where the linear complexity of
recurrent models is advantageous.
Q4
What is Attention?
Attention is a mechanism that allows neural networks to dynamically focus on the most relevant parts
of their input when producing an output. Inspired by the human cognitive ability to focus attention on
specific visual or auditory stimuli while filtering out irrelevant information, the attention mechanism in
deep learning assigns different importance weights to different positions in the input. Rather than
treating all input elements equally, attention enables the model to learn which input elements are most
relevant for each output element, creating weighted sums that emphasize important features. This
concept was first introduced in the context of sequence-to-sequence models for machine translation
(Bahdanau attention, 2014), where it allowed the decoder to attend to different source words at each
decoding step, dramatically improving translation quality for long sentences.
Self-attention is a specific form of attention where the query, key, and value all come from the same
input sequence. Instead of attending from a decoder to an encoder (cross-attention), self-attention
computes relationships between every pair of positions within a single sequence. For each token in the
sequence, self-attention looks at all other tokens (including itself) and determines how much each token
should contribute to the representation of the current token. This allows the model to capture both local
patterns (like adjacent words forming phrases) andlong-range dependencies (like a pronoun referring
to a noun many words earlier) in a single operation, without the sequential bottleneck of RNNs. The self-
attention mechanism is the core building block of the Transformer architecture, introduced by Vaswani et
al. in the landmark 2017 paper "Attention Is All You Need."
Self-attention operates through three learned linear projections of the input, drawing an analogy to
information retrieval systems. Each input token x is projected into three vectors: a Query(Q)
representing what the token is "looking for," a Key (K) representing what the token "contains," and a
Value (V) representing the actual content that will be aggregated. The attention score between two
tokens is computed as the dot product of the query from one token and the key from another — a higher
dot product means the two tokens are more relevant to each other. The input is projected into these
three spaces using learned weight matrices:
Here, X is the input matrix of shape (seq_len, dmodel), and WQ, WK, WVare learned projection matrices of
shape (dmodel, dk), where dk is the dimension of the key/query vectors (typically dk = dmodel / h for h
attention heads). These projections allow the model to learn different representations for querying,
matching, and content extraction, providing flexibility in what constitutes relevance.
The core self-attention computation is called scaled dot-product attention. It computes attention
weights by taking the dot product of Q and K, scaling by the square root of dk, applying softmax to obtain
normalized weights, and then using these weights to compute a weighted sum of V:
QK T
Attention(Q, K, V ) = softmax ( )V
dk
The scaling factor 1/√dk is critical: as the dimensionality dk increases, the dot products QKT grow in
magnitude (because the dot product of two random unit vectors has a variance proportional to dk).
Without scaling, these large values would push the softmax into regions with extremely small gradients,
resulting in vanishing gradients and poor learning. Dividing by √dk normalizes the variance of the dot
products to approximately 1, keeping the softmax in its active, gradient-friendly regime. The softmax
operation converts the scores into a probability distribution, ensuring all attention weights are non-
negative and sum to 1. The final output for each position is a weighted combination of all value vectors,
where the weights are determined by how relevant each key is to the query at that position.
Multi-Head Attention
Rather than performing a single attention function, the Transformer uses multi-head attention, which
runs h parallel attention operations (called "heads") with different learned projections. Each head
independently computes scaled dot-product attention using its own Qi, Ki, Vi projections. The outputs of
all heads are then concatenated and linearly projected to produce the final output. This allows the model
to jointly attend to information from different representation subspaces at different positions — one
head might learn to focus on syntactic relationships, another on semantic similarity, and yet another on
positional patterns. The multi-head mechanism provides richer representations than a single attention
head could, without significantly increasing computational cost (since the dimensionality per head is
reduced proportionally: dk = dmodel / h).
W_Q Q
Dot Product + Scale
QKᵀ / √d_k
(attention scores)
Weights × V
Out
Input X (weighted sum)
W_K K
(seq, d_model) Softmax
W_V V
Positional Encoding
Unlike RNNs, which inherently process tokens in order and thus capture positional information through
their sequential structure, the self-attention mechanism is permutation invariant — it treats the input
as an unordered set of vectors. To inject positional information, the Transformer uses positional
encoding, which adds a position-dependent vector to each token’s embedding. The original Transformer
uses sinusoidal positional encodings: PE(pos, 2i) = sin(pos / 100002i/d_model) and PE(pos, 2i+1) = cos(pos
/ 100002i/d_model). These functions produce unique encodings for each position that the model can learn
to interpret. Modern Transformers often use learned positional embeddingsinstead, where each
position has a trainable vector. This positional information is essential for the model to understand word
order, which is crucial in language tasks. Without positional encodings, the sentence "The dog bit the
man" and "The man bit the dog" would produce identical self-attention outputs despite having very
different meanings.
Q5
Write short notes on GPT, Llama, and Large Language Models (LLMs).
Large Language Models (LLMs) are a class of deep neural networks — typically based on the
Transformer architecture — trained on massive corpora of text data(often hundreds of billions to
trillions of tokens) using self-supervised learning objectives. The term "large" refers to both the model
size (ranging from billions to hundreds of billions of parameters) and the scale of training data. LLMs
learn statistical patterns, relationships, and structures in language by predicting the next token in a
sequence (autoregressive modeling) or by filling in masked tokens (masked language modeling). Through
this process, they develop emergent capabilities including text generation, question answering,
summarization, translation, code writing, and even reasoning-like behaviors. Key characteristics of LLMs
include their few-shot and zero-shot learning abilities — they can perform tasks they were not
explicitly trained on, simply by being given appropriate instructions or a few examples in the prompt.
This generalization capability, which scales predictably with model size and data volume, has made LLMs
one of the most impactful developments in artificial intelligence.
The evolution of GPT models has been remarkable. GPT-1 (2018, 117M parameters) demonstrated that
generative pre-training followed by discriminative fine-tuning improves NLP task [Link]-2
(2019, 1.5B parameters) showed that a sufficiently large language model can perform diverse tasks
without any task-specific fine-tuning (zero-shot learning), generating coherent paragraphs of text. GPT-3
(2020, 175B parameters) was a watershed moment, demonstrating strong few-shot performance across
dozens of tasks using only in-context learning (providing examples in the prompt rather than updating
weights). GPT-4 (2023) is multimodal (accepting both text and images), significantly more capable in
reasoning, and powers ChatGPT, which has become one of the most widely used AI applications in
history.
Key innovations in the GPT lineage include chain-of-thought prompting, which encourages the model
to generate step-by-step reasoning before producing an answer, dramatically improving performance on
mathematical and logical reasoning tasks. RLHF (Reinforcement Learning from Human Feedback)
was introduced with InstructGPT and GPT-3.5 to align model outputs with human preferences, making
responses more helpful, harmless, and honest. Applications of GPT models span virtually every domain:
ChatGPT for conversational AI, GitHub Copilot for code generation, content creation, educational tutoring,
legal document analysis, medical question answering, and countless others. GPT models are proprietary
and accessed through OpenAI’s API, with the model weights not publicly available.
LLaMA (Large Language Model Meta AI) is a family of open-weight language models developed by
Meta AI. Released in early 2023, LLaMA was designed to demonstrate that smaller models trained
onmore data can match or exceed the performance of much larger models trained on less data. This
challenged the prevailing assumption that bigger is always better and made high-quality language
models accessible to the broader research community.
LLaMA 1 (2023) was released in sizes ranging from 7B to 65B parameters, trained on 1 to 1.4 trillion
tokens. Despite being 10x smaller than GPT-3, LLaMA-13B outperformed GPT-3 on many
[Link] 2 (2023) expanded to 7B, 13B, and 70B parameters, with a 34B code-specialized
variant, trained on 2 trillion tokens. It also introduced fine-tuned chat versions (LLaMA-2-Chat) aligned
using [Link] 3 (2024) pushed boundaries further with 8B and 70B base models plus a 405B
flagship model, trained on over 15 trillion tokens, with significantly improved reasoning, coding, and
multilingual capabilities.
Modern LLMs exhibit an impressive array of capabilities: fluent text generation, multi-step reasoning
(with appropriate prompting), code generation and debugging, multilingual understanding and
translation, summarization of long documents, and even creative writing. However, they have well-
documented limitations:hallucinations (confidently generating factually incorrect
information),knowledge cutoff (they don’t know about events after their training data),lack of true
understanding (they are sophisticated pattern matchers, not reasoning engines), bias (reflecting
biases in their training data), andcomputational cost (training costs millions of dollars; inference
requires significant GPU resources). Understanding these capabilities and limitations is essential for using
LLMs effectively and responsibly in real-world applications.
Alignment
RLHF + constitutional AI (ChatGPT) RLHF (LLaMA 2/3 Chat), DPO variants
Method
Limited to prompt engineering and Full fine-tuning, LoRA, QLoRA, and other
Fine-tuning
API-level tools PEFT methods
Prompt engineering is the art and science of crafting effective inputs (prompts) for large language
models to elicit accurate, relevant, and useful responses. Since the quality of an LLM’s output is highly
dependent on how the input is structured, prompt engineering has emerged as a critical skill for working
with these models. Rather than modifying the model’s weights (which requires costly fine-tuning),
prompt engineering leverages the model’s existing capabilities by carefully designing the input context,
instructions, and examples. Effective prompts can dramatically improve an LLM’s performance on
specific tasks, reduce hallucinations, and ensure outputs follow desired formats. It is often the most
practical first approach when working with LLMs, as it requires no additional training and can be iterated
upon quickly.
Zero-shot prompting is the simplest form, where the model is given a task description without any
examples. It relies entirely on the model’s pre-trained knowledge. For example:"Classify the following
review as positive or negative: 'The food was amazing and the service was impeccable.'" The model uses
its understanding of sentiment to respond "Positive" without any prior examples. This works well for
straightforward tasks where the model has strong pre-existing knowledge.
Few-shot prompting provides the model with a small number of input-output examples (typically 2-5)
before asking it to perform the task on a new input. These examples teach the model the desired format
and reasoning pattern. For example:"Translate English to French. Example: 'Hello’ → 'Bonjour’. Example:
'Thank you’ → 'Merci’. Now translate: 'Good morning.'" Few-shot prompting is particularly effective for
formatting, style transfer, and domain-specific tasks where zero-shot performance may be inconsistent.
Chain-of-Thought (CoT) prompting encourages the model to generate intermediate reasoning steps
before producing a final answer. This is typically achieved by adding phrases like "Let’s think step by
step" or by providing examples that include reasoning traces. For example:"If a train travels at 60 mph
and needs to cover 180 miles, how long will it take? Let’s think step by step." The model then generates:
"Speed = 60 mph. Distance = 180 miles. Time = Distance / Speed = 180 / 60 = 3 hours. Answer: 3
hours." CoT prompting has been shown to dramatically improve performance on mathematical
reasoning, logic puzzles, and multi-step problems by forcing the model to decompose complex problems
into manageable steps.
Role-based prompting assigns the model a specific persona or expertise to frame its responses. For
example: "You are an expert pediatrician. A parent describes their 3-year-old having a fever of 103°F for
two days with no other symptoms. What are the possible causes and what should they do?" By
establishing a role, the model adjusts its vocabulary, level of detail, and perspective to match the
specified expertise, often producing more accurate and contextually appropriate responses.
Be specific and clear: Vague prompts produce vague outputs. Clearly state what you want, the
desired format, and any constraints.
Provide context: Include relevant background information that helps the model understand the
domain and constraints of the task.
Use delimiters: Separate different parts of your prompt with clear delimiters (triple quotes, XML tags,
or dashes) to avoid ambiguity.
Specify the output format: Tell the model whether you want JSON, a list, a paragraph, code, or a
specific structured format.
Iterate and refine: Prompt engineering is an iterative process. Analyze failures, adjust the prompt,
and test again.
The RAG pipeline consists of several key stages. First, documents are processed into chunks and
converted into vector embeddings using an embedding model. These embeddings are stored in a vector
database (such as Pinecone, Weaviate, FAISS, or ChromaDB) that supports efficient similarity search.
When a user query arrives, it is also converted into an embedding, and the vector database retrieves the
most similar document chunks using cosine similarity or similar metrics. The retrieved chunks are then
inserted into a prompt template alongside the user’s query, and this augmented prompt is sent to the
LLM for generation. The LLM can then produce a response that is grounded in the retrieved documents,
citing specific information.
Response
(grounded answer)
Knowledge Base
(documents, PDFs, etc.)
RAG addresses several critical limitations of standalone LLMs. First, it significantly reduces
hallucinations by grounding the LLM’s responses in retrieved documents — the model is explicitly told
what information to use, rather than relying solely on its potentially outdated or incomplete parametric
memory. Second, RAG provides up-to-date information: the knowledge base can be updated
independently of the model, so the system can answer questions about recent events without retraining.
Third, RAG enables domain-specific knowledge: organizations can populate the vector database with
their proprietary documents (product manuals, internal policies, research papers) and the LLM can
answer questions about this specialized knowledge without any fine-tuning. Fourth, RAG provides source
attribution: since responses are based on retrieved documents, the system can cite its sources,
increasing transparency and trustworthiness. The retrieval similarity is typically computed using cosine
similarity:
q ⋅d
sim(q, d) =
∥q∥∥d∥
where q is the query embedding vector, d is a document chunk embedding, and · denotes the dot
product. The cosine similarity measures the cosine of the angle between two vectors, ranging from -1
(completely opposite) to +1 (identical direction). In practice, with normalized embeddings, the top-k
document chunks with the highest cosine similarity scores are retrieved and included in the augmented
prompt. Advanced RAG systems may use hybrid search (combining vector similarity with keyword-based
BM25 search), re-ranking of retrieved documents, and query transformation techniques to improve
retrieval quality.
Consider a customer support chatbot for an electronics company. The company has thousands of
product manuals, troubleshooting guides, and FAQ documents. Without RAG, the LLM might generate
generic or incorrect troubleshooting advice. With RAG, the system works as follows: (1) A customer
asks,"My WidgetPro won’t turn on after the firmware update." (2) The query is embedded and the vector
database retrieves the most relevant chunks from the WidgetPro troubleshooting guide, which
includes:"If the device does not power on after a firmware update, hold the power button for 15 seconds
to perform a hard reset. If this does not resolve the issue, connect to a power source and wait 30 minutes
before attempting to power on again." (3) This context is combined with the user’s query into an
augmented prompt sent to the LLM. (4) The LLM generates a response that accurately follows the
documented procedure, citing the specific steps. This approach ensures the customer receives accurate,
company-approved advice rather than a potentially incorrect hallucinated response. The RAG system can
be updated by simply adding new documents to the knowledge base — no model retraining is required
when products are updated or new issues are documented.
Q1
Reinforcement Learning (RL) is a paradigm of machine learning in which an agent learns to make
decisions by interacting with an environment through trial and error. Unlike supervised learning, where
the model is provided with labeled examples of correct input-output pairs, RL provides only a scalar
reward signal — a numerical value that indicates how good or bad the agent’s action was. The agent’s
objective is to learn a strategy (called a policy) that maximises the cumulative reward over time. This
is fundamentally different from supervised learning, which minimises a loss function over a fixed dataset,
and from unsupervised learning, which discovers hidden structure in data without any external feedback.
In RL, the agent must balanceexploration (trying new, potentially rewarding actions) withexploitation
(leveraging actions already known to yield high rewards), a tension that does not exist in other learning
paradigms.
The core distinction of RL is that the learner is not told which actions to take but must instead discover
which actions yield the most reward by trying them. The feedback is delayed — the consequences of an
action may not be immediately apparent, and the agent must learn to attribute long-term outcomes to
earlier decisions. For example, in a game of chess, a single move early in the game may contribute to a
win or loss dozens of moves later. This temporal credit assignment problem is one of the central
challenges in RL and is addressed through techniques like value functions and temporal-difference
learning.
In contrast, supervised learning assumes access to a fixed dataset of input-output pairs and learns a
mapping between them, while unsupervised learning finds patterns or groupings in data without any
labels. RL, on the other hand, is inherently interactive and sequential: the agent’s actions affect the data
it sees next, creating a feedback loop between learning and experience. This makes RL uniquely suited
for problems involving sequential decision-making under uncertainty, such as robotics, game playing,
autonomous driving, and resource management.
The mathematical framework that formalises the RL problem is the Markov Decision Process (MDP).
An MDP provides a precise way to model sequential decision-making where outcomes are partly random
and partly under the control of the decision-maker. It is defined by a tuple ⟨S, A, P, R, γ⟩,
where each component plays a critical role. The key property of an MDP is the Markov property: the
future is conditionally independent of the past given the present state. This means the current state
contains all the information necessary to make optimal decisions, and the agent does not need to
remember the full history of previous states and actions.
States (S): The set of all possible situations the agent can find itself in. Each state represents a
complete description of the environment at a given time. For example, in chess, a state is the board
configuration — the positions of all pieces. In a robot navigation task, a state might be the robot’s
coordinates and orientation. The state space can be finite (discrete states) or infinite (continuous states),
and its size directly impacts the difficulty of solving the RL problem.
Actions (A): The set of all moves the agent can make. Actions can be discrete (e.g., up, down, left,
right in a grid world) or continuous (e.g., the torque applied to each joint of a robotic arm). The available
actions may depend on the current state, in which case we write A(s) to denote the actions available in
state s. The choice of action representation is a critical modelling decision.
Transition Probabilities P(s′|s, a): The dynamics of the environment, specifying the probability of
transitioning to state s′ when the agent takes action a in state s. This captures the stochasticity
(randomness) of the environment. For instance, a robot moving forward might slip with some probability
and end up in a slightly different position. When the environment is fully deterministic, P(s′|s, a) = 1 for
exactly one s′.
Rewards R(s, a, s′): A scalar feedback signal that tells the agent how good or bad the immediate
outcome of its action was. The reward function defines the goal of the RL problem — by specifying what is
good and what is bad, it indirectly defines the desired behaviour. For example, in a game, winning might
yield +1 and losing -1. In a robot task, reaching the goal gives +100, and each step taken incurs a small -1
penalty to encourage efficiency.
Discount Factor γ: A value between 0 and 1 that determines how much the agent cares about future
rewards relative to immediate ones. When γ = 0, the agent is "myopic" and only considers immediate
rewards. When γ is close to 1, the agent is far-sighted and values long-term rewards almost as much as
immediate ones. The discount factor ensures that the sum of rewards remains finite and allows us to
express a preference for sooner rewards.
a
s′
The Bellman optimality equation above is the cornerstone of RL theory. It states that the optimal
value of a state V*(s) is equal to the maximum over all actions of the immediate reward R(s,a) plus the
discounted sum of the optimal values of all possible next states, weighted by their transition
probabilities. This recursive equation expresses the principle of optimality: an optimal policy must choose
actions that not only yield good immediate rewards but also lead to states from which the agent can
continue to behave optimally in the future. Solving this equation (exactly or approximately) is the central
computational challenge in RL, and algorithms like value iteration, policy iteration, and Q-learning are all
different approaches to finding its solution.
Action aₜ
Environment
Agent
State sₜ₊₁ Transition P(s'|s,a)
Policy π(a|s)
Reward R(s,a,s')
Observes state sₜ
Selects action aₜ
Reward rₜ
At each time step t: Agent observes state, takes action, Environment returns new state and reward
The diagram above illustrates the fundamental agent-environment interaction loop. At each time
step t, the agent receives the current state sₜ from the environment, selects an action aₜ according to its
policy π, and sends it to the environment. The environment then transitions to a new state sₜ₊₁ according
to the transition dynamics P(s′|s,a) and generates a reward signal rₜ = R(sₜ, aₜ, sₜ₊₁). This loop continues
indefinitely (or until a terminal state is reached), and the agent’s goal is to learn a policy that maximises
the expected sum of discounted rewards over the entire trajectory.
Real-world examples of this framework are abundant. In chess, the agent is the player, the environment
is the board (including the opponent), states are board configurations, actions are legal moves, and the
reward is +1 for a win, -1 for a loss, and 0 for a draw. In robot navigation, the agent is the robot, the
environment is the physical world (with obstacles and a goal location), states are the robot’s position and
sensor readings, actions are movement commands, and the reward might be +100 for reaching the goal,
-10 for hitting an obstacle, and -1 per time step to encourage efficiency.
Q2
The reward function R(s, a) defines the immediate feedback the agent receives after taking action a in
state s. It is the most fundamental component of an MDP because itdefines the goal of the RL problem.
By specifying what is good and what is bad through numerical values, the reward function indirectly
specifies the desired behaviour without prescribing how to achieve it. For example, in chess, the reward
function might assign +1 for winning, -1 for losing, and 0 for every intermediate move. In a self-driving
car, the reward might include positive values for staying in the lane and reaching the destination, and
large negative values for collisions or running red lights. The reward function is typically designed by the
human engineer and reflects the task objective, though it can also be learned from demonstrations or
preferences (as in RLHF).
It is crucial to understand that the reward function defines what is good, not how to achieve it. The
agent must discover the optimal behaviour through interaction. A well-designed reward function is sparse
(providing feedback only at the end of an episode) or dense (providing feedback at every step). Sparse
rewards make learning harder because the agent receives little guidance, while dense rewards provide
more frequent feedback but may be harder to design correctly. The art of reward shaping — adding
intermediate rewards to guide learning without changing the optimal policy — is an important practical
consideration.
Policy π(a|s)
A policy π defines the agent’s strategy for selecting actions given states. It is the mapping from states to
actions that the agent follows. A policy can bedeterministic, where π(s) = a specifies a single action for
each state, or stochastic, where π(a|s) = P(aₜ = a | sₜ = s) specifies a probability distribution over
actions for each state. Stochastic policies are more general and are necessary when the optimal
behaviour involves randomness (e.g., in rock-paper-scissors, the optimal policy is to play each action with
probability 1/3). In deep RL, the policy is typically parameterised by a neural network with parameters θ,
written as πθ(a|s).
π(a∣s) = P (at = a ∣ st = s)
The policy is what the agent ultimately learns. The entire purpose of RL algorithms — whether value-
based (like Q-learning), policy-based (like REINFORCE), or actor-critic — is to find a policy that maximises
the expected cumulative discounted reward. The policy defines the agent’s behaviour at every step and
is the mechanism through which the agent acts in the environment. A good policy balances exploration
of new actions with exploitation of known rewarding actions, and adapts as the agent’s knowledge of the
environment improves.
∞
V π (s) = Eπ [∑ γ t rt ∣ s0 = s]
t=0
The optimal value function V*(s) is the maximum expected cumulative reward achievable from state s
by following any policy. It serves as an upper bound on how well the agent can possibly do from each
state. Value functions are central to many RL algorithms because they provide a signal for which states
are worth visiting and which actions are worth taking. By estimating V(s) accurately, the agent can make
informed decisions about which actions lead to the most rewarding states over the long run.
The action-value function Q(s, a) (also called the Q-function) goes one step further than the value
function. Instead of evaluating the long-term value of a state, it evaluates the long-term value of taking a
specific action a in state s and then following policy π thereafter. The Q-function answers the question:
"How good is it to take this particular action in this state?" This is more informative than V(s) because it
allows the agent to compare different actions directly: the best action in state s is simply the one with the
highest Q-value.
∞
Qπ (s, a) = Eπ [∑ γ t rt ∣ s0 = s, a0 = a]
t=0
The relationship between V and Q is straightforward: the value of a state under policy π is the expected
Q-value over all actions that π might take in that state — that is, Vπ(s) = Σₐ π(a|s) Qπ(s, a). The optimal
Q-function Q*(s, a) gives the maximum expected return achievable by taking action a in state s and then
acting optimally thereafter. Many practical RL algorithms (especially DQN and its variants) focus on
learning Q-functions because once you have Q*, the optimal policy is simply π*(s) = argmaxₐ Q*(s, a) —
take the action with the highest Q-value.
Q3
Q-learning is a classic value-based RL algorithm that learns the optimal Q-function Q*(s, a) without
requiring a model of the environment. The agent maintains a table of Q-values for every state-action pair
and updates them using the Bellman equation as an iterative update rule. At each step, the agent
observes state s, takes action a, receives reward r, and observes next state s′. The Q-value for (s, a) is
then updated to move closer to the target value r + γ maxₐ′ Q(s′, a′), which is a bootstrapped estimate of
the true value. Over many iterations, the Q-values converge to the optimal Q*, and the optimal policy is
simply to take the action with the highest Q-value in each state.
However, standard Q-learning has a critical limitation: it requires a Q-tablethat stores a separate value
for every state-action pair. This is infeasible for problems with large or continuous state spaces. Consider
an Atari game like Breakout: the raw input is a 210 × 160 pixel image with 128 colour values, yielding
over 10⁶⁰ possible states — far too many for a lookup table. Even a modest robot navigation task with
continuous sensor readings results in an infinite state space. Thiscurse of dimensionality motivated
the development of Deep Q-Networks (DQN), which replace the Q-table with a deep neural network that
can generalise across similar states.
DQN Architecture
Deep Q-Networks (DQN), introduced by DeepMind in 2013 and published in Nature in 2015, combine
Q-learning with deep neural networks to handle high-dimensional state spaces. The architecture takes
raw sensory input (such as game pixels) and processes it through a series ofconvolutional layers that
act as feature extractors, similar to how CNNs process images in computer vision tasks. For Atari games,
the input is typically a stack of 4 consecutive 84 × 84 grayscale frames (the stack provides temporal
information about motion and speed). The CNN extracts spatial features from these frames, which are
then flattened and passed throughfully connected (FC) layers that map the features to Q-values, one
output for each possible action. The network is trained to approximate the Q-function Q(s, a; θ), where θ
are the network weights.
The key insight is that by sharing parameters across similar states through the convolutional layers, the
network can generalise Q-values to states it has never seen before. If two states have similar visual
features (e.g., the paddle is in a similar position relative to the ball), the network will produce similar Q-
values for both. This generalisation is what makes DQN practical for high-dimensional problems.
However, naively combining neural networks with Q-learning introduces two major instabilities that had
to be addressed with key innovations.
The first challenge is that RL data is non-stationary and correlated: consecutive frames in a game are
highly similar, and the distribution of states changes as the policy improves. If the network is trained on
consecutive experiences, it tends to overfit to recent data and become unstable. Experience Replay
solves this by storing each transition (sₜ, aₜ, rₜ, sₜ₊₁) in a replay buffer — a large circular memory that
holds the most recent N transitions (typically N = 1 million). During training, instead of using the latest
transition, the algorithm samples a random mini-batch of transitions from the buffer. This breaks the
correlation between consecutive samples, makes the data distribution more stationary, and greatly
improves data efficiency since each transition can be reused many times for training.
The second challenge is that the Q-learning target r + γ maxₐ′ Q(s′, a′) uses the same network Q that is
being updated. This creates a moving target problem: as the network weights change, the target
values change too, leading to instability and [Link] Networks address this by maintaining a
separate, slowly-updated copy of the Q-network called Q_target. The target network’s weights θ⁻ are
frozen and only updated periodically (e.g., every 1000 steps) by copying the main network’s weights.
The target value is computed using the target network, which remains fixed between updates, providing
a stable learning signal.
y = r + γ max
′
Qtarget (s′ , a′ ; θ− )
The loss function for DQN is the mean squared temporal-difference error between the predicted Q-value
and the target value. The network is trained to minimise this loss using standard gradient descent
optimisers like Adam or RMSProp. The third key technique is epsilon-greedy exploration: with
probability ε (which decays over time from 1.0 to a small value like 0.01), the agent takes a random
action; otherwise, it takes the action with the highest Q-value. This ensures the agent explores the
environment sufficiently early in training before exploiting its learned knowledge.
Input Frames → CNN Feature Extraction → FC Layers → Q-values for each action
The DQN architecture demonstrated that a single algorithm could learn to play a wide variety of Atari
2600 games at human or superhuman levels using only raw pixel inputs and the game score as a reward
signal. DeepMind’s DQN achieved this by combining the representational power of deep convolutional
networks with the decision-making framework of Q-learning, stabilised through experience replay and
target networks. This was a landmark result that sparked the modern deep RL revolution. Subsequent
improvements like Double DQN (which addresses overestimation bias), Dueling DQN (which separates
state value and advantage estimation), and Prioritised Experience Replay (which samples more
informative transitions more frequently) have further improved the performance and stability of DQN-
style algorithms. Applications now extend well beyond games torobotics (visual servoing,
manipulation),autonomous driving (lane changing, navigation), andresource management (data
centre cooling, network routing).
Q4
Policy gradient methods are a family of RL algorithms that directly optimise the policy function πθ(a|s)
by adjusting its parameters θ in the direction of higher expected cumulative reward. Unlike value-based
methods (like Q-learning) that first learn a value function and then derive a policy from it, policy gradient
methods parameterise the policy directly with a neural network and optimise its parameters using
gradient ascent on the expected return. This approach is fundamentally different: instead of learning to
evaluate states or actions and then choosing the best one, the network directly outputs the probability of
each action given the current state. The parameters are updated so that actions that led to high rewards
become more probable, and actions that led to low rewards become less probable.
The theoretical foundation comes from the Policy Gradient Theorem, which proves that the gradient of
the expected return with respect to the policy parameters can be expressed as an expectation over
trajectories. This is significant because it means we can estimate the gradient using Monte Carlo
sampling — by running the policy in the environment, collecting trajectories (sequences of states,
actions, and rewards), and computing a sample-based estimate of the gradient. No backpropagation
through the environment is needed, making the approach applicable even when the environment is a
black box.
The simplest and most foundational policy gradient algorithm is REINFORCE, introduced by Ronald
Williams in 1992. REINFORCE uses the following gradient update: after collecting a complete episode (or
trajectory), the algorithm computes the total return Gₜ (sum of discounted rewards from time t onwards)
for each timestep. The policy parameters are then updated in proportion to the product of the log-
probability of the action taken and the return. The intuition is elegant: if an action led to a high return, we
increase the probability of taking that action in similar states in the future; if it led to a low return, we
decrease that probability.
Breaking down this equation: J(θ) is the objective function we want to maximise — the expected
cumulative discounted return. ∇θ log πθ(a|s) is the score function, which indicates the direction in
parameter space that would increase the probability of action a in state s. It is the gradient of the log-
probability with respect to θ, computed via standard backpropagation through the policy network.Gₜ is
the return (cumulative discounted reward) from time step t to the end of the episode. The product of the
score function and the return forms the policy gradient: positive returns push the parameters to increase
the action’s probability, while negative returns push in the opposite direction.
Policy gradient methods offer several key advantages over value-based methods. First, they can naturally
handlecontinuous action spaces, which is essential for robotics where actions might be continuous
motor torques or joint angles. Value-based methods require discretising the action space or using
specialised extensions, but policy gradient methods can directly output the parameters of a probability
distribution (like the mean and standard deviation of a Gaussian) over continuous actions. Second, policy
gradient methods can learn stochastic policies, which are optimal in many environments (e.g., rock-
paper-scissors, partial observability). Third, they provide adirected exploration strategy through the
policy’s own probability distribution, rather than relying on explicit exploration mechanisms like epsilon-
greedy.
However, policy gradient methods also have significant disadvantages. The most prominent is high
variance in the gradient estimates. Since each gradient update is based on a single trajectory (or a
small batch), the estimate can be very noisy, requiring many samples to obtain a reliable signal. This
makes policy gradient methodssample inefficient — they often require millions of environment
interactions to converge, which can be prohibitively expensive in real-world settings. Additionally, they
can converge to local optima rather than the global optimum, since the policy gradient follows the
gradient of expected return, which may not be a convex function of the parameters.
A more powerful approach is the Actor-Critic architecture, which combines policy gradient methods with
value-based methods. The actor is the policy network πθ(a|s) that selects actions, while the critic is a
value network Vφ(s) that evaluates how good states are. The critic provides a lower-variance baseline for
the actor’s policy gradient updates, and both networks are trained simultaneously: the critic learns to
accurately predict state values (using TD learning), and the actor uses the critic’s estimates as a baseline
to improve its policy. This synergy makes actor-critic methods among the most effective and widely-used
RL algorithms today, forming the basis of advanced methods like A3C, A2C, SAC, and PPO.
Outputs: action aₜ
Critic Vφ(s)
Value Network
Advantage
TD Error: δ = r + γV(s') - V(s) A(sₜ, aₜ) as baseline
Actor selects actions • Critic evaluates states • Advantage feedback reduces gradient variance
Policy gradient methods have found widespread application across diverse domains. Inrobotics, they are
used to train robots for complex manipulation tasks (such as grasping objects, folding cloth, or
assembling parts), locomotion (teaching quadruped robots to walk, run, and navigate rough terrain), and
dexterous in-hand manipulation. The ability to output continuous actions makes policy gradients the
natural choice for these problems. In game playing, policy gradient methods have achieved
superhuman performance in games with large action spaces, including StarCraft (AlphaStar), Dota 2
(OpenAI Five), and Go (AlphaGo Zero, which uses a variant of policy gradient called Monte Carlo Tree
Search guided by a policy network).
Perhaps the most impactful application in recent years is in Natural Language Processing, specifically
RLHF (Reinforcement Learning from Human Feedback). Modern large language models like
ChatGPT, Claude, and Gemini use policy gradient methods (specifically PPO, a policy gradient variant) to
fine-tune their behaviour based on human preference signals. The policy network is the language model,
the reward signal comes from a learned reward model trained on human comparisons, and the objective
is to generate text that humans find helpful, harmless, and honest. This application has fundamentally
transformed the field of NLP and demonstrates the incredible versatility of policy gradient methods
beyond their traditional domains.
Q5
What is RLHF?
Reinforcement Learning from Human Feedback (RLHF) is a technique that uses human
preferences and judgments as the reward signal for training RL agents, particularly large language
models (LLMs). In traditional RL, the reward function is hand-designed by the engineer, which is feasible
for games and robotics but extremely difficult for open-ended language tasks where "good" output is
subjective and nuanced. RLHF solves this problem bylearning a reward model from human
preference data and then using that learned reward model to guide RL optimisation. This approach was
instrumental in the development of ChatGPT and has become the standard method for aligning LLMs
with human values and intentions.
The motivation for RLHF stems from a fundamental challenge: language models trained solely on next-
token prediction (supervised learning on text corpora) can generate fluent but potentially harmful,
biased, or unhelpful text. They lack an understanding of what humans want — they simply predict what
text is statistically likely. RLHF bridges this gap by incorporating direct human feedback into the training
process, effectively teaching the model to generate text that is not only fluent but alsohelpful, honest,
and harmless (the "3H" criteria). This alignment problem — ensuring AI systems behave in accordance
with human intentions — is one of the most important challenges in modern AI safety research.
The RLHF process consists of three distinct stages, each building on the previous one. Understanding
these stages is essential for appreciating both the power and the limitations of the approach.
Step 2: Reward Model Training: Human annotators are shown pairs of responses generated by the
SFT model (or other models) for the same prompt and asked to indicate which response they prefer. This
creates a preference dataset of the form "response A is better than response B." A separate reward
model (typically a smaller language model with a linear head) is then trained on this dataset to predict
which response a human would prefer. The reward model learns to assign higher scores to responses that
align with human preferences, effectively internalising human judgment as a differentiable reward
function.
Step 3: RL Optimisation with PPO: The SFT model from Step 1 is further fine-tuned using Proximal
Policy Optimization (PPO), a policy gradient algorithm, with the reward model from Step 2 providing
the reward signal. At each step, the language model generates a response, the reward model assigns it a
score, and PPO updates the language model’s weights to increase the probability of generating high-
scoring responses. A KL divergence penalty prevents the model from diverging too far from the SFT model,
maintaining linguistic quality while improving alignment.
1
P (y1 > y2 ) = σ (r(y1 ) − r(y2 )) =
1 + e−(r(y1 )−r(y2 ))
The reward model in Step 2 is trained using the Bradley-Terry model, a classical model from preference
learning shown above. Here, r(y₁) and r(y₂) are the reward scores assigned to responses y₁ and y₂, and σ
is the sigmoid function. The reward model is trained to maximise the log-likelihood of the human
preference data — that is, it learns to assign higher rewards to the responses that humans actually
preferred. The sigmoid function maps the reward difference to a probability, and the model is trained so
that P(y₁ > y₂) is high when humans preferred y₁ and low when they preferred y₂. This formulation
naturally handles the inherent noise in human judgments — two humans may disagree on which
response is better, and the model learns to capture the aggregate preference signal.
provides
reward
The RLHF pipeline: supervised fine-tuning → reward model from human preferences → PPO against learned reward
Despite its remarkable success, RLHF faces several significant [Link] hacking (also called
reward gaming or specification gaming) occurs when the language model learns to exploit weaknesses in
the reward model rather than genuinely improving its responses. For example, the model might learn to
write longer responses (which the reward model may correlate with quality) without actually making
them more helpful. This is analogous to the classic RL problem of the agent finding unexpected ways to
achieve high reward that do not align with the true objective.
Human preference noise is another major challenge: human annotators frequently disagree with each
other, their preferences can be inconsistent over time, and cultural or personal biases can influence their
judgments. The reward model trained on this noisy data inherits these imperfections. Additionally, the
cost of human labeling is substantial — training a reward model requires tens of thousands of high-
quality human preference comparisons, each of which takes significant time and expertise. This makes
RLHF expensive and slow to iterate on. Researchers are exploring alternatives like Constitutional AI
(Anthropic), where an AI system critiques and revises its own outputs according to a set of principles,
reducing reliance on human annotators.
RLHF has had a transformative impact on the AI [Link] (OpenAI) was the first widely-
deployed product to use RLHF, demonstrating that language models could be made to follow
instructions, engage in dialogue, and refuse harmful requests in a way that feels natural and helpful.
Claude (Anthropic) uses a variant called Constitutional AI that combines RLHF with [Link]
(Google DeepMind) also incorporates human feedback in its training. Beyond text generation, RLHF
principles are being applied to image generation (DALL-E 3, Midjourney), code generation (GitHub
Copilot), and even video generation models. The success of RLHF has cemented reinforcement learning
as an indispensable tool in the modern AI toolkit, bridging the gap between raw model capability and the
nuanced, value-aligned behaviour that users expect.
Q6
Gaming has been the primary testing ground for RL research, and the achievements in this domain have
been nothing short of extraordinary. AlphaGo, developed by DeepMind, made headlines in 2016 when it
defeated world champion Lee Sedol in the ancient board game Go — a feat previously thought to be
decades away due to the game’s enormous search space (more possible positions than atoms in the
universe). AlphaGo combined deep neural networks with Monte Carlo Tree Search (MCTS) and was
trained initially on human expert games, then refined through self-play (RL where the agent plays against
itself). Its successor, AlphaGo Zero, went even further by learning entirely from self-play with no human
data, achieving superhuman performance in just 40 days.
OpenAI Five tackled the immensely complex team-based game Dota 2, where five AI agents had to
cooperate in real-time against professional human players. The game involves incomplete information
(fog of war), long time horizons (matches last 40-45 minutes), and vast state and action spaces. OpenAI
Five trained using a scaled-up version of PPO with self-play, requiring the equivalent of 45,000 years of
real-time gameplay during training. It defeated the world champion team OG in 2019, demonstrating that
RL could handle complex multi-agent strategic [Link] (DeepMind) achieved Grandmaster
level in StarCraft II, another real-time strategy game requiring strategic planning, resource management,
and rapid tactical decisions under uncertainty.
The significance of these gaming achievements extends far beyond entertainment. They demonstrate
that RL can learn superhuman strategies in complex, partially observable environments with
enormous decision spaces. The techniques developed for games — particularly self-play, policy gradient
methods, and value function approximation — have directly transferred to real-world applications. Games
provide a safe, inexpensive, and easily reproducible environment for developing and testing RL
algorithms before deploying them in high-stakes real-world scenarios.
In robotics, RL is used to train agents to perform complex physical tasks that are difficult to program
explicitly. Robotic arm manipulation is one of the most active areas: RL has been used to train robots
to grasp diverse objects, perform precise insertions (like inserting a USB cable or assembling furniture),
fold cloth, and even perform surgical tasks. Google’s robotics division used RL to train robots to sort
objects by colour, pick up items, and perform bimanual manipulation tasks. The key advantage of RL in
robotics is that it can learn policies that adapt to the physical dynamics of the real world, handling
variability in object positions, shapes, and weights.
Locomotion is another major application. Boston Dynamics’ robots (Atlas, Spot) use RL-inspired
techniques for dynamic balancing, running, jumping, and navigating rough terrain. Quadruped robots like
ANYmal and Unitree’s Go1 use RL to learn walking, trotting, and recovery behaviours that are robust to
perturbations like pushes and uneven surfaces. The Cassie bipedal robot from Agility Robotics learned to
run using deep RL, achieving speeds comparable to human [Link] automation is a rapidly
growing application: Amazon uses hundreds of thousands of robots in its fulfilment centres for picking,
packing, and sorting, with RL increasingly being used to optimise coordination between multiple robots
and improve individual task performance.
A critical challenge in robotic RL is sim-to-real transfer: training policies in simulation (which is fast and
safe) and then deploying them on physical robots (where sensor noise, actuator delays, and model
inaccuracies exist). Techniques like domain randomisation (randomly varying simulation parameters
during training), system identification (learning a more accurate simulation model), and residual RL
(adding an RL policy on top of a traditional controller) have made sim-to-real transfer increasingly
reliable. Sample efficiency remains a challenge — while a game agent can play millions of episodes in
hours, a physical robot can only perform a limited number of trials, making data-efficient RL algorithms
essential for practical robotics.
Autonomous Systems
Self-driving vehicles represent one of the most ambitious applications of RL. Companies like Waymo,
Cruise, and Tesla use RL components for specific driving subtasks: lane-changing decisions, highway
merging, intersection navigation, and adaptive speed control. Waymo’s autonomous taxi service uses a
combination of RL and traditional planning algorithms to handle the enormous variability of real-world
driving. RL is particularly valuable for learning interactive driving behaviours — anticipating and
responding to the actions of other drivers, pedestrians, and cyclists in a way that is difficult to capture
with hand-coded rules.
Autonomous drones use RL for aerial navigation, obstacle avoidance, and payload delivery in complex
environments. The US military and companies like Zipline and Wing (Alphabet) use RL-trained controllers
for autonomous package delivery. Traffic light control is a lower-profile but highly impactful application:
RL systems have been deployed to optimise traffic signal timing in real-time across entire city networks,
reducing congestion by 20-40% compared to fixed-timing or traditional adaptive systems. Data centre
resource management is another major application: Google used RL to optimise cooling in its data
centres, reducing energy consumption by 40% and saving billions in energy costs. The RL system learned
to control hundreds of actuators (fans, cooling units, windows) to maintain optimal temperature while
minimising energy use.
Beyond the three primary domains, RL is being applied to an increasingly diverse range of
[Link] systems at YouTube, TikTok, and Netflix use RL to optimise long-term user
engagement rather than just click-through rate on individual items. The policy learns to sequence
recommendations that keep users engaged over entire [Link] discovery companies like Insilico
Medicine use RL to design novel molecular structures with desired pharmacological properties, treating
molecule generation as a sequential decision-making problem. Finance applications include algorithmic
trading, portfolio optimisation, and dynamic pricing, where RL agents learn to make sequential
investment decisions that maximise risk-adjusted returns. Supply chain optimisation uses RL for
inventory management, logistics routing, and demand forecasting, adapting to changing conditions in
real-time.
Long-term engagement
RL for session-level
Recommendation YouTube / TikTok optimisation; billions of users
optimisation
served daily
Q1
A Deep Learning pipeline for healthcare diagnosis is an end-to-end system that transforms raw
medical data — such as X-rays, MRI scans, CT images, or electronic health records — into actionable
clinical predictions. Unlike a simple model training script, a production pipeline encompasses data
ingestion, preprocessing, model selection, training, evaluation, deployment, and continuous monitoring.
Each stage must be carefully designed to meet the stringent reliability, safety, and regulatory
requirements of the healthcare domain. The pipeline is typically implemented as a series of modular,
containerised microservices that can be independently scaled and updated, ensuring high availability
and fault tolerance in clinical environments where downtime can directly impact patient outcomes.
The pipeline begins with data collection from multiple sources: hospital PACS (Picture Archiving and
Communication Systems), wearable devices, lab results, and clinical notes. Medical imaging data arrives
in standard formats like DICOM, which must be parsed, de-identified, and converted into tensor-friendly
formats. Structured EHR data requires feature extraction and encoding. Once collected, data enters the
preprocessing stage, where images are resized to a uniform resolution (e.g., 224×224 for CNNs),
normalised to zero mean and unit variance, and augmented with techniques like random rotation,
flipping, and intensity jittering to artificially expand the training set. For tabular EHR data, preprocessing
involves imputation of missing values, one-hot encoding of categorical features, and temporal alignment
of longitudinal records.
DICOM, EHR, Labs resize, normalise, augment ResNet, EfficientNet, ViT cross-val, hyperparameter tuning Docker, Kubernetes, API
X-ray MRI Resize Normalise CNN ViT K-Fold SMOTE REST API Logging
Differential privacy, encryption Oversampling + cost-sensitive learning Explainable AI (XAI) for trust
For medical image diagnosis, the model selection stage typically evaluates several architectures.
ResNet-50 and EfficientNet-B4 are popular choices due to their proven performance on ImageNet and
strong transfer learning capabilities. For tasks requiring fine-grained spatial understanding, such as
segmenting tumour boundaries in MRI scans, a U-Net architecture with skip connections is preferred.
More recently, Vision Transformers (ViT) have shown state-of-the-art results on medical imaging
benchmarks by capturing long-range dependencies through self-attention, though they require larger
datasets to avoid overfitting. The selected model is trained using stratified k-fold cross-validation to
ensure robust performance estimates across different patient subgroups. Early stopping with patience
monitors validation loss to prevent overfitting, and learning rate schedulers (cosine annealing or
ReduceLROnPlateau) help converge to better minima.
The deployment stage packages the trained model into a REST API served via a framework like FastAPI
or Flask, containerised with Docker for environment reproducibility, and orchestrated with Kubernetes for
horizontal scaling. In a clinical setting, the model is typically deployed as asecond-reader system that
assists radiologists rather than replacing them. Real-time inference latency must be kept under 500
milliseconds for seamless integration into radiology workflows. Continuous monitoring tracks prediction
drift, data distribution shifts, and model performance metrics (sensitivity, specificity, AUC-ROC) over
time. A feedback loop allows clinicians to flag incorrect predictions, which are collected for periodic
model retraining and improvement. Regulatory compliance with FDA 510(k) or CE marking requires
extensive documentation of the pipeline, including data provenance, model architecture decisions, and
validation results across diverse demographic groups.
Three challenges dominate healthcare AI pipelines. Data privacy is non-negotiable: patient data is
protected by HIPAA (US), GDPR (EU), and similar regulations worldwide. Solutions include federated
learning (training across hospitals without sharing raw data), differential privacy (adding calibrated noise
to gradients), and secure multi-party computation. Class imbalanceis pervasive because healthy
patient scans vastly outnumber diseased ones, and rare conditions may have only dozens of examples.
Techniques like focal loss (which down-weights easy examples), SMOTE (synthetic minority
oversampling), and class-weighted loss functions are essential. Finally, interpretability is a clinical
requirement — a doctor cannot act on a black-box prediction. Grad-CAM heatmaps highlight image
regions that influenced the prediction, SHAP values quantify feature contributions for tabular data, and
attention rollout visualisations reveal which parts of an image a ViT focused on. Together, these
techniques build the trust needed for clinical adoption.
Q2
Plant disease detection is a critical agricultural application of computer vision. Farmers lose an
estimated30–40% of crop yields annually due to undetected diseases, and early identification can
prevent widespread infestation. A CNN-based system can analyse leaf images captured by a smartphone
and classify them into healthy or diseased categories, potentially identifying the specific pathogen. The
PlantVillage dataset is a widely used benchmark containing over 54,000 images of healthy and
diseased leaves across 38 classes, including crops like tomato, potato, apple, and rice. Each image is
captured under controlled conditions with uniform lighting, making it suitable for training deep learning
models. In practice, data augmentation is essential to simulate real-world variability in lighting, angle,
and background.
The architecture must balance accuracy withcomputational efficiency, since the model may need to
run on edge devices (mobile phones, Raspberry Pi) in the field. A moderate-depth CNN with
BatchNormalisation for training stability and Dropout for regularisation provides a strong baseline. The
use ofdata augmentation — random rotations, flips, zoom, and brightness adjustments — dramatically
improves generalisation by exposing the model to a wider variety of leaf appearances during training.
This is especially important because plant leaves in the wild appear at various orientations, scales, and
lighting conditions that differ from the controlled PlantVillage captures.
plant_disease_cnn.py
1 # ============================================================
2 # Plant Disease Detection CNN using TensorFlow/Keras
3 # Dataset: PlantVillage (54,000+ leaf images, 38 classes)
4 # ============================================================
5
6 import tensorflow as tf
7 from [Link] import layers, models, callbacks
8 from [Link] import ImageDataGenerator
9 import numpy as np
10 import os
11
12 # ── Step 1: Data Augmentation & Loading ──────────────────
13 # Data augmentation artificially expands the training set by
14 # applying random transformations, improving generalisation.
15 train_datagen = ImageDataGenerator(
16 rescale=1.0 / 255.0,
17 rotation_range=40, # Random rotation up to 40 degrees
18 width_shift_range=0.2, # Horizontal shift up to 20%
19 height_shift_range=0.2, # Vertical shift up to 20%
20 shear_range=0.2, # Shear transformations
21 zoom_range=0.2, # Random zoom up to 20%
22 horizontal_flip=True, # Random horizontal flip
23 fill_mode='nearest', # Fill pixels after transformation
24 validation_split=0.2, # 20% for validation
25 )
26
27 # Only rescale for validation — no augmentation
28 val_datagen = ImageDataGenerator(
29 rescale=1.0 / 255.0,
30 validation_split=0.2,
31 )
32
33 IMG_SIZE = (224, 224)
34 BATCH_SIZE = 32
35 DATA_DIR = './dataset/plantvillage'
36
37 train_generator = train_datagen.flow_from_directory(
38 DATA_DIR,
39 target_size=IMG_SIZE,
40 batch_size=BATCH_SIZE,
41 class_mode='categorical',
42 subset='training',
43 shuffle=True,
44 )
45
46 val_generator = val_datagen.flow_from_directory(
47 DATA_DIR,
48 target_size=IMG_SIZE,
49 batch_size=BATCH_SIZE,
50 class_mode='categorical',
51 subset='validation',
52 shuffle=False,
53 )
54
55 NUM_CLASSES = len(train_generator.class_indices)
56 print(f"Number of classes: {NUM_CLASSES}")
57 print(f"Class indices: {train_generator.class_indices}")
58
59 # ── Step 2: Build the CNN Architecture ──────────────────
60 # Architecture: 4 Conv blocks with BatchNorm -> GlobalAvgPool -> Dense
61 model = [Link]([
62 # First Convolutional Block
63 layers.Conv2D(32, (3, 3), activation='relu', padding='same',
64 input_shape=(224, 224, 3)),
65 [Link](),
66 layers.Conv2D(32, (3, 3), activation='relu', padding='same'),
67 [Link](),
68 layers.MaxPooling2D((2, 2)), # 224 -> 112
69 [Link](0.25),
70
71 # Second Convolutional Block
72 layers.Conv2D(64, (3, 3), activation='relu', padding='same'),
73 [Link](),
74 layers.Conv2D(64, (3, 3), activation='relu', padding='same'),
75 [Link](),
76 layers.MaxPooling2D((2, 2)), # 112 -> 56
77 [Link](0.25),
78
79 # Third Convolutional Block
80 layers.Conv2D(128, (3, 3), activation='relu', padding='same'),
81 [Link](),
82 layers.Conv2D(128, (3, 3), activation='relu', padding='same'),
83 [Link](),
84 layers.MaxPooling2D((2, 2)), # 56 -> 28
85 [Link](0.3),
86
87 # Fourth Convolutional Block
88 layers.Conv2D(256, (3, 3), activation='relu', padding='same'),
89 [Link](),
90 layers.MaxPooling2D((2, 2)), # 28 -> 14
91 [Link](0.3),
92
93 # Classification Head
94 layers.GlobalAveragePooling2D(), # (14, 14, 256) -> 256
95 [Link](512, activation='relu'),
96 [Link](),
97 [Link](0.5),
98 [Link](NUM_CLASSES, activation='softmax'),
99 ])
100
101 # ── Step 3: Compile with Class Weights ──────────────────
102 # Compute class weights to handle imbalance (some diseases
103 # have far fewer images than others)
104 from [Link].class_weight import compute_class_weight
105
106 labels = train_generator.classes
107 class_weights = compute_class_weight(
108 'balanced',
109 classes=[Link](labels),
110 y=labels,
111 )
112 class_weight_dict = dict(enumerate(class_weights))
113
114 [Link](
115 optimizer=[Link](learning_rate=1e-3),
116 loss='categorical_crossentropy',
117 metrics=['accuracy'],
118 )
119
120 [Link]()
121
122 # ── Step 4: Train with Callbacks ────────────────────────
123 early_stop = [Link](
124 monitor='val_loss', patience=5, restore_best_weights=True
125 )
126 reduce_lr = [Link](
127 monitor='val_loss', factor=0.5, patience=3, min_lr=1e-6
128 )
129
130 history = [Link](
131 train_generator,
132 epochs=30,
133 validation_data=val_generator,
134 class_weight=class_weight_dict,
135 callbacks=[early_stop, reduce_lr],
136 verbose=1,
137 )
138
139 # ── Step 5: Evaluate and Save ───────────────────────────
140 loss, acc = [Link](val_generator, verbose=2)
141 print(f"Validation Accuracy: {acc * 100:.2f}%")
142 [Link]('plant_disease_model.h5')
Architecture Rationale
The CNN follows a progressive architecture where the number of filters doubles at each block (32 → 64 →
128 → 256), enabling the network to learn increasingly abstract features. Early layers capture simple
patterns like edges and colour gradients, while deeper layers learn complex textures and disease-specific
lesion patterns. BatchNormalisation is applied after every convolution to stabilise training by
normalising activations to zero mean and unit variance, which allows higher learning rates and faster
convergence. GlobalAveragePooling2D replaces the traditional Flatten + Dense approach, reducing
parameters dramatically by averaging each feature map into a single value. This acts as a structural
regulariser and reduces overfitting. Dropoutis applied after each pooling layer (0.25–0.3) and before the
final dense layer (0.5) to prevent co-adaptation of neurons.
Class-weighted training addresses the inherent imbalance in the PlantVillage dataset, where some
diseases have thousands of images while others have only a few hundred. Thecompute_class_weight
function from scikit-learn inversely scales the loss based on class frequency, ensuring that rare diseases
receive proportionally more attention during training. Combined with data augmentation, this produces a
robust model that generalises well to unseen leaf images. The learning rate is dynamically reduced via
ReduceLROnPlateau, which halves the learning rate when validation loss stalls for 3 epochs, allowing
finer-grained optimisation in later training stages. Early stopping prevents overfitting by monitoring
validation loss and restoring the best weights after 5 epochs of no improvement.
Q3
Sentiment analysis is the task of classifying text as positive, negative, or neutral based on the
expressed opinion. It is one of the most widely deployed NLP applications, powering product review
analysis, social media monitoring, customer feedback systems, and financial market sentiment tracking.
AnLSTM (Long Short-Term Memory) network is particularly well-suited for this task because it can
capture long-range dependencies in text — for example, the word "not" early in a sentence can invert
the sentiment of words that appear much later. Unlike a standard RNN, which suffers from vanishing
gradients over long sequences, the LSTM’s gated architecture (input, forget, and output gates)
selectively retains or discards information at each time step, enabling it to model the nuanced structure
of natural language.
The pipeline begins with text preprocessing: raw review text is tokenised into integer sequences using
a vocabulary built from the training data. Each token is mapped to its frequency-ranked index, and
sequences are padded or truncated to a fixed length (e.g., 200 tokens) to create uniform tensor inputs.
An Embedding layer then converts these integer tokens into dense vector representations (typically
128 or 256 dimensions), which capture semantic relationships between words. These embeddings are
either learned from scratch during training or initialised with pre-trained word vectors like GloVe or
Word2Vec. The embedded sequences are fed into one or more LSTM layers, whose final hidden states are
passed to a dense classification head that outputs sentiment probabilities via softmax.
sentiment_lstm.py
1 # ============================================================
2 # LSTM Sentiment Analysis for Movie Reviews (IMDB Dataset)
3 # Using TensorFlow/Keras Sequential API
4 # ============================================================
5
6 import numpy as np
7 from [Link] import layers, models, callbacks
8 from [Link] import imdb
9 from [Link] import pad_sequences
10
11 # ── Step 1: Load and Preprocess the IMDB Dataset ─────────
12 # IMDB: 50,000 movie reviews (25K train, 25K test)
13 # Labels: 1 = positive, 0 = negative
14 VOCAB_SIZE = 20000 # Use top 20K most frequent words
15 MAX_LEN = 200 # Pad/truncate reviews to 200 tokens
16 EMBEDDING_DIM = 128 # Dimension of word embeddings
17
18 (x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=VOCAB_SIZE)
19
20 # Pad sequences to uniform length
21 # padding='post' adds zeros after the sequence
22 # truncating='post' cuts tokens beyond MAX_LEN from the end
23 x_train = pad_sequences(x_train, maxlen=MAX_LEN, padding='post', truncating='post')
24 x_test = pad_sequences(x_test, maxlen=MAX_LEN, padding='post', truncating='post')
25
26 print(f"Training samples: {x_train.shape[0]}") # 25000
27 print(f"Test samples: {x_test.shape[0]}") # 25000
28 print(f"Sequence length: {x_train.shape[1]}") # 200
29
30 # ── Step 2: Build the LSTM Model ────────────────────────
31 # Architecture: Embedding -> LSTM -> LSTM -> Dense -> Dense
32 model = [Link]([
33 # Embedding layer: maps integer tokens to 128-dim vectors
34 # input_length is deprecated in newer Keras; shape inferred
35 [Link](
36 input_dim=VOCAB_SIZE,
37 output_dim=EMBEDDING_DIM,
38 input_length=MAX_LEN,
39 mask_zero=True, # Ignore padding tokens in LSTM
40 ),
41
42 # First LSTM layer: 64 units, returns full sequence
43 # return_sequences=True passes output to the next LSTM
44 [Link](64, return_sequences=True, dropout=0.3, recurrent_dropout=0.2),
45
46 # Second LSTM layer: 32 units, returns only final state
47 [Link](32, dropout=0.3, recurrent_dropout=0.2),
48
49 # Classification head
50 [Link](64, activation='relu'),
51 [Link](0.5),
52 [Link](1, activation='sigmoid'), # Binary classification
53 ])
54
55 # ── Step 3: Compile the Model ───────────────────────────
56 # Binary crossentropy for 0/1 sentiment classification
57 [Link](
58 optimizer='adam',
59 loss='binary_crossentropy',
60 metrics=['accuracy'],
61 )
62
63 [Link]()
64
65 # ── Step 4: Train with Callbacks ────────────────────────
66 early_stop = [Link](
67 monitor='val_loss', patience=3, restore_best_weights=True
68 )
69 reduce_lr = [Link](
70 monitor='val_loss', factor=0.5, patience=2, min_lr=1e-5
71 )
72
73 history = [Link](
74 x_train, y_train,
75 epochs=15,
76 batch_size=128,
77 validation_split=0.2,
78 callbacks=[early_stop, reduce_lr],
79 verbose=1,
80 )
81
82 # ── Step 5: Evaluate and Predict ────────────────────────
83 loss, acc = [Link](x_test, y_test, verbose=2)
84 print(f"Test Accuracy: {acc * 100:.2f}%")
85
86 # Example prediction on a custom review
87 def predict_sentiment(text, word_index):
88 """Predict sentiment of a custom text input."""
89 # Reverse the word index (index -> word)
90 reverse_word_index = {v: k for k, v in word_index.items()}
91
92 # Tokenise: simple whitespace split + lowercasing
93 words = [Link]().split()
94 tokens = [word_index.get(w, 2) for w in words] # 2 = unknown token
95 tokens = [t if t < VOCAB_SIZE else 2 for t in tokens]
96 padded = pad_sequences([tokens], maxlen=MAX_LEN, padding='post')
97
98 pred = [Link](padded, verbose=0)[0][0]
99 sentiment = "POSITIVE" if pred >= 0.5 else "NEGATIVE"
100 print(f"Review: {text}")
101 print(f"Prediction: {sentiment} (confidence: {pred:.4f})")
102 return sentiment, pred
103
104 # Test with sample reviews
105 word_index = imdb.get_word_index()
106 predict_sentiment("This movie was absolutely fantastic and deeply moving", word_index)
107 predict_sentiment("Terrible film, waste of time, boring plot", word_index)
The model uses a two-layer stacked LSTM architecture. The first LSTM layer has
return_sequences=True, meaning it outputs the full sequence of hidden states rather than just the final
state. This allows the second LSTM to process the temporal output of the first, creating a deeper
temporal representation. The second LSTM returns only its final hidden state, which is a fixed-size vector
that summarises the entire review. Both LSTMs use dropout (0.3) on input connections and recurrent
dropout (0.2) on recurrent connections to prevent overfitting on the training reviews. The
mask_zero=True parameter in the Embedding layer ensures that padding tokens do not influence the
LSTM’s hidden state computation, which is critical for maintaining accurate temporal dynamics.
The Embedding layer is the bridge between discrete tokens and continuous vector space. With
VOCAB_SIZE=20000 and EMBEDDING_DIM=128, it creates a 20,000×128 learnable weight matrix. During
training, the network learns to position semantically similar words close together in this 128-dimensional
space. For production systems, this layer can be initialised with pre-trained GloVe embeddings (trained
on billions of words from Wikipedia and Common Crawl), which provide rich semantic knowledge even
before training begins. The binary crossentropy loss function is used because this is a binary
classification task (positive vs. negative), and the sigmoid output neuron produces a probability between
0 and 1. Adam optimiser with default parameters provides adaptive learning rates for each parameter,
making it robust to the different scales of gradients across the embedding, recurrent, and dense layers.
In practice, this LSTM model typically achieves 85–88% test accuracy on the IMDB dataset.
Performance can be further improved by using pre-trained embeddings, bidirectional LSTMs (which
process the sequence in both forward and backward directions), or by replacing the LSTM with
aTransformer encoder for better capture of long-range dependencies. However, the LSTM remains a
strong, interpretable baseline that trains quickly and is well-understood, making it an excellent starting
point for sentiment analysis projects.
Q4
Transfer learning is a technique where a model trained on a large dataset (source task) is repurposed
for a different but related task (target task). In computer vision, models pre-trained on ImageNet (1.2
million images, 1000 classes) have learned rich hierarchical feature representations — early layers detect
edges and textures, middle layers detect parts and patterns, and deep layers detect objects and scenes.
These features are highly transferable because the low-level visual patterns (edges, corners, colour
gradients) are universal across image domains. By reusing these pre-trained features, we can achieve
high accuracy on a target task with far less data and training timethan training from scratch. A model
that takes weeks to train on ImageNet can be fine-tuned on a custom dataset in minutes to hours. This
makes transfer learning the default approach for virtually all practical image classification projects.
Two main strategies exist: feature extraction (freezing the entire pre-trained base and training only a
new classification head) and fine-tuning(unfreezing some or all of the pre-trained layers and training
them with a very low learning rate alongside the new head). Feature extraction is ideal when the target
dataset is very small (hundreds of images) and similar to ImageNet. Fine-tuning is preferred when the
dataset is larger (thousands of images) or when the target domain differs significantly from ImageNet
(e.g., medical imaging, satellite imagery, industrial defect detection).ResNet-50 is the most popular
choice due to its residual connections that enable very deep networks (50 layers) to train effectively,
while VGG-16offers a simpler, more uniform architecture that is easy to understand and modify.
transfer_learning.py
1 # ============================================================
2 # Transfer Learning with ResNet50 for Custom Image Classification
3 # Strategy: Freeze base -> Train head -> Fine-tune top layers
4 # ============================================================
5
6 import tensorflow as tf
7 from [Link] import layers, models, callbacks
8 from [Link] import ResNet50
9 from [Link].resnet50 import preprocess_input
10 from [Link] import ImageDataGenerator
11 import numpy as np
12
13 # ── Step 1: Configuration ───────────────────────────────
14 IMG_SIZE = (224, 224)
15 BATCH_SIZE = 32
16 NUM_CLASSES = 5 # Custom dataset: 5 categories
17 DATA_DIR = './dataset/custom_images'
18 FINE_TUNE_AT = 140 # Unfreeze layers from block14 onwards
19
20 # ── Step 2: Data Loading with ResNet Preprocessing ──────
21 # ResNet50 expects images preprocessed with preprocess_input()
22 # which scales pixels to [-1, 1] range (not [0, 1])
23 train_datagen = ImageDataGenerator(
24 preprocessing_function=preprocess_input,
25 rotation_range=30,
26 width_shift_range=0.2,
27 height_shift_range=0.2,
28 shear_range=0.2,
29 zoom_range=0.2,
30 horizontal_flip=True,
31 validation_split=0.2,
32 )
33
34 val_datagen = ImageDataGenerator(
35 preprocessing_function=preprocess_input,
36 validation_split=0.2,
37 )
38
39 train_gen = train_datagen.flow_from_directory(
40 DATA_DIR, target_size=IMG_SIZE, batch_size=BATCH_SIZE,
41 class_mode='categorical', subset='training', shuffle=True,
42 )
43 val_gen = val_datagen.flow_from_directory(
44 DATA_DIR, target_size=IMG_SIZE, batch_size=BATCH_SIZE,
45 class_mode='categorical', subset='validation', shuffle=False,
46 )
47
48 # ── Step 3: Load Pre-trained ResNet50 (Feature Extractor) ─
49 # include_top=False removes the original 1000-class ImageNet head
50 # weights='imagenet' loads pre-trained parameters
51 base_model = ResNet50(
52 weights='imagenet',
53 include_top=False,
54 input_shape=(224, 224, 3),
55 )
56
57 # Freeze ALL base model layers initially (feature extraction phase)
58 base_model.trainable = False
59 print(f"Base model layers: {len(base_model.layers)}") # 175 layers
60 print(f"Trainable parameters: {sum(1 for l in base_model.layers if l.trainable_weights)}")
61
62 # ── Step 4: Build Custom Classification Head ────────────
63 inputs = [Link](shape=(224, 224, 3))
64
65 # Pass through frozen base model
66 x = base_model(inputs, training=False) # training=False keeps BN in inference mode
67
68 # Global Average Pooling: (7, 7, 2048) -> (2048,)
69 x = layers.GlobalAveragePooling2D()(x)
70
71 # Custom head with regularisation
72 x = [Link](256, activation='relu')(x)
73 x = [Link]()(x)
74 x = [Link](0.5)(x)
75
76 # Output layer
77 outputs = [Link](NUM_CLASSES, activation='softmax')(x)
78
79 model = [Link](inputs, outputs)
80
81 # ── Step 5: Phase 1 — Train Only the Head ───────────────
82 # Use a higher learning rate for the new head layers
83 [Link](
84 optimizer=[Link](learning_rate=1e-3),
85 loss='categorical_crossentropy',
86 metrics=['accuracy'],
87 )
88
89 print("
90 --- Phase 1: Training classification head (base frozen) ---")
91 history_phase1 = [Link](
92 train_gen,
93 epochs=10,
94 validation_data=val_gen,
95 callbacks=[
96 [Link](monitor='val_loss', patience=3,
97 restore_best_weights=True),
98 ],
99 verbose=1,
100 )
101
102 # ── Step 6: Phase 2 — Fine-Tune Top Layers ──────────────
103 # Unfreeze layers from FINE_TUNE_AT onwards
104 base_model.trainable = True
105 for layer in base_model.layers[:FINE_TUNE_AT]:
106 [Link] = False
107
108 # Recompile with a MUCH lower learning rate for fine-tuning
109 # Using 1e-5 (100x lower) to avoid destroying pre-trained weights
110 [Link](
111 optimizer=[Link](learning_rate=1e-5),
112 loss='categorical_crossentropy',
113 metrics=['accuracy'],
114 )
115
116 print(f"
117 --- Phase 2: Fine-tuning from layer {FINE_TUNE_AT} ---")
118 print(f"Trainable parameters now: {sum(1 for l in base_model.layers if l.trainable_weights)}")
119
120 history_phase2 = [Link](
121 train_gen,
122 epochs=10,
123 validation_data=val_gen,
124 callbacks=[
125 [Link](monitor='val_loss', patience=3,
126 restore_best_weights=True),
127 [Link](monitor='val_loss', factor=0.5,
128 patience=2, min_lr=1e-7),
129 ],
130 verbose=1,
131 )
132
133 # ── Step 7: Final Evaluation ────────────────────────────
134 loss, acc = [Link](val_gen, verbose=2)
135 print(f"
136 Final Validation Accuracy: {acc * 100:.2f}%")
137 [Link]('transfer_learning_resnet50.h5')
The implementation follows a two-phase training strategy that is the gold standard for transfer
learning. In Phase 1 (feature extraction), the entire ResNet-50 base is frozen (all weights are non-
trainable), and only the custom classification head (GlobalAveragePooling → Dense → BatchNorm →
Dropout → Output) is trained with a standard learning rate of 1e-3. This phase allows the randomly
initialised head weights to converge to a reasonable solution without disrupting the pre-trained features.
The training=False argument when calling the base model ensures that BatchNormalisation layers in
ResNet-50 use their running statistics (mean and variance) rather than batch statistics, which is critical
for stable training when the base is frozen. Typically 5–10 epochs are sufficient for the head to learn a
good decision boundary on top of the frozen features.
In Phase 2 (fine-tuning), the top layers of the base model (from layer 140 onwards, which corresponds
roughly to the last two residual blocks of ResNet-50) are unfrozen and trained alongside the head with a
very low learning rate of 1e-5 — roughly 100 times smaller than Phase 1. This low rate is essential
because the pre-trained weights are already close to optimal for generic feature extraction; a large
learning rate would catastrophically destroy these representations. The lower layers remain frozen
because they encode universal features (edges, textures) that transfer well across domains, while the
higher layers encode more task-specific features that benefit from adaptation. This selective unfreezing
strategy balances the trade-off between retaining useful pre-trained knowledge and adapting to the
target domain. The result is a model that achieves high accuracy with minimal data and training time
compared to training from scratch.
Q5
A production-grade customer support chatbot must handle multi-turn conversations, understand user
intent across diverse phrasing, retrieve relevant knowledge from a company’s knowledge base, and
generate natural, helpful responses. Modern chatbot architectures combineTransformer-based NLP
models with retrieval-augmented generation (RAG) to achieve these goals. The system can be
decomposed into three core components: Intent Recognition (classifying what the user
wants),Response Generation (producing a natural language reply), andContext Management
(maintaining conversation history and user state). These components work together in a pipeline where
each user message is processed sequentially through intent recognition, knowledge retrieval (if needed),
context update, and finally response generation.
The intent recognition module uses a fine-tuned Transformer encoder (such as BERT or DistilBERT) to
classify incoming messages into predefined intent categories like "refund_request", "order_status",
"technical_support", or "greeting". This classification determines which branch of the system handles the
query. For intents that require factual knowledge (e.g., "What is your return policy?"), the system routes
to a RAG pipeline that retrieves relevant documents from a vector database and generates a grounded
response. For simple intents like greeting or FAQ-style queries, pre-written templates may suffice. The
response generation module uses a decoder-only Transformer (like GPT-2 or LLaMA) fine-tuned on
customer support conversations, potentially augmented with retrieved context via RAG. The context
management module maintains a sliding window of recent conversation turns and user metadata
(account type, order history) to ensure coherent multi-turn interactions.
raw text
classify intent route + track Template Engine
The proposed system leverages Hugging Face Transformers as the core NLP library, providing access
to pre-trained models like BERT for intent classification, GPT-2 or LLaMA for response generation, and
sentence-transformers for embedding queries and documents. LangChainprovides the orchestration
framework for the RAG pipeline, managing the flow from query embedding through vector search to
prompt construction and LLM invocation. LangChain’s built-in conversation memory modules
(BufferWindowMemory, ConversationSummaryMemory) handle context management by maintaining a
sliding window of recent turns or generating summaries of older turns. For the vector database, FAISS
(Facebook AI Similarity Search) is the best choice for self-hosted deployments due to its speed and
simplicity, while Pinecone or Weaviateoffer managed, scalable alternatives for enterprise deployments.
The API layer is built withFastAPI for high-performance async request handling, withRedis for session
caching and rate limiting.
The context management subsystem is critical for multi-turn coherence. It maintains four key data
structures: a conversation history buffer(last N turns of the dialogue), a user profile/state (account
details, previous issues, preferences), slot filling data (extracted entities like order numbers, dates,
product names), and session management (timeout handling, session persistence across page
reloads). When a new message arrives, the context manager assembles a prompt that includes the
conversation history, retrieved RAG context (if applicable), and the current user message, which is then
passed to the LLM for response generation. This ensures that the chatbot can refer back to earlier
messages, maintain consistent persona, and provide contextually relevant answers throughout a multi-
turn conversation.
Q6
Explain Docker containerisation and RAG pipelines for AI model
deployment in production.
Docker has become the industry standard for packaging and deploying AI models in production. A
Docker container is a lightweight, standalone, executable package that includes everything needed to
run a piece of software — the code, runtime, system tools, libraries, and settings. For AI/ML workloads,
Docker solves the "it works on my machine" problem by ensuring that the exact same environment
(Python version, CUDA toolkit, TensorFlow/PyTorch versions, system libraries) is replicated identically
across development, testing, and production environments. This reproducibility is particularly critical for
deep learning, where subtle version differences in CUDA, cuDNN, or even NumPy can cause models to
produce different results or fail to run entirely. Docker containers are also significantly lighter than virtual
machines — they share the host OS kernel and start in seconds rather than minutes, making them ideal
for the rapid iteration cycles of ML model development and deployment.
The benefits of Docker for AI deployment extend beyond reproducibility. Isolationensures that multiple
models with conflicting dependencies (e.g., one requiring TensorFlow 1.x and another requiring PyTorch
2.x) can run on the same host without interference. Scalabilityis achieved through orchestration with
Kubernetes or Docker Swarm, where containers can be horizontally scaled based on request load.
Versioning is built-in — each container image is immutable and tagged, enabling rollbacks to previous
model versions if a new deployment exhibits degraded performance. CI/CD integration allows
automated testing and deployment pipelines: a model trained in a notebook can be packaged into a
Docker image, tested against a validation dataset, and deployed to production with a single command.
This tight integration between training and deployment pipelines dramatically reduces the time-to-
production for new and updated models.
Dockerfile
1 # ============================================================
2 # Dockerfile for AI Model Serving (FastAPI + TensorFlow GPU)
3 # ============================================================
4
5 # Base image with CUDA support for GPU inference
6 FROM nvidia/cuda:11.8.0-runtime-ubuntu22.04
7
8 # Set environment variables
9 ENV PYTHON_VERSION=3.10 DEBIAN_FRONTEND=noninteractive
10
11 # Install Python and system dependencies
12 RUN apt-get update && apt-get install -y \
13 python3.10 \
14 python3-pip \
15 python3.10-venv \
16 libgl1-mesa-glx \
17 libglib2.0-0 \
18 && rm -rf /var/lib/apt/lists/*
19
20 # Set working directory
21 WORKDIR /app
22
23 # Copy requirements and install Python packages
24 COPY [Link] .
25 RUN pip3 install --no-cache-dir -r [Link]
26
27 # Copy application code and model
28 COPY app/ ./app/
29 COPY models/ ./models/
30
31 # Expose the API port
32 EXPOSE 8000
33
34 # Health check endpoint
35 HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
36 CMD curl -f [Link] || exit 1
37
38 # Run the FastAPI server with Uvicorn
39 CMD ["uvicorn", "[Link]:app", "--host", "[Link]", "--port", "8000"]
Vector Database
User Query Embedding Model
(FAISS / Pinecone /
"How to deploy?" (sentence-BERT)
ChromaDB / Weaviate)
The choice of vector database is a critical infrastructure decision for any RAG deployment. A vector
database stores high-dimensional embeddings (typically 384 to 1536 dimensions) and supports efficient
approximate nearest neighbour (ANN) search using algorithms like HNSW (Hierarchical Navigable Small
World) or IVF (Inverted File Index). FAISS (Facebook AI Similarity Search) is an open-source library that
runs entirely in-memory, making it extremely fast for datasets up to a few million vectors, but it lacks
built-in persistence, access control, and [Link] is a fully managed cloud service that
handles scaling, replication, and metadata filtering automatically, ideal for teams that want to avoid
operational [Link] is an open-source embeddable database designed for AI applications
with built-in embedding support and a simple API, making it great for [Link] offers a
hybrid search capability that combines vector similarity with keyword-based BM25 search, providing
more accurate retrieval for queries that contain specific technical terms.
NVIDIA drivers and CUDA toolkit must NVIDIA Container Toolkit provides
GPU Support be manually installed and version- seamless GPU passthrough to
matched containers
In a production AI deployment, Docker and RAG work synergistically. The entire RAG pipeline —
embedding model, vector database client, LLM API client, and FastAPI server — is packaged into a Docker
container. This container can be deployed to any environment that supports Docker, from a single GPU
workstation for development to a Kubernetes cluster with auto-scaling for production. The Dockerfile
shown above uses an NVIDIA CUDA base image to ensure GPU acceleration for embedding models that
run locally. A [Link] file typically defines multiple services: the RAG API server, a vector
database instance (e.g., ChromaDB or Qdrant), a Redis cache for session management, and an Nginx
reverse proxy for load balancing and SSL termination. This modular architecture allows each component
to be independently updated, scaled, and monitored, while Docker guarantees that the entire system
runs identically in development, staging, and production environments.