The simplest type of artificial neural network where connections between nodes do not form cycles.
Author
Benedict Thekkel
Intuition and Where FNNs Fit
A Feedforward Neural Network (FNN), also called a Multi-Layer Perceptron (MLP) or a fully-connected network, is the canonical starting point of deep learning. Information flows in one direction only: from the input layer, through one or more hidden layers, to the output layer. There are no cycles or feedback loops (that is what distinguishes it from an RNN).
Each layer applies a linear transformation followed by a non-linear activation function. Stacking these layers lets the network learn hierarchical, non-linear feature combinations.
Universal Approximation Theorem: a feedforward network with a single hidden layer containing a finite number of neurons can approximate any continuous function on a compact domain to arbitrary precision, given a suitable activation. In practice we use deeper networks because depth makes representations more parameter-efficient than width.
Use an FNN when your input is a fixed-size vector with no strong spatial or temporal structure (tabular data, embeddings, the head of a larger model).
Architecture: Layers, Weights, and Biases
An FNN is organized into layers:
Input layer: holds the raw feature vector \(x \in \mathbb{R}^{d_{in}}\). It does no computation; it just feeds values forward.
Hidden layers: each neuron computes a weighted sum of the previous layer plus a bias, then applies a non-linearity. These layers learn intermediate representations.
Output layer: produces the final prediction. Its size and activation depend on the task (e.g. one logit per class for classification, a single linear unit for scalar regression).
For a layer \(l\) with weight matrix \(W^{(l)} \in \mathbb{R}^{n_l \times n_{l-1}}\) and bias \(b^{(l)} \in \mathbb{R}^{n_l}\):
where \(a^{(0)} = x\). Every neuron in layer \(l\) connects to every neuron in layer \(l-1\), hence fully connected. A layer mapping \(n_{l-1}\) inputs to \(n_l\) outputs has \(n_l \cdot n_{l-1} + n_l\) learnable parameters.
The Forward Pass Math (From Scratch)
The forward pass is just repeated affine transform + activation: \(h = \sigma(Wx + b)\). Below we implement a two-layer MLP forward pass using only NumPy so the mechanics are fully transparent. Note how matrix shapes line up: a batch of N examples with d_in features is a matrix X of shape (N, d_in), and we right-multiply by W.T (or store W already transposed) to broadcast across the batch.
import numpy as npdef relu(z):return np.maximum(0.0, z)def softmax(z):# numerically stable softmax over the last axis z = z - z.max(axis=-1, keepdims=True) e = np.exp(z)return e / e.sum(axis=-1, keepdims=True)rng = np.random.default_rng(0)N, d_in, d_hidden, d_out =4, 3, 5, 2# batch, in, hidden, outX = rng.standard_normal((N, d_in)) # (4, 3)W1 = rng.standard_normal((d_in, d_hidden)) *0.1# (3, 5)b1 = np.zeros(d_hidden) # (5,)W2 = rng.standard_normal((d_hidden, d_out)) *0.1# (5, 2)b2 = np.zeros(d_out) # (2,)z1 = X @ W1 + b1 # (4, 5) affinea1 = relu(z1) # (4, 5) hidden activationz2 = a1 @ W2 + b2 # (4, 2) output logitsprobs = softmax(z2) # (4, 2) class probabilitiesprint('logits shape :', z2.shape)print('probs shape :', probs.shape)print('rows sum to 1:', np.allclose(probs.sum(axis=1), 1.0))
logits shape : (4, 2)
probs shape : (4, 2)
rows sum to 1: True
Activation Functions
Non-linear activations are what give a deep network its expressive power; without them a stack of linear layers collapses into a single linear map. Common choices:
ReLU\(\max(0, x)\): cheap, sparse, the default for hidden layers. Can suffer from dead neurons.
Sigmoid\(1/(1+e^{-x})\): squashes to \((0, 1)\). Used for binary output probabilities; saturates and causes vanishing gradients in hidden layers.
Tanh\(\tanh(x)\): squashes to \((-1, 1)\), zero-centered; still saturates.
GELU\(x \cdot \Phi(x)\): smooth, used in Transformers; often outperforms ReLU.
import torchimport torch.nn.functional as Fx = torch.linspace(-3, 3, 7)print('input :', x.tolist())print('relu :', F.relu(x).tolist())print('sigm :', torch.sigmoid(x).round(decimals=3).tolist())print('tanh :', torch.tanh(x).round(decimals=3).tolist())print('gelu :', F.gelu(x).round(decimals=3).tolist())# nn.Module forms (handy inside Sequential):import torch.nn as nnacts = nn.ModuleList([nn.ReLU(), nn.Sigmoid(), nn.Tanh(), nn.GELU()])print([type(a).__name__for a in acts])
The idiomatic way is to subclass nn.Module. Define the layers in __init__ and wire them together in forward. nn.Linear(in_features, out_features) holds the weight and bias and applies \(Wx + b\).
import torchimport torch.nn as nnclass MLP(nn.Module):def__init__(self, d_in, d_hidden, d_out, p_drop=0.0):super().__init__()self.fc1 = nn.Linear(d_in, d_hidden)self.fc2 = nn.Linear(d_hidden, d_hidden)self.fc3 = nn.Linear(d_hidden, d_out)self.act = nn.ReLU()self.drop = nn.Dropout(p_drop)def forward(self, x): x =self.drop(self.act(self.fc1(x))) # (N, d_hidden) x =self.drop(self.act(self.fc2(x))) # (N, d_hidden)returnself.fc3(x) # (N, d_out) raw logitsmodel = MLP(d_in=20, d_hidden=64, d_out=3, p_drop=0.1)dummy = torch.randn(8, 20) # (batch=8, features=20)out = model(dummy) # (8, 3)n_params =sum(p.numel() for p in model.parameters())print('output shape:', tuple(out.shape))print('parameters :', n_params)
output shape: (8, 3)
parameters : 5699
The Same Network with nn.Sequential
When the data flow is a simple straight line with no branching, nn.Sequential is more concise. It chains modules and calls them in order.
The loss measures how wrong the prediction is; training minimizes it.
nn.CrossEntropyLoss for multi-class classification. It expects raw logits (do NOT apply softmax yourself) and integer class targets. It combines LogSoftmax + NLLLoss internally for numerical stability.
nn.BCEWithLogitsLoss for binary or multi-label classification (logits in, 0/1 float targets).
nn.MSELoss (mean squared error) for regression to continuous targets.
nn.L1Loss (mean absolute error) when you want robustness to outliers.
ce = nn.CrossEntropyLoss()logits = torch.randn(8, 3) # (N, C) raw, unnormalizedtargets = torch.randint(0, 3, (8,)) # (N,) integer class idsprint('cross-entropy:', ce(logits, targets).item())mse = nn.MSELoss()pred = torch.randn(8, 1)true = torch.randn(8, 1)print('mse loss :', mse(pred, true).item())
cross-entropy: 0.9214856028556824
mse loss : 1.2620948553085327
A Complete Minimal Training Loop
Every PyTorch training step follows the same four-beat rhythm:
Forward: compute predictions from inputs.
Loss: compare predictions to targets.
Backward: loss.backward() computes gradients via autograd.
Step: optimizer.step() updates weights; optimizer.zero_grad() clears old gradients.
Below we fit an MLP on a synthetic two-cluster classification problem.
Regularization fights overfitting and improves generalization.
Dropout randomly zeros a fraction of activations during training, preventing co-adaptation. It is disabled automatically in model.eval() mode.
Weight decay (L2 penalty) shrinks weights; pass it to the optimizer via weight_decay.
Batch normalization normalizes each layer’s pre-activations per mini-batch, which stabilizes and accelerates training and has a mild regularizing effect.
reg_model = nn.Sequential( nn.Linear(20, 64), nn.BatchNorm1d(64), # normalize 64 features across the batch nn.ReLU(), nn.Dropout(0.3), # drop 30% of activations during training nn.Linear(64, 2),)# Weight decay (L2) is set on the optimizer, not the model:opt = torch.optim.Adam(reg_model.parameters(), lr=1e-3, weight_decay=1e-4)reg_model.train() # dropout ON, batchnorm uses batch statsout_train = reg_model(torch.randn(8, 20))reg_model.eval() # dropout OFF, batchnorm uses running statsout_eval = reg_model(torch.randn(8, 20))print('train out shape:', tuple(out_train.shape))print('eval out shape:', tuple(out_eval.shape))
train out shape: (8, 2)
eval out shape: (8, 2)
Strengths, Weaknesses, and When to Use
Strengths
Universal approximators; flexible and simple to implement.
Excellent for fixed-size vector inputs (tabular data, embeddings, model heads).
Fast to train relative to convolutional or recurrent stacks of similar capacity.
Weaknesses
No built-in notion of spatial or temporal structure; they ignore the geometry of images and the order of sequences.
Fully-connected layers explode in parameter count for high-dimensional inputs (a raw 224x224x3 image is 150k inputs per neuron).
Prone to overfitting without regularization.
When to use which
FNN/MLP: tabular data, fixed-length feature vectors, the classifier head on top of a CNN or Transformer backbone.
CNN: grid-structured data such as images, where local patterns and translation invariance matter.
RNN/Transformer: sequential or variable-length data such as text, audio, or time series.
Key Takeaways
An FNN is a stack of affine + non-linear layers with no cycles: \(a^{(l)} = \sigma(W^{(l)} a^{(l-1)} + b^{(l)})\).
Non-linear activations (ReLU by default) are essential; without them depth adds no power.
Build them with nn.Module for flexibility or nn.Sequential for simple chains.
Pick the loss to match the task: CrossEntropyLoss (classification, raw logits in) vs MSELoss (regression).
The training loop is always: zero_grad, forward, loss, backward, step.
Kaiming init for ReLU nets; regularize with dropout, weight decay, and batchnorm.
Reach for a CNN or Transformer when the data has spatial or sequential structure an MLP cannot exploit.