Index Ai Academy
Phase 4 · Deep learning · Neural networks

The forward pass

Follow numbers through a network to a prediction.

About 30 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

Every time you use a neural network, it runs a forward pass. Numbers go in at one end and a prediction comes out the other. When a chatbot writes a word, it runs one forward pass through all its layers. At the end it has a score, called a logit, for every token it knows. Following those numbers, and checking their shapes, is how you'll find bugs in every model you build from here on.

What you'll be able to do

  • Trace one example through a network: weighted sums, activations, logits, probabilities
  • Run a whole batch through a network with matrix multiplications and track every shape
  • Turn logits into probabilities with a numerically stable softmax
  • Compute the cross-entropy loss that training will try to reduce

What it covers

  1. Numbers in, prediction out
  2. What happened at each step
  3. Worked example: the stepper's numbers
  4. A whole batch at once
  5. Stable softmax
  6. Logits in an LLM

Numbers in, prediction out

Think of a sandwich shop's assembly line. The first station lays out the bread. The next adds the fillings. The last one wraps the sandwich and hands it over. Each station does one small job and passes the result along.

A neural network works the same way. Each layer is a station. It takes a list of numbers, does a small calculation, and passes new numbers to the next layer. Running an input through every layer, from start to finish, is called the forward pass. It's "forward" because the numbers only move one way: from the input toward the output.

A small classifier does four jobs, in order. (A class is one of the possible answers, such as "spam" or "not spam".)

  1. Hidden layer. Each neuron takes a weighted sum of the inputs and adds its bias. Then it applies ReLU. You met all three in the neuron and activation lessons.
  2. Output layer. More weighted sums turn the hidden values into one raw score per class. These raw scores are called logits.
  3. Softmax. It turns the logits into probabilities that add up to 1.
  4. Loss. One number that says how wrong the prediction was.

One example, step by step

Here is a tiny network. It has 2 inputs, 2 hidden neurons and 2 output classes. Step through the code and watch each value appear.

Quick check
What does this print?
x = [1.0, 2.0]
w = [0.5, -1.0]
b = 1.0
print(sum(a * c for a, c in zip(x, w)) + b)
  1. -0.5
  2. 2.5
  3. 0.5
  4. -1.5
Show the answer

-0.5

1.0 × 0.5 is 0.5 and 2.0 × (−1.0) is −2, and adding the bias gives 0.5 − 2 + 1, which prints as -0.5. A forward pass is this, over and over, layer by layer.

Try it now

The output layer's raw scores are logits. Softmax turns them into probabilities. The loss is −log of the probability given to the correct class.

What happened at each step

The hidden layer. Each hidden neuron follows a recipe. It multiplies each input by a weight and adds the results. Then it adds a bias. Last, ReLU keeps the result if it's positive and turns it into 0 if it's negative.

Here is one neuron as a formula. The inputs are x1x_1 and x2x_2. The weights are w1w_1 and w2w_2. The bias is bb. The output is hh, for "hidden".

h=max⁡(0, w1x1+w2x2+b)h = \max(0,\ w_1 x_1 + w_2 x_2 + b)

In plain words: multiply each input by its weight, add them up, and add the bias. If the result is negative, use 0 instead.

The inputs are 1 and 2. Hidden neuron 1 has weights 0.5 and 0.25, and a bias of 0. Its sum is 0.5 × 1 + 0.25 × 2 + 0 = 1.0. That's positive, so h1=1.0h_1 = 1.0. Neuron 2 gives −1 × 1 + 0.5 × 2 + 0.5 = 0.5, so h2=0.5h_2 = 0.5.

The output layer. Each logit is a weighted sum of the hidden values. Logit 1 is 1 × 1.0 − 1 × 0.5 = 0.5. Logit 2 is −0.5 × 1.0 + 3 × 0.5 = 1.0. There's no ReLU here. Logits are allowed to be negative, and cutting them off at 0 would throw information away. Class 2 has the higher score, but a score isn't a probability yet.

