Example #1
0
    def next_generation(self):
        self._increment_generation()
        self._current_individual = 0

        # Calculate fitness of individuals
        for individual in self.population.individuals:
            individual.calculate_fitness()
        
        self.population.individuals = elitism_selection(self.population, self.settings['num_parents'])
        
        random.shuffle(self.population.individuals)
        next_pop: List[Snake] = []

        # parents + offspring selection type ('plus')
        if self.settings['selection_type'].lower() == 'plus':
            # Decrement lifespan
            for individual in self.population.individuals:
                individual.lifespan -= 1

            for individual in self.population.individuals:
                params = individual.network.params
                board_size = individual.board_size
                hidden_layer_architecture = individual.hidden_layer_architecture
                hidden_activation = individual.hidden_activation
                output_activation = individual.output_activation
                lifespan = individual.lifespan
                apple_and_self_vision = individual.apple_and_self_vision

                start_pos = individual.start_pos
                apple_seed = individual.apple_seed
                starting_direction = individual.starting_direction

                # If the individual is still alive, they survive
                if lifespan > 0:
                    s = Snake(board_size, chromosome=params, hidden_layer_architecture=hidden_layer_architecture,
                            hidden_activation=hidden_activation, output_activation=output_activation,
                            lifespan=lifespan, apple_and_self_vision=apple_and_self_vision)#,
                    next_pop.append(s)


        while len(next_pop) < self._next_gen_size:
            p1, p2 = roulette_wheel_selection(self.population, 2)

            L = len(p1.network.layer_nodes)
            c1_params = {}
            c2_params = {}

            # Each W_l and b_l are treated as their own chromosome.
            # Because of this I need to perform crossover/mutation on each chromosome between parents
            for l in range(1, L):
                p1_W_l = p1.network.params['W' + str(l)]
                p2_W_l = p2.network.params['W' + str(l)]  
                p1_b_l = p1.network.params['b' + str(l)]
                p2_b_l = p2.network.params['b' + str(l)]

                # Crossover
                # @NOTE: I am choosing to perform the same type of crossover on the weights and the bias.
                c1_W_l, c2_W_l, c1_b_l, c2_b_l = self._crossover(p1_W_l, p2_W_l, p1_b_l, p2_b_l)

                # Mutation
                # @NOTE: I am choosing to perform the same type of mutation on the weights and the bias.
                self._mutation(c1_W_l, c2_W_l, c1_b_l, c2_b_l)

                # Assign children from crossover/mutation
                c1_params['W' + str(l)] = c1_W_l
                c2_params['W' + str(l)] = c2_W_l
                c1_params['b' + str(l)] = c1_b_l
                c2_params['b' + str(l)] = c2_b_l

                # Clip to [-1, 1]
                np.clip(c1_params['W' + str(l)], -1, 1, out=c1_params['W' + str(l)])
                np.clip(c2_params['W' + str(l)], -1, 1, out=c2_params['W' + str(l)])
                np.clip(c1_params['b' + str(l)], -1, 1, out=c1_params['b' + str(l)])
                np.clip(c2_params['b' + str(l)], -1, 1, out=c2_params['b' + str(l)])

            # Create children from chromosomes generated above
            c1 = Snake(p1.board_size, chromosome=c1_params, hidden_layer_architecture=p1.hidden_layer_architecture,
                       hidden_activation=p1.hidden_activation, output_activation=p1.output_activation,
                       lifespan=self.settings['lifespan'])
            c2 = Snake(p2.board_size, chromosome=c2_params, hidden_layer_architecture=p2.hidden_layer_architecture,
                       hidden_activation=p2.hidden_activation, output_activation=p2.output_activation,
                       lifespan=self.settings['lifespan'])

            # Add children to the next generation
            next_pop.extend([c1, c2])
        
        # Set the next generation
        random.shuffle(next_pop)
        self.population.individuals = next_pop
