Symbolic Regression

Discovering Equations with Genetic Programming

ML for Science and Engineering · Lecture 12

The Limits of Fixed Libraries

In SINDy, we chose a fixed library of candidate terms:

$\dot{x} = \theta_1 x + \theta_2 x^2 + \theta_3 \sin(x) + \cdots$

This works when we have a good guess about what terms might appear.

But what if the true equation is:

$y = \frac{\sin(x^2)}{1 + e^{-x}}$

No finite polynomial library will contain this term. We need to search over functional forms themselves.

What is Symbolic Regression?

Given data $(x, y)$, find a mathematical expression $f$ such that $y \approx f(x)$.
Linear Regression
Fix form: $y = ax + b$
Search over: $a, b$
SINDy
Fix library: $[1, x, x^2, \sin x]$
Search over: $\theta$
Symbolic Regression
Fix nothing
Search over: all expressions
The search space is enormous: all possible compositions of $+, -, \times, \div, \sin, \cos, \exp, \log, \dots$ applied to variables and constants.

A Brief History

  • 1992. John Koza: Genetic Programming. Evolving computer programs (trees) via natural selection.
  • 2009. Schmidt & Lipson, Science: Distilling free-form natural laws from experimental data. Rediscovered Newton's laws from pendulum data.
  • 2023. Cranmer, PySR: High-performance symbolic regression combining evolutionary search with modern optimizations.

Expressions as Trees

Every mathematical expression has a natural tree structure:

$x + \sin(y)$

Internal nodes = operators ($+, \sin$)
Leaves = variables ($x, y$) or constants

Interactive: Expression Trees

Click an expression to see its tree representation:

$x + \sin(y)$ $\alpha x - \beta xy$ $\frac{x}{1 + e^{-x}}$ $\sin(x^2) + 0.5$
Nodes: 3
Depth: 2
Operators: +, sin
Expression:
x + sin(y)

The Search Space

Count the expression trees you could write with $k$ binary operators, $u$ unary ones and $v$ leaves. Move the sliders.

Each extra node multiplies the count, so a computer a thousand times faster moves the wall by about two nodes.

And this counts only the shapes Real expressions carry continuous constants, so the space is not large, it is infinite. That is what forces a heuristic search.

The Genetic Algorithm

Inspired by biological evolution: a population of candidate solutions evolves over generations.

1. Initialize
Random population of expression trees
2. Evaluate
Fitness = how well each tree fits the data
3. Evolve
Select, crossover, mutate. Repeat.
$\text{fitness}(f) = \underbrace{\text{MSE}(f, \text{data})}_{\text{accuracy}} + \alpha \cdot \underbrace{|f|}_{\text{complexity}}$

Selection

Tournament selection: pick $k$ individuals at random, keep the best one.

Other strategies: roulette wheel (probability proportional to fitness), rank selection, elitism (always keep the best).

Crossover: Swapping Subtrees

Pick a random subtree in each parent, swap them to create offspring:

Crossover combines structural features from both parents. A good subtree from one parent can be transplanted into another context.

Mutation: Random Changes

Selection and crossover only recombine what the population already has. Mutation is where new material comes from. Press a button.

Point Subtree Hoist Reset
Point
One node becomes another of the same arity. A small step.
Subtree
A whole branch is replaced at random. A large step.
Hoist
The tree is replaced by one of its own branches. The only one of the three that always makes the expression smaller, which is how a population fights its own tendency to grow.

The Full Loop


population = [random_tree() for _ in range(pop_size)]

for generation in range(max_generations):
    # Evaluate fitness
    for tree in population:
        tree.fitness = mse(tree, data) + alpha * tree.size

    # Selection + reproduction
    offspring = []
    while len(offspring) < pop_size:
        p1 = tournament_select(population, k=5)
        p2 = tournament_select(population, k=5)
        c1, c2 = crossover(p1, p2)
        c1 = maybe_mutate(c1, rate=0.1)
        c2 = maybe_mutate(c2, rate=0.1)
        offspring.extend([c1, c2])

    # Elitism: keep best from previous generation
    population = elites(population, n=5) + offspring
        

