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

Last change on this file since 1145 was 1145, checked in by Maciej Komosinski, 15 months ago

Added niching and novelty search with limited (i.e., local) competition ("nearest neighbors" according to dissimilarity measure)

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