Evolution Strategies (ES) are a class of stochastic, population-based optimization algorithms inspired by natural evolution. They are "black-box" optimizers, meaning they treat the objective function as a mystery: they don't require gradients or knowledge of the function’s internal structure—they simply observe the output for a given input.
Mathematically, we seek to minimize an objective function $f(\mathbf{x})$ (in general a non-linear, non-convex function) with respect to a real-parameter vector $\mathbf{x} \in \mathbb{R}^n$ (continuous domain). $$f: \mathbb R^n \rightarrow \mathbb R$$ Typically, the global optimum is often neither feasible (NP-hard) nor relevant in practice. Therefore, we want to find $\mathbf x$-values where $f(\mathbf{x})$ is as small as possible.
Here, we just consider the family of Estimation of Distribution Algorithms. Unlike traditional Genetic Algorithms, which evolve a population of encoded individuals (genotypes) with genetic operators such as crossover and bit-flip mutation, an EDA maintains a probabilistic model of the search space. In each generation, it "estimates" where the best solutions lie and updates its parameters accordingly.
General Framework
Initialization: Choose $\theta^{(0)}$
The optimization follows a repetitive four-step cycle:
The software component solver encapsulates the algorithmic engine that maintains a probabilistic model of the search space, proposing candidate solutions through sampling and iteratively refining its internal parameters based on fitness feedback.
Before looking at a specific example, we must define how the algorithm chooses which solutions "win" and how it uses them to move forward. In ES literature, we use two Greek letters to describe the population:
There are two primary ways to handle the transition between generations:
While these discrete selection methods were the standard in early Evolution Strategies, modern ES has evolved toward Recombination. Instead of treating the $\mu$ survivors as "individuals" that simply live to the next round, modern strategies treat them as statistical data points. We don't just "pick" the winners; we recombine them to update the center of our search distribution.
In a strategy where $\mu > 1$, we have multiple survivors. Instead of having each survivor start its own independent family, we merge their "knowledge." This process is called Recombination.
In Evolution Strategies, we use Intermediate Recombination. We calculate a single "Center of Mass" from all $\mu$ survivors. This new center becomes the mean $\mathbf{m}^{(g+1)}$ for the entire next generation. Mathematically: $$\mathbf{m}^{(g+1)} = \sum_{i=1}^{\mu} w_i \mathbf{x}_{i:\lambda}$$ Where:
How much influence should each survivor have? There are two main approaches:
| Strategy | Notation | Description |
|---|---|---|
| Equal Weights | $(\mu / \mu, \lambda)$ | All $\mu$ survivors contribute equally ($w_i = 1/\mu$). The new mean is a simple average. |
| Weighted Recombination | $(\mu / \mu_w, \lambda)$ | The best individual has the highest weight, the second-best slightly less, and so on. |
"Weighted Recombination" is explained in detail later. Let's look at an example before.
Recombination is generally more robust than selecting a single winner because it provides:
Mutation is the process of adding stochastic (random) noise to the parameters to explore the search space. In Evolution Strategies, this is typically done using a Gaussian (Normal) Distribution.
In this context, the state of our search is defined by the parameter set $\theta$. In the most general case, $\theta$ is a composite of three elements: $$\theta = \{ \mathbf{m}, \sigma, \mathbf{C} \}$$
The standard mutation for an individual $i$ is the realization of the current model:
$$\mathbf{x}_i = \mathbf{m} + \sigma \cdot \mathcal{N}(0, \mathbf{C})$$ Where:
In many basic Evolution Strategies, we do not yet try to learn the correlations between variables. If we fix the covariance matrix as the Identity matrix ($\mathbf{C} = \mathbf{I}$), our distribution remains perfectly spherical (isotropic). In this case, our state parameters simplify to:
$$\theta = \{ \mathbf{m}, \sigma \}$$
The "intelligence" of the algorithm resides in the update function $F_\theta$. It takes the results from the current generation and calculates the parameters for the next. For the simplified case where $\mathbf{C} = \mathbf{I}$, the function $F_\theta$ is composed of two distinct parts:
Now, let's visualize this. Imagine we are searching for the minimum of a function $f(\mathbf{x})$. We use a $(2/2, 10)$ strategy: we create 10 offspring, and the top 2 are averaged to find the next center.
Note: for clarity this example keeps the step-size $\sigma$ fixed. Adapting $\sigma$ is introduced in the "Step-Size Control" section below — with a constant $\sigma$ the search settles near the target but cannot tighten onto it.
import numpy as np
import matplotlib.pyplot as plt
# 1. Setup Parameters
mu_size = 2 # Number of parents to recombine
lambda_size = 10 # Total offspring per generation
sigma = 0.5
target = np.array([2.0, -3.0])
# Initialize a SINGLE mean (the center of the distribution)
m = np.array([-4.0, 4.0])
def evaluate(x):
return np.sum((x - target)**2)
# Grid for the landscape visualization
x_grid = np.linspace(-6, 6, 100)
y_grid = np.linspace(-6, 6, 100)
X, Y = np.meshgrid(x_grid, y_grid)
Z = (X - target[0])**2 + (Y - target[1])**2
# Visualization setup
gens_to_plot = [0, 2, 9, 14]
fig, axes = plt.subplots(2, 2, figsize=(12, 11))
axes = axes.flatten()
def plot_generation(ax, gen, target, offspring, current_mean, winners, new_mean, is_first):
ax.contourf(X, Y, Z, levels=25, cmap='viridis', alpha=0.2)
ax.scatter(target[0], target[1], c='green', marker='*', s=200, label='Target', zorder=5)
# Plot all offspring (The "Cloud")
ax.scatter(offspring[:, 0], offspring[:, 1], c='blue', alpha=0.4, s=20, label='Offspring')
# Plot current mean (The Center)
ax.scatter(current_mean[0], current_mean[1], edgecolors='blue', marker='o',
facecolors='none', s=100, linewidth=2, label='Current Mean (m)')
# Plot the survivors (The best mu)
ax.scatter(winners[:, 0], winners[:, 1], c='red', marker='x', s=80, label='Best mu=2')
# Plot the resulting new mean (The average of survivors)
ax.scatter(new_mean[0], new_mean[1], c='red', marker='P', s=100, label='New Mean (m_next)', zorder=6)
ax.set_title(f"Generation {gen}")
ax.set_aspect('equal')
if is_first: ax.legend(loc='upper right', fontsize='8')
# 2. The Evolution Loop (Ask-Tell)
plot_idx = 0
for gen in range(15):
# --- ASK ---
# All offspring are sampled from the same mean m
noise = np.random.standard_normal((lambda_size, 2))
offspring = m + sigma * noise
# --- EVALUATE ---
costs = np.array([evaluate(child) for child in offspring])
# --- TELL ---
# Sort by fitness and pick the mu_size best
best_indices = np.argsort(costs)[:mu_size]
winners = offspring[best_indices]
# RECOMBINATION: Calculate the new mean (Intermediate Recombination)
# Using equal weights (1/mu) for this example
m_next = np.mean(winners, axis=0)
# Snapshot plotting
if gen in gens_to_plot:
plot_generation(axes[plot_idx], gen, target, offspring, m, winners, m_next, plot_idx == 0)
plot_idx += 1
# UPDATE parameter for next generation
m = m_next
plt.tight_layout()
plt.show()
When the solver performs the Tell & Update step, it must decide how much to trust each survivor. While equal weights are simple, they treat the "champion" and the "borderline survivor" as equals.
A common choice in modern ES (and the default in CMA-ES) is logarithmic weighting. This strategy penalizes lower-ranked individuals more harshly than a linear drop-off would. It ensures the "champion" has a significantly stronger influence on the new mean $\mathbf{m}^{(g+1)}$ than the mediocre survivors at the bottom of the top-$\mu$ list.
The raw weights $w'$ are calculated as:
$$w'_i = \ln(\mu + 0.5) - \ln(i) \quad \text{for } i = 1 \dots \mu$$
Normalization:
To ensure the weights sum to 1, we normalize the raw values:
$$w_i = \frac{w'_i}{\sum_{j=1}^{\mu} w'_j}$$
Example Weight Calculation ($\mu=5$)
In this example, we use the common logarithmic weighting scheme: $w'_i = \ln(\mu + 0.5) - \ln(i)$.
| Rank ($i$) | Formula: $\ln(5.5) - \ln(i)$ | Raw Weight ($w'$) | Normalized Weight ($w_i$) |
|---|---|---|---|
| 1 (Best) | $\ln(5.5) - \ln(1)$ | $1.7047$ | 0.456 |
| 2 | $\ln(5.5) - \ln(2)$ | $1.0116$ | 0.271 |
| 3 | $\ln(5.5) - \ln(3)$ | $0.6061$ | 0.162 |
| 4 | $\ln(5.5) - \ln(4)$ | $0.3185$ | 0.085 |
| 5 | $\ln(5.5) - \ln(5)$ | $0.0953$ | 0.026 |
| Sum | 3.7362 | 1.000 |
Notice that the Best individual has about 18 times the influence of the 5th individual. This aggressive weighting allows the search center to shift decisively toward the most promising direction while still benefiting from the "smoothing" effect of the other survivors.
One of the most critical challenges in ES is determining the value of $\sigma$ (mutation rate).
Step-size control allows the algorithm to automatically adapt $\sigma$ during the search. This is often called Meta-Evolution because the algorithm is evolving not just the solution, but also the parameters of the search itself.
One of the oldest and most intuitive methods for controlling step size is the 1/5th Success Rule. It is based on the observation that if more than 20% of mutations are successful (better than the parent), the search is likely in a smooth area and should move faster.
| Observation | Action | Logic |
|---|---|---|
| Success rate > 1/5 | Increase $\sigma = \sigma / 0.85$ ($\approx 1.17$) | We are finding better points easily; we should explore further and "sprint." |
| Success rate < 1/5 | Decrease $\sigma = \sigma * 0.85$ | We are missing the "good" areas; we should search more locally to "settle." |
Why $0.85$ for increase/decrease?
Modern algorithms like CMA-ES (later in this course) use Evolution Paths. Instead of just looking at the current generation, they track the direction the mean has moved over several generations.
| Scenario | Movement Pattern | Adjustment | Goal |
|---|---|---|---|
| Sprinting | Multiple generations move in the same direction. | Increase $\sigma$ | Reach the neighborhood of the optimum faster. |
| Zig-Zagging | Generations jump back and forth across a valley. | Decrease $\sigma$ | Increase precision and "settle" into the minimum. |
| Stagnation | No offspring are better than the parent. | Increase $\sigma$ | Break out of a local optimum or flat region. |
So far our search cloud has been isotropic: with $\mathbf{C} = \mathbf{I}$, the offspring $\mathbf{x} = \mathbf{m} + \sigma\,\mathcal{N}(0, \mathbf{I})$ scatter equally in all directions — a circle (in 2D) or a sphere (in $n$D). This works well when the landscape looks the same in every direction near the optimum.
Real objective functions rarely do. Consider a long, narrow, tilted valley — the contour lines are stretched, rotated ellipses (a high condition number). A circular cloud now faces a dilemma:
The circle simply does not match the shape of the problem, and most samples land in unpromising directions. The figure below contrasts an isotropic cloud with one that has been reshaped to fit the valley.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
rng = np.random.default_rng(0)
# A tilted, elongated quadratic valley: f(x) = x^T A x, minimum at the origin.
theta = np.deg2rad(35)
Rrot = np.array([[np.cos(theta), -np.sin(theta)],
[np.sin(theta), np.cos(theta)]])
A = Rrot @ np.diag([1.0, 12.0]) @ Rrot.T # curvature ratio 12:1
def f(X, Y):
return A[0, 0]*X**2 + (A[0, 1] + A[1, 0])*X*Y + A[1, 1]*Y**2
# Put the mean ON the valley floor (the low-curvature eigen-direction), away from
# the target, so the search must travel along the valley to reach the optimum.
floor_dir = Rrot @ np.array([1.0, 0.0]) # eigenvector with the small eigenvalue
m = 4.5 * floor_dir
sigma = 0.9
# Two search shapes, normalized to the SAME area (det = 1) so we compare
# orientation / shape only, not overall size.
C_iso = np.eye(2)
C_ada = np.linalg.inv(A) # optimal shape ~ inverse Hessian
C_ada /= np.sqrt(np.linalg.det(C_ada)) # area-normalize -> det = 1
noise = rng.standard_normal((50, 2))
lim = 7.0
xg = yg = np.linspace(-lim, lim, 200)
Xg, Yg = np.meshgrid(xg, yg)
Z = f(Xg, Yg)
fig, axes = plt.subplots(1, 2, figsize=(13, 6))
for ax, C, title in [(axes[0], C_iso, "Isotropic N(0, I): circular"),
(axes[1], C_ada, "Adapted N(0, C): aligned to the valley")]:
ax.contourf(Xg, Yg, np.log1p(Z), levels=30, cmap='viridis', alpha=0.35) # log scale reveals the narrow floor
L = np.linalg.cholesky(C)
pts = m + sigma * noise @ L.T
ax.scatter(pts[:, 0], pts[:, 1], c='red', s=16, alpha=0.6, label='Offspring')
ax.scatter(*m, c='black', marker='o', s=60, zorder=6, label='Mean m')
ax.scatter(0, 0, c='lime', marker='*', s=250, zorder=6, label='Target (optimum)')
# The 2-sigma search ellipse = the actual shape of N(0, sigma^2 C)
vals, vecs = np.linalg.eigh(C) # ascending eigenvalues
angle = np.degrees(np.arctan2(vecs[1, -1], vecs[0, -1])) # major-axis direction
width, height = 2 * 2.0 * sigma * np.sqrt(vals[::-1]) # full axis lengths at 2 sigma
ax.add_patch(Ellipse(m, width, height, angle=angle, edgecolor='blue',
fc='none', lw=2, ls='--', label=r'Search area (2$\sigma$)'))
ax.set_title(title); ax.set_aspect('equal')
ax.set_xlim(-lim, lim); ax.set_ylim(-lim, lim)
ax.legend(loc='upper right', fontsize=8)
plt.tight_layout(); plt.show()
The right-hand cloud is clearly better matched to the valley: it takes large steps along the long direction and small, careful steps across the narrow one. That elongated, rotated ellipse is nothing more than the geometry of $\mathcal{N}(0, \mathbf{C})$ — the covariance matrix $\mathbf{C}$ stretches (through its eigenvalues) and rotates (through its eigenvectors) the search.
But we don't know the valley's shape in advance — that is the whole point of a black-box optimizer. The key insight of CMA-ES: the successful offspring reveal it for us. If the winners of a generation cluster along a particular direction, that direction is promising — so we adapt $\mathbf{C}$ toward the covariance of the selected winners (a small, incremental update each generation rather than a full reset) and sample the next generation from the updated shape. The cloud gradually molds itself to the local landscape, while $\sigma$ remains the global scale on top of it.
This closes the arc of the course. The same covariance matrix you have already met reappears here as the engine of the search:
In one paragraph — what is the Hessian $\mathbf{H}$? It is the matrix of second derivatives of $f$; it measures the local curvature, i.e. how fast the slope changes as you move. For an elliptical valley its eigenvectors point along the ellipse's axes and its eigenvalues are the curvatures — a large eigenvalue means a sharply-curving, narrow direction, a small one a gently-curving, long direction. The inverse $\mathbf{H}^{-1}$ is therefore large along the long directions and small along the narrow ones: exactly the shape the search cloud should have.
And what is Newton's method? It is the classical fast optimizer that exploits this curvature. Plain gradient descent just steps along the slope (the gradient), so it zig-zags in a narrow valley. Newton's method instead multiplies the gradient by $\mathbf{H}^{-1}$, which rescales every step by the local curvature — large along the gently-curving directions, small across the sharply-curving ones — and so heads almost straight to the minimum. Learning $\mathbf{C} \approx \mathbf{H}^{-1}$ gives CMA-ES this very same step-rescaling, but without any gradient or derivative: it reads the shape off the ranking of the sampled points alone (a "second-order" behaviour achieved by a black-box method).
And conjugate directions? Two directions are conjugate (with respect to the curvature) if optimizing along one does not undo the progress already made along the other. Searching along such non-interfering directions avoids the slow zig-zag that plain gradient descent shows in a narrow valley; CMA-ES's step-size path (CSA) steers the search toward them for the same reason.
For the full derivations see Hessian-matrix and conjugate-directions.
Basic ES fixes $\mathbf{C} = \mathbf{I}$ and only adapts $\mathbf{m}$ and $\sigma$. CMA-ES adds the missing ingredient — it learns $\mathbf{C}$ — which is why it is the state-of-the-art black-box optimizer.
Learning the full covariance matrix is powerful, but it is not free. Since $\mathbf{C}$ is a symmetric $n \times n$ matrix, it has $\tfrac{n(n+1)}{2} = \mathcal{O}(n^2)$ free parameters. Storing it costs $\mathcal{O}(n^2)$ memory, and — more importantly — learning that many numbers reliably takes on the order of $\mathcal{O}(n^2)$ function evaluations. For small or moderate dimension $n$ this is well spent; in high dimensions it becomes prohibitive.
The remedy is to restrict the shape of $\mathbf{C}$ to fewer parameters, trading generality for speed:
Diagonal covariance — keep only the $n$ diagonal entries of $\mathbf{C}$ (one variance per coordinate; $\mathcal{O}(n)$ parameters). The search ellipsoid can then stretch along the coordinate axes but not rotate. This learns roughly $n$ times faster and is the right choice for separable or nearly-separable problems — those where the variables are only weakly coupled, so the promising directions already line up with the coordinate axes. (This is the idea behind separable variants such as sep-CMA-ES.)
Low-rank + diagonal covariance — represent $\mathbf{C}$ as a diagonal plus a few $k \ll n$ dominant directions ($\mathcal{O}(n\,k)$ parameters). This captures the most important correlated directions while ignoring the rest. It suits very high-dimensional problems with a low effective dimensionality — where the difficulty is concentrated in a handful of coupled directions rather than spread across all $\binom{n}{2}$ variable pairs. (This is the idea behind limited-memory variants such as LM-CMA and VkD-CMA.)
Rule of thumb: use the full $\mathbf{C}$ for moderate $n$, or whenever correlations are strong or unknown (it is the robust default and cannot be hurt by correlations); use a diagonal model for separable / weakly-correlated problems; use a low-rank + diagonal model in very high dimension when only a few directions truly matter.
| Principle | Basic Evolution Strategy | CMA-ES (The Next Step) |
|---|---|---|
| Mutation | Circular: Uses an Identity Matrix. All directions are explored with equal probability. | Ellipsoidal: Learns a Covariance Matrix ($C$) to "stretch" and "rotate" the search toward the goal. |
| Recombination | Weighted Mean: The center of the next generation is the average of the survivors. | Strategic Mean: The mean update is used to calculate the "Evolution Path" and the new matrix. |
| Step-Size Control | 1/5th Success Rule: Reactive adjustment based on the ratio of "better" offspring. | Cumulative Step Adaptation (CSA): Analyzes the search path to detect "sprinting" vs. "zig-zagging." |
| Selection | Rank-based $(\mu/\mu_w, \lambda)$: Only the fitness rank matters, not the absolute values. | Same Logic: Remains a robust, gradient-free black-box optimizer. |