Part of Language AI Handbook
Explains how MLPs stack layers to solve problems no linear model can. Topics include hidden layers, weight matrices, forward pass, universal approximation.
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Multilayer Perceptrons
In the previous chapters, we explored linear classifiers and activation functions as separate building blocks. Linear classifiers can find decision boundaries, but only straight ones. Activation functions introduce non-linearity, but a single neuron with an activation still has limited representational power. The breakthrough comes when we stack these components together into layers, creating what we call a multilayer perceptron (MLP).
MLPs are the workhorses of deep learning. They can approximate virtually any function given enough neurons and proper training. From sentiment analysis to language modeling, understanding MLPs is essential because they form the building blocks of more complex architectures like transformers. This chapter shows you how to construct, understand, and implement MLPs from the ground up.
From Single Neurons to Hidden Layers
A single neuron computes a weighted sum of its inputs, adds a bias, and passes the result through an activation function. Given an input vector with features, the neuron computes:
where:
- : the input vector containing features
- : the weight vector, where each controls how much input influences the output
- : the dot product , computing a weighted sum of inputs
- : the bias term, which shifts the decision boundary
- : the activation function (e.g., ReLU, sigmoid), which introduces non-linearity
- : the scalar output of the neuron
This single neuron can learn a linear decision boundary (made non-linear by ), but it cannot solve problems requiring more complex boundaries.
A hidden layer is a collection of neurons that sits between the input and output of a neural network. Each neuron in a hidden layer receives the full input (or the output of the previous layer), applies its own weights and bias, and produces one scalar output. The term "hidden" reflects that these intermediate computations are not directly observed, only the final output layer is.
The XOR Problem
Consider the classic XOR problem: given two binary inputs, output 1 if exactly one input is 1, and 0 otherwise. No single linear boundary can separate the positive from negative examples. But with a hidden layer, we can first transform the inputs into a new representation where the classes become linearly separable.
The XOR truth table is:
| XOR output | ||
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
No matter how you try to draw a straight line through the four points, some misclassification will remain. XOR requires a non-linear boundary, and that is exactly what a hidden layer provides.
import numpy as np
# XOR inputs and outputs
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = np.array([0, 1, 1, 0])
The magic happens when we add a hidden layer. Each hidden neuron learns to detect a different feature or pattern in the input. The output layer then combines these learned features to make the final prediction.
To see this in action, let's manually construct a hidden layer that solves XOR and visualize how it transforms the input space:


The hidden layer has transformed the input space so that a simple linear classifier can now separate the classes. The first hidden neuron activates only when both inputs are high (AND-like behavior), while the second activates when either input is high (OR-like behavior). In this new coordinate system, the XOR pattern becomes trivially separable.
Network Architecture and Notation
An MLP consists of an input layer, one or more hidden layers, and an output layer. We describe the architecture by the number of units in each layer. For example, a network with 4 inputs, two hidden layers of 8 and 4 units, and 2 outputs would be written as 4-8-4-2.
Let's establish notation that will serve us throughout this chapter and beyond:
- : total number of layers (excluding input)
- : number of neurons in layer
- : weight matrix for layer , with shape
- : bias vector for layer , with shape
- : pre-activation values at layer
- : activations (post-activation values) at layer
- : the input treated as the activation of layer 0
The weight matrix connects layer to layer . Each row of contains the weights for one neuron in layer . The element represents the weight connecting neuron in layer to neuron in layer .
# Define a simple 3-layer MLP: 2 inputs -> 4 hidden -> 3 hidden -> 1 output
layer_sizes = [2, 4, 3, 1]
# Initialize weight matrices and bias vectors
np.random.seed(42)
weights = []
biases = []
for l in range(1, len(layer_sizes)):
W = np.random.randn(layer_sizes[l], layer_sizes[l - 1]) * 0.5
b = np.zeros((layer_sizes[l], 1))
weights.append(W)
biases.append(b)Network architecture: 2 -> 4 -> 3 -> 1 Weight matrix shapes: W[1]: (4, 2) (connects 2 neurons to 4 neurons) W[2]: (3, 4) (connects 4 neurons to 3 neurons) W[3]: (1, 3) (connects 3 neurons to 1 neurons) Bias vector shapes: b[1]: (4, 1) b[2]: (3, 1) b[3]: (1, 1) Total parameters: 31 (23 weights + 8 biases)
This 3-layer network has a modest parameter count, but the numbers grow quickly. A layer with 512 input neurons and 256 output neurons needs weights alone. Let's visualize what these weight matrices look like:



