Continuous Optimisation (Ackley Function)ΒΆ

How does ARIEL EC module work?ΒΆ

The ARIEL EC (Evolutionary Computing) module works a bit different than other EAs (Evolutionary Algorithms). While other EAs represent the population as a simple list of individuals, here they the population is made as its own class, with a population being type of list[Individuals].

Similarly, in traditional EA architecture an individual is chosen for certain operators (for example parent selection) according to some criteria, put into a separate list and then given to the operator function. ARIEL works by giving individuals what we call tags. An individual has tags that can be toggled, which qualify it for any and all operations. The tag can be whether an individual can crossover or mutate in the future, but it can also show if it can enter the learning cycle.

The tags can be changed at all times, and default values for each tag can be given to more closely represent a traditional EA structure.

Additionally, ARIEL utilizes an SQL database to handle the variables and outputs of the code. This makes the code run faster, but it adds an extra step to the process.

This file demonstrates the process of initializing an EA class and running it for a simple problem, in our case, the Sphere function.

[ ]:
# Standard library
import random
from typing import Literal, cast

# Third-party libraries
import numpy as np

# Function to show fitness landscape
from fitness_plot import fitness_landscape_plot, plot_fit_per_gen

# Pretty little errors and progress bars
from rich.console import Console
from rich.traceback import install

# Local libraries
from ariel.ec.a001 import Individual
from ariel.ec.a004 import EA, EASettings, EAStep, Population
from ariel.ec.a005 import Crossover

Define fitness functionΒΆ

[ ]:
# Define the fitness function
def Ackley(x):
    """source: https://www.sfu.ca/~ssurjano/ackley.html."""
    x = np.array(x)
    # Ackley function parameters
    a = 20
    b = 0.2
    c = 2 * np.pi
    dimension = len(x)

    # Individual terms
    term1 = -a * np.exp(-b * np.sqrt(sum(x**2) / dimension))
    term2 = -np.exp(sum(np.cos(c * xi) for xi in x) / dimension)
    return term1 + term2 + a + np.exp(1)


def evaluate_ind(ind: Individual) -> float:
    """Evaluate an individual by calculating its fitness using the Ackley function."""
    return Ackley(cast("list[float]", ind.genotype))


def evaluate_pop(population: Population) -> Population:
    """Evaluate a population by calculating the fitness of each individual."""
    for ind in population:
        if ind.requires_eval:
            ind.fitness = evaluate_ind(ind)
    return population


fitness_landscape_plot()

../../_images/source_EA_intro_ea_example_ackley_4_0.png

Initialize the global constantsΒΆ

[35]:
# A seed is optional, but it helps with reproducibility
SEED = None  # e.g., 42

# The database has a few handling modes
    # "delete" will delete the existing database
    # "halt" will stop the execution if a database already exists
DB_HANDLING_MODES = Literal["delete", "halt"]

# Initialize RNG
RNG = np.random.default_rng(SEED)

# Initialize rich console and traceback handler
install()
console = Console()

Initialize the EASettings class.ΒΆ

The EASettings class acts as the handles the database and other parameters

[36]:
# Set config
config = EASettings()
config.is_maximisation = False
config.db_handling = "delete"
config.target_population_size = 100

And just like that we have everything we need to get started. Now all we need to do define our evolutionary operatorsΒΆ

Keep in mind that all operators in ARIEL have to work with the Individual and Population classes. You could define your own operators from scratch, but using the built in ones is easier.

[ ]:
def create_individual(num_dims) -> Individual:
    ind = Individual()
    ind.genotype = cast("list[float]", np.random.normal(loc=0,
                                                        scale=50,
                                                        size=num_dims).tolist())
    return ind