Example #2
0
    def next_generation(self) -> None:
        if self.state == States.NEXT_GEN:
            self.stats_window.pop_size.setText(str(self._next_gen_size))
            self.current_batch = 0
            # Set next state to copy parents if its plus, otherwise comma is just going to create offspring
            if get_ga_constant('selection_type').lower() == 'plus':
                self.state = States.NEXT_GEN_COPY_PARENTS_OVER
            elif get_ga_constant('selection_type').lower() == 'comma':
                self.state = States.NEXT_GEN_CREATE_OFFSPRING
            else:
                raise Exception('Invalid selection_type: "{}"'.format(
                    get_ga_constant('selection_type')))

            self._offset_into_population = 0
            self._total_individuals_ran = 0  # Reset back to the first individual

            self.population.individuals = self._next_pop
            self._next_pop = []  # Reset the next pop

            # Calculate fit
            for individual in self.population.individuals:
                individual.calculate_fitness()

            # Should we save the pop
            if args.save_pop:
                path = os.path.join(
                    args.save_pop, 'pop_gen{}'.format(self.current_generation))
                if os.path.exists(path):
                    raise Exception(
                        '{} already exists. This would overwrite everything, choose a different folder or delete it and try again'
                        .format(path))
                os.makedirs(path)
                save_population(path, self.population, settings.settings)
            # Save best?
            if args.save_best:
                save_car(args.save_best,
                         'car_{}'.format(self.current_generation),
                         self.population.fittest_individual, settings.settings)

            self._set_previous_gen_avg_fitness()
            self._set_previous_gen_num_winners()
            self._increment_generation()

            # Grab the best individual and compare to best fitness
            best_ind = self.population.fittest_individual
            if best_ind.fitness > self.max_fitness:
                self.max_fitness = best_ind.fitness
                self._set_max_fitness()
                self.gen_without_improvement = 0
            else:
                self.gen_without_improvement += 1
            # Set text for gen improvement
            self.stats_window.gens_without_improvement.setText(
                str(self.gen_without_improvement))

            # Set the population to be just the parents allowed for reproduction. Only really matters if `plus` method is used.
            # If `plus` method is used, there can be more individuals in the next generation, so this limits the number of parents.
            self.population.individuals = elitism_selection(
                self.population, get_ga_constant('num_parents'))

            random.shuffle(self.population.individuals)

            # Parents + offspring selection type ('plus')
            if get_ga_constant('selection_type').lower() == 'plus':
                # Decrement lifespan
                for individual in self.population.individuals:
                    individual.lifespan -= 1

        num_offspring = min(self._next_gen_size - len(self._next_pop),
                            get_boxcar_constant('run_at_a_time'))
        self.cars = self._create_num_offspring(num_offspring)
        # Set number of cars alive
        self.num_cars_alive = len(self.cars)
        self.batch_size = self.num_cars_alive
        self.current_batch += 1
        self._set_number_of_cars_alive()
        self._next_pop.extend(self.cars)  # Add to next_pop
        self.game_window.cars = self.cars
        leader = self.find_new_leader()
        self.leader = leader
        self.game_window.leader = leader
        if get_ga_constant('selection_type').lower() == 'comma':
            self.state = States.NEXT_GEN_CREATE_OFFSPRING
        elif get_ga_constant('selection_type').lower(
        ) == 'plus' and self._offset_into_population >= len(
                self.population.individuals):
            self.state = States.NEXT_GEN_CREATE_OFFSPRING
