Optimizing Rastrigin Function Using GA-PSO Method

You can access pdf clicking in this link.

1. Imports:

import numpy as np
import random

These lines import the necessary libraries. NumPy (imported as np) is a powerful library for numerical computations in Python, particularly useful for working with arrays and matrices. The random module is used for generating random numbers, which is crucial for the stochastic nature of both PSO and GA algorithms.

2. PSO Parameters:

no_of_iterations = 100
no_of_particles = 100
c1 = 1.2
c2 = 1.9
w = 0.5
lower_bound = -5.12
upper_bound = 5.12
no_of_dimension = 7

These variables define the parameters for the Particle Swarm Optimization (PSO) algorithm:

  • no_of_iterations: The number of times the PSO algorithm will update particle positions and velocities.
  • no_of_particles: The number of particles in the swarm.
  • c1 and c2: Cognitive and social learning factors, respectively. They control how much the particle’s movement is influenced by its personal best position and the global best position.
  • w: Inertia weight, which determines how much of the previous velocity is retained in the next iteration.
  • lower_bound and upper_bound: The search space boundaries for each dimension.
  • no_of_dimension: The number of dimensions in the search space.

3. GA Parameters:

ga_population_size = 50
ga_top_n = 20
ga_mutation_rate = 0.1

These variables define the parameters for the Genetic Algorithm (GA):

  • ga_population_size: The number of individuals in each generation.
  • ga_top_n: The number of top solutions to select for breeding the next generation.
  • ga_mutation_rate: The probability of mutation occurring for each gene (dimension) of an individual.

4. Main Function:

def main():
    # Initialize PSO particles and velocities
    particles = np.random.uniform(lower_bound, upper_bound, size=(no_of_particles, no_of_dimension))
    velocities = np.random.uniform(-1, 1, size=(no_of_particles, no_of_dimension))
    p_best = particles.copy()
    p_best_value = np.apply_along_axis(functions, 1, p_best)
    g_best_index = np.argmax(p_best_value)
    g_best = p_best[g_best_index].copy()

    # Step 1: Run PSO
    pso_g_best, pso_p_best = pso(particles, velocities, p_best, g_best)
    print(f"PSO Best Solution: {pso_g_best}, Fitness: {functions(pso_g_best)}")

    # Step 2: Apply GA to the best solutions found by PSO
    ga_population = ga(pso_p_best)

    # Step 3: Combine PSO and GA results
    combined_population = np.vstack((pso_p_best, ga_population))
    final_best_solution, final_fitness_values = rank_solutions(combined_population)
    print(f"Hybrid Best Solution: {final_best_solution[0]}, Fitness: {final_fitness_values[0]}")

if __name__ == "__main__":
    main()

The main function orchestrates the hybrid PSO-GA algorithm:

a. PSO Initialization:

  • particles: Initializes particle positions randomly within the search space.
  • velocities: Initializes particle velocities randomly between -1 and 1.
  • p_best: Initially set to the current particle positions.
  • p_best_value: Calculates the fitness of each particle’s personal best position.
  • g_best: Selects the global best particle based on fitness values.

b. Algorithm Execution:

  • Step 1: Runs the PSO algorithm and prints the best solution found.
  • Step 2: Applies the GA to the best solutions found by PSO.
  • Step 3: Combines the results from PSO and GA, ranks the solutions, and prints the best hybrid solution.

c. Script Execution: The if __name__ == "__main__": block ensures that the main() function is only executed if the script is run directly (not imported as a module).

This initialization and introduction section sets up the foundation for the hybrid PSO-GA algorithm, defining key parameters and outlining the overall structure of the optimization process. It prepares the necessary components for both the PSO and GA parts of the algorithm, which are then implemented in the subsequent sections of the code.

Part 2

  1. PSO Function:
def pso(particles, velocities, p_best, g_best):
    for _ in range(no_of_iterations):
        for i in range(no_of_particles):
            p_best[i] = find_best(particles[i], p_best[i])
            if functions(p_best[i]) > functions(g_best):
                g_best = p_best[i].copy()
            velocities[i] = (w * velocities[i] +
                             c1 * random.random() * (p_best[i] - particles[i]) +
                             c2 * random.random() * (g_best - particles[i]))
            particles[i] += velocities[i]
            particles[i] = np.clip(particles[i], lower_bound, upper_bound)
    return g_best, p_best

This function implements the core PSO algorithm:

  • It iterates for a specified number of iterations (no_of_iterations).
  • For each particle in each iteration:
    1. Updates the particle’s personal best position (p_best[i]).
    2. Updates the global best position (g_best) if the particle’s personal best is better.
    3. Updates the particle’s velocity using the PSO velocity equation.
    4. Updates the particle’s position by adding the velocity.
    5. Clips the particle’s position to ensure it stays within the search space bounds.
  • Finally, it returns the global best solution and all particles’ personal best solutions.
  1. Find Best Function:
