source: framspy/evolalg/examples/niching_novelty.py @ 1182

Last change on this file since 1182 was 1182, checked in by Maciej Komosinski, 19 months ago

More concise code and less redundancy in dissimilarity classes, added support for archive of genotypes, added hard limit on the number of genotype chars

File size: 15.1 KB
RevLine 
[1113]1import argparse
[1139]2import logging
[1113]3import os
[1128]4import pickle
[1113]5import sys
6from enum import Enum
7
8import numpy as np
9
10from FramsticksLib import FramsticksLib
11from evolalg.base.lambda_step import LambdaStep
[1128]12from evolalg.base.step import Step
[1182]13from evolalg.dissimilarity.archive import ArchiveDissimilarity
[1113]14from evolalg.dissimilarity.frams_dissimilarity import FramsDissimilarity
15from evolalg.dissimilarity.levenshtein import LevenshteinDissimilarity
16from evolalg.experiment import Experiment
17from evolalg.fitness.fitness_step import FitnessStep
18from evolalg.mutation_cross.frams_cross_and_mutate import FramsCrossAndMutate
19from evolalg.population.frams_population import FramsPopulation
20from evolalg.repair.remove.field import FieldRemove
21from evolalg.repair.remove.remove import Remove
22from evolalg.selection.tournament import TournamentSelection
23from evolalg.statistics.halloffame_stats import HallOfFameStatistics
24from evolalg.statistics.statistics_deap import StatisticsDeap
25from evolalg.base.union_step import UnionStep
26from evolalg.utils.population_save import PopulationSave
27
28
29def ensureDir(string):
30    if os.path.isdir(string):
31        return string
32    else:
33        raise NotADirectoryError(string)
34
35
36class Dissim(Enum):
37    levenshtein = "levenshtein"
38    frams = "frams"
39
40    def __str__(self):
41        return self.name
42
43
44class Fitness(Enum):
45    raw = "raw"
46    niching = "niching"
47    novelty = "novelty"
[1145]48    knn_niching = "knn_niching"
49    knn_novelty = "knn_novelty"
[1113]50
51    def __str__(self):
52        return self.name
53
54
55def parseArguments():
56    parser = argparse.ArgumentParser(
57        description='Run this program with "python -u %s" if you want to disable buffering of its output.' % sys.argv[
58            0])
[1182]59    parser.add_argument('-path', type=ensureDir, required=True,
60                        help='Path to the Framsticks library without trailing slash.')
[1113]61    parser.add_argument('-opt', required=True,
[1145]62                        help='optimization criteria: vertpos, velocity, distance, vertvel, lifespan, numjoints, numparts, numneurons, numconnections (or other as long as it is provided by the .sim file and its .expdef). For multiple criteria optimization, see multicriteria.py.')
[1132]63    parser.add_argument('-lib', required=False, help="Filename of .so or .dll with the Framsticks library")
[1139]64
[1113]65    parser.add_argument('-genformat', required=False, default="1",
66                        help='Genetic format for the demo run, for example 4, 9, or B. If not given, f1 is assumed.')
[1182]67    parser.add_argument('-sim', required=False, default="eval-allcriteria.sim",
68                        help="Name of the .sim file with all parameter values. If you want to provide more files, separate them with a semicolon ';'.")
[1139]69    parser.add_argument('-fit', required=False, default=Fitness.raw, type=Fitness,
70                        help=' Fitness criteria, default: raw', choices=list(Fitness))
[1113]71    parser.add_argument('-dissim', required=False, type=Dissim, default=Dissim.frams,
[1139]72                        help='Dissimilarity measure, default: frams', choices=list(Dissim))
[1182]73    parser.add_argument('-knn', type=int,
74                        help="'k' value for knn-based fitness criteria (knn-niching and knn-novelty).")
[1139]75    parser.add_argument('-popsize', type=int, default=50, help="Population size, default: 50.")
76    parser.add_argument('-generations', type=int, default=5, help="Number of generations, default: 5.")
77    parser.add_argument('-tournament', type=int, default=5, help="Tournament size, default: 5.")
[1138]78
79    parser.add_argument('-max_numparts', type=int, default=None, help="Maximum number of Parts. Default: no limit")
80    parser.add_argument('-max_numjoints', type=int, default=None, help="Maximum number of Joints. Default: no limit")
81    parser.add_argument('-max_numneurons', type=int, default=None, help="Maximum number of Neurons. Default: no limit")
82    parser.add_argument('-max_numconnections', type=int, default=None, help="Maximum number of Neural connections. Default: no limit")
[1182]83    parser.add_argument('-max_numgenochars', type=int, default=10000, help="Maximum number of characters in genotype, to disable this option set it to -1. Default: 10 000")
[1140]84    parser.add_argument('-hof_size', type=int, default=10, help="Number of genotypes in Hall of Fame. Default: 10.")
[1182]85    parser.add_argument('-hof_evaluations', type=int, default=20,
86                        help="Number of final evaluations of each genotype in Hall of Fame to obtain reliable (averaged) fitness. Default: 20.")
[1128]87    parser.add_argument('-checkpoint_path', required=False, default=None, help="Path to the checkpoint file")
[1113]88    parser.add_argument('-checkpoint_interval', required=False, type=int, default=100, help="Checkpoint interval")
[1145]89    parser.add_argument('-debug', dest='debug', action='store_true', help="Prints names of steps as they are executed")
[1182]90    parser.add_argument('-archive_size', type=int, default=0, help="Size of the archive size for dissimilarity calculation")
[1139]91    parser.set_defaults(debug=False)
[1113]92    return parser.parse_args()
93
94
95def extract_fitness(ind):
96    return ind.fitness_raw
97
98
99def print_population_count(pop):
[1136]100    print("Current popsize:", len(pop))
[1113]101    return pop  # Each step must return a population
102
103
[1138]104class NumPartsHigher(Remove):
105    def __init__(self, max_number):
106        super(NumPartsHigher, self).__init__()
107        self.max_number = max_number
[1113]108
109    def remove(self, individual):
[1138]110        return individual.numparts > self.max_number
[1113]111
112
[1138]113class NumJointsHigher(Remove):
114    def __init__(self, max_number):
115        super(NumJointsHigher, self).__init__()
116        self.max_number = max_number
117
118    def remove(self, individual):
119        return individual.numjoints > self.max_number
120
121
122class NumNeuronsHigher(Remove):
123    def __init__(self, max_number):
124        super(NumNeuronsHigher, self).__init__()
125        self.max_number = max_number
126
127    def remove(self, individual):
128        return individual.numneurons > self.max_number
129
130
131class NumConnectionsHigher(Remove):
132    def __init__(self, max_number):
133        super(NumConnectionsHigher, self).__init__()
134        self.max_number = max_number
135
136    def remove(self, individual):
137        return individual.numconnections > self.max_number
138
[1182]139class NumCharsHigher(Remove):
140    def __init__(self, max_number):
141        super(NumCharsHigher, self).__init__()
142        self.max_number = max_number
[1145]143
[1182]144    def remove(self, individual):
145        return len(individual.genotype) > self.max_number
146
[1139]147class ReplaceWithHallOfFame(Step):
148    def __init__(self, hof, *args, **kwargs):
149        super(ReplaceWithHallOfFame, self).__init__(*args, **kwargs)
150        self.hof = hof
[1182]151
[1139]152    def call(self, population, *args, **kwargs):
153        super(ReplaceWithHallOfFame, self).call(population)
154        return list(self.hof.halloffame)
[1138]155
[1145]156
[1113]157def func_raw(ind): setattr(ind, "fitness", ind.fitness_raw)
158
159
160def func_novelty(ind): setattr(ind, "fitness", ind.dissim)
161
162
[1145]163def func_knn_novelty(ind): setattr(ind, "fitness", ind.dissim)
164
165
[1146]166def func_niching(ind): setattr(ind, "fitness", ind.fitness_raw * (1 + ind.dissim))
167
168
[1145]169def func_knn_niching(ind): setattr(ind, "fitness", ind.fitness_raw * (1 + ind.dissim))
170
171
[1128]172def load_experiment(path):
173    with open(path, "rb") as file:
174        experiment = pickle.load(file)
175    print("Loaded experiment. Generation:", experiment.generation)
176    return experiment
177
178
179def create_experiment():
[1113]180    parsed_args = parseArguments()
[1138]181    frams_lib = FramsticksLib(parsed_args.path, parsed_args.lib,
[1182]182                              parsed_args.sim.split(";"))
[1113]183    # Steps for generating first population
[1128]184    init_stages = [
[1138]185        FramsPopulation(frams_lib, parsed_args.genformat, parsed_args.popsize)
[1128]186    ]
[1113]187
188    # Selection procedure
[1129]189    selection = TournamentSelection(parsed_args.tournament,
[1128]190                                    copy=True)  # 'fitness' by default, the targeted attribute can be changed, e.g. fit_attr="fitness_raw"
[1113]191
192    # Procedure for generating new population. This steps will be run as long there is less than
193    # popsize individuals in the new population
[1138]194    new_generation_stages = [FramsCrossAndMutate(frams_lib, cross_prob=0.2, mutate_prob=0.9)]
[1113]195
[1146]196    # Steps after new population is created. Executed exactly once per generation.
[1113]197    generation_modifications = []
198
199    # -------------------------------------------------
200    # Fitness
201
[1138]202    fitness_raw = FitnessStep(frams_lib, fields={parsed_args.opt: "fitness_raw",
[1182]203                                                 "numparts": "numparts",
204                                                 "numjoints": "numjoints",
205                                                 "numneurons": "numneurons",
206                                                 "numconnections": "numconnections"},
[1138]207                              fields_defaults={parsed_args.opt: None, "numparts": float("inf"),
208                                               "numjoints": float("inf"), "numneurons": float("inf"),
209                                               "numconnections": float("inf")},
[1113]210                              evaluation_count=1)
211
[1138]212    fitness_end = FitnessStep(frams_lib, fields={parsed_args.opt: "fitness_raw"},
[1113]213                              fields_defaults={parsed_args.opt: None},
[1139]214                              evaluation_count=parsed_args.hof_evaluations)
[1113]215    # Remove
216    remove = []
217    remove.append(FieldRemove("fitness_raw", None))  # Remove individuals if they have default value for fitness
[1138]218    if parsed_args.max_numparts is not None:
219        # This could be also implemented by "LambdaRemove(lambda x: x.numparts > parsed_args.num_parts)"
220        # But this would not serialize in checkpoint.
221        remove.append(NumPartsHigher(parsed_args.max_numparts))
222    if parsed_args.max_numjoints is not None:
223        remove.append(NumJointsHigher(parsed_args.max_numjoints))
224    if parsed_args.max_numneurons is not None:
225        remove.append(NumNeuronsHigher(parsed_args.max_numneurons))
226    if parsed_args.max_numconnections is not None:
227        remove.append(NumConnectionsHigher(parsed_args.max_numconnections))
[1182]228    if parsed_args.max_numgenochars is not -1:
229        remove.append(NumCharsHigher(parsed_args.max_numgenochars))
[1138]230
[1113]231    remove_step = UnionStep(remove)
232
233    fitness_remove = UnionStep([fitness_raw, remove_step])
234
235    init_stages.append(fitness_remove)
236    new_generation_stages.append(fitness_remove)
237
238    # -------------------------------------------------
239    # Novelty or niching
[1145]240    knn = parsed_args.knn
241    if parsed_args.fit == Fitness.knn_novelty or parsed_args.fit == Fitness.knn_niching:
242        reduction_method = "knn_mean"
243        assert knn is not None, "'k' must be set for knn-based fitness."
244        assert knn > 0, "'k' must be positive."
245        assert knn < parsed_args.popsize, "'k' must be smaller than population size."
246    else:
247        reduction_method = "mean"
248        assert knn is None, "'k' is irrelevant unless knn-based fitness is used."
249
[1113]250    dissim = None
251    if parsed_args.dissim == Dissim.levenshtein:
[1145]252        dissim = LevenshteinDissimilarity(reduction=reduction_method, knn=knn, output_field="dissim")
[1113]253    elif parsed_args.dissim == Dissim.frams:
[1145]254        dissim = FramsDissimilarity(frams_lib, reduction=reduction_method, knn=knn, output_field="dissim")
[1113]255
256    if parsed_args.fit == Fitness.raw:
257        # Fitness is equal to finess raw
258        raw = LambdaStep(func_raw)
259        init_stages.append(raw)
260        generation_modifications.append(raw)
261
[1182]262    if parsed_args.fit == Fitness.niching:  # TODO reduce redundancy in the four cases below: dictionary?
263
[1113]264        niching = UnionStep([
[1182]265            ArchiveDissimilarity(parsed_args.archive_size, dissim),
[1113]266            LambdaStep(func_niching)
267        ])
268        init_stages.append(niching)
269        generation_modifications.append(niching)
270
271    if parsed_args.fit == Fitness.novelty:
272        novelty = UnionStep([
[1182]273            ArchiveDissimilarity(parsed_args.archive_size, dissim),
[1113]274            LambdaStep(func_novelty)
275        ])
276        init_stages.append(novelty)
277        generation_modifications.append(novelty)
[1182]278
[1145]279    if parsed_args.fit == Fitness.knn_niching:
280        knn_niching = UnionStep([
[1182]281            ArchiveDissimilarity(parsed_args.archive_size, dissim),
[1145]282            LambdaStep(func_knn_niching)
283        ])
284        init_stages.append(knn_niching)
285        generation_modifications.append(knn_niching)
[1182]286
[1145]287    if parsed_args.fit == Fitness.knn_novelty:
288        knn_novelty = UnionStep([
[1182]289            ArchiveDissimilarity(parsed_args.archive_size, dissim),
[1145]290            LambdaStep(func_knn_novelty)
291        ])
292        init_stages.append(knn_novelty)
293        generation_modifications.append(knn_novelty)
[1113]294
295    # -------------------------------------------------
296    # Statistics
[1140]297    hall_of_fame = HallOfFameStatistics(parsed_args.hof_size, "fitness_raw")  # Wrapper for halloffamae
[1139]298    replace_with_hof = ReplaceWithHallOfFame(hall_of_fame)
[1113]299    statistics_deap = StatisticsDeap([
300        ("avg", np.mean),
301        ("stddev", np.std),
302        ("min", np.min),
303        ("max", np.max)
304    ], extract_fitness)  # Wrapper for deap statistics
305
306    statistics_union = UnionStep([
307        hall_of_fame,
308        statistics_deap
309    ])  # Union of two statistics steps.
310
311    init_stages.append(statistics_union)
312    generation_modifications.append(statistics_union)
313
314    # -------------------------------------------------
[1146]315    # End stages: this will execute exactly once after all generations.
[1113]316    end_stages = [
[1139]317        replace_with_hof,
[1113]318        fitness_end,
[1128]319        PopulationSave("halloffame.gen", provider=hall_of_fame.halloffame, fields={"genotype": "genotype",
[1182]320                                                                                   "fitness": "fitness_raw"})]
[1113]321    # ...but custom fields can be added, e.g. "custom": "recording"
322
323    # -------------------------------------------------
[1139]324
[1113]325    # Experiment creation
326
327    experiment = Experiment(init_population=init_stages,
328                            selection=selection,
329                            new_generation_steps=new_generation_stages,
330                            generation_modification=generation_modifications,
331                            end_steps=end_stages,
332                            population_size=parsed_args.popsize,
333                            checkpoint_path=parsed_args.checkpoint_path,
334                            checkpoint_interval=parsed_args.checkpoint_interval
335                            )
[1128]336    return experiment
337
338
339def main():
340    print("Running experiment with", sys.argv)
341    parsed_args = parseArguments()
[1139]342    if parsed_args.debug:
343        logging.basicConfig(level=logging.DEBUG)
[1128]344
[1136]345    if parsed_args.checkpoint_path is not None and os.path.exists(parsed_args.checkpoint_path):
[1128]346        experiment = load_experiment(parsed_args.checkpoint_path)
347    else:
348        experiment = create_experiment()
349        experiment.init()  # init is mandatory
350
351    experiment.run(parsed_args.generations)
352
[1113]353    # Next call for experiment.run(10) will do nothing. Parameter 10 specifies how many generations should be
354    # in one experiment. Previous call generated 10 generations.
355    # Example 1:
356    # experiment.init()
357    # experiment.run(10)
358    # experiment.run(12)
359    # #This will run for total of 12 generations
360    #
361    # Example 2
362    # experiment.init()
363    # experiment.run(10)
364    # experiment.init()
365    # experiment.run(10)
366    # # All work produced by first run will be "destroyed" by second init().
367
368
369if __name__ == '__main__':
[1182]370    main()
Note: See TracBrowser for help on using the repository browser.