source: framspy/evolalg/base/experiment_niching_abc.py

Last change on this file was 1304, checked in by Maciej Komosinski, 10 days ago

Added archive to the NSLC algorithm

File size: 10.6 KB
RevLine 
[1190]1import time
2from abc import ABC, abstractmethod
3
4import numpy as np
5from deap import base, tools
6from deap.tools.emo import assignCrowdingDist
7
8from ..constants import BAD_FITNESS
9from ..structures.individual import Individual
10from .experiment_abc import ExperimentABC
11from .remove_diagonal import remove_diagonal
12
13
14class DeapFitness(base.Fitness):
15    weights = (1, 1)
16
17    def __init__(self, *args, **kwargs):
18        super(DeapFitness, self).__init__(*args, **kwargs)
19
20
21class ExperimentNiching(ExperimentABC, ABC):
22    fit: str = "niching"
23    normalize: str = "None"
24    archive_size: int = None
25
[1289]26    def __init__(self, fit, normalize, popsize, hof_size, save_only_best, knn_niching, knn_nslc, archive_size) -> None:
[1190]27        ExperimentABC.__init__(self,popsize=popsize, hof_size=hof_size, save_only_best=save_only_best)
28        self.fit = fit
[1194]29        self.normalize = normalize
[1271]30        self.knn_niching = knn_niching # this parameter is used for local novelty and local niching
[1190]31        self.knn_nslc = knn_nslc
32        self.archive_size=archive_size
33
[1271]34        # np.argpartition requires these parameters to be at most popsize-2; popsize is decreased by 1 because we remove_diagonal()
35        if self.knn_niching > popsize - 2:
36            raise ValueError("knn_niching (%d) should be at most popsize-2 (%d)" % (self.knn_niching, popsize-2))
37        if self.knn_nslc > popsize - 2:
38            raise ValueError("knn_nslc (%d) should be at most popsize-2 (%d)" % (self.knn_nslc, popsize-2))
39
40
[1190]41    def transform_indexes(self, i, index_array):
42        return [x+1 if x >= i else x for x in index_array]
43
44    def normalize_dissim(self, dissim_matrix):
[1271]45        dissim_matrix = remove_diagonal(np.array(dissim_matrix)) # on the diagonal we usually have zeros (an individual is identical with itself, so the dissimilarity is 0). In some techniques we need to find "k" most similar individuals, so we remove the diagonal so that self-similarity of individuals does not interfere with finding "k" other most similar individuals. The matrix from square n*n turns into n*(n-1).
[1190]46        if self.normalize == "none":
47            return dissim_matrix
48        elif self.normalize == "max":
49            divide_by = np.max(dissim_matrix)
50        elif self.normalize == "sum":
51            divide_by = np.sum(dissim_matrix)
52        else:
[1289]53            raise ValueError("Wrong normalization method: '%s'" % self.normalize)
[1190]54        if divide_by != 0:
55            return dissim_matrix/divide_by
56        else:
57            return dissim_matrix
58
59    def do_niching(self, population_structures):
60        population_archive = population_structures.population + population_structures.archive
61        dissim_matrix = self.dissimilarity(population_archive)
62        if "knn" not in self.fit:
63            dissim_list = np.mean(self.normalize_dissim(dissim_matrix), axis=1)
64        else:
65            dissim_list = np.mean(np.partition(
66                self.normalize_dissim(dissim_matrix), self.knn_niching)[:, :self.knn_niching], axis=1)
67
[1289]68        if Individual.fitness_set_negative_to_zero is False and ("niching" in self.fit or "novelty" in self.fit):
69                raise ValueError("Negative fitness values not tested in combination with niching or novelty. When using these techniques, verify formulas or consider using the flag -fitness_set_negative_to_zero") # once the formulas are verified/improved, the command-line flag and this conditional check can be removed.
70
[1190]71        if "niching" in self.fit:
72            for i, d in zip(population_archive, dissim_list):
73                i.fitness = i.rawfitness * d
74        elif "novelty" in self.fit:
75            for i, d in zip(population_archive, dissim_list):
76                i.fitness = d
77        else:
[1289]78            raise ValueError("Unsupported fit type: '%s'. Use the correct type or implement a new behavior." % self.fit)
[1190]79        population_structures.update_archive(dissim_matrix, population_archive)
80
81    def do_nsga2_dissim(self, population):
82        dissim_matrix = self.dissimilarity(population)
83        dissim_list = np.mean(self.normalize_dissim(dissim_matrix), axis=1)
84        for i, d in zip(population, dissim_list):
85            i.fitness = DeapFitness(tuple((d, i.rawfitness)))
86
[1304]87    def do_nslc_dissim(self, population_structures, pop_offspring):
88        population_archive = population_structures.archive + pop_offspring
89        dissim_matrix = self.dissimilarity(population_archive)
[1190]90        normalized_matrix = self.normalize_dissim(dissim_matrix)
91        for i in range(len(normalized_matrix)):
92            temp_dissim = normalized_matrix[i]
93            index_array = np.argpartition(temp_dissim, kth=self.knn_nslc, axis=-1)[:self.knn_nslc]
94            dissim_value = np.mean(np.take_along_axis(
95                temp_dissim, index_array, axis=-1))
[1304]96            temp_fitness = population_archive[i].rawfitness
[1271]97            population_of_most_similar = list(
[1304]98                map(population_archive.__getitem__, self.transform_indexes(i, index_array)))
[1190]99            temp_ind_fit = sum(
[1271]100                [1 for ind in population_of_most_similar if ind.rawfitness < temp_fitness])
[1304]101            population_archive[i].fitness = DeapFitness(
[1190]102                tuple((dissim_value, temp_ind_fit)))
[1304]103        population_structures.update_archive(dissim_matrix, population_archive)
[1190]104
[1304]105    def make_new_population_nsga2(self, population_structures, prob_mut, prob_xov):
[1194]106        expected_mut = int(self.popsize * prob_mut)
107        expected_xov = int(self.popsize * prob_xov)
108        assert expected_mut + expected_xov <= self.popsize, "If probabilities of mutation (%g) and crossover (%g) added together exceed 1.0, then the population would grow every generation..." % (prob_mut, prob_xov)
[1304]109        assignCrowdingDist(population_structures.population)
110        offspring = tools.selTournamentDCD(population_structures.population, self.popsize)
[1190]111
112        def addGenotypeIfValid(ind_list, genotype):
113            new_individual = Individual()
114            new_individual.set_and_evaluate(genotype, self.evaluate)
[1194]115            if new_individual.fitness is not BAD_FITNESS:
[1190]116                ind_list.append(new_individual)
117
118        counter = 0
119
[1194]120        def get_individual(pop, c):
[1190]121            if c < len(pop):
122                ind = pop[c]
123                c += 1
124                return ind, c
125            else:
126                c = 0
127                ind = pop[c]
128                c += 1
129                return ind, c
130
131        newpop = []
132        while len(newpop) < expected_mut:
[1194]133            ind, counter = get_individual(offspring, counter)
[1190]134            addGenotypeIfValid(newpop, self.mutate(ind.genotype))
135
136        # adding valid crossovers of selected individuals...
137        while len(newpop) < expected_mut + expected_xov:
[1194]138            ind1, counter = get_individual(offspring, counter)
139            ind2, counter = get_individual(offspring, counter)
140            addGenotypeIfValid(newpop, self.cross_over(ind1.genotype, ind2.genotype))
[1190]141
142        # select clones to fill up the new population until we reach the same size as the input population
[1304]143        while len(newpop) < len(population_structures.population):
[1194]144            ind, counter = get_individual(offspring, counter)
[1190]145            newpop.append(Individual().copyFrom(ind))
146
[1304]147        pop_offspring = population_structures.population + newpop # used both for nsga2 and nslc
148        # print(len(pop_offspring)) # for debugging
149        if self.fit == "nslc":
150            self.do_nslc_dissim(population_structures, pop_offspring)
[1190]151        elif self.fit == "nsga2":
152            self.do_nsga2_dissim(pop_offspring)
[1304]153        out_pop = tools.selNSGA2(pop_offspring, len(population_structures.population))
[1190]154        return out_pop
155
156    def evolve(self, hof_savefile, generations, initialgenotype, pmut, pxov, tournament_size):
157        file_name = self.get_state_filename(hof_savefile)
158        state = self.load_state(file_name)
159        if state is not None:  # loaded state from file
160            # saved generation has been completed, start with the next one
161            self.current_generation += 1
[1272]162            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), 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
[1190]163        else:
164            self.initialize_evolution(self.genformat, initialgenotype)
165
166        time0 = time.process_time()
167        for g in range(self.current_generation, generations):
168            if self.fit != "raw" and self.fit != "nsga2" and self.fit != "nslc":
169                self.do_niching(self.population_structures)
170
171            if type(self.population_structures.population[0].fitness) == DeapFitness:
[1271]172                self.population_structures.population = self.make_new_population_nsga2(  # used both for nsga2 and nslc
[1304]173                    self.population_structures, pmut, pxov)
[1190]174            else:
175                self.population_structures.population = self.make_new_population(
176                    self.population_structures.population, pmut, pxov, tournament_size)
177
178            self.update_stats(g, self.population_structures.population)
179
180            if hof_savefile is not None:
181                self.current_generation = g
182                self.time_elapsed += time.process_time() - time0
183                self.save_state(file_name)
184        if hof_savefile is not None:
185            self.save_genotypes(hof_savefile)
186        return self.population_structures.population, self.stats
187
188    @staticmethod
189    def get_args_for_parser():
190        parser = ExperimentABC.get_args_for_parser()
[1271]191        parser.add_argument("-dissim",type= int, default=1,
192                   help="Dissimilarity measure type. Available: -3:freq, -2:dens, -1:Leven, 1:frams-struct (default}, 2:frams-descr")
193        parser.add_argument("-fit",type= str, default="raw",
194                        help="Fitness type, availible types: niching, novelty, knn_niching (local), knn_novelty (local), nsga2, nslc and raw (default)")
[1272]195        parser.add_argument("-archive",type= int, default=50, help="Maximum archive size")
[1190]196        parser.add_argument("-normalize",type= str, default= "max",
[1271]197                            help="What normalization to use for the dissimilarity matrix: max (default}, sum, or none")
198        parser.add_argument("-knn_niching",type= int, default=5,
199                        help="The number of nearest neighbors for local novelty/niching. If knn==0, global is performed. Default: 5")
200        parser.add_argument("-knn_nslc",type= int, default=5,
201                        help="The number of nearest neighbors for NSLC. If knn==0, global is performed. Default: 5")
[1190]202        return parser
203       
204    @abstractmethod
205    def dissimilarity(self, population: list):
206        pass
Note: See TracBrowser for help on using the repository browser.