def find_best(particle_position, p_best):
    if functions(particle_position) > functions(p_best):
        p_best = particle_position.copy()
    return p_best

This function compares a particle’s current position with its personal best:

  • If the current position has a better fitness value, it becomes the new personal best.
  • It returns the updated personal best position.
  1. Fitness Function:
def functions(x):
    return 10 * len(x) + sum(xi**2 - 10 * np.cos(2 * np.pi * xi) for xi in x)

This is the objective function that the PSO algorithm is trying to optimize:

  • It implements the Rastrigin function, a common benchmark problem in optimization.
  • The function is defined for any number of dimensions.
  • It’s a challenging function with many local optima, making it suitable for testing optimization algorithms.

Part 3:

Key aspects of the PSO implementation:

  1. Velocity Update: The velocity update equation is a crucial part of PSO:

    velocities[i] = (w * velocities[i] +
                     c1 * random.random() * (p_best[i] - particles[i]) +
                     c2 * random.random() * (g_best - particles[i]))
    
    • w * velocities[i]: The inertia component, maintaining the particle’s current direction.
    • c1 * random.random() * (p_best[i] - particles[i]): The cognitive component, pulling the particle towards its personal best position.
    • c2 * random.random() * (g_best - particles[i]): The social component, pulling the particle towards the global best position.
  2. Position Update: After updating the velocity, the particle’s position is updated:

    particles[i] += velocities[i]
    
  3. Boundary Handling: To keep particles within the search space, their positions are clipped:

    particles[i] = np.clip(particles[i], lower_bound, upper_bound)
    
  4. Global Best Update: The global best is updated when a better solution is found:

    if functions(p_best[i]) > functions(g_best):
        g_best = p_best[i].copy()
    

This PSO implementation follows the standard PSO algorithm, with the addition of boundary handling to ensure particles stay within the defined search space. The algorithm balances exploration (searching new areas) and exploitation (refining solutions in promising areas) through its velocity update mechanism and the influence of personal and global best positions.

The PSO section of your code provides a robust implementation of the algorithm, capable of handling multi-dimensional optimization problems. It’s designed to work in conjunction with the GA section to form a hybrid optimization approach.

Part 4

  1. GA Function:
def ga(population):
    ranked_solutions, _ = rank_solutions(population)
    best_solutions = select_best_solutions(ranked_solutions, ga_top_n)
    new_population = create_new_generation(best_solutions, ga_population_size, ga_mutation_rate)
    return new_population

This function implements the main GA process:

  1. It ranks the solutions in the current population based on their fitness.
  2. It selects the best solutions from the ranked population.
  3. It creates a new generation using the best solutions.
  4. It returns the new population.

The GA function encapsulates the core genetic algorithm steps: selection, crossover (implicitly in create_new_generation), and mutation. Let’s break down each component:

  1. Rank Solutions Function:
def rank_solutions(solutions):
    fitness_values = np.apply_along_axis(lambda s: functions(s), 1, solutions)
    ranked_indices = np.argsort(fitness_values)[::-1]  # Descending order for maximization
    return solutions[ranked_indices], fitness_values[ranked_indices]

This function ranks the solutions based on their fitness:

  • It applies the fitness function (functions) to each solution in the population.
  • It sorts the solutions in descending order of fitness (for maximization problems).
  • It returns the sorted solutions and their corresponding fitness values.

The use of np.apply_along_axis is an efficient way to apply the fitness function to each solution in the population. The [::-1] in np.argsort(fitness_values)[::-1] reverses the order to sort in descending order, suitable for maximization problems.

  1. Select Best Solutions Function:
def select_best_solutions(ranked_solutions, top_n):
    return ranked_solutions[:top_n]

This function implements the selection process:

  • It simply returns the top top_n solutions from the ranked solutions.
  • This is a form of elitism, where the best solutions are guaranteed to be selected for the next generation.

The selection process is crucial in GA as it determines which solutions will contribute to the next generation. This implementation uses a straightforward truncation selection, which can be very effective but may lead to a loss of diversity if top_n is too small relative to the population size.

  1. Create New Generation Function:
def create_new_generation(best_solutions, pop_size, mutation_rate):
    new_population = []
    for _ in range(pop_size):
        parent1, parent2 = np.random.choice(len(best_solutions), 2, replace=False)
        child = 0.5 * (best_solutions[parent1] + best_solutions[parent2])
        if random.random() < mutation_rate:
            mutation_vector = np.random.uniform(-1, 1, size=len(child))
            child += mutation_vector
        child = np.clip(child, lower_bound, upper_bound)
        new_population.append(child)
    return np.array(new_population)

Part 5:

This function creates a new generation through crossover and mutation:

