source: framspy/evolalg/examples/multicriteria.py @ 1147

Last change on this file since 1147 was 1147, checked in by Maciej Komosinski, 3 years ago

Multi-criteria optimization, can optionally include diversity is one criterion

File size: 13.7 KB
Line 
1#TODO hof should be the complete non-dominated set from the entire process of evolution (all evaluated individuals), not limited in any way (remove '-hof_size'). Now we are not storing the entire Pareto front, but individuals that were better than others in hof [better=on all criteria?] at the time of inserting to hof. Hof has a size limit.
2#TODO when -dissim is used, print its statistics just like all other criteria
3#TODO in statistics, do not print "gen" (generation number) for each criterion
4#TODO if possible, when saving the final .gen file, instead of fitness as a python tuple, save individual criteria - so instead of fitness:(0.005251036058321138, 0.025849976588613266), write "velocity:...\nvertpos:..." (this also applies to other .py examples in this directory)
5
6
7import argparse
8import logging
9import os
10import pickle
11import sys
12import copy
13from enum import Enum
14
15import numpy as np
16
17from deap import base
18from deap import tools
19
20from FramsticksLib import FramsticksLib
21from evolalg.base.lambda_step import LambdaStep
22from evolalg.base.step import Step
23from evolalg.dissimilarity.frams_dissimilarity import FramsDissimilarity
24from evolalg.dissimilarity.levenshtein import LevenshteinDissimilarity
25from evolalg.experiment import Experiment
26from evolalg.fitness.fitness_step import FitnessStep
27from evolalg.mutation_cross.frams_cross_and_mutate import FramsCrossAndMutate
28from evolalg.population.frams_population import FramsPopulation
29from evolalg.repair.remove.field import FieldRemove
30from evolalg.repair.remove.remove import Remove
31from evolalg.selection.nsga2 import NSGA2Selection
32from evolalg.statistics.halloffame_stats import HallOfFameStatistics
33from evolalg.statistics.multistatistics_deap import MultiStatistics
34from evolalg.statistics.statistics_deap import StatisticsDeap
35from evolalg.base.union_step import UnionStep
36from evolalg.utils.population_save import PopulationSave
37
38
39def ensureDir(string):
40    if os.path.isdir(string):
41        return string
42    else:
43        raise NotADirectoryError(string)
44
45
46class Dissim(Enum):
47    levenshtein = "levenshtein"
48    frams = "frams"
49
50    def __str__(self):
51        return self.name
52
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])
59    parser.add_argument('-path', type=ensureDir, required=True,
60                        help='Path to the Framsticks library without trailing slash.')
61    parser.add_argument('-opt', required=True,
62                        help='optimization criteria seperated with a comma: vertpos, velocity, distance, vertvel, lifespan, numjoints, numparts, numneurons, numconnections (and others as long as they are provided by the .sim file and its .expdef). Single or multiple criteria.')
63    parser.add_argument('-lib', required=False, help="Filename of .so or .dll with the Framsticks library")
64
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.')
67    parser.add_argument('-sim', required=False, default="eval-allcriteria.sim",
68                        help="Name of the .sim file with all parameter values")
69    parser.add_argument('-dissim', required=False, type=Dissim, default=Dissim.frams,
70                        help='Dissimilarity measure, default: frams', choices=list(Dissim))
71    parser.add_argument('-popsize', type=int, default=40, help="Population size (must be a multiple of 4), default: 40.") # mod 4 because of DEAP
72    parser.add_argument('-generations', type=int, default=5, help="Number of generations, default: 5.")
73
74    parser.add_argument('-max_numparts', type=int, default=None, help="Maximum number of Parts. Default: no limit")
75    parser.add_argument('-max_numjoints', type=int, default=None, help="Maximum number of Joints. Default: no limit")
76    parser.add_argument('-max_numneurons', type=int, default=None, help="Maximum number of Neurons. Default: no limit")
77    parser.add_argument('-max_numconnections', type=int, default=None,
78                        help="Maximum number of Neural connections. Default: no limit")
79
80    parser.add_argument('-hof_size', type=int, default=10, help="Number of genotypes in Hall of Fame. Default: 10.")
81    parser.add_argument('-hof_evaluations', type=int, default=20,
82                        help="Number of final evaluations of each genotype in Hall of Fame to obtain reliable (averaged) fitness. Default: 20.")
83    parser.add_argument('-checkpoint_path', required=False, default=None, help="Path to the checkpoint file")
84    parser.add_argument('-checkpoint_interval', required=False, type=int, default=100, help="Checkpoint interval")
85    parser.add_argument('-debug', dest='debug', action='store_true', help="Prints names of steps as they are executed")
86    parser.set_defaults(debug=False)
87    return parser.parse_args()
88
89
90class NumPartsHigher(Remove):
91    def __init__(self, max_number):
92        super(NumPartsHigher, self).__init__()
93        self.max_number = max_number
94
95    def remove(self, individual):
96        return individual.numparts > self.max_number
97
98
99class NumJointsHigher(Remove):
100    def __init__(self, max_number):
101        super(NumJointsHigher, self).__init__()
102        self.max_number = max_number
103
104    def remove(self, individual):
105        return individual.numjoints > self.max_number
106
107
108class NumNeuronsHigher(Remove):
109    def __init__(self, max_number):
110        super(NumNeuronsHigher, self).__init__()
111        self.max_number = max_number
112
113    def remove(self, individual):
114        return individual.numneurons > self.max_number
115
116
117class NumConnectionsHigher(Remove):
118    def __init__(self, max_number):
119        super(NumConnectionsHigher, self).__init__()
120        self.max_number = max_number
121
122    def remove(self, individual):
123        return individual.numconnections > self.max_number
124
125
126class ReplaceWithHallOfFame(Step):
127    def __init__(self, hof, *args, **kwargs):
128        super(ReplaceWithHallOfFame, self).__init__(*args, **kwargs)
129        self.hof = hof
130
131    def call(self, population, *args, **kwargs):
132        super(ReplaceWithHallOfFame, self).call(population)
133        return list(self.hof.halloffame)
134
135
136class DeapFitness(base.Fitness):
137    weights = (1, 1)
138
139    def __init__(self, *args, **kwargs):
140        super(DeapFitness, self).__init__(*args, **kwargs)
141
142
143class Nsga2Fitness:
144    def __init__(self, fields):
145        self.fields = fields
146    def __call__(self, ind):
147        setattr(ind, "fitness", DeapFitness(tuple(getattr(ind, _) for _ in self.fields)))
148
149
150
151class ExtractField:
152    def __init__(self, field_name):
153        self.field_name = field_name
154    def __call__(self, ind):
155        return getattr(ind, self.field_name)
156
157def extract_fitness(ind):
158    return ind.fitness_raw
159
160
161def load_experiment(path):
162    with open(path, "rb") as file:
163        experiment = pickle.load(file)
164    print("Loaded experiment. Generation:", experiment.generation)
165    return experiment
166
167
168def create_experiment():
169    parsed_args = parseArguments()
170    frams_lib = FramsticksLib(parsed_args.path, parsed_args.lib,
171                              parsed_args.sim)
172
173    opt_dissim = []
174    opt_fitness = []
175    for crit in parsed_args.opt.split(','):
176        try:
177            Dissim(crit)
178            opt_dissim.append(crit)
179        except ValueError:
180            opt_fitness.append(crit)
181    if len(opt_dissim) > 1:
182        raise ValueError("Only one type of dissimilarity supported")
183
184    # Steps for generating first population
185    init_stages = [
186        FramsPopulation(frams_lib, parsed_args.genformat, parsed_args.popsize)
187    ]
188
189    # Selection procedure
190    selection = NSGA2Selection(copy=True)
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
194    new_generation_stages = [FramsCrossAndMutate(frams_lib, cross_prob=0.2, mutate_prob=0.9)]
195
196    # Steps after new population is created. Executed exactly once per generation.
197    generation_modifications = []
198
199    # -------------------------------------------------
200    # Fitness
201
202    fitness_raw = FitnessStep(frams_lib, fields={**{_:_ for _ in opt_fitness},
203                                                 "numparts": "numparts",
204                                                 "numjoints": "numjoints",
205                                                 "numneurons": "numneurons",
206                                                 "numconnections": "numconnections"},
207                              fields_defaults={parsed_args.opt: None, "numparts": float("inf"),
208                                               "numjoints": float("inf"), "numneurons": float("inf"),
209                                               "numconnections": float("inf"),
210                                               **{_:None for _ in opt_fitness}
211                                               },
212                              evaluation_count=1)
213
214    fitness_end = FitnessStep(frams_lib, fields={_:_ for _ in opt_fitness},
215                              fields_defaults={parsed_args.opt: None},
216                              evaluation_count=parsed_args.hof_evaluations)
217    # Remove
218    remove = []
219    remove.append(FieldRemove(opt_fitness[0], None))  # Remove individuals if they have default value for fitness
220    if parsed_args.max_numparts is not None:
221        # This could be also implemented by "LambdaRemove(lambda x: x.numparts > parsed_args.num_parts)"
222        # But this would not serialize in checkpoint.
223        remove.append(NumPartsHigher(parsed_args.max_numparts))
224    if parsed_args.max_numjoints is not None:
225        remove.append(NumJointsHigher(parsed_args.max_numjoints))
226    if parsed_args.max_numneurons is not None:
227        remove.append(NumNeuronsHigher(parsed_args.max_numneurons))
228    if parsed_args.max_numconnections is not None:
229        remove.append(NumConnectionsHigher(parsed_args.max_numconnections))
230
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    # Dissimilarity as one of the criteria
240    dissim = None
241    if len(opt_dissim) > 0 and Dissim(opt_dissim[0]) == Dissim.levenshtein:
242        dissim = LevenshteinDissimilarity(reduction="mean", output_field="dissim")
243    elif len(opt_dissim) > 0 and Dissim(opt_dissim[0]) == Dissim.frams:
244        dissim = FramsDissimilarity(frams_lib, reduction="mean", output_field="dissim")
245
246    if dissim is not None:
247        init_stages.append(dissim)
248        generation_modifications.append(dissim)
249
250    if dissim is not None:
251        nsga2_fittnes = Nsga2Fitness(["dissim"]+ opt_fitness)
252    else:
253        nsga2_fittnes = Nsga2Fitness(opt_fitness)
254
255    init_stages.append(LambdaStep(nsga2_fittnes))
256    generation_modifications.append(LambdaStep(nsga2_fittnes))
257
258    # -------------------------------------------------
259    # Statistics
260    hall_of_fame = HallOfFameStatistics(parsed_args.hof_size, "fitness")  # Wrapper for halloffamae
261    replace_with_hof = ReplaceWithHallOfFame(hall_of_fame)
262    statistics_deap = MultiStatistics({fit:StatisticsDeap([
263        ("avg", np.mean),
264        ("stddev", np.std),
265        ("min", np.min),
266        ("max", np.max)
267    ], ExtractField(fit)) for fit in opt_fitness}) # Wrapper for deap statistics
268
269    statistics_union = UnionStep(
270        [hall_of_fame,
271        statistics_deap]
272    )  # Union of two statistics steps.
273
274    init_stages.append(statistics_union)
275    generation_modifications.append(statistics_union)
276
277    # -------------------------------------------------
278    # End stages: this will execute exactly once after all generations.
279    end_stages = [
280        replace_with_hof,
281        fitness_end,
282        PopulationSave("halloffame.gen", provider=hall_of_fame.halloffame, fields={"genotype": "genotype",
283                                                                                   "fitness": "fitness"})]
284    # ...but custom fields can be added, e.g. "custom": "recording"
285
286    # -------------------------------------------------
287
288    # Experiment creation
289
290    experiment = Experiment(init_population=init_stages,
291                            selection=selection,
292                            new_generation_steps=new_generation_stages,
293                            generation_modification=generation_modifications,
294                            end_steps=end_stages,
295                            population_size=parsed_args.popsize,
296                            checkpoint_path=parsed_args.checkpoint_path,
297                            checkpoint_interval=parsed_args.checkpoint_interval
298                            )
299    return experiment
300
301
302def main():
303    print("Running experiment with", sys.argv)
304    parsed_args = parseArguments()
305    if parsed_args.debug:
306        logging.basicConfig(level=logging.DEBUG)
307
308    if parsed_args.checkpoint_path is not None and os.path.exists(parsed_args.checkpoint_path):
309        experiment = load_experiment(parsed_args.checkpoint_path)
310        FramsticksLib(parsed_args.path, parsed_args.lib,
311                      parsed_args.sim)
312    else:
313        experiment = create_experiment()
314        experiment.init()  # init is mandatory
315
316    experiment.run(parsed_args.generations)
317
318    # Next call for experiment.run(10) will do nothing. Parameter 10 specifies how many generations should be
319    # in one experiment. Previous call generated 10 generations.
320    # Example 1:
321    # experiment.init()
322    # experiment.run(10)
323    # experiment.run(12)
324    # #This will run for total of 12 generations
325    #
326    # Example 2
327    # experiment.init()
328    # experiment.run(10)
329    # experiment.init()
330    # experiment.run(10)
331    # # All work produced by first run will be "destroyed" by second init().
332
333
334if __name__ == '__main__':
335    main()
Note: See TracBrowser for help on using the repository browser.