The weight matrices grow with the product of consecutive layer sizes. This rapid growth in parameters is why network architecture design requires careful consideration.
Forward Pass Computation
With our notation established, we can now understand how an MLP converts an input into an output. This process, called the forward pass, is the heart of neural network computation. Think of it as a pipeline: data flows in one direction, from input through hidden layers to output, with each layer transforming the representation along the way.
The Two-Step Layer Computation
At each layer, the network performs two distinct operations that work together to create expressive transformations.
Step 1: Linear Transformation. First, we compute a weighted combination of inputs from the previous layer:
where:
- : the pre-activation vector at layer
- : the weight matrix with shape
- : the activations from the previous layer
- : the bias vector for layer
The weight matrix determines how strongly each input neuron influences each output neuron. The bias shifts the output, allowing neurons to activate even when inputs are zero.
Step 2: Non-linear Activation. Next, we apply an activation function element-wise to introduce non-linearity:
where:
- : the activation vector at layer , which becomes input to the next layer
- : the activation function applied element-wise
Without activation functions, stacking multiple linear transformations would collapse into a single linear transformation, no matter how many layers we add. As we established in the Activation Functions chapter, simplifies to a single linear transformation. Activation functions prevent this collapse.
The Complete Forward Pass
For a network with layers, the forward pass chains these two-step computations together. Starting with the input (which we treat as ), we propagate through each layer:
where:
- : the network's final output prediction
- The output layer activation is chosen based on task (sigmoid for binary classification, softmax for multiclass, identity for regression)
Why This Architecture Works
The power of this layered structure comes from composition. Each layer learns to detect increasingly abstract features:
- Early layers learn simple patterns directly from the input (edges, basic character shapes, common word fragments)
- Middle layers combine these simple patterns into more complex features (textures, object parts, phrases)
- Later layers assemble these features into high-level concepts (objects, categories, semantic meanings)
This hierarchical learning is what gives depth its power. Deeper networks discover representations at multiple levels of abstraction, something a single layer cannot achieve regardless of its width.
Implementing the Forward Pass
Let's translate these mathematical concepts into code. We'll build a forward pass function from scratch using NumPy to see exactly how the computation flows.
def relu(z):
"""ReLU activation function: max(0, z)"""
return np.maximum(0, z)
def sigmoid(z):
"""Sigmoid activation function: 1 / (1 + e^(-z))"""
return 1 / (1 + np.exp(-np.clip(z, -500, 500)))

