Helisa (HEL-EE-SAH) is a Scala frontend for solving problems with genetic algorithms and genetic programming.
It’s pretty much a Scala wrapper around the excellent jenetics
library [1].
helisa
is available on Maven central through:
"com.softwaremill" %% "helisa" % "0.8.0"
Currently only Scala 2.12 is supported.
The two things that are absolutely required are:
-
A genotype, i.e. a representation of possible solutions to the problem,
-
a fitness function, that scores the solutions generated from the genotypes.
Using an example for guessing a number between 0 and a 100, you would have:
import com.softwaremill.helisa._
case class Guess(num: Int) (1)
val genotype =
() => genotypes.uniform(chromosomes.int(0, 100)) (2)
def fitness(toGuess: Int) =
(guess: Guess) => 1.0 / (guess.num - toGuess).abs (3)
-
The representation of a solution to the problem (the phenotype)
-
A producer of genotypes.
-
The fitness function - the closer to the target number, the higher the fitness score.
We use the code above to set up the Evolver
, which encapsulates all configuration and generates
fresh population streams:
val evolver =
Evolver(fitness(Number), genotype) (1)
.populationSize(100) (2)
.build()
val stream = evolver.streamScalaStdLib() (3)
val best = stream.drop(1000).head.bestPhenotype (4)
println(best)
// Some(Guess(42))
-
Initialize the
Evolver
with our genotype and fitness function. -
Set the population size.
-
Obtain a
Stream
population stream (see Integrating for more information) -
Advance the stream and obtain the highest-scored phenotype.
You can additionally restrict the solution space by adding a phenotype validator:
val evolver =
Evolver(fitness(Number), genotype)
.populationSize(100)
.phenotypeValidator(_.num % 2 == 0) (1)
.build()
-
We know the number is even, so we’re restricting possible solutions to only those numbers.
As a reminder, the three main elements of evolution in genetic algorithms are:
-
the selection of fittest individuals (phenotypes),
-
the recombination of selected individuals to form new individuals in the next generation of the population,
-
the mutation of the new/remaining individuals.
Standard selectors are available from helisa.selectors
, you use them like this:
import com.softwaremill.helisa._
val evolver =
Evolver(fitnessFunction, genotype)
.offspringSelector(selectors.x) (1)
.survivorsSelector(selectors.y) (2)
.build()
-
Affect just the survivors.
-
Affects both the survivors and offspring.
You can also add a custom selector by passing the appropriate function to the survivorSelectorAsFunction or offspringSelectorAsFunction method.
Recombination and mutation is handled are both generalized to operators, available in helisa.operators
.
You use them as follows:
import com.softwaremill.helisa._
val evolver =
Evolver(fitnessFunction, genotype)
.geneticOperators(operators.crossover.x, (1)
operators.mutation.y) (2)
.build()
-
Recombination operators.
-
Mutation operators.
You can also add a custom operator by passing the appropriate function to the geneticOperatorsAsFunctions method.
See the doc of EvolverBuilder
for all Evolver
configuration options.
Integrations for:
-
scala.collection.Iterator
-
scala.collection.Stream
-
Akka Stream
Source
-
FS2
Stream
for anyAsync
-
Reactive Streams
Publisher
In addition:
-
Monix is not supported directly, but can be taken advantage with using the other integrations,
-
Spark integration is coming up.