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 own thousands of CPUs and a gradient method cannot keep them all fed.
Evolution strategies, in the form Salimans and co-authors described in 2017, 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 swap 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 versus exploitation.
The one idea you need first: a smoothed objective. Instead of maximizing the raw return , ES maximizes its average under noise, . That average has a gradient you can sample with zero derivatives, which is the whole trick.
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 map 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 it 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 from running those weights, for the noise scale (a small positive number), and for one perturbation drawn from a standard Gaussian, . The smoothed objective has a gradient with 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. Perturb, score, step: that is the whole optimizer.
Why antithetic sampling halves the variance
Instead of independent perturbations, draw of them and evaluate both signs, and :
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 each worker scores its own perturbations alone. The Salimans design adds 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. The price is sample hunger: ES typically needs far more episodes than a gradient method to reach the same policy, so you pay it when gradients are unavailable or parallelism is nearly free.
Worked example: one antithetic step
Take a 2D toy with return , best at with value . Start at (return ), set and step size , and fix two draws and .
Score the four antithetic copies:
- , return
- , return
- , return
- , return
Pair contrasts: and . The estimate with pairs is:
Step once: . The new return is , up from . The estimate points the same way as the true gradient (both entries positive) 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. It scores the four antithetic copies, forms the estimate, and takes one step.
def toy_return(theta): """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, perturbations, sigma, step_size): """One antithetic ES update: score plus/minus sigma times e, then step.""" pairs = [] for e in perturbations: plus = toy_return([theta[0] + sigma * e[0], theta[1] + sigma * e[1]]) minus = toy_return([theta[0] - sigma * e[0], theta[1] - sigma * e[1]]) pairs.append((plus, minus)) 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 pairs, gradient, updated
theta0 = [0.5, -0.3]pairs, grad, theta1 = es_step(theta0, [[1.0, 0.5], [-0.4, 0.8]], sigma=0.1, step_size=0.05)
print([round(v, 4) for v in pairs[0]]) # -> [3.715, 3.195]print([round(v, 4) for v in pairs[1]]) # -> [3.6716, 3.2396]print([round(v, 3) for v in grad]) # -> [0.868, 1.514]print([round(v, 4) for v in theta1]) # -> [0.5434, -0.2243]print(round(toy_return(theta0), 3), round(toy_return(theta1), 3)) # -> 3.47 3.742Watch Out For
Sigma set without looking at the return gap
The noise scale decides whether the plus/minus pair carries any signal. Too large, and the perturbations land in unrelated parts of the return surface: scores 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 repeating the exact same weights twice, raise . If single perturbations move 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 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 swap 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.