# Load required packages
library(dplyr) # For data manipulation
library(ggplot2) # For visualization
library(gganimate) # For animations
library(metR) # For geom_arrow
Building a Particle Swarm Optimizer from Scratch in R
Nature-inspired algorithms can solve complex optimization problems with surprising efficiency. This post shows you how to build a Particle Swarm Optimizer (PSO) from scratch in R – mimicking how birds flock or fish school to efficiently search for food.
Required Libraries
The Challenge: A Complex Optimization Surface
We’ll test our optimizer on Ackley’s function – a challenging benchmark with many local minima that can trap optimization algorithms:
<- function(x, y){
obj_func # Modified Ackley function with global minimum at (1,1)
-20 * exp(-0.2 * sqrt(0.5 *((x-1)^2 + (y-1)^2))) -
exp(0.5*(cos(2*pi*x) + cos(2*pi*y))) + exp(1) + 20
}
# Create a visualization grid
<- seq(-5, 5, length.out = 50)
x <- seq(-5, 5, length.out = 50)
y <- expand.grid(x, y, stringsAsFactors = FALSE)
grid $z <- obj_func(grid[,1], grid[,2])
grid
# Create a contour plot
<- ggplot(grid, aes(x = Var1, y = Var2)) +
contour_plot geom_contour_filled(aes(z = z), color = "black", alpha = 0.5) +
scale_fill_brewer(palette = "Spectral") +
theme_minimal() +
labs(x = "x", y = "y", title = "Ackley's Function")
contour_plot
How PSO Works
PSO mimics how birds find food by combining individual memory with social information:
- Scatter random “particles” across the search space
- Each particle remembers its personal best position
- The swarm shares information about the global best position
- Particles adjust their movement based on both personal and swarm knowledge
The movement equation balances three forces:
\[v_{new} = w \cdot v_{current} + c_1 \cdot r_1 \cdot (p_{best} - p_{current}) + c_2 \cdot r_2 \cdot (g_{best} - p_{current})\]
Where: - w
: Inertia weight (momentum) - c1
: Personal influence (memory) - c2
: Social influence (cooperation) - r1,r2
: Random values adding exploration
Building PSO Step by Step
Step 1: Initialize the Swarm
First, we create a random swarm of particles and place them across our search space:
# Set parameters
<- 20
n_particles <- 0.5 # Inertia weight
w <- 0.05 # Personal learning rate
c1 <- 0.1 # Social learning rate
c2
# Create random particle positions
<- seq(-5, 5, length.out = 20)
x_range <- seq(-5, 5, length.out = 20)
y_range <- data.frame(
X x = sample(x_range, n_particles, replace = FALSE),
y = sample(y_range, n_particles, replace = FALSE)
)
# Visualize initial positions
+
contour_plot geom_point(data = X, aes(x, y), color = "red", size = 2.5) +
labs(title = "Initial Particle Positions")
Step 2: Track Best Positions and Initialize Velocities
Next, we track each particle’s personal best position and the swarm’s global best position:
# Initialize random velocities
<- matrix(runif(n_particles * 2), ncol = 2) * w
dX
# Set initial personal best positions
<- X
pbest <- obj_func(X[,1], X[,2])
pbest_obj
# Find global best position
<- pbest[which.min(pbest_obj),]
gbest <- min(pbest_obj)
gbest_obj
# Visualize with arrows showing pull toward global best
<- X %>%
X_dir mutate(g_x = gbest[1,1],
g_y = gbest[1,2],
angle = atan((g_y - y)/(g_x - x))*180/pi,
angle = ifelse(g_x < x, 180 + angle, angle))
+
contour_plot geom_point(data = X, aes(x, y), color = "red", size = 2.5) +
geom_segment(data = X_dir,
aes(x = x, y = y,
xend = x + 0.5*cos(angle*pi/180),
yend = y + 0.5*sin(angle*pi/180)),
arrow = arrow(length = unit(0.1, "cm")),
color = "blue") +
labs(title = "Forces Acting on Particles")
Step 3: Update Particle Positions
Now we update each particle’s position based on its velocity and the forces acting on it:
# Calculate new velocities using PSO equation
<- w * dX +
dX *runif(1)*(pbest - X) +
c1*runif(1)*(as.matrix(gbest) - X)
c2
# Update positions
<- X + dX
X
# Evaluate function at new positions
<- obj_func(X[,1], X[,2])
obj
# Update personal best positions if improved
<- which(obj <= pbest_obj)
idx <- X[idx,]
pbest[idx,] <- obj[idx]
pbest_obj[idx]
# Update global best position
<- which.min(pbest_obj)
idx <- pbest[idx,]
gbest <- min(pbest_obj)
gbest_obj
# Visualize updated positions
<- X %>%
X_dir mutate(g_x = gbest[1,1],
g_y = gbest[1,2],
angle = atan((g_y - y)/(g_x - x))*180/pi,
angle = ifelse(g_x < x, 180 + angle, angle))
+
contour_plot geom_point(data = X, aes(x, y), color = "red", size = 2.5) +
geom_segment(data = X_dir,
aes(x = x, y = y,
xend = x + 0.5*cos(angle*pi/180),
yend = y + 0.5*sin(angle*pi/180)),
arrow = arrow(length = unit(0.1, "cm")),
color = "blue") +
labs(title = "Particles After First Update")
Complete PSO Implementation
Now let’s package everything into a reusable function:
<- function(obj_func, # Function to minimize
pso_optim c1 = 0.05, # Personal learning rate
c2 = 0.05, # Social learning rate
w = 0.8, # Inertia weight
n_particles = 20, # Swarm size
init_fact = 0.1, # Initial velocity factor
n_iter = 50 # Maximum iterations
){# Define search domain
<- seq(-5, 5, length.out = 100)
x <- seq(-5, 5, length.out = 100)
y
# Initialize particles
<- cbind(sample(x, n_particles, replace = FALSE),
X sample(y, n_particles, replace = FALSE))
<- matrix(runif(n_particles * 2) * init_fact, ncol = 2)
dX
# Initialize best positions
<- X
pbest <- obj_func(x = X[,1], y = X[,2])
pbest_obj <- pbest[which.min(pbest_obj),]
gbest <- min(pbest_obj)
gbest_obj
# Store positions for visualization
<- data.frame(X, iter = 0)
loc_df <- 1
iter
# Main optimization loop
while(iter < n_iter){
# Update velocities
<- w * dX +
dX *runif(1)*(pbest - X) +
c1*runif(1)*t(gbest - t(X))
c2
# Update positions
<- X + dX
X
# Evaluate and update best positions
<- obj_func(x = X[,1], y = X[,2])
obj <- which(obj <= pbest_obj)
idx <- X[idx,]
pbest[idx,] <- obj[idx]
pbest_obj[idx]
# Update global best
<- which.min(pbest_obj)
idx <- pbest[idx,]
gbest <- min(pbest_obj)
gbest_obj
# Store for visualization
<- iter + 1
iter <- rbind(loc_df, data.frame(X, iter = iter))
loc_df
}
return(list(X = loc_df,
obj = gbest_obj,
obj_loc = paste0(gbest, collapse = ",")))
}
Let’s test our optimizer on the Ackley function:
# Run the PSO algorithm
<- pso_optim(obj_func,
out c1 = 0.01, # Low personal influence
c2 = 0.05, # Moderate social influence
w = 0.5, # Medium inertia
n_particles = 50,
init_fact = 0.1,
n_iter = 200)
# Check the result (global minimum should be at (1,1))
$obj_loc out
[1] "0.999994436338726,1.00006666431293"
Visualizing the Swarm in Action
The real beauty of PSO is watching the particles converge on the solution:
# Create animation of the optimization process
ggplot(out$X) +
geom_contour(data = grid, aes(x = Var1, y = Var2, z = z), color = "black") +
geom_point(aes(X1, X2)) +
labs(x = "X", y = "Y") +
transition_time(iter) +
ease_aes("linear")
Fine-Tuning Your Swarm
The PSO algorithm’s behavior can be dramatically altered by adjusting three key parameters:
- Inertia Weight (w)
- High values (>0.8): Particles maintain momentum and explore widely
- Low values (<0.4): Particles slow down and focus on refining solutions
- Personal Learning Rate (c1)
- High values: Particles favor their own discoveries
- Low values: Particles ignore their history
- Social Learning Rate (c2)
- High values: Particles rush toward the global best
- Low values: Particles explore independently
Common parameter combinations: - Exploration focus: High w (0.9), balanced c1/c2 (0.5/0.5) - Exploitation focus: Low w (0.4), higher c2 than c1 (0.1/0.7)
Enhancing Your PSO Implementation
For real-world applications, consider these improvements:
- Add boundary constraints to keep particles within valid regions
- Implement adaptive parameters that change during optimization
- Add convergence-based stopping criteria
- Extend to higher dimensions for more complex problems
The R package pso
offers a production-ready implementation!