Professional Documents
Culture Documents
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from tqdm import tqdm
#np.random.seed(33)
Στο 7ο μάθημα είδαμε τον Γενετικό Αλγόριθμο, που είναι (με διαφορά) ο πιο δημοφιλής μετα-ευριστικός
αλγόριθμος βελτιστοποίησης. Στα μαθήματα 8 και 9 θα δούμε τους τέσσερις αμέσως δημοφιλέστερους μετα-
ευριστικούς αλγόριθμους, από δύο σε κάθε μάθημα.
Σήμερα θα δούμε δύο αλγόριθμους που χρησιμοποιούνται για προβλήματα τύπου Περιοδεύοντος Πωλητή.
Ας θυμηθούμε το πρόβλημα:
Έχουμε το παρακάτο σύνολο σημείων στον δισδιάστατο χώρο και ψάχνουμε το μονοπάτι που θα περάσει
από όλα τα σημεία διανύοντας την μικρότερη συνολική απόσταση, δηλαδή το συντομότερο μονοπάτι.
In [4]:
points = np.array([[2.12,0.7],[1.68,0.97],[1.28,1.5],[1.16,2.3],[1.12,4],
[1.2,5.4],[1.32,7.2],[1.64,8.4],[1.92,8.6],[2.2,8],
[2.52,7.6],[2.72,6.4],[2.8,4.8],[2.72,3.3],[2.56,2.3],[2.4,1.2]])
#points.shape
plt.scatter(points[:,0],points[:,1]);
Οι αλγόριθμοι που θα εξετάσουμε είναι οι Tabu Search, και Ant Colony Optimization.
1. Tabu Search
http://localhost:8888/lab 1/13
31/5/2022 8_Metaheuristics_TSP
H μέθοδος tabu search, όπως την υλοποιούμε εδώ, είναι σαν ένας γενετικός αλγόριθμος για προβλήματα
περιοδεύοντος πωλητή, με δύο κύριες διαφορές:
Με άλλα λόγια, η tabu search που υλοποιούμε εδώ είναι ένας γενετικός όπου: 1) γίνεται αναζήτηση μονο σε
γειτονικά μονοπάτια από αυτά που ήδη έχουμε (η λειτουργία crossover παράγει μονοπάτια αρκετά
διαφορετικά από τους "γονείς" τους, ενώ η mutation/bitswap φτιάχνει πολύ μικρές παραλλαγές των αρχικών),
και 2) έχουμε ένα είδος μνήμης όπου θυμόμαστε τα μονοπάτια που περάσαμε, και δεν ξαναπερνάμε από
αυτά για έναν τουλάχιστον αριθμό επαναλήψεων.
Υπάρχουν και άλλες μικροδιαφορές με τον Γενετικό αλγόριθμο, αλλά αυτές οι δύο είναι οι κύριες. Θα
γράψουμε τώρα κάποιες συναρτήσεις που θα χρησιμοποιήσουμε, και που μας επιτρέπουν:
In [78]:
def eucl_dist(x,y):
summation = 0
for dim in np.arange(len(x)):
summation += (x[dim] - y[dim])**2
return np.sqrt(summation)
In [11]:
def path_length(array):
length = 0
for i in np.arange(array.shape[0]-1):
length += eucl_dist(array[i],array[i+1])
length += eucl_dist(array[-1],array[0])
return length
http://localhost:8888/lab 2/13
31/5/2022 8_Metaheuristics_TSP
In [15]:
indices = np.arange(points.shape[0])
np.random.shuffle(indices)
print(path_length(points[indices]))
plt.plot(points[indices,0],points[indices,1]);
52.955720876862955
In [16]:
In [17]:
http://localhost:8888/lab 3/13
31/5/2022 8_Metaheuristics_TSP
1. αρχικοποιούμε ένα τυχαίο μονοπάτι και το βάζουμε στην λίστα ταμπού. Το αποθηκεύουμε επίσης στην
μεταβλητή με το καλύτερο μονοπάτι που βρήκαμε έως τώρα, και στην μεταβλητή με το μονοπάτι
υποψήφιο να αντικαταστήσει το καλύτερο μονοπάτι.
2. Χρησιμοποιώντας την συνάρτηση bitswap που ορίσαμε στο μάθημα με τους γενετικούς αλγορίθμους,
φτιάχνουμε έναν αριθμό παραλλαγών του βέλτιστου μονοπατιού.
3. Όποια μονοπάτια-παραλλαγές βρίσκονται στην λίστα ταμπού, δηλαδή τα έχουμε ήδη εξετάσει
πρόσφατα, τα προσπερνάμε χωρίς να τα ξανα-εξετάσουμε.
4. Απ'τα εναπομείμαντα μονοπάτια, αν το καλύτερο από αυτά είναι καλύτερο (συντομότερο) απ'το
μονοπάτι-υποψηφίου, το αντικαθιστάμε.
5. Αν το μονοπάτι-υποψήφιος είναι καλύτερο απ'το βέλτιστο μέχρι τώρα μονοπάτι, τότε ο υποψήφιος γίνεται
τώρα το βέλτιστο μονοπάτι.
6. Εξετάζουμε αν ο αριθμός μονοπατιών στην λίστα ταμπού ξεπαερνάει έναν προκαθορισμένο αριθμό
στοιχείων. Αν ναι, τότε αφαιρούμε το πιο παλιό μονοπάτι της λίστας, δηλαδή "ξεχνάμε" ότι έχουμε
περάσει από εκεί, και τώρα μπορούμε, αν τύχει, να ξαναπεράσουμε.
7. Επιστροφή στο βήμα 2. Loop για προκαθορισμένο αριθμό επαναλήψεων.
8. Το καλύτερο μονοπάτι που βρήκε ο αλγόριθμος βρίσκεται αποθηκευμένο στην μεταβλητή με το βέλτιστο
μονοπάτι.
http://localhost:8888/lab 4/13
31/5/2022 8_Metaheuristics_TSP
In [77]:
best_path = np.arange(points.shape[0])
np.random.shuffle(best_path)
best_distance = path_length(points[best_path])
candidate = best_path.copy()
candidate_dist = best_distance.copy()
tabu = []
tabu.append(str(best_path).replace(' ','')[1:-1])
for i in tqdm(range(n_iter)):
#select candidate
neighborhood = get_neighbors(best_path,neighborhood_size)
for neighbor in neighborhood:
neighbor_dist = path_length(points[neighbor])
if (str(neighbor).replace(' ','')[1:-1] in tabu) == False:
if neighbor_dist < candidate_dist:
candidate = neighbor.copy()
candidate_dist = neighbor_dist.copy()
break
return best_path
http://localhost:8888/lab 5/13
31/5/2022 8_Metaheuristics_TSP
In [76]:
path = tabu_salesman(points,
max_tabu_size = 170,
n_iter = 400,
neighborhood_size = 14)
plt.plot(points[path,0],
points[path,1]);
Ο δεύτερος αλγόριθμος που θα δούμε είναι ο Ant Colony Optimization. Αρχικά, προκειμένου να κάνουμε
αυτό το τμήμα του notebook αυτόνομο, ας επαναεισάγουμε τις βιβλιοθήκες και τα δεδομένα, και ας
ξαναορίσουμε τις συναρτήσεις υπολογισμού ευκλείδιας απόστασης και συνολικής απόστασης μονοπατιών.
In [2]:
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from tqdm import tqdm
#np.random.seed(33)
points = np.array([[2.12,0.7],[1.68,0.97],[1.28,1.5],[1.16,2.3],[1.12,4],
[1.2,5.4],[1.32,7.2],[1.64,8.4],[1.92,8.6],[2.2,8],
[2.52,7.6],[2.72,6.4],[2.8,4.8],[2.72,3.3],[2.56,2.3],[2.4,1.2]])
http://localhost:8888/lab 6/13
31/5/2022 8_Metaheuristics_TSP
In [3]:
def eucl_dist(x,y):
summation = 0
for dim in np.arange(len(x)):
summation += (x[dim] - y[dim])**2
return np.sqrt(summation)
In [4]:
def path_length(array):
length = 0
for i in np.arange(array.shape[0]-1):
length += eucl_dist(array[i],array[i+1])
length += eucl_dist(array[-1],array[0])
return length
Καθώς τα μυρμήγκια διασχίζουν τον χώρο, αφήνουν πίσω τους ένα ίχνος φερομόνης. Αν κάποιο άλλο
μυρμήγκι μυρίσει αυτές τις φερομόνες, αυξάνονται κατά κάποιο ποσοστό οι πιθανότητες να ακολουθήσει αυτό
το μονοπάτι και όχι κάποιο άλλο. Όσο περισσότερα μυρμήγκια περνάνε από ένα μονοπάτι, τόσο αυξάνονται
οι φερομόνες του μονοπατιού, και τόσο αυξάνονται οι πιθανότητες να ακολουθήσουν και άλλα μυρμήγκια, τα
οποία με τη σειρά τους θα αφήσουν τις δικές τους φερομόνες, αυξάνοντας τις πιθανότητες του μονοπατιού
επιπλέον.
Αυτός είναι ο λόγος που βλέπουμε τα μυρμήγκια να πηγαινοέρχονται σε μια συγκεκριμένη γραμμή, και αυτή
είναι μια λειτουργία που εξομειώνει ο αλγόριθμος ACO.
Συνοπτικά ο αλγόριθμος δουλεύει ως εξής. Κάθε μυρμήγκι βρίσκεται σε ένα τυχαίο σημείο. Το ποιο σημείο θα
επιλέξει ως το επόμενο στο μονοπάτι του εξαρτάται από τέσσερις παράγοντες:
Παρακάτω φτιάχνουμε μια συνάρτηση που να υπολογίζει έναν πίνακα αποστάσεων από κάθε σημείο σε όλα
τα άλλα. Ο αλγόριθμος θα συμβουλεύεται αυτόν τον πίνακα προεκιμένου να προσδιορίσει τον πρώτο από
τους τέσσερις παράγοντες που είδαμε πιο πάνω, δηλαδή για κάθε σημείο όπου θα βρισκόμαστε θα θέλουμε
να ξέρουμε πιο είναι το πλησιέστερο σημείο.
In [5]:
def distance_matrix(points):
dim = points.shape[0]
dist_mat = np.zeros((dim,dim))
for i, pi in enumerate(points):
for k, ki in enumerate(points):
dist_mat[i,k] = eucl_dist(pi,ki)
return dist_mat
http://localhost:8888/lab 7/13
31/5/2022 8_Metaheuristics_TSP
Σχηματίζουμε πίνακα αποστάσεων των σημείων-δεδομένων της παρούσας άσκησης, και σχεδιάζουμε ένα
heatmap του πίνακα για να δούμε πώς μοιάζει.
Η μαύρη διαγώνιος σημαίνει ότι κάθε σημείο απέχει μηδενική απόσταση από τον εαυτό του.
Το σχήμα σταυρού που σχηματίζουν οι φωτεινές περιοχές σημαίνουν ότι, επειδή τα σημεία μας σχηματίζουν
κυκλική διαδρομή, κάθε στοιχείο έχει ως κοντινότερο το διπλανό του, αμέσως κοντινότερο το διπλανό αυτού,
κτλ, μέχρι την απέναντι πλευρά του κύκλου, όπου έχουμε το μακρινότερο σημείο, και μετά ξαναεπιστρέφουμε,
σημείο προς σημείο, στην αρχική μας θέση. Έτσι στο heatmap, σε οποιοδήποτε μαύρο σημείο της διαγωνίου
κοιτάξεις, όσο πας προς τα δεξιά αρχίζει να γίνεται φωτεινότερο, κάποια στιγμή γίνεται το πιο φωτεινό από
όλα, δηλαδή φθάσαμε στην απέναντι πλευρά του κύκλου, και έπειτα αρχίζουν να σκουραίνουν καθώς
γυρνάμε πίσω στο αρχικό σημείο.
In [10]:
#dist_map demo
import seaborn as sns
dist_map = distance_matrix(points)
sns.heatmap(dist_map);
Πριν ξεκινήσουμε τον αλγόριθμο, μια άλλη δομή δεδομένων που θα χρειαστούμε είναι ο λεγόμενος "πίνακας
ήτα", ο οποίος βρίσκεται διαιρώντας κάθε στοιχείο του πίνακα αποστάσεων στο ένα και υψώνοντας σε μια
δύναμη $α$, με $α$ να είναι παράμετρος που ρυθμίζουμε εμείς.
Στην περίπτωσή μας $α=1$ οπότε, όπως βλέπουμε στο heatmap, ο πίνακας ήτα είναι αρνητική έκδοση του
πίνακα αποστάσεων, δηλαδή είναι το ίδιο σχέδιο με αντεστραμμένα χρώματα.
http://localhost:8888/lab 8/13
31/5/2022 8_Metaheuristics_TSP
In [15]:
dist_mat = distance_matrix(points)
a = 1
ita_mat = (1/dist_mat)**a
# fix the diagonal, it has value 'inf' because of divide by zero
for i in np.arange(dist_mat.shape[0]):
for j in np.arange(dist_mat.shape[1]):
if i==j:ita_mat[i,j] = 0
C:\Users\stefanos\.conda\envs\three-seven\lib\site-packages\ipykernel_laun
cher.py:3: RuntimeWarning: divide by zero encountered in true_divide
This is separate from the ipykernel package so we can avoid doing import
s until
To κάθε μυρμήγκι θα είναι ένα object της κλάσης Ant, και θα έχει τα εξής δεδομένα αποθηκευμένα και
συνδεδεμένα με την μεταβλητή στην οποία το ορίσαμε:
n_locations: ο αριθμός των κόμβων-σημείων στο εκάστοτε πρόβλημα που θέλουμε να λύσουμε
position: σε ποιο κόμβο-σημείο βρίσκεται το μυρμήγκι αυτή την στιγμή
places_visited: λίστα με τα σημεία από όπου έχει περάσει το μυρμήγκι
places_left: λίστα με τα σημεία από όπου δεν έχει περάσει ακόμα
phero_graph: πίνακας με τις φερομόνες που συνδέουν κάθε πιθανό ζευγάρι σημείων του δικτύου
travel_probas: λίστα με τις πιθανότητες για κάθε σημείο που έχει περάσει ακόμα το μυρμήγκι
tour_cost: το συνολικό μήκος της διαδρομής που έχει διανύσει το μυρμήγκι μέχρι αυτή την στιγμή
Σε κάθε επανάληψη εκτελείται η συνάρτηση ant_trip για κάθε μυρμήγκι ώσπου αυτό να εκτελέσει μια
ολοκληρωμένη διαδρομή, και η συνάρτηση ant_flush για να ξανα-αρχικοποιηθούν τα δεδομένα της
μεταβλητής τους, ώστε να είναι έτοιμα για την επόμενη επανάληψη.
http://localhost:8888/lab 9/13
31/5/2022 8_Metaheuristics_TSP
In [6]:
class Ant:
def __init__(self,n_locations):
self.n_locations = n_locations
self.position = np.random.choice(n_locations)
self.places_visited = [self.position]
self.places_left = list(np.arange(n_locations))
self.places_left.remove(self.position)
self.phero_graph = np.full((n_locations,n_locations),0.0)
self.travel_probas = np.zeros(n_locations-1)
self.tour_cost = 0.0
def ant_trip(self,g_phero_graph,dist_mat,ita_mat,a=1,b=1,Q=1):
for i in np.arange(len(self.places_left)):
#------------------------------------------
#determine probabilities for next move
#we will calculate numerators and denominators for proba-calculating fracti
ons
allowed_weights = []
for loc in self.places_left:
allowed_weights.append((g_phero_graph[self.position,loc]**a)*(ita_mat[s
elf.position,loc]**b)) #vector with numerators for each allowable moves
allowed_weights_sum = np.sum(allowed_weights)#this is the denominator of th
e proba-calculating fraction
travel_probas = allowed_weights/allowed_weights_sum
#------------------------------------------
#stochastically pick next destination, update everything that needs updatin
g
next_destination = np.random.choice(self.places_left,p=travel_probas)
def ant_flush(self):
self.position = np.random.choice(self.n_locations)
self.places_visited = [self.position]
self.places_left = list(np.arange(self.n_locations))
self.places_left.remove(self.position)
#self.phero_graph = np.full((n_locations,n_locations),0.0)
self.travel_probas = np.zeros(self.n_locations-1)
http://localhost:8888/lab 10/13
31/5/2022 8_Metaheuristics_TSP
self.tour_cost = 0.0
Είπαμε ότι οι πιθανότητες για το ποιο θα είναι το επόμενο σημείο στην διαδρομή ενός μυρμηγκιού
εξαρτώνται, εντός άλλων παραγόντων, και από τις φερομόνες που είναι συνδεδεμένες με κάθε πέρασμα από
ένα σημείο σε ένα άλλο του μονοπατιού. Αυτές οι φερομόνες είναι αποθηκευμένες σε ένα πίνακα, ο οποίος
θα ανανεώνεται κάθε φορά που ένα μυρμήγκι ολοκληρώνει μια διαδρομή. Η ανανέωση αυτή παίρνει υπόψιν:
In [7]:
def update_pheromones(g_phero_graph,ants,evapo_coef=0.05):
one_minus = 1 - evapo_coef
dim = g_phero_graph.shape[0]
for i in range(dim):
for j in range(dim):
g_phero_graph[i,j] = one_minus*g_phero_graph[i,j] + np.sum([ant.phero_graph
[i,j] for ant in ants])
return g_phero_graph
Γράφουμε τώρα τον κύριο αλγόριθμο σε μορφή συνάρτησης, και κατόπιν τον τρέχουμε. Η συνάρτηση παίρνει
τα εξής ορίσματα:
points: τα σημεία συντεταγμένων [x,y] μεταξύ των οποίων καλούμεστε να βρούμε βέλτιστη διαδρομή
a: πόσο μεγάλο ρόλο θα παίζουν οι φερομόνες στην επιλογή της διαδρομής,δηλαδή μεγαλύτερο a
σημαίνει πως κοιτάμε ολοένα και περισσότερο το τί διαδρομή κάναν τα άλλα μυρμήγκια
b: πόσο μεγάλο ρόλο θα παίζει ο πίνακας ήτα στην επιλογή της διαδρομής, δηλαδή μεγαλύτερο b
σημαίνει πιθανότερο να πάει το μυρμήγκι στο κοντινότερο σημείο από όπου βρίσκεται
evapo_coef: συντελεστής εξάτμισης φερομόνης, κάτι σαν μνήμη του συστήματος φερομονών
Q: παράμετρος που σχετίζεται με την δύναμη των φερομονών
colony_size: αριθμός μυρμηγκιών που θα χρησιμοποιηθούν
n_iter: πόσες επαναλήψεις θα τρέξουν
Σε κάθε επανάληψη υπολογίζουμε και αποθηκεύουμε το καλύτερο μονοπάτι, το οποίο και επιστρέφεται από
την συνάρτηση στο πέρας της εκτέλεσης του κώδικα.
http://localhost:8888/lab 11/13
31/5/2022 8_Metaheuristics_TSP
In [16]:
def aco(points,a,b,evapo_coef,Q,colony_size,n_iter):
n_locations = points.shape[0]
dist_mat = distance_matrix(points)
path_lengths = [] #to store length of each ant's path, later pick best
ita_mat = (1/dist_mat)**a
# to fix the diagonal, it has value 'inf' because of divide by zero
for i in np.arange(dist_mat.shape[0]):
for j in np.arange(dist_mat.shape[1]):
if i==j:ita_mat[i,j] = 0
for i in tqdm(range(n_iter)):
return best_path,monitor_costs
http://localhost:8888/lab 12/13
31/5/2022 8_Metaheuristics_TSP
In [23]:
plt.figure(figsize=(20,5));
plt.subplot(1,2,1);
plt.plot(points[best_path,0],
points[best_path,1]);
plt.subplot(1,2,2);
plt.plot(np.arange(len(monitor_costs)),monitor_costs);
14.8703780408434
http://localhost:8888/lab 13/13