Exemple #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
Exemple #2
0
    def _create_num_offspring(self, number_of_offspring) -> List[Individual]:
        """
        This is a helper function to decide whether to grab from current pop or create new offspring.

        Creates a number of offspring from the current population. This assumes that the current population are all able to reproduce.
        This is broken up from the main next_generation function so that we can create N individuals at a time if needed without going
        to the next generation. Mainly used if `run_at_a_time` is < the number of individuals that are in the next generation.
        """
        next_pop: List[Individual] = []
        #@TODO: comment this to new state
        # If the selection type is plus, then it means certain individuals survive to the next generation, so we need
        # to grab those first before we create new ones
        # if get_ga_constant('selection_type').lower() == 'plus' and len(self._next_pop) < get_ga_constant('num_parents'):
        if self.state == States.NEXT_GEN_COPY_PARENTS_OVER:
            # Select the subset of the individuals to bring to the next gen
            increment = 0  # How much did the offset increment by
            for idx in range(self._offset_into_population,
                             len(self.population.individuals)):
                # for individual in self.population.individuals[self._offset_into_population: self._offset_into_population + number_of_offspring]:
                individual = self.population.individuals[idx]
                increment += 1  # For offset
                world = self.world
                wheel_radii = individual.wheel_radii
                wheel_densities = individual.wheel_densities
                #wheel_motor_speeds = individual.wheel_motor_speeds
                chassis_vertices = individual.chassis_vertices
                chassis_densities = individual.chassis_densities
                winning_tile = individual.winning_tile
                lowest_y_pos = individual.lowest_y_pos
                lifespan = individual.lifespan

                # If the individual is still alive, they survive
                if lifespan > 0:
                    car = Car(
                        world,
                        wheel_radii,
                        wheel_densities,  # wheel_motor_speeds,       # Wheel
                        chassis_vertices,
                        chassis_densities,  # Chassis
                        winning_tile,
                        lowest_y_pos,
                        lifespan)
                    next_pop.append(car)
                    # Check to see if we've added enough parents. The reason we check here is if you requet 5 parents but
                    # 2/5 are dead, then you need to keep going until you get 3 good ones.
                    if len(next_pop) == number_of_offspring:
                        break
                else:
                    print("Oh dear, you're dead")
            # Increment offset for the next time
            self._offset_into_population += increment
            # If there weren't enough parents that made it to the new generation, we just accept it and move on.
            # Since the lifespan could have reached 0, you are not guaranteed to always have the same number of parents copied over.
            if self._offset_into_population >= len(
                    self.population.individuals):
                self.state = States.NEXT_GEN_CREATE_OFFSPRING
        # Otherwise just perform crossover with the current population and produce num_of_offspring
        # @NOTE: The state, even if we got here through State.NEXT_GEN or State.NEXT_GEN_COPY_PARENTS_OVER is now
        # going to switch to State.NEXT_GEN_CREATE_OFFSPRING based off this else condition. It's not set here, but
        # rather at the end of new_generation
        else:
            # Keep adding children until we reach the size we need
            while len(next_pop) < number_of_offspring:
                # Tournament crossover
                if get_ga_constant(
                        'crossover_selection').lower() == 'tournament':
                    p1, p2 = tournament_selection(
                        self.population, 2, get_ga_constant('tournament_size'))
                # Roulette
                elif get_ga_constant(
                        'crossover_selection').lower() == 'roulette':
                    p1, p2 = roulette_wheel_selection(self.population, 2)
                else:
                    raise Exception(
                        'crossover_selection "{}" is not supported'.format(
                            get_ga_constant('crossover_selection').lower()))

                # Crossover
                c1_chromosome, c2_chromosome = self._crossover(
                    p1.chromosome, p2.chromosome)

                # Mutation
                self._mutation(c1_chromosome)
                self._mutation(c2_chromosome)

                # Don't let the chassis density become <=0. It is bad
                smart_clip(c1_chromosome)
                smart_clip(c2_chromosome)

                # Create children from the new chromosomes
                c1 = Car.create_car_from_chromosome(
                    p1.world, p1.winning_tile, p1.lowest_y_pos,
                    get_ga_constant('lifespan'), c1_chromosome)
                c2 = Car.create_car_from_chromosome(
                    p2.world, p2.winning_tile, p2.lowest_y_pos,
                    get_ga_constant('lifespan'), c2_chromosome)

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

        # Return the next population that will play. Remember, this can be a subset of the overall population since
        # those parents still exist.
        return next_pop
    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
Exemple #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
Exemple #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
Exemple #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)
Exemple #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