SforAiDl/genrl

View on GitHub
genrl/evolutionary/genetic_hyperparam.py

Summary

Maintainability
A
3 hrs
Test Coverage
import gc
import random
from typing import Dict

from genrl.evolutionary.utils import (
    create_random_agent,
    get_params_agent,
    set_params_agent,
)

"""
Code is heavily inspired from https://github.com/harvitronix/neural-network-genetic-algorithm
"""


class GeneticHyperparamTuner:
    def __init__(
        self,
        parameter_choices: [Dict],
        retain: float = 0.4,
        random_select: float = 0.1,
        mutate_chance: float = 0.2,
    ):

        """
        Create a Genetic Hyperparameter Tuner for GenRL

        Args:
            parameter_choices(dict) : Possible network parameters
            retain (float) : Percentage of population to retain after each generation
            random_select (float): Probability of a rejected network remaining in the population
            mutate_chance (float): Probability a network will be randomly mutated
        """

        self.parameter_choices = parameter_choices
        self.retain = retain
        self.random_select = random_select
        self.mutate_chance = mutate_chance

    def initialize_population(self, no_of_parents, agent):
        """
        Create a population of random networks

        Args:
            no_of_parents(int): Number of parents in each generation (size of the population)
            agent (BaseAgent): Generic Agent Object
        Returns:
            (list) : Population of agents
        """

        population_agents = []

        # looping to create no_of_parents agents
        for _ in range(no_of_parents):

            agent = create_random_agent(self.parameter_choices, agent)

            population_agents.append(agent)

        return population_agents

    def breed(self, mother, father):
        """
        Make children as part of their parents

        Args:
            mother: agent
            father: agent

        Return:
            List of 2 children (agents)

        """

        mother_params = mother.get_hyperparams()
        father_params = father.get_hyperparams()

        children = []

        for _ in range(2):

            child_params = {}

            for key in self.parameter_choices:
                child_params[key] = random.choice(
                    [mother_params[key], father_params[key]]
                )

            child_agent = get_params_agent(child_params, father)

            children.append(child_agent)

        return children

    def mutate(self, agent):
        """
        Randomly mutates one part of the agent

        Args:
            agent(BaseAgent): The RL agent
        """

        # choose a hyperparameter to mutate
        mutation = random.choice(list(self.parameter_choices.keys()))

        # mutate the chosen hyperparameter

        # randomly choose the value
        mutation_value = random.choice(self.parameter_choices[mutation])
        # set the value in the agent
        agent = set_params_agent(agent, mutation, mutation_value)

        return agent

    def fitness(self, agent):
        """
        Return the mean rewards, which is our fitness function
        """

        return NotImplementedError

    def grade(self, population):
        """
        Average fitness of the population
        """
        summed = sum([self.fitness(agent) for agent in population])
        return summed / float(len(population))

    def evolve(self, population):
        """
        Evolve the population of the network

        Args:
            population(list): A list of agents

        """

        # Get scores for each network
        graded = [(self.fitness(agent), agent) for agent in population]

        # sort of basis of the score
        graded = [x[1] for x in sorted(graded, key=lambda x: x[0], reverse=True)]

        # get the number we want to kep for next gen
        retain_length = int(len(graded) * self.retain)

        # parents we want to retain
        parents = graded[:retain_length]
        to_be_deleted = []

        # for left out individuals we randomly keep some
        for individual in graded[retain_length:]:
            if self.random_select > random.random():
                parents.append(individual)
            else:
                to_be_deleted.append(individual)

        # DELETE ALL OTHERS in to_be_deleted
        # done to avoid any memory leak
        del to_be_deleted[:]
        del to_be_deleted
        gc.collect()

        # how many spots left to fill in the population
        parents_length = len(parents)
        children_length = len(population) - len(parents)

        children = []

        while len(children) < children_length:

            if parents_length == 1:
                male = parents[0]
                babies = self.breed(male, male)

                for baby in babies:
                    if len(children) < children_length:
                        children.append(baby)

            male = random.randint(0, parents_length - 1)
            female = random.randint(0, parents_length - 1)

            if male != female:
                male = parents[male]
                female = parents[female]

                babies = self.breed(male, female)

                for baby in babies:
                    if len(children) < children_length:
                        children.append(baby)

        parents.extend(children)

        return parents