Need to run a simple Genetic Algorithm in CoffeeScript? I did, so I wrote this library. Define your Chromosome, write your evaluation function and let the algorithm do its thing! Genie can now use Web Workers!
First, some definitions:
-
A Chromosome is a set of genes, which belongs to the current population.
-
A **Gene** is an individual piece of a **Chromosome**, which provides a single piece of information to the evaluation function. A group of **Genes** make up a **Chromosome**. When two parent **Chromosomes** come together to make a new child Chromosome, **Genes** are inherited from both parents, and can also be mutated. For now, each **Gene** is a numerical value.
-
A set of **Chromosomes**. The whole point of a genetic algorithm is to determine the best solution to a problem by evaluating the *fitness* of each member of the population. The weakest members of the population (the **Chromosome** with the lowest fitness) are killed, and replaced by the children of the stronger **Chromosome**.
-
The evaluation function determines the fitness of a particular **Chromosome** to the problem at hand.
Initialises the genetic algorithm. Takes one required argument:
options
- An oject containing the following properties
logging
- (boolean) - Whether Genie will log to the console or not. (Default:false
)mutationRate
- (number) - The chance that a given Gene will mutate. Must be from0
to1
. (Default:0.1
)numberOfGenes
- REQUIRED (number) - The number of Genes that are in each Chromosome.populationSize
- (number) - The number of Chromosomes in the population. Must be positive. (Default:32
)survivalRate
- (number) - The chance that a given Chromosome survives. Must be from0
to1
. (Default:0.1
)useWorkers
- (number) - Whether Genie will try to use WebWorkers or not. (Default:false
)There are
3
additional properties that are required or not, depending on the value ofuseWorkers
:
evaluateFitness
- REQUIRED ifuseWorkers
isfalse
. (function) - The function that determines and reports the fitness of a given Chromsome.workerScriptPath
- REQUIRED ifuseWorkers
isn'tfalse
. (string) - The path to the location of the Worker script that determines and reports the fitness of a given Chromosome.workerMessageHandler
- REQUIRED ifuseWorkers
isn'tfalse
. (function) - The function that handlers messages from the Worker script.These options are further explained in the examples below.
Starts the genetic algorithm. An error will be thrown is
Genie.init
isn't called first!
Records the result for a Chromosome. Takes two required arguments:
fitness
- the numerical fitness score for a Chromosome.index
- the numerical index that identifies the Chromosome.
Returns the current best fitting Chromosome and it's fitness score.
Returns the current generation number.
Say we want to find the largest sum of a set of 8 numbers.
First, we add the compiled genie.js file to our page, giving us access to the Genie module:
<script type='text/javascript' src='libraries/genie.js'></script>
Then, we initialise the options for the GA. There are only TWO required options:
1. numberOfGenes
- the number of Genes that are in each Chromosome.
2. evaluateFitness
- the function that is run to evaluate the fitness of a given Chromosome. When the function is complete, it must call Genie.reportFitness()
with the fitness value of that Chromosome. Within evaluateFitness
you also have access to:
-
the generation number -
Genie.getGeneration()
-
the current best fitting Chromosome -
Genie.getBestFit
. -
As well as these, the actual
chromosome
and thechromosomeIndex
are passed as parameters to the function.
There are several other optional options:
-
populationSize
- the number of Chromosomes in the population. Must be a positive number. (Defaults to32
) -
mutationRate
- the chance that a given Gene will mutate. Must be from0
to1
. (Defaults to0.1
) -
survivalRate
- the chance that a given Chromosome will survive. Must be from0
to1
. (Defaults to0.1
) -
logging
- whether or not the GA will output what it is doing. (Defaults tofalse
)
For our example of finding the largest sum of 8
numbers, our numberOfGenes
should be 8
. This means that each time our evaluateFitness
function is run, a set of 8
numbers will be passed in as a parameter.
Our evaluation function will look something like this:
evaluateFitness = (chromosome, chromosomeIndex) ->
# Define a `sum` function to add up the numbers in an array:
sum = (arr, s = 0) ->
for val in arr
s += val
s
# Report the result of the summation back to Genie, along with the `chromosomeIndex`:
Genie.reportFitness sum(chromosome.genes), chromosomeIndex)
# Log some information about the progress:
console.log Genie.getBestFit
The other properties can be set as you please:
options =
evaluateFitness: evaluateFitness
mutationRate: 0.2
numberOfGenes: 8
populationSize: 64
options
should be passed into the Genie.init
function:
Genie.init options
Finally, we start the algorithm:
Genie.run()
Assuming that we have the Genie script in the page, we again need to initialise it with the right options.
The first difference is that we need to pass in the useWorkers
option. This defaults to false
, but if you want to use Workers, it should be a numerical value.
One option is to use this library to get the right number for the device that you are using, but you can just pass any set number.
When we assign the useWorkers
option, Genie will no longer look for an evaluateFunction
option. Instead it requires:
1. workerScriptPath
: The path to the worker script.
2. workerMessageHandler
: A function that handles the result coming from the Worker threads.
In order to be as flexible in this regard as possible, the Worker script and the handler function can be whatever you want, as long as they follow a few small rules:
When Genie starts the Worker, it passes the string 'evaluateFitness'
as a func
property.
The Worker message
handler needs to look for that property (e.data.func
), and then run the function to evaluate the fitness of the Chromosome.
Other useful data is sent along with the func
property:
e.data.chromsome
- The actual Chromosome object to be evaluatede.data.index.chromosomeIndex
- The index of the Chromosome in the populatione.data.index.workerIndex
- The index of the Worker.
When the worker is finished evaluating the Chromosome, it has to report the result back to the main thread using self.postMessage
.
As you will define the function that handles how that message is processed, you can send back the result however you want. However, you need to make sure you also send back the index
of the chromosome.
So, our base Worker script (genieWorkerScript.js) should look something like this:
importScripts 'myEvaluationFunctionModule.js'
messageHandler = (e) ->
{ func } = e.data
switch func
when 'evaluateFitness'
{ chromosome, index} = e.data
{ workerIndex, chromosomeIndex } = index
result = someBigFunctionThatWillEvaluateThis chromosome
self.postMessage
func: 'complete'
result: result
index: chromosomeIndex
self.addEventListener 'message', messageHandler, false
The message handler can do whatever you want! It is here that you will have access to the Genie module outside your worker script, so you need to call Genie.reportFitness
with the result and the chromosome index.
Our handler might look something like this:
messageHandler = (e) ->
{ func } = e.data
switch func
when 'complete'
Genie.reportFitness e.data.result, e.data.index
Then we can initialise and run Genie just like the other example:
options =
useWorkers: 4
workerScriptPath: 'genieWorkerScript.js'
workerMessageHandler: messageHandler
mutationRate: 0.2
numberOfGenes: 8
populationSize: 64
Genie.init options
Genie.run()
And that's all there is to it!
- 0.2.0 -
- WebWorkers! Genie can now use WebWorkers to run the evaluation functions heaps faster! (You might want to use this library to work out how many WebWorkers you should use!)
- Instead of having a Genie class, there is now a Genie module
- General refactoring!
-
Tests
-
Make it better
- Add the ability to provide existing population members.
- Before and after functions.
- Handle different Gene types.