|
1 | 1 | import numpy
|
| 2 | +import random |
| 3 | +import sys |
| 4 | +import matplotlib.pyplot |
2 | 5 |
|
3 |
| -def cal_pop_fitness(equation_inputs, pop): |
4 |
| - # Calculating the fitness value of each solution in the current population. |
5 |
| - # The fitness function calulates the sum of products between each input and its corresponding weight. |
6 |
| - fitness = numpy.sum(pop*equation_inputs, axis=1) |
7 |
| - return fitness |
8 |
| - |
9 |
| -def select_mating_pool(pop, fitness, num_parents): |
10 |
| - # Selecting the best individuals in the current generation as parents for producing the offspring of the next generation. |
11 |
| - parents = numpy.empty((num_parents, pop.shape[1])) |
12 |
| - for parent_num in range(num_parents): |
13 |
| - max_fitness_idx = numpy.where(fitness == numpy.max(fitness)) |
14 |
| - max_fitness_idx = max_fitness_idx[0][0] |
15 |
| - parents[parent_num, :] = pop[max_fitness_idx, :] |
16 |
| - fitness[max_fitness_idx] = -99999999999 |
17 |
| - return parents |
18 |
| - |
19 |
| -def crossover(parents, offspring_size): |
20 |
| - offspring = numpy.empty(offspring_size) |
21 |
| - # The point at which crossover takes place between two parents. Usually, it is at the center. |
22 |
| - crossover_point = numpy.uint8(offspring_size[1]/2) |
23 |
| - |
24 |
| - for k in range(offspring_size[0]): |
25 |
| - # Index of the first parent to mate. |
26 |
| - parent1_idx = k%parents.shape[0] |
27 |
| - # Index of the second parent to mate. |
28 |
| - parent2_idx = (k+1)%parents.shape[0] |
29 |
| - # The new offspring will have its first half of its genes taken from the first parent. |
30 |
| - offspring[k, 0:crossover_point] = parents[parent1_idx, 0:crossover_point] |
31 |
| - # The new offspring will have its second half of its genes taken from the second parent. |
32 |
| - offspring[k, crossover_point:] = parents[parent2_idx, crossover_point:] |
33 |
| - return offspring |
34 |
| - |
35 |
| -def mutation(offspring_crossover, num_mutations=1): |
36 |
| - mutations_counter = numpy.uint8(offspring_crossover.shape[1] / num_mutations) |
37 |
| - # Mutation changes a number of genes as defined by the num_mutations argument. The changes are random. |
38 |
| - for idx in range(offspring_crossover.shape[0]): |
39 |
| - gene_idx = mutations_counter - 1 |
40 |
| - for mutation_num in range(num_mutations): |
| 6 | +class GA: |
| 7 | + # Parameters of the genetic algorithm. |
| 8 | + sol_per_pop = None # Number of solutions in the population. |
| 9 | + num_parents_mating = None # Number of solutions to be selected as parents in the mating pool. |
| 10 | + num_generations = None # Number of generations. |
| 11 | + pop_size = None # Population size = (number of chromosomes, number of genes per chromosome) |
| 12 | + |
| 13 | + population = None # A NumPy array holding the opulation. |
| 14 | + |
| 15 | + # NumPy arrays holding information about the generations. |
| 16 | + best_outputs = [] # A list holding the value of the best solution for each generation. |
| 17 | + best_outputs_fitness = [] # A list holding the fitness value of the best solution for each generation. |
| 18 | + |
| 19 | + # Parameters of the function to be optimized. |
| 20 | + function_inputs = None # Inputs of the function to be optimized. |
| 21 | + function_output = None # Desired outuput of the function. |
| 22 | + num_weights = None |
| 23 | + |
| 24 | + # Mutation parameters. |
| 25 | + mutation_percent_genes=None |
| 26 | + mutation_num_genes=None |
| 27 | + mutation_min_val=None |
| 28 | + mutation_max_val=None |
| 29 | + |
| 30 | + def __init__(self, num_generations, |
| 31 | + sol_per_pop, |
| 32 | + num_parents_mating, |
| 33 | + function_inputs, |
| 34 | + function_output, |
| 35 | + mutation_percent_genes=10, |
| 36 | + mutation_num_genes=None, |
| 37 | + mutation_min_val=-1.0, |
| 38 | + mutation_max_val=1.0): |
| 39 | + |
| 40 | + # Parameters of the genetic algorithm. |
| 41 | + self.sol_per_pop = sol_per_pop |
| 42 | + self.num_parents_mating = num_parents_mating |
| 43 | + self.num_generations = num_generations |
| 44 | + |
| 45 | + # Properties of the function to be optimized. |
| 46 | + self.function_inputs = function_inputs # Funtion inputs. |
| 47 | + self.function_output = function_output # Function output. |
| 48 | + self.num_weights = len(self.function_inputs) # Number of parameters in the function. |
| 49 | + |
| 50 | + # Parameters of the mutation operation. |
| 51 | + self.mutation_percent_genes = mutation_percent_genes |
| 52 | + self.mutation_num_genes = mutation_num_genes |
| 53 | + self.mutation_min_val = mutation_min_val |
| 54 | + self.mutation_max_val = mutation_max_val |
| 55 | + |
| 56 | + # Initializing the population. |
| 57 | + self.initialize_population() |
| 58 | + |
| 59 | + def initialize_population(self): |
| 60 | + self.pop_size = (self.sol_per_pop,self.num_weights) # The population will have sol_per_pop chromosome where each chromosome has num_weights genes. |
| 61 | + # Creating the initial population randomly. |
| 62 | + self.population = numpy.random.uniform(low=-4.0, high=4.0, size=self.pop_size) |
| 63 | + |
| 64 | + def train(self): |
| 65 | + for generation in range(self.num_generations): |
| 66 | + # Measuring the fitness of each chromosome in the population. |
| 67 | + fitness = self.cal_pop_fitness() |
| 68 | + |
| 69 | + # Selecting the best parents in the population for mating. |
| 70 | + parents = self.select_mating_pool(fitness) |
| 71 | + |
| 72 | + # Generating next generation using crossover. |
| 73 | + offspring_crossover = self.crossover(parents, |
| 74 | + offspring_size=(self.pop_size[0]-parents.shape[0], self.num_weights)) |
| 75 | + |
| 76 | + # Adding some variations to the offspring using mutation. |
| 77 | + offspring_mutation = self.mutation(offspring_crossover) |
| 78 | + |
| 79 | + if (len(offspring_mutation) == 2): |
| 80 | + print(offspring_mutation[1]) |
| 81 | + sys.exit(1) |
| 82 | + |
| 83 | + # Creating the new population based on the parents and offspring. |
| 84 | + self.population[0:parents.shape[0], :] = parents |
| 85 | + self.population[parents.shape[0]:, :] = offspring_mutation |
| 86 | + |
| 87 | + def cal_pop_fitness(self): |
| 88 | + # Calculating the fitness value of each solution in the current population. |
| 89 | + # The fitness function calulates the sum of products between each input and its corresponding weight. |
| 90 | + outputs = numpy.sum(self.population*self.function_inputs, axis=1) |
| 91 | + fitness = 1.0 / numpy.abs(outputs - self.function_output) |
| 92 | + |
| 93 | + # The best result in the current iteration. |
| 94 | + self.best_outputs.append(outputs[numpy.where(fitness == numpy.max(fitness))[0][0]]) |
| 95 | + self.best_outputs_fitness.append(numpy.max(fitness)) |
| 96 | + |
| 97 | + return fitness |
| 98 | + |
| 99 | + def select_mating_pool(self, fitness): |
| 100 | + # Selecting the best individuals in the current generation as parents for producing the offspring of the next generation. |
| 101 | + parents = numpy.empty((self.num_parents_mating, self.population.shape[1])) |
| 102 | + for parent_num in range(self.num_parents_mating): |
| 103 | + max_fitness_idx = numpy.where(fitness == numpy.max(fitness)) |
| 104 | + max_fitness_idx = max_fitness_idx[0][0] |
| 105 | + parents[parent_num, :] = self.population[max_fitness_idx, :] |
| 106 | + fitness[max_fitness_idx] = -99999999999 |
| 107 | + return parents |
| 108 | + |
| 109 | + def crossover(self, parents, offspring_size): |
| 110 | + offspring = numpy.empty(offspring_size) |
| 111 | + # The point at which crossover takes place between two parents. Usually, it is at the center. |
| 112 | + crossover_point = numpy.uint8(offspring_size[1]/2) |
| 113 | + |
| 114 | + for k in range(offspring_size[0]): |
| 115 | + # Index of the first parent to mate. |
| 116 | + parent1_idx = k%parents.shape[0] |
| 117 | + # Index of the second parent to mate. |
| 118 | + parent2_idx = (k+1)%parents.shape[0] |
| 119 | + # The new offspring will have its first half of its genes taken from the first parent. |
| 120 | + offspring[k, 0:crossover_point] = parents[parent1_idx, 0:crossover_point] |
| 121 | + # The new offspring will have its second half of its genes taken from the second parent. |
| 122 | + offspring[k, crossover_point:] = parents[parent2_idx, crossover_point:] |
| 123 | + return offspring |
| 124 | + |
| 125 | + def mutation(self, offspring): |
| 126 | + """ |
| 127 | + The mutation() method applies mutation over the offspring. |
| 128 | + The parameters are: |
| 129 | + -offspring: The offspring to which mutation is applied. |
| 130 | + -percent_genes: Defaults to 10 and refers to the percentage of genes to which mutation is applied. Based on the percentage, the number of genes are calculated. |
| 131 | + -num_genes: The number of genes to which mutaiton is applied. If None, then the number of genes is calculated based on the argument percent_genes. If not None, then it overrides the value of the argument percent_genes. |
| 132 | + -mutation_min_val & mutation_max_val: The range from which random values are selected and added to the selected genes. |
| 133 | + """ |
| 134 | + if self.mutation_num_genes == None: |
| 135 | + if self.mutation_percent_genes <= 0: |
| 136 | + return None, "ERROR: The percentage of genes for which mutation is applied must be > 0. Please specify a valid value for the percent_genes argument." |
| 137 | + self.mutation_num_genes = numpy.uint32((self.mutation_percent_genes*offspring.shape[1])/100) |
| 138 | + # Based on the percentage of genes, if the number of selected genes for mutation is less than the least possible value which is 1, then the number will be set to 1. |
| 139 | + if self.mutation_num_genes == 0: |
| 140 | + self.mutation_num_genes = 1 |
| 141 | + if self.mutation_num_genes <= 0: |
| 142 | + return None, "ERROR: Number of genes for mutation must be > 0. Please specify a valid value for the mutation_num_genes argument." |
| 143 | + else: |
| 144 | + self.mutation_num_genes = int(self.mutation_num_genes) |
| 145 | + mutation_indices = numpy.array(random.sample(range(0, offspring.shape[1]), self.mutation_num_genes)) |
| 146 | + # Mutation changes a single gene in each offspring randomly. |
| 147 | + for idx in range(offspring.shape[0]): |
41 | 148 | # The random value to be added to the gene.
|
42 |
| - random_value = numpy.random.uniform(-1.0, 1.0, 1) |
43 |
| - offspring_crossover[idx, gene_idx] = offspring_crossover[idx, gene_idx] + random_value |
44 |
| - gene_idx = gene_idx + mutations_counter |
45 |
| - return offspring_crossover |
| 149 | + random_value = numpy.random.uniform(self.mutation_min_val, self.mutation_max_val, 1) |
| 150 | + offspring[idx, mutation_indices] = offspring[idx, mutation_indices] + random_value |
| 151 | + return offspring |
| 152 | + |
| 153 | + def best_solution(self): |
| 154 | + # Getting the best solution after finishing all generations. |
| 155 | + # At first, the fitness is calculated for each solution in the final generation. |
| 156 | + fitness = self.cal_pop_fitness() |
| 157 | + # Then return the index of that solution corresponding to the best fitness. |
| 158 | + best_match_idx = numpy.where(fitness == numpy.max(fitness)) |
| 159 | + |
| 160 | + best_solution = self.population[best_match_idx, :][0][0] |
| 161 | + best_solution_fitness = fitness[best_match_idx][0] |
| 162 | + |
| 163 | + return best_solution, best_solution_fitness |
| 164 | + |
| 165 | + def plot_result(self): |
| 166 | + matplotlib.pyplot.figure() |
| 167 | + matplotlib.pyplot.plot(self.best_outputs) |
| 168 | + matplotlib.pyplot.xlabel("Iteration") |
| 169 | + matplotlib.pyplot.ylabel("Outputs") |
| 170 | + matplotlib.pyplot.show() |
| 171 | + |
| 172 | + matplotlib.pyplot.figure() |
| 173 | + matplotlib.pyplot.plot(self.best_outputs_fitness) |
| 174 | + matplotlib.pyplot.xlabel("Iteration") |
| 175 | + matplotlib.pyplot.ylabel("Fitness") |
| 176 | + matplotlib.pyplot.show() |
0 commit comments