VERY IMPORTANT NOTICE
running the program as it is now will not execute correctly, since the weights are now assigned statically. The file that has been kept in the repository must be interpreted as read-only
Genetic Algorithm for Refining AI Weights
Within this section, we delve into the intricacies of employing a genetic algorithm to fine-tune the weights of an artificial intelligence system within the specific domain of Tablut gameplay. The primary objective of this algorithm is to iteratively evolve a population of weight sets, refining the AI's performance over time. These weights serve as crucial hyperparameters for the associated heuristics.
Concise Overview of the Algorithm
Genetic algorithms, commonly applied in optimization scenarios, found utility in our context by framing an optimization problem around Tablut matches. This involved formulating a fitness function, which, given a match with a set of hyperparameters and the count of moves required for victory, yields a value denoted as N.
The algorithm's primary objective is to maximize the derived value N. Notably, 30 represents the maximum allowable moves per game, and if the game extends beyond this limit, the fitness is constrained to -1.
Algorithmic Workflow
Initiating with a starting population of hyperparameters comprising 10 tuples, the algorithm orchestrates 10 games, recording the move count for each victory. Subsequently, it calculates a new population by selecting the best-performing elements and subjecting them to mutations. Noteworthy is the inclusion of two entirely random elements in each population, strategically introduced to mitigate the risk of converging into a local minimum.
Genetic Algorithm Components
Crossover
The cross_chromosome
function performs crossover between two parent chromosomes, generating two new chromosomes.
def cross_chromosome(chromosome1, chromosome2):
alpha0, beta0, gamma0, theta0, epsilon0 = chromosome1
alpha1, beta1, gamma1, theta1, epsilon1 = chromosome2
new_chromosome1 = [alpha0, beta1, gamma0, theta1, epsilon0]
new_chromosome2 = [alpha1, beta0, gamma1, theta0, epsilon1]
return new_chromosome1, new_chromosome2
Mate Selection
The mate
function randomly selects two chromosomes from the population and creates a new chromosome through averaging their values.
def mate(elements):
random1 = copy.deepcopy(random.choice(elements))
random2 = copy.deepcopy(random.choice(elements))
alpha0, beta0, gamma0, theta0, epsilon0 = random1
alpha1, beta1, gamma1, theta1, epsilon1 = random2
new_chromosome = [(alpha0+alpha1)/2, (beta0+beta1)/2, (gamma0+gamma1)/2, (theta0+theta1)/2, (epsilon0+epsilon1)/2]
return new_chromosome
Mutation
The mutate
function introduces random mutations to the population, affecting the weights of each chromosome.
def mutate(population, mutation_rate=0.6):
for i in range(1, len(population)):
if random.random() < mutation_rate:
alpha, beta, gamma, theta, epsilon = population[i]
alpha += random.uniform(-1, 1)
beta += random.uniform(-1, 1)
gamma += random.uniform(-1, 1)
theta += random.uniform(-1, 1)
epsilon += random.uniform(-1, 1)
population[i] = [alpha, beta, gamma, theta, epsilon]
return population
Population Initialization
The create_starting_population
function initializes the population with random chromosomes.
def create_starting_population(elements=10):
population = []
for i in range(elements):
population.append([random.uniform(0, 2), random.uniform(0, 2), random.uniform(0, 10), random.uniform(0, 2), random.uniform(0, 3)])
return population
Main Loop
The genetic algorithm iterates over a predefined number of generations, evaluating the fitness of each chromosome in the population by playing games. The top-performing chromosomes are selected for crossover, mating, and mutation to produce the next generation.