Particle Swarm Optimization (PSO): Complete Guide — Principles, Examples, and Python Implementation
Summary
Particle Swarm Optimization (PSO) is a stochastic optimization algorithm inspired by the collective behavior of biological swarms. Proposed by James Kennedy and Russell Eberhart in 1995, PSO simulates a swarm of particles that jointly explore the search space to identify the minimum (or maximum) of an objective function. Each particle adjusts its trajectory by combining its own individual experience (personal best position, pbest) and the collective experience of the group (global best position, gbest). This individual cognition — social information duality is at the heart of the algorithm. Unlike evolutionary algorithms, PSO uses neither crossover nor mutation: particle movement is based entirely on velocity and position updates. Simple to implement, easy to parameterize, and effective on non-convex continuous problems, PSO has established itself as one of the most popular metaheuristics in optimization and machine learning.
Mathematical Principles
PSO maintains a swarm of N particles in a D-dimensional search space. Each particle i has:
- A position x_i(t) ∈ ℝ^D at iteration t.
- A velocity v_i(t) ∈ ℝ^D that determines its movement.
- A personal best position pbest_i — the position where the particle has achieved the best score since the beginning.
- A global best position gbest — the best position discovered by the entire swarm.
Update Equations
At each iteration, the velocity and position of each particle are updated according to the following formulas:
Velocity update:
v(t+1) = w · v(t) + c₁ · r₁ · (pbest − x) + c₂ · r₂ · (gbest − x)
Position update:
x(t+1) = x(t) + v(t+1)
Where:
| Parameter | Role |
|---|---|
| w (inertia) | Controls the fraction of previous motion retained. High inertia favors exploration; low inertia favors exploitation. |
| c₁ (cognitive coefficient) | Weights the attraction to the personal best position. This is the particle’s individual memory. |
| c₂ (social coefficient) | Weights the attraction to the global best position. This is the group’s influence. |
| r₁, r₂ | Random numbers drawn uniformly from [0, 1], recomputed at each step and for each dimension. They introduce the stochastic nature of the algorithm. |
In practice, a velocity ceiling |v| ≤ v_max is often applied to prevent particles from abruptly leaving the search space. Similarly, the decreasing inertia weight strategy (inertia weight decay) is widely used: start with a high w (broad exploration) and gradually reduce it toward a low w (local refinement).
Intuition: The School of Fish
To understand PSO without formulas, imagine a school of fish searching for food in a darkened lake. Each fish:
- Remembers the spot where it has already found the most food (pbest — personal memory).
- Observes the group and identifies the area where the luckiest fish found the most food (gbest — collective information).
- Swims by combining these two signals: it tends toward its own favorite zone while being attracted to the group’s best spot. Each advances based on its own memory (where I’ve found food before) and the collective memory (where the group has found the most).
If a fish were guided only by its personal memory (c₂ = 0), it would circle around its own past discoveries — no collective learning. Conversely, if it only followed the group’s best (c₁ = 0), the entire swarm would immediately converge to a single point, at risk of missing better solutions elsewhere. The balance between these two forces — individual cognition and social collaboration — is what makes PSO effective.
The inertia w acts like momentum: a fast fish continues in its direction despite new information (exploration), while a slowing fish finely adjusts its trajectory (exploitation).
Python Implementation
Here is a from-scratch implementation of PSO applied to the Rastrigin function, a classic optimization benchmark:
import numpy as np
def rastrigin(x):
"""Rastrigin function (global minimum = 0 at x = 0)."""
A = 10
D = len(x)
return A * D + np.sum(x**2 - A * np.cos(2 * np.pi * x))
class ParticleSwarmOptimizer:
def __init__(self, n_particles, dim, bounds, w=0.7, c1=1.5, c2=1.5,
max_iter=200, v_max=None):
self.n_particles = n_particles
self.dim = dim
self.bounds = bounds
self.w = w
self.c1 = c1
self.c2 = c2
self.max_iter = max_iter
self.v_max = v_max if v_max else (bounds[0][1] - bounds[0][0]) * 0.2
# Random initialization
lows = np.array([b[0] for b in bounds])
highs = np.array([b[1] for b in bounds])
self.positions = np.random.uniform(lows, highs, (n_particles, dim))
self.velocities = np.random.uniform(-self.v_max, self.v_max,
(n_particles, dim))
self.pbest_pos = self.positions.copy()
self.pbest_val = np.array([self._evaluate(p) for p in self.positions])
gbest_idx = np.argmin(self.pbest_val)
self.gbest_pos = self.pbest_pos[gbest_idx].copy()
self.gbest_val = self.pbest_val[gbest_idx]
self.convergence_history = []
def _evaluate(self, x):
return rastrigin(x)
def optimize(self):
for t in range(self.max_iter):
# Linear decreasing inertia
w_current = self.w - (self.w - 0.3) * (t / self.max_iter)
for i in range(self.n_particles):
r1 = np.random.rand(self.dim)
r2 = np.random.rand(self.dim)
# Velocity update
cognitive = self.c1 * r1 * (self.pbest_pos[i] - self.positions[i])
social = self.c2 * r2 * (self.gbest_pos - self.positions[i])
self.velocities[i] = (w_current * self.velocities[i]
+ cognitive + social)
# Velocity ceiling
self.velocities[i] = np.clip(self.velocities[i],
-self.v_max, self.v_max)
# Position update
self.positions[i] += self.velocities[i]
# Boundary enforcement
for d in range(self.dim):
self.positions[i, d] = np.clip(
self.positions[i, d], self.bounds[d][0], self.bounds[d][1])
# Evaluation
val = self._evaluate(self.positions[i])
# Update pbest
if val < self.pbest_val[i]:
self.pbest_val[i] = val
self.pbest_pos[i] = self.positions[i].copy()
# Update gbest
if val < self.gbest_val:
self.gbest_val = val
self.gbest_pos = self.positions[i].copy()
self.convergence_history.append(self.gbest_val)
return self.gbest_pos, self.gbest_val
# Execution
if __name__ == "__main__":
dim = 2
bounds = [(-5.12, 5.12)] * dim
pso = ParticleSwarmOptimizer(
n_particles=30, dim=dim, bounds=bounds,
w=0.9, c1=2.0, c2=2.0, max_iter=100
)
best_pos, best_val = pso.optimize()
print(f"Best position: {best_pos}")
print(f"Best value : {best_val:.6f}")
print(f"Theoretical value : 0.000000 (at x = 0)")
print(f"History (last 5) : {pso.convergence_history[-5:]}")
Trajectory and Convergence Visualization
To visualize swarm convergence, plot the gbest history over iterations:
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 5))
plt.plot(pso.convergence_history, linewidth=2, color="steelblue")
plt.xlabel("Iteration")
plt.ylabel("Best value (gbest)")
plt.title("PSO Convergence on the Rastrigin Function")
plt.grid(True, alpha=0.3)
plt.yscale("log")
plt.show()
On the 2D Rastrigin function, fast convergence is generally observed in a few dozen iterations, with gbest approaching the theoretical value of 0 with precision from 10⁻³ to 10⁻⁵ depending on the chosen parameters. To visualize the individual particle trajectory, the paths of a few representative particles can be overlaid on the Rastrigin function contour, revealing how the swarm progressively densifies around the global optimum.
Hyperparameters
PSO is remarkably economical in terms of parameters. Here are the six main hyperparameters and their typical values:
| Parameter | Description | Typical Values | Impact |
|---|---|---|---|
| n_particles | Number of particles in the swarm | 20 – 50 | The larger the swarm, the better the space coverage, but computational cost increases linearly. |
| w (inertia) | Weight of previous velocity | 0.4 – 0.9 | High inertia favors exploration; low inertia favors local exploitation. Decreasing inertia is the recommended strategy. |
| c₁ (cognitive) | Attraction to pbest | 1.5 – 2.5 | Reinforces each particle’s autonomy. A high c₁ increases swarm diversity. |
| c₂ (social) | Attraction to gbest | 1.5 – 2.5 | Accelerates collective convergence. A too-high c₂ can cause premature convergence. |
| max_iter | Maximum number of iterations | 50 – 500 | Determines the computational budget. A stopping criterion based on gbest stagnation is often preferable. |
| bounds | Search space boundaries | Problem-specific | Define the exploration window. Bounds too wide dilute the search; too narrow, they risk excluding the optimum. |
Reference Values
The values proposed by Shi and Eberhart (1998) — w = 0.729, c₁ = c₂ = 1.494 — are considered a robust starting point for most problems. The number of particles is often set to 30 as a default compromise. A common empirical rule for the number of particles is: n_particles ≈ 10 + 2 × √D, where D is the problem dimensionality.
Advantages and Limitations
Advantages
- Implementation simplicity: a few dozen lines of code suffice. No complex operators (no crossover, no mutation, no selection).
- Few parameters: only 5 to 6 hyperparameters to tune, compared to dozens for some evolutionary algorithms.
- Fast convergence: on continuous, smooth functions, PSO generally converges faster than a classical genetic algorithm.
- Natural parallelization: each particle’s evaluation is independent, enabling massive speedup on GPU or distributed clusters.
- Flexibility: adaptable to discrete problems (binary PSO), constrained spaces, and non-differentiable objective functions.
Limitations
- Premature convergence: on multimodal functions with many local minima (such as Rastrigin in high dimensions), the swarm can get trapped in a local optimum if gbest converges too quickly.
- Parameter sensitivity: although few in number, hyperparameters strongly influence performance. Poor choices lead to stagnation or divergence.
- Dimensionality: performance degrades beyond ~50 dimensions (the curse of dimensionality).
- No theoretical guarantee: unlike some convex optimization methods, PSO offers no guarantee of convergence to the global optimum.
- Discrete problems: the original version is designed for continuous space. Binary or combinatorial adaptations require significant modifications.
4 Concrete Use Cases
1. Neural Network Training
PSO can directly optimize the weights of a neural network by treating the entire set of weights as a particle’s position. Although gradient backpropagation remains dominant for training, PSO is valuable for initializing weights before fine-tuning via gradient descent, or for optimizing non-differentiable networks. Research has shown that PSO can outperform backpropagation on irregular or discontinuous activation functions.
2. Model Hyperparameter Optimization
Just like Bayesian Optimization, PSO can search for the best hyperparameters of a machine learning model (learning rate, number of layers, regularization, activation functions, etc.). Each particle encodes a combination of hyperparameters and its fitness corresponds to the cross-validation of the model trained with those parameters. This approach is particularly effective when the number of hyperparameters is high and evaluations are fast.
3. Engineering and Structural Design
In civil and aerospace engineering, PSO is used to optimize the shape of mechanical parts, the distribution of composite materials, or flight control parameters. Its ability to handle expensive-to-evaluate objective functions (finite element simulations, virtual wind tunnel tests) makes it a relevant choice. For example, the design of an aircraft wing can be formulated as minimizing drag subject to lift and structural strength constraints — an ideal problem for PSO.
4. Path Planning and Robotics
In mobile robotics, PSO plans optimal trajectories while avoiding obstacles. Each particle represents a candidate trajectory, encoded as a sequence of waypoints. The objective function penalizes path length, potential collisions, and abrupt accelerations. This approach is particularly effective for autonomous robots in dynamic environments, where the swarm can be reinitialized at each environmental configuration change to recalculate the optimal trajectory in real time.
See Also
- Title: Mastering the Python Interview Question: Calculate the Square Root of x
- Check Point Membership in a Convex Polygon in O(log N) with Python