Watch Evolution in Action

Target: discover $f(x)$ from noisy data. Watch the population evolve.

Run Step Run 10 Reset
Best expression:
MSE: | Size:

Now Do It On Something Real

2,902 planets outside the solar system, every one of them measured: how long its year is, how far it orbits, and how heavy its star is.

NASA Exoplanet Archive, confirmed planets with all three quantities measured. The CSV is on this site. Discovered 1992 to 2026, mostly by transit and radial velocity.

Year against orbital distance. Every dot is a real planet around a real star.

A Straight Line Where You Did Not Expect One

Take the logarithm of both axes and the cloud becomes a line. A line in log space is a power law: $P = C\,a^{\alpha}$.

$\log P = \alpha \log a + c$

Least squares on 2,902 planets gives $\alpha = 1.457$, with $R^2 = 0.9896$.

Kepler's third law says the exponent is exactly $3/2$. We got 1.457. Close, and wrong. A search that stops here reports a law that does not exist.

The scatter around the line is not noise in the measurements. It is a variable we left out.

The Missing Variable Was the Star

Residual scatter, before and after the star's mass enters the expression.

$\log P = \alpha \log a + \beta \log M + c$

Fit again on the same 2,902 planets: $\alpha = 1.497$, $\beta = -0.479$. Kepler to three digits, and the scatter falls by 68 per cent.

$P = \sqrt{\dfrac{a^3}{M}}$

Median error across every planet: 0.19 per cent. Newton could have written it, and did.

Fitting a curve is not finding a law. The search has to range over which variables matter, not only over exponents.

The Accuracy-Complexity Tradeoff

Every candidate is a dot: how big it is against how wrong it is, on the 2,902 planets. Click one.

The front is the set of candidates that nothing smaller beats. Everything above it is dominated: another expression is both simpler and more accurate.

The interesting place is the knee, where the error falls off a cliff and then stops improving. Here the knee is at size 8, and the expression sitting on it is Kepler's third law.

Notice the size 13 candidate: it is more complicated than the law and worse. Complexity that buys nothing is how a search fools you.

PySR: Modern Symbolic Regression


from pysr import PySRRegressor
import numpy as np

X = np.random.randn(100, 2)      # two input variables
y = X[:, 0] * np.sin(X[:, 1])    # true relationship

model = PySRRegressor(
    niterations=100,
    binary_operators=["+", "-", "*", "/"],
    unary_operators=["sin", "cos", "exp"],
    populations=20,               # parallel populations
    maxsize=25,                   # max expression complexity
)

model.fit(X, y)
print(model)                      # Pareto front of equations
        

PySR outputs a Pareto front of equations ranked by complexity vs. loss. Uses Julia backend for speed.

✍ Check Your Understanding

Challenges and Solutions

Challenges

  • Bloat: trees grow large without improving fitness
  • Premature convergence: population loses diversity
  • Constants: hard to evolve precise numeric values
  • Runtime: expensive for large datasets

Solutions

  • Parsimony pressure, tree depth limits
  • Multiple populations, island model
  • Gradient-based constant optimization
  • Parallelization, batching, Julia/C++

Symbolic Regression in Context

MethodSearch spaceInterpretable?Extrapolates?
Neural networkWeights of fixed architectureNoPoorly
SINDyCoefficients of fixed libraryYesIf correct
Symbolic regressionAll expressionsYesIf correct
Physics-informed NNNN + known PDE constraintPartiallySomewhat
Symbolic regression is the most flexible equation discovery method, but also the most computationally expensive. Use SINDy when you have a good library guess. Use SR when you do not.

✍ Check Your Understanding

✍ Check Your Understanding

Summary

1. Represent
Expressions as trees.
Operators + variables + constants.
2. Evolve
Genetic algorithm: select, crossover, mutate over generations.
3. Select
Pareto front: accuracy vs. complexity. Find the knee.
Data $\longrightarrow$ evolve expression trees $\longrightarrow$ discover $f(x)$

Schmidt & Lipson, Science (2009)  |  PySR (Cranmer, 2023)

✍ Check Your Understanding