In it's most basic form, a genetic algorithm solves a mathematically posed problem by doing the following:
- Generate random solutions.
- Evaluate the solutions.
- Sort the solutions by increasing (or decreasing) order.
- Apply genetic operators (such as mutation and crossover).
- Repeat from step 2 until satisfied.
Genetic algorithms can be applied to many problems, the only variable being the problem itself. Indeed, the underlying structure does not have to change between problems. With this in mind, gago
has been built to be reusable. What's more, gago
is a multi-population genetic algorithm implementing the migration model, in that sense it performs better than a traditional genetic algorithm.
Genetic algorithms are notorious for being embarrassingly parallel. Indeed, most calculations can be run in parallel because they only affect one individual. Luckily Go provides good support for parallelism. As some gophers may know, the math/rand
module can be problematic because there is a global lock the random number generator, the problem is described in this stackoverflow post. This can be circumvented by providing each deme with it's own generator.
The terms surrounding genetic algorithms (GAs) are roughly analogous to those found in biology.
- GAs are intended to optimize a function called the fitness function.
- Candidate solutions to the optimization problem are called individuals.
- Each individual has genes to which a fitness is assigned.
- The number of genes is simply the number of variables defined by the problem.
- Individuals are sorted based on their fitness towards a problem.
- Offsprings are born by applying crossover on selected individuals.
- The selection method is crucial and determines most of the behavior of the algorithm.
- Genes can be randomly modified through mutation.
- Classically, individuals are contained in a structure called a population.
- Multi-population GAs add another layer in-between called demes.
- Demes exchange individuals through a process known as migration.
The following code shows a basic usage of gago
.
package main
import (
"math"
"github.com/MaxHalford/gago"
)
// Sphere function minimum is 0
func sphere(X []float64) float64 {
sum := 0.0
for _, x := range X {
sum += math.Pow(x, 2)
}
return sum
}
func main() {
// Instantiate a population
ga := gago.Float
// Wrap the function
ga.Ff = gago.FloatFunction{sphere}
// Initialize the genetic algorithm with two variables per individual
ga.Initialize(2)
// Enhance the individuals
for i := 0; i < 20; i++ {
ga.Enhance()
}
// Display the best individual
ga.Best.Display()
}
The user has a function Sphere
he wishes to minimize. He initiates a predefined set of parameters gago.Float
. He then wraps his function with gago.FloatFunction{Sphere}
so that gago
knows what it has to minimize. Finally the user orders gago
to find an appropriate solution with ga.Enhance()
. By convention gago
will try to minimize the fitness function. If instead you want to maximize a function f(x)
, you can minimize -f(x)
or 1/f(x)
.
To modify the behavior off the GA, you can change the gago.Population
struct before running ga.Initialize
. You can either instantiate a new gago.Population
or use a predefined one from the configuration.go
file.
Variable in the code | Type | Description |
---|---|---|
NbDemes |
int |
Number of demes in the population. |
NbIndividuals |
int |
Number of individuals in each deme. |
NbGenes |
int |
Number of genes in each individual. |
Initializer (struct) |
Initializer (interface) |
Method for initializing a new individual. |
Selector (struct) |
Selector (interface) |
Method for selecting one individual from a group of individuals. |
Crossover (struct) |
Crossover (interface) |
Method for producing a new individual (called the offspring). |
Mutators (struct) |
[]Mutator (slice of interface) |
Method for modifying an individual's genes. |
Migrator (struct) |
Migrator (interface) |
Method for exchanging individuals between the demes. |
The gago.Population
struct also contains a Best
variable which is of type Individual
, it represents the best individual overall demes for the current generation. Alternatively the Demes
variable is a slice containing each deme in the population; the demes are sorted at each generation so that the first individual in the deme is the best individual from that deme.
gago
is designed to be flexible. You can change every parameter of the algorithm as long as you implement functions that use the correct types as input/output. A good way to start is to look into the source code and see how the methods are implemented, I've made an effort to comment it. If you want to add a new generic operator (initializer, selector, crossover, mutator, migrator), then you can simply copy and paste an existing method into your code and change the logic as you please. All that matters is that you correctly implement the existing interfaces.
If you wish to not use certain genetic operators, you can set them to nil
. This is available for the Crossover
, the Mutator
and the Migrator
(the other ones are part of minimum requirements).
Some genetic operators target a specific type, these ones are prefixed with the name of the type (Float
, String
). The ones that don't have prefixes work with any types, which is down to the way they are implemented. Default configurations are available in configuration.go
.
You should think of gago
as a framework for implementing your problems, and not as an all in one solution. It's quite easy to implement your own for exotic problems, for example the TSP problem.
The only requirement for solving a problem is that the problem can be modeled as a function that returns a floating point value, that's it. Because Go is statically typed, you have to provide a wrapper for the function and make sure that the genetic operators make sense for your problem.
- godoc
- Each operator (selection, crossover, mutation, migration) is described in it's comments.
- An introduction to genetic algorithms is quite thorough.
- The Multipopulation Genetic Algorithm: Local Selection and Migration is an easy read.
- Check out the examples/minimization/ folder for basic examples. Test functions were found here.
- examples/plot-fitness/ is an example of plotting the fitness per generation with gonum/plot.
- examples/curve-fitting/ is an attempt to fit a set of points with non-linear polynomial function.
- examples/tsp/ contain examples of solving the Traveling Salesman Problem.
- Error handling.
- More tests.
- Statistics.
- Benchmarking.
- Compare with other algorithms/libraries.
- Implement/generalize genetic operators.
- Add possibility to apply multiple genetic operators of the same kind.
- More examples.
- Please post suggestions/issues in GitHub's issues section.
- You can use the reddit thread or my email address for comments/enquiries.
- I'm quite happy with the syntax and the naming in general, however things are not set in stone and some stuff may change to incorporate more functionalities.
- Genetic algorithms are a deep academic interest of mine, I am very motivated to maintain
gago
and implement state-of-the-art methods. - Although I believe the code structure is quite simple and well structured, I've split the logic into separate functions for testing purposes. Don't worry if you see functions call other ones, it's sane.
- As far as I know the
GOMAXPROCS
from theruntime
library defaults to the number of available thread, hence we haven't set it in the source code.