Tutorial 1: Max Ones
In this tutorial, we will be using Wallace to implement a simple Genetic Algorithm to solve the benchmark Max Ones optimisation problem, in which the object is to maximise the number of ones in a fixed-length binary string. This problem is trivial for humans, of course, but proves to be a little trickier for "blind" evolutionary algorithms.
This tutorial assumes:
- A minimal knowledge of Julia.
- You know how to navigate the Julia REPL when using Wallace.
- You know how to load Wallace specification files and run them.
- You know a little bit about the basic structure of Wallace specification files.
By the end of this tutorial, you will be able to:
- Write a simple binary-string genetic algorithm from scratch.
- Use Wallace to solve simple binary-string optimisation problems, such as the Max Ones problem.
Creating a skeleton for our specification file.
To start off with, let's create an empty Wallace specification file within our
current working directory, called my_max_ones.cfg. Now let's open up our
skeleton specification in your favourite text editor and start writing a simple
GA to solve our problem.
Tip: If you're running Wallace through the Julia REPL, try using shell mode by typing ";" into the console; this will allow your to access command-line text editors such as
vimandnanofrom within the REPL session.
To begin with, we need to specify the type of the object that we wish our
specification to describe. In this case, we'll be using our specification to
describe a simple evolutionary algorithm to, so we prepend the
algorithm/simple_evolutionary_algorithm type to the front of our description
in order to let Wallace know how we want the object to be built.
type: algorithm/simple_evolutionary_algorithm
What happens if you omit the type of the root object in the specification file?
Then Wallace will be unable to construct the object when you callcompose(my_specification). However, you can still force Wallace to construct as if it were a given type by usingcompose_as(my_specification, some_type).
Specifying the components of our algorithm.
Now we have a skeleton for our algorithm specification in place, let's go about
specifying each of the components and parameters of our algorithm. Before we
can do this, we need to know more about the structure and properties of the
algorithm/simple_evolutionary_algorithm type; in order to find out this
information, we can call the properties or help function, as demonstrated
below:
julia> help("algorithm/simple_evolutionary_algorithm")
Properties:
* evaluator
* replacement
* termination
* population
* loggers
julia> help("algorithm/simple_evolutionary_algorithm:evaluator")
The method of evaluation used by this algorithm to assign fitness scores to
individuals.
Tip: If you're running Wallace through the Julia REPL, try using help mode by typing "?" into the console. Once in help mode, simply type the name of the Wallace or Julia type you wish to know more about.
Setting up the population.
To begin with, let's make use of the look-up operator, $, in order to specify
the population setup used by our algorithm. By default, the standard population
type within Wallace is composed of a number of demes (also known as sub-populations),
each of which undergoes its own evolutionary processes in isolation, perhaps running
on its own compute node. However, for our problem, we can use the far simpler
population/simple type to create a single deme and species population of a given
size.
Following the example below, we can create such a population, composed of 100 individuals of a single species and using a given breeding setup, both defined elsewhere within the file.
type: algorithm/simple_evolutionary_algorithm
population<population/simple>:
size: 100
species: $(_my_species)
breeder: $(_my_breeder)
Setting up the species and representation.
Now we have our single deme population in place, let's go about specifying its
species and the representation used by its members, by specifying the
_my_species property we referenced earlier. In order to find out what
information we need to provide our species definition with, we can once again
make use of help("species") to find out more information about a given type,
as demonstrated below:
julia> help("species")
Properties:
- stages
julia> help("species:stages")
An indexed collection describing each of the different stages of development
for an individual belonging to this species, indexed by the name of the
developmental stage.
Type: IndexedCollection{String, species_stage}
As we can see from above, species are represented by a series of developmental stages; this is one of the features unique to Wallace. For a lot of problems, we're only interested in using a single stage to represent the genome, but for more complex setups, such as grammatical evolution, we model the individual using a sequence of representations.
Using the help() function, find out the structure of species_stage
definitions, before composing one for your algorithm specification. In this
tutorial we won't be discussing how more complex multiple stage species are
composed.
julia> help("species_stage")
Describes a particular stage of individual development for a given species.
Properties:
- Representation
- From
- Lamarckian
For our simple, single-stage problem, the only parameter we need to concern
ourselves with is the representation parameter, which describes the
representation used by this stage of development; in the case of this problem,
we'll be using a bit vector. In order to find the appropriate type of
representation for our problem, try using the listall("PARENT_TYPE")
function to list all known representations registered with Wallace, before
using help() to figure out what parameters to supply it with.
Once you've done that, your species definition should look something like the one below:
_my_species:
stages:
bits:
representation: representation/bit_vector { length: 100 }
Specifying the breeding operations.
Now we have the species of our single deme in place, let's go about specifying the breeding operations responsible for producing the offspring for our deme at each generation. For this problem, we will be implementing a single method of selection, mutation and recombination, however, you may produce any arbitrary chain of operations that you wish.
To describe these operations, we use a breeder component; in this case we'll
be using the default breeder/fast breeder. The breeder is composed of a
number of sources, provided to its sources collection. For this problem we'll
be using selection sources to implement our selection methods, and variation
sources to implement our methods of crossover and mutation.
Each breeder source accepts a single operator, and in the case of variation
methods, it also accepts a stage property, specifying which stage of the
individual the operator is applied to. Variation sources also accept a source
parameter, informing the breeder which source should be used to provide inputs.
An example breeder setup is given below. Using the listall() and help()
functions, try to build your own selection-crossover-mutation breeding sequence.
...
_my_breeder<breeder/fast>:
sources:
s<selection>:
operator<selection/tournament>: { size: 2 }
x<variation>:
operator<crossover/one_point>: { rate: 1.0 }
source: "s"
stage: "bits"
m<variation>:
operator<mutation/bit_flip>: { rate: 0.1 }
source: "x"
stage: "bits"
...
Why doesn't the selector specify a source or stage?
The selector operates on individuals as a whole, considering only their fitness values; no changes are made to the individual. No source is required as the selector uses the contents of the deme it is connected to as its source.What happens if I omit the
stageproperty for a variation operator?
Wallace will assume the operator acts upon the canonical genome of an individual (rather than any of its other stages). Removing thestageproperty for this example should have no effect.What happens if I omit the
sourceproperty for a variation operator?
The breeder will fail to compile and will raise an error.Are there types of breeder sources other than
selectionandvariation?
Another less common type of breeder source is themultisource, which draws a requested number of individual from a number of attached breeding sources. The number of individuals picked from each source may be based on either fixed proportions or a given probability distribution.
Specifying a replacement scheme.
Setting up the evaluator.
Next, let's provide an evaluation function for algorithm so that we can assess
the relative fitness of potential solutions. For this problem, you should use
the evaluator/simple evaluator, which simply computes the fitness of each
individual within a given population using a provided objective function.
This objective function will be implemented as a Julia function, accepting a
single candidate individual, i, and producing a Fitness object containing
its calculated objective value. In order to provide a Julia function to the
Wallace specification language, we simply need to prepend | onto the start
of our objective definition, immediately after the opening colons; this
instructs Wallace to treat all the code within the block below as a single
block of text, and not as a specification.
To calculate the fitness of a candidate individual for our given problem, we
need to count the number of binary 1s within the bits stage of the given
individual. In order to access the candidate's bits stage, we simply call
get(i.bits). We can then calculate the number of ones within this string
by simply summing it using Julia's sum function. Finally, we wrap our
computed objective value within a new SimpleFitness object, and specify
that the objective should be maximised by setting its first parameter to
true.
...
evaluator<evaluator/simple>:
objective: ->
SimpleFitness{Int}(true, sum(get(i.bits)))
...
Why do I have to call
get(i.bits)rather thani.bits?
i.bitsstores anIndividualStageobject that acts as a lightweight proxy to the value of a given stage. This proxy is required to avoid illegally accessing an undefined stage. Although this feature serves little use in this example, it becomes critical when performing grammatical evolution, where the latter stages of an individual may fail to generate.How do I check if a given stage is safe to access?
By simply callingissafe(i.NAME), whereNAMEis replaced by the name of the stage you wish to check; if the stage is safe to access, the function will returntrue, else it will returnfalse.
Specifying the termination conditions.
Finally, provide your algorithm specification with a set of termination
conditions, implemented using the termination property, as shown in the example
below. Once the state of the algorithm has satisfied any of these conditions, it
will terminate before the start of the next iteration. For now, let's specify
a simple limit on the number of iterations that the algorithm may run for.
...
termination:
iterations<criterion/iterations>: { limit: 100 }
...
What happens if I don't supply any termination conditions?
The algorithm won't terminate until you force the program to close; we don't advise doing this!What other types of termination condition are there?
Calllistall("termination")from within the REPL to produce a list of all known termination condition types registered with Wallace. To find out more about a particular type, just callhelp("name_of_type")in the REPL.
Running the algorithm and analysing the results.
Having followed the steps above, you should be left with a specification looking somewhat similar to the one given below:
type: algorithm/simple_evolutionary_algorithm
evaluator<evaluator/simple>:
objective: |
SimpleFitness{Int}(true, sum(get(i.bits)))
replacement<replacement/generational>: { elitism: 0 }
termination:
iterations<criterion/iterations>: { limit: 1000 }
_my_species:
stages:
bits:
representation<representation/bit_vector>: { length: 100 }
_my_breeder<breeder/fast>:
sources:
s<selection>:
operator<selection/tournament>: { size: 2 }
x<variation>:
operator<crossover/one_point>: { rate: 1.0 }
source: s
stage: bits
m<variation>:
operator<mutation/bit_flip>: { rate: 0.1 }
source: x
stage: bits
population:
demes:
- capacity: 100
breeder: $(_my_breeder)
species: $(_my_species)