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

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

Added support for loading multiple .sim files where each can overwrite selected settings

File size: 13.8 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. If you want to provide more files, separate them with a semicolon ';'.")
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, parsed_args.sim.split(";"))
171
172    opt_dissim = []
173    opt_fitness = []
174    for crit in parsed_args.opt.split(','):
175        try:
176            Dissim(crit)
177            opt_dissim.append(crit)
178        except ValueError:
179            opt_fitness.append(crit)
180    if len(opt_dissim) > 1:
181        raise ValueError("Only one type of dissimilarity supported")
182
183    # Steps for generating first population
184    init_stages = [
185        FramsPopulation(frams_lib, parsed_args.genformat, parsed_args.popsize)
186    ]
187
188    # Selection procedure
189    selection = NSGA2Selection(copy=True)
190
191    # Procedure for generating new population. This steps will be run as long there is less than
192    # popsize individuals in the new population
193    new_generation_stages = [FramsCrossAndMutate(frams_lib, cross_prob=0.2, mutate_prob=0.9)]
194
195    # Steps after new population is created. Executed exactly once per generation.
196    generation_modifications = []
197
198    # -------------------------------------------------
199    # Fitness
200
201    fitness_raw = FitnessStep(frams_lib, fields={**{_:_ for _ in opt_fitness},
202                                                 "numparts": "numparts",
203                                                 "numjoints": "numjoints",
204                                                 "numneurons": "numneurons",
205                                                 "numconnections": "numconnections"},
206                              fields_defaults={parsed_args.opt: None, "numparts": float("inf"),
207                                               "numjoints": float("inf"), "numneurons": float("inf"),
208                                               "numconnections": float("inf"),
209                                               **{_:None for _ in opt_fitness}
210                                               },
211                              evaluation_count=1)
212
213    fitness_end = FitnessStep(frams_lib, fields={_:_ for _ in opt_fitness},
214                              fields_defaults={parsed_args.opt: None},
215                              evaluation_count=parsed_args.hof_evaluations)
216    # Remove
217    remove = []
218    remove.append(FieldRemove(opt_fitness[0], None))  # Remove individuals if they have default value for fitness
219    if parsed_args.max_numparts is not None:
220        # This could be also implemented by "LambdaRemove(lambda x: x.numparts > parsed_args.num_parts)"
221        # But this would not serialize in checkpoint.
222        remove.append(NumPartsHigher(parsed_args.max_numparts))
223    if parsed_args.max_numjoints is not None:
224        remove.append(NumJointsHigher(parsed_args.max_numjoints))
225    if parsed_args.max_numneurons is not None:
226        remove.append(NumNeuronsHigher(parsed_args.max_numneurons))
227    if parsed_args.max_numconnections is not None:
228        remove.append(NumConnectionsHigher(parsed_args.max_numconnections))
229
230    remove_step = UnionStep(remove)
231
232    fitness_remove = UnionStep([fitness_raw, remove_step])
233
234    init_stages.append(fitness_remove)
235    new_generation_stages.append(fitness_remove)
236
237    # -------------------------------------------------
238    # Dissimilarity as one of the criteria
239    dissim = None
240    if len(opt_dissim) > 0 and Dissim(opt_dissim[0]) == Dissim.levenshtein:
241        dissim = LevenshteinDissimilarity(reduction="mean", output_field="dissim")
242    elif len(opt_dissim) > 0 and Dissim(opt_dissim[0]) == Dissim.frams:
243        dissim = FramsDissimilarity(frams_lib, reduction="mean", output_field="dissim")
244
245    if dissim is not None:
246        init_stages.append(dissim)
247        generation_modifications.append(dissim)
248
249    if dissim is not None:
250        nsga2_fittnes = Nsga2Fitness(["dissim"]+ opt_fitness)
251    else:
252        nsga2_fittnes = Nsga2Fitness(opt_fitness)
253
254    init_stages.append(LambdaStep(nsga2_fittnes))
255    generation_modifications.append(LambdaStep(nsga2_fittnes))
256
257    # -------------------------------------------------
258    # Statistics
259    hall_of_fame = HallOfFameStatistics(parsed_args.hof_size, "fitness")  # Wrapper for halloffamae
260    replace_with_hof = ReplaceWithHallOfFame(hall_of_fame)
261    statistics_deap = MultiStatistics({fit:StatisticsDeap([
262        ("avg", np.mean),
263        ("stddev", np.std),
264        ("min", np.min),
265        ("max", np.max)
266    ], ExtractField(fit)) for fit in opt_fitness}) # Wrapper for deap statistics
267
268    statistics_union = UnionStep(
269        [hall_of_fame,
270        statistics_deap]
271    )  # Union of two statistics steps.
272
273    init_stages.append(statistics_union)
274    generation_modifications.append(statistics_union)
275
276    # -------------------------------------------------
277    # End stages: this will execute exactly once after all generations.
278    end_stages = [
279        replace_with_hof,
280        fitness_end,
281        PopulationSave("halloffame.gen", provider=hall_of_fame.halloffame, fields={"genotype": "genotype",
282                                                                                   "fitness": "fitness"})]
283    # ...but custom fields can be added, e.g. "custom": "recording"
284
285    # -------------------------------------------------
286
287    # Experiment creation
288
289    experiment = Experiment(init_population=init_stages,
290                            selection=selection,
291                            new_generation_steps=new_generation_stages,
292                            generation_modification=generation_modifications,
293                            end_steps=end_stages,
294                            population_size=parsed_args.popsize,
295                            checkpoint_path=parsed_args.checkpoint_path,
296                            checkpoint_interval=parsed_args.checkpoint_interval
297                            )
298    return experiment
299
300
301def main():
302    print("Running experiment with", sys.argv)
303    parsed_args = parseArguments()
304   
305    if parsed_args.popsize % 4 != 0:
306        raise ValueError("popsize must be a multiple of 4 (for example %d)." % (parsed_args.popsize//4*4)) # required by deap.tools.selTournamentDCD()
307       
308    if parsed_args.debug:
309        logging.basicConfig(level=logging.DEBUG)
310
311    if parsed_args.checkpoint_path is not None and os.path.exists(parsed_args.checkpoint_path):
312        experiment = load_experiment(parsed_args.checkpoint_path)
313    else:
314        experiment = create_experiment()
315        experiment.init()  # init is mandatory
316
317    experiment.run(parsed_args.generations)
318
319    # Next call for experiment.run(10) will do nothing. Parameter 10 specifies how many generations should be
320    # in one experiment. Previous call generated 10 generations.
321    # Example 1:
322    # experiment.init()
323    # experiment.run(10)
324    # experiment.run(12)
325    # #This will run for total of 12 generations
326    #
327    # Example 2
328    # experiment.init()
329    # experiment.run(10)
330    # experiment.init()
331    # experiment.run(10)
332    # # All work produced by first run will be "destroyed" by second init().
333
334
335if __name__ == '__main__':
336    main()
Note: See TracBrowser for help on using the repository browser.