github.com/yaricom/goNEAT@v0.0.0-20210507221059-e2110b885482/experiments/experiment.go (about)

     1  package experiments
     2  
     3  import (
     4  	"time"
     5  	"sort"
     6  	"github.com/yaricom/goNEAT/neat/genetics"
     7  	"io"
     8  	"encoding/gob"
     9  	"fmt"
    10  	"math"
    11  )
    12  
    13  // An Experiment is a collection of trials for one experiment. It's useful for statistical analysis of a series of
    14  // experiments
    15  type Experiment struct {
    16  	Id              int
    17  	Name            string
    18  	Trials
    19  	// The maximal allowed fitness score as defined by fitness function of experiment.
    20  	// It is used to normalize fitness score value used in efficiency score calculation. If this value
    21  	// is not set, than fitness score will not be normalized during efficiency score estimation.
    22  	MaxFintessScore float64
    23  }
    24  
    25  // Calculates average duration of experiment's trial
    26  // Note, that most trials finish after solution solved, so this metric can be used to represent how efficient the solvers
    27  // was generated
    28  func (e *Experiment) AvgTrialDuration() time.Duration {
    29  	total := time.Duration(0)
    30  	for _, t := range e.Trials {
    31  		total += t.Duration
    32  	}
    33  	return total / time.Duration(len(e.Trials))
    34  }
    35  
    36  // Calculates average duration of evaluations among all generations of organism populations in this experiment
    37  func (e *Experiment) AvgEpochDuration() time.Duration {
    38  	total := time.Duration(0)
    39  	for _, t := range e.Trials {
    40  		total += t.AvgEpochDuration()
    41  	}
    42  	return total / time.Duration(len(e.Trials))
    43  }
    44  
    45  // Calculates average number of generations evaluated per trial during this experiment. This can be helpful when estimating
    46  // algorithm efficiency, because when winner organism is found the trial is terminated, i.e. less evaluations - more fast
    47  // convergence.
    48  func (e *Experiment) AvgGenerationsPerTrial() float64 {
    49  	total := 0.0
    50  	for _, t := range e.Trials {
    51  		total += float64(len(t.Generations))
    52  	}
    53  	return total / float64(len(e.Trials))
    54  }
    55  
    56  // Returns time of last trial's execution
    57  func (e *Experiment) LastExecuted() time.Time {
    58  	var u time.Time
    59  	for _, e := range e.Trials {
    60  		ut := e.LastExecuted()
    61  		if u.Before(ut) {
    62  			u = ut
    63  		}
    64  	}
    65  	return u
    66  }
    67  
    68  // Finds the most fit organism among all epochs in this trial. It's also possible to get the best organism only among the ones
    69  // which was able to solve the experiment's problem. Returns the best fit organism in this experiment among with ID of trial
    70  // where it was found and boolean value to indicate if search was successful.
    71  func (e *Experiment) BestOrganism(onlySolvers bool) (*genetics.Organism, int, bool) {
    72  	var orgs = make(genetics.Organisms, 0, len(e.Trials))
    73  	for i, t := range e.Trials {
    74  		org, found := t.BestOrganism(onlySolvers)
    75  		if found {
    76  			orgs = append(orgs, org)
    77  			org.Flag = i
    78  		}
    79  
    80  	}
    81  	if len(orgs) > 0 {
    82  		sort.Sort(sort.Reverse(orgs))
    83  		return orgs[0], orgs[0].Flag, true
    84  	} else {
    85  		return nil, -1, false
    86  	}
    87  
    88  }
    89  
    90  func (e *Experiment) Solved() bool {
    91  	for _, t := range e.Trials {
    92  		if t.Solved() {
    93  			return true
    94  		}
    95  	}
    96  	return false
    97  }
    98  
    99  // The fitness values of the best organisms for each trial
   100  func (e *Experiment) BestFitness() Floats {
   101  	var x Floats = make([]float64, len(e.Trials))
   102  	for i, t := range e.Trials {
   103  		if org, ok := t.BestOrganism(false); ok {
   104  			x[i] = org.Fitness
   105  		}
   106  	}
   107  	return x
   108  }
   109  
   110  // The age values of the organisms for each trial
   111  func (e *Experiment) BestAge() Floats {
   112  	var x Floats = make([]float64, len(e.Trials))
   113  	for i, t := range e.Trials {
   114  		if org, ok := t.BestOrganism(false); ok {
   115  			x[i] = float64(org.Species.Age)
   116  		}
   117  	}
   118  	return x
   119  }
   120  
   121  // The complexity values of the best organisms for each trial
   122  func (e *Experiment) BestComplexity() Floats {
   123  	var x Floats = make([]float64, len(e.Trials))
   124  	for i, t := range e.Trials {
   125  		if org, ok := t.BestOrganism(false); ok {
   126  			x[i] = float64(org.Phenotype.Complexity())
   127  		}
   128  	}
   129  	return x
   130  }
   131  
   132  // Diversity returns the average number of species in each trial
   133  func (e *Experiment) Diversity() Floats {
   134  	var x Floats = make([]float64, len(e.Trials))
   135  	for i, t := range e.Trials {
   136  		x[i] = t.Diversity().Mean()
   137  	}
   138  	return x
   139  }
   140  
   141  // Trials returns the number of epochs in each trial
   142  func (e *Experiment) Epochs() Floats {
   143  	var x Floats = make([]float64, len(e.Trials))
   144  	for i, t := range e.Trials {
   145  		x[i] = float64(len(t.Generations))
   146  	}
   147  	return x
   148  }
   149  
   150  // The number of trials solved
   151  func (e *Experiment) TrialsSolved() int {
   152  	count := 0
   153  	for _, t := range e.Trials {
   154  		if t.Solved() {
   155  			count++
   156  		}
   157  	}
   158  	return count
   159  }
   160  
   161  // The success rate
   162  func (e *Experiment) SuccessRate() float64 {
   163  	soved := float64(e.TrialsSolved())
   164  	return soved / float64(len(e.Trials))
   165  }
   166  
   167  // Returns average number of nodes, genes, organisms evaluations, and species diversity of winner genomes among all
   168  // trials, i.e. for all trials where winning solution was found
   169  func (e *Experiment) AvgWinner() (avg_nodes, avg_genes, avg_evals, avg_diversity float64) {
   170  	total_nodes, total_genes, total_evals, total_diversity := 0, 0, 0, 0
   171  	count := 0
   172  	for i := 0; i < len(e.Trials); i++ {
   173  		t := e.Trials[i]
   174  		if t.Solved() {
   175  			nodes, genes, evals, diversity := t.Winner()
   176  			total_nodes += nodes
   177  			total_genes += genes
   178  			total_evals += evals
   179  			total_diversity += diversity
   180  
   181  			count++
   182  		}
   183  	}
   184  	avg_nodes = float64(total_nodes) / float64(count)
   185  	avg_genes = float64(total_genes) / float64(count)
   186  	avg_evals = float64(total_evals) / float64(count)
   187  	avg_diversity = float64(total_diversity) / float64(count)
   188  	return avg_nodes, avg_genes, avg_evals, avg_diversity
   189  }
   190  
   191  // Calculates the efficiency score of the solution
   192  // We are interested in efficient solver search solution that take
   193  // less time per epoch, less generations per trial, and produce less complicated winner genomes.
   194  // At the same time it should have maximal fitness score and maximal success rate among trials.
   195  func (ex *Experiment) EfficiencyScore() float64 {
   196  	mean_complexity, mean_fitness := 0.0, 0.0
   197  	if len(ex.Trials) > 1 {
   198  		count := 0.0
   199  		for i := 0; i < len(ex.Trials); i++ {
   200  			t := ex.Trials[i]
   201  			if t.Solved() {
   202  				if t.WinnerGeneration == nil {
   203  					// find winner
   204  					t.Winner()
   205  				}
   206  
   207  				mean_complexity += float64(t.WinnerGeneration.Best.Phenotype.Complexity())
   208  				mean_fitness += t.WinnerGeneration.Best.Fitness
   209  
   210  				count++
   211  			}
   212  		}
   213  		mean_complexity /= count
   214  		mean_fitness /= count
   215  	}
   216  
   217  	// normalize and scale fitness score if appropriate
   218  	fitness_score := mean_fitness
   219  	if ex.MaxFintessScore > 0 {
   220  		fitness_score /= ex.MaxFintessScore
   221  		fitness_score *= 100
   222  	}
   223  
   224  	score := ex.AvgEpochDuration().Seconds() * 1000.0 * ex.AvgGenerationsPerTrial() * mean_complexity
   225  	if score > 0 {
   226  		score = ex.SuccessRate() * fitness_score / math.Log(score)
   227  	}
   228  
   229  	return score
   230  }
   231  
   232  // Prints experiment statistics
   233  func (ex *Experiment) PrintStatistics() {
   234  	fmt.Printf("\nSolved %d trials from %d, success rate: %f\n", ex.TrialsSolved(), len(ex.Trials), ex.SuccessRate())
   235  	fmt.Printf("Average\n\tTrial duration:\t\t%s\n\tEpoch duration:\t\t%s\n\tGenerations/trial:\t%.1f\n",
   236  		ex.AvgTrialDuration(), ex.AvgEpochDuration(), ex.AvgGenerationsPerTrial())
   237  	// Print absolute champion statistics
   238  	if org, trid, found := ex.BestOrganism(true); found {
   239  		nodes, genes, evals, divers := ex.Trials[trid].Winner()
   240  		fmt.Printf("\nChampion found in %d trial run\n\tWinner Nodes:\t\t%d\n\tWinner Genes:\t\t%d\n\tWinner Evals:\t\t%d\n\n\tDiversity:\t\t%d",
   241  			trid, nodes, genes, evals, divers)
   242  		fmt.Printf("\n\tComplexity:\t\t%d\n\tAge:\t\t\t%d\n\tFitness:\t\t%f\n",
   243  			org.Phenotype.Complexity(), org.Species.Age, org.Fitness)
   244  	} else {
   245  		fmt.Println("\nNo winner found in the experiment!!!")
   246  	}
   247  
   248  	// Print average winner statistics
   249  	mean_complexity, mean_diversity, mean_age, mean_fitness := 0.0, 0.0, 0.0, 0.0
   250  	if len(ex.Trials) > 1 {
   251  		avg_nodes, avg_genes, avg_evals, avg_divers, avg_generations := 0.0, 0.0, 0.0, 0.0, 0.0
   252  		count := 0.0
   253  		for i := 0; i < len(ex.Trials); i++ {
   254  			t := ex.Trials[i]
   255  
   256  			if t.Solved() {
   257  				nodes, genes, evals, diversity := t.Winner()
   258  				avg_nodes += float64(nodes)
   259  				avg_genes += float64(genes)
   260  				avg_evals += float64(evals)
   261  				avg_divers += float64(diversity)
   262  				avg_generations += float64(len(t.Generations))
   263  
   264  				mean_complexity += float64(t.WinnerGeneration.Best.Phenotype.Complexity())
   265  				mean_age += float64(t.WinnerGeneration.Best.Species.Age)
   266  				mean_fitness += t.WinnerGeneration.Best.Fitness
   267  
   268  				count++
   269  
   270  				// update trials array
   271  				ex.Trials[i] = t
   272  			}
   273  		}
   274  		avg_nodes /= count
   275  		avg_genes /= count
   276  		avg_evals /= count
   277  		avg_divers /= count
   278  		avg_generations /= count
   279  		fmt.Printf("\nAverage among winners\n\tWinner Nodes:\t\t%.1f\n\tWinner Genes:\t\t%.1f\n\tWinner Evals:\t\t%.1f\n\tGenerations/trial:\t%.1f\n\n\tDiversity:\t\t%f\n",
   280  			avg_nodes, avg_genes, avg_evals, avg_generations, avg_divers)
   281  
   282  		mean_complexity /= count
   283  		mean_age /= count
   284  		mean_fitness /= count
   285  		fmt.Printf("\tComplexity:\t\t%f\n\tAge:\t\t\t%f\n\tFitness:\t\t%f\n",
   286  			mean_complexity, mean_age, mean_fitness)
   287  	}
   288  
   289  	// Print the average values for each population of organisms evaluated
   290  	count := float64(len(ex.Trials))
   291  	for _, t := range ex.Trials {
   292  		fitness, age, complexity := t.Average()
   293  
   294  		mean_complexity += complexity.Mean()
   295  		mean_diversity += t.Diversity().Mean()
   296  		mean_age += age.Mean()
   297  		mean_fitness += fitness.Mean()
   298  	}
   299  	mean_complexity /= count
   300  	mean_diversity /= count
   301  	mean_age /= count
   302  	mean_fitness /= count
   303  	fmt.Printf("\nAverages for all organisms evaluated during experiment\n\tDiversity:\t\t%f\n\tComplexity:\t\t%f\n\tAge:\t\t\t%f\n\tFitness:\t\t%f\n",
   304  		mean_diversity, mean_complexity, mean_age, mean_fitness)
   305  
   306  	score := ex.EfficiencyScore()
   307  	fmt.Printf("\nEfficiency score:\t\t%f\n\n", score)
   308  }
   309  
   310  // Encodes experiment and writes to provided writer
   311  func (ex *Experiment) Write(w io.Writer) error {
   312  	enc := gob.NewEncoder(w)
   313  	return ex.Encode(enc)
   314  }
   315  
   316  // Encodes experiment with GOB encoding
   317  func (ex *Experiment) Encode(enc *gob.Encoder) error {
   318  	err := enc.Encode(ex.Id)
   319  	err = enc.Encode(ex.Name)
   320  
   321  	// encode trials
   322  	err = enc.Encode(len(ex.Trials))
   323  	for _, t := range ex.Trials {
   324  		err = t.Encode(enc)
   325  		if err != nil {
   326  			return err
   327  		}
   328  	}
   329  	return err
   330  }
   331  
   332  // Reads experiment data from provided reader and decodes it
   333  func (ex *Experiment) Read(r io.Reader) error {
   334  	dec := gob.NewDecoder(r)
   335  	return ex.Decode(dec)
   336  }
   337  
   338  // Decodes experiment data
   339  func (ex *Experiment) Decode(dec *gob.Decoder) error {
   340  	err := dec.Decode(&ex.Id)
   341  	err = dec.Decode(&ex.Name)
   342  
   343  	// decode Trials
   344  	var t_num int
   345  	err = dec.Decode(&t_num)
   346  	if err != nil {
   347  		return err
   348  	}
   349  
   350  	ex.Trials = make([]Trial, t_num)
   351  	for i := 0; i < t_num; i++ {
   352  		trial := Trial{}
   353  		err = trial.Decode(dec)
   354  		ex.Trials[i] = trial
   355  	}
   356  	return err
   357  }
   358  
   359  // Experiments is a sortable list of experiments by execution time and Id
   360  type Experiments []Experiment
   361  
   362  func (es Experiments) Len() int {
   363  	return len(es)
   364  }
   365  func (es Experiments) Swap(i, j int) {
   366  	es[i], es[j] = es[j], es[i]
   367  }
   368  func (es Experiments) Less(i, j int) bool {
   369  	ui := es[i].LastExecuted()
   370  	uj := es[j].LastExecuted()
   371  	if ui.Equal(uj) {
   372  		return es[i].Id < es[j].Id
   373  	}
   374  	return ui.Before(uj)
   375  }