Quick check
Hidden neuron 2 computes max(0, −1 × x1 + 0.5 × x2 + 0.5). Keep x2 = 2 but change x1 to 3. What is h2?
  1. −1.5
  2. 0.5
  3. 1.5
  4. 0
Show the answer

0

−1 × 3 + 0.5 × 2 + 0.5 = −1.5. That's negative, so ReLU turns it into 0.

Softmax. You met softmax in Phase 2. It raises ee (a fixed number, about 2.718) to the power of each logit. That makes every value positive. Then it divides each one by the total, so they add up to 1. Call the two logits z1z_1 and z2z_2. The letter zz is the usual name for a logit. The probability of class 1, p1p_1, is:

p1=ez1ez1+ez2p_1 = \frac{e^{z_1}}{e^{z_1} + e^{z_2}}

In plain words: take ee to the power of this class's score. Divide it by the total of ee to the power of every score.

Here the logits are 0.5 and 1.0. e0.5e^{0.5} is about 1.649 and e1.0e^{1.0} is about 2.718. Their total is about 4.367. So p1p_1 is about 1.649 ÷ 4.367 ≈ 0.378, and p2p_2 is about 0.622. The network leans toward class 2.

The loss. The true answer for this example is class 2. Classifiers use a loss called cross-entropy. It looks only at the probability the network gave to the true class. Write that probability as ptruep_{\text{true}}.

loss=−log⁡ptrue\text{loss} = -\log p_{\text{true}}

Here log⁡\log is the natural log, ln, from the exponentials and logarithms lesson.

In plain words: take the log of the probability given to the right answer, and flip its sign. A confident right answer gives a loss near 0. A confident wrong answer gives a big loss.

Here ptruep_{\text{true}} is p2p_2, about 0.622. The loss comes to about 0.474. If the network had given the true class 0.9, the loss would be about 0.105. At 0.1, it would be about 2.303.

Quick check
For one example, the network gives the true class a probability of 0.1. For another, it gives the true class 0.9. Which example has the bigger cross-entropy loss?
  1. The one with 0.9
  2. The one with 0.1
  3. You can't tell without the logits
  4. They have the same loss
Show the answer

The one with 0.1

−ln 0.1 ≈ 2.303 and −ln 0.9 ≈ 0.105. A low probability on the right answer means a high loss.

Worked example

Worked example: the stepper's numbers

Same network, same inputs (1 and 2), and the true class is 2. This version prints each stage, so you can match it against the steps above.

import math

x1, x2 = 1.0, 2.0
h1 = max(0.0, 0.5 * x1 + 0.25 * x2 + 0.0)
h2 = max(0.0, -1.0 * x1 + 0.5 * x2 + 0.5)
o1 = 1.0 * h1 - 1.0 * h2
o2 = -0.5 * h1 + 3.0 * h2
print("hidden:", h1, h2)
print("logits:", o1, o2)

e1, e2 = math.exp(o1), math.exp(o2)
p1, p2 = e1 / (e1 + e2), e2 / (e1 + e2)
print("probabilities:", round(p1, 3), round(p2, 3))
print("loss:", round(-math.log(p2), 3))
It runs right here in your browser, with nothing to install.

The hidden values are 1.0 and 0.5. The logits are 0.5 and 1.0. Softmax gives 0.378 and 0.622, and the loss is 0.474. Each line is one of the formulas above.

A whole batch at once

Real networks rarely see one example at a time. They take a batch: many examples pushed through together. Think of a teacher marking a whole stack of tests with one answer key open on the desk.

You stack the examples as the rows of a table called XX. Each row is one example. Each column is one input. The layer's weights go in a table called W1W_1, with one column per neuron. The biases go in a list called b1b_1, with one bias per neuron. Now the whole layer is one matrix multiplication, which you met in Phase 2. The HH on the left is the table of hidden values.

