Metaheuristics
Metaheuristics are high-level heuristics that aim to guide
other heuristics toward better solutions. They are often used to solve
large-scale, complex optimization problems where the search space is too vast
to be searched exhaustively. Metaheuristics typically work by iteratively
improving a candidate solution, trying to escape local optima and find a global
optimum. Some examples of metaheuristics include simulated annealing, tabu
search, and evolutionary algorithms.
Metaheuristics
●
A metaheuristic is a master strategy used to guide
a simpler heuristic search for a solution
○
For example, a simple 2-opt heuristic can be
greatly improved by adapting it using simulated annealing or tabu search.
○
Further metaheuristics used to escape local
optima are
■ Variable
neighbourhood search
■ Guided
local search
■ Iterated
local search
Evolutionary algorithms (EA) are another type of
metaheuristic. These algorithms are inspired by the process of natural
evolution and use mechanisms such as reproduction, mutation, and selection to
search for solutions to problems. EAs are commonly used to solve optimization
problems and are particularly useful for problems with a large search space and
no known algorithm for finding an optimal solution. Examples of evolutionary
algorithms include genetic algorithms, differential evolution, and particle
swarm optimization.
Construction metaheuristics
Construction
metaheuristics are methods that iteratively build up a solution to a problem,
usually starting from a partially constructed or empty solution and adding new
elements until a complete solution is obtained. Some examples of construction
metaheuristics include:
● Greedy algorithms: These algorithms make the
locally optimal choice at each step, hoping that these choices will lead to a
globally optimal solution.
● Genetic algorithms: These algorithms use
principles of natural evolution, such as selection, crossover, and mutation, to
generate new solutions and improve upon existing ones.
● Ant colony optimization: This algorithm
simulates the behavior of ants searching for food, using pheromone trails left
by other ants to guide their search.
Construction
metaheuristics are often used to solve problems in which the solution can be
built up incrementally, such as scheduling or routing problems. They are
typically less effective at solving optimization problems that require a
significant amount of modification to an existing solution in order to improve
it.
We can use metaheuristics to adapt greedy, construction
algorithms as well as perturbation searches.
One example of a construction metaheuristic is the greedy
randomized adaptive search procedure (GRASP). This algorithm starts by
constructing a solution using a greedy approach, but then introduces randomness
to escape local optima. At each step of the construction process, a set of
candidates is created and a random candidate is chosen to be included in the
solution. The algorithm then continues the construction process until a
complete solution is formed. GRASP can be used to solve a variety of
optimization problems, including the knapsack problem and the traveling
salesman problem.
Let’s take an adaptation of the nearest neighbour algorithm
for the TSP.
The nearest neighbour algorithm is:
●
Start at a given node
●
Find the node nearest to it and add to the tour
●
Then keep picking the nearest unused node and
add until there are no more nodes left.
This can produce some pretty bad results - but we can
improve it using a metaheuristic...
Here is an
example of how we can use the metaheuristic of simulated annealing to improve
the nearest neighbor algorithm for the TSP:
import random
def calc_distance(tour):
# Calculate the
total distance of a given tour
distance = 0
for i in range(len(tour)
- 1):
distance +=
distance_matrix[tour[i]][tour[i + 1]]
return distance
def simulated_annealing(tour, temperature, cooling_rate):
# Improve the given
tour using simulated annealing
while temperature
> 0.1:
# Pick two random
nodes to swap
i =
random.randint(0, len(tour) - 1)
j =
random.randint(0, len(tour) - 1)
if i == j:
continue
# Calculate the
current and new distances
current_distance
= calc_distance(tour)
new_distance =
calc_distance(tour[:i] + tour[i:j + 1][::-1] + tour[j + 1:])
delta =
new_distance - current_distance
# If the new tour
is better, accept it
if delta < 0:
tour = tour[:i]
+ tour[i:j + 1][::-1] + tour[j + 1:]
else:
# If the new tour
is worse, accept it with a probability
# proportional
to the current temperature
if
random.random() < 2.71828 ** (-delta / temperature):
tour =
tour[:i] + tour[i:j + 1][::-1] + tour[j + 1:]
# Decrease the
temperature
temperature *= 1
- cooling_rate
return tour
# Initialize variables
nodes = [0, 1, 2, 3, 4]
distance_matrix = [[0, 10, 15, 20, 25],
[10, 0, 35, 25, 30],
[15, 35, 0, 30, 15],
[20, 25, 30, 0, 40],
[25, 30, 15, 40, 0]]
tour = [0, 1, 2, 3, 4]
# Run the simulated annealing algorithm
tour = simulated_annealing(tour, 100, 0.1)
# Print the final tour and distance
print("Final tour:", tour)
print("Distance:", calc_distance(tour))
This code first defines a function calc_distance that
calculates the total distance of a given tour. Then it defines the simulated_annealing function,
which takes in a tour, a starting temperature, and a cooling rate. It repeatedly
swaps two random nodes in the tour and calculates the new distance. If the new
tour has a shorter distance, it is always accepted. If the new tour has a
longer distance, it is accepted with a probability proportional to the current
temperature. The temperature is decreased at each iteration by the cooling
rate. Finally, the code initializes some variables and runs the simulated_annealing function to
improve the initial tour.
GRASP
GRASP (Greedy
Randomized Adaptive Search Procedure) is a metaheuristic for combinatorial
optimization problems that combines the benefits of greediness and
randomization. It works by iteratively constructing a solution by greedily
selecting the locally best available option and then randomly perturbing the
solution to escape local optima.
The algorithm works
as follows:
1.
Initialize
an empty solution
2.
While
the solution is not complete:
3.
Select
the best available option using a greedy heuristic
4.
Randomly
perturb the solution by making a small number of changes
5.
Return
the final solution
The key idea behind
GRASP is to use the greediness of a local search algorithm to quickly find a
good solution, while using the randomization to escape local optima and improve
the overall quality of the solution.
The GRASP, or Greedy Random Adaptive Search Procedure
metaheuristic takes a greedy algorithm, randomises its choices from a Reduced
Candidate List (RCL) and repeats.
●
Repeat until no improvement found in k passes
○
Start at a random node
○
While there are unused nodes
■ Select
the nearest c neighbours from the tour’s endpoints
■ Randomly
pick one and add to the tour
○
(optional) do 2-opt search on resulting tour
We can perform a much more intensive search using this
method, and keep getting different results each time.
Task
Try to produce a GRASP algorithm for the vertex cover
problem by adapting one of the greedy algorithms.
To create a GRASP algorithm for the vertex cover problem, we
can start by adapting the greedy algorithm that iteratively adds the vertex
with the highest degree to the cover until all edges are covered.
Here is an example of how this could be implemented in
Python:
def grasp_vertex_cover(G):
# G is the input graph in the form of a list of edges
# Initialize an empty vertex cover
vertex_cover = set()
# Repeat the following process a number of times (we can experiment with
different values for the number of iterations)
for i in range(10):
# Create a list of the remaining vertices that are not yet in the cover
remaining_vertices = set(G.keys()) - vertex_cover
# Initialize an empty candidate set for the current iteration
candidate_set = set()
# While there are still remaining vertices
while len(remaining_vertices) > 0:
# Find the vertex with the highest degree
highest_degree_vertex = max(remaining_vertices, key=lambda v: len(G[v]))
# Add the highest degree vertex to the candidate set and remove it from
the list of remaining vertices
candidate_set.add(highest_degree_vertex)
remaining_vertices.remove(highest_degree_vertex)
# Remove all the edges incident to the highest degree vertex from the
graph
for neighbor in G[highest_degree_vertex]:
del G[neighbor][highest_degree_vertex]
# Choose the best solution from the candidate set using a randomization
procedure (we can use a random selection or a greedy selection based on the
vertex degrees)
vertex_cover.update(random.sample(candidate_set, k=1))
# Return the final vertex cover
return vertex_cover
This GRASP algorithm for the vertex cover problem works by
repeating the greedy algorithm a number of times, each time constructing a
candidate set of vertices using the greedy selection strategy. The final vertex
cover is then chosen from the union of all the candidate sets using a
randomization procedure. This allows the algorithm to escape local optima and
potentially find a better overall solution.
Ant Colony Optimisation (ACO)
Ant Colony
Optimisation (ACO) is a heuristic optimization algorithm that simulates the
behavior of ants searching for food. It is often used to find solutions to
combinatorial optimization problems, such as the travelling salesman problem or
the knapsack problem.
ACO works by
maintaining a population of "virtual ants" that iteratively build
solutions to the optimization problem by constructing a path through a graph.
At each step, an ant chooses the next node in its path based on a combination
of the pheromone levels on the edges and the distance between the nodes. As the
ants traverse the graph, they leave behind a trail of pheromones that influence
the choices of the other ants. Over time, the pheromone levels on the edges of
the graph will converge towards the optimal solution, as the ants are more
likely to follow paths with higher pheromone levels.
Here is a simple
example of ACO implemented in Python for solving the travelling salesman
problem:
import random
# Number of cities
n = 20
# Distance matrix
dist = [[0] * n for _ in range(n)]
# Randomly generate distance matrix
for i in range(n):
for j in
range(n):
if i == j:
dist[i][j] = 0
elif dist[i][j]
== 0:
dist[i][j] = dist[j][i] = random.randint(1, 10)
# Number of ants
m = 10
# Number of iterations
t = 100
# pheromone matrix
pheromone = [[1 / (n * n)] * n for _ in range(n)]
# Heuristic information matrix
heuristic = [[1 / dist[i][j] for j in range(n)] for i in
range(n)]
# Evaporation rate
q = 0.5
# Intensity of pheromone deposit
d = 0.1
# Find the shortest path using ACO
best_path = float("inf")
best_tour = []
for _ in range(t):
tours = []
for k in
range(m):
tour =
[random.randint(0, n - 1)]
while
len(tour) < n:
last =
tour[-1]
choices =
[i for i in range(n) if i not in tour]
probs =
[pheromone[last][i] ** q * heuristic[last][i] ** (1 - q) for i in choices]
norm =
sum(probs)
probs =
[p / norm for p in probs]
tour.append(choices[weighted_choice(probs)])
tours.append(tour)
for tour in
tours:
length =
sum([dist[tour[i]][tour[i + 1]] for i in range(-1, n - 1)])
if length
< best_path:
best_path
= length
best_tour
= tour
for i in
range(-1, n - 1):
pheromone[tour[i]][tour[i + 1]] += d / length
pheromone[tour[i + 1]][tour[i]] += d / length
for i in
range(n):
for j in
range(n):
pheromone[i][j] *= 1 - q
print(best_path)
print(best_tour)
This ACO algorithm works by simulating the behavior of ants
searching for food. In this case, each ant is searching for the shortest path
between a set of cities. At each step, the ant chooses the next city to visit
based on the pheromone levels on the edges and the heuristic value of the
cities. Pheromones are a chemical trail left by ants that other ants can
follow, and in this case, the pheromone levels represent the desirability of an
edge. The heuristic value represents the distance between two cities, with
shorter distances being more desirable.
The algorithm runs for a specified number of iterations,
with each iteration consisting of multiple ants building tours. At the end of
each iteration, the pheromone levels on the edges are updated based on the
length of the tours and the amount of pheromone deposited by the ants. The
pheromone update rule used in this example is:
pheromone[i][j] =
(1-rho)*pheromone[i][j] + delta_pheromone[i][j]
Where rho is the pheromone
evaporation coefficient (set to 0.1 in this example) and delta_pheromone[i][j]
is the amount of pheromone deposited on edge (i,j) by the ants. This amount is
proportional to the length of the tour and the quality of the solution, with
shorter and better tours depositing more pheromone.
The probability of an ant
choosing a particular edge is also influenced by the pheromone levels on that
edge. An edge with high pheromone levels will be more attractive to the ants,
while an edge with low pheromone levels will be less attractive. The
probability of an ant choosing edge (i,j) is given by:
probability[i][j] =
(pheromone[i][j]**alpha) * ((1.0/distance[i][j])**beta)
Where alpha and beta are
parameters that control the relative importance of the pheromone and distance
factors. In this example, alpha is set to 1 and beta is set to 2.
The ACO algorithm iterates for a
specified number of iterations, with each iteration consisting of the following
steps:
1.
Initialise
pheromone levels on all edges to a small positive value.
2.
For
each ant:
1.
Initialise
the ant at a random node.
2.
While
the ant is not at the final node:
1.
Choose
the next node based on the probabilities of the available edges.
2.
Update
the ant's tour length.
3.
Deposit
pheromone on the chosen edge.
1.
Update
the best tour length and the corresponding tour if the ant's tour is better.
1.
Update
the pheromone levels on all edges using the pheromone update rule.
2.
Repeat
from step 2 until the number of iterations is reached.
At the end of the
algorithm, the best tour found by the ants is returned as the solution.
ACO is a metaheuristic based on the foraging behaviour of
Argentine ants.
●
It is observed that ant colonies are good at
finding quick routes to food sources on a tree.
●
They achieve this by a process called stigmergy
○
Ants explore the tree randomly but leave
pheromone trails when they return from a food source
○
The pheromone trail is stronger the nearer the
food source is
○
Subsequent ants are still random, but bias their
choices in favour of the branches with strong pheromone trails
○
The good trails get stronger and eventually all
ants converge on the best route - until the food runs out.
○
This has actually been simulated for large scale
network routing and outperforms many standard approaches
ACO for the TSP
We could do a simple adaptation for the TSP as follows:
●
Start with all pheromone levels equal (or zero)
●
While there is no improvement in k
iterations:
○
Send out a number of ants (in parallel ideally)
○
For each ant
■ while
there are unused nodes
● Choose
an edge randomly to join the tour, based on pheromone distribution
■ Do
2-opt or other iterative improvement search (optional)
■ Update
pheromone levels so that highest levels are on edges in shortest tours
Evaporate (remove a percentage of pheromones from all edges)
Population vs Trajectory based techniques
Population based techniques refer to algorithms that
maintain a population of candidate solutions and use some form of selection and
variation to improve the solutions over time. Examples of population based
techniques include evolutionary algorithms and genetic algorithms.
Trajectory based techniques, on the other hand, refer to
algorithms that follow a single candidate solution as it evolves over time.
These algorithms do not maintain a population of solutions and instead only
focus on a single solution at a time. Examples of trajectory based techniques include
simulated annealing and tabu search.
This is our first example of a population based
technique, which works on many candidate solutions at a time and not just one.
Other examples of population based techniques include
genetic algorithms, particle swarm optimization, and ant colony optimization.
These techniques are often inspired by natural processes and involve the
manipulation and evolution of a group of potential solutions.
On the other hand, trajectory based techniques involve the
manipulation of a single solution, which is iteratively improved upon until it
reaches an optimal or satisfactory state. Examples of trajectory based
techniques include local search algorithms such as simulated annealing, tabu
search, and iterated local search. These techniques work by making small
changes to a current solution and evaluating whether it results in an
improvement. If it does, the solution is accepted and the process continues,
otherwise the solution is rejected and the process returns to the previous state.
These are particularly useful as they lend themselves well
to parallel computation.
Genetic algorithms (GAs) are another example of population
based techniques. They work by creating a population of candidate solutions
(often called chromosomes), which are then evolved over time using the
principles of natural selection and genetics.
In each iteration, the fitness of each chromosome is
evaluated and the best performing ones are selected to create a new population
of offspring through processes such as crossover (recombination of genes) and
mutation. This process is repeated until a satisfactory solution is found or a
pre-determined number of iterations have been reached.
GAs are often used to solve optimization problems, but they
can also be applied to problems in machine learning and data mining. They are
particularly useful for problems where the search space is large and the
objective function is complex and non-linear.
Here is an example of a simple genetic algorithm implemented
in Python for solving the knapsack problem:
Initialize population
population = [Individual() for i in
range(POPULATION_SIZE)]
Evaluate population
for individual in population: individual.fitness
= evaluate_fitness(individual.chromosome)
Loop until termination condition is
met
while not
termination_condition_met(): # Select parents parents =
select_parents(population)
# Generate offspring
offspring = []
for i in range(0, len(parents), 2):
offspring += crossover(parents[i], parents[i+1])
# Mutate offspring
for i in offspring:
mutate(i)
# Evaluate offspring
for individual in offspring:
individual.fitness = evaluate_fitness(individual.chromosome)
# Replace population with offspring
population = offspring
Return the best individual in the
final population
best_individual = sorted(population,
key=lambda x: x.fitness, reverse=True)[0] return best_individual
Genetic Algorithms and Evolutionary Computing
Genetic algorithms are a type of optimization algorithm that
are inspired by the process of natural selection and evolution in biology. They
are used to find solutions to optimization problems by simulating the process
of evolution in a population of candidate solutions.
The basic idea behind genetic algorithms is to start with a
population of candidate solutions, known as "chromosomes," and apply
a set of rules to generate a new population of chromosomes for each iteration
of the algorithm. These rules are inspired by the mechanisms of natural
evolution, such as reproduction, mutation, and natural selection.
In each iteration, the genetic algorithm selects the
best-performing chromosomes from the current population and reproduces them to
create a new population of offspring. It also introduces random mutations to
the offspring to add diversity to the population and prevent it from getting
stuck in a local minimum. Finally, it applies a selection process to choose the
fittest chromosomes to survive and carry on to the next generation.
The process is repeated until the algorithm reaches a
satisfactory level of performance or a predefined number of iterations has been
reached. At this point, the best-performing chromosome in the final population
is returned as the solution to the optimization problem.
Genetic algorithms are widely used in various fields,
including machine learning, data analysis, and engineering, to find solutions
to a wide range of optimization problems. They are particularly useful for
problems that have a large search space and are difficult to solve using
traditional optimization techniques.
Genetic algorithms are similar to local search but with two
main differences
●
GAs are population based so operate on many
solutions at a time
●
They allow for crossover and recombination
of candidate solutions as well as mutation
○
Our algorithms up until now have allowed only
mutation, applying a small alteration to a single solution
○
Crossover and recombination allow the creation
of new solutions by combining characteristics of existing ones (think parents
and children here)
○
Genetic algorithms traditionally operated only
on solutions represented by bitstrings, a similar process using a more complex
solution (eg. containing integers) is called an evolutionary algorithm.
○
Many GAs only involve mutation as crossover is
tricky and often breaks feasibility
GAs
|
Make
changes to current population to create a new generation (crossover/mutation) |
|
Evaluate
new generation |
|
Discard
weak solutions |
|
Initial
population |
Ridiculous metaphor
One way to think about genetic algorithms is to imagine that
you are trying to climb a very tall mountain. You are not sure exactly where
the peak is, but you have a pretty good idea of the general direction. You also
know that the mountain is very steep, and there are a lot of cliffs and other
obstacles that will make it difficult to reach the top.
To climb the mountain, you have a group of people (called
the population). Each person has a unique set of traits that will help them
navigate the mountain and reach the peak. These traits might include things
like strength, agility, endurance, and so on.
As the group begins to climb, some people will naturally do
better than others. The stronger and more agile people will be able to climb
faster and more efficiently, while the weaker and less agile people will
struggle.
Over time, the group will naturally evolve. The stronger and
more agile people will reproduce and pass on their traits to their offspring,
while the weaker and less agile people will either die off or be left behind.
As the group continues to evolve and climb the mountain,
they will eventually reach the peak. At this point, the group will have the
optimal set of traits for navigating the mountain and reaching the top.
This is essentially how genetic algorithms work. The
mountain represents the problem you are trying to solve, and the population
represents the different possible solutions. Through a process of evolution and
natural selection, the population will eventually find the best solution to the
problem.
●
Iterative improvement is like getting a kangaroo
to climb a hill by always going upwards. It very quickly gets stuck on top of a
small hump.
●
Simulated annealing gets the kangaroo drunk first,
so it staggers around with a bit of an upwards bias but eventually sobers up
and starts acting like iterative improvement again.
●
Tabu search allows the kangaroo to go down if
there is no other alternative and gives it an electric shock if it tries to go
back on itself.
●
Genetic algorithms have loads of kangaroos, let
them walk whenever they want but periodically shoot any kangaroos going the
wrong way.
●
Ant colony optimisation has many kangaroos which
walk around randomly but start shrieking as they climb higher. Subsequent
kangaroos follow the noise.

No comments:
Post a Comment