Example #3
0
    def next_generation(self):
        self._increment_generation()
        self._current_individual = 0

        self.population.individuals = elitism_selection(
            self.population, self.settings['num_parents'])

        random.shuffle(self.population.individuals)
        next_pop: List[Puzzle] = []

        # parents + offspring selection type ('plus')
        if self.settings['selection_type'].lower() == 'plus':

            for individual in self.population.individuals:
                params = individual.network.params
                board_size = individual.board_size
                hidden_layer_architecture = individual.hidden_layer_architecture
                hidden_activation = individual.hidden_activation
                output_activation = individual.output_activation

                # If the individual is still alive, they survive
                if individual._fitness < 1000:
                    nrG = random.randint(self.nrGroupRange[0],
                                         self.nrGroupRange[1])
                    nrC = random.randint(self.nrBlocksRange[0],
                                         self.nrBlocksRange[1])
                    s = Puzzle(
                        board_size,
                        nrG,
                        nrC,
                        chromosome=params,
                        hidden_layer_architecture=hidden_layer_architecture,
                        hidden_activation=hidden_activation,
                        output_activation=output_activation)
                    next_pop.append(s)

        while len(next_pop) < self._next_gen_size:
            p1, p2 = roulette_wheel_selection(self.population, 2)

            L = len(p1.network.layer_nodes)
            c1_params = {}
            c2_params = {}

            # Each W_l and b_l are treated as their own chromosome.
            # Because of this I need to perform crossover/mutation on each chromosome between parents
            for l in range(1, L):
                p1_W_l = p1.network.params['W' + str(l)]
                p2_W_l = p2.network.params['W' + str(l)]
                p1_b_l = p1.network.params['b' + str(l)]
                p2_b_l = p2.network.params['b' + str(l)]

                # Crossover
                # @NOTE: I am choosing to perform the same type of crossover on the weights and the bias.
                c1_W_l, c2_W_l, c1_b_l, c2_b_l = self._crossover(
                    p1_W_l, p2_W_l, p1_b_l, p2_b_l)

                # Mutation
                # @NOTE: I am choosing to perform the same type of mutation on the weights and the bias.
                self._mutation(c1_W_l, c2_W_l, c1_b_l, c2_b_l)

                # Assign children from crossover/mutation
                c1_params['W' + str(l)] = c1_W_l
                c2_params['W' + str(l)] = c2_W_l
                c1_params['b' + str(l)] = c1_b_l
                c2_params['b' + str(l)] = c2_b_l

                # Clip to [-1, 1]
                np.clip(c1_params['W' + str(l)],
                        -1,
                        1,
                        out=c1_params['W' + str(l)])
                np.clip(c2_params['W' + str(l)],
                        -1,
                        1,
                        out=c2_params['W' + str(l)])
                np.clip(c1_params['b' + str(l)],
                        -1,
                        1,
                        out=c1_params['b' + str(l)])
                np.clip(c2_params['b' + str(l)],
                        -1,
                        1,
                        out=c2_params['b' + str(l)])

            # Create children from chromosomes generated above
            nrG = random.randint(self.nrGroupRange[0], self.nrGroupRange[1])
            nrC = random.randint(self.nrBlocksRange[0], self.nrBlocksRange[1])
            c1 = Puzzle(p1.board_size,
                        nrG,
                        nrC,
                        chromosome=c1_params,
                        hidden_layer_architecture=p1.hidden_layer_architecture,
                        hidden_activation=p1.hidden_activation,
                        output_activation=p1.output_activation)

            nrG = random.randint(self.nrGroupRange[0], self.nrGroupRange[1])
            nrC = random.randint(self.nrBlocksRange[0], self.nrBlocksRange[1])
            c2 = Puzzle(p2.board_size,
                        nrG,
                        nrC,
                        chromosome=c2_params,
                        hidden_layer_architecture=p2.hidden_layer_architecture,
                        hidden_activation=p2.hidden_activation,
                        output_activation=p2.output_activation)

            # Add children to the next generation
            next_pop.extend([c1, c2])

        # Set the next generation
        random.shuffle(next_pop)
        self.population.individuals = next_pop