a. Crossover:

  • For each new child, two parents are randomly selected from the best solutions.
  • The child is created by taking the average of the two parents: 0.5 * (best_solutions[parent1] + best_solutions[parent2]).
  • This is a form of arithmetic crossover, which works well for real-valued representations.

b. Mutation:

  • Each child has a chance to mutate, determined by the mutation_rate.
  • If mutation occurs, a random vector is added to the child.
  • The mutation vector has values uniformly distributed between -1 and 1.
  • This allows for both small and large mutations, promoting exploration of the search space.

c. Boundary Handling:

  • After crossover and mutation, the child’s values are clipped to ensure they remain within the problem’s bounds.

d. Population Creation:

  • This process is repeated until a new population of size pop_size is created.
  • The new population is returned as a NumPy array for efficient processing.

Key Aspects of the GA Implementation:

  1. Representation: The solutions are represented as real-valued vectors, which is appropriate for the continuous optimization problem at hand (the Rastrigin function).

  2. Selection: The implementation uses elitism by selecting the top ga_top_n solutions. This ensures that the best solutions are always preserved, but it may lead to premature convergence if not balanced with sufficient exploration.

  3. Crossover: The arithmetic crossover (taking the average of two parents) is used. This is a common approach for real-valued GAs, as it allows for the creation of children that are “in between” their parents in the search space.

  4. Mutation: The mutation operator adds a random vector to the child. This allows for both small tweaks (when the random values are close to zero) and larger jumps in the search space (when the random values are closer to -1 or 1). The mutation rate controls how often this occurs.

  5. Boundary Handling: The np.clip function ensures that all solutions remain within the specified bounds of the search space. This is crucial for maintaining valid solutions throughout the evolutionary process.

  6. Diversity: The current implementation doesn’t have explicit diversity preservation mechanisms beyond mutation. In more advanced implementations, techniques like niching or diversity-based selection could be added to maintain population diversity.

  7. Termination: The GA function doesn’t include a termination condition. Instead, it creates one new generation each time it’s called. The number of generations is implicitly controlled by how many times the ga function is called in the main loop.

Potential Improvements and Variations:

  1. Parent Selection: Instead of random selection from the top solutions, you could implement tournament selection or roulette wheel selection, which might better balance exploration and exploitation.

  2. Adaptive Parameters: The mutation rate and the number of top solutions selected could be made adaptive, changing based on the population’s diversity or the improvement rate.

  3. Different Crossover Operators: For real-valued problems, you could experiment with other crossover methods like SBX (Simulated Binary Crossover) or BLX-α (Blend Crossover).

  4. Local Search: You could incorporate a local search step after mutation to fine-tune solutions, creating a memetic algorithm.

  5. Multi-objective Optimization: If dealing with multi-objective problems, you could extend the GA to use Pareto ranking and maintain a set of non-dominated solutions.

Integration with PSO:

In your hybrid approach, the GA is applied to the best solutions found by PSO. This is an interesting way to combine the two algorithms:

  1. PSO first explores the search space and identifies promising regions.
  2. GA then takes these good solutions and attempts to refine them further through the evolutionary process.

This combination leverages PSO’s ability to quickly converge on good solutions with GA’s ability to maintain population diversity and potentially escape local optima.

The GA implementation in your code provides a solid foundation for genetic optimization. It incorporates the key elements of selection, crossover, and mutation in a way that’s well-suited for real-valued optimization problems. When combined with PSO in the hybrid approach, it creates a powerful optimization tool capable of tackling complex, multi-modal problems like the Rastrigin function.

Main Function:

def main():
    # Initialize PSO particles and velocities
    particles = np.random.uniform(lower_bound, upper_bound, size=(no_of_particles, no_of_dimension))
    velocities = np.random.uniform(-1, 1, size=(no_of_particles, no_of_dimension))
    p_best = particles.copy()
    p_best_value = np.apply_along_axis(functions, 1, p_best)
    g_best_index = np.argmax(p_best_value)
    g_best = p_best[g_best_index].copy()

    # Step 1: Run PSO
    pso_g_best, pso_p_best = pso(particles, velocities, p_best, g_best)
    print(f"PSO Best Solution: {pso_g_best}, Fitness: {functions(pso_g_best)}")

    # Step 2: Apply GA to the best solutions found by PSO
    ga_population = ga(pso_p_best)

    # Step 3: Combine PSO and GA results
    combined_population = np.vstack((pso_p_best, ga_population))
    final_best_solution, final_fitness_values = rank_solutions(combined_population)
    print(f"Hybrid Best Solution: {final_best_solution[0]}, Fitness: {final_fitness_values[0]}")

if __name__ == "__main__":
    main()

