Skip to content
AI360Xpert
Beta

Evolution Strategies (ES)

Evolution strategies tune a policy without backpropagation: they shake the weights with Gaussian noise, score each shaky copy, and step toward the noise that scored best.

Evolution strategies score Gaussian perturbations of the policy weights in parallel and step toward the perturbations with the highest returns.
Evolution strategies score Gaussian perturbations of the policy weights in parallel and step toward the perturbations with the highest returns.

Why Does This Exist?

Most policy optimization leans on gradients. You run stochastic gradient descent in RL, which needs a differentiable loss, backpropagation through the policy, and careful handling of moving bootstrap targets. That machinery breaks down when the policy is not differentiable (a simulator with discrete switches, a programmatic controller), when rewards arrive only at the end of a long episode, or when you have thousands of CPUs and a gradient method that cannot keep them all fed.

Evolution strategies (ES), in the form Salimans et al. described in 2017 ("Evolution Strategies as a Scalable Alternative to Reinforcement Learning"), take a different deal. Forget derivatives entirely: treat the whole environment as a black box that maps a weight vector to one number, the episode return. Shake the weights with Gaussian noise, run one episode per shaky copy, and move the weights toward the noise that scored well. Each episode is independent, so the work splits across as many parallel workers as you own, and workers exchange only random seeds plus scalar returns instead of gradients or weight vectors.

This page covers the Gaussian-perturbation estimator, the two variance tricks that make it practical (antithetic sampling and rank shaping), and why it scales. For the gradient-based counterpart, read policy gradient methods. For how ES keeps searching instead of collapsing early, see exploration vs. exploitation.

Think of It Like This

The Foggy-Hill Scout Team

You are camped on a foggy hill and want the summit, but you cannot see more than a few steps. Each morning you send out four scouts, each walking a short fixed distance in a random compass direction, then shouting back their altitude. You move camp a small step toward the average of the scouts' directions, weighted by how high each one got.

The mapping is exact: your camp is the weight vector θ\theta, each scout's walk is a perturbation σϵ\sigma \epsilon, the shouted altitude is the episode return FF, and the camp move is the update. You also send scouts in opposite pairs (one northeast, one southwest), so a slope that lifts one and drops the other tells you the direction twice as clearly: that is antithetic sampling. And if one scout stumbles onto a helicopter pad with a silly altitude reading, you rank the reports instead of trusting raw numbers: that is fitness shaping.

Where the analogy stops: your hill has two or three directions, while a policy has thousands of weights, so ES needs far more scouts than four. Real scouts also report exact altitudes, while episode returns are noisy, which is why the step size stays small and the noise scale σ\sigma needs tuning.

How It Actually Works

Write θ\theta for the policy weights (a flat vector with dd entries), F(θ)F(\theta) for the total episode return earned by running the policy with those weights, σ\sigma for the noise scale (a small positive number), and ϵ\epsilon for one perturbation drawn from a standard Gaussian, ϵ∼N(0,I)\epsilon \sim \mathcal{N}(0, I). Smoothing the return with noise gives J(θ)=E[F(θ+σϵ)]J(\theta) = \mathbb{E}[F(\theta + \sigma \epsilon)], whose gradient has a form you can sample without any derivative:

g=1nσ∑i=1nF(θ+σϵi) ϵig = \frac{1}{n \sigma} \sum_{i=1}^{n} F(\theta + \sigma \epsilon_i) \, \epsilon_i

Each term pushes θ\theta along ϵi\epsilon_i in proportion to the return that perturbation earned. Then θ←θ+αg\theta \leftarrow \theta + \alpha g, with α\alpha the step size. That is the whole optimizer: perturb, score, step.

Why antithetic sampling halves the variance

Instead of nn independent perturbations, ES draws mm of them and evaluates both signs, θ+σϵi\theta + \sigma \epsilon_i and θ−σϵi\theta - \sigma \epsilon_i. The estimator becomes:

