Pranav's Blog

Day 1 of 100 days of building

Neural Network Phase

Project

Building a Neural Network from Scratch

Today I worked through Andrew Trask's "A Neural Network in 11 Lines of Python".

Goal: Understand the intuition behind forward propagation and backpropagation rather than simply copying the implementation.


The Code

import numpy as np

X = np.array([
    [0,0,1],
    [0,1,1],
    [1,0,1],
    [1,1,1]
])

y = np.array([[0,1,1,0]]).T

w1 = 2*np.random.random((3,4)) - 1
w2 = 2*np.random.random((4,1)) - 1

for j in range(6000):

    l1 = 1/(1+np.exp(-(np.dot(X,w1))))
    l2 = 1/(1+np.exp(-(np.dot(l1,w2))))

    l2_delta = (y-l2)*(l2*(1-l2))
    l1_delta = l2_delta.dot(w2.T)*(l1*(1-l1))

    w2 += l1.T.dot(l2_delta)
    w1 += X.T.dot(l1_delta)

Variables

Variable Meaning
X Input data
y Ground truth labels
w1 Weights between Input Layer and Hidden Layer
w2 Weights between Hidden Layer and Output Layer
l1 Hidden layer activations
l2 Output prediction

Network Architecture

Input (X)
     β”‚
     β–Ό
Linear Layer (X @ W1)
     β”‚
     β–Ό
Sigmoid
     β”‚
     β–Ό
Hidden Activations (L1)
     β”‚
     β–Ό
Linear Layer (L1 @ W2)
     β”‚
     β–Ό
Sigmoid
     β”‚
     β–Ό
Prediction (L2)

There are two linear layers in this network.

X @ W1
L1 @ W2

Every np.dot() operation is a linear transformation.


Tensor Shapes

X          (4,3)

W1         (3,4)

↓

L1         (4,4)

W2         (4,1)

↓

L2         (4,1)

Y          (4,1)

L2_delta   (4,1)

L1_delta   (4,4)

Understanding tensor shapes is one of the most important debugging skills in deep learning.


What does a neuron compute?

Every neuron first computes

z = Wx + b

where

Then it applies an activation function

a = sigmoid(z)

where a becomes the neuron's output.


Why random weights?

If every neuron started with identical weights,

every neuron would learn exactly the same feature.

Random initialization breaks this symmetry and allows different neurons to learn different patterns.


Why do we need an activation function?

Without an activation function,

multiple linear layers are still equivalent to one single linear layer.

Activation functions introduce non-linearity, allowing the network to learn complex patterns.

This tutorial uses Sigmoid because its derivative is easy to derive by hand.

Modern neural networks usually use


Loss Function

This tutorial uses Mean Squared Error (MSE)

L = 1/2 (y - l2)^2

Training tries to minimize this loss.


Forward Pass

Input

↓

Linear (W1)

↓

Sigmoid

↓

Hidden Layer

↓

Linear (W2)

↓

Sigmoid

↓

Prediction

Backward Pass (Backpropagation)

Prediction

↓

Compute Output Gradient (l2_delta)

↓

Propagate Error to Hidden Layer (l1_delta)

↓

Update W2

↓

Update W1

Backpropagation simply means

propagating the error backwards through the network.


Why can't we simply add the error to every weight?

Every weight contributes differently to the prediction.

Some weights have a huge influence.

Others barely affect the output.

The gradient tells us

"How much should this particular weight change to reduce the loss?"


Understanding l2_delta

l2_delta = (y-l2) * (l2*(1-l2))

Break it into two parts.

1. Prediction Error

(y-l2)

This measures how wrong the prediction is.


2. Sigmoid Derivative

Since

a = sigmoid(z)

its derivative is

Οƒ'(z) = Οƒ(z)(1-Οƒ(z))

Since l2 already stores

Οƒ(z)

we can compute

l2*(1-l2)

Therefore

l2_delta

=

Prediction Error

Γ—

Sigmoid Derivative

This is the output gradient.


Understanding l1_delta

l1_delta = l2_delta.dot(w2.T) * (l1*(1-l1))

This one is more difficult.

The hidden layer does not know the true labels.

Instead,

the error must first travel backwards from the output layer.

Step 1

Take the output gradient

l2_delta

Step 2

Send the error backwards through the hidden→output weights

l2_delta.dot(w2.T)

This distributes the output error among all hidden neurons according to how much each hidden neuron influenced the output.

A stronger connection means


Step 3

The hidden neurons also use sigmoid,

so multiply by its derivative

l1*(1-l1)

Therefore

l1_delta

=

Output Gradient

Γ—

Connection Strength (W2)

Γ—

Hidden Sigmoid Derivative

l1_delta tells each hidden neuron

"How much should you change?"


Why do we use W2 instead of W1?

The output neuron only receives information from the hidden layer.

Therefore,

to determine how much each hidden neuron contributed to the final prediction,

the error must first travel backwards through W2.

Only after computing the hidden layer gradient do we update W1.

Output Error

↓

W2

↓

Hidden Layer Gradient

↓

Update W1

Why transpose (W2.T)?

This is purely because of matrix dimensions.

L2_delta

(4Γ—1)

Γ—

W2.T

(1Γ—4)

=

(4Γ—4)

Without the transpose,

matrix multiplication would be impossible because the dimensions would not align.


Why use dot()?

If there were only one hidden neuron,

we could simply write

hidden_error = output_error * weight

But there are 4 hidden neurons.

Each hidden neuron receives a different amount of the output error.

Matrix multiplication (dot) efficiently distributes the error to every hidden neuron simultaneously.


Why is matrix multiplication important?

Instead of computing

matrix multiplication computes

in one operation.

This is one of the reasons neural networks run efficiently on GPUs.


Things I Learned Today


Questions for Tomorrow