Customization
Genex offers a number of operators and strategies for crafting Genetic Algorithms; however, there is often a need to have full control over the process.
This guide is an attempt to illustrate the flexibility and power offered by Genex.
Configuration
Genex offers basic customization through a number of configuration options. Please see the Configuration guide for more information.
Rates
In some cases, you will need to customize rates based on some population characteristics. Genex uses 3 rates throughout the GA life cycle: crossover_rate
, mutation_rate
, radiation
.
Crossover Rate
The crossover rate dictates the number of parents to breed during the crossover phase. This number n
equals Floor(Crossover Rate * Population Size)
.
The default crossover rate is 0.75
. This number means 75 percent of the chromosomes in the population are selected for crossover during each generation.
The crossover rate can be changed by overriding the crossover_rate/1
function in your implementation module. The function accepts a population struct and returns a float between 0 and 1.
defmodule MyGA do
use Genex
...
def crossover_rate(population), do: 0.60
end
Mutation Rate
The mutation rate dictates the number of chromosomes mutated during each generation. This number n
equals Floor(Mutation Rate * Population Size)
.
The default mutation rate is 0.05
. This number means 5 percent of the chromosomes in the population are mutated during each generation.
The mutation rate can be changed by overriding the mutation_rate/1
function in your implementation module. The function accepts a population struct and returns a float between 0 and 1.
defmodule MyGA do
use Genex
...
def mutation_rate(population), do: 0.10
end
Radiation
The radiation level dictates the "aggressiveness" of mutation. It is used to determine the number of genes in the chromosome that are altered when a chromosome is selected for mutation.
The default radiation level is 0.5
. This number means 50 percent of the chromosome on average will be changed during mutation.
The radiation level can be changed by overriding the radiation/1
function in your implementation module. The function accepts a population struct and returns a float between 0 and 1.
defmodule MyGA do
use Genex
...
def radiation(population), do: 0.25
end
Seeding the Population
Constructing an initial population is the first step in the Genex life cycle. By default, Genex constructs a list of n
chromosomes where n
is equal to the population_size
parameter. These chromosomes are constructed using the encoding/0
function to generate a random set of genes for each.
Sometimes it's better to start with a known, pre-constructed population. This is entirely possible thanks to Genex's flexibility. This ability is especially useful when you have a population stored in a file or database.
You can start with a pre-constructed population by overriding the seed/0
function in your implementation module. The function MUST return {:ok, %Population{}}
. Please not that you also must initialize the history
field of the population struct using Genealogy.init()
as well as the size
field of the population.
defmodule MyGA do
use Genex
...
def seed do
history = Genex.Genealogy.init()
chromosomes = read_from_somewhere()
pop = %Population{chromosomes: chromosomes, history: history, size: length(chromosomes)}
{:ok, pop}
end
end
Evaluating the Population
While most Genetic Algorithms evaluate chromosomes individually based on some predefined fitness function, you may find yourself needing to adjust the evaluation strategy of your algorithm.
To do so, override the evaluation/1
function in your implementation module. For best results, ensure your function assigns: fitness to each of the chromosomes, a strongest chromosome, and the max fitness for the population. The function MUST return {:ok, %Population{...}}
.
Please also note that in order to avoid changing other fields in the population struct inadvertently, you must use Elixir's Map
update syntax.
defmodule MyGA do
use Genex
...
def evaluate(population) do
chromosomes =
population.chromosomes
|> Enum.map(fn c -> some_custom_evaluation_technique(c) end)
strongest = Enum.max_by(chromosomes, &Chromosome.get_fitness/1)
pop = %Population{population | chromosomes: chromosomes, strongest: strongest, max_fitness: strongest.fitness}
{:ok, pop}
end
end
Parent Selection
Genex comes with a number of prepackaged selection operators. However, there are some limitations to what the library provides. Genex offers the ability to implement a customized parent selection phase.
As of this version of Genex, there is no ability to grow or shrink the population by default. Customizing this function offers a way around this shortcoming.
In order to customize parent selection, override the select_parents/1
function in your implementation module. Please note that your function MUST assign a List
of Tuple
of Chromosomes to the parents
field of the returned population struct. The tuples MUST be 2-tuples and your function MUST return {:ok, %Population{}}
.
Please also note that in order to avoid changing other fields in the population struct inadvertently, you must use Elixir's Map
update syntax.
defmodule MyGA do
use Genex
...
def select_parents(population) do
chromosomes = population.chromosomes
n = floor(crossover_rate(population) * length(chromosomes))
parents =
chromosomes
|> some_selection_method(n)
|> Enum.chunk_every(2, 2, :discard)
|> Enum.map(fn f -> List.to_tuple(f) end)
pop = %Population{population | parents: parents}
{:ok, pop}
end
end
Crossover
Genex comes with a number of prepackaged crossover operators. However, there are some limitations to what the library provides. Genex offers the ability to customize the crossover phase.
In order to customize crossover, override the crossover/1
function in your implementation module. You can access the parents selected for crossover in the parents
field of the population struct. Please note your function MUST assign a List
of chromosomes to the children
field of the population struct. The function MUST return {:ok, %Population{}}
. Additionally, you may want to update the history
by making calls to Genealogy.update/4
after each new child is created.
Please also note that in order to avoid changing other fields in the population struct inadvertently, you must use Elixir's Map
update syntax.
defmodule MyGA do
use Genex
...
def crossover(population) do
children =
population.parents
|> Enum.map(
fn {p1, p2} ->
{c1, c2} = some_crossover(p1, p2) end)
Genealogy.update(population.history, c1, p1, p2)
Genealogy.update(population.history, c2, p1, p2)
[c1, c2]
end
|> List.flatten()
pop = %Population{population | children: children}
{:ok, pop}
end
end
Mutation
Genex comes with a number of mutation oeprators. However, there are some limitations to what the library provides. Genex offers the ability to customize the mutation phase.
In order to customize mutation, override the mutate/1
function in your implementation module. You should access and modify the chromosomes
field of the population struct. The function accepts a population struct. Your function MUST return {:ok, %Population{}}
.
*Please also note that in order to avoid changing other fields in the population struct inadvertently, you must use Elixir's Map
update syntax.
defmodule MyGA do
use Genex
...
def mutate(population) do
chromosomes =
population.chromosomes
|> Enum.map(some_mutation_function(mutation_rate()))
pop = %Population{population | chromosomes: chromosomes}
{:ok, pop}
end
end
Survivor Selection
Genex supports customization of the "reinsertion" phase of Genetic algorithms using it's select_survivors/1
method. Later versions of Genex will support defining "kill criteria" and other constraints; however, this method will allow you to customize how populations grow and decay over time.
In order to customize survivor selection, override the select_survivors/1
method in your implementation module. This function accepts a population struct. To ensure smooth running of the rest of the algorithm, this function should populate the survivors
field of the population struct it returns. This function MUST return {:ok, %Population{}}
.
defmodule MyGA do
use Genex
...
def select_survivors(population) do
survivors =
population.chromosomes
|> Enum.filter(fn c -> c.age < 5 end)
pop = %Population{population | survivors: survivors}
{:ok, pop}
end
end