Skip to content

Commit f8fe184

Browse files
committed
πŸ“ Add comprehensive README with algorithm explanation
- Explain diffusion evolution concept with accessible foggy room analogy - Include quick start guide with code examples and configuration options - Document benchmark suite usage for parameter tuning - Add technical deep dive with step-by-step algorithm breakdown and equations - Reference the source paper (arXiv:2410.02543)
1 parent bee332c commit f8fe184

1 file changed

Lines changed: 221 additions & 0 deletions

File tree

β€ŽREADME.mdβ€Ž

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# Devol
2+
3+
**Diffusion Evolution** - What if evolution worked like image generation?
4+
5+
## What Is This?
6+
7+
Traditional evolutionary algorithms create new solutions by *copying and mutating* successful ones. Devol does something different: it starts with pure noise and *denoises* toward good solutions, guided by fitness.
8+
9+
The core idea: instead of asking "what should the children of good solutions look like?", we ask "given this random noise, what good solution could it have come from?"
10+
11+
This reframing gives us an algorithm that naturally transitions from broad exploration to precise optimization - without any special tuning.
12+
13+
**The intuition**: Imagine you're in a foggy room full of people, each standing at a different elevation. You can only see your immediate neighbors through the fog. To find the highest point, you don't just copy the person next to you - you look at everyone nearby, weight them by height, and move toward the weighted average. As the fog clears (denoising), your steps become smaller and more precise.
14+
15+
## Quick Start
16+
17+
```python
18+
import numpy as np
19+
from devol import DiffusionEvolution, DiffusionConfig
20+
21+
# Define what you're optimizing
22+
def sphere(x: np.ndarray) -> float:
23+
"""Simple sphere function - maximum at origin."""
24+
return -np.sum(x ** 2)
25+
26+
# Configure the algorithm
27+
config = DiffusionConfig(
28+
population_size=128,
29+
num_steps=100,
30+
param_dim=10,
31+
sigma_m=0.5,
32+
)
33+
34+
# Run evolution
35+
algo = DiffusionEvolution(config, sphere)
36+
algo.run(initial_population=None)
37+
38+
# Get results
39+
best_solution, best_fitness = algo.get_best_individual()
40+
print(f"Best fitness: {best_fitness:.6f}")
41+
```
42+
43+
### Configuration Options
44+
45+
| Parameter | Description | Default |
46+
|-----------|-------------|---------|
47+
| `population_size` | Number of candidate solutions | 512 |
48+
| `num_steps` | Denoising iterations | 50 |
49+
| `param_dim` | Dimensionality of search space | (required) |
50+
| `sigma_m` | Mutation scale [0, 1] | 1.0 |
51+
| `schedule.type` | `linear`, `cosine`, or `ddpm` | `cosine` |
52+
| `fitness.mapping` | How fitness converts to weights | `direct` |
53+
| `fitness.temperature` | Sharpness of fitness weighting | 1.0 |
54+
55+
### Using YAML Configuration
56+
57+
```python
58+
from devol import DiffusionEvolution
59+
from devol.config import DiffusionConfig
60+
from pydantic_yaml import parse_yaml_file_as
61+
62+
config = parse_yaml_file_as(DiffusionConfig, "config.yaml")
63+
algo = DiffusionEvolution(config, your_fitness_function)
64+
```
65+
66+
Example `config.yaml`:
67+
```yaml
68+
population_size: 256
69+
num_steps: 200
70+
param_dim: 32
71+
sigma_m: 0.5
72+
schedule:
73+
type: cosine
74+
epsilon: 0.0001
75+
fitness:
76+
mapping: exponential
77+
temperature: 2.0
78+
normalize: min_max
79+
```
80+
81+
## Benchmarks
82+
83+
The benchmark suite helps you understand how different configurations perform on challenging optimization landscapes.
84+
85+
### Why Benchmark?
86+
87+
Different problems favor different settings:
88+
- **Multimodal landscapes** (many local optima): May need higher `sigma_m` and more steps
89+
- **High-dimensional spaces**: May need larger populations
90+
- **Smooth landscapes**: Can often use fewer steps with aggressive schedules
91+
92+
The benchmarks use the **Rastrigin function** - a notoriously difficult test case with a global optimum surrounded by a regular grid of local optima. If your configuration works on Rastrigin, it has a fighting chance on real problems.
93+
94+
### Running Benchmarks
95+
96+
```bash
97+
uv run -m benchmark.main
98+
```
99+
100+
This runs a grid search across:
101+
- **Schedule types**: linear, cosine, ddpm
102+
- **Population sizes**: 64, 128, 256
103+
- **Steps**: 64, 128, 256, 512
104+
- **Dimensions**: 8, 16, 32, 64
105+
- **Mutation scales**: 0.2, 0.5, 0.8, 1.0
106+
107+
Results are saved to `benchmark_results/` with visualizations showing:
108+
- Best fitness achieved per configuration
109+
- How different schedules compare
110+
- The effect of population size vs. steps tradeoffs
111+
112+
### Custom Benchmarks
113+
114+
```python
115+
from benchmark import GridSearchRunner
116+
117+
def your_objective(x):
118+
# Your fitness function here
119+
return score
120+
121+
runner = GridSearchRunner(
122+
fitness_fn=your_objective,
123+
schedule_types=["cosine", "ddpm"],
124+
population_sizes=[128, 256],
125+
num_steps_list=[100, 200],
126+
param_dims=[16],
127+
sigma_m_values=[0.5, 0.8],
128+
seeds=[42, 123],
129+
)
130+
131+
results = runner.run(verbose=True)
132+
```
133+
134+
The runner uses multiprocessing to parallelize experiments across CPU cores.
135+
136+
---
137+
138+
## How It Actually Works
139+
140+
If you're curious about the mechanics, here's the full story.
141+
142+
### The Diffusion Perspective
143+
144+
The key insight: if you add enough random noise to any population of solutions, they all become indistinguishable - just random static. The magic is in *reversing* that process.
145+
146+
- **Forward process**: Take good solutions and gradually add noise until they're unrecognizable
147+
- **Reverse process**: Start with pure noise and gradually remove it, guided by fitness
148+
149+
The reverse process is where evolution happens. At each step, we ask: "Given this noisy solution, what did the *clean* solution probably look like?" And we answer using two signals:
150+
151+
1. **Fitness**: Better solutions should be more likely origins
152+
2. **Proximity**: Solutions that are closer in parameter space are more relevant
153+
154+
This is captured in a beautifully simple equation. To estimate what a noisy point `xβ‚œ` originally was, we compute a weighted average:
155+
156+
```
157+
xΜ‚β‚€ = Ξ£ (fitness_weight Γ— proximity_weight Γ— candidate) / normalization
158+
```
159+
160+
Each candidate solution contributes based on both how good it is *and* how close it is to the noisy observation. This creates a kind of "gravitational pull" toward high-fitness regions, but filtered through local structure.
161+
162+
### Why Proximity Matters
163+
164+
The proximity weighting is the secret sauce. In traditional evolution, a mutation in New York affects a solution in Tokyo with the same probability. In diffusion evolution, the influence is local - solutions only "see" their neighbors.
165+
166+
This means:
167+
- **Early iterations** (high noise): Large-scale structure emerges, populations cluster toward promising regions
168+
- **Late iterations** (low noise): Fine-grained optimization, solutions converge precisely to peaks
169+
170+
The algorithm naturally transitions from exploration to exploitation without any explicit scheduling.
171+
172+
### The Algorithm Step by Step
173+
174+
Here's what happens at each denoising step:
175+
176+
**Step 1: Estimate the clean solution**
177+
178+
For each noisy solution `xβ‚œ`, we estimate what it was before noise was added:
179+
180+
```
181+
xΜ‚β‚€ = (1/Z) Ξ£ g[f(x)] Γ— N(xβ‚œ; βˆšΞ±β‚œΒ·x, 1-Ξ±β‚œ) Γ— x
182+
```
183+
184+
where:
185+
- `g[f(x)]` is a fitness-based weight (fitter solutions contribute more)
186+
- `N(...)` is a Gaussian that weights by proximity
187+
- `Z` normalizes everything
188+
189+
**Step 2: Compute the predicted noise**
190+
191+
```
192+
Ξ΅Μ‚ = (xβ‚œ - βˆšΞ±β‚œ Β· xΜ‚β‚€) / √(1-Ξ±β‚œ)
193+
```
194+
195+
This is the noise we think was added to get from `xΜ‚β‚€` to `xβ‚œ`.
196+
197+
**Step 3: Take the evolution step**
198+
199+
```
200+
xβ‚œβ‚‹β‚ = βˆšΞ±β‚œβ‚‹β‚ Β· xΜ‚β‚€ + direction_term Β· Ξ΅Μ‚ + Οƒβ‚œ Β· noise
201+
```
202+
203+
We move toward our estimate of the clean solution, partially preserving the predicted noise direction, and add fresh stochasticity controlled by `Οƒβ‚œ`.
204+
205+
### The Noise Schedule
206+
207+
The parameter `Ξ±β‚œ` controls how much "signal" remains at step `t`:
208+
- `Ξ±β‚œ = 1`: Pure signal, no noise
209+
- `Ξ±β‚œ = 0`: Pure noise, no signal
210+
211+
The schedule (linear, cosine, or DDPM) determines how quickly we transition. Cosine schedules spend more time in the middle range where interesting structure emerges.
212+
213+
## References
214+
215+
This implementation is based on:
216+
217+
> **Diffusion Models are Evolutionary Algorithms**
218+
> arXiv:2410.02543
219+
> https://arxiv.org/abs/2410.02543
220+
221+
The paper establishes the theoretical connection between diffusion models and evolutionary computation, showing that the iterative denoising process can be interpreted as fitness-guided evolution with proximity-aware selection.

0 commit comments

Comments
Β (0)