Example #4
0
    def next_generation(self) -> None:
        self._increment_generation()
        self._current_individual = 0

        if not args.no_display:
            self.info_window.current_individual.setText('{}/{}'.format(
                self._current_individual + 1, self._next_gen_size))

        # Calculate fitness
        # print(', '.join(['{:.2f}'.format(i.fitness) for i in self.population.individuals]))

        if args.debug:
            print(
                f'----Current Gen: {self.current_generation}, True Zero: {self._true_zero_gen}'
            )
            fittest = self.population.fittest_individual
            print(
                f'Best fitness of gen: {fittest.fitness}, Max dist of gen: {fittest.farthest_x}'
            )
            num_wins = sum(individual.did_win
                           for individual in self.population.individuals)
            pop_size = len(self.population.individuals)
            print(
                f'Wins: {num_wins}/{pop_size} (~{(float(num_wins)/pop_size*100):.2f}%)'
            )

        if self.config.Statistics.save_best_individual_from_generation:
            folder = self.config.Statistics.save_best_individual_from_generation
            best_ind_name = 'best_ind_gen{}'.format(self.current_generation -
                                                    1)
            best_ind = self.population.fittest_individual
            save_mario(folder, best_ind_name, best_ind)

        if self.config.Statistics.save_population_stats:
            fname = self.config.Statistics.save_population_stats
            save_stats(self.population, fname)

        self.population.individuals = elitism_selection(
            self.population, self.config.Selection.num_parents)

        random.shuffle(self.population.individuals)
        next_pop = []

        # Parents + offspring
        if self.config.Selection.selection_type == 'plus':
            # Decrement lifespan
            for individual in self.population.individuals:
                individual.lifespan -= 1

            for individual in self.population.individuals:
                config = individual.config
                chromosome = individual.network.params
                hidden_layer_architecture = individual.hidden_layer_architecture
                hidden_activation = individual.hidden_activation
                output_activation = individual.output_activation
                lifespan = individual.lifespan
                name = individual.name

                # If the indivdual would be alve, add it to the next pop
                if lifespan > 0:
                    m = Mario(config, chromosome, hidden_layer_architecture,
                              hidden_activation, output_activation, lifespan)
                    # Set debug if needed
                    if args.debug:
                        m.name = f'{name}_life{lifespan}'
                        m.debug = True
                    next_pop.append(m)

        num_loaded = 0

        while len(next_pop) < self._next_gen_size:
            selection = self.config.Crossover.crossover_selection
            if selection == 'tournament':
                p1, p2 = tournament_selection(
                    self.population, 2, self.config.Crossover.tournament_size)
            elif selection == 'roulette':
                p1, p2 = roulette_wheel_selection(self.population, 2)
            else:
                raise Exception(
                    'crossover_selection "{}" is not supported'.format(
                        selection))

            L = len(p1.network.layer_nodes)
            c1_params = {}
            c2_params = {}

            # Each W_l and b_l are treated as their own chromosome.
            # Because of this I need to perform crossover/mutation on each chromosome between parents
            for l in range(1, L):
                p1_W_l = p1.network.params['W' + str(l)]
                p2_W_l = p2.network.params['W' + str(l)]
                p1_b_l = p1.network.params['b' + str(l)]
                p2_b_l = p2.network.params['b' + str(l)]

                # Crossover
                # @NOTE: I am choosing to perform the same type of crossover on the weights and the bias.
                c1_W_l, c2_W_l, c1_b_l, c2_b_l = self._crossover(
                    p1_W_l, p2_W_l, p1_b_l, p2_b_l)

                # Mutation
                # @NOTE: I am choosing to perform the same type of mutation on the weights and the bias.
                self._mutation(c1_W_l, c2_W_l, c1_b_l, c2_b_l)

                # Assign children from crossover/mutation
                c1_params['W' + str(l)] = c1_W_l
                c2_params['W' + str(l)] = c2_W_l
                c1_params['b' + str(l)] = c1_b_l
                c2_params['b' + str(l)] = c2_b_l

                #  Clip to [-1, 1]
                np.clip(c1_params['W' + str(l)],
                        -1,
                        1,
                        out=c1_params['W' + str(l)])
                np.clip(c2_params['W' + str(l)],
                        -1,
                        1,
                        out=c2_params['W' + str(l)])
                np.clip(c1_params['b' + str(l)],
                        -1,
                        1,
                        out=c1_params['b' + str(l)])
                np.clip(c2_params['b' + str(l)],
                        -1,
                        1,
                        out=c2_params['b' + str(l)])

            c1 = Mario(self.config, c1_params, p1.hidden_layer_architecture,
                       p1.hidden_activation, p1.output_activation, p1.lifespan)
            c2 = Mario(self.config, c2_params, p2.hidden_layer_architecture,
                       p2.hidden_activation, p2.output_activation, p2.lifespan)

            # Set debug if needed
            if args.debug:
                c1_name = f'm{num_loaded}_new'
                c1.name = c1_name
                c1.debug = True
                num_loaded += 1

                c2_name = f'm{num_loaded}_new'
                c2.name = c2_name
                c2.debug = True
                num_loaded += 1

            next_pop.extend([c1, c2])

        # Set next generation
        random.shuffle(next_pop)
        self.population.individuals = next_pop
