def test_crossbreed(self): """ Test if crossbreeding works properly and results in a entirely new WeightSet """ # Get two arbitrary arrays for _ in range(1000): # Get arbitrary array for crossbreeding ws2 = WeightSet(layer_sizes=self.layer_sizes) crossbreeded_ws = WeightSet.crossbreed(self.weightset, ws2) # Crossbreeded WeightSet cannot be the same as the input instances self.assertNotEqual(crossbreeded_ws, self.weightset) self.assertNotEqual(crossbreeded_ws, ws2) # Crossbreeded weights cannot be the same as either of the initial weights self.assertNotEqual(crossbreeded_ws.weights, self.weightset.weights) self.assertNotEqual(crossbreeded_ws.weights, ws2.weights) # Crossbreeded weightset must be a WeightSet instance self.assertIsInstance(crossbreeded_ws, WeightSet) self.assertIsInstance(crossbreeded_ws.weights, np.ndarray) # Activation function must be the same as the first weightset self.assertEqual(self.weightset.activation_name, crossbreeded_ws.activation_name)
def reset(self): """ Initialize arbitrary WeightSet """ # Initialize random layers and sizes model_depth = np.random.randint(3, 10) self.layer_sizes = [ np.random.randint(2, 50) for _ in range(model_depth) ] # Output size must be smaller than last hidden layer self.output_size = np.random.randint(1, self.layer_sizes[-1]) self.weightset = WeightSet(layer_sizes=self.layer_sizes + [self.output_size]) # Save input size for testing self.input_size = self.layer_sizes[0]
def generate_next_generation(self): """ With the current generation and the achieved scores of each WeightSet, construct a new set of WeightSets to use for the next generation. """ # Create a list for the new population we are creating new_population = list() # Sort the population based on the achieved scores sorted_population = [ weight_set for _, weight_set in sorted(zip(self.scores, self.population), key=lambda pair: pair[0], reverse=True) ] # Calculate how many of elites, crossovers and mutations we will have in the new population num_elites = int(self.POPULATION_SIZE * self.ELITE_FRACTION) num_crossovers = int( (self.POPULATION_SIZE - num_elites) * self.CROSSOVER_FRACTION) num_mutations = self.POPULATION_SIZE - num_elites - num_crossovers # Let the best elite percentage directly carry over new_population.extend(sorted_population[:num_elites]) # Scale the scores so that together they sum to 1 (used to then sample randomly from it) if np.sum(self.scores) <= 0: # Prevent division by 0 if no WeightSet achieved any points scaled_scores = np.full(self.POPULATION_SIZE, 1.0 / self.POPULATION_SIZE) else: scaled_scores = np.array(self.scores) / np.sum(self.scores) # Cross-overs from 2 parents sampled with probability linked to their score for parent1, parent2 in np.random.choice(self.population, p=scaled_scores, size=(num_crossovers, 2)): # New child from crossover child = WeightSet.crossbreed(parent1, parent2) # Also mutate a little bit if wanted if self.CROSSOVER_MUTATION_PROBABILITY > 0: child.mutate(self.CROSSOVER_MUTATION_PROBABILITY) new_population.append(child) # Mutate the remainder from current WeightSets sampled with probability linked to their score for parent in np.random.choice(self.population, p=scaled_scores, size=num_mutations): mutant: WeightSet = parent.clone() mutant.mutate(self.MUTATION_PROBABILITY) new_population.append(mutant) # Replace the old generation's population with the new population (which now does not have a tested score yet) self.population = new_population self.scores = list()
def test_action(self): """ Make sure the action is gotten from the (correct) active WeightSet by feed-forwarding the input """ GeneticModel.POPULATION_SIZE = 50 genetic_model = GeneticModel( game_name="SnakeGen-v1", input_shape=(2, ), action_space=self.environment.action_space) for i in {0, 4, 10, 24, 49}: weight_set = WeightSet(layer_sizes=[2, 4, 5]) # Create a new WeightSet genetic_model.current_weight_set_id = i # Select the i'th WeightSet genetic_model.population[ i] = weight_set # Insert our WeightSet in spot i in the population inputs = [(i * 17) % 21, (i * 11) % 7] # Slightly random input # Make sure the action gotten from the genetic model is actually the action you get when # you feedforward our input in our WeightSet that we injected self.assertEqual(genetic_model.action(inputs), weight_set.feedforward(inputs))
def __init__(self, game_name, input_shape, action_space, logger_path="output/genetic_model"): """ Initialize the population """ # Initialize variables self.population = list( ) # Set of WeightSets containing the weights of all agents in the current population self.scores = list( ) # The scores that the WeightSets of the population achieved when running a game self.current_weight_set_id = 0 # The index of the WeightSet that is currently playing self.generation_id = 0 # The generation number we are currently in self.generation_outcomes = list() # Stores results for each generation self.weight_set_score = list( ) # The scores of the GAMES_PER_GENERATION games played with one WeightSet # Calculate sizes of all layers of the neural net input_size = int(np.prod(input_shape)) output_size = action_space.n layer_sizes = [input_size, *self.HIDDEN_LAYERS, output_size] # Populate with POPULATION_SIZE nets / WeightSets for i in range(self.POPULATION_SIZE): self.population.append(WeightSet(layer_sizes)) # If enabled, create a replayer that plays games with the best WeightSets of the previous generation if self.RENDER_BEST_WEIGHTSETS: self.replayer = PlayWeightSet(game_name, self.population[0]) self.replayer.start() # Prevent any plots from making the terminal hang if self.PLOT_STATS: plt.ion() # If saving certain logs, make sure the folders for it exists if self.SAVE_BEST_WEIGHTSETS or self.SAVE_GEN_SCORES: # Make sure the LOGS folder exists if not os.path.exists(self.dir_logs): os.makedirs(self.dir_logs) if self.SAVE_GEN_SCORES: # Make sure the SCORES folder exists if not os.path.exists(self.dir_scores): os.makedirs(self.dir_scores) if self.SAVE_GEN_SCORES: # Make sure the WEIGHTSETS folder exists if not os.path.exists(self.dir_weightsets): os.makedirs(self.dir_weightsets) super().__init__(game_name, input_shape, action_space, logger_path)
class TestWeightSet(unittest.TestCase): """ Test class for the WeightSet class. """ def setUp(self): """ Setup WeightSet for every test """ # Initialize random layers and sizes model_depth = np.random.randint(3, 10) self.layer_sizes = [ int(np.random.randint(2, 50)) for _ in range(model_depth) ] # Output size must be smaller than last hidden layer self.output_size = np.random.randint(1, self.layer_sizes[-1]) self.weightset = WeightSet(layer_sizes=self.layer_sizes + [self.output_size]) # Save input size for testing self.input_size = self.layer_sizes[0] def reset(self): """ Initialize arbitrary WeightSet """ # Initialize random layers and sizes model_depth = np.random.randint(3, 10) self.layer_sizes = [ np.random.randint(2, 50) for _ in range(model_depth) ] # Output size must be smaller than last hidden layer self.output_size = np.random.randint(1, self.layer_sizes[-1]) self.weightset = WeightSet(layer_sizes=self.layer_sizes + [self.output_size]) # Save input size for testing self.input_size = self.layer_sizes[0] def test_clone(self): """ Test cloning (deepcopy) """ clone = self.weightset.clone() # Test if they are not the same instance self.assertNotEqual(self.weightset, clone) # Test if arguments are the same self.assertEqual(self.weightset.layer_sizes, clone.layer_sizes) self.assertEqual(self.weightset.activation, clone.activation) self.assertEqual(self.weightset.initialization_name, clone.initialization_name) # Test if deepcopy argument change does not change original clone.activation = 'foo' clone.layer_sizes = [-999, -999] clone.initialization_name = 'foo' self.assertNotEqual(self.weightset.activation, clone.activation) self.assertNotEqual(self.weightset.activation, clone.activation) self.assertNotEqual(self.weightset.initialization_name, clone.initialization_name) def test_randn_init(self): """ Test random initialization of weights """ for _ in range(1000): self.reset() self.assertIsInstance(self.weightset.weights, np.ndarray) def test_feedforward(self): """ Test the feedforward method """ for _ in range(100): # Get random set of weights self.reset() # The feedforward pass must work on all activation functions for act_func in self.weightset.activation_dir.values(): self.weightset.activation = act_func # Make feedforward pass model_input = np.random.uniform(-999, 999, self.input_size) output = self.weightset.feedforward(model_input) # Output must be an integer self.assertIsInstance(output, np.int64) def test_mutate(self): """ Test if mutate changes the weights in-place """ for _ in range(1000): mutation_probability = np.random.uniform(0.00001, 1) weights_before = deepcopy(self.weightset.weights) self.weightset.mutate(mutation_probability) weights_after = self.weightset.weights # Weights cannot be the same after mutating if mutation_probability > 0 self.assertNotEqual(weights_before, weights_after) def test_crossbreed(self): """ Test if crossbreeding works properly and results in a entirely new WeightSet """ # Get two arbitrary arrays for _ in range(1000): # Get arbitrary array for crossbreeding ws2 = WeightSet(layer_sizes=self.layer_sizes) crossbreeded_ws = WeightSet.crossbreed(self.weightset, ws2) # Crossbreeded WeightSet cannot be the same as the input instances self.assertNotEqual(crossbreeded_ws, self.weightset) self.assertNotEqual(crossbreeded_ws, ws2) # Crossbreeded weights cannot be the same as either of the initial weights self.assertNotEqual(crossbreeded_ws.weights, self.weightset.weights) self.assertNotEqual(crossbreeded_ws.weights, ws2.weights) # Crossbreeded weightset must be a WeightSet instance self.assertIsInstance(crossbreeded_ws, WeightSet) self.assertIsInstance(crossbreeded_ws.weights, np.ndarray) # Activation function must be the same as the first weightset self.assertEqual(self.weightset.activation_name, crossbreeded_ws.activation_name) def test_get_init_std(self): """ Test if all initialization schemes are working properly """ # Create arbitrary values for rows and columns for _ in range(1000): rows = np.random.randint(1, 1000) columns = np.random.randint(1, 1000) for init in initializations: self.weightset.initialization_name = init std = self.weightset.get_init_std(rows, columns) self.assertIsInstance(std, np.float64) self.assertGreater(std, 0) def test_activations(self): """ Test all activations that are included in the activation directory """ for _ in range(100): # Initialize random weights and get arbitrary layer self.weightset.weights = self.weightset.randn_init() weight_array = self.weightset.weights[0][0] bias_array = self.weightset.weights[0][0] for act_func in self.weightset.activation_dir.values(): result1 = act_func(weight_array) result2 = act_func(bias_array) self.assertIsInstance(result1, np.ndarray) self.assertIsInstance(result2, np.ndarray)