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.
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 , each scout's walk is a perturbation , the shouted altitude is the episode return , 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 needs tuning.
How It Actually Works
Write for the policy weights (a flat vector with entries), for the total episode return earned by running the policy with those weights, for the noise scale (a small positive number), and for one perturbation drawn from a standard Gaussian, . Smoothing the return with noise gives , whose gradient has a form you can sample without any derivative:
Each term pushes along in proportion to the return that perturbation earned. Then , with the step size. That is the whole optimizer: perturb, score, step.
Why antithetic sampling halves the variance
Instead of independent perturbations, ES draws of them and evaluates both signs, and . The estimator becomes:
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 episode scores for 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 is replaced by a shaped utility computed from ranks. Sort the scores, map the best to a positive utility and the worst to a negative one (centered so the utilities sum to zero), and use in place of . 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 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 , best at with value . Start at (return ), set and step size , and fix two perturbation draws and .
Score the four antithetic copies:
- , return
- , return
- , return
- , return
Pair contrasts: and . The gradient estimate with pairs is:
Step once: . The new return is , up from . Notice the estimate points the same way as the true gradient (both entries positive) but with sampling noise on the magnitudes: that is the ES bargain in miniature. With only ranks kept, the ordering 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.742Watch Out For
Sigma set without looking at the return gap
The noise scale decides whether the 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 and differ by less than the episode-to-episode noise, so the update chases randomness and learning stalls while looking busy.
The fix: scale to your weights (a starting point near a hundredth to a tenth of the typical weight magnitude), then watch one diagnostic, the average over pairs. If the gap is indistinguishable from the noise you see when repeating the exact same weights twice, raise . 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 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 and 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 and 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.