eaopt

repository·master·Indexed 21 days ago

https://github.com/maxhalford/eaopt

A Go library for evolutionary optimization providing a flexible framework to implement and run algorithms including Genetic Algorithms (GA), Particle Swarm Optimization (PSO), Differential Evolution (DE), OpenAI Evolution Strategy (OES), Hill Climbing, and Simulated Annealing. It features a customizable GA struct with support for various population replacement models, speciation, multi-population migration, and parallel evaluation.

Tokens
18.2K
Snippets
79
Records
92
Agent score
75%

What's inside eaopt

  1. Overview of eaopt

    master

    eaopt is an evolutionary optimization library for Go. It provides implementations for various evolutionary optimization algorithms, including Genetic Algorithms (GA), Particle Swarm Optimization (PSO), Differential Evolution (DE), OpenAI Evolution Strategy (OES), Hill Climbing, and Simulated Annealing.

    The library is designed around a highly flexible GA struct, which allows users to customize mutation, crossover, selection, and replacement procedures. This flexibility means most other algorithms in the library are implemented as special cases of a genetic algorithm, and users can also implement their own custom operators.

  2. How speciation works in eaopt

    master

    Speciation (or clustering) partitions individuals into smaller groups (species) of similar individuals based on a metric (e.g., fitness). This encourages genetic operators to be applied to similar individuals, preventing new evolutionary traits from being immediately lost during selection (a common issue in neural network topology optimization).

    To use speciation, you must specify a Speciator in the Speciator field of the GA struct.

  3. Ensure Mutate and Crossover modify Genomes in-place

    master

    The Mutate and Crossover methods must modify the Genome values in-place.

    • For slices (e.g., []float64): Since slices are references to underlying data in Go, modifying the slice inside the method will affect the original genome.
    • For structs: You must use a pointer receiver (e.g., *Name) to ensure the method modifies the actual instance rather than a copy.
    // Example 1: Using a slice (works because slices are references)
    type Vector []float64
    
    func (X Vector) Mutate(rng *rand.Rand) {
        eaopt.MutNormal(X, rng, 0.5)
    }
    
    // Example 2: Using a struct (requires a pointer receiver)
    type Name string
    
    func (n *Name) Mutate(rng *rand.Rand) {
        *n = randomName() // Use pointer to modify the value
    }
  4. Implement Simulated Annealing using Genetic Algorithms

    master

    Simulated annealing is implemented using a Genetic Algorithm with the ModSimulatedAnnealing model. This model requires an Accept function that defines the probability of accepting a worse move.

    The Accept function signature is: func(g, ng uint, e0, e1 float64) float64 where:

    • g: current generation
    • ng: total number of generations
    • e0: fitness of the current point
    • e1: fitness of the mutated point

    Common acceptance strategies include:

    • Linear (Temperature): 1.0 - float64(g)/float64(ng)
    • Exponential: math.Exp(-3.0 * (1.0 - float64(g)/float64(ng)))
    • Cosine: (math.Cos(t*math.Pi) + 1.0) / 2.0 where t = 1.0 - float64(g)/float64(ng)
    cfg := eaopt.NewDefaultGAConfig()
    cfg.Model = eaopt.ModSimulatedAnnealing{
        Accept: func(g, ng uint, e0, e1 float64) float64 {
            t := 1.0 - float64(g)/float64(ng)
            return (math.Cos(t*math.Pi) + 1.0) / 2.0
        },
    }
    cfg.NGenerations = 999
    
    ga, err := cfg.NewGA()
    // ... call ga.Minimize
  5. How different Genetic Algorithm models work

    master

    eaopt supports several population replacement models for Genetic Algorithms (GA):

    • Generational model: Generates $n$ offspring from a population of size $n$ and replaces the entire population with these offspring. Offspring are created via crossover and optionally mutated.
    • Steady state model: Does not replace the entire population. Instead, it selects the 2 best individuals from a pool of 2 parents and 2 children to maintain a constant population size.
    • Select down to size model: Uses two selection rounds. Offspring are merged with the original population, and a second selection round determines which individuals survive to keep the population at size $n$.
    • Ring model: Uses a one-directional ring topology where neighbors undergo crossover. The best out of the 4 individuals (2 parents + 2 offspring) replaces the first neighbor.
    • Mutation only: Runs a GA without crossover by only applying mutations. This is implemented via the ModMutationOnly struct. It includes a strict field to decide if a mutant only replaces an individual if its fitness is better.
  6. How multi-populations and migration work

    master

    eaopt allows running multiple independent populations in parallel to increase diversity.

    • Multi-populations: Controlled by the Populations field in the GA struct. The number of populations is defined in GAConfig.NPops.
    • Migration: If a Migrator and MigFrequency are provided, individuals are exchanged between populations at every generation divisible by MigFrequency (e.g., if frequency is 5, migration occurs at generations 5, 10, 15, etc.).
    • Independence: If Migrator and MigFrequency are omitted, populations run independently.
  7. Configure parallelism in eaopt

    master

    By default, eaopt evolves populations in parallel. You can further optimize performance by enabling parallelism for specific heavy operations using the GA struct fields:

    • ParallelEval: Set to true to evaluate individuals in parallel. This is beneficial if your Evaluate method is computationally expensive.
    • ParallelInit: Set to true to initialize individuals in parallel. This is beneficial if your genome initialization method is computationally expensive.

    Note that these settings can be used regardless of whether you are using a single or multiple populations.

  8. Minimize a function using a Genetic Algorithm

    master

    To use a Genetic Algorithm in eaopt, you must implement the eaopt.Genome interface for your data type. This involves defining Evaluate, Mutate, Crossover, and Clone methods. You then use a GenomeFactory function to generate initial individuals.

    Steps to run an optimization:

    1. Define a type that implements eaopt.Genome.
    2. Create a factory function that returns a new instance of your type (the GenomeFactory).
    3. Instantiate a GA using eaopt.NewDefaultGAConfig().NewGA().
    4. Configure the GA (e.g., NGenerations, Callback).
    5. Call ga.Minimize(factory) to start the optimization process.
    package main
    
    import (
        "fmt"
        m "math"
        "math/rand"
    
        "github.com/MaxHalford/eaopt"
    )
    
    // A Vector contains float64s.
    type Vector []float64
    
    // Evaluate a Vector with the Drop-Wave function.
    func (X Vector) Evaluate() (float64, error) {
        var (
            numerator   = 1 + m.Cos(12*m.Sqrt(m.Pow(X[0], 2)+m.Pow(X[1], 2)))
            denominator = 0.5*(m.Pow(X[0], 2)+m.Pow(X[1], 2)) + 2
        )
        return -numerator / denominator, nil
    }
    
    // Mutate a Vector by resampling each element from a normal distribution.
    func (X Vector) Mutate(rng *rand.Rand) {
        eaopt.MutNormalFloat64(X, 0.8, rng)
    }
    
    // Crossover a Vector with another Vector using uniform crossover.
    func (X Vector) Crossover(Y eaopt.Genome, rng *rand.Rand) {
        eaopt.CrossUniformFloat64(X, Y.(Vector), rng)
    }
    
    // Clone a Vector to produce a new one.
    func (X Vector) Clone() eaopt.Genome {
        var Y = make(Vector, len(X))
        copy(Y, X)
        return Y
    }
    
    // VectorFactory returns a random vector.
    func VectorFactory(rng *rand.Rand) eaopt.Genome {
        return Vector(eaopt.InitUnifFloat64(2, -10, 10, rng))
    }
    
    func main() {
        // Instantiate a GA with a GAConfig
        var ga, err = eaopt.NewDefaultGAConfig().NewGA()
        if err != nil {
            fmt.Println(err)
            return
        }
    
        // Set the number of generations to run
        ga.NGenerations = 10
    
        // Add a custom print function to track progress
        ga.Callback = func(ga *eaopt.GA) {
            fmt.Printf("Best fitness at generation %d: %f\n", ga.Generations, ga.HallOfFame[0].Fitness)
        }
    
        // Find the minimum
        err = ga.Minimize(VectorFactory)
        if err != nil {
            fmt.Println(err)
            return
        }
    }
  9. Use OpenAI Evolution Strategy (OES)

    master

    OpenAI's evolution strategy uses natural gradients to optimize a center point mu by sampling points around it using a normal distribution.

    You can use eaopt.NewDefaultOES() for a quick setup or eaopt.NewOES() for fine-grained control over the evolution parameters.

    To use it, provide a fitness function (a function that takes a slice of float64 and returns a float64) to the Minimize method of the OES instance.

    // Instantiate OES with custom parameters
    nPoints := uint(10)
    nSteps := uint(100)
    sigma := 0.05
    lr := 0.01
    parallel := true
    var rng *rand.Rand = nil
    
    var oes, err = eaopt.NewOES(nPoints, nSteps, sigma, lr, parallel, rng)
    
    // Run minimization on a function
    _, y, err := oes.Minimize(Rastrigin, 2)
  10. Best practices for using eaopt

    master

    When using the eaopt library, follow these guidelines to ensure correct initialization and performance:

    • Use Constructor Functions: Always use the provided constructor functions (e.g., NewPSO instead of instantiating a PSO struct directly). These functions perform necessary parameter validation and error checking.
    • Leverage Built-in Operators: If using the GA struct, check if evolutionary operators are already implemented in eaopt before writing your own.
    • Understand Algorithm Abstractions: Many algorithms in the library are implemented as special cases of Genetic Algorithms (GAs). This design allows for more efficient underlying processing even if you only need a specific optimization method.
    • Check Minimize signatures: To understand what type of function a specific method can optimize, inspect its Minimize function signature.
  11. Implement a Genome without Crossover

    master

    If your specific problem does not require crossover, you must still satisfy the Genome interface. You can do this by providing a blank implementation of the Crossover method.

    type Vector []float64
    
    // Provide a blank implementation to satisfy the interface
    func (X Vector) Crossover(Y eaopt.Genome, rng *rand.Rand) {}
  12. Implement Hill Climbing using Genetic Algorithms

    master

    Hill climbing can be implemented in eaopt by configuring a Genetic Algorithm (GA) with the ModMutationOnly model and setting Strict: true.

    To implement this, your genome must satisfy the eaopt.Genome interface, which requires implementing Evaluate(), Mutate(), Crossover(), and Clone(). For hill climbing, Crossover should do nothing.

    cfg := eaopt.NewDefaultGAConfig()
    cfg.Model = eaopt.ModMutationOnly{Strict: true}
    cfg.NGenerations = 9999
    
    ga, err := cfg.NewGA()
    err = ga.Minimize(func(rng *rand.Rand) eaopt.Genome {
        // Return a new random genome instance
        return &Coord2D{X: rng.Float64(), Y: rng.Float64()}
    })