Source code for axelrod.strategies.ann

from typing import List, Tuple

import numpy as np
from axelrod.action import Action
from axelrod.evolvable_player import (
    EvolvablePlayer,
    InsufficientParametersError,
    crossover_lists,
)
from axelrod.load_data_ import load_weights
from axelrod.player import Player

C, D = Action.C, Action.D
nn_weights = load_weights()

# Neural Network and Activation functions
relu = np.vectorize(lambda x: max(x, 0))


def num_weights(num_features, num_hidden):
    size = num_features * num_hidden + 2 * num_hidden
    return size


[docs]def compute_features(player: Player, opponent: Player) -> np.ndarray: """ Compute history features for Neural Network: * Opponent's first move is C * Opponent's first move is D * Opponent's second move is C * Opponent's second move is D * Player's previous move is C * Player's previous move is D * Player's second previous move is C * Player's second previous move is D * Opponent's previous move is C * Opponent's previous move is D * Opponent's second previous move is C * Opponent's second previous move is D * Total opponent cooperations * Total opponent defections * Total player cooperations * Total player defections * Round number """ if len(opponent.history) == 0: opponent_first_c = 0 opponent_first_d = 0 opponent_second_c = 0 opponent_second_d = 0 my_previous_c = 0 my_previous_d = 0 my_previous2_c = 0 my_previous2_d = 0 opponent_previous_c = 0 opponent_previous_d = 0 opponent_previous2_c = 0 opponent_previous2_d = 0 elif len(opponent.history) == 1: opponent_first_c = 1 if opponent.history[0] == C else 0 opponent_first_d = 1 if opponent.history[0] == D else 0 opponent_second_c = 0 opponent_second_d = 0 my_previous_c = 1 if player.history[-1] == C else 0 my_previous_d = 1 if player.history[-1] == D else 0 my_previous2_c = 0 my_previous2_d = 0 opponent_previous_c = 1 if opponent.history[-1] == C else 0 opponent_previous_d = 1 if opponent.history[-1] == D else 0 opponent_previous2_c = 0 opponent_previous2_d = 0 else: opponent_first_c = 1 if opponent.history[0] == C else 0 opponent_first_d = 1 if opponent.history[0] == D else 0 opponent_second_c = 1 if opponent.history[1] == C else 0 opponent_second_d = 1 if opponent.history[1] == D else 0 my_previous_c = 1 if player.history[-1] == C else 0 my_previous_d = 1 if player.history[-1] == D else 0 my_previous2_c = 1 if player.history[-2] == C else 0 my_previous2_d = 1 if player.history[-2] == D else 0 opponent_previous_c = 1 if opponent.history[-1] == C else 0 opponent_previous_d = 1 if opponent.history[-1] == D else 0 opponent_previous2_c = 1 if opponent.history[-2] == C else 0 opponent_previous2_d = 1 if opponent.history[-2] == D else 0 # Remaining Features total_opponent_c = opponent.cooperations total_opponent_d = opponent.defections total_player_c = player.cooperations total_player_d = player.defections return np.array( ( opponent_first_c, opponent_first_d, opponent_second_c, opponent_second_d, my_previous_c, my_previous_d, my_previous2_c, my_previous2_d, opponent_previous_c, opponent_previous_d, opponent_previous2_c, opponent_previous2_d, total_opponent_c, total_opponent_d, total_player_c, total_player_d, len(player.history), ) )
[docs]def activate( bias: List[float], hidden: List[float], output: List[float], inputs: np.ndarray, ) -> float: """ Compute the output of the neural network: output = relu(inputs * hidden_weights + bias) * output_weights """ hidden_values = bias + np.dot(hidden, inputs) hidden_values = relu(hidden_values) output_value = np.dot(hidden_values, output) return output_value
[docs]def split_weights( weights: List[float], num_features: int, num_hidden: int ) -> Tuple[List[List[float]], List[float], List[float]]: """Splits the input vector into the the NN bias weights and layer parameters.""" # Check weights is the right length expected_length = num_hidden * 2 + num_features * num_hidden if expected_length != len(weights): raise ValueError("NN weights array has an incorrect size.") number_of_input_to_hidden_weights = num_features * num_hidden number_of_hidden_to_output_weights = num_hidden input2hidden = [] for i in range(0, number_of_input_to_hidden_weights, num_features): input2hidden.append(weights[i : i + num_features]) start = number_of_input_to_hidden_weights end = number_of_input_to_hidden_weights + number_of_hidden_to_output_weights hidden2output = weights[start:end] bias = weights[end:] return input2hidden, hidden2output, bias
[docs]class ANN(Player): """Artificial Neural Network based strategy. A single layer neural network based strategy, with the following features: * Opponent's first move is C * Opponent's first move is D * Opponent's second move is C * Opponent's second move is D * Player's previous move is C * Player's previous move is D * Player's second previous move is C * Player's second previous move is D * Opponent's previous move is C * Opponent's previous move is D * Opponent's second previous move is C * Opponent's second previous move is D * Total opponent cooperations * Total opponent defections * Total player cooperations * Total player defections * Round number Original Source: https://gist.github.com/mojones/550b32c46a8169bb3cd89d917b73111a#file-ann-strategy-test-L60 Names - Artificial Neural Network based strategy: Original name by Martin Jones """ name = "ANN" classifier = { "memory_depth": float("inf"), "stochastic": False, "inspects_source": False, "manipulates_source": False, "manipulates_state": False, "long_run_time": False, } def __init__( self, num_features: int, num_hidden: int, weights: List[float] = None ) -> None: Player.__init__(self) self.num_features = num_features self.num_hidden = num_hidden self._process_weights(weights, num_features, num_hidden) def _process_weights(self, weights, num_features, num_hidden): self.weights = list(weights) (i2h, h2o, bias) = split_weights(weights, num_features, num_hidden) self.input_to_hidden_layer_weights = np.array(i2h) self.hidden_to_output_layer_weights = np.array(h2o) self.bias_weights = np.array(bias)
[docs] def strategy(self, opponent: Player) -> Action: """Actual strategy definition that determines player's action.""" features = compute_features(self, opponent) output = activate( self.bias_weights, self.input_to_hidden_layer_weights, self.hidden_to_output_layer_weights, features, ) if output > 0: return C else: return D
[docs]class EvolvableANN(ANN, EvolvablePlayer): """Evolvable version of ANN.""" name = "EvolvableANN" def __init__( self, num_features: int, num_hidden: int, weights: List[float] = None, mutation_probability: float = None, mutation_distance: int = 5, seed: int = None, ) -> None: EvolvablePlayer.__init__(self, seed=seed) ( num_features, num_hidden, weights, mutation_probability, ) = self._normalize_parameters( num_features, num_hidden, weights, mutation_probability ) ANN.__init__( self, num_features=num_features, num_hidden=num_hidden, weights=weights, ) self.mutation_probability = mutation_probability self.mutation_distance = mutation_distance self.overwrite_init_kwargs( num_features=num_features, num_hidden=num_hidden, weights=weights, mutation_probability=mutation_probability, ) def _normalize_parameters( self, num_features=None, num_hidden=None, weights=None, mutation_probability=None, ): if not (num_features and num_hidden): raise InsufficientParametersError( "Insufficient Parameters to instantiate EvolvableANN" ) size = num_weights(num_features, num_hidden) if not weights: weights = [self._random.uniform(-1, 1) for _ in range(size)] if mutation_probability is None: mutation_probability = 10.0 / size return num_features, num_hidden, weights, mutation_probability def mutate_weights( self, weights, num_features, num_hidden, mutation_probability, mutation_distance, ): size = num_weights(num_features, num_hidden) randoms = self._random.random(size) for i, r in enumerate(randoms): if r < mutation_probability: p = 1 + self._random.uniform(-1, 1) * mutation_distance weights[i] *= p return weights
[docs] def mutate(self): weights = self.mutate_weights( self.weights, self.num_features, self.num_hidden, self.mutation_probability, self.mutation_distance, ) return self.create_new(weights=weights)
[docs] def crossover(self, other): if other.__class__ != self.__class__: raise TypeError( "Crossover must be between the same player classes." ) weights = crossover_lists(self.weights, other.weights, self._random) return self.create_new(weights=weights)
[docs]class EvolvedANN(ANN): """ A strategy based on a pre-trained neural network with 17 features and a hidden layer of size 10. Trained using the `axelrod_dojo` version: 0.0.8 Training data is archived at doi.org/10.5281/zenodo.1306926 Names: - Evolved ANN: Original name by Martin Jones. """ name = "Evolved ANN" def __init__(self) -> None: num_features, num_hidden, weights = nn_weights["Evolved ANN"] super().__init__( num_features=num_features, num_hidden=num_hidden, weights=weights )
[docs]class EvolvedANN5(ANN): """ A strategy based on a pre-trained neural network with 17 features and a hidden layer of size 5. Trained using the `axelrod_dojo` version: 0.0.8 Training data is archived at doi.org/10.5281/zenodo.1306931 Names: - Evolved ANN 5: Original name by Marc Harper. """ name = "Evolved ANN 5" def __init__(self) -> None: num_features, num_hidden, weights = nn_weights["Evolved ANN 5"] super().__init__( num_features=num_features, num_hidden=num_hidden, weights=weights )
[docs]class EvolvedANNNoise05(ANN): """ A strategy based on a pre-trained neural network with a hidden layer of size 5, trained with noise=0.05. Trained using the `axelrod_dojo` version: 0.0.8 Training data i archived at doi.org/10.5281/zenodo.1314247. Names: - Evolved ANN Noise 5: Original name by Marc Harper. """ name = "Evolved ANN 5 Noise 05" def __init__(self) -> None: num_features, num_hidden, weights = nn_weights["Evolved ANN 5 Noise 05"] super().__init__( num_features=num_features, num_hidden=num_hidden, weights=weights )