Now we implement the forward pass itself. The function iterates through each layer, applying the two-step computation: linear transformation followed by activation.
def forward_pass(
x, weights, biases, hidden_activation=relu, output_activation=sigmoid
):
"""
Compute forward pass through the network.
Args:
x: Input vector of shape (n_features, 1) or (n_features,)
weights: List of weight matrices
biases: List of bias vectors
hidden_activation: Activation function for hidden layers
output_activation: Activation function for output layer
Returns:
activations: List of activations for each layer (including input)
pre_activations: List of pre-activation values for each layer
"""
a = x.reshape(-1, 1) if x.ndim == 1 else x
activations = [a]
pre_activations = []
for l in range(len(weights)):
z = weights[l] @ a + biases[l]
pre_activations.append(z)
if l == len(weights) - 1:
a = output_activation(z)
else:
a = hidden_activation(z)
activations.append(a)
return activations, pre_activationsTracing Through a Concrete Example
To solidify our understanding, let's trace through the forward pass with actual numbers. We'll use the 2-4-3-1 network we defined earlier and pass a single input through it.
# Single input example
x_input = np.array([[0.5], [0.8]])
# Run forward pass
activations, pre_activations = forward_pass(x_input, weights, biases)Input x: Shape: (2, 1), values: [0.5 0.8] Layer 1: z[1]: shape (4, 1), values: [0.0689, 0.7711, -0.1522, 0.7018] a[1]: shape (4, 1), values: [0.0689, 0.7711, 0.0000, 0.7018] Layer 2: z[2]: shape (3, 1), values: [0.0296, -0.9267, -0.4093] a[2]: shape (3, 1), values: [0.0296, 0.0000, 0.0000] Layer 3: z[3]: shape (1, 1), values: [0.0217] a[3]: shape (1, 1), values: [0.5054] Final output: 0.5054
The forward pass transforms our 2D input through three layers, ultimately producing a single scalar output. Several key observations emerge from this trace:
- ReLU's effect: Notice how negative pre-activation values become zero after ReLU. This sparsity helps prevent overfitting and makes computations more efficient.
- Dimensional changes: The representation changes size at each layer, from 2 dimensions (input) to 4, then 3, then finally 1 (output). The network progressively compresses information.
- Sigmoid's bounded output: The final layer's sigmoid activation squashes the output to a probability between 0 and 1.
- Composition creates complexity: Although each individual step is simple (matrix multiply, add bias, apply activation), the composition of many such steps creates a highly non-linear function.
Batch Processing with Matrix Operations
The forward pass we implemented processes one example at a time, but this is inefficient in practice. Modern hardware, especially GPUs, is designed for parallel computation. By processing multiple examples simultaneously, we can achieve dramatic speedups.
For a batch of examples, the input becomes a matrix of shape . The forward pass equations generalize elegantly:
where:
- : the activation matrix from the previous layer, with shape
- : the weight matrix with shape
- : the pre-activation matrix with shape
- : the bias vector with shape , broadcast across all columns
- : the batch size
The matrix multiplication computes the linear transformation for all examples at once. The bias vector is broadcast (replicated) across all columns.
def forward_pass_batch(
X, weights, biases, hidden_activation=relu, output_activation=sigmoid
):
"""
Compute forward pass for a batch of inputs.
Args:
X: Input matrix of shape (n_features, batch_size)
weights: List of weight matrices
biases: List of bias vectors
Returns:
activations: List of activation matrices for each layer
pre_activations: List of pre-activation matrices for each layer
"""
A = X if X.ndim == 2 else X.reshape(-1, 1)
activations = [A]
pre_activations = []
for l in range(len(weights)):
Z = weights[l] @ A + biases[l]
pre_activations.append(Z)
if l == len(weights) - 1:
A = output_activation(Z)
else:
A = hidden_activation(Z)
activations.append(A)
return activations, pre_activations
# Create a batch of 5 examples
X_batch = np.random.randn(2, 5)
activations_batch, _ = forward_pass_batch(X_batch, weights, biases)Batch input shape: (2, 5) (2 features, 5 examples) Batch output shape: (1, 5) (1 output, 5 examples) Outputs for each example: Example 1: 0.5000 Example 2: 0.5000 Example 3: 0.5000 Example 4: 0.5528 Example 5: 0.5000
All five examples are processed in a single matrix operation, producing five outputs simultaneously. Batch processing also provides more stable gradient estimates during training, as we will see in the Backpropagation chapter.
Representational Capacity and the Universal Approximation Theorem
One of the most remarkable properties of MLPs is their ability to approximate any continuous function. The Universal Approximation Theorem formalizes this power.
A feedforward network with a single hidden layer containing a finite number of neurons, and a non-polynomial activation function, can approximate any continuous function on a compact subset of to arbitrary accuracy. This holds given appropriate weights, but does not guarantee that gradient descent will find such weights.
The theorem tells us that MLPs are expressive enough to represent complex functions. However, it says nothing about:
- How many neurons are needed (potentially exponentially many for a single hidden layer)
- Whether training will find good weights
- How well the network will generalize to new data
In practice, deeper networks often work better than wider networks for the same total number of parameters. Depth enables hierarchical feature learning, where early layers detect simple patterns and later layers combine them into complex concepts.
Depth vs. Width Tradeoff
Depth and width trade off representational capacity in different ways. A shallow but wide network might require exponentially more neurons to represent the same function as a moderately deep network. The intuition is similar to building with modular components: a few large building blocks can cover area, but many small components arranged hierarchically create more complex structures with fewer total pieces.
Consider three architectures with similar parameter budgets but different depth/width tradeoffs:
# Demonstrate function approximation with varying network sizes
def create_mlp(layer_sizes):
"""Create an MLP with given layer sizes using Xavier initialization."""
np.random.seed(42)
w_list = []
b_list = []
for l in range(1, len(layer_sizes)):
scale = np.sqrt(2.0 / (layer_sizes[l - 1] + layer_sizes[l]))
W = np.random.randn(layer_sizes[l], layer_sizes[l - 1]) * scale
b = np.zeros((layer_sizes[l], 1))
w_list.append(W)
b_list.append(b)
return w_list, b_list
shallow_mlp = create_mlp([128, 64, 1]) # 1 hidden layer, 64 neurons
medium_mlp = create_mlp([128, 32, 32, 1]) # 2 hidden layers, 32 neurons each
deep_mlp = create_mlp([128, 16, 16, 16, 1]) # 3 hidden layers, 16 neurons each
def count_params(w_list, b_list):
return sum(W.size for W in w_list) + sum(b.size for b in b_list)Network architectures and parameter counts: Shallow (128-64-1): 8,321 parameters (1 hidden layer) Medium (128-32-32-1): 5,217 parameters (2 hidden layers) Deep (128-16-16-16-1): 2,625 parameters (3 hidden layers)
The shallow network has the most parameters despite having only one hidden layer. The deeper networks distribute their capacity across more layers with fewer neurons each. Let's visualize this tradeoff:

