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

Last change on this file since 1138 was 1138, checked in by Maciej Komosinski, 4 years ago

Added limits for the number of Parts, Joints, Neurons and Neural connections

File size: 11.8 KB
Line 
1import argparse
2import os
3import pickle
4import sys
5from enum import Enum
6
7import numpy as np
8
9from FramsticksLib import FramsticksLib
10from evolalg.base.lambda_step import LambdaStep
11from evolalg.base.step import Step
12from evolalg.dissimilarity.frams_dissimilarity import FramsDissimilarity
13from evolalg.dissimilarity.levenshtein import LevenshteinDissimilarity
14from evolalg.experiment import Experiment
15from evolalg.fitness.fitness_step import FitnessStep
16from evolalg.mutation_cross.frams_cross_and_mutate import FramsCrossAndMutate
17from evolalg.population.frams_population import FramsPopulation
18from evolalg.repair.remove.field import FieldRemove
19from evolalg.repair.remove.remove import Remove
20from evolalg.selection.tournament import TournamentSelection
21from evolalg.statistics.halloffame_stats import HallOfFameStatistics
22from evolalg.statistics.statistics_deap import StatisticsDeap
23from evolalg.base.union_step import UnionStep
24from evolalg.utils.population_save import PopulationSave
25import time
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
48    def __str__(self):
49        return self.name
50
51
52def parseArguments():
53    parser = argparse.ArgumentParser(
54        description='Run this program with "python -u %s" if you want to disable buffering of its output.' % sys.argv[
55            0])
56    parser.add_argument('-path', type=ensureDir, required=True, help='Path to the Framsticks library without trailing slash.')
57    parser.add_argument('-opt', required=True,
58                        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). Single or multiple criteria.')
59    parser.add_argument('-lib', required=False, help="Filename of .so or .dll with the Framsticks library")
60    parser.add_argument('-genformat', required=False, default="1",
61                        help='Genetic format for the demo run, for example 4, 9, or B. If not given, f1 is assumed.')
62    parser.add_argument('-sim', required=False, default="eval-allcriteria.sim", help="Name of the .sim file with all parameter values")
63    parser.add_argument('-dissim', required=False, type=Dissim, default=Dissim.frams,
64                        help='Dissimilarity measure, default = frams', choices=list(Dissim))
65    parser.add_argument('-fit', required=False, default=Fitness.raw, type=Fitness,
66                        help=' Fitness criteria, default = raw', choices=list(Fitness))
67    parser.add_argument('-popsize', type=int, default=50, help="Population size, default 50.")
68    parser.add_argument('-generations', type=int, default=5, help="Number of generations, default 5.")
69    parser.add_argument('-tournament', type=int, default=5, help="Tournament size, default 5.")
70
71    parser.add_argument('-max_numparts', type=int, default=None, help="Maximum number of Parts. Default: no limit")
72    parser.add_argument('-max_numjoints', type=int, default=None, help="Maximum number of Joints. Default: no limit")
73    parser.add_argument('-max_numneurons', type=int, default=None, help="Maximum number of Neurons. Default: no limit")
74    parser.add_argument('-max_numconnections', type=int, default=None, help="Maximum number of Neural connections. Default: no limit")
75
76    parser.add_argument('-checkpoint_path', required=False, default=None, help="Path to the checkpoint file")
77    parser.add_argument('-checkpoint_interval', required=False, type=int, default=100, help="Checkpoint interval")
78    return parser.parse_args()
79
80
81def extract_fitness(ind):
82    return ind.fitness_raw
83
84
85def print_population_count(pop):
86    print("Current popsize:", len(pop))
87    return pop  # Each step must return a population
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
126def func_niching(ind): setattr(ind, "fitness", ind.fitness_raw * (1 + ind.dissim))
127
128
129def func_raw(ind): setattr(ind, "fitness", ind.fitness_raw)
130
131
132def func_novelty(ind): setattr(ind, "fitness", ind.dissim)
133
134
135def load_experiment(path):
136    with open(path, "rb") as file:
137        experiment = pickle.load(file)
138    print("Loaded experiment. Generation:", experiment.generation)
139    return experiment
140
141
142def create_experiment():
143    parsed_args = parseArguments()
144    frams_lib = FramsticksLib(parsed_args.path, parsed_args.lib,
145                          parsed_args.sim)
146    # Steps for generating first population
147    init_stages = [
148        FramsPopulation(frams_lib, parsed_args.genformat, parsed_args.popsize)
149    ]
150
151    # Selection procedure
152    selection = TournamentSelection(parsed_args.tournament,
153                                    copy=True)  # 'fitness' by default, the targeted attribute can be changed, e.g. fit_attr="fitness_raw"
154
155    # Procedure for generating new population. This steps will be run as long there is less than
156    # popsize individuals in the new population
157    new_generation_stages = [FramsCrossAndMutate(frams_lib, cross_prob=0.2, mutate_prob=0.9)]
158
159    # Steps after new population is created. Executed exacly once per generation.
160    generation_modifications = []
161
162    # -------------------------------------------------
163    # Fitness
164
165    fitness_raw = FitnessStep(frams_lib, fields={parsed_args.opt: "fitness_raw",
166                                             "numparts": "numparts",
167                                             "numjoints": "numjoints",
168                                             "numneurons": "numneurons",
169                                             "numconnections": "numconnections"},
170                              fields_defaults={parsed_args.opt: None, "numparts": float("inf"),
171                                               "numjoints": float("inf"), "numneurons": float("inf"),
172                                               "numconnections": float("inf")},
173                              evaluation_count=1)
174
175    fitness_end = FitnessStep(frams_lib, fields={parsed_args.opt: "fitness_raw"},
176                              fields_defaults={parsed_args.opt: None},
177                              evaluation_count=100)  # evaluate the contents of the last population 100 times (TODO replace this approach and evaluate HOF instead of the last population)
178    # Remove
179    remove = []
180    remove.append(FieldRemove("fitness_raw", None))  # Remove individuals if they have default value for fitness
181    if parsed_args.max_numparts is not None:
182        # This could be also implemented by "LambdaRemove(lambda x: x.numparts > parsed_args.num_parts)"
183        # But this would not serialize in checkpoint.
184        remove.append(NumPartsHigher(parsed_args.max_numparts))
185    if parsed_args.max_numjoints is not None:
186        remove.append(NumJointsHigher(parsed_args.max_numjoints))
187    if parsed_args.max_numneurons is not None:
188        remove.append(NumNeuronsHigher(parsed_args.max_numneurons))
189    if parsed_args.max_numconnections is not None:
190        remove.append(NumConnectionsHigher(parsed_args.max_numconnections))
191
192    remove_step = UnionStep(remove)
193
194    fitness_remove = UnionStep([fitness_raw, remove_step])
195
196    init_stages.append(fitness_remove)
197    new_generation_stages.append(fitness_remove)
198
199    # -------------------------------------------------
200    # Novelty or niching
201    dissim = None
202    if parsed_args.dissim == Dissim.levenshtein:
203        dissim = LevenshteinDissimilarity(reduction="mean", output_field="dissim")
204    elif parsed_args.dissim == Dissim.frams:
205        dissim = FramsDissimilarity(frams_lib, reduction="mean", output_field="dissim")
206
207    if parsed_args.fit == Fitness.raw:
208        # Fitness is equal to finess raw
209        raw = LambdaStep(func_raw)
210        init_stages.append(raw)
211        generation_modifications.append(raw)
212
213    if parsed_args.fit == Fitness.niching:
214        niching = UnionStep([
215            dissim,
216            LambdaStep(func_niching)
217        ])
218        init_stages.append(niching)
219        generation_modifications.append(niching)
220
221    if parsed_args.fit == Fitness.novelty:
222        novelty = UnionStep([
223            dissim,
224            LambdaStep(func_novelty)
225        ])
226        init_stages.append(novelty)
227        generation_modifications.append(novelty)
228
229    # -------------------------------------------------
230    # Statistics
231    hall_of_fame = HallOfFameStatistics(100, "fitness_raw")  # Wrapper for halloffamae
232    statistics_deap = StatisticsDeap([
233        ("avg", np.mean),
234        ("stddev", np.std),
235        ("min", np.min),
236        ("max", np.max)
237    ], extract_fitness)  # Wrapper for deap statistics
238
239    statistics_union = UnionStep([
240        hall_of_fame,
241        statistics_deap
242    ])  # Union of two statistics steps.
243
244    init_stages.append(statistics_union)
245    generation_modifications.append(statistics_union)
246
247    # -------------------------------------------------
248    # End stages: this will execute exacly once after all generations.
249    end_stages = [
250        fitness_end,
251        PopulationSave("halloffame.gen", provider=hall_of_fame.halloffame, fields={"genotype": "genotype",
252                                                                                  "fitness": "fitness_raw"})]
253    # ...but custom fields can be added, e.g. "custom": "recording"
254
255    # -------------------------------------------------
256    # Experiment creation
257
258    experiment = Experiment(init_population=init_stages,
259                            selection=selection,
260                            new_generation_steps=new_generation_stages,
261                            generation_modification=generation_modifications,
262                            end_steps=end_stages,
263                            population_size=parsed_args.popsize,
264                            checkpoint_path=parsed_args.checkpoint_path,
265                            checkpoint_interval=parsed_args.checkpoint_interval
266                            )
267    return experiment
268
269
270def main():
271    print("Running experiment with", sys.argv)
272    parsed_args = parseArguments()
273
274    if parsed_args.checkpoint_path is not None and os.path.exists(parsed_args.checkpoint_path):
275        experiment = load_experiment(parsed_args.checkpoint_path)
276        FramsticksLib(parsed_args.path, parsed_args.lib,
277                      parsed_args.sim)
278    else:
279        experiment = create_experiment()
280        experiment.init()  # init is mandatory
281
282
283    experiment.run(parsed_args.generations)
284
285    # Next call for experiment.run(10) will do nothing. Parameter 10 specifies how many generations should be
286    # in one experiment. Previous call generated 10 generations.
287    # Example 1:
288    # experiment.init()
289    # experiment.run(10)
290    # experiment.run(12)
291    # #This will run for total of 12 generations
292    #
293    # Example 2
294    # experiment.init()
295    # experiment.run(10)
296    # experiment.init()
297    # experiment.run(10)
298    # # All work produced by first run will be "destroyed" by second init().
299
300
301
302if __name__ == '__main__':
303    main()
Note: See TracBrowser for help on using the repository browser.