def parent_selection(population: Population) -> Population:
    """Tournament Selection."""
    tournament_size: int = 5

    # Ensure all individuals have a tags dict and reset parent-selection tag
    for ind in population:
        if ind.tags is None:
            ind.tags = {}
        ind.tags["ps"] = False

    # Decide how many parents we want (even number)
    num_parents = (len(population) // 2) * 2
    if num_parents == 0 and len(population) >= 2:
        num_parents = 2

    winners = []
    for _ in range(num_parents):
        # sample competitors with replacement
        competitors = [random.choice(population) for _ in range(tournament_size)]

        # pick best competitor depending on maximisation/minimisation
        if config.is_maximisation:
            winner = max(competitors, key=lambda ind: ind.fitness)
        else:
            winner = min(competitors, key=lambda ind: ind.fitness)

        winners.append(winner)

    # mark winners as parents
    for w in winners:
        w.tags["ps"] = True

    return population


def crossover(population: Population) -> Population:
    """One point crossover."""
    parents = [ind for ind in population if ind.tags.get("ps", False)]
    for idx in range(0, len(parents), 2):
        parent_i = parents[idx]
        parent_j = parents[idx]
        genotype_i, genotype_j = Crossover.one_point(
            cast("list[float]", parent_i.genotype),
            cast("list[float]", parent_j.genotype),
        )

        # First child
        child_i = Individual()
        child_i.genotype = genotype_i
        child_i.tags = {"mut": True}
        child_i.requires_eval = True

        # Second child
        child_j = Individual()
        child_j.genotype = genotype_j
        child_j.tags = {"mut": True}
        child_j.requires_eval = True

        population.extend([child_i, child_j])
    return population


def mutation(population: Population) -> Population:
    for ind in population:
        if ind.tags.get("mut", False):
            genes = list(ind.genotype)
            if random.random() < 0.5:
                mutated = [i + random.uniform(-5, 5) for i in genes]
            else:
                mutated = genes.copy()

            ind.genotype = mutated
    return population


def survivor_selection(population: Population) -> Population:
    tournament_size: int = 5

    # Decide how many parents we want (even number)
    pop_len = len(population)
    # if num_survivors == 0 and len(population) >= 2:
    #     num_survivors = 2

    for _ in range(config.target_population_size):
        # Sample competitors with replacement
        pop_alive = [ind for ind in population if ind.alive is True]
        death_candidates = [random.choice(pop_alive) for _ in range(tournament_size)]

        # Pick best competitor depending on maximisation/minimisation
        if config.is_maximisation:
            about_to_be_killed_lol = min(death_candidates, key=lambda ind: ind.fitness)
        else:
            about_to_be_killed_lol = max(death_candidates, key=lambda ind: ind.fitness)

        about_to_be_killed_lol.alive = False

        pop_len -= 1
        if pop_len <= config.target_population_size:
            break

    return population

Define evolutionary loopΒΆ

Now that all our operators are done, we can define the evolutionary loop and run the algorithm

[38]:
def main(pop_size) -> EA:
    """Entry point."""
    # Create initial population
    population_list = [create_individual(num_dims=10) for _ in range(pop_size)]
    population_list = evaluate_pop(population_list)

    # Create EA steps
    ops = [
        EAStep("parent_selection", parent_selection),
        EAStep("crossover", crossover),
        EAStep("mutation", mutation),
        EAStep("evaluation", evaluate_pop),
        EAStep("survivor_selection", survivor_selection),
    ]

    # Initialize EA
    ea = EA(
        population_list,
        operations=ops,
        num_of_generations=100,
    )

    ea.run()

    best = ea.get_solution("best", only_alive=False)
    console.log(best)

    median = ea.get_solution("median", only_alive=False)
    console.log(median)

    worst = ea.get_solution("worst", only_alive=False)
    console.log(worst)

    console.log(ea.target_population_size)

    return ea
[39]:
ea = main(pop_size=100)
[14:15:25] Database file exists at                                                                      a004.py:105
           /Users/johng/Documents/JohnGrigoriadis_Thesis/ariel/docs/source/EA_intro/__data__/database.d            
           b!                                                                                                      
           Behaviour is set to: 'delete' --> ⚠️  Deleting file!                                                     
───────────────────────────────────────────────── EA Initialised ──────────────────────────────────────────────────
─────────────────────────────────────────────── EA Finished Running ───────────────────────────────────────────────
[14:15:26] Individual(                                                                             1599251029.py:26
               time_of_death=100,                                                                                  
               alive=True,                                                                                         
               fitness_=4.470366137487645,                                                                         
               genotype_=[                                                                                         
                   -0.13032007834864778,                                                                           
                   3.3213379675624175,                                                                             
                   4.916682994651243,                                                                              
                   5.363719810581923,                                                                              
                   2.809325171397628,                                                                              
                   -4.486065373468503,                                                                             
                   -3.4010540101260958,                                                                            
                   0.34903523310947016,                                                                            
                   1.3745800486801416,                                                                             
                   -1.1707287367311103                                                                             
               ],                                                                                                  
               time_of_birth=93,                                                                                   
               id=3828,                                                                                            
               requires_eval=False,                                                                                
               requires_init=False,                                                                                
               tags_={'mut': True}                                                                                 
           )                                                                                                       
           Individual(                                                                             1599251029.py:29
               time_of_death=39,                                                                                   
               alive=False,                                                                                        
               fitness_=19.73576839189217,                                                                         
               genotype_=[-8, -10, -11, -9, -42, -25, -7, -24, 2, -36],                                            
               time_of_birth=36,                                                                                   
               id=1528,                                                                                            
               requires_eval=False,                                                                                
               requires_init=False,                                                                                
               tags_={'mut': True}                                                                                 
           )                                                                                                       
           Individual(                                                                             1599251029.py:32
               time_of_death=4,                                                                                    
               alive=False,                                                                                        
               fitness_=22.20891146577531,                                                                         
               genotype_=[                                                                                         
                   -74.50105308964281,                                                                             
                   70.44962744782093,                                                                              
                   -72.39744685359017,                                                                             
                   -65.50114647937897,                                                                             
                   2.5896785759546566,                                                                             
                   48.59713439086729,                                                                              
                   12.38689756380671,                                                                              
                   39.63507973150107,                                                                              
                   -7.57616888027764,                                                                              
                   -27.046695769648657                                                                             
               ],                                                                                                  
               time_of_birth=4,                                                                                    
               id=226,                                                                                             
               requires_eval=False,                                                                                
               requires_init=False,                                                                                
               tags_={'mut': True}                                                                                 
           )                                                                                                       

To do the plotting, we need to get the data out of the database. The database is automatically created when using the EA class with EAStep.

There are many ways to get data out of the database, we have separate pages for doing is with Pandas in the ARIEL Database Handling section of the documentation.

[40]:
plot_fit_per_gen()
../../_images/source_EA_intro_ea_example_ackley_15_0.png