g=12mσ∑i=1m(F(θ+σϵi)−F(θ−σϵi)) ϵig = \frac{1}{2 m \sigma} \sum_{i=1}^{m} \big(F(\theta + \sigma \epsilon_i) - F(\theta - \sigma \epsilon_i)\big) \, \epsilon_i

The difference cancels any baseline shift shared by the pair, so a region where every return is uniformly high or low contributes no noise. You get 2m2m episode scores for mm random draws, and each pair votes on its axis with the contrast between its two ends.

Why rank shaping tames wild returns

Raw returns are badly behaved: one lucky episode can outscore everything else combined, and adding a constant to all rewards should change nothing but would rescale the step. So in practice FF is replaced by a shaped utility uu computed from ranks. Sort the 2m2m scores, map the best to a positive utility and the worst to a negative one (centered so the utilities sum to zero), and use uu in place of FF. The update then depends only on ordering, never on reward scale, offsets, or outliers.

Why it scales to thousands of workers

Episodes never interact, so worker kk can score its own perturbations alone. The Salimans design goes one step further with a shared noise table: every worker holds the same pre-generated Gaussian table and indexes into it with a seed, so workers broadcast only (seed, scalar return) pairs. Communication per episode stays tiny while the population grows into the thousands, which is how ES rivalled then-standard policy-gradient methods on locomotion and game benchmarks despite using no gradients at all. The price is sample hunger: ES typically needs far more episodes than a gradient method to reach the same policy, so you only pay it when gradients are unavailable or parallelism is nearly free.

Worked numerical example

Take a 2D toy with return F(θ)=−(θ1−1)2−2(θ2−0.5)2+5F(\theta) = -(\theta_1 - 1)^2 - 2(\theta_2 - 0.5)^2 + 5, best at [1.0,0.5][1.0, 0.5] with value 5.05.0. Start at θ0=[0.5,−0.3]\theta_0 = [0.5, -0.3] (return 3.4703.470), set σ=0.1\sigma = 0.1 and step size α=0.05\alpha = 0.05, and fix two perturbation draws ϵ1=[1.0,0.5]\epsilon_1 = [1.0, 0.5] and ϵ2=[−0.4,0.8]\epsilon_2 = [-0.4, 0.8].

Score the four antithetic copies:

  • θ0+0.1 ϵ1=[0.6,−0.25]\theta_0 + 0.1 \, \epsilon_1 = [0.6, -0.25], return 3.7153.715
  • θ0−0.1 ϵ1=[0.4,−0.35]\theta_0 - 0.1 \, \epsilon_1 = [0.4, -0.35], return 3.1953.195
  • θ0+0.1 ϵ2=[0.46,−0.22]\theta_0 + 0.1 \, \epsilon_2 = [0.46, -0.22], return 3.67163.6716
  • θ0−0.1 ϵ2=[0.54,−0.38]\theta_0 - 0.1 \, \epsilon_2 = [0.54, -0.38], return 3.23963.2396

Pair contrasts: d1=3.715−3.195=0.520d_1 = 3.715 - 3.195 = 0.520 and d2=3.6716−3.2396=0.432d_2 = 3.6716 - 3.2396 = 0.432. The gradient estimate with m=2m = 2 pairs is:

g=12×2×0.1(0.520 ϵ1+0.432 ϵ2)=2.5×[0.3472,0.6056]=[0.868,1.514]g = \frac{1}{2 \times 2 \times 0.1} \big(0.520 \, \epsilon_1 + 0.432 \, \epsilon_2\big) = 2.5 \times [0.3472, 0.6056] = [0.868, 1.514]

Step once: θ1=[0.5,−0.3]+0.05×[0.868,1.514]=[0.5434,−0.2243]\theta_1 = [0.5, -0.3] + 0.05 \times [0.868, 1.514] = [0.5434, -0.2243]. The new return is 3.7423.742, up from 3.4703.470. Notice the estimate points the same way as the true gradient [1.0,3.2][1.0, 3.2] (both entries positive) but with sampling noise on the magnitudes: that is the ES bargain in miniature. With only ranks kept, the ordering F1+>F2+>F2−>F1−F_1^+ > F_2^+ > F_2^- > F_1^- alone would drive the same directional vote.

