Skip to content

Commit 224cbe0

Browse files
authored
Merge pull request #24 from MaxHalford/hall-of-fame
Hall of fame
2 parents 03d38af + 0ed6fed commit 224cbe0

File tree

3 files changed

+120
-68
lines changed

3 files changed

+120
-68
lines changed

README.md

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -266,11 +266,14 @@ Let's have a look at the GA struct.
266266

267267
```go
268268
type GA struct {
269-
// Fields that are provided by the user
269+
// Required fields
270270
NewGenome NewGenome
271271
NPops int
272272
PopSize int
273273
Model Model
274+
275+
// Optional fields
276+
NBest int
274277
Migrator Migrator
275278
MigFrequency int
276279
Speciator Speciator
@@ -279,32 +282,37 @@ type GA struct {
279282
RNG *rand.Rand
280283
ParallelEval bool
281284

282-
// Fields that are generated at runtime
285+
// Fields generated at runtime
283286
Populations Populations
284-
Best Individual
285-
CurrentBest Individual
287+
HallOfFame Individuals
286288
Age time.Duration
287289
Generations int
288290
}
289291
```
290292

291293
You have to fill in the first set of fields, the rest are generated when calling the `GA`'s `Initialize()` method. Check out the examples in `presets.go` to get an idea of how to fill them out.
292294

