[CT421]: Assignment 1 mostly done
This commit is contained in:
File diff suppressed because it is too large
Load Diff
33
year4/semester2/CT421/assignments/assignment1/code/plots.py
Executable file
33
year4/semester2/CT421/assignments/assignment1/code/plots.py
Executable file
@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import pandas as pd
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
# Load TSV file
|
||||||
|
def plot_fitness_trends(file_path):
|
||||||
|
df = pd.read_csv(file_path, sep='\t')
|
||||||
|
|
||||||
|
plt.figure(figsize=(10, 5))
|
||||||
|
plt.plot(df['generation'], df['avg_fitness'], label='Avg Fitness', marker=None)
|
||||||
|
plt.plot(df['generation'], df['generation_best'], label='Generation Best', marker=None)
|
||||||
|
plt.plot(df['generation'], df['current_best'], label='Current Best', marker=None)
|
||||||
|
|
||||||
|
plt.xlabel('Generation')
|
||||||
|
plt.ylabel('Fitness')
|
||||||
|
plt.title('Fitness Trends Across Generations')
|
||||||
|
plt.legend()
|
||||||
|
plt.grid()
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
# Main function
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description='Plot fitness trends from a TSV file.')
|
||||||
|
parser.add_argument('file_path', type=str, help='Path to the TSV file')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
plot_fitness_trends(args.file_path)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
@ -108,6 +108,15 @@ def select(population, fitnesses, number_to_select):
|
|||||||
|
|
||||||
return random.choices(population, weights, k=number_to_select)
|
return random.choices(population, weights, k=number_to_select)
|
||||||
|
|
||||||
|
# tournament
|
||||||
|
def tournament_select(population, fitnesses, number_to_select, tournament_size=3):
|
||||||
|
selected = []
|
||||||
|
for _ in range(number_to_select):
|
||||||
|
tournament = random.sample(list(zip(population, fitnesses)), tournament_size)
|
||||||
|
winner = min(tournament, key=lambda x: x[1]) # Assuming lower fitness is better
|
||||||
|
selected.append(winner[0])
|
||||||
|
return selected
|
||||||
|
|
||||||
|
|
||||||
# general crossover function
|
# general crossover function
|
||||||
def crossover(population, crossover_rate, number_to_replace):
|
def crossover(population, crossover_rate, number_to_replace):
|
||||||
@ -122,16 +131,16 @@ def crossover(population, crossover_rate, number_to_replace):
|
|||||||
if random.random() < crossover_rate:
|
if random.random() < crossover_rate:
|
||||||
# randomly alternate between the two crossover operators (50-50)
|
# randomly alternate between the two crossover operators (50-50)
|
||||||
if random.random() < 0.5:
|
if random.random() < 0.5:
|
||||||
child = pmx_crossover(parent1, parent2)
|
child = pmx(parent1, parent2)
|
||||||
else:
|
else:
|
||||||
child = ox_crossover(parent1, parent2)
|
child = ox(parent1, parent2)
|
||||||
|
|
||||||
offspring.append(child)
|
offspring.append(child)
|
||||||
|
|
||||||
return offspring
|
return offspring
|
||||||
|
|
||||||
# function to perform partially mapped crossover (as defined on wikipedia) on two parents
|
# function to perform partially mapped crossover (as defined on wikipedia) on two parents
|
||||||
def pmx_crossover(parent1, parent2):
|
def pmx(parent1, parent2):
|
||||||
size = len(parent1)
|
size = len(parent1)
|
||||||
child = [None] * size
|
child = [None] * size
|
||||||
|
|
||||||
@ -175,7 +184,7 @@ def pmx_crossover(parent1, parent2):
|
|||||||
|
|
||||||
|
|
||||||
# function to perform order crossover on two parents
|
# function to perform order crossover on two parents
|
||||||
def ox_crossover(parent1, parent2):
|
def ox(parent1, parent2):
|
||||||
size = len(parent1)
|
size = len(parent1)
|
||||||
child = [None] * size
|
child = [None] * size
|
||||||
|
|
||||||
@ -202,21 +211,56 @@ def ox_crossover(parent1, parent2):
|
|||||||
|
|
||||||
# general mutation function
|
# general mutation function
|
||||||
def mutate(offspring, mutation_rate):
|
def mutate(offspring, mutation_rate):
|
||||||
offspring = []
|
for index in range(len(offspring)):
|
||||||
|
if random.random() < mutation_rate:
|
||||||
|
# randomly alternate between the two mutation operators (50-50)
|
||||||
|
if random.random() < 0.5:
|
||||||
|
offspring[index] = rsm(offspring[index])
|
||||||
|
else:
|
||||||
|
offspring[index] = psm(offspring[index])
|
||||||
|
|
||||||
return offspring
|
return offspring
|
||||||
|
|
||||||
|
|
||||||
def main():
|
# reverse sequence mutation
|
||||||
parser = argparse.ArgumentParser(description="Program to solve the travelling salesman problem for a given TSP file using a genetic algorithm.")
|
def rsm(tour):
|
||||||
parser.add_argument("-i", "--input-file", type=str, help="Path to input file in TSP format", required=True)
|
size = len(tour)
|
||||||
parser.add_argument("-s", "--size", type=int, help="Initial population size", required=False, default=100)
|
|
||||||
parser.add_argument("-g", "--num-generations", type=int, help="Number of generations", required=False, default=500)
|
|
||||||
parser.add_argument("-a", "--give-up-after", type=int, help="Number of generations to give up after if best solution has remained unchanged", required=False, default=100)
|
|
||||||
parser.add_argument("-p", "--selection-proportion", type=float, help="The proportion of the population to be selected (survive) on each generation", required=False, default=0.2)
|
|
||||||
parser.add_argument("-c", "--crossover-rate", type=float, help="Probability of a selected pair of solutions to sexually reproduce", required=False, default=0.8)
|
|
||||||
parser.add_argument("-m", "--mutation-rate", type=float, help="Probability of a selected offspring to undergo mutation", required=False, default=0.2)
|
|
||||||
args=parser.parse_args()
|
|
||||||
|
|
||||||
|
# pick two random indices to define our subsequence
|
||||||
|
start = random.randint(0, size)
|
||||||
|
end = random.randint(0, size)
|
||||||
|
|
||||||
|
# swap them if start is after end
|
||||||
|
if end < start:
|
||||||
|
start, end = end, start
|
||||||
|
|
||||||
|
# reverse subsequence
|
||||||
|
tour[start:end] = tour[start:end][::-1]
|
||||||
|
|
||||||
|
return tour
|
||||||
|
|
||||||
|
|
||||||
|
# partial shuffle mutation
|
||||||
|
def psm(tour):
|
||||||
|
size = len(tour)
|
||||||
|
|
||||||
|
# pick two random indices to define our subsequence
|
||||||
|
start = random.randint(0, size)
|
||||||
|
end = random.randint(0, size)
|
||||||
|
|
||||||
|
# swap them if start is after end
|
||||||
|
if end < start:
|
||||||
|
start, end = end, start
|
||||||
|
|
||||||
|
subsequence = tour[start:end]
|
||||||
|
random.shuffle(subsequence)
|
||||||
|
tour[start:end] = subsequence
|
||||||
|
|
||||||
|
return tour
|
||||||
|
|
||||||
|
|
||||||
|
# function to run the genetic algorithm over a number of generations
|
||||||
|
def evolve(graph, adjacency_matrix, args):
|
||||||
print("Input file: " + str(args.input_file))
|
print("Input file: " + str(args.input_file))
|
||||||
print("Initial population size: " + str(args.size))
|
print("Initial population size: " + str(args.size))
|
||||||
print("Number of generations: " + str(args.num_generations))
|
print("Number of generations: " + str(args.num_generations))
|
||||||
@ -225,9 +269,6 @@ def main():
|
|||||||
print("Crossover rate: " + str(args.crossover_rate))
|
print("Crossover rate: " + str(args.crossover_rate))
|
||||||
print("Mutation rate: " + str(args.crossover_rate))
|
print("Mutation rate: " + str(args.crossover_rate))
|
||||||
|
|
||||||
graph = graph_from_file(args.input_file)
|
|
||||||
adjacency_matrix = adjacency_matrix_from_graph(graph)
|
|
||||||
|
|
||||||
# get initial population & its details
|
# get initial population & its details
|
||||||
population = initialise_population(args.size, graph)
|
population = initialise_population(args.size, graph)
|
||||||
fitnesses = list_of_fitnesses(population, adjacency_matrix)
|
fitnesses = list_of_fitnesses(population, adjacency_matrix)
|
||||||
@ -235,19 +276,19 @@ def main():
|
|||||||
|
|
||||||
# appending results to an array of strings rather than to a string as it's more efficient
|
# appending results to an array of strings rather than to a string as it's more efficient
|
||||||
results = ["timestamp\tgeneration\tpopulation_size\tavg_fitness\tgeneration_best\tcurrent_best"]
|
results = ["timestamp\tgeneration\tpopulation_size\tavg_fitness\tgeneration_best\tcurrent_best"]
|
||||||
results.append(str(time.time()) + "\t" + "0\t" + str(len(population)) + "\t" + str(sum(fitnesses)/len(fitnesses)) + "\t" + str(current_best["fitness"]) + "\t" + str(current_best["fitness"]))
|
results.append(str(time.time()) + "\t" + "0\t" + str(len(population)) + "\t" + str(sum(fitnesses) / len(fitnesses)) + "\t" + str(current_best["fitness"]) + "\t" + str(current_best["fitness"]))
|
||||||
|
|
||||||
# this is where efficiency gets critical lol
|
# this is where efficiency gets critical lol
|
||||||
for generation in range(1, args.num_generations):
|
for generation in range(1, args.num_generations):
|
||||||
print("Generation " + str(generation) + " of " + str(args.num_generations))
|
print("Generation " + str(generation) + " of " + str(args.num_generations))
|
||||||
# deselect solutions from population probabilistically
|
# deselect solutions from population probabilistically
|
||||||
population = select(population, fitnesses, int(len(population) * args.selection_proportion))
|
population = tournament_select(population, fitnesses, int(len(population) * args.selection_proportion))
|
||||||
|
|
||||||
# create a number of offspring with crossover to replace the number deselected
|
# create a number of offspring with crossover to replace the number deselected
|
||||||
offspring = crossover(population, args.crossover_rate, args.size - len(population))
|
offspring = crossover(population, args.crossover_rate, args.size - len(population))
|
||||||
|
|
||||||
# mutate offspring and add them to the original population to restore size
|
# mutate offspring and add them to the original population to restore size
|
||||||
population += offspring
|
population += mutate(offspring, args.mutation_rate)
|
||||||
|
|
||||||
# calculate fitnesses
|
# calculate fitnesses
|
||||||
fitnesses = list_of_fitnesses(population, adjacency_matrix)
|
fitnesses = list_of_fitnesses(population, adjacency_matrix)
|
||||||
@ -257,7 +298,7 @@ def main():
|
|||||||
if generation_best["fitness"] < current_best["fitness"]:
|
if generation_best["fitness"] < current_best["fitness"]:
|
||||||
current_best = generation_best
|
current_best = generation_best
|
||||||
|
|
||||||
results.append(str(time.time()) + "\t" + str(generation) + "\t" + str(len(population)) + "\t" + str(sum(fitnesses)/len(fitnesses)) + "\t" + str(generation_best["fitness"]) + "\t" + str(current_best["fitness"]))
|
results.append(str(time.time()) + "\t" + str(generation) + "\t" + str(len(population)) + "\t" + str(sum(fitnesses) / len(fitnesses)) + "\t" + str(generation_best["fitness"]) + "\t" + str(current_best["fitness"]))
|
||||||
|
|
||||||
if (generation - current_best["generation"]) >= args.give_up_after:
|
if (generation - current_best["generation"]) >= args.give_up_after:
|
||||||
print("Best solution has not changed in " + str(args.give_up_after) + " generations. Giving up.")
|
print("Best solution has not changed in " + str(args.give_up_after) + " generations. Giving up.")
|
||||||
@ -267,10 +308,31 @@ def main():
|
|||||||
print("Fitness of best solution: " + str(current_best["fitness"]))
|
print("Fitness of best solution: " + str(current_best["fitness"]))
|
||||||
print("Best solution found in generation: " + str(current_best["generation"]))
|
print("Best solution found in generation: " + str(current_best["generation"]))
|
||||||
|
|
||||||
with open("output.tsv", "w") as file:
|
with open(args.output_file, "w") as file:
|
||||||
for line in results:
|
for line in results:
|
||||||
file.write(line + "\n")
|
file.write(line + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# parse command line arguments and flags
|
||||||
|
parser = argparse.ArgumentParser(description="Program to solve the travelling salesman problem for a given TSP file using a genetic algorithm.")
|
||||||
|
parser.add_argument("-i", "--input-file", type=str, help="Path to input file in TSP format", required=True)
|
||||||
|
parser.add_argument("-s", "--size", type=int, help="Initial population size", required=False, default=100)
|
||||||
|
parser.add_argument("-g", "--num-generations", type=int, help="Number of generations", required=False, default=500)
|
||||||
|
parser.add_argument("-a", "--give-up-after", type=int, help="Number of generations to give up after if best solution has remained unchanged", required=False, default=100)
|
||||||
|
parser.add_argument("-p", "--selection-proportion", type=float, help="The proportion of the population to be selected (survive) on each generation", required=False, default=0.2)
|
||||||
|
parser.add_argument("-c", "--crossover-rate", type=float, help="Probability of a selected pair of solutions to sexually reproduce", required=False, default=0.8)
|
||||||
|
parser.add_argument("-m", "--mutation-rate", type=float, help="Probability of a selected offspring to undergo mutation", required=False, default=0.2)
|
||||||
|
parser.add_argument("-o", "--output-file", type=str, help="File to write TSV results to", required=False, default="output.tsv")
|
||||||
|
args=parser.parse_args()
|
||||||
|
|
||||||
|
# read in files and generate graph + adjacency matrix:w
|
||||||
|
graph = graph_from_file(args.input_file)
|
||||||
|
adjacency_matrix = adjacency_matrix_from_graph(graph)
|
||||||
|
|
||||||
|
# run the genetic algorithm
|
||||||
|
evolve(graph, adjacency_matrix, args)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
https://link.springer.com/article/10.1023/A:1006529012972
|
||||||
|
https://arxiv.org/pdf/1203.5028
|
Reference in New Issue
Block a user