Suppose you are planning a very big event and realize that you have to determine the most efficient way of distributing the workload among the team members. You attempt a couple of approaches but find yourself getting stuck and are unable to move forward. This is where local search algorithms come in. Hill climbing and simulated annealing are some of these tricks that will assist you escape these repetitive problems and develop improved solutions.
In this article, We will discuss about the LS algorithms, where they are used in AI, and how it can make you better problem solver no matter you are in job scheduling or function optimization.
Local search algorithms are intended to solve optimization problems by moving from one solution to the other in the neighborhood. In simple terms, it consists of taking an initial solution and making incremental changes to it to optimize it.
To effectively implement local search algorithms, follow these steps:
Let us now look into some local search algorithms below in detail.
Hill Climbing is a straightforward approach that moves to the neighboring solution with the highest value. Although intuitive, it can get stuck in local optima.
def hill_climbing(initial_solution, objective_function):
current_solution = initial_solution
current_score = objective_function(current_solution)
while True:
neighbors = generate_neighbors(current_solution)
best_neighbor = None
best_neighbor_score = current_score
for neighbor in neighbors:
score = objective_function(neighbor)
if score > best_neighbor_score:
best_neighbor = neighbor
best_neighbor_score = score
if best_neighbor is None:
break
current_solution = best_neighbor
current_score = best_neighbor_score
return current_solution, current_score
def generate_neighbors(solution):
# Example neighbor generation for a simple case
return [solution + 1, solution - 1]
def objective_function(x):
return -x**2 # Example: maximization problem
initial_solution = 0
best_solution, best_score = hill_climbing(initial_solution, objective_function)
print(f"Best solution: {best_solution} with score: {best_score}")
Output:
Best solution: 0 with score: 0
The basis of the Simulated Annealing algorithm is the annealing process referring to metallurgy where the metal is gradually cooled in order to eliminate the presence of defects in its structure. It initializes the temperature to be high, such that the algorithm can traverse more space of solution and then comes down with low temperatures to reduce the time of accepting solution which is worse.
Let focus on the formal problem, such as a traveling salesman problem in which a salesman has to travel through several cities and get back to the starting point in the minimum amount of time. One technique to quickly find a constraint-optimal route is to use simulated annealing. This method sometimes accepts a longer route in hopes of discovering a better overall route.
import random
import math
def objective_function(route):
# Example function: the total distance of the route
return sum(math.sqrt((route[i] - route[i-1])**2) for i in range(len(route)))
def simulated_annealing(initial_route, temperature, cooling_rate):
current_route = initial_route
current_score = objective_function(current_route)
best_route = current_route
best_score = current_score
while temperature > 0.1:
new_route = current_route[:]
i, j = random.sample(range(len(route)), 2)
new_route[i], new_route[j] = new_route[j], new_route[i]
new_score = objective_function(new_route)
if new_score < current_score or random.random() < math.exp((current_score - new_score) / temperature):
current_route = new_route
current_score = new_score
if new_score < best_score:
best_route = new_route
best_score = new_score
temperature *= cooling_rate
return best_route, best_score
# Example usage
route = [0, 1, 2, 3, 4]
best_route, best_score = simulated_annealing(route, 1000, 0.995)
print(f"Best route: {best_route} with score: {best_score}")
Output:
Best route: [0, 1, 2, 3, 4] with score: 8.0
Tabu Search uses memory structures to keep track of recently visited solutions, preventing the algorithm from revisiting them. This helps in avoiding cycles and encourages exploration of new areas of the solution space.
You can employ tabu search in job scheduling problems to allocate jobs to different machines and minimize total completion time by avoiding recently tried job allocations.
import random
def objective_function(schedule):
# Example function: total completion time
return sum(job['duration'] for job in schedule)
def tabu_search(initial_schedule, iterations, tabu_tenure):
current_schedule = initial_schedule
best_schedule = current_schedule
best_score = objective_function(current_schedule)
tabu_list = []
for _ in range(iterations):
neighbors = generate_neighbors(current_schedule)
best_neighbor = None
best_neighbor_score = float('inf')
for neighbor in neighbors:
if neighbor not in tabu_list:
score = objective_function(neighbor)
if score < best_neighbor_score:
best_neighbor = neighbor
best_neighbor_score = score
if best_neighbor:
current_schedule = best_neighbor
tabu_list.append(current_schedule)
if len(tabu_list) > tabu_tenure:
tabu_list.pop(0)
if best_neighbor_score < best_score:
best_schedule = best_neighbor
best_score = best_neighbor_score
return best_schedule, best_score
def generate_neighbors(schedule):
# Generate neighbors by swapping job allocations
neighbors = []
for i in range(len(schedule)):
for j in range(i + 1, len(schedule)):
neighbor = schedule[:]
neighbor[i], neighbor[j] = neighbor[j], neighbor[i]
neighbors.append(neighbor)
return neighbors
# Example usage
schedule = [{'job': 'A', 'duration': 3}, {'job': 'B', 'duration': 2}, {'job': 'C', 'duration': 1}]
best_schedule, best_score = tabu_search(schedule, 100, 5)
print(f"Best schedule: {best_schedule} with score: {best_score}")
Output:
Best schedule: [{'job': 'A', 'duration': 3}, {'job': 'B', 'duration': 2}, {'job': 'C', 'duration': 1}] with score: 6
Many organizations use GA build up solution piece by piece and it’s often choosing the piece that brings the most benefits in the short run. While may not be the best solutions always, they could be powerful in kinds of problems.
In the knapsack problem, if you need to capture as much value as possible within the allowed weight of the bag, you can tackle it by adopting a greedy algorithm. This approach sorts items based on their value-to-weight ratio.
def knapsack_greedy(items, capacity):
items = sorted(items, key=lambda x: x['value'] / x['weight'], reverse=True)
total_value = 0
total_weight = 0
for item in items:
if total_weight + item['weight'] <= capacity:
total_weight += item['weight']
total_value += item['value']
else:
break
return total_value
# Example usage
items = [{'value': 60, 'weight': 10}, {'value': 100, 'weight': 20}, {'value': 120, 'weight': 30}]
capacity = 50
best_value = knapsack_greedy(items, capacity)
print(f"Maximum value in knapsack: {best_value}")
Output:
Maximum value in knapsack: 160
PSO is based on the imitation of birds’ and fishes’ activity. Agents (or particles) roam in the search space of the problems while modifying their positions according to their own learning experiences as well as the learning experiences of their neighbors.
You can apply PSO to function optimization problems, where particles explore the function’s domain and update their positions based on their individual and collective best solutions.
import numpy as np
def objective_function(x):
return sum(x**2)
def particle_swarm_optimization(num_particles, dimensions, iterations):
particles = np.random.rand(num_particles, dimensions)
velocities = np.random.rand(num_particles, dimensions)
personal_best = particles.copy()
global_best = particles[np.argmin([objective_function(p) for p in particles])]
for _ in range(iterations):
for i in range(num_particles):
r1, r2 = np.random.rand(dimensions), np.random.rand(dimensions)
velocities[i] = 0.5 * velocities[i] + 2 * r1 * (personal_best[i] - particles[i]) + 2 * r2 * (global_best - particles[i])
particles[i] += velocities[i]
if objective_function(particles[i]) < objective_function(personal_best[i]):
personal_best[i] = particles[i]
if objective_function(personal_best[i]) < objective_function(global_best):
global_best = personal_best[i]
return global_best, objective_function(global_best)
# Example usage
best_position, best_value = particle_swarm_optimization(30, 5, 100)
print(f"Best position: {best_position} with value: {best_value}")
Output:
Best position: [ 3.35110987e-07 6.94381793e-07 -1.03625781e-06 2.22941746e-06
-9.73259302e-07] with value: 7.585831600413816e-12
The local search algorithms are efficient tools for the decision-making to solve the optimization issues, considering the improvement of the certain neighborhood solutions. That is why introduction to indices even from the aspect of local search is instrumental upon the accomplishment of cognitive Preliminary theorems regardless of the tasks you are likely to encounter – schedule determination, routing or varieties of design problems. If you make a good choice of the algorithm, tune the parameters correctly and check the results, can cope with complex solution of the space and obtain a good or almost the best solution to solve the problem under consideration.
A. Local search algorithms are effective at finding good solutions to optimization problems through iterative improvement, making them suitable for problems where exact solutions are difficult to obtain.
A. You can improve local search algorithms by incorporating techniques like simulated annealing, tabu search, or hybrid approaches to escape local optima and enhance solution quality.
A. Hill climbing can get stuck in local optima and may not explore the entire solution space, which limits its ability to find the global optimum.
A. Simulated annealing allows occasional moves to worse solutions to escape local optima, whereas hill climbing only moves to better solutions.
A. The tabu list in tabu search helps avoid revisiting recently explored solutions, thereby enhancing the search’s ability to explore new areas of the solution space.