H=max⁡(0, XW1+b1)H = \max(0,\ X W_1 + b_1)

In plain words: for every example and every neuron, do the weighted sum plus bias. Then replace any negative result with 0. It's the one-neuron recipe, done for all rows and all neurons at once.

Here is the stepper's hidden layer written this way. Row 1 is the stepper's input, (1, 2). Row 2 is a new input, (3, 2). For row 2, neuron 1 gives 0.5 × 3 + 0.25 × 2 + 0 = 2.0. Neuron 2 gives −1 × 3 + 0.5 × 2 + 0.5 = −1.5, and ReLU turns that into 0.

import numpy as np

X = np.array([[1.0, 2.0],      # example 1: the inputs from the stepper
              [3.0, 2.0]])     # example 2: a new pair of inputs
W1 = np.array([[0.5, -1.0],    # column 1 holds neuron 1's weights,
               [0.25, 0.5]])   # column 2 holds neuron 2's weights
b1 = np.array([0.0, 0.5])      # one bias per neuron

H = np.maximum(0, X @ W1 + b1) # weighted sums for both examples, then ReLU
print(H)
print(H.shape)

The first row matches the stepper: 1.0 and 0.5. NumPy prints 1.0 as 1. to save space. The second row is 2.0 and 0, as worked out above.

A real dataset

Now a bigger case. The Iris dataset has 150 flowers. Each has 4 measurements, such as petal length, and belongs to one of 3 species. The code takes 8 of the flowers. It runs them through a 4 → 16 → 3 network: 4 inputs, 16 hidden neurons and 3 output classes. The weights are small and random, so the network is untrained.

Two details in the code are new. First, it standardizes the inputs. Each measurement is rescaled so that, across all 150 flowers, its average is 0 and its standard deviation is 1. That stops one large measurement from drowning out the others. Second, P[np.arange(len(y)), y] picks out, for each flower, the probability given to its true species.

import numpy as np
from sklearn.datasets import load_iris

data = load_iris()
X, y = data.data[::19], data.target[::19]                     # 8 flowers spread over the 3 species
X = (X - data.data.mean(axis=0)) / data.data.std(axis=0)      # standardize the inputs

rng = np.random.default_rng(0)
W1, b1 = rng.normal(0, 0.1, size=(4, 16)), np.zeros(16)       # small random starting weights
W2, b2 = rng.normal(0, 0.1, size=(16, 3)), np.zeros(3)

Z1 = X @ W1 + b1                     # (8, 4) @ (4, 16) → (8, 16)
H = np.maximum(0, Z1)                # ReLU, same shape
logits = H @ W2 + b2                 # (8, 16) @ (16, 3) → (8, 3)
P = np.exp(logits - logits.max(axis=1, keepdims=True))
P = P / P.sum(axis=1, keepdims=True) # softmax per row: (8, 3), each row sums to 1

for name, arr in [("X", X), ("Z1 / H", H), ("logits", logits), ("P", P)]:
    print(f"{name:<7} shape {arr.shape}")
print("\nprobabilities for the first 3 flowers:\n", P[:3].round(3))
loss = -np.mean(np.log(P[np.arange(len(y)), y]))
print(f"\ncross-entropy loss: {loss:.3f}   (random guessing among 3 classes gives ln 3 = {np.log(3):.3f})")

With small random weights, the untrained network gives every species about a third. It has learned nothing yet, so it spreads its bets evenly. Its loss lands close to ln 3, which is the loss of guessing evenly among 3 classes, because −ln⁡13=ln⁡3-\ln \tfrac{1}{3} = \ln 3. Training will push the loss down by changing the weights. The next lessons show how to work out which way to change each one.

Tip: an untrained classifier's first loss should be close to ln of the number of classes. If it starts much higher, look for a bug or starting weights that are too big.

Shapes are the first thing to check

