Evolutionary Methods and Neuroevolution
Instead of computing gradients, keep a population of policies, score each one, and breed the winners with small random tweaks until behavior improves.
Why Does This Exist?
Gradients need a slope to follow. In reinforcement learning that slope often does not exist. The reward can be sparse, like a game that pays only after hundreds of correct moves. It can be deceptive, where the nearby improvement points away from the real goal. Or the policy itself can be non-differentiable, like a discrete rule set, a compiled simulator, or a physical robot you cannot backpropagate through.
Policy gradient methods handle continuous actions by estimating a gradient from returns, and deep reinforcement learning adds neural networks that learn features from raw input. Both still assume you can nudge parameters in the direction of higher return and get a useful signal. When the reward stays flat for long stretches, that nudge carries no information and learning stalls. That flat reward search is the same problem behind exploration versus exploitation: you must try far-apart behaviors before you can refine any of them.
Evolution takes a different deal. Do not compute a derivative at all. Keep a population of full policies, run each one, score it with episode return, keep the winners, mix them, add random tweaks, and repeat. It works with any reward function and any policy form because it only looks at scores, never at slopes.
This page teaches that shared umbrella loop: selection, crossover, and mutation, plus when to reach for it. It also sketches the three specialized children so you can tell them apart: Evolution Strategies, which samples Gaussian noise around a mean policy; CMA-ES, which adapts the shape of that sampling distribution as it learns; and NEAT, which grows both network weights and network topology starting from small nets with speciation to protect new ideas. Those three have no dedicated pages in this change, so they get summaries here and no links.
Think of It Like This
Saving seeds from your best tomato plants
You plant four tomato varieties in spring. At harvest you taste each row and weigh the yield. You save seeds only from the two tastiest, most productive rows. Next spring you plant those saved seeds, cross two of the best plants to mix traits, and accept that a little natural variation will show up on its own. You also replant your single best row unchanged, in case the new mixes flop.
The map is direct. The four rows are the population. Taste plus yield is fitness, the episode return. Saving only two rows is truncation selection. Mixing two plants is crossover. Natural variation is mutation. Replanting the best row unchanged is elitism.
Where the analogy stops: your garden gives you one harvest per year with weather noise you cannot control. Here you choose the fitness function, usually total episode return, and you choose the tweak size, usually a Gaussian spread you can shrink over time. You also test every candidate from the same start, which no garden lets you do.
How It Actually Works
Call the policy weights . That is one flat vector holding every number the policy needs, whether it is four scalars or four million network weights. Call fitness . That is the score you get after running the policy, usually total episode return. Call the population size, here . Call the number of survivors you breed from, here . Call the mutation spread, here , the standard deviation of the Gaussian noise you add.
One generation runs five steps:
- Start with weight vectors.
- Evaluate each one for one or more full episodes and record .
- Select the top scorers and discard the rest.
- Vary the survivors by crossover, often a plain average, plus mutation, usually fresh samples from added to each weight.
- Replace the old population with the children, keeping the single best parent unchanged, and repeat.
No gradient appears anywhere. The only signal is the ranking of scores.
Three flavors you will meet
Evolution Strategies keeps a mean vector and samples children as with drawn from a standard Gaussian. It then moves toward the noise directions that scored well. OpenAI popularized this form for RL because shared random seeds let thousands of workers generate the same perturbations without sending big weight vectors around.
CMA-ES keeps the same sampling idea but also learns the covariance of the noise, so sampling becomes . When one direction keeps paying off, the distribution stretches along it. That helps on stretched, ill-conditioned problems where a round ball wastes samples, at the price of more bookkeeping per step.
NEAT evolves the network itself, not just its weights. It starts from small nets with no hidden units, adds nodes and links by mutation, tracks every new gene with a historical marking so two nets can be crossed over sensibly, and groups similar topologies into species that share fitness so a promising new shape is not killed before its weights catch up.
Worked example: selecting from four policies
Take four one-weight policies with these measured returns:
- ,
- ,
- ,
- ,
Total fitness is . Mean fitness is . Ranked best to worst: at 35, at 22, at 15, at 8. Truncation with keeps and and drops the rest. The cut sits at 22, so anything below 22 is gone.
Crossover averages the two parents:
Now apply the fixed mutation draws for teaching, all consistent with :
- Crossover child:
- Copy of best parent:
- Copy of second parent:
With elitism the next generation is:
- , elite copy of , fitness 35 carried
- , crossover plus mutation
- , best parent mutated
- , second parent mutated
Those four weights cluster near the two winners, 0.3 and 0.9, which is exactly what you want after one step. None of the children has a score yet. The next step must run four fresh episodes to score them, which is why the diagram closes the loop back to the population and flags the cost at the bottom.
Code
from typing import List, Tuple
weights: List[float] = [-0.4, 0.3, 0.9, 1.6]fitness: List[float] = [8.0, 22.0, 35.0, 15.0]
mean_f: float = sum(fitness) / len(fitness)ranked: List[Tuple[float, float]] = sorted( zip(weights, fitness), key=lambda t: t[1], reverse=True)parents: List[Tuple[float, float]] = ranked[:2]child_mean: float = round((parents[0][0] + parents[1][0]) / 2.0, 2)
elite: float = parents[0][0]child_crossover: float = round(child_mean + 0.12, 2)child_best: float = round(parents[0][0] - 0.07, 2)child_second: float = round(parents[1][0] + 0.03, 2)next_gen: List[float] = [elite, child_crossover, child_best, child_second]
print(f"mean fitness: {mean_f:.1f}")print(f"parents: {parents}")print(f"child mean: {child_mean:.2f}")print(f"next gen: [{elite:.2f}, {child_crossover:.2f}, {child_best:.2f}, {child_second:.2f}]")# -> expected output:mean fitness: 20.0parents: [(0.9, 35.0), (0.3, 22.0)]child mean: 0.60next gen: [0.90, 0.72, 0.83, 0.33]Watch Out For
Sample inefficiency: each step burns full episodes
The symptom is slow wall-clock progress. This page used 4 episodes for one update. Real runs use hundreds or thousands of policies per generation, and each one needs full episodes before the update. If your simulator is slow or each episode runs minutes on hardware, one generation can take all day.
The fix is to match the tool to the reward. When rewards are dense and the policy is differentiable, prefer policy gradient methods because each episode gives gradient information for every step. When you must evolve, parallelize rollouts across workers, share random seeds instead of weight vectors, and start with small populations before scaling up.
Premature convergence: everyone crowds the first winner
The symptom is a population that looks done after a few rounds but scores poorly. All weights sit near 0.9 in the example, variation shrinks toward zero, and no child ever tries far regions again. Greedy top-k selection plus tiny sigma causes this fast.
The fix is to protect diversity on purpose. Raise sigma early and shrink it later, use tournament selection instead of always taking the top two, keep an elite of one rather than half the population, and restart or inject fresh random members when the spread of fitness drops near zero for several rounds.
The Quick Version
- Neuroevolution scores whole policies and breeds winners, so it works without differentiable losses or slope signals.
- One generation selects the top scorers, mixes parents by averaging, then adds Gaussian noise with spread sigma.
- The four-policy example scores 8, 22, 35 and 15 for mean 20.0 and keeps the policies at 0.9 and 0.3.
- Crossover mean 0.6 plus fixed tweaks builds the next generation at 0.90, 0.72, 0.83 and 0.33 before rescoring.
- Evolution fits sparse, deceptive, or non-differentiable rewards; gradients fit dense rewards with cheap differentiable policies.