|
| 1 | +import numpy |
| 2 | +cimport numpy |
| 3 | +import time |
| 4 | +import cython |
| 5 | +from libc.stdlib cimport rand, RAND_MAX |
| 6 | + |
| 7 | +cdef double DOUBLE_RAND_MAX = RAND_MAX # a double variable holding the maximum random integer in C |
| 8 | + |
| 9 | +@cython.wraparound(False) |
| 10 | +@cython.cdivision(True) |
| 11 | +@cython.nonecheck(False) |
| 12 | +@cython.boundscheck(False) |
| 13 | +cpdef cal_pop_fitness(numpy.ndarray[numpy.double_t, ndim=1] equation_inputs, numpy.ndarray[numpy.double_t, ndim=2] pop): |
| 14 | + # Calculating the fitness value of each solution in the current population. |
| 15 | + # The fitness function calulates the sum of products between each input and its corresponding weight. |
| 16 | + cdef numpy.ndarray[numpy.double_t, ndim=1] fitness |
| 17 | + fitness = numpy.zeros(pop.shape[0]) |
| 18 | + # fitness = numpy.sum(pop*equation_inputs, axis=1) # slower than looping. |
| 19 | + for i in range(pop.shape[0]): |
| 20 | + for j in range(pop.shape[1]): |
| 21 | + fitness[i] += pop[i, j]*equation_inputs[j] |
| 22 | + return fitness |
| 23 | + |
| 24 | +@cython.wraparound(False) |
| 25 | +@cython.cdivision(True) |
| 26 | +@cython.nonecheck(False) |
| 27 | +@cython.boundscheck(False) |
| 28 | +cpdef numpy.ndarray[numpy.double_t, ndim=2] select_mating_pool(numpy.ndarray[numpy.double_t, ndim=2] pop, numpy.ndarray[numpy.double_t, ndim=1] fitness, int num_parents): |
| 29 | + # Selecting the best individuals in the current generation as parents for producing the offspring of the next generation. |
| 30 | + cdef numpy.ndarray[numpy.double_t, ndim=2] parents |
| 31 | + cdef int parent_num, max_fitness_idx, min_val, max_fitness, a |
| 32 | + |
| 33 | + min_val = -99999999999 |
| 34 | + |
| 35 | + parents = numpy.empty((num_parents, pop.shape[1])) |
| 36 | + for parent_num in range(num_parents): |
| 37 | + max_fitness_idx = 0 |
| 38 | + # numpy.where(fitness == numpy.max(fitness)) # slower than argmax() by 150 ms. |
| 39 | + # max_fitness_idx = numpy.argmax(fitness) # slower than looping by 100 ms. |
| 40 | + for a in range(1, fitness.shape[0]): |
| 41 | + if fitness[a] > fitness[max_fitness_idx]: |
| 42 | + max_fitness_idx = a |
| 43 | + |
| 44 | + # parents[parent_num, :] = pop[max_fitness_idx, :] # slower han looping by 20 ms |
| 45 | + for a in range(parents.shape[1]): |
| 46 | + parents[parent_num, a] = pop[max_fitness_idx, a] |
| 47 | + fitness[max_fitness_idx] = min_val |
| 48 | + return parents |
| 49 | + |
| 50 | +@cython.wraparound(True) |
| 51 | +@cython.cdivision(True) |
| 52 | +@cython.nonecheck(False) |
| 53 | +@cython.boundscheck(False) |
| 54 | +cpdef numpy.ndarray[numpy.double_t, ndim=2] crossover(numpy.ndarray[numpy.double_t, ndim=2] parents, tuple offspring_size): |
| 55 | + cdef numpy.ndarray[numpy.double_t, ndim=2] offspring |
| 56 | + offspring = numpy.empty(offspring_size) |
| 57 | + # The point at which crossover takes place between two parents. Usually, it is at the center. |
| 58 | + cdef int k, parent1_idx, parent2_idx |
| 59 | + cdef numpy.int_t crossover_point |
| 60 | + crossover_point = (int) (offspring_size[1]/2) |
| 61 | + |
| 62 | + for k in range(offspring_size[0]): |
| 63 | + # Index of the first parent to mate. |
| 64 | + parent1_idx = k%parents.shape[0] |
| 65 | + # Index of the second parent to mate. |
| 66 | + parent2_idx = (k+1)%parents.shape[0] |
| 67 | + |
| 68 | + for m in range(crossover_point): |
| 69 | + # The new offspring will have its first half of its genes taken from the first parent. |
| 70 | + offspring[k, m] = parents[parent1_idx, m] |
| 71 | + for m in range(crossover_point-1, -1): |
| 72 | + # The new offspring will have its second half of its genes taken from the second parent. |
| 73 | + offspring[k, m] = parents[parent2_idx, m] |
| 74 | + |
| 75 | + # The next 2 lines are slower than using the above loops because they run with C speed. |
| 76 | + # offspring[k, 0:crossover_point] = parents[parent1_idx, 0:crossover_point] |
| 77 | + # offspring[k, crossover_point:] = parents[parent2_idx, crossover_point:] |
| 78 | + return offspring |
| 79 | + |
| 80 | +@cython.wraparound(False) |
| 81 | +@cython.cdivision(True) |
| 82 | +@cython.nonecheck(False) |
| 83 | +@cython.boundscheck(False) |
| 84 | +cpdef numpy.ndarray[numpy.double_t, ndim=2] mutation(numpy.ndarray[numpy.double_t, ndim=2] offspring_crossover, int num_mutations=1): |
| 85 | + cdef int idx, mutation_num, gene_idx |
| 86 | + cdef double random_value |
| 87 | + cdef Py_ssize_t mutations_counter |
| 88 | + mutations_counter = (int) (offspring_crossover.shape[1] / num_mutations) # using numpy.uint8() is slower than using (int) |
| 89 | + # Mutation changes a number of genes as defined by the num_mutations argument. The changes are random. |
| 90 | + cdef double rand_num |
| 91 | + for idx in range(offspring_crossover.shape[0]): |
| 92 | + gene_idx = mutations_counter - 1 |
| 93 | + for mutation_num in range(num_mutations): |
| 94 | + # The random value to be added to the gene. |
| 95 | + # random_value = numpy.random.uniform(-1.0, 1.0, 1) # Slower by 300 ms compared to native C rand() function. |
| 96 | + rand_double = rand()/DOUBLE_RAND_MAX |
| 97 | + random_value = rand_double * (1.0 - (-1.0)) + (-1.0); # The new range is from 1.0 to -1.0. |
| 98 | + offspring_crossover[idx, gene_idx] = offspring_crossover[idx, gene_idx] + random_value |
| 99 | + gene_idx = gene_idx + mutations_counter |
| 100 | + return offspring_crossover |
| 101 | + |
| 102 | +@cython.wraparound(False) |
| 103 | +@cython.cdivision(True) |
| 104 | +@cython.nonecheck(False) |
| 105 | +@cython.boundscheck(False) |
| 106 | +cpdef optimize(): |
| 107 | + # Inputs of the equation. |
| 108 | + cdef numpy.ndarray equation_inputs, parents, new_population, fitness, offspring_crossover, offspring_mutation |
| 109 | + cdef int num_weights, sol_per_pop, num_parents_mating, num_generations |
| 110 | + cdef list pop_size |
| 111 | + cdef double t1, t2, t |
| 112 | + |
| 113 | + equation_inputs = numpy.array([4,-2,3.5,5,-11,-4.7]) |
| 114 | + num_weights = equation_inputs.shape[0] |
| 115 | + |
| 116 | + # Number of the weights we are looking to optimize. |
| 117 | + sol_per_pop = 8 |
| 118 | + num_weights = equation_inputs.shape[0] |
| 119 | + num_parents_mating = 4 |
| 120 | + |
| 121 | + # Defining the population size. |
| 122 | + pop_size = [sol_per_pop,num_weights] # The population will have sol_per_pop chromosome where each chromosome has num_weights genes. |
| 123 | + #Creating the initial population. |
| 124 | + new_population = numpy.random.uniform(low=-4.0, high=4.0, size=pop_size) |
| 125 | + |
| 126 | + num_generations = 1000000 |
| 127 | + t1 = time.time() |
| 128 | + for generation in range(num_generations): |
| 129 | + # Measuring the fitness of each chromosome in the population. |
| 130 | + fitness = cal_pop_fitness(equation_inputs, new_population) |
| 131 | + |
| 132 | + # Selecting the best parents in the population for mating. |
| 133 | + parents = select_mating_pool(new_population, fitness, |
| 134 | + num_parents_mating) |
| 135 | + |
| 136 | + # Generating next generation using crossover. |
| 137 | + offspring_crossover = crossover(parents, |
| 138 | + offspring_size=(pop_size[0]-parents.shape[0], num_weights)) |
| 139 | + |
| 140 | + # Adding some variations to the offspring using mutation. |
| 141 | + offspring_mutation = mutation(offspring_crossover, num_mutations=2) |
| 142 | + |
| 143 | + # Creating the new population based on the parents and offspring. |
| 144 | + new_population[0:parents.shape[0], :] = parents |
| 145 | + new_population[parents.shape[0]:, :] = offspring_mutation |
| 146 | + t2 = time.time() |
| 147 | + t = t2-t1 |
| 148 | + print("Total Time %.20f" % t) |
| 149 | + print(cal_pop_fitness(equation_inputs, new_population)) |
0 commit comments