source: framspy/evolalg/base/experiment_abc.py @ 1190

Last change on this file since 1190 was 1190, checked in by Maciej Komosinski, 17 months ago

Added the "evolalg" module for evolutionary optimization

File size: 9.9 KB
Line 
1import argparse
2import json
3import os
4import pickle
5import time
6from abc import ABC, abstractmethod
7
8from ..base.random_sequence_index import RandomIndexSequence
9from ..constants import BAD_FITNESS
10from ..json_encoders import Encoder
11from ..structures.hall_of_fame import HallOfFame
12from ..structures.individual import Individual
13from ..structures.population import PopulationStructures
14
15
16class ExperimentABC(ABC):
17
18    population_structures = None
19    hof = []
20    stats = []
21    current_generation = 0
22    time_elapsed = 0
23   
24
25    def __init__(self, popsize, hof_size, save_only_best=True) -> None:
26        self.hof = HallOfFame(hof_size)
27        self.popsize = popsize
28        self.save_only_best = save_only_best
29
30    def select(self, individuals, tournament_size, random_index_sequence):
31        """Tournament selection, returns the index of the best individual from those taking part in the tournament"""
32        best_index = None
33        for i in range(tournament_size):
34            rnd_index = random_index_sequence.getNext()
35            if best_index is None or individuals[rnd_index].fitness > best_index.fitness:
36                best_index = individuals[rnd_index]
37        return best_index
38
39    def addGenotypeIfValid(self, ind_list, genotype):
40        new_individual = Individual()
41        new_individual.set_and_evaluate(genotype, self.evaluate)
42        if new_individual.fitness is not BAD_FITNESS:  # this is how we defined BAD_FITNESS in evaluate()
43            ind_list.append(new_individual)
44
45    def make_new_population(self, individuals, prob_mut, prob_xov, tournament_size):
46        """'individuals' is the input population (a list of individuals).
47        Assumptions: all genotypes in 'individuals' are valid and evaluated (have fitness set).
48        Returns: a new population of the same size as 'individuals' with prob_mut mutants, prob_xov offspring, and the remainder of clones."""
49
50        newpop = []
51        N = len(individuals)
52        expected_mut = int(N * prob_mut)
53        expected_xov = int(N * prob_xov)
54        assert expected_mut + \
55            expected_xov <= N, "If probabilities of mutation (%g) and crossover (%g) added together exceed 1.0, then the population would grow every generation..." % (
56                prob_mut, prob_xov)
57        ris = RandomIndexSequence(N)
58
59        # adding valid mutants of selected individuals...
60        while len(newpop) < expected_mut:
61            ind = self.select(
62                individuals, tournament_size=tournament_size, random_index_sequence=ris)
63            self.addGenotypeIfValid(newpop, self.mutate(ind.genotype))
64
65        # adding valid crossovers of selected individuals...
66        while len(newpop) < expected_mut + expected_xov:
67            ind1 = self.select(
68                individuals, tournament_size=tournament_size, random_index_sequence=ris)
69            ind2 = self.select(
70                individuals, tournament_size=tournament_size, random_index_sequence=ris)
71            self.addGenotypeIfValid(
72                newpop, self.cross_over(ind1.genotype, ind2.genotype))
73
74        # select clones to fill up the new population until we reach the same size as the input population
75        while len(newpop) < len(individuals):
76            ind = self.select(
77                individuals, tournament_size=tournament_size, random_index_sequence=ris)
78            newpop.append(Individual().copyFrom(ind))
79
80        return newpop
81
82    def save_state(self, state_filename):
83        state = self.get_state()
84        if state_filename is None:
85            return
86        state_filename_tmp = state_filename + ".tmp"
87        try:
88            with open(state_filename_tmp, "wb") as f:
89                pickle.dump(state, f)
90            # ensures the new file was first saved OK (e.g. enough free space on device), then replace
91            os.replace(state_filename_tmp, state_filename)
92        except Exception as ex:
93            raise RuntimeError("Failed to save evolution state '%s' (because: %s). This does not prevent the experiment from continuing, but let's stop here to fix the problem with saving state files." % (
94                state_filename_tmp, ex))
95
96    def load_state(self, state_filename):
97        if state_filename is None:
98            # print("Loading evolution state: file name not provided")
99            return None
100        try:
101            with open(state_filename, 'rb') as f:
102                state = pickle.load(f)
103                self.set_state(state)
104        except FileNotFoundError:
105            return None
106        print("...Loaded evolution state from '%s'" % state_filename)
107        return True
108
109    def get_state_filename(self, save_file_name):
110        return None if save_file_name is None else save_file_name + '_state.pkl'
111
112    def get_state(self):
113        return [self.time_elapsed, self.current_generation, self.population_structures, self.hof, self.stats]
114
115    def set_state(self, state):
116        self.time_elapsed, self.current_generation, self.population_structures, hof_, self.stats = state
117        # sorting: ensure that we add from worst to best so all individuals are added to HOF
118        for h in sorted(hof_, key=lambda x: x.rawfitness):
119            self.hof.add(h)
120
121    def update_stats(self, generation, all_individuals):
122        worst = min(all_individuals, key=lambda item: item.rawfitness)
123        best = max(all_individuals, key=lambda item: item.rawfitness)
124        # instead of single best, could add all individuals in population here, but then the outcome would depend on the order of adding
125        self.hof.add(best)
126        self.stats.append(
127            best.rawfitness if self.save_only_best else best)
128        print("%d\t%d\t%g\t%g" % (generation, len(
129            all_individuals), worst.rawfitness, best.rawfitness))
130
131    def initialize_evolution(self, initialgenotype):
132        self.current_generation = 0
133        self.time_elapsed = 0
134        self.stats = []  # stores the best individuals, one from each generation
135        initial_individual = Individual()
136        initial_individual.set_and_evaluate(initialgenotype, self.evaluate)
137        self.hof.add(initial_individual)
138        self.stats.append(
139            initial_individual.rawfitness if self.save_only_best else initial_individual)
140        self.population_structures = PopulationStructures(
141            initial_individual=initial_individual, archive_size=0, popsize=self.popsize)
142
143    def evolve(self, hof_savefile, generations, initialgenotype, pmut, pxov, tournament_size):
144        file_name = self.get_state_filename(hof_savefile)
145        state = self.load_state(file_name)
146        if state is not None:  # loaded state from file
147            # saved generation has been completed, start with the next one
148            self.current_generation += 1
149            print("...Resuming from saved state: population size = %d, hof size = %d, stats size = %d, archive size = %d, generation = %d/%d" % (len(self.population_structures.population), len(self.hof),
150                                                                                                                                                 len(self.stats),  (len(self.population_structures.archive)), self.current_generation, generations))  # self.current_generation (and g) are 0-based, parsed_args.generations is 1-based
151
152        else:
153            self.initialize_evolution(initialgenotype)
154        time0 = time.process_time()
155        for g in range(self.current_generation, generations):
156            self.population_structures.population = self.make_new_population(
157                self.population_structures.population, pmut, pxov, tournament_size)
158            self.update_stats(g, self.population_structures.population)
159            if hof_savefile is not None:
160                self.current_generation = g
161                self.time_elapsed += time.process_time() - time0
162                self.save_state(file_name)
163        if hof_savefile is not None:
164            self.save_genotypes(hof_savefile)
165        return self.population_structures.population, self.stats
166
167    @abstractmethod
168    def mutate(self, gen1):
169        pass
170
171    @abstractmethod
172    def cross_over(self, gen1, gen2):
173        pass
174
175    @abstractmethod
176    def evaluate(self, genotype):
177        pass
178
179    def save_genotypes(self, filename):
180        """Implement if you want to save finall genotypes,in default implementation this function is run once at the end of evolution"""
181        state_to_save = {
182            "number_of_generations": self.current_generation,
183            "hof": [{"genotype": individual.genotype,
184                     "fitness": individual.rawfitness} for individual in self.hof.hof]}
185        with open(f"{filename}.json", 'w') as f:
186            json.dump(state_to_save, f, cls=Encoder)
187
188   
189    @staticmethod
190    def get_args_for_parser():
191        parser = argparse.ArgumentParser()
192        parser.add_argument('-popsize',type= int, default= 50,
193                            help="Population size, default: 50.")
194        parser.add_argument('-generations',type= int, default= 5,
195                                help="Number of generations, default: 5.")
196        parser.add_argument('-tournament',type= int, default= 5,
197                            help="Tournament size, default: 5.")
198        parser.add_argument('-pmut',type= float, default= 0.7,
199                        help="Probability of mutation, default: 0.7")
200        parser.add_argument('-pxov',type= float, default= 0.2,
201                        help="Probability of crossover, default: 0.2")
202        parser.add_argument('-hof_size',type= int, default= 10,
203                            help="Number of genotypes in Hall of Fame. Default: 10.")
204        parser.add_argument('-hof_savefile',type= str, required= False,
205                                help= 'If set, Hall of Fame will be saved in Framsticks file format (recommended extension *.gen. This also activates saving state (checpoint} file and auto-resuming from the saved state, if this file exists.')
206        parser.add_argument('-save_only_best',type= bool, default= True, required= False,
207                            help="")
208       
209        return parser
Note: See TracBrowser for help on using the repository browser.