Every array has a shape: its size along each direction. A shape of (8, 16) means 8 rows and 16 columns. Most bugs in neural-network code show up as a shape you didn't expect. So print shapes often.

StepShapeRule
inputs XX(batch, features)one row per example
XW1+b1X W_1 + b_1(batch, hidden)inner sizes must match: (b, 4) @ (4, 16)
activation(batch, hidden)element-wise, shape unchanged
logits(batch, classes)one score per class per example
softmax(batch, classes)each row sums to 1
lossa single numberaveraged over the batch

The key rule for matrix multiplication is that the inner sizes must match. (8, 4) @ (4, 16) works because both inner numbers are 4. The result keeps the outer sizes, so it's (8, 16).

The bias b1b_1 has shape (16,), one number per neuron. NumPy broadcasts it. That means it adds the same list of biases to every row, so every example gets the same biases.

Quick check
A batch of 32 examples with 10 features goes into a layer with 64 neurons. What shape is the layer's output?
  1. (32, 64)
  2. (64, 32)
  3. (10, 64)
  4. (32, 10)
Show the answer

(32, 64)

(32, 10) @ (10, 64) → (32, 64): one row per example, one column per neuron.

Stable softmax

Computers store numbers with a limited size. e1000e^{1000} is far too big, so it comes out as infinity, and softmax breaks. Real models can produce large logits, so this matters.

The fix is to subtract the row's largest score from every score before you take powers of ee. The probabilities come out exactly the same. Think of measuring everyone's height against the tallest person in the room. The numbers change, but the gaps between people don't. Softmax only cares about the gaps.

After the shift, the biggest score is 0 and the rest are negative. So every power of ee is at most 1, and nothing overflows. Every real library does this. In the batch code above, it's the logits - logits.max(axis=1, keepdims=True) part.

import math

def softmax(scores):
    top = max(scores)
    exps = [math.exp(s - top) for s in scores]
    total = sum(exps)
    return [round(e / total, 3) for e in exps]

print(softmax([2.0, 1.0]))
print(softmax([1002.0, 1001.0]))

Both lines print [0.731, 0.269]. The gap is 1 in both cases, so the answer is the same. Without the shift, the second line would try to compute e1002e^{1002} and fail.

Go deeper Why the shift changes nothing

Call any one score ziz_i, and the row's largest score mm. The symbol ∑j\sum_j means "add up over every score". Subtracting mm turns each ezie^{z_i} into ezi−me^{z_i - m}, which equals ezi×e−me^{z_i} \times e^{-m}.

ezi−m∑jezj−m=ezi e−me−m∑jezj=ezi∑jezj\frac{e^{z_i - m}}{\sum_j e^{z_j - m}} = \frac{e^{z_i}\, e^{-m}}{e^{-m} \sum_j e^{z_j}} = \frac{e^{z_i}}{\sum_j e^{z_j}}

Read it as: the same factor e−me^{-m} appears on the top and on the bottom, so it cancels out.

Quick check
Stable softmax gets the scores [7, 3, 5]. What does it turn them into before taking powers of e?
  1. [0.7, 0.3, 0.5]
  2. [7, 3, 5]
  3. [0, −4, −2]
  4. [1, 0, 0]
Show the answer

[0, −4, −2]

The largest score is 7. Subtracting 7 from each gives [0, −4, −2]. The gaps stay the same, so the probabilities do too.

Logits in an LLM

A language model's forward pass ends the same way. For each position in the text, it produces a list of logits, one for each token in its vocabulary. That's often 100,000 tokens or more. Softmax turns them into probabilities for the next token. The "temperature" setting you met in Phase 1 divides those logits before the softmax.

Practice what you just read

  • 4 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 5 review cards for spaced repetition

Create a free account

Try a quiz question

What are logits?

  1. The raw output scores before softmax turns them into probabilities
  2. The probabilities that softmax produces, one for each class
  3. The weights of the last layer, read straight out of the model
  4. The true labels, written as numbers so they can be compared

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson