228 lines
10 KiB
Python
228 lines
10 KiB
Python
import numpy
|
|
import numpy as np
|
|
|
|
|
|
class GeneticAlgorithm:
|
|
"""Genetic algorithm.
|
|
Implementation of the real-valued Genetic algorithm. The mutations are
|
|
normally distributed perturbations, the selection mechanism is a tournament
|
|
selection, and the crossover oepration is the standard linear combination
|
|
taken at a randomly generated cutting point.
|
|
The total number of evaluations are popsize x ngen
|
|
:param function: Function that can be used to evaluate the entire
|
|
population. It needs to take an input of size pop_size x dim and
|
|
return a numpy.array of size pop_size x 1
|
|
:type function: Object
|
|
:param dim: Number of dimensions
|
|
:type dim: int
|
|
:param lb: Lower variable bounds, of length dim
|
|
:type lb: numpy.array
|
|
:param ub: Lower variable bounds, of length dim
|
|
:type ub: numpy.array
|
|
:param int_var: List of indices with the integer valued variables
|
|
(e.g., [0, 1, 5])
|
|
:type int_var: list
|
|
:param pop_size: Population size
|
|
:type pop_size: int
|
|
:param num_gen: Number of generations
|
|
:type num_gen: int
|
|
:param start: Method for generating the initial population
|
|
:type start: string
|
|
:ivar nvariables: Number of variables (dimensions)
|
|
:ivar nindividuals: population size
|
|
:ivar lower_boundary: lower bounds for the optimization problem
|
|
:ivar upper_boundary: upper bounds for the optimization problem
|
|
:ivar integer_variables: List of variables that are integer valued
|
|
:ivar start: Method for generating the initial population
|
|
:ivar sigma: Perturbation radius. Each pertubation is N(0, sigma)
|
|
:ivar p_mutation: Mutation probability (1/dim)
|
|
:ivar tournament_size: Size of the tournament (5)
|
|
:ivar p_cross: Cross-over probability (0.9)
|
|
:ivar ngenerations: Number of generations
|
|
:ivar function: Object that can be used to evaluate the objective function
|
|
"""
|
|
|
|
def __init__(self, function, dim, lb, ub, int_var=None, pop_size=20, num_gen=300, start="Random"):
|
|
|
|
self.nvariables = dim # column
|
|
self.nindividuals = pop_size + (pop_size % 2) # Make sure this is even row
|
|
self.lower_boundary = np.array(lb)
|
|
self.upper_boundary = np.array(ub)
|
|
self.integer_variables = []
|
|
if int_var is not None:
|
|
self.integer_variables = np.array(int_var)
|
|
self.start = start
|
|
self.sigma = 0.2
|
|
self.p_mutation = 1.0 / dim
|
|
self.tournament_size = 5
|
|
self.p_cross = 0.9
|
|
self.ngenerations = num_gen
|
|
self.function = function
|
|
|
|
def optimize(self):
|
|
"""Method used to run the Genetic algorithm
|
|
:return: Returns the best individual and its function value
|
|
:rtype: numpy.array, float
|
|
"""
|
|
# Initialize population
|
|
if isinstance(self.start, np.ndarray):
|
|
if self.start.shape[0] != self.nindividuals or self.start.shape[1] != self.nvariables:
|
|
raise ValueError("Initial population has incorrect size")
|
|
if any(np.min(self.start, axis=0) < self.lower_boundary) or any(
|
|
np.max(self.start, axis=0) > self.upper_boundary
|
|
):
|
|
raise ValueError("Initial population is outside the domain", self.lower_boundary, self.upper_boundary,
|
|
self.start)
|
|
population = self.start
|
|
elif self.start == "SLHD":
|
|
from pySOT.experimental_design import SymmetricLatinHypercube
|
|
|
|
exp_des = SymmetricLatinHypercube(self.nvariables, self.nindividuals)
|
|
population = self.lower_boundary + exp_des.generate_points() * (self.upper_boundary - self.lower_boundary)
|
|
elif self.start == "LHD":
|
|
from pySOT.experimental_design import LatinHypercube
|
|
|
|
exp_des = LatinHypercube(self.nvariables, self.nindividuals)
|
|
population = self.lower_boundary + exp_des.generate_points() * (self.upper_boundary - self.lower_boundary)
|
|
elif self.start == "Random":
|
|
population = self.lower_boundary + np.random.rand(self.nindividuals, self.nvariables) * (
|
|
self.upper_boundary - self.lower_boundary
|
|
)
|
|
else:
|
|
raise ValueError("Unknown argument for initial population")
|
|
|
|
new_population = []
|
|
# Round positions
|
|
if len(self.integer_variables) > 0: # 对特定列进行操作
|
|
new_population = np.copy(population)
|
|
population[:, self.integer_variables] = np.round(population[:, self.integer_variables])
|
|
for i in self.integer_variables:
|
|
ind = np.where(population[:, i] < self.lower_boundary[i])
|
|
population[ind, i] += 1
|
|
ind = np.where(population[:, i] > self.upper_boundary[i])
|
|
population[ind, i] -= 1
|
|
|
|
# Evaluate all individuals
|
|
# function_values = self.function(population) we cannot compute in this way to ensure x is one-dim in policy
|
|
n_row, n_dim = population.shape
|
|
function_values = []
|
|
for r in range(n_row):
|
|
function_values.append(self.function(population[r, :]))
|
|
function_values = np.array(function_values)
|
|
|
|
if len(function_values.shape) == 2:
|
|
function_values = np.squeeze(np.asarray(function_values))
|
|
|
|
# Save the best individual
|
|
ind = np.argmin(function_values)
|
|
best_individual = np.copy(population[ind, :]) # 找到最优个体
|
|
best_value = function_values[ind]
|
|
|
|
if len(self.integer_variables) > 0:
|
|
population = new_population
|
|
|
|
# Main loop
|
|
for _ in range(self.ngenerations):
|
|
print('------------------------------')
|
|
print("当前为第{}代".format(_))
|
|
print("最优个体为:{}".format(best_individual))
|
|
print("最优值为:{}".format(best_value))
|
|
print("------------------------------")
|
|
# Do tournament selection to select the parents
|
|
competitors = np.random.randint(0, self.nindividuals, (self.nindividuals, self.tournament_size))
|
|
ind = np.argmin(function_values[competitors], axis=1)
|
|
winner_indices = np.zeros(self.nindividuals, dtype=int)
|
|
for i in range(self.tournament_size): # This loop is short
|
|
winner_indices[np.where(ind == i)] = competitors[np.where(ind == i), i]
|
|
|
|
parent1 = population[winner_indices[0: self.nindividuals // 2], :]
|
|
parent2 = population[winner_indices[self.nindividuals // 2: self.nindividuals], :]
|
|
|
|
# Averaging Crossover
|
|
cross = np.where(np.random.rand(self.nindividuals // 2) < self.p_cross)[0]
|
|
nn = len(cross) # Number of crossovers
|
|
alpha = np.random.rand(nn, 1)
|
|
|
|
# Create the new chromosomes
|
|
parent1_new = np.multiply(alpha, parent1[cross, :]) + np.multiply(1 - alpha, parent2[cross, :])
|
|
parent2_new = np.multiply(alpha, parent2[cross, :]) + np.multiply(1 - alpha, parent1[cross, :])
|
|
parent1[cross, :] = parent1_new
|
|
parent2[cross, :] = parent2_new
|
|
population = np.concatenate((parent1, parent2))
|
|
|
|
# Apply mutation
|
|
scale_factors = self.sigma * (self.upper_boundary - self.lower_boundary) # Scale
|
|
perturbation = np.random.randn(self.nindividuals, self.nvariables) # Generate perturbations
|
|
perturbation = np.multiply(perturbation, scale_factors) # Scale accordingly
|
|
perturbation = np.multiply(
|
|
perturbation, (np.random.rand(self.nindividuals, self.nvariables) < self.p_mutation)
|
|
)
|
|
|
|
perturbation = round_vars(perturbation, self.integer_variables, self.lower_boundary, self.upper_boundary)
|
|
population = round_vars(population, self.integer_variables, self.lower_boundary, self.upper_boundary)
|
|
|
|
population += perturbation # Add perturbation
|
|
population = np.maximum(np.reshape(self.lower_boundary, (1, self.nvariables)), population)
|
|
population = np.minimum(np.reshape(self.upper_boundary, (1, self.nvariables)), population)
|
|
|
|
# Round chromosomes
|
|
new_population = []
|
|
if len(self.integer_variables) > 0:
|
|
new_population = np.copy(population)
|
|
population = round_vars(population, self.integer_variables, self.lower_boundary, self.upper_boundary)
|
|
|
|
# Keep the best individual
|
|
population[0, :] = best_individual
|
|
|
|
# Evaluate all individuals
|
|
# function_values = self.function(population) we cannot compute in this way to ensure x is one-dim in policy
|
|
n_row, n_dim = population.shape
|
|
function_values = []
|
|
for r in range(n_row):
|
|
function_values.append(self.function(population[r, :]))
|
|
function_values = np.array(function_values)
|
|
|
|
if len(function_values.shape) == 2:
|
|
function_values = np.squeeze(np.asarray(function_values))
|
|
|
|
# Save the best individual
|
|
ind = np.argmin(function_values)
|
|
best_individual = np.copy(population[ind, :])
|
|
best_value = function_values[ind]
|
|
|
|
# Use the positions that are not rounded
|
|
if len(self.integer_variables) > 0:
|
|
population = new_population
|
|
|
|
# return best_individual, best_value
|
|
|
|
|
|
def round_vars(x: np.ndarray, int_var, lb, ub):
|
|
"""Round integer variables to closest integer in the domain.
|
|
:param x: Set of points, of size npts x dim
|
|
:type x: numpy.array
|
|
:param int_var: Set of indices of integer variables
|
|
:type int_var: numpy.array
|
|
:param lb: Lower bounds, of size 1 x dim
|
|
:type lb: numpy.array
|
|
:param ub: Upper bounds, of size 1 x dim
|
|
:type ub: numpy.array
|
|
:return: The set of points with the integer variables
|
|
rounded to the closest integer in the domain
|
|
:rtype: numpy.array
|
|
"""
|
|
# Make sure we don't violate the bound constraints
|
|
for i in int_var:
|
|
ind = np.where(x[:, i] < lb[i])
|
|
x[ind, i] = lb[i]
|
|
ind = np.where(x[:, i] > ub[i])
|
|
x[ind, i] = ub[i]
|
|
|
|
if len(int_var) > 0:
|
|
# Round the original ranged integer variables
|
|
x[:, int_var] = np.round(x[:, int_var])
|
|
x = x.astype(numpy.int32, copy=True)
|
|
else:
|
|
x = x.astype(numpy.float64, copy=True)
|
|
return x
|