EvoTorch Documentation

repository·master·Indexed 22 days ago

https://github.com/nnaisense/evotorch

An evolutionary computation library built on PyTorch for solving non-differentiable optimization problems, including black-box optimization, reinforcement learning, and supervised learning. It features a stateful object-oriented API and a Functional API for batched population searches, multi-objective optimization, and non-numeric solutions. EvoTorch supports vectorization and parallelization on GPUs and can scale across clusters using Ray. Supported algorithms include SNES, PGPE, CMA-ES, CEM, and MAPElites.

Tokens
61.8K
Snippets
164
Records
201
Agent score
78%

What's inside EvoTorch

  1. Overview of EvoTorch capabilities

    master

    EvoTorch is an open-source evolutionary computation library built on PyTorch. It is designed to solve optimization problems that may not be differentiable (i.e., do not allow gradient descent).

    Supported problem types:

    • Black-box optimization problems (continuous or discrete)
    • Reinforcement learning tasks
    • Supervised learning tasks

    EvoTorch algorithms benefit from PyTorch's vectorization and parallelization on GPUs. It can also scale across multiple CPUs, GPUs, or computers in a cluster using Ray.

  2. Overview of EvoTorch features

    master

    EvoTorch is an evolutionary algorithm library built on PyTorch designed for optimizing and evolving solutions to complex problems. Key capabilities include:

    • Algorithm Variety: Access to state-of-the-art distribution-based and population-based evolutionary algorithms.
    • Scalability: Integration with Ray clusters to scale evolutionary searches.
    • Hardware Acceleration: Support for running both fitness functions and search algorithms on CUDA-accelerated hardware.
    • Experiment Tracking: Integration with modern machine-learning logging software.
    • Modularity: A modular architecture that allows for building custom evolutionary algorithms and conducting research experiments.
  3. Overview of EvoTorch Example Scripts

    master

    The examples/scripts directory contains several demonstration scripts for different optimization and learning paradigms:

    Black-box Optimization

    • bbo_vectorized.py: Single-objective black-box optimization using a distribution-based algorithm, optimized via vectorization on a single GPU or CPU.
    • moo_parallel.py: Multi-objective optimization using parallelization across all CPU cores (without vectorization).

    Reinforcement Learning

    • rl_gym.py: Solving a standard Gymnasium problem using the PGPE algorithm and ClipUp optimizer.
    • rl_clipup.py: A CLI tool for training policies in environments like Lunar Lander, Walker-2D, Humanoid (MuJoCo), and Humanoid (PyBullet).
  4. Overview of the EvoTorch Functional API

    master
    EvoTorch provides a Functional API as an alternative to its standard object-oriented stateful API. The functional paradigm allows for advanced capabilities such as operating on a batch of populations simultaneously rather than a single population. This is particularly useful for running multiple searches with different hyperparameter configurations in parallel.
  5. Understand the difference between population-based and distribution-based algorithms

    master

    EvoTorch categorizes its evolutionary algorithms into two main conceptual frameworks:

    Population-based Algorithms

    These follow the conventional evolutionary model. They maintain an explicit population of individuals $X$ and their associated fitness values $F$. In each iteration, operators for Selection (S), Recombination (R), and Mutation (C) are applied to the current population to produce a new one, biasing the search towards high-performing solutions from the previous generation.

    Distribution-based Algorithms

    Instead of maintaining an explicit population that competes and reproduces, these algorithms model the population using a probability distribution $\pi(\theta)$ parameterized by $\theta$.

    1. A population $X$ is sampled from the distribution $\pi(\theta)$.
    2. Fitness values are used to compute a change in the parameters $\triangledown \theta$.
    3. The parameters are updated (typically via gradient descent) to $\theta = \theta + \alpha \triangledown \theta$.

    Most modern distribution-based algorithms in EvoTorch incorporate Natural Gradient to achieve re-parameterization invariance.

  6. Use NEProblem for Neuroevolution

    master

    To perform neuroevolution, define a fitness function that takes a torch.nn.Module and returns a scalar fitness value. Then, instantiate an NEProblem by specifying the objective_sense ('min' or 'max'), the network definition, and the network_eval_func (your fitness function).

    Supported network definitions include:

    • A torch.nn.Module instance (not recommended for large-scale parallelization).
    • A string representation for sequential modules using the >> operator.
    • A function that returns a torch.nn.Module instance.
    • A torch.nn.Module class.

    Once the problem is defined, you can pass it to an algorithm like PGPE to run the evolution.

    from evotorch.neuroevolution import NEProblem
    import torch
    
    def my_fitness_func(network: torch.nn.Module):
        # ... implementation ...
        return fitness_score
    
    problem = NEProblem(
        objective_sense="max",
        network=torch.nn.Linear(3, 1),
        network_eval_func=my_fitness_func,
    )
    
    from evotorch.algorithms import PGPE
    searcher = PGPE(problem, popsize=50)
    searcher.run(50)
  7. How to use EvoTorch to solve an optimization problem

    master

    Using EvoTorch involves a four-stage workflow:

    1. Create a Problem: Wrap your objective function (a PyTorch function) in a Problem instance. You must specify the optimization goal ("min" or "max"), the solution_length, and optionally initial_bounds.
    2. Create a Searcher: Instantiate an algorithm (e.g., SNES) by passing the Problem instance to it. You can configure algorithm-specific parameters like stdev_init.
    3. Attach Loggers: Create a logger (e.g., StdOutLogger) and pass the searcher to it to monitor progress.
    4. Run the Algorithm: Execute the search using searcher.step() for a single iteration or searcher.run(n) for multiple iterations.
    from evotorch import Problem
    from evotorch.algorithms import SNES
    from evotorch.logging import StdOutLogger
    import torch
    
    # 1. Define objective
    def sphere(x: torch.Tensor) -> torch.Tensor:
        return torch.sum(x.pow(2.0))
    
    # 2. Create Problem
    problem = Problem("min", sphere, solution_length=10, initial_bounds=(-1, 1))
    
    # 3. Create Searcher
    searcher = SNES(problem, stdev_init=5)
    
    # 4. Attach Logger
    logger = StdOutLogger(searcher)
    
    # 5. Run
    searcher.run(3)
  8. Optimize small neuro-evolution problems on a single GPU

    master

    For neuro-evolution problems that are small enough, it may be more performant to run both the network evaluations and the evolutionary algorithm on a single GPU rather than parallelizing across multiple actors.

    To do this, set device to a specific CUDA device (e.g., device='cuda:0') and ensure num_actors is set to None, 0, or 1 to avoid the overhead of Ray actors.

    # Example configuration for single-GPU execution
    problem = MyProblem(device='cuda:0', num_actors=None)
  9. How to implement a custom Logger

    master

    To create a custom logger in evotorch, you must subclass evotorch.logging.Logger and implement two primary methods:

    1. __init__(self, searcher): Accepts a SearchAlgorithm instance. You must call super().__init__(searcher) to properly attach the logger to the searcher.
    2. _log(self, status: dict): This method is automatically called by the SearchAlgorithm during its execution. It receives a status dictionary containing current algorithm metrics (e.g., status["iter"]).

    Once initialized, you attach the logger to a searcher by passing the searcher instance to the logger's constructor. The logger will then receive updates whenever the searcher performs a step or runs.

    from evotorch.logging import Logger
    
    class MyLogger(Logger):
        def __init__(self, searcher):
            super().__init__(searcher)
            # Perform additional initialization here
    
        def _log(self, status: dict):
            # Process the status dictionary here
            print(f"Iteration: {status['iter']}")
    
    # Usage
    my_logger = MyLogger(searcher)
    searcher.step()
    from evotorch.logging import Logger
    
    class MyLogger(Logger):
        def __init__(self, searcher):
            super().__init__(searcher)
    
        def _log(self, status: dict):
            pass
    
    my_logger = MyLogger(searcher)
    searcher.step()
  10. How hooks work in evotorch

    master

    Hooks allow you to inject custom code into different stages of the evolutionary process. They can be attached to two main types of objects:

    1. Problem instances: Used to intercept the evaluation process of a SolutionBatch.
    2. SearchAlgorithm instances: Used to intercept the execution of the algorithm's internal _step method.

    Both types of hooks support 'before' and 'after' execution patterns. Some hooks allow you to return a dict, which is automatically merged into the object's status dictionary, making that data available to loggers and other monitoring tools.

  11. Slice, index, and concatenate SolutionBatch instances

    master

    Slicing and Indexing

    SolutionBatch supports standard Python slicing and integer/list indexing.

    • Slicing: batch[start:stop] returns a view of the original batch. Modifying the slice (via set_values, set_evals, or access_values) will modify the parent batch.
    • List Indexing: batch[[idx1, idx2]] returns a view of specific solutions. Modifying this sub-batch also modifies the parent.

    Concatenation

    You can combine multiple SolutionBatch instances from the same Problem using SolutionBatch.cat([batch1, batch2]) or batch1.concat(batch2).

    Note: Concatenation creates a new copy of the data. Modifying the resulting batch will not affect the original parent batches.

    # Slicing (returns a view)
    last_3 = batch[2:5]
    last_3.set_values(problem.make_gaussian(3, 2)) # Modifies 'batch'
    
    # Indexing (returns a view)
    subbatch = batch[[0, 1, 3]]
    
    # Concatenation (returns a copy)
    second_batch = problem.generate_batch(3)
    new_batch = SolutionBatch.cat([last_3, second_batch])
    # Modifying 'new_batch' does NOT modify 'batch' or 'second_batch'
  12. How to implement a custom SearchAlgorithm

    master

    To define a custom search algorithm in EvoTorch, you must subclass SearchAlgorithm and implement two primary methods:

    1. __init__(self, problem: Problem): Accepts a Problem instance. You should call super().__init__(problem) to initialize the base class. This is where you perform initial setup, such as generating the initial population using self._problem.generate_batch(popsize).
    2. _step(self): The core iterative method. This method is called repeatedly by the .run() loop. Inside _step, you should:
      • Generate a new population (or update the existing one).
      • Evaluate the new population using self.problem.evaluate(new_population).
      • Update the internal state of the algorithm.

    Once these are implemented, the algorithm can be used with any Problem and any Logger (e.g., PandasLogger).

    from evotorch import Problem
    from evotorch.algorithms import SearchAlgorithm
    
    
    class MySearcher(SearchAlgorithm):
        def __init__(self, problem: Problem):
            super().__init__(problem)
            # any additional desired initialisation
            ...
    
        def _step(self):
            # Generate a new population
            new_population = ...
            # Evaluate the new population
            self.problem.evaluate(new_population)
            ...