Code

A pure-Python transcription of the worked example above. It scores the four antithetic copies, forms the estimate, and takes one step.

def toy_return(theta: list[float]) -> float:    """Quadratic toy return. Best at [1.0, 0.5], where it equals 5.0."""    return -((theta[0] - 1.0) ** 2) - 2.0 * ((theta[1] - 0.5) ** 2) + 5.0

def es_step(    theta: list[float],    perturbations: list[list[float]],    sigma: float,    step_size: float,) -> tuple[list[float], list[float]]:    """One antithetic ES update: score +sigma*e and -sigma*e, then step."""    pairs = [        (            toy_return([theta[0] + sigma * e[0], theta[1] + sigma * e[1]]),            toy_return([theta[0] - sigma * e[0], theta[1] - sigma * e[1]]),        )        for e in perturbations    ]    gradient = [        sum((plus - minus) * e[i] for (plus, minus), e in zip(pairs, perturbations))        / (2.0 * len(perturbations) * sigma)        for i in range(2)    ]    updated = [theta[i] + step_size * gradient[i] for i in range(2)]    return updated, gradient

theta0 = [0.5, -0.3]new_theta, grad = es_step(theta0, [[1.0, 0.5], [-0.4, 0.8]], sigma=0.1, step_size=0.05)print([round(v, 3) for v in grad])       # -> [0.868, 1.514]print([round(v, 4) for v in new_theta])  # -> [0.5434, -0.2243]print(round(toy_return(theta0), 3), round(toy_return(new_theta), 3))  # -> 3.47 3.742

Watch Out For

Sigma set without looking at the return gap

The noise scale σ\sigma decides whether the ±\pm pair carries any signal. Too large, and the perturbations land in unrelated parts of the landscape: returns swing wildly, the estimate is mostly noise, and the policy thrashes instead of climbing. Too small, and F(θ+σϵ)F(\theta + \sigma \epsilon) and F(θ−σϵ)F(\theta - \sigma \epsilon) differ by less than the episode-to-episode noise, so the update chases randomness and learning stalls while looking busy.

The fix: scale σ\sigma to your weights (a starting point near a hundredth to a tenth of the typical weight magnitude), then watch one diagnostic, the average ∣F+−F−∣|F^+ - F^-| over pairs. If the gap is indistinguishable from the noise you see when repeating the exact same weights twice, raise σ\sigma. If single perturbations change returns by a large fraction of the total return range, lower it.

Unreproducible runs from unshared noise

ES runs are only reproducible if you can replay the exact perturbations. The classic failure: each worker samples its own noise from an unseeded generator, the run improves, and nobody can reconstruct which ϵ\epsilon produced the winning update, so the result cannot be audited or debugged. A subtler variant logs the returns but not the seeds.

The fix, borrowed from the original large-scale design: generate one shared noise table up front, hand workers (seed index, return) pairs, and log the table seed plus σ\sigma and α\alpha with every experiment. Then any single perturbation is exactly replayable from its index, and a rerun with the same table seed reproduces the whole optimization path.

The Quick Version

  • Evolution strategies optimize a policy with no derivatives: they score Gaussian perturbations of the weights and step toward the noise that earned the best returns.
  • The antithetic estimator scores θ+σϵ\theta + \sigma \epsilon and θ−σϵ\theta - \sigma \epsilon per draw, so shared baseline shifts cancel and each pair votes with its contrast.
  • Rank shaping replaces raw returns with centered rank utilities, making the update immune to reward scale, offsets, and one lucky outlier episode.
  • Workers share a noise table and exchange only seeds plus scalar returns, which lets ES use thousands of parallel CPUs with tiny communication.
  • The trade is sample hunger: ES needs far more episodes than gradient methods, so reach for it when the policy is non-differentiable or parallelism is nearly free.