Skip to content

Commit

Permalink
Rename Enhance to Evolve and add parallel methods
Browse files Browse the repository at this point in the history
  • Loading branch information
MaxHalford committed Nov 28, 2017
1 parent def1672 commit 6c17807
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 75 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func main() {

fmt.Printf("Best fitness at generation 0: %f\n", ga.Best.Fitness)
for i := 1; i < 10; i++ {
ga.Enhance()
ga.Evolve()
fmt.Printf("Best fitness at generation %d: %f\n", i, ga.Best.Fitness)
}
}
Expand Down Expand Up @@ -301,7 +301,7 @@ You have to fill in the first set of fields, the rest are generated when calling
- `Migrator` and `MigFrequency` should be provided if you want to exchange individuals between populations in case of a multi-population GA. If not the populations will be run independently. Again this is an advanced concept in the genetic algorithms field that you shouldn't deal with at first.
- `Speciator` will split each population in distinct species at each generation. Each specie will be evolved separately from the others, after all the species has been evolved they are regrouped.
- `Logger` is optional and provides basic population statistics, you can read more about it in the [logging section](#logging-population-statistics).
- `Callback` is optional will execute any piece of code you wish every time `ga.Enhance()` is called. `Callback` will also be called when `ga.Initialize()` is. Using a callback can be useful for many things:
- `Callback` is optional will execute any piece of code you wish every time `ga.Evolve()` is called. `Callback` will also be called when `ga.Initialize()` is. Using a callback can be useful for many things:
- Calculating specific population statistics that are not provided by the logger
- Changing parameters of the GA after a certain number of generations
- Monitoring for converging populations
Expand All @@ -310,13 +310,13 @@ You have to fill in the first set of fields, the rest are generated when calling
- Fields populated at runtime
- `Populations` is where all the current populations and individuals are kept.
- `HallOfFame` contains the `NBest` individuals ever encountered. This slice is always sorted, meaning that the first element of the slice will be the best individual ever encountered.
- `Age` indicates the duration the GA has spent calling the `Enhance` method.
- `Generations` indicates how many times the `Enhance` method has been called.
- `Age` indicates the duration the GA has spent calling the `Evolve` method.
- `Generations` indicates how many times the `Evolve` method has been called.


### Running a GA

Once you have implemented the `Genome` interface and instantiated a `GA` struct you are good to go. You can call the `GA`'s `Enhance` method which will apply a model once (see the [models section](#models)). It's your choice if you want to call `Enhance` method multiple by using a loop or by imposing a time limit. The `Enhance` method will return an `error` which you should handle. If your population is evolving when you call `Enhance` it's most likely because `Enhance` did not return a `nil` error.
Once you have implemented the `Genome` interface and instantiated a `GA` struct you are good to go. You can call the `GA`'s `Evolve` method which will apply a model once (see the [models section](#models)). It's your choice if you want to call `Evolve` method multiple by using a loop or by imposing a time limit. The `Evolve` method will return an `error` which you should handle. If your population is evolving when you call `Evolve` it's most likely because `Evolve` did not return a `nil` error.

At any time you have access to the `GA`'s `Best` field which contains a `Fitness` field and a `Genome` field respectively indicating the overall best obtained solution and the parameters of that solution. Moreover, the `GA`'s`CurrentBest` field contains the best solution and parameters obtained by the current generation.

Expand Down
16 changes: 8 additions & 8 deletions benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"testing"
)

