Neural Networks From Scratch
Forward Propagation & Loss Functions
Forward Propagation — Data Flowing ONE DIRECTION
Jr Codex Deep Learning Notes
Level: Intermediate Prerequisites: Chapter 2: Activation Functions Time to complete: ~20 minutes
Table of Contents
- Forward Propagation, Formally
- A Full Forward Pass, Layer by Layer
- What a Loss Function Does
- Regression Losses
- Classification Losses
- Putting It Together in PyTorch
- Summary & Next Steps
1. Forward Propagation, Formally
Forward propagation (or the "forward pass") is simply running input data through every layer of the network, in order, to produce a final output — exactly what Chapter 1's dense layers and Chapter 2's activation functions do when chained together.
Forward Propagation — Data Flowing ONE DIRECTION
─────────────────────────────────────────
Input → Layer 1 → Activation → Layer 2 → Activation → ... → Output
Each layer's OUTPUT becomes the NEXT layer's INPUT — data
flows strictly forward, never backward, during this pass.
(Backpropagation, Chapter 4, is what flows information
BACKWARD, after the forward pass completes.)
─────────────────────────────────────────
2. A Full Forward Pass, Layer by Layer
import numpy as np
def relu(z):
return np.maximum(0, z)
def sigmoid(z):
return 1 / (1 + np.exp(-z))
# A 2-layer network: 4 inputs -> 3 hidden neurons -> 1 output
np.random.seed(42)
W1 = np.random.randn(3, 4) * 0.1
b1 = np.zeros(3)
W2 = np.random.randn(1, 3) * 0.1
b2 = np.zeros(1)
def forward_pass(x):
z1 = W1 @ x + b1
a1 = relu(z1) # hidden layer's output, after activation
z2 = W2 @ a1 + b2
a2 = sigmoid(z2) # output layer's output (a probability, for binary classification)
return a2, (z1, a1, z2, a2) # cache intermediate values — Chapter 4's backprop needs them
x = np.array([0.5, 0.3, 0.9, 0.1])
prediction, cache = forward_pass(x)
print(f"Prediction: {prediction}")Caching every intermediate value (z1, a1, z2, a2) matters — Chapter 4's backpropagation algorithm needs these exact values to compute gradients; this is a detail that PyTorch handles automatically via its computational graph, but is essential to understand when implementing backprop by hand.
3. What a Loss Function Does
A loss function (also called a cost function or objective function) measures how wrong a single prediction is, compared to the true label — directly the same concept as the ML Notes' evaluation metrics, but computed per example, during every training step, to drive learning itself (not just to evaluate a finished model).
The Role of Loss in Training
─────────────────────────────────────────
1. Forward pass (Section 2) produces a prediction
2. Loss function compares prediction to TRUE label — outputs
a single number (higher = worse)
3. Backpropagation (Chapter 4) computes how each weight
should change to REDUCE this loss
4. Gradient descent (Chapter 5) actually updates the weights
5. Repeat, over many examples and many passes through the
data (called EPOCHS) — the loss should trend DOWNWARD
─────────────────────────────────────────
4. Regression Losses
For predicting continuous values — directly reusing the ML Notes' regression evaluation metrics.
def mean_squared_error(y_true, y_pred):
return np.mean((y_true - y_pred) ** 2)
def mean_absolute_error(y_true, y_pred):
return np.mean(np.abs(y_true - y_pred))
y_true = np.array([3.0, 5.0, 2.5])
y_pred = np.array([2.8, 5.2, 3.0])
print(f"MSE: {mean_squared_error(y_true, y_pred):.4f}")
print(f"MAE: {mean_absolute_error(y_true, y_pred):.4f}")MSE vs MAE — the Same Tradeoff Covered in ML Notes
─────────────────────────────────────────
MSE: penalizes LARGE errors disproportionately (squaring) —
sensitive to outliers, but produces smoother gradients
for optimization
MAE: treats all errors proportionally — more robust to
outliers, but its gradient is constant, which can
slow convergence near the minimum
─────────────────────────────────────────
5. Classification Losses
Cross-entropy loss (also called log loss) is the standard loss for classification, pairing naturally with sigmoid (binary) or softmax (multi-class) output activations from Chapter 2.
def binary_cross_entropy(y_true, y_pred, epsilon=1e-15):
y_pred = np.clip(y_pred, epsilon, 1 - epsilon) # avoid log(0)
return -np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))
y_true = np.array([1, 0, 1])
y_pred = np.array([0.9, 0.1, 0.8]) # CONFIDENT and CORRECT predictions
print(f"BCE (good predictions): {binary_cross_entropy(y_true, y_pred):.4f}") # LOW loss
y_pred_bad = np.array([0.1, 0.9, 0.2]) # CONFIDENT and WRONG predictions
print(f"BCE (bad predictions): {binary_cross_entropy(y_true, y_pred_bad):.4f}") # HIGH lossWhy Cross-Entropy, and Not MSE, for Classification
─────────────────────────────────────────
Cross-entropy specifically PUNISHES confident wrong
predictions much more harshly than MSE would — a prediction
of 0.99 for the wrong class produces a much larger cross-
entropy loss than MSE would assign to the same error,
producing a STRONGER, more useful gradient signal for the
network to learn from.
─────────────────────────────────────────
def categorical_cross_entropy(y_true_onehot, y_pred_probs, epsilon=1e-15):
y_pred_probs = np.clip(y_pred_probs, epsilon, 1)
return -np.sum(y_true_onehot * np.log(y_pred_probs)) / len(y_true_onehot)
# 3-class example: true class is class 1 (index 1), one-hot encoded
y_true_onehot = np.array([0, 1, 0])
y_pred_probs = np.array([0.1, 0.7, 0.2]) # softmax output (Ch.2, Section 6)
print(f"Categorical cross-entropy: {categorical_cross_entropy(y_true_onehot, y_pred_probs):.4f}")6. Putting It Together in PyTorch
import torch
import torch.nn as nn
model = nn.Sequential(
nn.Linear(4, 3),
nn.ReLU(),
nn.Linear(3, 1),
)
x = torch.tensor([0.5, 0.3, 0.9, 0.1])
y_true = torch.tensor([1.0])
prediction = model(x) # forward pass (Section 2)
loss_fn = nn.BCEWithLogitsLoss() # combines sigmoid + binary cross-entropy in ONE stable operation
loss = loss_fn(prediction, y_true)
print(f"Loss: {loss.item():.4f}")
# Common PyTorch loss functions, by task
mse_loss = nn.MSELoss() # regression
bce_loss = nn.BCEWithLogitsLoss() # binary classification
ce_loss = nn.CrossEntropyLoss() # multi-class classification (expects RAW logits, applies softmax internally)BCEWithLogitsLoss and CrossEntropyLoss expect raw, un-activated logits, not post-sigmoid/softmax outputs — this is a deliberate PyTorch design choice for numerical stability (combining the activation and loss computation avoids precision issues with very confident predictions), and a common source of beginner bugs when a model's forward pass includes an unnecessary final activation.
7. Summary & Next Steps
Key Takeaways
- Forward propagation is running input data through every layer in sequence, caching intermediate values needed for backpropagation.
- A loss function quantifies how wrong a prediction is; training is the process of adjusting weights to minimize it, over many examples and epochs.
- MSE and MAE are standard regression losses; cross-entropy is the standard classification loss, chosen specifically because it penalizes confident wrong predictions more harshly than MSE would.
- PyTorch's
BCEWithLogitsLossandCrossEntropyLossexpect raw logits (not post-activation outputs) for numerical stability — a common source of bugs if misunderstood.
Concept Check
- Why does the forward pass need to cache intermediate values like
z1anda1, rather than just the final output? - Why is cross-entropy loss generally preferred over MSE for classification tasks?
- What's a common mistake when using PyTorch's
CrossEntropyLoss, related to activation functions?
Next Chapter
→ Chapter 4: Backpropagation Explained
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index | Back to DL Index