Source code for ga.class_individuo

################################################################################
#               CÓDIGO OPTIMIZACIÓN CON ALGORITMO GENÉTICO                     #
#                                                                              #
# This work by Joaquín Amat Rodrigo is licensed under a Creative Commons       #
# Attribution 4.0 International License.                                       #
################################################################################
# coding=utf-8

################################################################################
#                              CLASE INDIVIDUO                                 #
################################################################################

[docs]class Individuo: """ Esta clase representa un individuo con unas características inicial definida por una combinación de valores numéricos aleatorios. El rango de posibles valores para cada variable puede estar acotado. Parameters ---------- n_variables : `int` número de variables que definen al individuo. limites_inf : `list` or `numpy.ndarray`, optional límite inferior de cada variable. Si solo se quiere predefinir límites de alguna variable, emplear ``None``. Los ``None`` serán remplazados por el valor (-10**3). (default ``None``) limites_sup : `list` or `numpy.ndarray`, optional límite superior de cada variable. Si solo se quiere predefinir límites de alguna variable, emplear ``None``. Los ``None`` serán remplazados por el valor (+10**3). (default ``None``) verbose : `bool`, optional mostrar información del individuo creado. (default ``False``) Attributes ---------- n_variables : `int` número de variables que definen al individuo. limites_inf : `list` or `numpy.ndarray` límite inferior de cada variable. Si solo se quiere predefinir límites de alguna variable, emplear ``None``. Los ``None`` serán remplazados por el valor (-10**3). limites_sup : `list` or `numpy.ndarray` límite superior de cada variable. Si solo se quiere predefinir límites de alguna variable, emplear ``None``. Los ``None`` serán remplazados por el valor (+10**3). valor_variables : `numpy.ndarray` array con el valor de cada una de las variables. fitness : `float` valor de fitness del individuo. valor_funcion : `float` valor de la función objetivo para el individuo. Raises ------ raise Exception si `limites_inf` es distinto de None y su longitud no coincide con `n_variables`. raise Exception si `limites_sup` es distinto de None y su longitud no coincide con `n_variables`. Examples -------- Ejemplo creación individuo. >>> individuo = Individuo( n_variables = 3, limites_inf = [-1,2,0], limites_sup = [4,10,20], verbose = True ) """ def __init__(self, n_variables, limites_inf=None, limites_sup=None, verbose=False): # Número de variables del individuo self.n_variables = n_variables # Límite inferior de cada variable self.limites_inf = limites_inf # Límite superior de cada variable self.limites_sup = limites_sup # Valor de las variables del individuo self.valor_variables = np.repeat(None, n_variables) # Fitness del individuo self.fitness = None # Valor de la función objetivo self.valor_funcion = None # CONVERSIONES DE TIPO INICIALES # ---------------------------------------------------------------------- # Si limites_inf o limites_sup no son un array numpy, se convierten en # ello. if self.limites_inf is not None \ and not isinstance(self.limites_inf, np.ndarray): self.limites_inf = np.array(self.limites_inf) if self.limites_sup is not None \ and not isinstance(self.limites_sup,np.ndarray): self.limites_sup = np.array(self.limites_sup) # COMPROBACIONES INICIALES: EXCEPTIONS Y WARNINGS # ---------------------------------------------------------------------- if self.limites_inf is not None \ and len(self.limites_inf) != self.n_variables: raise Exception( "limites_inf debe tener un valor por cada variable. " + "Si para alguna variable no se quiere límite, emplear None. " + "Ejemplo: limites_inf = [10, None, 5]" ) elif self.limites_sup is not None \ and len(self.limites_sup) != self.n_variables: raise Exception( "limites_sup debe tener un valor por cada variable. " + "Si para alguna variable no se quiere límite, emplear None. " + "Ejemplo: limites_sup = [10, None, 5]" ) elif (self.limites_inf is None) or (self.limites_sup is None): warnings.warn( "Es altamente recomendable indicar los límites dentro de los " + "cuales debe buscarse la solución de cada variable. " + "Por defecto se emplea [-10^3, 10^3]." ) elif any(np.concatenate((self.limites_inf, self.limites_sup)) == None): warnings.warn( "Los límites empleados por defecto cuando no se han definido " + "son: [-10^3, 10^3]." ) # COMPROBACIONES INICIALES: ACCIONES # ---------------------------------------------------------------------- # Si no se especifica limites_inf, el valor mínimo que pueden tomar las # variables es -10^3. if self.limites_inf is None: self.limites_inf = np.repeat(-10**3, self.n_variables) # Si no se especifica limites_sup, el valor máximo que pueden tomar las # variables es 10^3. if self.limites_sup is None: self.limites_sup = np.repeat(+10**3, self.n_variables) # Si los límites no son nulos, se reemplazan aquellas posiciones None por # el valor por defecto -10^3 y 10^3. if self.limites_inf is not None: self.limites_inf[self.limites_inf == None] = -10**3 if self.limites_sup is not None: self.limites_sup[self.limites_sup == None] = +10**3 # BUCLE PARA ASIGNAR UN VALOR A CADA UNA DE LAS VARIABLES # ---------------------------------------------------------------------- for i in np.arange(self.n_variables): # Para cada variable, se genera un valor aleatorio dentro del rango # permitido para esa variable. self.valor_variables[i] = random.uniform( self.limites_inf[i], self.limites_sup[i] ) # INFORMACIÓN DEL PROCESO (VERBOSE) # ---------------------------------------------------------------------- if verbose: print("Nuevo individuo creado") print("----------------------") print("Valor variables: " + str(self.valor_variables)) print("Valor función objetivo: " + str(self.valor_funcion)) print("Fitness: " + str(self.fitness)) print("Límites inferiores de cada variable: " \ + str(self.limites_inf)) print("Límites superiores de cada variable: " \ + str(self.limites_sup)) print("") def __repr__(self): """ Información que se muestra cuando se imprime un objeto individuo. """ texto = "Individuo" \ + "\n" \ + "---------" \ + "\n" \ + "Valor variables: " + str(self.valor_variables) \ + "\n" \ + "Valor función objetivo: " + str(self.valor_funcion) \ + "\n" \ + "Fitness: " + str(self.fitness) \ + "\n" \ + "Límites inferiores de cada variable: " \ + str(self.limites_inf) \ + "\n" \ + "Límites superiores de cada variable: " \ + str(self.limites_sup) \ + "\n" return(texto)
[docs] def calcular_fitness(self, funcion_objetivo, optimizacion, verbose = False): """ Este método obtiene el fitness del individuo calculando el valor que toma la función objetivo con el valor de sus variables. Parameters ---------- funcion_objetivo : `function` función que se quiere optimizar. optimizacion : {'maximizar', 'minimizar'} ver notas para más información. verbose : `bool`, optional mostrar información del proceso por pantalla. (default ``False``) Raises ------ raise Exception si el argumento `optimizacion` es distinto de 'maximizar' o 'minimizar' Notes ----- Cada individuo de la población debe ser evaluado para cuantificar su bondad como solución al problema, a esta cuantificación se le llama fitness. Dependiendo de si se trata de un problema de maximización o minimización, la relación del fitness con la función objetivo :math:`f` puede ser: Maximización: el individuo tiene mayor fitness cuanto mayor es el valor de la función objetivo :math:`f(individuo)`. Minimización: el individuo tiene mayor fitness cuanto menor es el valor de la función objetivo :math:`f(individuo)`, o lo que es lo mismo, cuanto mayor es el valor de la función objetivo, menor el fitness. El algoritmo genético selecciona los individuos de mayor fitness, por lo que, para problemas de minimización, el fitness puede calcularse como :math:`−f(individuo)` o también :math:`\\frac{1}{1+f(individuo)}`. Examples -------- Ejemplo evaluar individuo con una función objetivo. >>> individuo = Individuo( n_variables = 3, limites_inf = [-1,2,0], limites_sup = [4,10,20], verbose = True ) >>> def funcion_objetivo(x_0, x_1, x_2): f= x_0**2 + x_1**2 + x_2**2 return(f) >>> individuo.calcular_fitness( funcion_objetivo = funcion_objetivo, optimizacion = "minimizar", verbose = True ) """ # COMPROBACIONES INICIALES: EXCEPTIONS Y WARNINGS # ---------------------------------------------------------------------- if not optimizacion in ["maximizar", "minimizar"]: raise Exception( "El argumento optimizacion debe ser: 'maximizar' o 'minimizar'" ) # EVALUACIÓN DE LA FUNCIÓN OBJETIVO CON LAS VARIABLES DEL INDIVIDUO Y # CÁLCULO DEL FITNESS # ---------------------------------------------------------------------- self.valor_funcion = funcion_objetivo(*self.valor_variables) if optimizacion == "maximizar": self.fitness = self.valor_funcion elif optimizacion == "minimizar": self.fitness = -self.valor_funcion # INFORMACIÓN DEL PROCESO (VERBOSE) # ---------------------------------------------------------------------- if verbose: print("El individuo ha sido evaluado") print("-----------------------------") print("Valor función objetivo: " + str(self.valor_funcion)) print("Fitness: " + str(self.fitness)) print("")
[docs] def mutar(self, prob_mut=0.01, distribucion="uniforme", media_distribucion=1, sd_distribucion=1, min_distribucion=-1, max_distribucion=1, verbose=False): """ Este método somete al individuo a un proceso de mutación en el que, cada una de sus posiciones, puede verse modificada con una probabilidad `prob_mut`. Tras mutar, los atributos `valor_funcion` y `fitness` se reinician. Parameters ---------- prob_mut : `float`, optional probabilidad que tiene cada posición del individuo de mutar. (default 0.01) distribucion : {"normal", "uniforme", "aleatoria"}, optional distribución de la que obtener el factor de mutación. (default "uniforme") media_distribucion : `float`, optional media de la distribución si se selecciona `distribucion = "normal"` (default 1) sd_distribucion : `float`, optional desviación estándar de la distribución si se selecciona `distribucion = "normal"`. (default 1) min_distribucion : `float`, optional mínimo de la distribución si se selecciona `distribucion = "uniforme"`. (default -1) max_distribucion : `float`, optional máximo de la distribución si se selecciona `distribucion = "uniforme"`. (default +1) verbose : `bool`, optional mostrar información del proceso por pantalla. (default ``False``) Raises ------ raise Exception si el argumento `distribucion` es distinto de 'normal', 'uniforme' o 'aleatoria'. Examples -------- Ejemplo mutar individuo. >>> individuo = Individuo( n_variables = 3, limites_inf = [-1,2,0], limites_sup = [4,10,20], verbose = True ) >>> individuo.mutar( prob_mut = 0.5, distribucion = "uniforme", min_distribucion = -1, max_distribucion = 1, verbose = True ) Notes ----- El proceso de mutación añade diversidad al proceso y evitar que el algoritmo caiga en mínimos locales por que todos los individuos sean demasiado parecidos de una generación a otra. Existen diferentes estrategias para controlar la magnitud del cambio que puede provocar una mutación. Distribución uniforme: la mutación de la posición i se consigue sumándole al valor de i un valor extraído de una distribución uniforme, por ejemplo una entre [-1,+1]. Distribución normal: la mutación de la posición i se consigue sumándole al valor de i un valor extraído de una distribución normal, comúnmente centrada en 0 y con una determinada desviación estándar. Cuanto mayor la desviación estándar, con mayor probabilidad la mutación introducirá cambios grandes. Aleatorio: la mutación de la posición i se consigue reemplazando el valor de i por nuevo valor aleatorio dentro del rango permitido para esa variable. Esta estrategia suele conllevar mayores variaciones que las dos anteriores. Hay que tener en cuenta que, debido a las mutaciones, un valor que inicialmente estaba dentro del rango permitido puede salirse de él. Una forma de evitarlo es: si el valor tras la mutación excede alguno de los límites acotados, se sobrescribe con el valor del límite. Es decir, se permite que los valores se alejen como máximo hasta el límite impuesto. """ # COMPROBACIONES INICIALES: EXCEPTIONS Y WARNINGS # ---------------------------------------------------------------------- if not distribucion in ["normal", "uniforme", "aleatoria"]: raise Exception( "El argumento distribucion debe ser: 'normal', 'uniforme' o " \ + "'aleatoria'" ) # SELECCIÓN PROBABILISTA DE POSICIONES (VARIABLES) QUE MUTAN #----------------------------------------------------------------------- posiciones_mutadas = np.random.uniform( low=0, high=1, size=self.n_variables ) posiciones_mutadas = posiciones_mutadas < prob_mut # MODIFICACIÓN DE LOS VALORES DE LAS VARIABLES SELECCIONADAS #----------------------------------------------------------------------- # Si la distribución seleccionada es "uniforme" o "normal", se extrae un # valor aleatorio de la distribución elegida que se suma para modificar # la/las posiciones mutadas. if distribucion in ["normal", "uniforme"]: if distribucion == "normal": factor_mut = np.random.normal( loc = media_distribucion, scale = sd_distribucion, size = np.sum(posiciones_mutadas) ) if distribucion == "uniforme": factor_mut = np.random.uniform( low = min_distribucion, high = max_distribucion, size = np.sum(posiciones_mutadas) ) self.valor_variables[posiciones_mutadas] = \ self.valor_variables[posiciones_mutadas] + factor_mut # Se comprueba si algún valor mutado supera los límites impuestos. # En tal caso se sobrescribe con el valor del límite correspondiente. for i in np.flatnonzero(posiciones_mutadas): if self.valor_variables[i] < self.limites_inf[i]: self.valor_variables[i] = self.limites_inf[i] if self.valor_variables[i] > self.limites_sup[i]: self.valor_variables[i] = self.limites_sup[i] # Si la distribución seleccionada es "aleatoria", se sobreescribe el # valor de la variable con un nuevo valor aleatorio dentro de los # límites establecidos. if distribucion == "aleatoria": for i in np.flatnonzero(posiciones_mutadas): self.valor_variables[i] = random.uniform( self.limites_inf[i], self.limites_sup[i] ) # REINICIO DEL VALOR Y DEL FITNESS #----------------------------------------------------------------------- # Dado que el individuo ha mutado, el valor de su fitness y de la # función objetivo ya no son validos. self.fitness = None self.valor_funcion = None # INFORMACIÓN DEL PROCESO (VERBOSE) # ---------------------------------------------------------------------- if verbose: print("El individuo ha sido mutado") print("---------------------------") print("Total mutaciones: " + str(np.sum(posiciones_mutadas))) print("Valor variables: " + str(self.valor_variables)) print("")