This tradeoff matters in practice. Depth enables compositionality: the network can learn that "a phrase that contains a question word followed by a verb" is a pattern, without needing to enumerate every possible such combination.
MLP for Classification
Classification is one of the most common tasks for MLPs. The network takes features as input and produces probabilities for each class. For binary classification, we use a single output neuron with sigmoid activation. For multiclass, we use multiple output neurons with softmax activation.
Binary Classification
In binary classification, the output represents the probability that the input belongs to class 1. We train the network by minimizing the binary cross-entropy loss:
where:
- : the number of training examples in the batch
- : the true label for example (either 0 or 1)
- : the predicted probability that example belongs to class 1
When the true label is , only the first term is active, penalizing low predicted probabilities. When , only the second term is active, penalizing high predicted probabilities. The loss approaches zero when predictions are confident and correct, and grows very large when predictions are confidently wrong.

Let's build a complete binary classifier using PyTorch on the two moons dataset, a classic non-linearly separable benchmark:
import torch
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
# Create a non-linearly separable dataset
X_moons, y_moons = make_moons(n_samples=1000, noise=0.25, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(
X_moons, y_moons, test_size=0.2, random_state=42
)
# Standardize features
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
# Convert to PyTorch tensors
X_train_t = torch.FloatTensor(X_train)
y_train_t = torch.FloatTensor(y_train).reshape(-1, 1)
X_test_t = torch.FloatTensor(X_test)
y_test_t = torch.FloatTensor(y_test).reshape(-1, 1)import torch.nn as nn
import torch.optim as optim
class BinaryClassifierMLP(nn.Module):
def __init__(self, input_size, hidden_sizes, dropout_rate=0.2):
super().__init__()
layers = []
prev_size = input_size
for hidden_size in hidden_sizes:
layers.append(nn.Linear(prev_size, hidden_size))
layers.append(nn.ReLU())
layers.append(nn.Dropout(dropout_rate))
prev_size = hidden_size
# Output layer with sigmoid for binary classification
layers.append(nn.Linear(prev_size, 1))
layers.append(nn.Sigmoid())
self.network = nn.Sequential(*layers)
def forward(self, x):
return self.network(x)
# Create model: 2 inputs -> 16 -> 8 -> 1 output
model = BinaryClassifierMLP(input_size=2, hidden_sizes=[16, 8])
criterion = nn.BCELoss()
optimizer = optim.Adam(model.parameters(), lr=0.01)# Training loop
train_losses = []
test_accuracies = []
for epoch in range(200):
model.train()
y_pred = model(X_train_t)
loss = criterion(y_pred, y_train_t)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_losses.append(loss.item())
model.eval()
with torch.no_grad():
y_test_pred = model(X_test_t)
accuracy = ((y_test_pred > 0.5) == y_test_t).float().mean().item()
test_accuracies.append(accuracy)Training Results: Final training loss: 0.2116 Final test accuracy: 95.50% Initial loss: 0.6903 Loss reduction: 69.3%
The model achieves strong test accuracy after 200 epochs. The significant reduction in loss indicates successful learning. Let's visualize the decision boundary:


The MLP learns a curved decision boundary that cleanly separates the two moon-shaped clusters. A linear classifier would fail entirely on this task, but the hidden layer converts the input into a representation where the classes become separable.
Multiclass Classification
For problems with more than two classes, we use softmax activation in the output layer. Softmax converts a vector of raw scores (called logits) into a probability distribution. Given an output vector from the final layer, softmax computes the probability for each class:
where:
- : the total number of classes
- : the logit (raw score) for class
- : the exponential of , which ensures all values become positive
- : the sum of exponentials across all classes, serving as a normalization constant
The exponential function amplifies differences between logits. If one class has a much higher score than others, it will dominate the probability distribution. The denominator ensures all probabilities sum to 1.


from sklearn.datasets import load_iris
# Load iris dataset (3 classes)
iris = load_iris()
X_iris, y_iris = iris.data, iris.target
X_train_iris, X_test_iris, y_train_iris, y_test_iris = train_test_split(
X_iris, y_iris, test_size=0.2, random_state=42
)
scaler_iris = StandardScaler()
X_train_iris = scaler_iris.fit_transform(X_train_iris)
X_test_iris = scaler_iris.transform(X_test_iris)
X_train_iris_t = torch.FloatTensor(X_train_iris)
y_train_iris_t = torch.LongTensor(y_train_iris)
X_test_iris_t = torch.FloatTensor(X_test_iris)
y_test_iris_t = torch.LongTensor(y_test_iris)class MulticlassClassifierMLP(nn.Module):
def __init__(self, input_size, hidden_sizes, num_classes):
super().__init__()
layers = []
prev_size = input_size
for hidden_size in hidden_sizes:
layers.append(nn.Linear(prev_size, hidden_size))
layers.append(nn.ReLU())
layers.append(nn.Dropout(0.2))
prev_size = hidden_size
# Output layer: no softmax since CrossEntropyLoss includes it
layers.append(nn.Linear(prev_size, num_classes))
self.network = nn.Sequential(*layers)
def forward(self, x):
return self.network(x)
model_mc = MulticlassClassifierMLP(
input_size=4, hidden_sizes=[16, 8], num_classes=3
)
criterion_mc = nn.CrossEntropyLoss()
optimizer_mc = optim.Adam(model_mc.parameters(), lr=0.01)
for epoch in range(200):
model_mc.train()
logits = model_mc(X_train_iris_t)
loss = criterion_mc(logits, y_train_iris_t)
optimizer_mc.zero_grad()
loss.backward()
optimizer_mc.step()Test Results:
Accuracy: 100.00% (30/30 correct)
Sample predictions (first 5 test examples):
Classes: [np.str_('setosa'), np.str_('versicolor'), np.str_('virginica')]
[correct] True: versicolor | Predicted: versicolor | Probs: [0.001, 0.997, 0.002]
[correct] True: setosa | Predicted: setosa | Probs: [1.000, 0.000, 0.000]
[correct] True: virginica | Predicted: virginica | Probs: [0.000, 0.000, 1.000]
[correct] True: versicolor | Predicted: versicolor | Probs: [0.002, 0.989, 0.009]
[correct] True: versicolor | Predicted: versicolor | Probs: [0.001, 0.994, 0.005]The multiclass model achieves high accuracy on the iris dataset, correctly classifying most test samples. Notice how the softmax outputs sum to 1.0 for each example, creating valid probability distributions. When the model is confident, one class dominates with probability close to 1.0 while others are near zero.
MLP for Regression
Regression tasks require predicting continuous values rather than class labels. The key differences from classification are:
- No activation function on the output layer (identity function, allowing unbounded predictions)
- Mean squared error (MSE) or mean absolute error (MAE) as the loss function
- Output layer has one neuron per target variable
# Create a regression dataset: sin(x) + 0.1 * x^2 with Gaussian noise
np.random.seed(42)
X_reg = np.random.uniform(-3, 3, (500, 1))
y_reg = np.sin(X_reg) + 0.1 * X_reg**2 + np.random.normal(0, 0.15, X_reg.shape)
X_train_reg, X_test_reg, y_train_reg, y_test_reg = train_test_split(
X_reg, y_reg, test_size=0.2, random_state=42
)
X_train_reg_t = torch.FloatTensor(X_train_reg)
y_train_reg_t = torch.FloatTensor(y_train_reg)
X_test_reg_t = torch.FloatTensor(X_test_reg)
y_test_reg_t = torch.FloatTensor(y_test_reg)class RegressionMLP(nn.Module):
def __init__(self, input_size, hidden_sizes):
super().__init__()
layers = []
prev_size = input_size
for hidden_size in hidden_sizes:
layers.append(nn.Linear(prev_size, hidden_size))
layers.append(nn.ReLU())
prev_size = hidden_size
# Output layer with no activation for regression
layers.append(nn.Linear(prev_size, 1))
self.network = nn.Sequential(*layers)
def forward(self, x):
return self.network(x)
model_reg = RegressionMLP(input_size=1, hidden_sizes=[32, 16])
criterion_reg = nn.MSELoss()
optimizer_reg = optim.Adam(model_reg.parameters(), lr=0.01)
train_losses_reg = []
for epoch in range(300):
model_reg.train()
y_pred_reg = model_reg(X_train_reg_t)
loss = criterion_reg(y_pred_reg, y_train_reg_t)
optimizer_reg.zero_grad()
loss.backward()
optimizer_reg.step()
train_losses_reg.append(loss.item())

Regression Results: Test MSE: 0.0245 Test RMSE: 0.1564 Training Progress: Initial MSE: 0.8912 Final MSE: 0.0220 Improvement: 97.5%
The low test MSE and RMSE indicate the model fits the underlying function well. Unlike polynomial regression where you must choose the degree up front, the MLP automatically discovers the appropriate level of complexity through its hidden representations.
Architecture Design Guidelines
Designing an MLP architecture involves choosing the number of layers, neurons per layer, activation functions, and regularization techniques. While there is no universal formula, several principles guide these decisions.
Depth vs. Width
Deeper networks can represent more complex hierarchical features. However, they are harder to train due to vanishing or exploding gradients. Wider networks have more parameters per layer but may struggle to learn compositional patterns.
As a starting point:
- For simple problems, 1-2 hidden layers often suffice
- For complex patterns, 3-5 hidden layers may be needed
- Very deep networks (10+ layers) typically require residual connections to avoid gradient issues
Layer Sizes
Common patterns for hidden layer sizes include:
- Funnel: Decreasing sizes (e.g., 256-128-64) that progressively compress information
- Constant: Same size throughout (e.g., 128-128-128) for uniform capacity
- Bottleneck: Narrow middle layer (e.g., 256-32-256) to force compressed representations



The input and output sizes are determined by your problem. Hidden sizes are hyperparameters to tune based on validation performance.
Activation Functions
ReLU is the default choice for hidden layers due to its simplicity and effectiveness. Alternatives help in specific situations:
- ReLU: Fast, simple, works well in most cases
- Leaky ReLU/PReLU: Addresses the dying ReLU problem where neurons output constant zero
- GELU: Smooth approximation, popular in transformers
- Tanh/Sigmoid: Rarely used in hidden layers now due to saturation
For output layers:
- Sigmoid: Binary classification (outputs probability in range (0, 1))
- Softmax: Multiclass classification (outputs probability distribution)
- Identity (none): Regression (outputs unbounded values)
Regularization
To prevent overfitting, we use regularization techniques:
- Dropout: Randomly zeros neurons during training (typical rates: 0.2-0.5)
- Weight decay: L2 penalty on weights (typical values: 1e-4 to 1e-2)
- Batch normalization: Normalizes layer inputs, also acts as a regularizer
class WellDesignedMLP(nn.Module):
"""An MLP following best practices for architecture design."""
def __init__(
self,
input_size,
num_classes,
hidden_sizes=[256, 128, 64],
dropout_rate=0.3,
use_batch_norm=True,
):
super().__init__()
layers = []
prev_size = input_size
for hidden_size in hidden_sizes:
layers.append(nn.Linear(prev_size, hidden_size))
if use_batch_norm:
layers.append(nn.BatchNorm1d(hidden_size))
layers.append(nn.ReLU())
layers.append(nn.Dropout(dropout_rate))
prev_size = hidden_size
layers.append(nn.Linear(prev_size, num_classes))
self.network = nn.Sequential(*layers)
def forward(self, x):
return self.network(x)Well-designed MLP architecture:
WellDesignedMLP(
(network): Sequential(
(0): Linear(in_features=100, out_features=256, bias=True)
(1): BatchNorm1d(256, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
(2): ReLU()
(3): Dropout(p=0.3, inplace=False)
(4): Linear(in_features=256, out_features=128, bias=True)
(5): BatchNorm1d(128, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
(6): ReLU()
(7): Dropout(p=0.3, inplace=False)
(8): Linear(in_features=128, out_features=64, bias=True)
(9): BatchNorm1d(64, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
(10): ReLU()
(11): Dropout(p=0.3, inplace=False)
(12): Linear(in_features=64, out_features=10, bias=True)
)
)
Parameter count:
Total parameters: 68,554
Trainable parameters: 68,554This architecture applies the Linear, BatchNorm, ReLU, Dropout pattern for each hidden layer. The funnel shape (256, 128, 64) progressively compresses the representation, forcing the network to distill the most important features as information flows toward the output.
Limitations and Impact
Despite their power, MLPs have significant limitations that motivated the development of more specialized architectures.
The most fundamental limitation is their treatment of input as a flat vector. When processing images, MLPs ignore spatial structure, treating each pixel as an independent feature. A small shift in the image produces a completely different input vector, yet the semantic content remains the same. This lack of translation invariance means MLPs need to learn the same pattern multiple times for different positions. Convolutional neural networks address this by sharing weights across spatial locations.
Similarly, for sequential data like text, MLPs treat each position independently. The sentence "The cat sat on the mat" becomes a fixed-size vector where position 1 and position 5 have no structural relationship. This makes learning long-range dependencies extremely difficult. The meaning of "it" in "The trophy doesn't fit in the suitcase because it is too big" depends on understanding the full context, something an MLP struggles with. Recurrent networks were developed to handle sequential dependencies, and transformers later extended this with attention mechanisms that can relate any position to any other position directly.
MLPs also struggle with variable-length inputs. Every MLP has a fixed input size determined at architecture design time. Processing sentences of different lengths requires padding to a maximum length or using techniques like bag-of-words that lose positional information entirely. This rigidity limits their applicability to many real-world problems.
Despite these limitations, MLPs remain foundational. The feed-forward layers within transformers are MLPs. The classification heads on top of pre-trained language models are MLPs. Understanding how information flows through layers, how weights connect neurons, and how activations transform representations is essential knowledge for working with any modern neural architecture.
The representational power of MLPs demonstrated that neural networks could, in principle, learn complex functions. The universal approximation theorem provided theoretical justification. The practical challenge became not representation but optimization: finding the right weights among billions of possibilities. The techniques developed to train MLPs, gradient descent, backpropagation, and regularization, form the foundation of all deep learning. We will explore backpropagation in detail in the next chapter.
Summary
Multilayer perceptrons extend single neurons into networks capable of learning complex, non-linear functions. By stacking layers with non-linear activations, MLPs can approximate virtually any function, a property formalized in the universal approximation theorem.
Key takeaways from this chapter:
- Hidden layers transform inputs into new representations where patterns become easier to detect, as demonstrated by the XOR problem
- Weight matrices connect layers, with shape for the matrix connecting layer to layer
- Forward pass propagates information through linear transformations and activations:
- Batch processing uses matrix operations for efficiency, stacking examples as columns of shape
- Classification uses sigmoid (binary) or softmax (multiclass) output activations with cross-entropy loss
- Regression uses identity (no) output activation with MSE loss
- Architecture design involves balancing depth, width, activation functions, and regularization
The next chapter explores backpropagation in detail, examining how gradients flow backward through the network to update weights. Backpropagation explains how MLPs learn from data.
Key Parameters
When building MLPs in PyTorch, several parameters significantly impact model performance.
Architecture Parameters:
- hidden_sizes: List of neurons per hidden layer (e.g.,
[256, 128, 64]). Larger sizes increase capacity but also parameter count and risk of overfitting. Start with powers of 2 for computational efficiency. - input_size: Number of input features, determined by your data.
- num_classes / output_size: Number of output neurons. Use 1 for binary classification or regression, for -class classification.
Regularization Parameters:
- dropout_rate: Probability of zeroing neurons during training (typically 0.1-0.5). Higher values provide stronger regularization but may slow convergence. Use 0.2-0.3 as a starting point.
- weight_decay: L2 regularization strength in the optimizer (typically 1e-4 to 1e-2). Penalizes large weights to reduce overfitting.
Training Parameters:
- lr (learning rate): Step size for gradient updates (typically 1e-4 to 1e-1). Too high causes instability; too low causes slow convergence. Adam optimizer often works well with lr=0.001.
- epochs: Number of complete passes through the training data. Monitor validation loss to avoid overfitting.
- batch_size: Number of examples per gradient update (typically 32-256). Larger batches provide more stable gradients but require more memory.
Activation Choices:
- Hidden layers: ReLU is the default. Consider Leaky ReLU if many neurons die (output constant zero).
- Output layer: Sigmoid for binary classification, Softmax (via CrossEntropyLoss) for multiclass, Identity (none) for regression.
Loss Functions:
nn.BCELoss(): Binary cross-entropy for binary classification with sigmoid output.nn.CrossEntropyLoss(): Combines softmax and negative log-likelihood for multiclass classification. Expects raw logits, not probabilities.nn.MSELoss(): Mean squared error for regression tasks.
Quiz
Ready to test your understanding? Take this quick quiz to reinforce what you've learned about multilayer perceptrons.
Multilayer Perceptrons Quiz
Reference
Citation details
Cite or share this article.
Continue with the full handbook
This chapter is part of Language AI Handbook. Use the handbook page to browse the complete table of contents and continue reading in sequence.
Explore Language AI HandbookStay up to date
Get articles, book updates, and news delivered to your inbox.
No spam, unsubscribe anytime.
Join the community
Sign in to remove popups, track your reading progress, and join the discussion.

Comments
1 comment
fantastic article
Thank you very much for your comment!