Example #5
0
    def next_generation(self):
        self._increment_generation()
        self._current_individual = 0

        # Calculate fitness of individuals
        for individual in self.population.individuals:
            individual.calculate_fitness()

        self.population.individuals = elitism_selection(
            self.population, self.settings['num_parents'])

        random.shuffle(self.population.individuals)
        next_pop: List[Snake] = []

        # parents + offspring selection type ('plus')
        if self.settings['selection_type'].lower() == 'plus':
            # Giảm tuổi thọ
            for individual in self.population.individuals:
                individual.lifespan -= 1

            for individual in self.population.individuals:
                params = individual.network.params
                board_size = individual.board_size
                hidden_layer_architecture = individual.hidden_layer_architecture
                hidden_activation = individual.hidden_activation
                output_activation = individual.output_activation
                lifespan = individual.lifespan
                apple_and_self_vision = individual.apple_and_self_vision

                start_pos = individual.start_pos
                apple_seed = individual.apple_seed
                starting_direction = individual.starting_direction

                # Nếu cá thể vẫn còn sống, nó vẫn tồn tại
                if lifespan > 0:
                    s = Snake(
                        board_size,
                        chromosome=params,
                        hidden_layer_architecture=hidden_layer_architecture,
                        hidden_activation=hidden_activation,
                        output_activation=output_activation,
                        lifespan=lifespan,
                        apple_and_self_vision=apple_and_self_vision)  #,
                    next_pop.append(s)

        while len(next_pop) < self._next_gen_size:
            p1, p2 = roulette_wheel_selection(self.population, 2)

            L = len(p1.network.layer_nodes)
            c1_params = {}
            c2_params = {}

            # Mỗi W_l và b_l được coi là nhiễm sắc thể của riêng họ.
            # Vì điều này, ta cần thực hiện trao đổi chéo / đột biến trên mỗi nhiễm sắc thể giữa cha mẹ
            for l in range(1, L):
                p1_W_l = p1.network.params['W' + str(l)]
                p2_W_l = p2.network.params['W' + str(l)]
                p1_b_l = p1.network.params['b' + str(l)]
                p2_b_l = p2.network.params['b' + str(l)]

                # Crossover
                # @NOTE: I am choosing to perform the same type of crossover on the weights and the bias.
                c1_W_l, c2_W_l, c1_b_l, c2_b_l = self._crossover(
                    p1_W_l, p2_W_l, p1_b_l, p2_b_l)

                # Mutation
                # @NOTE: Ta đang chọn để thực hiện cùng một loại đột biến về trọng lượng và sai lệch.
                self._mutation(c1_W_l, c2_W_l, c1_b_l, c2_b_l)

                # Chỉ định con cái từ chéo / đột biến
                c1_params['W' + str(l)] = c1_W_l
                c2_params['W' + str(l)] = c2_W_l
                c1_params['b' + str(l)] = c1_b_l
                c2_params['b' + str(l)] = c2_b_l

                # Clip to [-1, 1]
                np.clip(c1_params['W' + str(l)],
                        -1,
                        1,
                        out=c1_params['W' + str(l)])
                np.clip(c2_params['W' + str(l)],
                        -1,
                        1,
                        out=c2_params['W' + str(l)])
                np.clip(c1_params['b' + str(l)],
                        -1,
                        1,
                        out=c1_params['b' + str(l)])
                np.clip(c2_params['b' + str(l)],
                        -1,
                        1,
                        out=c2_params['b' + str(l)])

            # Tạo con từ nhiễm sắc thể được tạo ở trên
            c1 = Snake(p1.board_size,
                       chromosome=c1_params,
                       hidden_layer_architecture=p1.hidden_layer_architecture,
                       hidden_activation=p1.hidden_activation,
                       output_activation=p1.output_activation,
                       lifespan=self.settings['lifespan'])
            c2 = Snake(p2.board_size,
                       chromosome=c2_params,
                       hidden_layer_architecture=p2.hidden_layer_architecture,
                       hidden_activation=p2.hidden_activation,
                       output_activation=p2.output_activation,
                       lifespan=self.settings['lifespan'])

            # Thêm con vào thế hệ tiếp theo
            next_pop.extend([c1, c2])

        # Đặt thế hệ tiếp theo
        random.shuffle(next_pop)
        self.population.individuals = next_pop
Example #6
0
    def next_generation(self):
        self._increment_generation()
        # Calculate fitness of individuals
        for individual in self.population.individuals:
            individual.calculate_fitness()

        self.population.individuals = elitism_selection(
            self.population, self.cfg['num_parents'])
        random.shuffle(self.population.individuals)

        next_pop: List[Player] = []
        while len(next_pop) < self._next_gen_size:
            p1, p2 = roulette_wheel_selection(self.population, 2)

            L = p1.brain.layer_nodes
            c1_params = {}
            c2_params = {}

            # Each W_l and b_l are treated as their own chromosome.
            # Because of this I need to perform crossover/mutation on each chromosome between parents
            for l in range(0, L, 2):
                p1_W_l = p1.brain.net[l].weight.data.numpy()
                p2_W_l = p2.brain.net[l].weight.data.numpy()
                p1_b_l = np.array([p1.brain.net[l].bias.data.numpy()])
                p2_b_l = np.array([p2.brain.net[l].bias.data.numpy()])

                # Crossover
                # @NOTE: I am choosing to perform the same type of crossover on the weights and the bias.
                c1_W_l, c2_W_l, c1_b_l, c2_b_l = self._crossover(
                    p1_W_l, p2_W_l, p1_b_l, p2_b_l)

                # Mutation
                # @NOTE: I am choosing to perform the same type of mutation on the weights and the bias.
                self._mutation(c1_W_l, c2_W_l, c1_b_l, c2_b_l)

                # Assign children from crossover/mutation
                c1_params['W' + str(l)] = c1_W_l
                c2_params['W' + str(l)] = c2_W_l
                c1_params['b' + str(l)] = c1_b_l
                c2_params['b' + str(l)] = c2_b_l

                # Clip to [-1, 1]
                np.clip(c1_params['W' + str(l)],
                        -1,
                        1,
                        out=c1_params['W' + str(l)])
                np.clip(c2_params['W' + str(l)],
                        -1,
                        1,
                        out=c2_params['W' + str(l)])
                np.clip(c1_params['b' + str(l)],
                        -1,
                        1,
                        out=c1_params['b' + str(l)])
                np.clip(c2_params['b' + str(l)],
                        -1,
                        1,
                        out=c2_params['b' + str(l)])

            # Create children from chromosomes generated above
            brain = NN()
            boat = Boat()
            brain.transform_weights(c1_params)
            p1 = Player(brain, boat)

            brain = NN()
            boat = Boat()
            brain.transform_weights(c2_params)
            p2 = Player(brain, boat)

            # Add children to the next generation
            next_pop.extend([p1, p2])

        # Set the next generation
        random.shuffle(next_pop)
        self.group = next_pop
        self.population = Population(self.group)
Example #7
0
    def next_generation(self):
        self.current_generation += 1
        # reset for new game
        self.score = 0
        self.pipe_list.clear()
        self.pipe_list.extend(self.create_pipe())
        # reset timer so don't have the second pipe comming too soon
        # pygame.time.set_timer(self.SPAWNPIPE, 0)
        # pygame.time.set_timer(self.SPAWNPIPE, 1200)         ## TODO: test if this is neccessary
        self.spawn_pipe_counter = 0

        # Calculate fitness of individuals
        for individual in self.population.individuals:
            individual.calculate_fitness()

        # Find winner from each generation and champion
        self.winner = self.population.fittest_individual
        self.winner_index = self.population.individuals.index(self.winner)
        if self.winner.fitness > self.champion_fitness:
            self.champion_fitness = self.winner.fitness
            self.champion = self.winner
            self.champion_index = self.winner_index

        # Save the network of best birds from each generation
        if self.current_generation <= 100:
            individual_name = 'bird' + str(self.current_generation)
            self.save_bird('Flappy_Genetic/plot/best_birds_each_generation',
                           individual_name, self.winner, settings)

        self.winner.reset()
        self.champion.reset()

        # Print results from each generation
        print('======================= Gneration {} ======================='.
              format(self.current_generation))
        print('----Max fitness:', self.population.fittest_individual.fitness)
        # print('----Best Score:', self.population.fittest_individual.score)
        print('----Average fitness:', self.population.average_fitness)

        self.population.individuals = elitism_selection(
            self.population, settings['num_parents'])

        random.shuffle(self.population.individuals)
        next_pop: List[Bird] = []

        # parents + offspring selection type ('plus')
        if settings['selection_type'].lower() == 'plus':
            next_pop.append(self.winner)
            next_pop.append(self.champion)

        while len(next_pop) < self._next_gen_size:
            p1, p2 = roulette_wheel_selection(self.population, 2)

            L = len(p1.network.layer_nodes)
            c1_params = {}
            c2_params = {}

            # Each W_l and b_l are treated as their own chromosome.
            # Because of this I need to perform crossover/mutation on each chromosome between parents
            for l in range(1, L):
                p1_W_l = p1.network.params['W' + str(l)]
                p2_W_l = p2.network.params['W' + str(l)]
                p1_b_l = p1.network.params['b' + str(l)]
                p2_b_l = p2.network.params['b' + str(l)]

                # Crossover
                # @NOTE: I am choosing to perform the same type of crossover on the weights and the bias.
                c1_W_l, c2_W_l, c1_b_l, c2_b_l = self._crossover(
                    p1_W_l, p2_W_l, p1_b_l, p2_b_l)

                # Mutation
                # @NOTE: I am choosing to perform the same type of mutation on the weights and the bias.
                self._mutation(c1_W_l, c2_W_l, c1_b_l, c2_b_l)

                # Assign children from crossover/mutation
                c1_params['W' + str(l)] = c1_W_l
                c2_params['W' + str(l)] = c2_W_l
                c1_params['b' + str(l)] = c1_b_l
                c2_params['b' + str(l)] = c2_b_l

                # Clip to [-1, 1]
                np.clip(c1_params['W' + str(l)],
                        -1,
                        1,
                        out=c1_params['W' + str(l)])
                np.clip(c2_params['W' + str(l)],
                        -1,
                        1,
                        out=c2_params['W' + str(l)])
                np.clip(c1_params['b' + str(l)],
                        -1,
                        1,
                        out=c1_params['b' + str(l)])
                np.clip(c2_params['b' + str(l)],
                        -1,
                        1,
                        out=c2_params['b' + str(l)])

            # Create children from chromosomes generated above
            c1 = Bird(chromosome=c1_params,
                      hidden_layer_architecture=p1.hidden_layer_architecture,
                      hidden_activation=p1.hidden_activation,
                      output_activation=p1.output_activation)
            c2 = Bird(chromosome=c2_params,
                      hidden_layer_architecture=p2.hidden_layer_architecture,
                      hidden_activation=p2.hidden_activation,
                      output_activation=p2.output_activation)

            # Add children to the next generation
            next_pop.extend([c1, c2])

        # Set the next generation
        random.shuffle(next_pop)
        self.population.individuals = next_pop