1 | import time |
---|
2 | from abc import ABC, abstractmethod |
---|
3 | |
---|
4 | import numpy as np |
---|
5 | from deap import base, tools |
---|
6 | from deap.tools.emo import assignCrowdingDist |
---|
7 | |
---|
8 | from ..constants import BAD_FITNESS |
---|
9 | from ..structures.individual import Individual |
---|
10 | from .experiment_abc import ExperimentABC |
---|
11 | from .remove_diagonal import remove_diagonal |
---|
12 | |
---|
13 | |
---|
14 | class DeapFitness(base.Fitness): |
---|
15 | weights = (1, 1) |
---|
16 | |
---|
17 | def __init__(self, *args, **kwargs): |
---|
18 | super(DeapFitness, self).__init__(*args, **kwargs) |
---|
19 | |
---|
20 | |
---|
21 | class ExperimentNiching(ExperimentABC, ABC): |
---|
22 | fit: str = "niching" |
---|
23 | normalize: str = "None" |
---|
24 | archive_size: int = None |
---|
25 | |
---|
26 | def __init__(self, fit, normalize, popsize, hof_size, save_only_best=True, knn_niching=5, knn_nslc=10, archive_size=0) -> None: |
---|
27 | ExperimentABC.__init__(self,popsize=popsize, hof_size=hof_size, save_only_best=save_only_best) |
---|
28 | self.fit = fit |
---|
29 | self.normalize = normalize |
---|
30 | self.knn_niching = knn_niching # this parameter is used for local novelty and local niching |
---|
31 | self.knn_nslc = knn_nslc |
---|
32 | self.archive_size=archive_size |
---|
33 | |
---|
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 | |
---|
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): |
---|
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). |
---|
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: |
---|
53 | raise Exception(f"Wrong normalization method,", self.normalize) |
---|
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 | |
---|
68 | if "niching" in self.fit: |
---|
69 | for i, d in zip(population_archive, dissim_list): |
---|
70 | i.fitness = i.rawfitness * d |
---|
71 | elif "novelty" in self.fit: |
---|
72 | for i, d in zip(population_archive, dissim_list): |
---|
73 | i.fitness = d |
---|
74 | else: |
---|
75 | raise Exception("Wrong fit type: ", self.fit, |
---|
76 | f" choose the correct one or implement a new behavior.") |
---|
77 | population_structures.update_archive(dissim_matrix, population_archive) |
---|
78 | |
---|
79 | def do_nsga2_dissim(self, population): |
---|
80 | dissim_matrix = self.dissimilarity(population) |
---|
81 | dissim_list = np.mean(self.normalize_dissim(dissim_matrix), axis=1) |
---|
82 | for i, d in zip(population, dissim_list): |
---|
83 | i.fitness = DeapFitness(tuple((d, i.rawfitness))) |
---|
84 | |
---|
85 | def do_nslc_dissim(self, population): |
---|
86 | dissim_matrix = self.dissimilarity(population) |
---|
87 | normalized_matrix = self.normalize_dissim(dissim_matrix) |
---|
88 | for i in range(len(normalized_matrix)): |
---|
89 | temp_dissim = normalized_matrix[i] |
---|
90 | index_array = np.argpartition(temp_dissim, kth=self.knn_nslc, axis=-1)[:self.knn_nslc] |
---|
91 | dissim_value = np.mean(np.take_along_axis( |
---|
92 | temp_dissim, index_array, axis=-1)) |
---|
93 | temp_fitness = population[i].rawfitness |
---|
94 | population_of_most_similar = list( |
---|
95 | map(population.__getitem__, self.transform_indexes(i, index_array))) |
---|
96 | temp_ind_fit = sum( |
---|
97 | [1 for ind in population_of_most_similar if ind.rawfitness < temp_fitness]) |
---|
98 | population[i].fitness = DeapFitness( |
---|
99 | tuple((dissim_value, temp_ind_fit))) |
---|
100 | |
---|
101 | def make_new_population_nsga2(self, population, prob_mut, prob_xov): |
---|
102 | expected_mut = int(self.popsize * prob_mut) |
---|
103 | expected_xov = int(self.popsize * prob_xov) |
---|
104 | 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) |
---|
105 | assignCrowdingDist(population) |
---|
106 | offspring = tools.selTournamentDCD(population, self.popsize) |
---|
107 | |
---|
108 | def addGenotypeIfValid(ind_list, genotype): |
---|
109 | new_individual = Individual() |
---|
110 | new_individual.set_and_evaluate(genotype, self.evaluate) |
---|
111 | if new_individual.fitness is not BAD_FITNESS: |
---|
112 | ind_list.append(new_individual) |
---|
113 | |
---|
114 | counter = 0 |
---|
115 | |
---|
116 | def get_individual(pop, c): |
---|
117 | if c < len(pop): |
---|
118 | ind = pop[c] |
---|
119 | c += 1 |
---|
120 | return ind, c |
---|
121 | else: |
---|
122 | c = 0 |
---|
123 | ind = pop[c] |
---|
124 | c += 1 |
---|
125 | return ind, c |
---|
126 | |
---|
127 | newpop = [] |
---|
128 | while len(newpop) < expected_mut: |
---|
129 | ind, counter = get_individual(offspring, counter) |
---|
130 | addGenotypeIfValid(newpop, self.mutate(ind.genotype)) |
---|
131 | |
---|
132 | # adding valid crossovers of selected individuals... |
---|
133 | while len(newpop) < expected_mut + expected_xov: |
---|
134 | ind1, counter = get_individual(offspring, counter) |
---|
135 | ind2, counter = get_individual(offspring, counter) |
---|
136 | addGenotypeIfValid(newpop, self.cross_over(ind1.genotype, ind2.genotype)) |
---|
137 | |
---|
138 | # select clones to fill up the new population until we reach the same size as the input population |
---|
139 | while len(newpop) < len(population): |
---|
140 | ind, counter = get_individual(offspring, counter) |
---|
141 | newpop.append(Individual().copyFrom(ind)) |
---|
142 | |
---|
143 | pop_offspring = population + newpop # this is OK for NSGA2, but TODO verify if this should also be used for NSLC? |
---|
144 | print(len(pop_offspring)) |
---|
145 | if self.fit == "nslc": # TODO should NSLC be also equipped with a novelty archive? (with an admittance threshold?) |
---|
146 | self.do_nslc_dissim(pop_offspring) |
---|
147 | elif self.fit == "nsga2": |
---|
148 | self.do_nsga2_dissim(pop_offspring) |
---|
149 | out_pop = tools.selNSGA2(pop_offspring, len(population)) |
---|
150 | return out_pop |
---|
151 | |
---|
152 | def evolve(self, hof_savefile, generations, initialgenotype, pmut, pxov, tournament_size): |
---|
153 | file_name = self.get_state_filename(hof_savefile) |
---|
154 | state = self.load_state(file_name) |
---|
155 | if state is not None: # loaded state from file |
---|
156 | # saved generation has been completed, start with the next one |
---|
157 | self.current_generation += 1 |
---|
158 | 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 |
---|
159 | else: |
---|
160 | self.initialize_evolution(self.genformat, initialgenotype) |
---|
161 | |
---|
162 | time0 = time.process_time() |
---|
163 | for g in range(self.current_generation, generations): |
---|
164 | if self.fit != "raw" and self.fit != "nsga2" and self.fit != "nslc": |
---|
165 | self.do_niching(self.population_structures) |
---|
166 | |
---|
167 | if type(self.population_structures.population[0].fitness) == DeapFitness: |
---|
168 | self.population_structures.population = self.make_new_population_nsga2( # used both for nsga2 and nslc |
---|
169 | self.population_structures.population, pmut, pxov) |
---|
170 | else: |
---|
171 | self.population_structures.population = self.make_new_population( |
---|
172 | self.population_structures.population, pmut, pxov, tournament_size) |
---|
173 | |
---|
174 | self.update_stats(g, self.population_structures.population) |
---|
175 | |
---|
176 | if hof_savefile is not None: |
---|
177 | self.current_generation = g |
---|
178 | self.time_elapsed += time.process_time() - time0 |
---|
179 | self.save_state(file_name) |
---|
180 | if hof_savefile is not None: |
---|
181 | self.save_genotypes(hof_savefile) |
---|
182 | return self.population_structures.population, self.stats |
---|
183 | |
---|
184 | @staticmethod |
---|
185 | def get_args_for_parser(): |
---|
186 | parser = ExperimentABC.get_args_for_parser() |
---|
187 | parser.add_argument("-dissim",type= int, default=1, |
---|
188 | help="Dissimilarity measure type. Available: -3:freq, -2:dens, -1:Leven, 1:frams-struct (default}, 2:frams-descr") |
---|
189 | parser.add_argument("-fit",type= str, default="raw", |
---|
190 | help="Fitness type, availible types: niching, novelty, knn_niching (local), knn_novelty (local), nsga2, nslc and raw (default)") |
---|
191 | parser.add_argument("-archive",type= int, default=50, help="Maximum archive size") |
---|
192 | parser.add_argument("-normalize",type= str, default= "max", |
---|
193 | help="What normalization to use for the dissimilarity matrix: max (default}, sum, or none") |
---|
194 | parser.add_argument("-knn_niching",type= int, default=5, |
---|
195 | help="The number of nearest neighbors for local novelty/niching. If knn==0, global is performed. Default: 5") |
---|
196 | parser.add_argument("-knn_nslc",type= int, default=5, |
---|
197 | help="The number of nearest neighbors for NSLC. If knn==0, global is performed. Default: 5") |
---|
198 | return parser |
---|
199 | |
---|
200 | @abstractmethod |
---|
201 | def dissimilarity(self, population: list): |
---|
202 | pass |
---|