func BenchmarkEnhance1Pop(b *testing.B) {
func BenchmarkEvolve1Pop(b *testing.B) {
ga = GA{
NewGenome: NewVector,
NPops: 1,
Expand All @@ -19,11 +19,11 @@ func BenchmarkEnhance1Pop(b *testing.B) {
}
ga.Initialize()
for i := 0; i < b.N; i++ {
ga.Enhance()
ga.Evolve()
}
}

func BenchmarkEnhance2Pops(b *testing.B) {
func BenchmarkEvolve2Pops(b *testing.B) {
runtime.GOMAXPROCS(runtime.NumCPU())
ga = GA{
NewGenome: NewVector,
Expand All @@ -38,11 +38,11 @@ func BenchmarkEnhance2Pops(b *testing.B) {
}
ga.Initialize()
for i := 0; i < b.N; i++ {
ga.Enhance()
ga.Evolve()
}
}

func BenchmarkEnhance3Pops(b *testing.B) {
func BenchmarkEvolve3Pops(b *testing.B) {
ga = GA{
NewGenome: NewVector,
NPops: 3,
Expand All @@ -56,10 +56,10 @@ func BenchmarkEnhance3Pops(b *testing.B) {
}
ga.Initialize()
for i := 0; i < b.N; i++ {
ga.Enhance()
ga.Evolve()
}
}
func BenchmarkEnhance4Pops(b *testing.B) {
func BenchmarkEvolve4Pops(b *testing.B) {
ga = GA{
NewGenome: NewVector,
NPops: 4,
Expand All @@ -73,6 +73,6 @@ func BenchmarkEnhance4Pops(b *testing.B) {
}
ga.Initialize()
for i := 0; i < b.N; i++ {
ga.Enhance()
ga.Evolve()
}
}
64 changes: 31 additions & 33 deletions ga.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import (
"math/rand"
"sort"
"time"

"golang.org/x/sync/errgroup"
)

// A GA contains population which themselves contain individuals.
Expand Down Expand Up @@ -104,7 +102,7 @@ func (ga GA) Initialized() bool {
}

// Initialize each population in the GA and assign an initial fitness to each
// individual in each population. Running Initialize after running Enhance will
// individual in each population. Running Initialize after running Evolve will
// reset the GA entirely.
func (ga *GA) Initialize() {
// Check the NBest field
Expand Down Expand Up @@ -142,10 +140,10 @@ func (ga *GA) Initialize() {
}
}

// Enhance each population in the GA. The population level operations are done
// Evolve each population in the GA. The population level operations are done
// in parallel with a wait group. After all the population operations have been
// run, the GA level operations are run.
func (ga *GA) Enhance() error {
func (ga *GA) Evolve() error {
var start = time.Now()
ga.Generations++
// Check the GA has been initialized
Expand All @@ -158,39 +156,39 @@ func (ga *GA) Enhance() error {
if len(ga.Populations) > 1 && ga.Migrator != nil && ga.Generations%ga.MigFrequency == 0 {
ga.Migrator.Apply(ga.Populations, ga.RNG)
}
var g errgroup.Group
for i := range ga.Populations {
i := i // https://golang.org/doc/faq#closures_and_goroutines
g.Go(func() error {
var err error
// Apply speciation if a positive number of species has been specified
if ga.Speciator != nil {
err = ga.Populations[i].speciateEvolveMerge(ga.Speciator, ga.Model)
if err != nil {
return err
}
} else {
// Else apply the evolution model to the entire population
err = ga.Model.Apply(&ga.Populations[i])
if err != nil {
return err
}

var f = func(pop *Population) error {
var err error
// Apply speciation if a positive number of species has been specified
if ga.Speciator != nil {
err = pop.speciateEvolveMerge(ga.Speciator, ga.Model)
if err != nil {
return err
}
// Evaluate and sort
ga.Populations[i].Individuals.Evaluate(ga.ParallelEval)
ga.Populations[i].Individuals.SortByFitness()
ga.Populations[i].Age += time.Since(start)
ga.Populations[i].Generations++
// Log current statistics if a logger has been provided
if ga.Logger != nil {
ga.Populations[i].Log(ga.Logger)
} else {
// Else apply the evolution model to the entire population
err = ga.Model.Apply(pop)
if err != nil {
return err
}
return err
})
}
// Evaluate and sort
pop.Individuals.Evaluate(ga.ParallelEval)
pop.Individuals.SortByFitness()
pop.Age += time.Since(start)
pop.Generations++
// Log current statistics if a logger has been provided
if ga.Logger != nil {
pop.Log(ga.Logger)
}
return err
}
if err := g.Wait(); err != nil {

var err = ga.Populations.Apply(f, true)
if err != nil {
return err
}

// Update HallOfFame
for _, pop := range ga.Populations {
updateHallOfFame(ga.HallOfFame, pop.Individuals)
Expand Down
20 changes: 10 additions & 10 deletions ga_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func TestValidationSpeciator(t *testing.T) {
func TestApplyWithSpeciator(t *testing.T) {
var speciator = ga.Speciator
ga.Speciator = SpecFitnessInterval{4}
if ga.Enhance() != nil {
if ga.Evolve() != nil {
t.Error("Calling Apply with a valid Speciator should not return an error")
}
ga.Speciator = speciator
Expand Down Expand Up @@ -280,36 +280,36 @@ func TestCallback(t *testing.T) {
if counter != 1 {
t.Error("Counter was not incremented by the callback at initialization")
}
ga.Enhance()
ga.Evolve()
if counter != 2 {
t.Error("Counter was not incremented by the callback at enhancement")
}
}

func TestGAEnhanceModelRuntimeError(t *testing.T) {
func TestGAEvolveModelRuntimeError(t *testing.T) {
var model = ga.Model
ga.Model = ModRuntimeError{}
// Check invalid model doesn't raise error
if ga.Validate() != nil {
t.Errorf("Expected nil, got %s", ga.Validate())
}
// Enhance
var err = ga.Enhance()
// Evolve
var err = ga.Evolve()
if err == nil {
t.Error("An error should have been raised")
}
ga.Model = model
}

func TestGAEnhanceSpeciatorRuntimeError(t *testing.T) {
func TestGAEvolveSpeciatorRuntimeError(t *testing.T) {
var speciator = ga.Speciator
ga.Speciator = SpecRuntimeError{}
// Check invalid speciator doesn't raise error
if ga.Validate() != nil {
t.Errorf("Expected nil, got %s", ga.Validate())
}
// Enhance
var err = ga.Enhance()
// Evolve
var err = ga.Evolve()
if err == nil {
t.Error("An error should have been raised")
}
Expand Down Expand Up @@ -347,13 +347,13 @@ func TestGAConsistentResults(t *testing.T) {
// Run the first GA
ga1.Initialize()
for i := 0; i < 20; i++ {
ga1.Enhance()
ga1.Evolve()
}

// Run the second GA
ga2.Initialize()
for i := 0; i < 20; i++ {
ga2.Enhance()
ga2.Evolve()
}

// Compare best individuals
Expand Down
45 changes: 27 additions & 18 deletions individuals.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,31 +41,40 @@ func newIndividuals(n int, newGenome NewGenome, rng *rand.Rand) Individuals {
return indis
}

// Evaluate each individual. If parallel is true then each Individual will be
// Apply a function to a slice of Individuals.
func (indis Individuals) Apply(f func(indi *Individual) error, parallel bool) error {
if parallel {
var g errgroup.Group
for i := range indis {
i := i // https://golang.org/doc/faq#closures_and_goroutines
g.Go(func() error {
return f(&indis[i])
})
}
return g.Wait()
}
var err error
for i := range indis {
err = f(&indis[i])
if err != nil {
return err
}
}
return err
}

// Evaluate each Individual. If parallel is true then each Individual will be
// evaluated in parallel thanks to the golang.org/x/sync/errgroup package. If
// not then a simple sequential loop will be used. Evaluating in parallel is
// only recommended for cases where evaluating an Individual takes a "long"
// time. Indeed there won't necessarily be a speed-up when evaluating in
// parallel. In fact performance can be degraded if evaluating an Individual is
// too cheap.
func (indis Individuals) Evaluate(parallel bool) {
// Evaluate sequentially
if !parallel {
for i := range indis {
indis[i].Evaluate()
}
return
}
// Evaluate in parallel
var g errgroup.Group
for i := range indis {
i := i // https://golang.org/doc/faq#closures_and_goroutines
g.Go(func() error {
indis[i].Evaluate()
return nil
})
}
g.Wait()
indis.Apply(
func(indi *Individual) error { indi.Evaluate(); return nil },
parallel,
)
}

// Mutate each individual.
Expand Down
24 changes: 24 additions & 0 deletions population.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"log"
"math/rand"
"time"

"golang.org/x/sync/errgroup"
)

// A Population contains individuals. Individuals mate within a population.
Expand Down Expand Up @@ -44,3 +46,25 @@ func (pop Population) Log(logger *log.Logger) {

// Populations type is necessary for migration and speciation purposes.
type Populations []Population

// Apply a function to a slice of Populations.
func (pops Populations) Apply(f func(pop *Population) error, parallel bool) error {
if parallel {
var g errgroup.Group
for i := range pops {
i := i // https://golang.org/doc/faq#closures_and_goroutines
g.Go(func() error {
return f(&pops[i])
})
}
return g.Wait()
}
var err error
for i := range pops {
err = f(&pops[i])
if err != nil {
return err
}
}
return err
}
2 changes: 1 addition & 1 deletion setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func newRand() *rand.Rand {
func init() {
ga.Initialize()
for i := 0; i < nbrGenerations; i++ {
ga.Enhance()
ga.Evolve()
}
}

Expand Down

0 comments on commit 6c17807

Please sign in to comment.