Let’s break down this hybrid approach step by step:

  1. Initialization: The main function starts by initializing the PSO components:

    • particles: An array of particle positions, randomly initialized within the search space bounds.
    • velocities: An array of particle velocities, randomly initialized between -1 and 1.
    • p_best: Initially set to the current particle positions.
    • p_best_value: The fitness values of the initial particle positions.
    • g_best: The global best position, initially set to the best among the initial particles.

    This initialization sets the stage for the PSO algorithm, creating a diverse initial population of solutions spread across the search space.

  2. Step 1: Running PSO

    pso_g_best, pso_p_best = pso(particles, velocities, p_best, g_best)
    print(f"PSO Best Solution: {pso_g_best}, Fitness: {functions(pso_g_best)}")
    

    The PSO algorithm is executed first. It returns:

    • pso_g_best: The global best solution found by PSO.
    • pso_p_best: The personal best positions of all particles.

    The best solution found by PSO is then printed, giving an intermediate result before the hybrid approach continues.

  3. Step 2: Applying GA

    ga_population = ga(pso_p_best)
    

    The GA is then applied to the personal best positions found by PSO. This is a key aspect of the hybrid approach:

    • Instead of starting with a random population, GA begins with the already good solutions found by PSO.
    • This allows GA to focus on refining these promising solutions rather than exploring the entire search space from scratch.
  4. Step 3: Combining PSO and GA Results

    combined_population = np.vstack((pso_p_best, ga_population))
    final_best_solution, final_fitness_values = rank_solutions(combined_population)
    print(f"Hybrid Best Solution: {final_best_solution[0]}, Fitness: {final_fitness_values[0]}")
    

    The final step combines the results from both algorithms:

    • The personal best solutions from PSO (pso_p_best) and the population produced by GA (ga_population) are combined into a single population.
    • This combined population is then ranked to find the overall best solution.
    • The best solution from this hybrid approach is printed, showing the final result of the optimization process.

Key Aspects of the Hybrid Approach:

  1. Synergy between PSO and GA:

    • PSO is known for its ability to quickly converge on promising areas of the search space.
    • GA is effective at maintaining diversity and can potentially escape local optima through its crossover and mutation operators.
    • By using PSO first and then applying GA to its results, the hybrid approach aims to leverage the strengths of both algorithms.
  2. Information Transfer:

    • The personal best positions from PSO are used as the initial population for GA.
    • This transfer of information allows GA to start from a set of already good solutions, potentially leading to faster convergence to the global optimum.
  3. Exploration vs. Exploitation:

    • PSO provides a good initial exploration of the search space.
    • GA then focuses on exploiting the promising regions found by PSO, while still maintaining some level of exploration through its genetic operators.
  4. Diversity Preservation:

    • While PSO might converge quickly, potentially losing diversity, the subsequent application of GA can reintroduce diversity through its crossover and mutation operations.
  5. Final Evaluation:

    • By combining the results of both algorithms and ranking them together, the approach ensures that the best overall solution is selected, regardless of which algorithm produced it.

Potential Variations and Improvements:

  1. Iterative Hybrid: Instead of running PSO once followed by GA, you could implement an iterative approach where PSO and GA alternate multiple times. This could potentially lead to even better results by repeatedly benefiting from the strengths of both algorithms.

  2. Adaptive Parameter Tuning: The parameters for both PSO and GA (like inertia weight, learning factors, mutation rate) could be made adaptive, changing based on the progress of the optimization process.

  3. Multi-Swarm Approach: Multiple swarms could be used in the PSO phase, each focusing on different regions of the search space. The best solutions from each swarm could then be fed into the GA phase.

  4. Local Search: A local search procedure could be added after the GA phase to further refine the best solutions found.

  5. Parallel Implementation: The hybrid approach lends itself well to parallelization. PSO particles could be evaluated in parallel, and the GA operations could also be parallelized for larger population sizes.

  6. Problem-Specific Modifications: Depending on the specific optimization problem, custom operators or problem-specific knowledge could be incorporated into either the PSO or GA phases.

Execution Flow: The if __name__ == "__main__": block ensures that the main() function is only executed if the script is run directly, not if it’s imported as a module. This is a common Python idiom that allows the same file to be both imported and run as a script.

Conclusion: This hybrid PSO-GA approach represents a sophisticated optimization strategy that aims to combine the strengths of two powerful metaheuristic algorithms. By first using PSO to quickly identify promising regions of the search space and then applying GA to refine and potentially improve upon these solutions, the hybrid method attempts to overcome the limitations of each individual algorithm.

The implementation provided in your code offers a solid foundation for this hybrid approach. It’s flexible enough to be applied to various optimization problems and can serve as a starting point for further enhancements and adaptations to specific problem domains.

The printing of both the PSO best solution and the final hybrid best solution allows for a comparison of the performance, showcasing the potential benefits of the hybrid approach over using PSO alone. This hybrid method represents an interesting direction in the field of optimization, combining global search capabilities with evolutionary refinement to tackle complex, multimodal optimization problems.