293-
- `NewGenome` is a method that returns a random genome that you defined in the previous step. gago will use this method to produce an initial population. Again, gago provides some methods for common random genome generation.
294-
- `NPops` determines the number of populations that will be used.
295-
- `PopSize` determines the number of individuals inside each population.
296-
- `Model` determines how to use the genetic operators you chose in order to produce better solutions, in other words it's a recipe. A dedicated section is available in the [model section](#models).
297-
- `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.
298-
- `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.
299-
- `Logger` is optional and provides basic population statistics, you can read more about it in the [logging section](#logging-population-statistics).
300-
- `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:
295+
- Required fields
296+
- `NewGenome` is a method that returns a random genome that you defined in the previous step. gago will use this method to produce an initial population. Again, gago provides some methods for common random genome generation.
297+
- `NPops` determines the number of populations that will be used.
298+
- `PopSize` determines the number of individuals inside each population.
299+
- `Model` determines how to use the genetic operators you chose in order to produce better solutions, in other words it's a recipe. A dedicated section is available in the [model section](#models).
300+
- Optional fields
301+
- `NBest` determines how many of the best individuals encountered should be regarded in the `HallOfFame` field. This defaults to 1.
302+
- `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.
303+
- `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.
304+
- `Logger` is optional and provides basic population statistics, you can read more about it in the [logging section](#logging-population-statistics).
305+
- `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:
301306
- Calculating specific population statistics that are not provided by the logger
302307
- Changing parameters of the GA after a certain number of generations
303308
- Monitoring for converging populations
304-
- `RNG` can be set to make results reproducible. If it is not provided then a default `rand.New(rand.NewSource(time.Now().UnixNano()))` will be used. If you want to make your results reproducible use a constant source, e.g. `rand.New(rand.NewSource(42))`.
305-
- `ParallelEval` determines if a population is evaluated in parallel. The rule of thumb is to set this to `true` if your `Evaluate` method is expensive, if not it won't be worth the overhead. Refer to the [section on parallelism](#a-note-on-parallelism) for a more comprehensive explanation.
306-
307-
Essentially, only `NewGenome`, `NPops`, `PopSize` and `Model` are required to initialize and run a GA. The other fields are optional.
309+
- `RNG` can be set to make results reproducible. If it is not provided then a default `rand.New(rand.NewSource(time.Now().UnixNano()))` will be used. If you want to make your results reproducible use a constant source, e.g. `rand.New(rand.NewSource(42))`.
310+
- `ParallelEval` determines if a population is evaluated in parallel. The rule of thumb is to set this to `true` if your `Evaluate` method is expensive, if not it won't be worth the overhead. Refer to the [section on parallelism](#a-note-on-parallelism) for a more comprehensive explanation.
311+
- Fields populated at runtime
312+
- `Populations` is where all the current populations and individuals are kept.
313+
- `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.
314+
- `Age` indicates the duration the GA has spent calling the `Enhance` method.
315+
- `Generations` indicates how many times the `Enhance` method has been called.
308316

309317

310318
### Running a GA

ga.go

Lines changed: 50 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,22 @@ import (
55
"log"
66
"math"
77
"math/rand"
8+
"sort"
89
"time"
910

1011
"golang.org/x/sync/errgroup"
1112
)
1213

1314
// A GA contains population which themselves contain individuals.
1415
type GA struct {
15-
// Fields that are provided by the user
16-
NewGenome NewGenome `json:"-"`
17-
NPops int `json:"-"` // Number of Populations
18-
PopSize int `json:"-"` // Number of Individuls per Population
19-
Model Model `json:"-"`
16+
// Required fields
17+
NewGenome NewGenome `json:"-"`
18+
NPops int `json:"-"` // Number of Populations
19+
PopSize int `json:"-"` // Number of Individuls per Population
20+
Model Model `json:"-"`
21+
22+
// Optional fields
23+
NBest int `json:"-"` // Length of HallOfFame
2024
Migrator Migrator `json:"-"`
2125
MigFrequency int `json:"-"` // Frequency at which migrations occur
2226
Speciator Speciator `json:"-"`
@@ -25,12 +29,11 @@ type GA struct {
2529
RNG *rand.Rand `json:"-"`
2630
ParallelEval bool `json:"-"`
2731

28-
// Fields that are generated at runtime
29-
Populations Populations `json:"pops"`
30-
Best Individual `json:"overall_best"`
31-
CurrentBest Individual `json:"generation_best"`
32-
Age time.Duration `json:"duration"`
33-
Generations int `json:"generations"`
32+
// Fields generated at runtime
33+
Populations Populations `json:"populations"`
34+
HallOfFame Individuals `json:"hall_of_fame"` // Sorted best Individuals ever encountered
35+
Age time.Duration `json:"duration"` // Duration during which the GA has been evolved
36+
Generations int `json:"generations"` // Number of generations the GA has been evolved
3437
}
3538

3639
// Validate the parameters of a GA to ensure it will run correctly; some
@@ -71,23 +74,25 @@ func (ga GA) Validate() error {
7174
return nil
7275
}
7376

74-
// Find the best current Individual in each Population and then compare the best
75-
// overall Individual to the current best Individual.
76-
func (ga *GA) findBest() {
77+
// Find the best current Individual in each population and then compare the best
78+
// overall Individual to the current best Individual. The Individuals in each
79+
// population are expected to be sorted.
80+
func updateHallOfFame(hof Individuals, indis Individuals) {
81+
var k = len(hof)
7782
// Start by finding the current best Individual
78-
ga.CurrentBest = Individual{Fitness: math.Inf(1)}
79-
for _, pop := range ga.Populations {
80-
if !pop.Individuals.IsSortedByFitness() {
81-
pop.Individuals.SortByFitness()
82-
}
83-
if pop.Individuals[0].Fitness < ga.CurrentBest.Fitness {
84-
ga.CurrentBest = pop.Individuals[0].Clone(pop.rng)
83+
for _, indi := range indis[:min(k, len(indis))] {
84+
// Find if and where the Individual should fit in the hall of fame
85+
var (
86+
f = func(i int) bool { return indi.Fitness < hof[i].Fitness }
87+
i = sort.Search(k, f)
88+
)
89+
if i < k {
90+
// Shift the hall of fame to the right
91+
copy(hof[i+1:], hof[i:])
92+
// Insert the new Individual
93+
hof[i] = indi
8594
}
8695
}
87-
// Compare the current best Individual to the overall Individual
88-
if ga.CurrentBest.Fitness < ga.Best.Fitness {
89-
ga.Best = ga.CurrentBest.Clone(ga.RNG)
90-
}
9196
}
9297

9398
// Initialized indicates if the GA has been initialized or not.
@@ -102,26 +107,35 @@ func (ga GA) Initialized() bool {
102107
// individual in each population. Running Initialize after running Enhance will
103108
// reset the GA entirely.
104109
func (ga *GA) Initialize() {
110+
// Check the NBest field
111+
if ga.NBest < 1 {
112+
ga.NBest = 1
113+
}
105114
// Initialize the random number generator if it hasn't been set
106115
if ga.RNG == nil {
107116
ga.RNG = rand.New(rand.NewSource(time.Now().UnixNano()))
108117
}
109118
ga.Populations = make([]Population, ga.NPops)
110119
for i := range ga.Populations {
111-
// Generate a population
120+
// Generate a Population
112121
ga.Populations[i] = newPopulation(ga.PopSize, ga.NewGenome, ga.RNG)
113-
// Evaluate its individuals
122+
// Evaluate its Individuals
114123
ga.Populations[i].Individuals.Evaluate(ga.ParallelEval)
115-
// Sort its individuals
124+
// Sort it's Individuals
116125
ga.Populations[i].Individuals.SortByFitness()
117126
// Log current statistics if a logger has been provided
118127
if ga.Logger != nil {
119128
ga.Populations[i].Log(ga.Logger)
120129
}
121130
}
122-
// Find the initial best Individual
123-
ga.Best = ga.Populations[0].Individuals[0]
124-
ga.findBest()
131+
// Initialize HallOfFame
132+
ga.HallOfFame = make(Individuals, ga.NBest)
133+
for i := range ga.HallOfFame {
134+
ga.HallOfFame[i] = Individual{Fitness: math.Inf(1)}
135+
}
136+
for _, pop := range ga.Populations {
137+
updateHallOfFame(ga.HallOfFame, pop.Individuals)
138+
}
125139
// Execute the callback if it has been set
126140
if ga.Callback != nil {
127141
ga.Callback(ga)
@@ -177,13 +191,15 @@ func (ga *GA) Enhance() error {
177191
if err := g.Wait(); err != nil {
178192
return err
179193
}
180-
// Check if there is an individual that is better than the current one
181-
ga.findBest()
182-
ga.Age += time.Since(start)
194+
// Update HallOfFame
195+
for _, pop := range ga.Populations {
196+
updateHallOfFame(ga.HallOfFame, pop.Individuals)
197+
}
183198
// Execute the callback if it has been set
184199
if ga.Callback != nil {
185200
ga.Callback(ga)
186201
}
202+
ga.Age += time.Since(start)
187203
// No error
188204
return nil
189205
}

ga_test.go

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -123,29 +123,57 @@ func TestRandomNumberGenerators(t *testing.T) {
123123
func TestBest(t *testing.T) {
124124
for _, pop := range ga.Populations {
125125
for _, indi := range pop.Individuals {
126-
if ga.Best.Fitness > indi.Fitness {
126+
if ga.HallOfFame[0].Fitness > indi.Fitness {
127127
t.Error("The current best individual is not the overall best")
128128
}
129129
}
130130
}
131131
}
132132

133-
func TestFindBest(t *testing.T) {
134-
// Check sure the findBest method works as expected
135-
var fitness = ga.Populations[0].Individuals[0].Fitness
136-
ga.Populations[0].Individuals[0].Fitness = math.Inf(-1)
137-
ga.findBest()
138-
if ga.Best.Fitness != math.Inf(-1) {
139-
t.Error("findBest didn't work")
140-
}
141-
ga.Populations[0].Individuals[0].Fitness = fitness
142-
// Check the best individual doesn't a share a pointer with anyone
143-
fitness = ga.Best.Fitness
144-
ga.Best.Fitness = 42
145-
if ga.Populations[0].Individuals[0].Fitness == 42 {
146-
t.Error("Best individual shares a pointer with an individual in the populations")
133+
func TestUpdateHallOfFame(t *testing.T) {
134+
var (
135+
testCases = []struct {
136+
hofIn Individuals
137+
indis Individuals
138+
hofOut Individuals
139+
}{
140+
{
141+
hofIn: Individuals{
142+
Individual{Fitness: math.Inf(1)},
143+
},
144+
indis: Individuals{
145+
Individual{Fitness: 0},
146+
},
147+
hofOut: Individuals{
148+
Individual{Fitness: 0},
149+
},
150+
},
151+
{
152+
hofIn: Individuals{
153+
Individual{Fitness: 0},
154+
Individual{Fitness: math.Inf(1)},
155+
},
156+
indis: Individuals{
157+
Individual{Fitness: 1},
158+
},
159+
hofOut: Individuals{
160+
Individual{Fitness: 0},
161+
Individual{Fitness: 1},
162+
},
163+
},
164+
}
165+
)
166+
for i, tc := range testCases {
167+
t.Run(fmt.Sprintf("TC %d", i), func(t *testing.T) {
168+
updateHallOfFame(tc.hofIn, tc.indis)
169+
// Compare the obtained hall of fame to the expected one)
170+
for i, indi := range tc.hofIn {
171+
if indi.Fitness != tc.hofOut[i].Fitness {
172+
t.Errorf("Expected %v, got %v", tc.hofOut[i], indi)
173+
}
174+
}
175+
})
147176
}
148-
ga.Best.Fitness = fitness
149177
}
150178

151179
// TestDuration verifies the sum of the duration of each population is higher
@@ -329,8 +357,8 @@ func TestGAConsistentResults(t *testing.T) {
329357
}
330358

331359
// Compare best individuals
332-
if ga1.Best.Fitness != ga2.Best.Fitness {
333-
t.Errorf("Expected %f, got %f", ga1.Best.Fitness, ga2.Best.Fitness)
360+
if ga1.HallOfFame[0].Fitness != ga2.HallOfFame[0].Fitness {
361+
t.Errorf("Expected %f, got %f", ga1.HallOfFame[0].Fitness, ga2.HallOfFame[0].Fitness)
334362
}
335363

336364
}

0 commit comments

Comments
 (0)