Subido por Julian Santos Morales

algoritmos-geneticos-con-python-un-enfoque-practico-para-resolver-problemas-de-ingenieria

Anuncio
Algoritmos Genéticos con Python
Un enfoque práctico para resolver problemas de ingeniería
Primera edición
Algoritmos Genéticos con Python
Un enfoque práctico para resolver problemas de ingeniería
Primera edición
Algoritmos Genéticos con Python
Un enfoque práctico para resolver problemas de ingeniería
© 2020 Daniel Gutiérrez Reina, Alejandro Tapia Córdoba y Alvaro Rodríguez
del Nozal
Primera edición, 2020
© 2020 MARCOMBO, S.L.
www.marcombo.com
Diseño de la cubierta: Alejandro Tapia Córdoba
Diseño de interior: Daniel Gutiérrez Reina, Alejandro Tapia Córdoba y Alvaro
Rodríguez del Nozal
Correctora: Anna Alberola
Directora de producción: M.a Rosa Castillo
«Cualquier forma de reproducción, distribución, comunicación pública o
transformación de esta obra solo puede ser realizada con la autorización de sus
titulares, salvo excepción prevista por la ley. Diríjase a CEDRO (Centro Español
de Derechos Reprográficos, www.cedro.org) si necesita fotocopiar o escanear
algún fragmento de esta obra».
ISBN: 978-84-267-3068-8
Producción del ebook: booqlab
A Mónica, Laura, Sol y Lucía.
Prefacio
I
Parte 1: Introducción a los algoritmos genéticos
1Introducción
1.1 Introducción a los algoritmos genéticos
1.2 Primeros pasos mediante un problema sencillo
1.3 Definición del problema y generación de la población inicial
1.3.1 Creación del problema
1.3.2 Creación de la plantilla del individuo
1.3.3 Crear individuos aleatorios y población inicial
1.4 Función objetivo y operadores genéticos
1.4.1 Función objetivo
1.5 Operadores genéticos
1.6 Últimos pasos: Algoritmo genético como caja negra
1.6.1 Configuración algoritmo genético
1.6.2 Resultados del algoritmo genético
1.7 ¿Cómo conseguir resultados consistentes?
1.8 Convergencia del algoritmo
1.9 Exploración versus explotación en algoritmos genéticos
1.10 Código completo y lecciones aprendidas
1.11 Para seguir aprendiendo
2El problema del viajero
2.1 Introducción al problema del viajero
2.2 Definición del problema y generación de la población inicial
2.2.1 Creación del problema y plantilla para el individuo
2.2.2 Crear individuos aleatorios y población inicial
2.3 Función objetivo y operadores genéticos
2.3.1 Función objetivo
2.3.2 Operadores genéticos
2.4 Selección del algoritmo genético
2.5 Últimos pasos
2.5.1 Configuración del algoritmo genético µ + λ
2.6 Comprobar la convergencia del algoritmo en problemas complejos
2.7 Ajuste de los hiperparámetros: Probabilidades de cruce y mutación
2.8 Acelerando la convergencia del algoritmo: El tamaño del torneo
2.9 Acelerando la convergencia del algoritmo: Aplicar elitismo
2.10 Complejidad del problema: P vs NP
2.11 Código completo y lecciones aprendidas
2.12 Para seguir aprendiendo
3Algoritmos genéticos y benchmarking
3.1 Introducción a las funciones de benchmark
3.2 Aprendiendo a usar las funciones de benchmark : Formulación del
problema 78
3.2.1 Función h1
3.2.2 Función Ackley
3.2.3 Función Schwefel
3.3 Definición del problema y generación de la población inicial
3.4 Función objetivo y operadores genéticos
3.4.1 Función objetivo
3.4.2 Operadores genéticos
3.5 Código completo
3.6 Evaluación de algunas funciones de benchmark
3.6.1 Función h1
3.6.2 Función Ackley
3.6.3 Función Schwefel
3.7 Ajuste de los hiperparámetros de los operadores genéticos
3.8 Lecciones aprendidas
3.9 Para seguir aprendiendo
4Algoritmos genéticos con múltiples objetivos
4.1 Introducción a los problemas con múltiples objetivos
4.2 Introducción a la Pareto dominancia
4.3 Selección del algoritmo genético
4.4 El problema de la suma de subconjuntos con múltiples objetivos
4.4.1 Formulación del problema
4.4.2 Definición del problema y generación de la población inicial
4.4.3 Definición del problema y plantilla del individuo
4.4.4 Función objetivo y operadores genéticos
4.4.5 Últimos pasos: Ejecución del algoritmo multiobjetivo
4.4.6 Configuración del algoritmo genético multiobjetivo
4.4.7 Algunos apuntes sobre los algoritmos genéticos con múltiples objetivos
4.4.8 Código completo
4.5 Funciones de benchmark con múltiples objetivos
4.5.1 Definición del problema y población inicial
4.5.2 Función objetivo y operadores genéticos
4.5.3 Ejecución del algoritmo multiobjetivo
4.5.4 Representación del frente de Pareto
4.5.5 Ajuste de los hiperpámetros de los operadores genéticos
4.5.6 Código completo
4.6 Lecciones aprendidas
4.7 Para seguir aprendiendo
II
Parte 2: Algoritmos genéticos para ingeniería
5Funcionamiento óptimo de una microrred
5.1 Introducción
5.2 Formulación del problema
5.2.1 Recursos renovables
5.2.2 Unidades despachables
5.2.3 Sistema de almacenamiento de energía
5.2.4 Balance de potencia
5.3 Problema con un objetivo: Minimizar el coste de operación
5.3.1 Definición del problema y generación de la población inicial
5.3.2 Operadores genéticos
5.3.3 Función objetivo
5.3.4 Ejecución del algoritmo
5.3.5 Resultados obtenidos
5.4 Problema con múltiples objetivos: Minimizando el coste de operación y
el ciclado de la batería
5.4.1 Definición del problema, población inicial y operadores genéticos
5.4.2 Función objetivo
5.4.3 Ejecución del algoritmo
5.4.4 Resultados obtenidos
5.5 Código completo y lecciones aprendidas
5.6 Para seguir aprendiendo
6Diseño de planta microhidráulica
6.1 Introducción
6.2 Formulación del problema
6.2.1 Modelado de la central micro-hidráulica
6.3 Problema con un objetivo: Minimizando el coste de instalación
6.3.1 Definición del problema y generación de la población inicial
6.3.2 Operadores genéticos
6.3.3 Función objetivo o de fitness
6.3.4 Ejecución del algoritmo
6.3.5 Resultados obtenidos
6.4 Problema con múltiples objetivos: Minimizando el coste de instalación y
maximizando la potencia generada
6.4.1 Definición del problema, población inicial y operadores genéticos
6.4.2 Función objetivo o de fitness
6.4.3 Ejecución del algoritmo
6.4.4 Resultados obtenidos
6.5 Código completo y lecciones aprendidas
6.6 Para seguir aprendiendo
7Posicionamiento de sensores
7.1 Introducción
7.2 Formulación del problema
7.3 Problema con un objetivo: Maximizando el número de puntos cubiertos
189
7.3.1 Definición del problema y generación de la población inicial
7.3.2 Operadores genéticos
7.3.3 Función objetivo
7.3.4 Ejecución del algoritmo
7.3.5 Resultados obtenidos
7.4 Problema con múltiples objetivos: maximizando el número de puntos
cubiertos y la redundancia
7.4.1 Definición del problema, población inicial y operadores genéticos
7.4.2 Función objetivo
7.4.3 Ejecución del algoritmo
7.4.4 Resultados obtenidos
7.5 Código completo y lecciones aprendidas
7.6 Para seguir aprendiendo
Epílogo
AHerencia de arrays de numpy
A.1 Introducción a las secuencias en Python
A.2 Slicing en secuencias y operadores genéticos de deap
A.3 Operador de comparación en secuencias
BProcesamiento paralelo
B.1 Procesamiento paralelo con el módulo multiprocessing
B.2 Procesamiento paralelo con el módulo Scoop
Glosario
Bibliografía
¿Por qué algoritmos genéticos en el ámbito de la
ingeniería?
Los ingenieros nos enfrentamos día a día a numerosos problemas. La
complejidad de estos problemas crece de una forma exponencial debido a las
herramientas informáticas que nos permiten desarrollar modelos más complejos.
Hemos pasado, pues, de una ingeniería con papel, lápiz y calculadora, a otra con
herramientas digitales, gran cantidad de datos (big data) y súper ordenadores.
Todo ello ha sido posible gracias a los avances conseguidos en disciplinas como
la tecnología electrónica, el desarrollo software e Internet. En la actualidad, los
modelos con los que nos enfrentamos son más realistas, pero también son más
difíciles de analizar mediante técnicas analíticas o exactas. En los últimos años,
los algoritmos de inteligencia artificial se han convertido en una herramienta
indispensable para resolver problemas de ingeniería de una manera aproximada.
Estos algoritmos están soportados por la gran capacidad de cálculo que tienen
nuestros ordenadores. En la actualidad, cualquier ordenador personal o portátil
es capaz de realizar millones de operaciones por segundo. Por otro lado,
lenguajes de programación de alto nivel, como Python, han generado una gran
cantidad de usuarios, agrupados en comunidades, que trabajan conjuntamente
para desarrollar un amplio abanico de recursos abiertos. Estos recursos se
engloban en librerías, repositorios abiertos, congresos nacionales e
internacionales, comunidades locales, seminarios, etc. El resultado es una
democratización de la inteligencia artificial abierta y al alcance de todos. Es
decir, las técnicas de inteligencia artificial están a disposición de cualquiera que
tenga unos conocimientos medios en matemáticas y programación. Así, los
ingenieros -como grandes artífices del desarrollo tecnológico- debemos estar en
la cresta de la ola de la inteligencia artificial, para seguir aportando soluciones a
los problemas que nos surgen día a día.
En esta gran caja de herramientas compuesta por todos los algoritmos de
inteligencia artificial -tales como algoritmos de machine learning, redes
neuronales, etc.- los algoritmos genéticos son una herramienta indispensable, ya
que nos permiten obtener soluciones adecuadas a problemas muy complejos que
no se pueden abordar con métodos clásicos. Por lo tanto, cualquiera que se
considere experto en la materia debe dominar la técnica. Siempre debemos tener
presente que lo importante de cada método es saber en qué escenarios se debe
utilizar, qué ventajas tiene y cuáles son las limitaciones de cada herramienta. En
este libro se muestran, mediante una serie de ejemplos prácticos, las bondades y
las limitaciones de los algoritmos genéticos para resolver problemas de
ingeniería.
Como introducción al contenido del libro, debemos anticipar que los algoritmos
genéticos se basan en la naturaleza. Esto significa que muchos de los problemas
de ingeniería se pueden resolver, simplemente, observando cómo funcionan los
seres vivos. El mecanismo utilizado no es otro que la Teoría de la Evolución de
Charles Darwin (Darwin, 1859), la cual nos dice que los individuos que mejor se
adaptan al medio tienen más probabilidades de sobrevivir y en consecuencia, de
dejar descendencia. Esta idea, aparentemente tan alejada del mundo de la
ingeniería, ha dado lugar a una metodología de optimización de problemas: los
algoritmos evolutivos o computación evolutiva, donde se encuentran
enmarcados los algoritmos genéticos.
A lo largo del libro se abordarán distintos problemas de optimización que se
resolverán mediante algoritmos genéticos. El objetivo principal del libro es dejar
patente la gran capacidad que tienen los algoritmos genéticos como técnica de
resolución de problemas de ingeniería. Así, esperamos que este libro sirva para
que muchos ingenieros se introduzcan dentro del mundo de la optimización
metaheurística y la apliquen en sus problemas en el futuro.
Daniel Gutiérrez Reina, Alejandro Tapia Córdoba, Álvaro Rodríguez del Nozal
Sevilla, mayo de 2020
Objetivos y estructura del libro
Este libro pretende ofrecer una visión general sobre el desarrollo y la
programación de algoritmos genéticos desde un punto de vista práctico y con un
enfoque orientado a la resolución de problemas de ingeniería. El libro se ha
orientado a un aprendizaje mediante ejemplos (learning by doing). Esto significa
que los conceptos se van describiendo conforme aparecen en el problema que se
aborda. Por lo tanto, no existe un capítulo donde se encuentren todos los
operadores genéticos, o las implementaciones de algoritmos genéticos, etc. No
obstante, el glosario del libro permite identificar fácilmente la página donde se
encuentra cada concepto.
El libro se estructura en dos partes bien diferenciadas. En la primera parte, se
cubren los conceptos básicos de los algoritmos genéticos mediante varios
ejemplos clásicos. En el primero de los cuatro capítulos que constituyen esta
primera parte, se resuelve un problema muy sencillo formulado con variables
continuas, con el propósito de ilustrar los principales componentes de los
algoritmos genéticos. Aunque el primer capítulo es largo, es necesario leerlo con
detenimiento para poder comprender los principales mecanismos que hay detrás
de un algoritmo genético. Por lo tanto, se recomienda no avanzar si no se tienen
claros los conceptos descritos en este capítulo. En el segundo capítulo, se aborda
el problema del viajero o Traveling Salesman Problem (TSP), sin duda uno de
los problemas combinatorios clásicos con variables discretas más estudiados, y
que constituye un ejemplo perfecto para demostrar la potencialidad de los
algoritmos genéticos para resolver problemas complejos. Una vez conocida la
estructura fundamental de los algoritmos genéticos, en el tercer capítulo, se
profundiza en el uso de las funciones de benchmark para validar tanto sus
capacidades como sus potenciales vulnerabilidades, lo cual la constituye una
herramienta fundamental para su depuración. Las funciones de benchmark son
funciones que la comunidad científica utiliza para la evaluación de algoritmos de
optimización. Estas presentan diversas dificultades a los algoritmos de
optimización. Por ejemplo, tener varios máximos o mínimos (funciones
multimodales), o tener un máximo/mínimo local cerca del absoluto. Por último,
en el cuarto capítulo se introduce el enfoque multiobjetivo de los algoritmos
genéticos, lo cual constituye una de las capacidades más interesantes y versátiles
de este tipo de algoritmos. Desde el punto de vista de los problemas de
ingeniería, el enfoque multiobjetivo es muy importante, ya que los ingenieros
siempre debemos tener en cuenta una relación de compromiso entre el coste y la
adecuación de las soluciones al problema. Los problemas multiobjetivo se
abordarán mediante dos ejemplos. En primer lugar, se resolverá un problema
clásico como es la suma de subconjuntos. Y en segundo lugar, se usarán
funciones de benchmark con múltiples objetivos. Al finalizar la primera parte, el
lector habrá adquirido suficiente destreza como para poder abordar problemas de
optimización mediante algoritmos genéticos.
En la segunda parte, se introducirán una serie de problemas ingenieriles, cuya
resolución se abordará mediante el desarrollo de algoritmos genéticos. Todos los
problemas se tratan tanto desde el punto de vista de un único objetivo
(problemas unimodales) como desde el punto de vista de un multiobjetivo
(problemas multimodales). En el primer capítulo, se estudia el problema del
despacho económico de una microrred eléctrica. Este problema, formulado en
variables continuas, constituye uno de los problemas más complejos y de más
relevancia en el área de sistemas eléctricos de potencia, y persigue la
programación de la potencia suministrada por un conjunto de generadores, para
abastecer una demanda durante un periodo determinado y de forma óptima. En
el segundo capítulo, se aborda un problema de optimización relativo al diseño de
una planta micro-hidráulica. Este problema, formulado con variables binarias,
persigue determinar el trazado óptimo de la planta, y constituye un problema de
especial interés dado el alto número de combinaciones posibles, lo que hace
inabordable su resolución mediante estrategias analíticas o exactas. Por último,
en el tercer capítulo se aborda el problema del posicionamiento óptimo de
sensores, en el cual se persigue determinar las posiciones más adecuadas para
instalar una serie de sensores de manera que la mayor parte posible de puntos de
interés queden cubiertos.
En todos los capítulos se incluye una sección de código completo, lecciones
aprendidas y ejercicios propuestos. Las lecciones aprendidas hacen referencias a
los aspecto más relevantes que se deben adquirir en dicho capítulo. Los
ejercicios sirven para afianzar conceptos y coger destreza en la aplicación de
algoritmos genéticos. Por último, para finalizar cada capítulo se incluye una
sección con bibliografía adicional para seguir profundizando en los temas
abordados en el capítulo.
Prerrequisitos para seguir el libro
Para seguir correctamente el libro, se presupone unos conocimientos medios del
lenguaje de programación Python. Este libro no cubre los conceptos básicos de
este lenguaje, y da por hecho que el lector parte con conocimientos básicos de
programación orientada a objetos.
En cuanto a los algoritmos genéticos, el libro cubre desde cero, y paso a paso,
los conceptos básicos de dichos algoritmos -tanto de aquellos con un único
objetivo, como de aquellos con múltiples objetivos-. El contenido matemático
del libro es mínimo, y únicamente es de relevancia en la segunda parte, donde
algunas ecuaciones son necesarias para plantear los problemas de ingeniería
propuestos.
Se recomienda una lectura en profundidad de los primeros capítulos antes de
pasar a la segunda parte del libro. En la segunda parte se pasa más rápidamente
por los componentes de los algoritmos genéticos que se han detallado en la
primera parte.
Código
Descripción del código
La presentación del código en todos los capítulos se hace mediante el siguiente
procedimiento:
■En primer lugar, se describen por separado cada una de las partes del código
utilizadas para resolver los problemas planteados en cada capítulo. De esta
forma, se describen paso a paso los principales componentes del algoritmo.
■En segundo lugar, todos los capítulos tienen una sección que incluye el código
completo necesario para resolver el problema. Así, el lector puede ver de manera
conjunta todas las líneas de código, junto a una breve descripción del mismo.
Para desarrollar el código se ha utilizado el paquete Anaconda con Python 3.
Este paquete incluye tanto el intérprete de Python como librerías básicas de este
lenguaje de programación, como pueden ser numpy o matplotlib. Además, nos
provee de entornos de desarrollo para generar nuestro código, como pueden ser
Spyder o Jupyter.
Repositorio
Todos los scripts utilizados en cada una de las secciones, así como diverso
material complementario del libro, se pueden encontrar en el siguiente
repositorio de Github: https://github.com/Dany503/Algoritmos-Geneticos-enPython-Un-Enfoque-Practico.
Descripción del código
Todos los fragmentos de código desarrollados en este libro se clasifican en
cuatro categorías: archivos de texto, resultados, scripts y códigos completos.
Para facilitar su identificación, cada una de estas categorías se corresponde con
un color:
Algoritmos y operadores de referencia
Aunque las herramientas fundamentales de los algoritmos genéticos se
introducirán en el primer capítulo, a lo largo del libro se irán presentando
diferentes propuestas para su implementación, en base a las necesidades del
problema en estudio. Junto con cada nueva propuesta se presentará una
descripción detallada en un entorno como el siguiente:
Así, utilizaremos este entorno para describir operadores genéticos (mutación,
cruce y selección) y algoritmos genéticos. Para localizar dónde se describe una
función en particular, podemos consultar el glosario.
Librerías necesarias
Todos los scripts de Python se han desarrollado en Anaconda1. La versión de
Python utilizada es 3.6 para Windows. No obstante, no debe haber problemas
con otras versiones de Python y otros sistemas operativos.
A continuación, se listan las librerías utilizadas:
■deap : versión 1.3.
■matplotlib : versión 3.1.3
■numpy : versión 1.16.3
■scipy : versión 1.2.1
■scoop : versión 0.7
■Módulos nativos de Python como random , arrays , multiprocessing , JSON ,
math , etc.
Para instalar la librería deap con pip2:
Si realiza la instalación desde Spyder o Google colab:
Para instalar con conda3:
Agradecimientos
Los autores quieren transmitir sus agradecimientos a la Universidad de Sevilla y
a la Universidad Loyola Andalucía, instituciones donde actualmente trabajan.
Los autores agradecen a todos los desarrolladores de la librería deap (Fortin et
al., 2012) por la documentación disponible y el esfuerzo desarrollado en los
últimos años. Por último, agradecer a compañeros de trabajo, familiares y
amigos, por su apoyo.
Sobre los autores y datos de contacto
Daniel Gutiérrez Reina es Doctor Ingeniero en Electrónica por la Universidad de
Sevilla (2015). Trabaja actualmente como investigador postdoctoral en el
Departamento de Ingeniería Electrónica de la Universidad de Sevilla. Ha sido
investigador visitante en la John Moores University (Reino Unido), en Freie
Universität Berlin (Alemania), en Colorado School of Mines (Estados Unidos) y
en Leeds Beckett University (Reino Unido). También trabajó en la Universidad
Loyola Andalucía en el Departamento de Ingeniería durante año y medio. Su
investigación se centra en la optimización de problemas de ingeniería utilizando
técnicas de optimización metaheurísticas y machine learning. Es docente de un
gran número de cursos de Python, optimización y machine learning en la
Universidad de Sevilla, en la Universidad de Málaga y en la Universidad de
Córdoba. Para contactar con el autor: [email protected].
Alejandro Tapia Córdoba es Ingeniero Industrial especializado en Materiales
(2014) por la Universidad de Sevilla. En 2019 recibió su título de Doctor en
Ciencia de los Datos por la Universidad Loyola Andalucía, donde actualmente
trabaja como profesor asistente en el Departamento de Ingeniería. Ha sido
investigador visitante en la Universidad de Greenwich (UK). Su investigación se
enmarca en el desarrollo de estrategias de optimización para aplicaciones en
diferentes áreas de la ingeniería. Para contactar con el autor: [email protected].
Álvaro Rodríguez del Nozal es Ingeniero Industrial especializado en Automática
Industrial (2013) y Máster en Sistemas de Energía Eléctrica (2016) por la
Universidad de Sevilla. Recibió su título de Doctor en Ingeniería de Control por
la Universidad Loyola Andalucía en el año 2019. Actualmente trabaja como
investigador postdoctoral en el Departamento de Ingeniería Eléctrica de la
Universidad de Sevilla. Ha sido investigador visitante en el Laboratoire
d’analyse et d’architecture des systèmes (Francia) y en el Politecnico di Milano
(Italia). Su investigación se centra en el control y estimación distribuida de
sistemas dinámicos, así como en la integración de energías renovables en la red
eléctrica. Para contactar con el autor: [email protected].
_________________
1Se puede descargar de forma gratuita en
https://www.anaconda.com/distribution/
2https://pypi.org/project/deap/
3https://anaconda.org/conda-forge/deap
IParte 1: Introducción a los algoritmos genéticos
1Introducción
1.1 Introducción a los algoritmos genéticos
1.2 Primeros pasos mediante un problema sencillo
1.3 Definición del problema y generación de la población inicial
1.4 Función objetivo y operadores genéticos
1.5 Operadores genéticos
1.6 Últimos pasos: Algoritmo genético como caja negra
1.7 ¿Cómo conseguir resultados consistentes?
1.8 Convergencia del algoritmo
1.9 Exploración versus explotación en algoritmos genéticos
1.10 Código completo y lecciones aprendidas
1.11 Para seguir aprendiendo
2El problema del viajero
2.1 Introducción al problema del viajero
2.2 Definición del problema y generación de la población inicial
2.3 Función objetivo y operadores genéticos
2.4 Selección del algoritmo genético
2.5 Últimos pasos
2.6 Comprobar la convergencia del algoritmo en problemas complejos
2.7 Ajuste de los hiperparámetros: Probabilidades de cruce y mutación
2.8 Acelerando la convergencia del algoritmo: El tamaño del torneo
2.9 Acelerando la convergencia del algoritmo: Aplicar elitismo
2.10 Complejidad del problema: P vs NP
2.11 Código completo y lecciones aprendidas
2.12 Para seguir aprendiendo
3Algoritmos genéticos y benchmarking
3.1 Introducción a las funciones de benchmark
3.2 Aprendiendo a usar las funciones de benchmark : Formulación del problema
3.3 Definición del problema y generación de la población inicial
3.4 Función objetivo y operadores genéticos
3.5 Código completo
3.6 Evaluación de algunas funciones de benchmark
3.7 Ajuste de los hiperparámetros de los operadores genéticos
3.8 Lecciones aprendidas
3.9 Para seguir aprendiendo
4Algoritmos genéticos con múltiples objetivos
4.1 Introducción a los problemas con múltiples objetivos
4.2 Introducción a la Pareto dominancia
4.3 Selección del algoritmo genético
4.4 El problema de la suma de subconjuntos con múltiples objetivos
4.5 Funciones de benchmark con múltiples objetivos
4.6 Lecciones aprendidas
4.7 Para seguir aprendiendo
1.1 Introducción a los algoritmos genéticos
Los algoritmos genéticos son técnicas de optimización metaheurísticas, también
llamadas estocásticas o probabilísticas (Holland et al., 1992) (Goldberg, 2006).
Aunque fueron propuestos en la década de los 60s por Jonh Holland (Holland,
1962) (Holland, 1965) (Holland et al., 1992), no ha sido posible su aplicación en
problemas de ingeniería reales hasta hace un par de décadas, debido
principalmente a que son computacionalmente intensivos y que, por lo tanto,
necesitan una capacidad computacional elevada para llevar a cabo multitud de
operaciones en poco tiempo. La idea principal de un algoritmo genético es
realizar una búsqueda guiada a partir de un conjunto inicial de posibles
soluciones, denominado población inicial, el cual va evolucionando a mejor en
cada iteración del algoritmo (Lones, 2011). Dichas iteraciones se conocen como
generaciones y, normalmente, la última generación incluye la mejor o las
mejores soluciones a nuestro problema de optimización. Cada posible solución a
nuestro problema se conoce como individuo, y cada individuo codifica las
variables independientes del problema de optimización. Estas variables
representan los genes de la cadena cromosómica que representa a un individuo.
Los algoritmos genéticos están basados en la Teoría Evolucionista de Charles
Darwin (Darwin, 1859). Dicha teoría, explicado de forma muy simple, indica
que los individuos que mejor se adaptan al medio son aquellos que tienen más
probabilidades de dejar descendencia, y cuyos genes pasarán a las siguientes
generaciones. La teoría de Darwin también describe que aquellas modificaciones
genéticas que hacen que los individuos se adapten mejor al medio, tienen mayor
probabilidad de perdurar en el tiempo. Estas ideas son las que utilizan los
algoritmos genéticos para realizar una búsqueda estocástica guiada de forma
eficiente. En los problemas de optimización numéricos, los individuos son
potenciales soluciones al problema y la adaptación al medio se mide mediante la
función que queremos optimizar, también llamada función objetivo, fitness
function o función de evaluación1. Un individuo se adaptará bien al medio si
produce un desempeño o fitness2 alto, en caso de que se quiera maximizar la
función objetivo, o si produce un desempeño bajo en caso de que estemos ante
un problema de minimización. Ambos problemas son siempre duales3, por lo
que pasar de un problema de maximización a un problema de minimización es
tan sencillo como multiplicar por –1 el resultado de la función objetivo.
En cada iteración del algoritmo, nuevos individuos (descendientes o, en inglés,
offsprings) son creados mediante operaciones genéticas, dando lugar a nuevas
poblaciones. Dichas operaciones genéticas, que se pueden resumir en tres
bloques -selección, cruce y mutación- son el motor de búsqueda de un algoritmo
genético. Cada vez que se crea un nuevo conjunto de individuos, se crea una
nueva generación, y dicho proceso termina con la generación final, la cual debe
incluir los mejores individuos encontrados a lo largo de las generaciones. Así, la
Figura 1.1 representa el funcionamiento general de un algoritmo genético. Como
se puede observar, se parte de una población inicial aleatoria y, a través de las
operaciones genéticas, se van obteniendo nuevas generaciones hasta que se
alcanza la población final. En este primer capítulo del libro, vamos a entrar en
detalle en cada uno de los pasos y mecanismos que conforman un algoritmo
genético; para ello, utilizaremos la librería de Python deap4. Esta librería nos
facilita el diseño e implementación de distintos algoritmos genéticos, ya que
incluye muchas funciones de librería que desarrollan los principales
componentes de un algoritmo genético.
Figura 1.1. Esquema del funcionamiento de un algoritmo genético.
¿Por qué recurrimos a la optimización metaheurística?
A lo largo de nuestra vida académica y profesional como ingenieros, con
frecuencia nos encontramos con problemas de optimización de gran
complejidad. A veces, esta radica en la gran cantidad de variables que hay que
manejar; otras, en la complejidad de las ecuaciones que las gobiernan. A veces,
incluso nos planteamos si la solución a nuestro problema existe. Pero, en
general, solemos decir que estos problemas son difíciles de resolver. Pero ¿qué
significa que un problema sea difícil de resolver? Aunque pueda parecer una
pregunta trivial, es la primera que debemos formular cuando nos planteamos el
uso de optimización metaheurística. Por supuesto, no es una pregunta fácil de
responder.
Los métodos de optimización se pueden clasificar en dos grandes grupos bien
diferenciados: métodos exactos y métodos metaheurísticos o aproximados.
La diferencia fundamental entre ellos está clara: los métodos exactos garantizan
la obtención de la solución óptima, mientras que los metaheurísticos no.
Llegados a este punto, uno podría preguntarse qué sentido tiene inclinarse por la
segunda opción pudiendo utilizar un método exacto. Pues bien, la realidad es que
no siempre podemos encontrar un método exacto que permita resolver nuestro
problema. Es más: si lo hay, es muy posible que su aplicación no sea viable para
un problema de cierta complejidad; por ejemplo, por el tiempo de resolución
(una búsqueda extensiva para un problema combinatorio de algunos cientos de
variables puede tardar meses o años5), o por las simplificaciones que pueden
requerir para su aplicación (por ejemplo puede ser necesario linealizar las
restricciones del problema). Además, las estrategias de resolución analíticas,
como los métodos basados en gradiente, pueden converger a óptimos locales y
no alcanzar el óptimo global del problema.
Así, para decidir qué estrategia utilizar para abordar un problema difícil de
resolver deberíamos plantearnos, al menos, las siguientes cuestiones:
■¿Cómo de grande es mi problema?
Evaluar el tamaño del espacio de búsqueda (esto es, el número de soluciones
posibles) es un buen indicador de la complejidad del problema. Si conocemos el
tiempo necesario para evaluar una solución, podemos hacer una estimación del
tiempo que sería necesario para realizar una búsqueda extensiva.
■¿Necesito resolverlo rápido?
Está claro que es preferible disponer de la solución lo antes posible, pero
debemos pensar si realmente necesitamos que nuestro problema se resuelva en
cuestión de segundos o si, por el contrario, varias horas (o días) son un plazo
aceptable.
■¿Hay muchas restricciones? ¿Cómo son?
Un elevado número de restricciones, y, -sobretodo, una gran cantidad de no
linealidades en las mismaspuede constituir un obstáculo insalvable para abordar
analíticamente nuestro problema de optimización. Resolver de forma analítica
una versión suficientemente simplificada (por ejemplo, relajando ciertas
restricciones) de nuestro problema puede ser una buena idea, y nos puede ayudar
en el desarrollo de estrategias metaheurísticas para el problema completo.
■¿Qué precisión necesito en los resultados?
Cuanto más precisa sea nuestra solución mejor, por supuesto. Pero ¿cuánto es
suficiente? ¿Considerarías adecuada una solución un 1% peor que el óptimo
global? ¿Y un 5%? Es posible que con un 10% tu solución sea más que útil para
cumplir su propósito, y te permitiría ahorrar una gran parte de tiempo o de
recursos.
Limitaciones de los métodos tradicionales basados en gradiente
Tradicionalmente, en los cursos de cálculo, tanto en enseñanza secundaria como
en niveles superiores, los métodos de optimización estudiados son los métodos
basados en gradiente. De forma genérica, para una función f (x) el procedimiento
consiste en:
1. Calculamos las derivadas de la función f ' ( x ).
2. Obtenemos los puntos para los que el gradiente se hace cero f ' ( x i ) = 0.
3. Calculamos la segunda deriva f '' ( x ) y evaluamos los puntos anteriores para
saber si es un máximo f '' ( x i < 0) o un mínimo f '' ( x i > 0).
Cuando tenemos problemas con varias variables, debemos trabajar con derivadas
parciales y obtener las matrices Hessianas. Como podemos observar, los
métodos basados en gradientes se basan en el cálculo de las derivadas de la
función (o derivadas parciales). Sin embargo, existen muchos casos en los que el
cálculo de las derivadas es muy complejo o incluso imposible. Imaginemos que
queremos optimizar el funcionamiento una planta industrial de la que no
tenemos el modelo, pero sí tenemos un software de simulación de la planta. En
esta situación, no podemos aplicar los métodos basados en gradiente, pero sí
podremos aplicar los métodos metaheurísticos, como veremos más adelante.
Otro problema de los métodos basados en gradiente es que se pueden quedar
atrapados en óptimos locales, ya que pueden existir varios puntos de la función
en los que el gradiente se haga cero. Por último, también es importante indicar
que en los métodos numéricos basados en gradiente, se debe indicar un punto
inicial. Por lo tanto, el funcionamiento del algoritmo dependerá de la selección
de dicho punto, y pueden aparecer problemas de convergencia en algunos casos.
A continuación, veremos que los algoritmos metaheurísticos -y en concreto los
algoritmos genéticos- nos permiten obtener soluciones realmente buenas a
problemas en los que los métodos tradicionales basados en gradiente pueden
presentar problemas.
1.2 Primeros pasos mediante un problema sencillo
Como la mejor forma de aprender a programar es simplemente programando,
vamos a resolver un problema sencillo paso a paso, con el fin de poder describir
los distintos componentes de un algoritmo genético, así como su implementación
en Python.
En este simple ejemplo, las variables independientes son x e y, y la función
objetivo o función de fitness es f (x,y). Un individuo, pues, debe codificar dichas
variables independientes en una cadena cromosómica (información genética del
individuo) en la que cada variable independiente corresponde a un gen. Así, en
nuestro problema ejemplo, la cadena cromosómica estaría formada por dos
genes que al confinarse en forma de lista, quedarían como [xi, yi], con i = 1,...,n
(siendo n el número de individuos que componen la población). La Figura 1.2
muestra gráficamente la representación de un individuo y de una población de n
individuos.
Figura 1.2. Representación de un individuo y de una población.
En principio, vamos a considerar que la población del algoritmo no cambia de
tamaño a lo largo de las generaciones; por lo tanto, n será constante.
En los algoritmos genéticos tradicionales, el tamaño de la población es siempre constante. Ad
Es decir, queremos tener genes de muchos tipos. En caso contrario, si los
individuos de la población inicial se parecieran mucho, estaríamos limitando el
proceso de búsqueda del algoritmo genético. Por lo tanto, a la hora de abordar la
resolución de un problema mediante algoritmos genéticos, uno de los primeros
pasos que tenemos que dar es buscar un mecanismo para generar soluciones
aleatorias a nuestro problema que difieran lo suficiente las unas de las otras.
Imaginemos que la población inicial está compuesta por diez individuos (n =
10); en consecuencia, se deberán generar diez soluciones aleatorias. En la Tabla
1.1 se muestran las soluciones iniciales generadas, siendo esta solo una posible
muestra de diez soluciones aleatorias.
Tabla 1.1. Población inicial.
Individuo x
y
1
2
3
4
5
6
7
8
9
10
81.62
0.93
-43.63
51.16
23.67
-49.89
81.94
96.55
62.04
80.43
68.88
51.59
-15.88
-48.21
2.25
-19.01
56.75
-39.33
-4.68
16.67
Generar valores aleatorios en Python es muy sencillo y existen dos módulos que
nos pueden ayudar mucho en esta tarea: i) el módulo nativo random6 y ii) el
submódulo de numpy numpy.random7. Por citar las diferencias más importantes
entre ambos: el módulo random es nativo, por lo que viene integrado con el
intérprete de Python y genera números pseudoaleatorios. Los números
totalmente aleatorios no existen en programación: hablaremos más adelante
sobre este detalle. Por otro lado, el numpy.random es un submódulo que nos
permite crear vectores pseudoaleatorios de distintos tamaños y dimensiones. En
definitiva, un módulo me permite generar números aleatorios y el otro vectores
aleatorios.
Veamos dos formas de obtener poblaciones parecidas a las mostradas en la Tabla
1.1. En el siguiente script se utiliza el módulo random. Con el fin de obtener
siempre los mismos números aleatorios, es posible fijar una semilla mediante el
método seed:
Un generador de números aleatorios, no es más que una función que nos
devuelve un número pseudoaleatorio dependiendo de la semilla. Si la semilla es
siempre distinta, la función nos devolverá un número distinto. En cambio, si
utilizamos la misma semilla, dicha función siempre nos devolverá el mismo
número. En el ejemplo mostrado, se utiliza el método uniform8 para generar
números entre –100 y 100 (estos dos valores no están incluidos), y se utilizan
dos list comprenhension9 para encapsular todos los datos en las listas x e y.
Otra posibilidad es generar dos vectores de diez valores comprendidos entre –
100 y 100, con una forma (1,10) (1 fila y 10 columnas); esto implica que son dos
vectores de tipo fila con diez valores.
Se puede comprobar, que en este segundo caso también hemos fijado la semilla
para obtener los mismos valores. Cuando utilicemos el módulo deap para definir
nuestros algoritmos genéticos, siempre tendremos que utilizar funciones para
generar soluciones aleatorias. Dichas soluciones aleatorias serán nuestra
población inicial, es decir, el punto de partida de nuestro algoritmo genético.
Además, dichas soluciones deben ser válidas. En nuestro ejemplo, una solución
sería no válida si alguna de las variables independientes se saliera de los rangos
establecidos, los cuales están comprendidos entre –100 y 100. Es muy común en
los problemas de optimización tener restricciones en las variables, por lo que
normalmente siempre tendremos que comprobar la validez de nuestras
soluciones. Veremos cómo se hace eso más adelante.
Volviendo a nuestro problema ejemplo, la idea es encontrar los valores que
maximizan la función
Tabla 1.2. Soluciones óptimas a nuestro problema ejemplo.
Individuo x
y
1
2
3
4
100
100
-100
-100
100
-100
-100
100
Es sencillo ver que nuestro problema tiene las cuatro posibles soluciones
óptimas, mostradas en la Tabla 1.2. El objetivo en este primer capítulo
introductorio, es que nuestro algoritmo genético encuentre alguna o algunas de
las soluciones a nuestro problema de manera eficiente, es decir, en el menor
número de iteraciones posibles. Antes de continuar, es importante decir que este
problema de optimización se podría resolver sin necesidad de un algoritmo
genético; cualquier algoritmo de optimización basado en gradiente de los que
vienen incluidos en el módulo optimize de scipy10 nos valdría para obtener una
solución a nuestro problema de manera sencilla, ya que la función de nuestro
ejemplo es convexa11. No obstante, siempre es adecuado empezar con un
problema de optimización sencillo, en el que sepamos la solución para saber que
estamos haciendo las cosas bien. Aprovechamos este momento para señalar una
idea muy importante en cuanto a la aplicación de los algoritmos genéticos:
Los algoritmos genéticos se deben emplear en aquellos problemas de optimización en los que
Este comentario puede desanimarnos, ya que si no tenemos la certeza de que
vamos a obtener la solución óptima ¿qué utilidad tiene utilizar un algoritmo
genético? Pues la utilidad es elevada, ya que utilizando un algoritmo genético
tendremos una solución bastante buena y en un tiempo razonable o que al menos
podremos acotar. Más adelante veremos que ambas características son
importantes en problemas de optimización complejos. En definitiva, con un
algoritmo genético siempre vamos a terminar con una solución al problema que
será mejor que realizar una búsqueda totalmente aleatoria.
Volvamos a nuestro ejemplo. Antes de entrar en la programación del algoritmo,
vamos a visualizar el espacio de búsqueda en el que tendrá que trabajar el
algoritmo genético. El espacio de búsqueda o landscape es el conjunto de valores
que pueden tomar las variables independientes y se conoce como el dominio de
la función. En nuestro ejemplo, el espacio de búsqueda es infinito ya que
estamos trabajando con variables continuas. La Figura 1.3 representa la función
de optimización y, por lo tanto, el espacio de búsqueda. Las cuatro soluciones
óptimas al problema (ver Tabla 1.2) corresponden a los cuatro picos de la
superficie.
Figura 1.3. Representación de la función de optimización.
A continuación, mostramos el código que se ha utilizado para obtener la Figura
1.3:
En este script, la función de optimización se ha definido como
funcion_prueba(x) y la variable de entrada corresponde a la lista o vector de
variables independientes x e y. Así, la variable x corresponde a x[0] y la variable
y corresponde a x[1].
Llegados a este punto, podemos empezar a codificar nuestro algoritmo genético
utilizando el módulo deap. El procedimiento va a ser tipo receta, de forma que
hay una serie de pasos que siempre tenemos que dar y que solo se cambiarán
dependiendo de las características de nuestro problema de optimización; por
ejemplo, dependiendo del tipo de variables independientes que tengamos
(continuas, discretas, reales, binarias, etc.). Para utilizar el módulo deap es
importante tener ciertas nociones de programación orientada a objetos, ya que se
utiliza la propiedad de herencia entre clases. El diseño de algoritmos genéticos
con deap puede parecer un poco complejo al principio, pero veremos cómo al
final el procedimiento es bastante repetitivo. A continuación, vamos a dividir el
proceso en varias partes, para poder explicar cada uno de los pasos con el mayor
detalle posible. Finalmente, se mostrará el código completo que solo incluye lo
estrictamente necesario para ejecutar el algoritmo genético.
1.3 Definición del problema y generación de la
población inicial
En esta sección se definen aspectos muy relevantes del algoritmo genético, como
son: (i) el tipo de problema de optimización (maximizar o minimizar), (ii) el tipo
de objeto de Python o plantilla que va a contener el individuo (lista, vector, etc.)
y sus atributos, y (iii) el objeto caja de herramientas o toolbox que contendrá,
mediante registro, un conjunto de funciones utilizadas por el algoritmo durante
su ejecución. Entre los tipos de funciones que se registran en la caja de
herramientas, destacan las siguientes: a) las funciones para crear los individuos
de forma aleatoria, b) la función para crear la población, c) los operadores
genéticos (selección, cruce y mutación) y d) la función objetivo. En esta sección,
trataremos el registro de las funciones para a) y b), dejando para la siguiente
sección las funciones de c) y d).
A continuación, mostramos un script que incluye las sentencias para realizar las
operaciones i), ii) y iii) mencionadas anteriormente. A lo largo de la sección,
iremos explicando cada una de las sentencias de manera individual.
En conveniente aclarar que, aunque en este script se han importado las librerías
que hacen falta, en el resto de los fragmentos de código del capítulo no se
incluirán dichas líneas de código (salvo la sección de código completo), aunque
sean también necesarias.
1.3.1 Creación del problema
Comenzamos por la creación del problema y, para ello, nos apoyamos en el
método create de la clase creator. En la siguiente línea se crea el tipo de
problema:
El método create crea una nueva clase llamada FitnessMax (podemos darle el
nombre que queramos), que hereda de base.Fitness y que tiene un atributo que se
denomina weigths. Esta línea de código merece más detalles para poder entender
todo lo que se realiza en una sola sentencia. El método create tiene los siguientes
argumentos:
■name : Nombre la clase que se crea.
■base : Clase de la que hereda.
■attribute : Uno o más atributos que se quieran añadir a la clase cuando se cree.
Este parámetro es opcional.
Por lo tanto, en esa línea de código estamos creando una nueva clase que se
denomina FitnessMax12. Ese nombre no ha sido elegido al azar, ya que nos
indica que estamos ante un problema de maximización. Aunque podríamos
haber elegido cualquier otro nombre, es conveniente elegir nombres que reflejen
el tipo de problema al que nos estamos enfrentando (si lo llamamos «Problema»,
no sabremos distinguir a simple vista el tipo de problema). Esta clase hereda las
propiedades de la clase base del módulo deap. La última operación que realiza el
método create, es crear un atributo denominado weigths; este atributo es
importante ya que indica el tipo de problema de optimización que estamos
definiendo. De forma genérica, weights contendrá una tupla con tantas
componentes como objetivos tenga el problema, y con un valor que indicará si
estos objetivos son de maximización o minimización. En nuestro ejemplo, el
problema es de maximización de un solo objetivo. Esto es así porque la tupla
solo tiene un elemento con valor de (1,0,). Si el problema fuese de minimización
con un solo objetivo, la tupla weights contendría el valor de (–1,0,). Por el
contrario, si el problema fuera multiobjetivo, el atributo weights tendría tantos
unos o menos unos como objetivos se quieran definir para maximizar o
minimizar, como veremos más adelante (Capítulo 4).
El objeto base (base.Fitness) contiene los atributos encargados de almacenar el
fitness o desempeño de un individuo. En concreto, el objeto base.Fitness
contiene los siguientes atributos13:
■values : Es una tupla que contiene los valores de fitness de cada uno de los
objetivos de nuestro problema. En este primer capítulo, vamos a empezar con
problemas de un solo objetivo, pero este es solo un caso particular del problema
más general, que será multiobjetivo. Así pues, values contendrá la calidad de
cada individuo en cada uno de los objetivos de nuestro problema de
optimización.
■dominates : Devuelve verdadero ( True ) si una solución es estrictamente peor
que otra. Este atributo se utilizará en los algoritmos genéticos con múltiples
objetivos.
■valid : Indica si el fitness de un individuo es válido. Este atributo se utiliza para
saber el número de individuos que se tienen que evaluar en cada iteración del
algoritmo genético. En general, si un individuo tiene el atributo values vacío, el
atributo valid será False .
Por lo tanto, la clase que estamos creando también tendrá disponibles estos tres
atributos gracias a la propiedad de herencia de clases de Python14.
Aunque las operaciones del método create pueden parecer muy complejas, a
continuación se muestra un código equivalente en Python al método create.
Simplemente se define una nueva clase MaxFitness que hereda de otra clase
base.Fitness y que tiene un atributo en su declaración15:
Como resumen de creator.create, nos debemos quedar con que en dicha línea de
código debemos definir dos cosas:
1. El tipo de problema (maximizar 1 , 0 o minimizar –1 , 0).
2. El número de objetivos que tiene nuestro problema (uno o varios, según unos
o menos unos contenga la tupla del atributo weights ).
1.3.2 Creación de la plantilla del individuo
El método create se vuelve a utilizar, en este caso para definir la clase que
encapsula al individuo:
En esta línea de código, estamos creando una clase que se denomina Individual,
que hereda de la clase lista (por lo tanto, tiene todos los métodos de una lista16)
y que contiene el atributo fitness, el cual ha sido inicializado con el objeto
FitnessMax creado en la anterior línea. Es decir, el individuo será una lista que
tiene un atributo fitness que almacenará la calidad o desempeño de este.
Veamos, a continuación, un código equivalente realizado estrictamente en
Python sin utilizar el método creator17:
Se puede observar que la operación que realiza creator es, simplemente, crear
una nueva clase que hereda de otra y que tiene unos atributos que podemos
indicar. En definitiva, en esta sentencia lo que se está haciendo es crear la
plantilla que contendrá la información del individuo.
Definir los individuos como una lista nos permite poder acceder a cada uno de
los genes mediante la posición que ocupa. Cada posición de la secuencia es una
variable distinta. Así, volviendo a nuestro ejemplo, la primera posición será la
variable x y la segunda será la variable y.
Ya hemos definido el tipo de problema y el tipo de individuo que vamos a
utilizar. Estos dos pasos se van a dar siempre y, en la mayoría de los casos,
ambas líneas de código se repetirán con pequeñas modificaciones -dependiendo
del número de objetivos y del tipo de objeto que almacene los individuos-.
Definir los individuos como una lista de variables es un procedimiento muy
eficiente y flexible, ya que cada variable independiente será una posición de la
lista. El tamaño de la lista se define cuando se crean los individuos de la
población inicial, como veremos más adelante.
1.3.3 Crear individuos aleatorios y población inicial
A continuación, debemos definir funciones que nos permitan crear individuos
aleatorios y, en consecuencia, la población inicial. La siguiente línea define un
objeto toolbox de tipo base.Toolbox o caja de herramientas18:
Este objeto permite registrar funciones que se utilizarán durante la operación del
algoritmo genético. El registro de funciones se realiza mediante el método
register de la clase base.Toolbox. El método register tiene los siguientes
atributos:
■alias : El nombre con el que registramos la función en la caja de herramientas.
■function : La función que estamos registrando en la caja de herramientas.
■argument : Los argumentos (uno o varios) que se pasan a la función que se está
registrando.
En primer lugar, vamos a registrar las funciones que nos permiten crear
individuos aleatorios. Para ello, necesitamos desarrollar una función que nos
permita generar un valor aleatorio para cada variable independiente (cada gen
del cromosoma), esto es, cada una de las posiciones de la lista. Además,
conviene que dicho valor esté comprendido entre los valores límites de nuestras
variables, con el fin de obtener una solución factible al problema. La siguiente
sentencia realiza dicha operación:
El método register registra una función en el objeto toolbox con el nombre attr_uniform. Este
Es decir, el método register, nos permite registrar una función, que será un
método del objeto toolbox, en la caja de herramientas mediante un alias.
Después del alias, se debe indicar la función a la que se llamará cuando se utilice
el método y, a continuación, los parámetros que se le pasan a la función (si existe
alguno). Una vez que se registra la función, es posible acceder a esta desde el
objeto toolbox como un método, por ejemplo toolbox.attr_uniform(). Cada vez
que se llame a dicho método, se generará un número aleatorio comprendido
entre –100 y 100.
Registrar funciones es una funcionalidad que nos permite cambiarles el nombre y tenerlas disp
Para más información sobre la implementación del método register se
recomienda echar un vistazo al funcionamiento del método partial del módulo
nativo de Python functools19. El siguiente script muestra el código equivalente
en Python que realiza el registro de una función como un atributo del objeto
toolbox20.
A continuación, y con el fin de crear el individuo completo, necesitamos llamar a
la función que genera cada uno de los genes tantas veces como variables
independientes tengamos. Para ello, se registra una función que se denomina
individual. A su vez, esta función llama a la función tools.initRepeat de la
siguiente forma:
La función tools.initRepeat tiene como parámetros:
■container : El tipo de dato donde se almacenará el resultado del argumento func
.
■func : Función a la que se llamará n veces.
■n : Número de veces que se llamará a la función func .
En nuestro caso, el container es la clase creator.individual, creada anteriormente.
La función func es la que utilizamos para crear cada gen (toolbox.attr_uniform)
y n será el número de genes que hay que crear, que en el caso de nuestro
problema con dos variables será n = 2. Por lo tanto, el método initRepeat nos
permite ejecutar varias veces la función registrada attr_uniform y almacenar el
resultado en el individuo que queremos crear. Como resultado se crea un
individuo aleatorio. Por ilustrar el funcionamiento con un ejemplo, se puede
crear un individuo aleatorio mediante la sentencia toolbox.individual(), que
proporciona el siguiente resultado21:
Así, cada vez que se ejecute toolbox.inidivual() se creará un individuo aleatorio.
Es importante recordar que individuo es una lista que tiene un atributo fitness
donde se almacena la calidad del mismo. Dicho atributo debe estar creado junto
con el individuo y, además, debe estar vacío, ya que el individuo todavía no ha
sido evaluado. Así, si accedemos al atributo fitness de un individuo recién
creado, obtendremos el siguiente resultado22:
Una vez detallado el procedimiento para crear un individuo de forma aleatoria,
el procedimiento para crear la población inicial es análogo. La línea de código
que registra el método para crear la población inicial es:
En esta sentencia la función que se registra se llama population, la cual utiliza
initRepeat para llamar diez veces a la función individual (se llama una vez por
cada individuo que formará la población inicial). El resultado se guarda en una
lista que contiene la población inicial generada. Es decir, con respecto a los
argumentos de initRepeat, el container es una lista, la función func es
toolbox.individual y n = 10 (tamaño de la población). Aunque se ha definido un
tamaño de diez para la población inicial, este valor se puede cambiar al tamaño
que queramos. Se recomienda elegir números divisibles entre cuatro, ya que
algunas operaciones genéticas del módulo deap pueden dar problemas si no se
cumple este requisito. Como ejemplo de creación de una población inicial de
prueba, el resultado de toolbox.population sería el siguiente23:
Se puede ver que se ha creado una lista de diez listas (una por cada individuo)
con dos componentes. Si queremos acceder a alguno de los individuos, podemos
hacerlo a través del índice. Por ejemplo, para acceder al segundo individuo de la
población inicial podemos hacer:
Es conveniente hacer un pequeño paréntesis para hablar del tamaño de las
poblaciones en los algoritmos genéticos. En principio no existe un tamaño
óptimo de población para los problemas de optimización, pero sí debe estar en
proporción al número de variables independientes que tengamos. En nuestro
problema tenemos dos variables independientes x e y, y se ha definido un
tamaño de diez, que puede resultar válido ya que el problema que vamos a
resolver es bastante sencillo. De todas formas, más adelante haremos pruebas
con distintos tamaños.
Cuanto mayor sea el número de variables independientes mayor debe ser el tamaño de la pobl
No obstante, si observamos que no obtenemos resultados satisfactorios con un
tamaño determinado, podemos aumentar el tamaño de la población. Además,
debemos tener en cuenta que un tamaño mayor implica un número mayor de
evaluaciones de la función objetivo y, por lo tanto, más tiempo de computación.
Así pues, hay casos en los que el tamaño de la población viene limitado por el
tiempo que estamos dispuestos a esperar para obtener una solución para nuestro
problema.
Como resumen de esta importante sección, hasta este punto lo único que hemos
hecho ha sido definir el procedimiento para generar la población inicial. El
procedimiento puede parecer complejo, pero una vez que entendamos su
estructura veremos que la mayoría de pasos eran similares cuando resolvamos
diferentes problemas. Esto se debe a los siguientes motivos:
■Los problemas solo pueden ser de dos tipos (maximizar o minimizar); por lo
tanto, cuando se cree el problema lo único que variará será si en la tupla weights
ponemos 1.0 o -1.0 (los problemas multiobjetivo se verán más adelante).
■Los individuos serán listas en la mayoría de los casos. Por lo tanto, la siguiente
línea nos valdrá en la mayoría de casos:
■Necesitamos una función para generar cada uno de los genes de nuestro
individuo. Esto sí será diferente para cada problema. Aunque en la mayoría de
problemas con variables continuas la función random.uniform nos puede valer,
en el resto de casos simplemente tendremos que cambiar los límites.
■Una vez que tenemos la función para generar los genes de nuestro individuo, el
registro de funciones para crear individuos aleatorios y la población inicial serán
casi siempre los mismos. Lo único que podemos cambiar es el tamaño de los
individuos y la población.
1.4 Función objetivo y operadores genéticos
Continuamos con los pasos que debemos realizar para codificar nuestro
algoritmo genético. En esta sección, trataremos el registro de la función objetivo
y de los operadores genéticos en el objeto toolbox. El siguiente script muestra el
código que se describirá paso a paso en esta sección:
1.4.1 Función objetivo
La función objetivo de un algoritmo genético es, sin duda, la parte más particular
del problema de optimización. Podemos dividir las funciones objetivo en dos
tipos: (i) funciones objetivo que están codificadas en Python y (ii) funciones
objetivo que son el resultado de un programa o software externo. En el primer
caso, debemos codificar la función objetivo de nuestro problema como una
función de Python. El módulo nativo math24 y las librerías numpy y scipy25
pueden ser útiles, ya que contienen una gran cantidad de funciones matemáticas
disponibles. En el segundo caso, nuestro script de Python llamará a un programa
externo para obtener el desempeño del individuo. Este segundo caso es muy
interesante, ya que nos permite utilizar modelos más complejos incluidos en
software específicos. Sin embargo, este caso queda fuera del objetivo de este
libro y no será abordado en el mismo. Únicamente destacaremos que existen
funciones en Python para ejecutar otros programas externos. Solo por poner un
ejemplo, el módulo nativo os26, incluye la función system que permite hacer
llamadas al sistema. Otro ejemplo sería el módulo subprocess27.
Siguiendo con nuestro ejemplo, a continuación, vamos a definir nuestra función
objetivo como una función en Python, la cual se puede ver en el siguiente script.
En ella, hemos utilizado el módulo nativo math28 para calcular la raíz cuadrada:
Antes de detallar cómo registrar esta función, es importante destacar el hecho de
que si una solución no cumple las restricciones, debe ser descartada. Así, se
puede observar en el código anterior que si una de las dos variables
independientes toma valores fuera del dominio de la función, la función objetivo
devolverá un –1. Esto se conoce como aplicar la pena de muerte. Observe cómo,
al tratarse de una función de maximización, las soluciones válidas solo aportarán
valores de la función objetivo positivos y, por tanto, un –1 será un valor que
penaliza totalmente el resultado. La pena de muerte hace que un individuo no
participe en las operaciones genéticas de cruce y mutación; por lo tanto, sus
genes no se utilizarán para generar las siguientes generaciones.
La pena de muerte es un mecanismo por el cual se inhabilita a un individuo de una determinad
Para registrar la función de fitness, debemos proceder de la siguiente forma:
Podemos evaluar a un individuo generado con toolbox.individual() de la
siguiente forma:
Obtendremos el mismo resultado si evaluamos al individuo mediante
toolbox.evaluate(individuo). Un detalle importante que no debemos pasar por
alto -y que está relacionado con el módulo deap- es que la función de fitness
devuelve una tupla, con independencia del número de objetivos del problema.
Esto será así siempre, debido a que:
El caso con un único objetivo no es más que un caso particular del problema genérico multiob
Por lo tanto, no hay que olvidar que la función objetivo siempre debe devolver
una tupla, aunque una de las componentes esté vacía.
Antes de terminar con este apartado, es importante destacar la relevancia de
codificar de manera eficiente la función objetivo. Dicha función se ejecutará una
gran cantidad de veces. En consecuencia, cualquier ahorro en tiempo de
computación que podamos aplicar en la función objetivo supondrá una gran
ventaja (en el Apéndice B se aborda la paralelización de los algoritmos genéticos
en deap). Siempre que podamos, deberemos evitar bucles o condiciones que
puedan dejar colgado el algoritmo.
1.5 Operadores genéticos
A continuación, vamos a pasar a definir las operaciones genéticas.
Las operaciones genéticas son aquellos mecanismos que nos permiten generar nuevos individ
Las operaciones genéticas son de tres tipos: (i) selección (selection), (ii) cruce
(mate) y (iii) mutación (mutation). La selección es el procedimiento por cual se
seleccionan los individuos que participarán en las operaciones de cruce y
mutación. La selección es un procedimiento siempre elitista, de forma que un
individuo tendrá mayor probabilidad de dejar descendencia si su fitness es más
adecuado al problema de optimización.
En el caso de problemas de maximización, cuanto mayor sea el fitness, mayor
será la probabilidad de participar en las operaciones de cruce y mutación.
Notemos que este razonamiento está en línea con la teoría evolutiva de Darwin,
la cual indica que las posibilidades de dejar descendencia en las futuras
generaciones crecen cuando crece la adaptación del individuo al medio.
La operación de cruce es una operación probabilística que permite que dos
individuos seleccionados crucen o intercambien su información genética para
crear dos nuevos individuos. Es importante indicar de nuevo, que la operación de
cruce es probabilística; esto quiere decir que, aunque dos individuos sean
seleccionados, puede que no sean modificados. La probabilidad de cruce es un
hiperparámetro de los algoritmos genéticos que tendremos que definir. No existe
un valor óptimo universal para la probabilidad de cruce (óptimo para todos los
problemas), por lo que habrá que probar con distintos valores.
Por otro lado, la operación de mutación es una operación probabilística que
permite que un individuo seleccionado modifique su información genética para
crear un nuevo individuo. Al igual que el cruce, la mutación es una operación
probabilística cuyo resultado depende de la probabilidad de mutación, la cual
también debemos definir nosotros como otro hiperparámetro; de nuevo, no
existe un valor óptimo que sirva para todos los problemas. Por lo tanto,
tendremos que ajustarlo en cada problema.
En un algoritmo genético clásico o canónico, primero se realiza la selección de
individuos. Estos individuos seleccionados se cruzan, en caso de que la
probabilidad sea favorable, y después se mutan, de nuevo en caso de que la
probabilidad sea favorable. Como ambas operaciones son probabilísticas, se
puede dar el caso de que un individuo que se ha seleccionado no sea modificado
debido a que ninguna de las probabilidades le sea favorable. Es decir, puede
suceder que el individuo ni se cruce ni se mute. Por lo tanto, pasaría a la
siguiente generación sin ningún tipo de modificación. El ajuste de las
probabilidades de cruce y mutación es sumamente importante para el
funcionamiento adecuado de un algoritmo genético. La Figura 1.4 muestra el
flujo de creación de la descendencia u offspring de una población. Hay que
destacar, que ambos mecanismos son el motor para explorar y explotar zonas del
espacio de búsqueda. Se puede observar que un individuo seleccionado se puede
cruzar con otro individuo y/o puede sufrir mutación. Es decir, ambas operaciones
son independientes.
Figura 1.4. Flujo de creación de la descendencia u offspring de una población.
Volvamos a nuestro ejemplo para definir todas estas operaciones. En este punto
del diseño del algoritmo es donde vamos a sacar partido al módulo deap, ya que
este contiene una gran variedad de algoritmos de selección, cruce y mutación,
que nos permiten definir algoritmos genéticos de una manera sencilla29. La
Tabla 1.3 muestra todas las operaciones genéticas que están implementadas en el
módulo deap30. La aplicación de cada una de ellas dependerá del problema al
que nos enfrentemos, ya que algunas operaciones son adecuadas para problemas
con variables continuas y otras son adecuadas para problemas con variables
discretas. Se debe utilizar la documentación oficial para saber qué operación
realiza cada uno de los métodos31. No obstante, a lo largo del libro se irán
describiendo muchos de los operadores según se vayan utilizando en los
problemas. Además, el Glosario incluye información sobre dónde encontrar la
descripción de cada uno de los operadores.
Tabla 1.3. Listado de operaciones implementadas en deap.
Veamos, a continuación, el registro de los operadores genéticos que se utilizarán
en las iteraciones del algoritmo. En primer lugar, definimos el mecanismo que
utilizaremos para realizar el cruce (mate) entre individuos. En este caso,
utilizamos el operador cxOnePoint, o cruce de un punto. Es importante recordar
que esta operación es transparente para nosotros, ya que el operador será
utilizado internamente por el algoritmo genético utilizado como caja negra
(black box optimization32).
En este caso, al ser la longitud de los individuos dos, solo existe un posible
punto de cruce. Por lo tanto, en nuestro ejemplo el cruce es simplemente
intercambiar los valores de x e y.
Para la mutación se ha utilizado el operador mutGaussian (mutación Gaussiana)
con una media de cero y una desviación típica de 5. Estos valores son solo de
ejemplo y no garantizan ser los más adecuados. Es por ello que se deben probar
distintos valores para ver el funcionamiento del algoritmo genético en función de
dichos valores.
Es importante elegir adecuadamente el parámetro indpb, que define la
probabilidad de mutación de cada gen (no olvidemos que tanto la operación de
cruce como la de mutación son operaciones probabilísticas).
Las probabilidades de cruce y mutación que utilizará el algoritmo genético se
definirán más adelante. En el caso de la mutación, se deben definir dos
probabilidades: la probabilidad de mutar un individuo y la probabilidad de mutar
cada uno de los genes del individuo (indpb). En nuestro ejemplo, hemos definido
una probabilidad indpb de 0.1. Este valor, en general, debe ser bajo para que la
mutación no modifique en exceso al individuo. Cabe destacar que valores muy
altos de esta probabilidad pueden provocar que el algoritmo no converja
correctamente, o que no se intensifiquen ciertas zonas del espacio de búsqueda.
En resumen, el método tools.mutGaussian recibe como parámetros de entrada un
individuo seleccionado y los parámetros mu, sigma e indpb. Es importante
indicar que, al igual que ocurre con el cruce, la operación de mutación se
aplicará de forma transparente a nosotros como usuarios. Será el algoritmo como
caja negra quien se encargue de realizar todas las operaciones genéticas.
Para el proceso de selección, se ha utilizado el operador selTournament, que nos
permitirá realizar una selección mediante torneo. En este caso fijaremos el
tamaño igual a tres. Se ha demostrado que este tamaño funciona relativamente
bien para la mayoría de los casos (Lones, 2011).
El algoritmo realiza tantos torneos como individuos tiene la población, ya que tal y como se mostró en la Figura 1.4-, los individuos seleccionados primero se
cruzan y después se mutan. De nuevo, el proceso de selección es transparente
para nosotros, ya que lo realiza internamente el algoritmo genético.
Aunque se ha demostrado que un tamaño de torneo de tres es válido para la
mayoría de los casos, cuando la población crece mucho se deben utilizar
tamaños más altos para hacer más rápida la convergencia del algoritmo (se
hablará de ello en el Capítulo 2). La selección con torneo es muy elitista y hace
que el algoritmo converja a mayor velocidad si lo comparamos con otros
algoritmos de selección como, por ejemplo, la selección mediante ruleta (se
abordará en el Capítulo 3). Se debe observar que todas las funciones han sido
registradas en el objeto toolbox utilizando el método register mediante un alias
(primer parámetro del método register). Dichos alias no deben ser modificados,
ya que son utilizados por la función del submódulo algorithms de deap que
implementa el algoritmo genético como una caja negra o black box. Es decir,
esos nombres no son elegidos al azar y deben ser respetados.
Los alias del registro de los operadores genéticos deben ser: mate para el cruce, mutate para la
Lo que sí podemos cambiar son las funciones del submódulo tools que se
utilizan para realizar las operaciones de selección, cruce y mutación (ver Tabla
1.3). Incluso podemos definir nuevos operados genéticos que se ajusten a
nuestro problema de optimización (este paso lo haremos más adelante).
Antes de continuar con los siguientes pasos del algoritmo genético, es
importante volver a destacar la importancia de los operadores genéticos y la
función que tiene cada uno. En el caso del cruce, el objetivo es encontrar bloques
dentro de la cadena cromosómica que den origen a buenos resultados de la
función de evaluación. Estos bloques serán intercambiados con mayor
probabilidad a otros individuos. Por lo tanto, mediante las operaciones de cruce
los individuos tenderán a parecerse los unos a los otros a lo largo de las
generaciones. Sin embargo, mediante operaciones de cruce el poder exploratorio
del algoritmo está limitado por los valores máximos y mínimos de los genes en
la población inicial. Veamos dicha limitación con un ejemplo. Imaginemos que
los valores máximos y mínimos de las variables x e y de la población inicial de
nuestro problema los representamos en un plano x vs y, tal y como muestra la
Figura 1.5. Se puede observar que los valores máximos y mínimos determinan
un área de confinamiento de todas las soluciones que podemos obtener según la
población inicial generada. Es decir, simplemente con operaciones de cruce no
podremos salirnos de dicha área, ya que las operaciones de cruce lo único que
hacen es intercambiar información genética33. Así, si dicha zona es amplia, las
operaciones de cruce permitirán explorar una gran cantidad de soluciones dentro
de la misma. Pero si el óptimo global está fuera de dicha región, nunca lo
podremos encontrar simplemente aplicando operaciones de cruce. No hay que
confundir la zona de confinamiento de posibles soluciones dada por los valores
máximos y mínimos de cada variable en una generación, con los valores
máximos y mínimos de las variables, que en nuestro caso siempre serán [–
100,100]. Es importante indicar, que en problemas con más dimensiones, no
tendremos un área de confinamiento, sino un hiperplano de n dimensiones,
siendo n el número de variables independientes del problema.
Figura 1.5. Limitaciones de exploración de los operadores de cruce.
El operador de mutación, nos permite ampliar el área de las posibles soluciones
de la población inicial (y cualquier otra generación), incrementando los valores
máximos y mínimos de las variables independientes. Con respecto a la Figura
1.5, las operaciones de mutación nos permiten ampliar el recuadro rojo. La
Figura 1.6 muestra tres hijos creados con la mutación Gaussiana para distintos
valores de σ. Se puede observar que conforme aumenta el valor de σ, aumenta la
distancia de los hijos con respecto a los padres. Podemos ver, en la Figura 1.6,
que los genes pueden desplazarse en cualquier dirección. Por lo tanto, mediante
la operación de mutación, se puede modificar la zona de confinamiento
presentada en la Figura 1.5. El valor de σ es sumamente importante, ya que
podemos observar, que para valores bajos (σ = 1) prácticamente no modificamos
el gen.
Figura 1.6. Generación de progenitores mediante mutación Gaussiana.
Por último, la operación de selección nos permite aplicar una componente elitista
al algoritmo, de manera que aquellos individuos que mejor se adapten serán los
que con mayor probabilidad intercambien sus genes o los muten.
1.6 Últimos pasos: Algoritmo genético como caja
negra
Ya tenemos casi todo listo para lanzar nuestro algoritmo genético. En esta
sección describiremos, por un lado, la función main que configura el algoritmo
genético y, por otro lado, la representación de los resultados del algoritmo. El
siguiente script muestra el código que se analizará en esta sección:
1.6.1 Configuración algoritmo genético
La primera sentencia de la función main define la semilla del generador de
números aleatorios34. Este paso se suele dar para tener resultados reproducibles;
hablaremos de este aspecto en la siguiente sección.
A continuación, se definen tres parámetros muy importantes del funcionamiento
del algoritmo, como son la probabilidad de cruce CXPB, la probabilidad de
mutación MUTPB, y el número de generaciones NGEN. En cuanto a las
probabilidades de los operadores genéticos, en nuestro caso se ha definido una
probabilidad de cruce de 0.5 (50%), una probabilidad de mutación de 0.2 (20%)
y un número de generaciones igual a 20. Estos valores no son mágicos -ni
siquiera tienen por qué ser los óptimos para nuestro problema-, solo son unos
valores de prueba para obtener unos resultados preliminares35.
A continuación, se genera la población inicial mediante el método population del
objeto toolbox. Veremos, en otros capítulos, que el tamaño de la población se
puede definir también en este punto.
Después, se define un objeto hof de tipo HallOfFame que, como indica la
documentación36, almacena el mejor individuo encontrado a lo largo de las
generaciones del algoritmo genético.
El método HallOfFame recibe dos parámetros:
■maxsize : Número de individuos a almacenar.
■similar : Una función para comparar si dos individuos son iguales. Si no se
pone nada, utilizará por defecto el método operator.eq del módulo operator 37.
En nuestro caso, se ha definido maxsize como 1. Hay que destacar, que este es el
mecanismo que implementa deap para no perder al mejor individuo a lo largo de
la evolución del algoritmo. Es decir, con el algoritmo eaSimple que veremos a
continuación, se puede dar el caso de que la mejor solución se pierda debido a
los operadores genéticos. Es por ello que el objeto hof es importante para no
perder nunca esta solución. La clase HallOfFame se encuentra definida en el
submódulo tools y se debe indicar el número de individuos que debe almacenar.
En nuestro caso solo almacena uno, ya que solo estamos interesados en
almacenar el mejor individuo creado a lo largo de las generaciones. En cada
generación del algoritmo genético, el objeto hof es actualizado mediante el
método update de manera transparente para nosotros. El método update recibe
como entrada la población actual y actualiza el contenido del objeto hof. Cabe
destacar que, en este ejemplo, en ningún momento se ha utilizado elitismo para
hacer que los mejores individuos avancen directamente (se hablará más adelante
del elitismo).
El siguiente paso es definir un objeto para generar las estadísticas de la
población a lo largo de las generaciones del algoritmo. Este objeto es de tipo
Statistics y se encuentra definido en el submódulo tools.
Al crear el objeto se debe indicar sobre qué atributo de los individuos se van a
generar las estadísticas 38. En nuestro caso, las estadísticas se van a generar
sobre el fitness de los individuos. A continuación, se deben registrar en el objeto
stats las funciones estadísticas que se van a utilizar. Para registrar funciones se
debe utilizar el método register, que recibe los mismos parámetros de entrada
que el método register del objeto toolbox. Por lo tanto, el procedimiento es
análogo al que se realizó para registrar las funciones en la caja de herramientas.
En primer lugar, se incluye un alias y, como segundo parámetro, se indica la
función a la que se llamará. En este caso se han utilizado funciones de librería de
numpy. Las funciones que se registran calculan la media (np.mean39), la
desviación típica (np.std40), el mínimo (np.min41) y el máximo (np.max42) para
cada generación del algoritmo.
Dichos cálculos se realizarán de manera trasparente para nosotros. El objeto stats
tiene un método, denominado compile43, que recibe como entrada la población
que permite generar las estadísticas. A dicho método se lo llama internamente en
cada generación del algoritmo.
Llegado este momento, estamos en disposición de poder ejecutar el algoritmo
genético. En este ejemplo, vamos a utilizar un algoritmo genético de librería. La
librería deap dispone de varias implementaciones de algoritmos genéticos listos
para ser utilizados de manera sencilla como cajas negras o black boxes. Las
distintas versiones de algoritmos genéticos que están disponibles en deap se
encuentran en el submódulo algorithms44. En este primer ejercicio, vamos a
utilizar el algoritmo eaSimple. Una vez configurado, podemos lanzar la función
main para ver los resultados.
1.6.2 Resultados del algoritmo genético
Para ejecutar la función main, nos faltaría incluir la siguientes líneas45:.
Se puede observar que la función main devuelve el mejor individuo almacenado
en el objeto hof, así como la población final. El Resultado 1.1 muestra la
solución obtenida por el algoritmo genético.
Resultado 1.1. Resultados del algoritmo genético.
Durante la ejecución del algoritmo se han ido generando los datos referentes al
número de generaciones (gen), número de evaluaciones (nevals), desempeño
medio de la población (avg), desviación típica (std), valor mínimo de la
población (min) y valor máximo de la población (max). Estos datos
corresponden a las funciones registradas en el objeto stats. Finalmente se indica
el mejor fitness obtenido (136.77) y el mejor individuo encontrado [–94.69,–
98.70]. Es decir, la solución es x = –94.69 e y = –98.70. Podemos observar que
el valor obtenido está muy cerca del valor óptimo real, pero no es exactamente el
mismo. Con respecto al código, hay que indicar que aunque el objeto hof se ha
definido de forma que solo almacene un individuo, este objeto es una secuencia
(funciona como una lista en Python), por lo que para obtener tanto el fitness
como el individuo debemos utilizar el índice cero. Debemos recordar que para
acceder al fitness del individuo tenemos que invocar el atributo fitness.values.
Es importante volver a indicar que en este problema estamos «haciendo trampa»,
ya que estamos optimizando una función cuyo valor máximo sabemos cuál es.
Saber el resultado óptimo puede llevarnos a pensar que el algoritmo genético no
está funcionando bien ya que no ha sido capaz de encontrar dicho valor. Nada
más lejos de la realidad, ya que el algoritmo genético nos ha proporcionado en
muy pocos pasos una solución que es bastante buena -no es la mejor, pero está
muy cerca-. Además, como veremos a continuación, todavía podemos mejorar
los resultados.
Analizando los datos de funcionamiento mostrados en el Resultado 1.1, podemos
hacer algunos comentarios generales:
■A lo largo de las generaciones, el valor medio avg de la población se va
incrementando. Esto es positivo ya que significa que los individuos que forman
la población son, en media, mejores.
■La desviación típica std va disminuyendo, en general. Esto indica que los
individuos cada vez son más parecidos. Esto es esperable ya que el algoritmo
genético es elitista por el proceso de selección, por lo que los individuos
tenderán a parecerse.
■El valor máximo max va aumentando a lo largo de las generaciones. Esto es
tremendamente positivo ya que indica que el algoritmo está funcionando
correctamente.
La columna nevals en el Resultado 1.1 muestra el número de individuos que han
sido evaluados. Este número no se corresponde con el número total de
individuos de la población; esto es así porque las operaciones de cruce y
mutación son probabilísticas. Por ello, un individuo seleccionado no tiene por
qué ser modificado. Todo individuo que, aun siendo seleccionado, no haya
participado en ninguna operación genética, no será evaluando, ahorrando así
operaciones redundantes. Si aumentamos la probabilidad de mutación,
aumentaremos el número de individuos que serán evaluados en cada generación.
Ocurrirá lo mismo si aumentamos la probabilidad de cruce. Un aspecto que no
está optimizado en el módulo deap es que si dos individuos son iguales no se
evalúen dos veces. Esto es así porque el parámetro que hace que un individuo
sea evaluado o no es la validez de su fitness, atributo valid del fitness. Cuando
un individuo es modificado debido a una operación genética (cruce o mutación),
su fitness se invalida, esto es, se pone a False. En cada generación, todos los
individuos que tienen un fitness inválido deben ser evaluados. Esta
comprobación se realiza internamente en el algoritmo eaSimple. Así, aunque dos
individuos sean exactamente iguales, ambos tendrán fitness válidos, por lo que
serán evaluados de nuevo. Por lo tanto, aunque a lo largo de las distintas
generaciones se generen individuos iguales, estos serán evaluados de nuevo46.
1.7 ¿Cómo conseguir resultados consistentes?
La cuestión ahora es la siguiente: ¿corremos una sola vez el algoritmo genético y
aceptamos el resultado? La respuesta es: por supuesto, no. Como ya hemos
repetido en varias ocasiones, un algoritmo genético es un algoritmo estocástico
y, además, tiene muchos parámetros de ajuste que deben ser modificados para
ver cómo afectan a los resultados. Así, para mostrar un resultado consistente
debemos obtener y mostrar ciertas estadísticas sobre el comportamiento de
nuestro algoritmo genético. A continuación, se detallan algunas buenas prácticas,
las cuales llevaremos a cabo en este y sucesivos ejemplos:
■Aumentar la población hasta que no veamos mejoras significativas. Podemos
probar con pocas generaciones e ir aumentando el número de individuos.
■Aumentar el número de generaciones y comprobar que el algoritmo converja.
Para ello, lo ideal es mostrar una gráfica de convergencia del algoritmo. Lo
veremos en la siguiente sección.
■Hacer un barrido de valores de probabilidades de cruce y mutación, y mostrar
algunas estadísticas para ver de qué manera afectan dichas probabilidades. Lo
veremos en los siguientes capítulos.
Por ejemplo, si probamos con una población de 30 individuos, los resultados que
obtenemos son bastante mejores, como muestra el Resultado 1.2.
Resultado 1.2. Resultados del algoritmo genético con una población de 30
individuos.
El mejor fitness ha cambiado de 136.77 a 140.10, lo que significa una mejora
considerable. Pero, en este punto, debemos preguntarnos algo: ¿dicha mejora es
debida a que hemos aumentado el número de individuos o es simplemente azar?
Para comprobar qué es lo que está ocurriendo debemos lanzar el algoritmo
genético con ambas configuraciones y obtener algunas métricas. El siguiente
script muestra el código necesario para lanzar el algoritmo genético 20 veces. Se
ha utilizado la lista lista_mejores para almacenar el fitness del mejor individuo
de cada intento del algoritmo genético. Cambiando el valor de range, podemos
cambiar el número de veces que se lanza el algoritmo. Al terminar el bucle, se
calcula la media y el mejor resultado de todos los intentos.
Es importante recordar que en la sección anterior se ajustó la semilla mediante
random.seed(42) en el main; mediante esta sentencia hacemos que siempre se
genere la misma población inicial y que los resultados probabilísticos sean los
mismos. En este caso, la semilla se debe ajustar fuera del main; en caso
contrario, obtendríamos los mismos resultados para las 20 iteraciones del
algoritmo.
Utilizando el script anterior, se debe ejecutar el algoritmo genético para una
población de 10, 30 y 50 individuos. Se destaca que, al igual que en casos
anteriores, el algoritmo genético se ejecuta dentro de la función main; por lo
tanto, al estar dentro de una función, permite que se ejecute varias veces de una
manera sencilla. En capítulos posteriores, veremos cómo podemos pasar a la
función main argumentos de configuración del algoritmo como, por ejemplo, las
probabilidades de mutación y cruce.
La Tabla 1.4 muestra los resultados obtenidos para los tres casos. Aunque el
valor máximo en los tres casos es parecido, en media se puede ver que los
resultados del algoritmo con una población de 30 o 50 individuos son
significativamente mejores. Por otro lado, los resultados entre 30 y 50 individuos
no son muy diferentes con respecto al valor medio. En vista de los resultados,
parece lógico pensar que aumentar más la población no nos aporta mucho,
mientras que sí estamos aumentando la carga computacional del algoritmo. En la
Tabla 1.4 podemos observar que, para una población de 50 individuos y 20
generaciones, el número de veces que debemos evaluar la función objetivo es
1000. En este ejemplo sencillo, la función de evaluación se ejecuta rápidamente,
por lo que este aspecto no es importante; pero cuando tengamos funciones
objetivo mucho más complejas, el número de evaluaciones sí será importante.
Tabla 1.4. Comparación de resultados para distintos tamaños de población.
No Individuos
Valor medio
Máximo
No Evaluaciones
10
30
50
132.8
139.0
145.9
140.5
141.2
141.3
200
600
1000
Aunque en esta sección se ha estudiado el impacto del tamaño de la población,
no se ha abordado el análisis de otros parámetros importantes como son las
probabilidades de cruce y mutación. El estudio de dichos parámetros se deja para
los siguientes capítulos, donde se abordarán problemas más complejos.
1.8 Convergencia del algoritmo
El siguiente paso que vamos a dar es comprobar la convergencia del algoritmo.
Para ello, vamos a utilizar un objeto de tipo Logbook47, el cual nos permite
almacenar todos los datos de evolución del algoritmo genético en un registro.
Hay que recordar que en la función main se declaró el objeto logbook de la
siguiente forma:
Como se indicó anteriormente, el objeto logbook guarda un registro de la
evolución del algoritmo genético48. La información se almacena mediante
diccionarios de Python. La clase Logbook tiene diversos métodos, entre los que
destacan los siguientes:
■record : Generar una nueva entrada en el registro. Los registros tienen
asociados un nombre o key como los diccionarios de Python . Normalmente,
como keys se utilizan los alias de las funciones registradas en el objeto stats .
■select : Permite obtener la información asociada a una key .
El siguiente script muestra la función que nos permite representar la evolución
del algoritmo genético. Hay que destacar que se ha utilizado el módulo
matplotlib.pyplot para generar dicha gráfica. La función plot_converge recibe
como parámetro de entrada el objeto logbook, que se actualiza en cada
generación en el algoritmo genético eaSimple. El objeto logbook permite
mantener un registro de las métricas calculadas en cada generación. Para obtener
los datos del registro se debe utilizar el método select, pasando como parámetro
de entrada, el alias utilizado para registrar las funciones en el objeto stats.
La función plot_evolucion se debe ejecutar una vez termine el algoritmo
genético, como se expone a continuación:
La Figura 1.7 muestra la evolución del algoritmo genético representando tres
curvas: en azul se muestra el valor mínimo de la función objetivo en cada
generación, en color negro con línea discontinua se muestra el valor medio en
cada generación y, por último, en rojo se muestra el valor máximo en cada
generación. Se puede observar que el algoritmo converge a partir de la
generación número 10. Al ser un problema sumamente sencillo, prácticamente
desde la primera generación aparecen soluciones cercanas al mejor individuo.
No obstante, se puede observar que el valor medio de la generación va en
aumento durante las primeras generaciones. Esto es así hasta que la mayoría de
los individuos de la población son parecidos, por lo que el valor medio de la
generación se estabiliza. Los picos -tanto en el valor mínimo como en el valor
medio- se deben a que, a causa de las operaciones genéticas, pueden aparecer
individuos que no se ajusten bien a la función objetivo, por lo que generen un
fitness muy bajo que afectará tanto a la media como al valor mínimo. Debemos
recordar que se ha aplicado la pena de muerte, por lo que se penaliza con un
fitness de -1 a aquellos individuos que no cumplen las restricciones.
Figura 1.7. Evolución del algoritmo genético.
En general, a lo largo del libro se va a utilizar la pena de muerte en aquellos
casos en los que no se cumplan las restricciones del problema. No obstante, la
librería deap permite realizar penalizaciones menos drásticas que la pena de
muerte49.
1.9 Exploración versus explotación en algoritmos
genéticos
Esta sección aborda un aspecto de tremendo debate en el ámbito de la
optimización metaheurística como es el dilema «exploración versus
explotación». Imaginemos un problema de optimización de una sola dimensión,
tal y como se muestra en la Figura 1.8, en el que queremos maximizar y = f (x).
Por un lado, la exploración del algoritmo debe permitir que todas las zonas del
espacio de búsqueda de f (x) sean exploradas. Si alguna zona no es explorada,
podemos perder la oportunidad de alcanzar el óptimo global, y quedarnos en un
óptimo local o relativo. Por ejemplo, en la Figura 1.8 podríamos quedarnos en un
máximo relativo y no alcanzar nunca el máximo absoluto. Por otro lado, el
mecanismo de explotación de un algoritmo debe permitir intensificar ciertas
zonas del espacio de búsqueda. Volviendo a la Figura 1.8, cuando el algoritmo
genético es capaz de encontrar una solución cerca del máximo absoluto, debe
tener la capacidad de seguir intensificando la búsqueda por dicha zona hasta
alcanzar dicho máximo. Una vez que una solución encuentra una montaña en la
Figura 1.8, esta debe ser capaz de generar nuevas soluciones que suban por la
ladera de la montaña hasta alcanzar el pico (máximo absoluto); así podría verse
el proceso de intensificación.
Figura 1.8. Exploración versus explotación.
La cuestión ahora es saber qué mecanismo u operadores genético (selección,
cruce y/o mutación) favorece la exploración y cuál la explotación. La respuesta a
esa cuestión no está clara y es un tema de discusión en la literatura especializada.
Es evidente que la combinación de selección, cruce y mutación es lo que permite
que el algoritmo genético explore y explote distintas zonas del espacio de
búsqueda. Aunque la aportación de cada operación genética es un tema abierto a
debate, permitan que aportemos nuestro punto de vista al respecto.
Durante las primeras generaciones, en las que los individuos de la población son
distintos (siempre y cuando se garantice una buena diversidad en la población
inicial), la operación de cruce permite explorar -mediante el intercambio de
información genética entre individuos- la zona de confinamiento que determinan
los valores máximos y mínimos de cada una de las variables de los individuos
(ver Figura 1.5). En cuanto a la mutación, permite explotar ciertas regiones del
espacio de búsqueda próximas a los individuales actuales. Es decir, al principio
la zona de confinamiento es grande, por lo que hay mucho que explorar dentro
de ella. Por el contrario, cuando avanzamos en las generaciones, los individuos
son cada vez más parecidos, ya que tienden a converger, estrechando la zona de
confinamiento. Así, la mutación es el único procedimiento que nos permite
obtener nuevas soluciones fuera de la información que ya tienen los individuos.
En este punto, la mutación es el único mecanismo para explorar nuevas zonas y,
a su vez, explotar las soluciones encontradas. En cuanto a la selección, las
principales técnicas de selección (torneo y ruleta) son técnicas elitistas y, en
consecuencia, promueven la explotación de buenos individuos.
Por último, debemos tener en cuenta que, debido a la naturaleza estocástica del
algoritmo genético, una buena solución podría perderse a lo largo de la
evolución del mismo. De todas formas, más adelante veremos que el módulo
deap implementa mecanismos para que esto no suceda.
1.10 Código completo y lecciones aprendidas
Para finalizar esta sección introductoria, el Código 1.3 contiene todas las líneas
necesarias para implementar un algoritmo genético sencillo que nos permita
resolver el problema planteado en este capítulo, es decir, resolver la ecuación
bajo las restricciones {x,y} [–100,100]. Como resumen del código:
■Las líneas 1-8 incluyen los módulos y submódulos necesarios para
implementar el algoritmo genético con deap , librerías básicas de Python como
random y math , y librerías científicas de Python como numpy y matplotlib .
■La línea 11 define la clase FitnessMax de los individuos, que establece el tipo
de problema (maximización) y el número de objetivos del problema (uno en este
caso).
■La línea 12 define la clase del individuo, que hereda de una lista, y crea un
atributo en dicha clase para el fitness del tipo FitnessMax , justo el que se ha
creado en la línea 11. Estamos creando así la plantilla de los individuos.
■La definición de la función objetivo funcion_objetivo) se presenta en las líneas
14-22. La función debe recibir un individuo de entrada (se ha denominado x en
estado caso) y debe devolver la calidad del individuo return res . Debemos
recordar que siempre hay que devolver una tupla porque el problema con un
único objetivo en deap es un caso particular del problema con múltiples
objetivos.
■La línea 24 crea el objeto caja de herramientas ( toolbox ) donde se registrarán
las funciones para generar los genes o variables (línea 27) de los individuos
aleatorios (línea 30), la población inicial (línea 32), la función objetivo (línea 36)
y los operadores genéticos (líneas 37-40). Es importante volver a destacar que el
último parámetro de la línea 30 representa el número variables del problema (2
en este caso) y que el último parámetro de la línea 32 define el número de
individuos de la población.
■La operación de cruce que se realiza es un cruce de un punto (línea 37). En
cuanto a la mutación, se realiza una mutación Gaussiana de media ( mu ) 0, y
desviación típica ( sigma ) 5. La probabilidad de mutar cada uno de los genes es
de 0.1 (parámetro indpb en la línea 38).
■La selección de los individuos es mediante torneo (línea 40). El tamaño del
torneo es de 3.
■No hay que olvidar que los nombres con los que se registran las operaciones
genéticas y la función objetivo se deben respetar ( selection , mate , mutation y
evaluate ).
■La función plot_convergencia nos permite visualizar la evolución de los
individuos (líneas 42-62). A su vez, nos permite almacenar una gráfica que nos
muestra el mejor individuo en cada generación, el peor y el valor medio.
Además, la figura se guarda en el directorio de trabajo.
■La función main() ejecuta el algoritmo genético. En primer lugar, se ajusta la
semilla del generador de números aleatorios (línea 65). Seguidamente, en la
línea 66, se definen las probabilidades de cruce CXPB , mutación MUTPB , y el
número de generaciones del algoritmo NGEN . En la siguiente sentencia, se
define la población inicial (línea 67). A continuación, se define el objeto hof que
contendrá el mejor individuo a lo largo de la evolución (línea 68). El objeto
estadística ( stats ) se define en la línea 69 y, seguidamente, se registran las
funciones estadísticas que se aplicarán (líneas 70-73). El registro de evolución se
define en la línea 74. Finalmente, el algoritmo se inicia en la línea 75, mediante
la llamada del método eaSimple con todos los parámetros que necesita el
algoritmo genético. Una vez finalizado, se devuelven el mejor individuo y el
registro de evolución (línea 78).
■En la línea 81 se llama a la función main() y, a continuación, se imprime el
fitness del mejor individuo junto al individuo en sí (líneas 82 y 83). Por último,
en la línea 84, se llama a la función plot_evolution , a la que se le debe pasar
como parámetro de entrada el registro log , y que permite visualizar y almacenar
la convergencia y evolución del algoritmo.
Código 1.3. Código completo: Problema sencillo con variables continuas.
En cuanto a las lecciones aprendidas en este primer capítulo introductorio, son
muchas y nos acompañarán a los largo del resto del libro. Por lo tanto, no
debemos olvidar los siguientes aspectos:
■Los algoritmos genéticos son algoritmos estocásticos, es decir, están basados
en operaciones genéticas probabilísticas: selección, cruce y mutación. El
funcionamiento del algoritmo depende de las probabilidades de cruce y mutación
(hiperparámetros). La selección también tiene una componente estocástica, ya
que los individuos que participan en el torneo se seleccionan de manera
aleatoria.
■Los algoritmos genéticos son elitistas, de forma que un individuo que tiene un
fitness alto tendrá más posibilidades de participar en las operaciones genéticas.
Por ejemplo, en el caso de la selección mediante torneo, un individuo con fitness
alto tendrá más posibilidades de ganar torneos.
■Los algoritmos genéticos no garantizan encontrar el óptimo absoluto, pero sí
podemos obtener soluciones que satisfagan nuestros requisitos en un tiempo
moderado.
■Los algoritmos genéticos son computacionalmente intensivos, ya que se basan
en evaluar la función objetivo muchas veces. Si definimos N como el número de
generaciones, P como el número de individuos de la población y T como el
tiempo que tarda en ejecutarse la función objetivo, el tiempo total para obtener la
solución al problema es T t = N × P × T . Si, por ejemplo, en el ejercicio anterior
consideramos una población de 100 individuos, con 100 generaciones y cuya
función objetivo tarda 1 segundo en ejecutarse, tendremos un T t = 10 , 000 s .
Es decir, más de 2 horas y 42 minutos 50. Si suponemos que la función objetivo
tarda 5 veces más, tendremos que esperar más de 10 horas para tener el
resultado. Es por ello, que siempre debemos estimar el tiempo que tardará el
algoritmo en ejecutarse completamente. Es importante indicar que existen
mecanismos para evaluar los individuos de una población en paralelo (consultar
Apéndice B).
■Si queremos obtener resultados consistentes, debemos evaluar el
funcionamiento genético utilizando diferentes configuraciones en términos de
tamaño de población y probabilidades de los operadores genéticos. En cuanto al
tamaño de la población, es buena idea empezar por un orden de magnitud mayor
al número de variables que tiene nuestro problema. No obstante, si la función de
evaluación se evalúa en poco tiempo, se puede y se debe probar con poblaciones
mayores. En cuanto a las probabilidades de los operadores genéticos, se debe
hacer un barrido de probabilidades hasta encontrar la mejor configuración.
Dicho barrido se debe realizar ejecutando varias veces el algoritmo genético para
una misma configuración y obteniendo métricas estadísticas de los resultados
obtenidos. Por último, se debe comprobar la evolución y convergencia del
algoritmo para visualizar que el algoritmo está evolucionando de manera
correcta y que converge en un valor.
1.11 Para seguir aprendiendo
Para aquellos que deseen seguir profundizando en los temas relacionados con
este primer capítulo, se recomiendan las siguientes referencias:
■Uno de los trabajos pioneros del creador de la computación evolutiva John
Holland es (Holland et al., 1992).
■Además, se recomiendan los trabajos de David E. Goldberg (realizó la tesis
doctoral con John Holland), entre ellos (Goldberg, 2006) (Zames et al., 1981).
■Más información sobre algoritmos genéticos y técnicas metaheurísticas de una
forma práctica, se puede encontrar en (Smith, 2012).
■En (Ser et al., 2019) se encuentra una de las referencias más actualizadas y
completas sobre algoritmos de optimización bioinspirados. Este artículo presenta
una amplia fotográfica del espectro de técnicas que han surgido en los últimos
años. Además, para cada tema los autores aportan una gran bibliografía.
■Para más información sobre el dilema «exploración versus explotación» en
algoritmos evolutivos se recomienda la siguiente referencia: (Črepinšek et al.,
2013).
Como ejercicios, se plantean los siguientes:
■La función de Rastrigin 51 se define de la siguiente forma: con A = 10 y xi
[–5,12,5,12]. Implemente dicha función en Python y minimícela para distintos
valores de n. El mínimo global de la función se encuentra en x* = (0,...,0) y f
(x*) = 0.
■Para la función anterior con n = 10, compare el funcionamiento del algoritmo
genético para distintos métodos de cruce; por ejemplo, cruce de un punto, de dos
puntos y uniforme. Realice un barrido para distintas probabilidades de cruce y
mutación, y compare el valor mínimo, medio y la desviación típica.
■En el Apéndice B del libro se aborda un método para medir el tiempo que tarda
el algoritmo genético en ejecutarse utilizando la librería time . Haga las
modificaciones oportunas en el ejercicio anterior para medir el tiempo que tarda
en ejecutarse el algoritmo.
■Busque información en Internet sobre la función de Bohachevsky .
Impleméntela en Python y obtenga el mínimo global mediante un algoritmo
genético.
_________________
1Los tres términos hacen referencia a la función que determina la calidad o
desempeño de una solución.
2A lo largo del libro se utilizará el anglicismo fitness para hacer referencia a la
calidad o desempeño de una solución.
3En la mayoría de los textos técnicos escritos sobre optimización se suele
utilizar el problema de minimización como norma.
4https://deap.readthedocs.io/en/master/
5En la sección 2.10 hablaremos más sobre la complejidad de los problemas.
6https://docs.python.org/3/library/random.html
7https://docs.scipy.org/doc/numpy-1.16.1/reference/routines.random.html
8https://docs.python.org/3.6/library/random.html
9La traducción sería «comprensión de lista» pero suena tan mal que preferimos
dejarlo en inglés.
10https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html#scipy.op
11La solución que nos daría cualquier algoritmo basado en gradiente dependería
del valor inicial que se le pasara al algoritmo.
12Para problemas de minimización se suele utilizar FitnessMin.
13https://deap.readthedocs.io/en/master/api/base.html#deap.base.Fitness
14https://docs.python.org/3.6/tutorial/classes.html
15Este código equivalente no se utiliza en el problema; solo se define para
ilustrar el funcionamiento de creator.create.
16https://docs.python.org/3/tutorial/datastructures.html
17Este código equivalente no se utiliza en el problema; solo se define para
ilustrar el funcionamiento de creator.create.
18https://deap.readthedocs.io/en/master/api/base.html#toolbox
19https://docs.python.org/3/library/functools.html
20Este código equivalente no se utiliza en el problema; solo se define para
ilustrar el funcionamiento de toolbox.register.
21En cada caso el resultado puede ser distinto.
22Lo mismo se puede hacer para el resto de atributos de individual.
23El resultado puede variar en cada caso.
24https://docs.python.org/3.6/library/math.html
25https://www.scipy.org/scipylib/index.html
26https://docs.python.org/3/library/os.html
27https://docs.python.org/3.6/library/subprocess.html
28https://docs.python.org/3/library/math.html
29También podemos definir nosotros nuevos operadores. Pero este
procedimiento es más complejo y se verá más adelante.
30Se recomienda consultar la documentación de manera periódica, ya que
nuevas versiones del módulo suelen añadir nuevas funcionalidades.
31https://deap.readthedocs.io/en/master/api/tools.html?highlight=tools
32Este término se suele utilizar en la literatura para hacer referencia a que se
utiliza el algoritmo de optimización como una caja negra, sin saber realmente
cómo funciona por dentro.
33Esto es así según el operador de cruce de un punto que hemos definido.
Existen algunos operadores de cruce que permiten que nos salgamos de la zona
de confinamiento.
34El orden de pasos que se da en la función main puede variar, ya que hay pasos
que son intercambiables.
35De hecho, con unos valores adecuados el algoritmo convergería muy rápido.
Es por ello, que se han elegido esos valores para poder ver cierta evolución.
36https://deap.readthedocs.io/en/master/api/tools.html#hall-of-fame
37https://docs.python.org/3/library/operator.html#operator.eq
38https://deap.readthedocs.io/en/master/api/tools.html#statistics
39https://docs.scipy.org/doc/numpy/reference/generated/numpy.mean.html
40https://docs.scipy.org/doc/numpy/reference/generated/numpy.std.html?
highlight=numpy%20std#numpy.std
41https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.min.html?
highlight=numpy%20min#numpy.ndarray.min
42https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.max.html
43https://deap.readthedocs.io/en/master/tutorials/basic/part3.html
44https://deap.readthedocs.io/en/master/api/algo.html
45Para aquellos que no estén familiarizados con la sentencia if __name__ ==
"__main__": se recomienda consultar el siguiente enlace:
https://stackoverflow.com/questions/419163/what-does-if-name-main-do
46Es ineficiente sí, pero no podemos hacer nada al respecto.
47https://deap.readthedocs.io/en/master/api/tools.html#logbook
48https://deap.readthedocs.io/en/master/api/tools.html#logbook
49https://deap.readthedocs.io/en/master/tutorials/advanced/constraints.html
50Sin tener en cuenta el tiempo que tarda el algoritmo genético en hacer otras
operaciones, como las operaciones genéticas, por lo que el tiempo real será
mayor.
51https://en.wikipedia.org/wiki/Rastrigin_function
2.1 Introducción al problema del viajero
El problema del agente viajero, problema del viajero o Travelling Salesman
Problem (TSP) es uno de los problemas de optimización combinatorios clásicos
más populares con variables discretas que se pueden encontrar en la literatura. El
problema consiste en encontrar el tour óptimo para un vendedor que visite un
conjunto de ciudades, partiendo de una ciudad y finalizando en la misma ciudad
(realmente si no acaba en la misma ciudad es igualmente complicado),
recorriendo la distancia mínima y sin visitar ninguna ciudad más de una vez. Es
decir, estamos ante un problema de minimización. Aunque el enunciado del
problema es sencillo, a día de hoy no existe un algoritmo determinista que
resuelva el problema de manera óptima para cualquier número de ciudades en un
tiempo razonable1. El problema tiene variables discretas, ya que el conjunto de
ciudades es un conjunto discreto. Por otro lado, es obvio que el problema se
complica con el número de ciudades. De hecho, el número de posibles
soluciones al problema es (N – 1)!/2, considerando que no importa la ciudad de
origen entre todas las ciudades y que la distancia entre ciudades es simétrica. Es
decir, las ciudades están unidas por una sola carretera de dos sentidos. En cuanto
al número de soluciones, para diez ciudades tenemos 181,440 soluciones y para
veinte ciudades tenemos 6.082e + 16. Si consideramos que en evaluar cada
solución tardamos un 1 segundo, ¿cuánto tiempo tardaríamos en evaluar todas
las posibles soluciones? Pues en el caso de diez ciudades tardaríamos 50.4 horas,
y en el caso de veinte ciudades tardaríamos casi dos mil millones de años. Es
evidente que con el paso del tiempo tendremos ordenadores más potentes2, pero
difícilmente se puede abordar un problema como este intentando probar todas las
posibles soluciones (fuerza bruta). Este tipo de problemas se denominan
problemas intratables, debido a su complejidad. Por otro lado, también cabe
indicar que para un número pequeño de ciudades existen algoritmos
deterministas y eficientes que consiguen resolver el problema de manera óptima.
Desde el punto de vista del ámbito de la ingeniería, el problema del viajero tiene
aplicación en problemas de transporte, logística y planificación de rutas de
vehículos autónomos, entre otras aplicaciones.
Veamos cómo podemos plantear el problema del viajero mediante teoría de
grafos. La Figura 2.1 muestra un grafo formado por cuatro ciudades: A, B, C y
D. En el grafo, cada uno de los vértices se corresponde con una ciudad y las
aristas del grafo son las conexiones entre las ciudades. En la definición clásica,
todas las ciudades están conectadas con el resto. Es por ello que siempre va a
existir una arista entre dos ciudades cualesquiera. El objetivo del viajero es
recorrer todas las ciudades partiendo de una de ellas. Por ejemplo, una solución
al problema del viajero con respecto al grafo de la Figura 2.1 sería el tour
formado por [A,B,C,D]. Esa solución implica que el viajero parte del nodo A, y
después visita los nodos B, C y D, respectivamente. Otra posible solución sería
el tour formado por [A,D,C,B], y así podríamos seguir indicando distintas
soluciones. Ya se puede apreciar que las soluciones del problema del viajero
vienen dadas por una lista de ciudades que el viajero visita en dicho orden. Una
característica importante de este problema es que no puede haber ciudades
repetidas en la solución, y eso, como veremos más adelante, determina las
posibles operaciones genéticas que podamos realizar sobre los individuos. Es
decir, no podemos intercambiar la información genética de dos individuos de
cualquier forma. Desde el punto de vista de la teoría de grafos, el problema que
se quiere resolver es similar a encontrar el ciclo Hamiltoniano del grafo
compuesto por las ciudades. Un ciclo Hamiltoniano es aquel que solo pasa una
vez por cada uno de los vértices del grafo. Sin embargo, el problema del viajero
es aun más complejo, porque se quiere encontrar el ciclo Hamiltoniano de
distancia mínima.
Figura 2.1. Grafo ejemplo del problema del viajero con cuatro ciudades.
Una vez definido el problema del viajero, lo siguiente que necesitamos es una
matriz de distancias que nos permita determinar la distancia entre cualquier par
de ciudades. Esta matriz será simétrica ya que, como hemos dicho, la distancia
entre A y B será la misma que entre B y A. Asimismo, la diagonal de dicha
matriz estará compuesta por ceros, ya que la distancia de una ciudad con
respecto a ella misma es nula. A partir de la matriz de distancia y la ruta,
podemos calcular la distancia que recorre el viajero. Así, por ejemplo, para la
solución [A,D,C,B], la distancia que recorre el viajero es d = dAD + dDC + dCB
+ dBA.
Estamos, por tanto, en posición de definir formalmente el problema:
Observe que a partir del valor de las variables xij es posible conocer la ruta que ha seguido el
Una vez descrito el problema, la manera de representar las soluciones y el
cálculo de la distancia que recorre el viajero, esto es, la calidad del individuo, ya
podemos ponernos manos a la obra con el código en deap.
2.2 Definición del problema y generación de la
población inicial
El problema que vamos a resolver es el problema del viajero para el caso de 17
ciudades. El siguiente script muestra información relevante sobre el problema
que queremos resolver. Se trata de un archivo JSON que contiene:
■Toursize: Representa el número de ciudades del problema.
■OptTour: Solución óptima al problema.
■OptDistance: Distancia óptima que recorre el viajero para el tour óptimo.
■DistanceMatrix: Matriz de distancia.
La ventaja de trabajar con archivos JSON es que se pueden cargar fácilmente
como un diccionario en Python. Aunque para este conjunto de ciudades
conocemos el tour óptimo, esto no es lo común, pero en este caso nos sirve como
referencia. Es decir, estamos haciendo “un poco de trampa”, ya que sabemos el
óptimo del problema.
El siguiente script incluye los primeros pasos que necesitamos para resolver el
problema del viajero mediante un algoritmo genético. A continuación,
pasaremos a describir las distintas líneas de código, centrándonos sobre todo en
aquellas que son nuevas o que son modificaciones con respecto al Capítulo 1.
En primer lugar, se importan los módulos que van a ser necesarios:
A continuación, se carga el fichero JSON:
A partir de este punto, los datos del archivo están en el diccionario tsp. El objeto
distance_map almacena la matriz de distancias, que se utilizará en la función
objetivo. Además, el objeto IND_SIZE se usa para almacenar el número de
ciudades del problema, que se utilizará para definir la longitud del individuo. En
este caso será IND_SIZE = 17, ya que estamos resolviendo el problema para 17
ciudades:
2.2.1 Creación del problema y plantilla para el individuo
A continuación, debemos definir el tipo de problema tal y como lo hicimos en el
Capítulo 1. Para ello, se crea la clase FitnessMin, que hereda de la clase
base.Fitness y que tiene el atributo weights = –1, por lo que estamos definiendo
un problema de minimización. Se puede comprobar que el procedimiento es
análogo al realizado en el capítulo anterior, con la diferencia de que en este caso
queremos minimizar en vez de maximizar.
La clase Individual se utilizará para almacenar el individuo. Al heredar de una
lista, tendrá todos los métodos de este tipo de clase. A su vez, se crea el atributo
fitness que almacenará la calidad del individuo.
Al igual que en el problema del Capítulo 1, se está utilizando una lista para
representar los individuos. Si usamos Si para indicar la i-ésima ciudad visitada
por el viajero, nuestros individuos tendrán la estructura representada en la Figura
2.2.
Figura 2.2. Representación de un individuo para el problema del viajero con 17
ciudades.
2.2.2 Crear individuos aleatorios y población inicial
El siguiente paso es crear el objeto caja de herramientas, donde se registrarán
todas las funciones necesarias para el algoritmo genético. Para generar tours
aleatorios nos apoyaremos en la función indices. Dicha función nos permite
generar una muestra de números aleatorios sin repetir entre 0 y el número de
ciudades que tiene nuestro problema menos uno3 (IND_SIZE):
Es decir, la función indices genera una lista que representa un individuo
aleatorio. Esta función realmente está llamando al método random.sample, que
nos permite generar una muestra aleatoria, sin repetición, de un conjunto de
datos4. El Resultado 2.1 muestra el resultado de ejecutar random.sample para un
conjunto de 17 valores comprendidos en el intervalo [0,17). Por lo tanto, ese es
el mismo resultado que el que provocará el método indices, tal y como se puede
observar en el Resultado 2.2.
Resultado 2.1. Resultado random.sample.
Resultado 2.2. Resultado toolbox.indices.
Es importante destacar que en este problema, a diferencia del anterior, estamos
registrando una función que nos proporciona un individuo aleatorio completo.
Este hecho tiene algunas consecuencias a la hora de registrar la función
individual en el objeto toolbox. En esta ocasión, se utiliza el método
tools.initIterate en vez de utilizar el método tools.initRepeat empleado
anteriormente.
Como norma general, cuando tengamos una función que genere un gen o variable del individu
Para entender mejor la diferencia entre tools.initRepeat y tools.initIterate
podemos consultar la documenación de deap5. En primer lugar, debemos indicar
que el método tools.initRepeat recibe los siguientes parámetros:
■container : El tipo de dato al que se convertirá el resultado de la función func .
■func : Función a la que se llamará n veces.
■n : Número de veces que se llamará a la función func .
Al igual que en el ejemplo del Capítulo 1, el container que utilizaremos será
creator.Individual. Sin embargo, los parámetros serán diferentes. Si recordamos,
la func era la función que generaba cada uno de los genes
(random.uniform(-100,100), la cual dependía del problema) y n era el número de
genes (2 en el ejercicio del Capítulo 1). El valor de n puede ser distinto para cada
problema, ya que dependerá del número de variables independientes.
En el caso de tools.initIterate6, los parámetros de entrada son:
■container : El tipo de dato al que se convertirá el resultado de la función f unc .
■generator : Una función que devuelve un iterable (listas, tuplas, etc.). El
contenido de este iterable rellenará el container .
Para el problema del viajero, el container es el objeto creator.Individual y el
generador es toolbox.indices que, como se ha dicho anteriormente, devuelve un
objeto iterador. Ese iterador no es más que una muestra de ciudades.
En resumen, la diferencia entre tools.initRepeat y tools.initIterate es que
tools.initIterate llama en el segundo parámetro a una función que devuelve un
objeto iterable que contiene una muestra completa del cromosoma del individuo,
que se convertirá en individuo. Por otro lado, en tools.initRepeat la función func
devuelve un solo gen del cromosoma, por lo que para completar el individuo
completo se debe llamar n veces, siendo n el tamaño del individuo.
Con respecto a la generación de la población inicial, siempre se utilizará el
método tools.initRepeat:
En tools.initRepeat, el container siempre será una lista, ya que la población
siempre será una lista de individuos. La función que se llamará es justo la que se
ha registrado en la anterior línea toolbox.individual, la cual genera un individuo
aleatorio. El último parámetro, n, será el número de individuos que forman la
población inicial. En este caso, la población inicial está compuesta de 100
individuos.
2.3 Función objetivo y operadores genéticos
A continuación, definimos la función objetivo de nuestro problema. El siguiente
script muestra el fragmento de código necesario para definir la función objetivo
del problema y el registro de los operadores genéticos que se utilizarán:
A continuación, se describirán por separado la función objetivo y cada uno de los
operadores genéticos.
2.3.1 Función objetivo
En primer lugar, la siguiente línea obtiene la distancia entre la primera y la
ultima ciudad que visita el viajero. Se debe recordar que en Python el último
elemento de la secuencia se obtiene con el índice –1.
A continuación, el bucle calcula el resto de distancias. Para ello, mediante la
función nativa de Python zip se obtiene una secuencia de tuplas compuestas por
los elementos del primero al penúltimo y del segundo al último. Así, cada tupla
de la secuencia contendrá dos ciudades consecutivas en el tour:
Por último, cabe recordar de nuevo que se debe devolver una tupla como
resultado, ya que en deap los problemas con un solo objetivo son un caso
particular de los problemas multiobjetivo:
2.3.2 Operadores genéticos
En primer lugar, se registra la operación de cruce ordenado tools.cxOrdered:
En el problema del viajero no se puede utilizar cualquier tipo de operador de
cruce, ya que podrían darse soluciones no válidas. Por ejemplo, si utilizamos el
cruce de un punto (o de dos puntos) que vimos en el Capítulo 1, un mismo
individuo podría tener una misma ciudad repetida. Aunque podríamos invalidar
dichos individuos penalizándolos en la función objetivo (pena de muerte),
siempre es preferible utilizar operadores genéticos que no produzcan individuos
inválidos.
En el operador de cruce elegido, es importante observar que el segundo punto
aleatorio determina la posición por la que se continúa completando a la
descendencia. En el caso de la izquierda, el descendiente debería haberse
rellenado con la ciudad D, que es la que corresponde a dicha posición en el otro
progenitor. Sin embargo, como dicha información ya la incluye el descendiente,
se debe saltar dicho gen y continuar con la siguiente posición. Como regla, el
orden se determina de izquierda a derecha pero, en este caso, como es la última
posición del progenitor, se debe continuar de manera circular por la primera
posición de este. Es por ello que el descendiente se rellena con la ciudad B. Para
rellenar la siguiente posición del descendiente, debemos proceder igual. Así,
debido a que la B ocupa la última posición, debemos continuar con la primera,
rellenando con genes del padre en el mismo punto en que lo dejamos en la
última operación. Por lo tanto, el descendiente se rellena con la ciudad A. Ya
solo nos queda una última ciudad que, siguiendo con el procedimiento,
corresponde a la ciudad E. En el caso de la derecha, el procedimiento es el
mismo. En este caso, se empieza con la ciudad B, ya que la E no se puede incluir
porque ya está en el progenitor; lo mismo ocurre con la A, por lo que se incluye
la B. Continuamos por la D y, finalmente, se termina de rellenar el descendiente
con la ciudad F. En la librería deap existe otro algoritmo de cruce que también se
puede utilizar en este problema, que se denomina cxPartialyMatched 7, el cual se
describirá más adelante en este capítulo.
En cuanto a la mutación, en este problema utilizaremos el operador
tools.mutShuffleIndexes para intercambiar dos ciudades:
Podemos ver que hemos elegido un valor bajo de la probabilidad de mutación
del gen (indpb = 0.05), o lo que es lo mismo, una probabilidad del 5% de
intercambiar un gen por otro dentro del cromosoma. El valor de indpb se elige
normalmente bajo, ya que de otra forma los individuos resultantes serían muy
distintos a los individuos padres. Otra forma de ajustar dicho parámetro es
hacerlo inversamente proporcional a la longitud del cromosoma. Es decir, cuanto
mayor sea el número de variables del problema menor será dicha probabilidad.
Es importante indicar que si hacemos muy grande el valor de indpb podremos
tener problemas de convergencia, ya que estamos tendiendo a realizar una
búsqueda aleatoria.
Finalmente, el registro de la función de evaluación y el mecanismo de selección
se realizan como en el ejemplo del capítulo anterior. Para la selección, se utiliza
de nuevo la técnica de torneo con tamaño tres:
2.4 Selección del algoritmo genético
En este problema se va a utilizar otra popular implementación de algoritmo
genético denominado algoritmo mupluslambda (µ + λ), diferente del eaSimple
que hemos estudiado en el Capítulo 1. Este algoritmo es más elitista y también
requiere más tiempo de computación. Sin embargo, también arroja mejores
resultados en una gran cantidad de posibles escenarios.
Existe una tercera implementación de algoritmo genético en la librería deap: el
algoritmo µ,λ, accesible a través del submódulo algorithms mediante
algorithm.eaMuCommaLambda. En esta implementación, λ individuos son
creados a partir de los µ padres, a través de los operadores genéticos registrados.
Entre los λ descendientes se realiza el proceso de selección de µ individuos para
formar la siguiente generación. Por lo tanto, se puede considerar una versión
elitista de los descendientes del algoritmo eaSimple. En este libro no se utilizará
esta implementación, pero se puede encontrar más información en la
documentación oficial8.
Por último, la librería también permite crear nuevas implementaciones de
algoritmos genéticos desde cero, aunque nosotros no abordaremos este tema9.
2.5 Últimos pasos
El siguiente script muestra el código necesario para lanzar el algoritmo genético.
Como se ha explicado, en este caso se utilizará un algoritmo distinto al eaSimple
estudiado en el Capítulo 1, el algoritmo µ + λ, cuya configuración es un poco
diferente.
A continuación, se describen los aspectos más importantes del código
presentado.
2.5.1 Configuración del algoritmo genético µ + λ
En primer lugar, se ajusta la semilla del generador de números aleatorios:
A continuación, se definen las probabilidades de cruce y mutación, así como el
número de generaciones. Se ha definido una probabilidad de cruce de 0.7, una
probabilidad de mutación de 0.3 y 120 como el número de generaciones que
correrá el algoritmo. Como pasó en el Capítulo 1, esa configuración puede no ser
la óptima, por lo que tendremos que ajustarla:
Después, se pasa a crear la población inicial. El tamaño de la población se
definió al registrar la función population en el toolbox. Se consideró una
población de 100 individuos.
El siguiente paso es asignar valores a los parámetros µ y λ del algoritmo
genético:
Con dichos valores, se ha considerado una población extendida de cuyo tamaño
es el doble de la población, ya que µ = 100 y λ = 100. Así, se crean 100
descendientes y se seleccionan entre la población extendida los µ individuos que
pasarán a la siguiente generación. La siguiente generación está compuesta de λ
individuos seleccionados. Por lo tanto, el tamaño de la población no varía a lo
largo de las generaciones. Hay que tener en cuenta que, tanto para la creación de
la descendencia como la creación de la siguiente generación, se ha utilizado la
selección mediante torneo de tamaño tres, tal y como se registró en el objeto
toolbox. El resto de parámetros de configuración han sido ya descritos en el
Capítulo 1, por lo que solo los comentaremos rápidamente.
Definimos el objeto hof para almacenar el mejor individuo a lo largo de las
generaciones del algoritmo:
Creamos el objeto para generar las estadísticas de evolución y registro de las
funciones para calcular las métricas estadísticas:
Declaramos el registro de evolución log:
A continuación, se lanza el algoritmo genético µ + λ:
Cuando finaliza la ejecución del algoritmo genético, este devuelve la población
final y el registro de evolución del algoritmo. En la Figura 2.3 se muestra la
evolución del algoritmo. Se puede observar que la convergencia es buena, ya que
a partir de la generación 40 no se observa ninguna mejora. En las primeras
generaciones se pueden apreciar las diferencias entre el valor máximo y mínimo
del fitness de la población. Conforme avanzan las generaciones, los individuos
de la población tienden a ser parecidos; es por ello que el fitness tiende a
converger. Los picos que podemos observar en generaciones más tardías (60, 80
y 100) se deben a que en algunas generaciones las operaciones genéticas pueden
dar lugar a individuos peores, afectando notablemente a los resultados máximo y
medio de la población.
Una buena convergencia del algoritmo no implica un resultado óptimo. Un algoritmo genético
Figura 2.3. Evolución TSP.
Para ver con más detalle la evolución de las primeras generaciones del
algoritmo, el Resultado 2.3 muestra los valores estadísticos de la función
objetivo para las primeras 15 generaciones del algoritmo. Se puede observar
cómo en cada generación el algoritmo va mejorando los resultados, ya que se
van reduciendo tanto el valor mínimo como el medio y el máximo.
Resultado 2.3. Primeras generaciones del algoritmo genético para el problema
TSP.
Por el contrario, en el Resultado 2.4 se puede observar cómo para las últimas 15
generaciones del algoritmo no podemos observar ninguna diferencia entre los
valores mínimo, medio y máximo de la función objetivo. Estos resultados son
una muestra clara de que el algoritmo ha convergido.
Resultado 2.4. Últimas generaciones del algoritmo genético para el problema
TSP.
En cuanto al mejor individuo, el Resultado 2.5 muestra el mejor individuo del
algoritmo. Podemos observar que el resultado final es muy cercano al óptimo del
problema, que es 2085, tal y como se muestra en el archivo JSON. Se puede ver
que la mejor solución parte de la ciudad 15, continuando con la 11 y siguiendo la
secuencia hasta llegar a la ciudad 0.
Resultado 2.5. Resultado algoritmo genético µ + λ para el problema TSP.
2.6 Comprobar la convergencia del algoritmo en
problemas complejos
Aunque en el Capítulo 1 se cubrió el procedimiento para representar la
convergencia del algoritmo, en un problema complejo como el del viajero
debemos pararnos de nuevo en este aspecto.
En la sección anterior se ha utilizado NGEN = 120 y se han obtenido unos
resultados satisfactorios. Ese número puede parecer un poco mágico... ¿Por qué
no 100 o, incluso, un valor mucho más bajo, como 40? En principio, si no
tenemos problemas con el tiempo que tarde en ejecutarse el algoritmo, debemos
comenzar con un número elevado de generaciones. No obstante, la
representación de la evolución del algoritmo nos puede dar una idea de si
debemos aumentar el número de generaciones. Así, imaginemos que para el
código utilizado en la sección anterior fijamos NGEN = 20. Entonces, la
evolución del algoritmo sería la mostrada en la Figura 2.4. Se puede observar
que el algoritmo no ha convergido, ya que no se puede apreciar un codo en la
tendencia del valor mínimo. Por lo tanto, parece evidente que debemos elevar el
número de generaciones del algoritmo.
Figura 2.4. Evolución TSP para NGEN = 20.
El caso contrario sería considerar NGEN = 500; los resultados se pueden ver en
la Figura 2.5. En este caso, el problema está en que estamos desperdiciando
mucho tiempo de computación, ya que la convergencia del algoritmo se da
mucho antes de llegar a la generación 500. Se puede observar que el valor
mínimo no ha variado prácticamente desde la generación 40-50. Por lo tanto,
estamos malgastando 450 generaciones. Ese tiempo de computación lo podemos
utilizar para lanzar muchas veces el algoritmo o para probar distintas
configuraciones con el fin de asegurarnos de que el algoritmo no se ha quedado
bloqueado en un mínimo local.
Figura 2.5. Evolución TSP para NGEN = 500.
Por último, cabe indicar que estas comprobaciones se deben realizar más de una
vez, para evitar que la convergencia o no del algoritmo sea debida al azar.
2.7 Ajuste de los hiperparámetros: Probabilidades de
cruce y mutación
En esta sección vamos a analizar los resultados del algoritmo genético en
función de los hiperparámetros básicos de un algoritmo genético, como son las
probabilidades de cruce y mutación. El objetivo de esta sección es seleccionar la
combinación de probabilidades que nos proporcione mejores resultados. El
siguiente script muestra las modificaciones de código necesarias en la función
main para ejecutar el algoritmo genético varias veces con distintas
probabilidades de cruce y mutación. El resto del código es exactamente igual
que en anteriores secciones, y es por ello por lo que no se incluye. Para poder
analizar con mayor profundidad los resultados, estos se van a almacenar en dos
archivos de texto. Se han utilizado dos archivos para no mezclar los valores de
fitness de los individuos que almacenan los tours con las ciudades. Así,
facilitamos el posterior análisis de los resultados.
A continuación, se detallan los cambios más importantes respecto al ejemplo del
primer capítulo. La función main ahora recibe dos parámetros, c y m, que son las
probabilidades de cruce y mutación.
Otro aspecto que debemos modificar es la posición del ajuste de la semilla de
números aleatorios. En este caso, no se puede colocar dentro de la función main,
debido a que obtendríamos los mismos resultados cada vez que ejecutáramos la
función11. Por ello, el ajuste de la semilla se realiza desde fuera. El resto de la
función main no cambia.
Las listas prob_cruce y prob_mutación definen las secuencias de valores de
probabilidad de cruce y mutación que se evaluarán. Esos valores cubren un
espectro importante de valores de configuración del algoritmo genético.
Probabilidades de cruce mayores de 0.8 son rara vez utilizadas, ya que se daría muy poco mar
Para guardar los resultados se abren dos archivos en modo escritura.
El archivo FitnessTSP.txt contendrá los resultados del mejor individuo obtenido
en la evolución del algoritmo genético para los parámetros c y m (probabilidad
de cruce y mutación, respectivamente). El archivo IndividuosTSP.txt contendrá
los individuos que ha generado el fitness del archivo FitnessTSP.txt. Es decir,
contendrá los mejores individuos.
El siguiente paso es realizar un bucle doble. El primer bucle itera sobre los
valores de la listas de probabilidades (c y m) gracias a la función nativa de
Python zip13. El segundo bucle itera diez veces sobre dichos valores.
La idea es ejecutar el algoritmo genético diez veces con la misma configuración
para poder analizar los resultados. En cada iteración, el resultado del algoritmo
se escribe en los archivos de texto. Cada resultado o individuo se escribe en una
línea diferente del archivo de texto. Además, se ha incluido el número de
iteración y las probabilidades de cruce y mutación. Una vez que se terminan los
bucles, debemos cerrar ambos archivos para que los cambios tengan efecto:
Ambos archivos de texto no tienen que estar creados previamente por nosotros;
si no existen en el directorio de trabajo, Python los creará.
Para poder visualizar las soluciones obtenidas, basta con abrir el archivo
FitnessTSP.txt. Dicho archivo se muestra en el Resultado 2.6. Se puede observar
que en las iteraciones id = 9, c = 0.8, m = 0.2 e id = 3, c = 0.7, m = 0.3 se han
alcanzado los óptimos del problema con una distancia mínima de 2085.
Texto 2.6. Resultados del algoritmo del archivo FitnessTSP.txt.
La Tabla 2.1 muestra el análisis de los resultados contenidos en el archivo
FitnessTSP.txt. En vista de estos, la mejor configuración del algoritmo genético
es c = 0.7 y m = 0.3, ya que obtiene el valor mínimo; además, en media los
resultados obtenidos son mejores que en las otras configuraciones.
Métrica
c
m
Fitness
máximo
0.8
0.2
2238.0
mínimo
0.8
0.2
2085.0
media
0.8
0.2
2161.0
desviación
0.8
0.2
47.9
máximo
0.7
0.3
2184.0
mínimo
0.7
0.3
2085.0
media
0.7
0.3
2111.5
desviación
0.7
0.3
31.3
máximo
0.6
0.4
2194.0
mínimo
0.6
0.4
2090.0
media
0.6
0.4
2132.3
desviación
0.6
0.4
34.87
Tabla 2.1. Análisis de los resultados en el archivo FitnessTSP.txt.
Para completar el análisis, el Resultado 2.7 muestra los mejores individuos
obtenidos en cada intento del algoritmo genético. Se puede observar que los
individuos que presentan mejores resultados (id = 9, c = 0.8 y m = 0.2) y (id = 3,
c = 0.7 y m = 3) son prácticamente idénticos; simplemente ocurre que las
ciudades de inicio y fin son distintas.
Texto 2.7. Mejores individuos almacenados en el archivo IndividuosTSP.txt.
En esta sección, solo se han analizado los efectos de las probabilidades de cruce
y de mutación. Sin embargo, existen otros parámetros que también pueden
afectar a los resultados y que pueden ser considerados para un análisis más
profundo; por ejemplo, la probabilidad indpb puede aumentar el efecto del
mecanismo de mutación. En este capítulo, al igual que en el Capítulo 1, hemos
considerado un valor bajo, pero más adelante veremos que podemos aumentar
dicho valor en caso de ser necesario. Otro parámetro que se podría modificar es
el mecanismo de cruce. Por ejemplo, utilizando el cruce cxPartiallyMatched14.
Para utilizar dicho operador de cruce, solo tendríamos que registrarlo de la
siguiente forma:
En definitiva, tenemos muchos parámetros de configuración que podemos variar
para ver cómo afectan a los resultados. Pero, sin duda, las probabilidades de
cruce y mutación tienen un gran impacto en los mismos.
2.8 Acelerando la convergencia del algoritmo: El
tamaño del torneo
Cuando registramos el operador de selección por torneo indicamos que un
tamaño de tres es un valor adecuado para la mayoría de los casos. No obstante,
en algunos casos este valor puede provocar una convergencia lenta del
algoritmo, especialmente cuando el problema es sumamente complejo. En estos
casos, podemos acelerar la convergencia del algoritmo incrementando el tamaño
del torneo, de manera que los buenos individuos se utilicen más veces en las
operaciones genéticas que generan la siguiente población. Nunca debemos
perder de vista que queremos la mejor solución posible pero en un tiempo
razonable.
Aumentar el tamaño del torneo se traduce en aumentar el elitismo del proceso de selección, ya
Podemos cuantificar la probabilidad de seleccionar el mejor individuo para un
tamaño del torneo dado. En nuestro ejemplo, hemos considerado una población
de 100 individuos, por lo que la probabilidad de seleccionar aleatoriamente al
mejor individuo es p = 1/100 (un 1%). Cada selección es un experimento de
Bernoulli, con dos posibles salidas: 1 (se selecciona el mejor individuo) y 0 (no
se selecciona el mejor individuo). Por otro lado, la probabilidad de no
seleccionar al mejor individuo es q = 1 – p = 0.99 (un 99%). Si realizamos 3
veces ese proceso, y teniendo en cuenta que cada selección es independiente,
podemos calcular la probabilidad p de no seleccionar al mejor individuo en
ninguno de los tres intentos como p = (99/100)3. Hay que tener en cuenta que el
experimento completo sigue una distribución binomial. Esto nos da una
probabilidad p = 0.97 (97%) de no seleccionar al mejor individuo, por lo que la
probabilidad de seleccionar al mejor individuo es q = 1 – p = 0.03 (un 3%). En
definitiva, con un torneo de tamaño 3, tenemos un 3% de probabilidad de que el
mejor individuo de la población participe en las operaciones de cruce y
mutación. Analicemos ahora qué ocurre si aumentamos el tamaño del torneo a
10. En este caso, la probabilidad de no seleccionar al mejor individuo en
ninguno de los 10 intentos es p = (99/100)10 = 0.90 (un 90%); por lo tanto, la
probabilidad de elegir al mejor en alguno de los intentos es q = 1 – p = 0.10 (un
10%). Podemos observar que hemos incrementado la probabilidad de escoger al
mejor individuo del 3% al 10%, de forma que ahora es más probable que este
participe en las operaciones genéticas.
La Figura 2.6 muestra la evolución del mejor individuo para el problema del TSP
con distintos tamaños de torneo. Se puede ver que al aumentar el tamaño del
torneo aceleramos la convergencia del algoritmo. No obstante, en este caso
particular, acelerar la convergencia del algoritmo tiene un efecto negativo, ya
que el algoritmo empeora su resultado con respecto al mejor individuo, por lo
que hay que tener especial cuidado con aumentar el tamaño del torneo. En líneas
generales, solo se recomienda aumentar el tamaño del torneo en problemas
donde el número de variables de diseño sea muy elevado y se detecten
problemas de convergencia. Es decir, si el problema es sumamente complejo y
no podemos esperar para obtener un resultado satisfactorio15, podemos acelerar
la convergencia del algoritmo, penalizando en ocasiones el mejor resultado
obtenido.
Por otro lado, es posible aumentar la velocidad de convergencia del algoritmo
paralelizando su funcionamiento, como se muestra en el Apéndice B.
2.9 Acelerando la convergencia del algoritmo: Aplicar
elitismo
En la sección anterior hemos visto que aumentando el tamaño del torneo
podemos hacer que el algoritmo converja de manera más rápida. Aumentar el
tamaño del torneo también se puede considerar un aumento de elitismo en la
selección de los individuos que participarán en las operaciones genéticas. No
obstante, existe una técnica aún más drástica, que consiste en que los mejores
individuos pasen directamente a la siguiente generación; este mecanismo se
conoce como “elitismo”. El elitismo se debe aplicar con cuidado ya que si nos
pasamos haremos que el funcionamiento del algoritmo genético no sea
adecuado.
Figura 2.6. Evolución del fitness para el problema TSP utilizando selección
mediante torneo con distintos tamaños.
El porcentaje de individuos que se seleccionan mediante elitismo debe ser pequeño, normalme
Si se considera un porcentaje muy alto en la aplicación del elitismo, corremos el
riesgo de que el algoritmo genético sufra de una convergencia prematura.
Se conoce como “convergencia prematura”, a la rápida convergencia del algoritmo genético e
La librería deap dispone del método tools.selBest del módulo tools que permite
realizar la operación de elitismo. Los parámetros de la función son:
■individuals : Individuos sobre los que seleccionar; normalmente será la
población del algoritmo.
■k : Número de individuos que se seleccionarán. Es decir, se seleccionarán los k
mejores individuos de la población.
■fit_attr : El atributo utilizado para la selección. Por defecto es el fitness de los
individuos.
Desgraciadamente, la librería deap no incluye el elitismo en ninguno de los
algoritmos del módulo algorithms. Por lo tanto, si queremos aplicar elitismo
tendremos que realizar nuestra propia implementación del algoritmo. La
implementación desde cero de un algoritmo genético se escapa a los objetivos de
este libro. No obstante, en la documentación oficial de la librería vienen algunos
ejemplos sobre cómo realizar un algoritmo genéticos desde cero16.
2.10 Complejidad del problema: P vs NP
El problema del viajero es de tipo NP-duro en cuanto a complejidad. A
continuación, vamos a realizar una breve introducción sobre la complejidad de
los problemas de optimización para poder entender qué significa este tipo de
complejidad.
En primer lugar, debemos hablar de los problemas de tipo NP (Nondeterministic
Polynomial time), que son aquellos en los que se puede comprobar de manera
sencilla, y en un tiempo razonable y determinista, que una solución es factible.
Es decir, podemos verificar si la respuesta que nos ha dado un algoritmo al
problema es correcta o no al problema. Ese tiempo razonable se conoce como
“tiempo polinomial”. Por contra, en los problemas NP no existe un
procedimiento o algoritmo determinista para resolver el problema en tiempo
polinomial.
El tiempo polinomial para resolver o comprobar la solución a un problema hace referencia a r
Un ejemplo de problema NP es la resolución de sudokus. En general, podemos
saber si una solución del sudoku es correcta de manera sencilla, únicamente
utilizando sumas. Sin embargo, realizar un algoritmo que resuelva un sudoku no
es sencillo. Existen soluciones para sudokus de dimensiones pequeñas (n
pequeño), pero no existe un algoritmo determinista que resuelva el problema en
tiempo polinomial para cualquier tamaño. Por lo tanto, siempre hay que tener en
cuenta que:
Verificar que una solución es correcta para un problema no garantiza que se pueda encontrar u
Por otro lado, están los problemas de tipo P, que son aquellos para los que que sí
se puede encontrar una solución en un tiempo razonable al problema. Es decir,
en tiempo polinomial y de una manera determinista. Por lo tanto, para los
problema de tipo P podemos hacer las dos cosas en tiempo polinomial.
Para los problemas de tipo P podemos: i) comprobar que una solución es válida y correcta, y i
Básicamente, los problemas tipo P son aquellos que se pueden resolver mediante
una combinación de operaciones sencillas tales como sumas, restas,
multiplicaciones, etc. Ejemplos de este tipo de problemas son: algoritmos de
ordenación de listas o vectores, por ejemplo, el algoritmo de la burbuja tiene una
complejidad N2, búsqueda binaria (log(N)) o multiplicación de matrices (N3),
entre otros.
La Figura 2.7 muestra los problemas de tipo P como un subconjunto de los
problemas NP. Esto es así por la concepción que se tiene de estos dos tipos de
problemas a día de hoy. Cada cierto tiempo un problema que es de tipo NP pasa
a ser de tipo P porque alguien encuentra un procedimiento o algoritmo
determinista para resolver el problema en tiempo polinomial. Por lo tanto, se
piensa en P como un subconjunto de NP. Los problemas que hoy en día son de
tipo NP en un futuro pueden ser de tipo P17.
Figura 2.7. Representación de la función de optimización.
Antes de continuar con otros tipos de complejidades, debemos hablar de
algoritmos deterministas y no deterministas. Cuando hemos hablado del tipo NP,
se ha hecho referencia a que no existe un algoritmo determinista que resuelva el
problema en tiempo polinomial. Un algoritmo es determinista si no tiene ningún
tipo de funcionamiento probabilístico en su ejecución. Es decir, el procedimiento
es totalmente determinista y así será el resultado. Por el contrario, un algoritmo
no determinista o estocástico tiene componentes probabilísticas en su
procedimiento y, por consiguiente, el resultado del algoritmo puede ser distinto
en cada intento. Veamos este concepto con un ejemplo. El siguiente script
muestra un algoritmo para encontrar el mínimo de una lista de valores (l1). Se
puede ver que el algoritmo no es determinista, ya que utiliza la función
random.randint para obtener una posición aleatoria de la lista y comprobar si el
elemento de dicha posición es el mínimo o no. Así, el comportamiento de este
algoritmo será distinto en cada intento. Esto no significa que el algoritmo no
haga bien su trabajo, simplemente significa que no podemos garantizar de forma
determinista su comportamiento.
Un algoritmo genético es un algoritmo no determinista, ya que tiene muchas
componentes aleatorias en su ejecución (probabilidades de cruce y mutación).
No obstante, el número de operaciones que realiza un algoritmo genético sí está
acotado por el número de individuos que tiene, la población y el número de
generaciones.
A continuación, debemos hablar de los problemas de tipo NP-duro. Según la
Figura 2.7, este tipo de problemas pueden ser NP o no. Para estos problemas
pueden existir algoritmos o procedimientos que pueden resolverlos pero: i) no
son deterministas o ii) requieren un tiempo exponencial 2n (no es tiempo
polinomial). Hay que tener en cuenta que un tiempo exponencial de tipo 2n
significa muchas operaciones y, en consecuencia, mucho tiempo de
computación. De hecho, en muchos casos significa probar todas las posibles
combinaciones, es decir, emplear un algoritmo de fuerza bruta, cosa que puede
ser prohibitiva.
Los problemas de tipo NP-duro son problemas que requieren algoritmos no deterministas y/o
Continuemos por el último tipo de problemas que vamos a definir, los problemas
NP-completos. Si observamos la Figura 2.7, vemos que los problemas NPcompletos están dentro del conjunto NP y de NP-duro. Por lo tanto, un problema
es de tipo NP-completo si es de tipo NP y de tipo NP-duro a la vez. Es decir, es
un problema intratable pero se puede comprobar la validez de la solución. Los
problemas de tipo NP-completos son problemas de decisión. Podemos entender
mejor los problemas NP-completos estudiando el problema factibilidad
booleana. De hecho, la teoría de los problemas NP-completos surge a partir del
teorema de Cook, demostrado para el problema de factibilidad booleana o SAT
(Boolean satisfiability problem). Este problema consiste en obtener los valores
de varias variables booleanas que cumplen cierta condición o función lógica18.
Por ejemplo, la siguiente expresión lógica:
Cada valor de xi puede ser 1 o 0 e i = 4, ya que tenemos cuatro variables
booleanas. Por lo tanto, tenemos 24 posibles soluciones. Para resolver el
problema, debemos evaluar todas las posibilidades y comparar si se cumple la
condición o no. Este procedimiento se conoce en electrónica digital como “tabla
de verdad” y requiere tiempo exponencial (2n intentos en el peor de los casos).
Se puede ver que el problema de factibilidad se puede reducir a 2n problemas de
decisión binarios, en los que la decisión es comprobar si se cumple (1 lógico) o
no se cumple la condición (0 lógico) para cada combinación de las variables.
Para la función lógica planteada, la solución sería x1 = 1,x2 = 0,x3 = 1,x4 = 1.
En general, todo problema en el que podamos hacer este tipo de reducción, será
de tipo NP-completo y, por consiguiente, también NP-duro. De hecho, este es el
procedimiento estándar para comprobar si un problema es de tipo NP-completo.
Lo que ocurre es que esta reducción solo se puede aplicar en problemas de
decisión, como el problema expuesto, en el que solo se comprueba si se cumple
la condición de satisfacción o no. En general, los problemas NP-completos o
NP-duros, para n pequeños, se pueden incluso resolver a mano. Sin embargo, si
aumentamos n necesitamos un ordenador para comprobar todas las condiciones;
y si aumentamos mucho n, es inabordable incluso para un súperordenador. Por
ello, este tipo de problemas se conocen como “problemas intratables”. Volviendo
al problema de factibilidad booleana, este es NP-completo ya que se puede
comprobar en tiempo polinomial que la solución es válida; basta con realizar las
operaciones lógicas correspondientes para una combinación de valores de las
variables. Sin embargo, hay problemas en los que esto no ocurre, por ejemplo el
problema del viajero. En el TSP, la única forma de saber si la solución es
óptima19 es resolver el problema, y el problema solo se puede resolver en
tiempo no polinomial o mediante un algoritmo no determinista. Por lo tanto, el
problema del viajero no es tipo NP, por lo que no puede ser NP-completo. Para
entender mejor la diferencia entre NP-completo y NP-duro en el contexto del
TSP, podemos mencionar el problema de encontrar un ciclo Hamiltoniano en un
grafo. Un ciclo es Hamiltoniano si pasa solo una vez por cada nodo del grafo.
Este problema es muy parecido al problema del TSP, pero en este caso es un
problema de decisión ya que solo hay que encontrar un ciclo que cumpla la
condición. Por lo tanto, estamos ante un problema NP-completo. En el caso del
TSP, no nos basta con encontrar un grafo Hamiltoniano; queremos el grafo de
distancia mínima, por lo que ya no estamos ante un problema de decisión.
Los problemas NP-completos son problemas de tipo NP-duros y NP a la vez. Los problemas N
Como hemos dicho, los problemas de tipo NP-completo se consideran los
problemas más complicados de resolver dentro de NP. Se puede demostrar,
mediante el teorema de Cook, que un problema NP se puede convertir por una
transformación determinista en tiempo polinomial en NP-completo. Esta
propiedad es muy importante ya que si se encontrara una solución a un problema
NP-completo en tiempo polinomial con una máquina determinista, se podrían
resolver todos los problemas de tipo NP.
Existen más tipos de complejidad, pero su clasificación está fuera del alcance de
este libro20. Es evidente que los algoritmos genéticos son una buena herramienta
para aquellos problemas que son del tipo NP-duro y NP-completo, ya que
requieren tiempo exponencial para ser resueltos. En los problema de tipo P,
cuando el número de variables es muy grande, el número de operaciones
necesarias para obtener una solución exacta puede ser muy elevado; en dicho
caso, los algoritmos genéticos son también una buena alternativa. La Figura 2.8
representa el número de operaciones necesarias según su complejidad. Se puede
observar cómo la complejidad de tipo exponencial 2n crece mucho incluso para
valores pequeños de n.
Figura 2.8. Número de operación según la complejidad.
Por último, nos gustaría comentar que existe hoy en día un debate abierto sobre
si todos los problemas NP son o serán de tipo P ya que, de vez en cuando,
aparecen soluciones en tiempo polinomial y con algoritmo deterministas a
problemas de tipo NP, convirtiéndolos así en tipo P. Es por ello que existe un
debate abierto en la comunidad científica que consiste en demostrar si P = NP.
En otras palabras, se intenta demostrar si ambos conjuntos se solapan. Este
problema está considerado como uno de los problemas del milenio, y hay una
recompensa de un millón de dolares para quien lo demuestre21.
2.11 Código completo y lecciones aprendidas
El siguiente Código 2.8 muestra todas las líneas de código necesarias para
resolver el problema del viajero. Como resumen del código:
■Las líneas 1-8 importan las librerías y módulos necesarios.
■Las líneas 11-12 cargan el archivo JSON como un diccionario en el objeto tsp .
Dicho diccionario incluye información relevante del problema como, por
ejemplo, la matriz de distancias entre ciudades ( distance_map ) y el tamaño del
tour o número de ciudades ( IND_SIZE ).
■Las líneas 20 y 21 definen el problema de minimización mediante el atributo
weights = (–1.0 , ) y la clase plantilla para los individuos, que en la mayoría de
los casos será una lista. En cuanto a la representación de las soluciones, cada
individuo define el orden de visita de las ciudades.
■La línea 23 crea el objeto caja de herramientas. La línea 25 registra la función
indices necesaria para crear los genes aleatorios de los individuos. La línea 28
registra la función individual que permite generar un individuo aleatorio. La
línea 30 registra la función population que permite crear la población inicial.
También se define el tamaño de la población, que será de 100 individuos.
■Las líneas 33-40 definen la función objetivo, la cual mide la distancia total del
tour . Para ello, primero calcula la distancia entre la primera y la última ciudad
del tour y, después, se recorren de dos en dos las ciudades consecutivas en el
tour gracias al bucle de las líneas 38-39. Finalmente, se devuelve la distancia
total recorrida. No hay olvidar la coma, ya que la función objetivo siempre debe
devolver una tupla.
■Las líneas 43-46 registran los operadores genéticos utilizados: cruce ordenado
(línea 43), mutación mediante intercambio de índices (línea 44), selección
mediante torneo (línea 45) de tamaño tres y el registro de la función objetivo
(línea 46).
■La función plot_evolucion (líneas 48-66) ya ha sido utilizada en el ejemplo del
Capítulo 1 ; en este caso, simplemente se han modificado los valores máximo y
mínimo del eje y en la línea 58 para poder visualizar mejor la evolución del
algoritmo.
■La función main ejecuta el algoritmo genético. En primer lugar, definimos la
semilla del generador de números aleatorios (línea 69). La línea 70 define los
hiperparámetros del algoritmo, tales como la probabilidad de cruce ( CXPB =
0.7), la probabilidad de mutación ( MUTPB = 0.3) y el número de generaciones
del algoritmo ( NGEN = 120). La línea 71 crea la población inicial de 100
individuos. La línea 72 indica el tamaño los parámetros µ y λ del algoritmo µ + λ
. Se definen del mismo tamaño que la población inicial: por lo tanto, la
población extendida es de doble tamaño. La línea 73 crea el objeto hof que
almacenará el mejor individuo a lo largo de la evolución del algoritmo. Las
líneas 74-78 definen el objeto estadístico y registran las funciones para obtener
las métricas de evolución de la población. La línea 79 crea el objeto registro de
evolución log , que almacenará todos los datos de evolución del algoritmo.
Finalmente, la línea 80 ejecuta el algoritmo µ + λ y devuelve la población final (
pop ) y el registro de evolución.
■Para finalizar el código, la línea 87 llama a la función main para ejecutar el
algoritmo y las líneas 88-89 muestran por pantalla los mejores resultados. En
último lugar, la línea 90 ejecuta la función plot_evolucion para visualizar la
evolución del algoritmo.
Código 2.8. Código completo problema TSP.
En cuanto a las lecciones aprendidas:
■En este capítulo hemos aprendido la diferencia entre init.Repeat y init.Iterate
para la creación de individuos aleatorios. Si disponemos de una función que nos
crea de manera aleatoria cada uno de los genes, debemos utilizar init.Repeat .
Por otro lado, si tenemos una función que nos genera todos los genes del
individuo debemos utilizar init.Iterate . También es importante recordar que para
la creación de la población inicial siempre utilizaremos init.Repeat .
■Existen problemas en los que las variables no pueden estar repetidas, como en
el problema del viajero. Para ese tipo de casos existen algoritmos de cruce y
mutación que respetan dicha condición. En este capítulo se ha utilizado el cruce
ordenado, que permite intercambiar información genética entre dos individuos
para crear otros dos individuos sin que exista duplicidad de genes en la
descendencia. El operador de mutación utilizado en este capítulo, basado en la
mezcla de índices, también respeta la condición de que no se repitan valores de
variables, siempre y cuando el individuo original respete la condición.
■Se ha utilizado un algoritmo genético µ + λ , el cual está basado en crear una
población extendida de tamaño µ + λ , creando una descendencia de tamaño λ ,
mediante los operadores genéticos (selección, cruce y mutación). De la
población extendida (población actual más descendencia) se seleccionan λ
individuos para la siguiente generación. Si los valores de µ y λ coinciden, el
tamaño de la población no varía. La principal novedad de este algoritmo es que
presenta un mayor elitismo que el eaSimple , ya que los progenitores compiten
con su descendencia para pasar a la siguiente generación.
■Hemos estudiado cómo acelerar la convergencia del algoritmo genético. En
concreto, hemos analizado dos técnicas: aumentar el tamaño del torneo y aplicar
elitismo.
■En este capítulo también hemos aprendido cómo utilizar archivos de texto para
realizar un análisis más profundo del impacto de las probabilidades de cruce y
mutación en los resultados obtenidos. Esta técnica es muy útil cuando se quiere
comparar los resultados de distintas configuraciones. También se puede utilizar
para comparar distintos algoritmos genéticos u operadores genéticos; por
ejemplo, si queremos comparar los resultados del algoritmo genético para
distintos operadores de cruce y/o mutación.
■Por último, hemos aprendido a clasificar los problemas de optimización según
su complejidad. En particular, se han definido los problemas de tipo NP, P, NPduro y NP-completo, el nicho de aplicación que tienen los algoritmos genéticos
para la resolución de problemas NP-duros, NP-completos y los problemas de
tipo P con un gran número de dimensiones.
2.12 Para seguir aprendiendo
La literatura sobre el problema del viajero es amplia, ya que es un problema
clásico. A continuación, se destacan varias fuentes:
■Una referencia clásica sobre el problema del viajero se puede encontrar en
(Goldberg et al., 1985).
■En (Abdoun et al., 2012) se analiza la solución del problema considerando
distintos operadores genéticos. Para más información sobre el algoritmo genético
µ + λ se recomienda consultar (Ter-Sarkisov y Marsland, 2011).
■El modelo del problema del TSP puede ser utilizado para resolver problemas de
cobertura o planificación de rutas en vehículos autónomos. En (Arzamendia et
al., 2016), (Arzamendia et al., 2019b) y (Arzamendia et al., 2019) se utilizó el
problema del viajero para planificar las rutas óptimas de un vehículo acuático
autónomo de superficie para monitorizar la calidad del agua del lago Ypacarai en
Asunción (Paraguay). Una extensión de dichos trabajos se encuentra en
(Arzamendia et al., 2018) y (Arzamendia et al., 2019a). En este caso, el
problema de planificación de rutas del vehículo se plantea con el modelo del
cartero chino ( Chinese Postman Problem ) ((Edmonds y Johnson, 1973)),
resuelto también con algoritmos genéticos 22.
■Existe una amplia literatura sobre el dilema P = NP . Por ejemplo, en (Fortnow,
2009) se puede encontrar una buena revisión sobre este problema.
Como ejercicios se plantean los siguientes:
■En el repositorio del libro 23, se encuentran los archivos JSON para 24 y 120
ciudades; se propone a los lectores que modifiquen el código de esta sección
para resolver el problema del TSP para esos números de ciudades.
■Realice una comparación entre los operadores de cruce estudiados en esta
sección.
■Realice una comparación entre las dos implementaciones de algoritmo genético
estudiadas ( eaSimple y µ + λ ) para el problema con 24 ciudades. Para ambas
implementaciones debe utilizar la misma configuración en cuanto a operadores
genéticos, tamaño de población y probabilidades.
■Cambie el objetivo del problema del viajero para maximizar la distancia que
recorre el agente. Debemos comprobar que el algoritmo funciona correctamente.
■Para el problema con 17 ciudades, introduzca rutas prohibidas en la matriz de
distancia. Cuando una ruta esté prohibida entre dos ciudades, penalice con el
valor de 1e6 (u otro valor elevado). Utilice la pena de muerte para penalizar
aquellas soluciones que sean inválidas.
_________________
1Más adelante cuando hablemos de la complejidad de los problemas, se definirá
qué es un tiempo razonable.
2Según la Ley de Moore, que predice que el número de transistores que incluye
un microprocesador se duplica cada 18-24 meses.
3Hay que recordar que range(N) genera valores desde 0 a N – 1.
4Para más información sobre random.sample, consultar
https://docs.python.org/3/library/random.html
5https://deap.readthedocs.io/en/master/api/tools.html#deap.tools.initRepeat
6https://deap.readthedocs.io/en/master/api/tools.html#deap.tools.initIterate
7https://deap.readthedocs.io/en/master/api/tools.html#deap.tools.cxPartialyMatched
8https://deap.readthedocs.io/en/master/api/algo.html
9https://deap.readthedocs.io/en/master/examples/eda.html
10El número de veces dependerá del tiempo que estemos dispuestos a esperar.
11También se podría pasar como argumento al main.
12Siempre se pueden encontrar excepciones a esta afirmación. Por lo tanto, no
hay que tomarlo como una regla general sino más bien como una
recomendación.
13https://docs.python.org/3.3/library/functions.html#zip
14Solo se debe registrar un operador por cada operación genética. En caso
contrario, solo el último tendrá efecto.
15Este aspecto dependerá de los recursos computacionales que tengamos
disponibles.
16https://deap.readthedocs.io/en/master/examples/ga_onemax.html
17Es solo una posibilidad, puede ser que sí o puede ser que no.
18Este problema se considera la madre de los problemas NP-completos.
19No estamos buscando si existe una posible ruta; estamos buscando la ruta
óptima.
20En el siguiente vídeo se explica con más detalles los tipos de problemas que
podemos encontrar: https://www.youtube.com/watch?v=YX40hbAHx3s
21Si algún lector se anima, aquí dejamos el enlace, le deseamos suerte y le
animamos a que comparta el premio con nosotros
http://www.claymath.org/millennium-problems/millennium-prize-problems
22El problema del cartero chino es similar al problema del viajero, pero en este
caso se quiere encontrar el ciclo Euleriano de distancia mínima de un grafo.
23https://github.com/Dany503/Algoritmos-Geneticos-en-Python-Un-EnfoquePractico
3.1 Introducción a las funciones de benchmark
Llegados a este punto, ya somos capaces de desarrollar nuestros propios
algoritmos genéticos. No obstante, como hemos visto en el primer capítulo, el
uso de estrategias metaheurísticas tiene el inconveniente de no proporcionar
certeza alguna sobre la optimalidad de las soluciones obtenidas. Así, antes de
aventurarnos a resolver problemas de ingeniería con los algoritmos que
programamos, es de vital importancia que nos aseguremos de su correcto
funcionamiento.
¿Cómo comprobamos si funcionan bien, si hemos dejado claro que el algoritmo no nos da nin
Bien, aquí entran en juego las denominadas “funciones de benchmark". Se
denominan así a una serie de funciones de diferente naturaleza y complejidad,
pero cuyas soluciones óptimas son conocidas, de manera que pueden servir de
test para comprobar el buen funcionamiento de nuestros algoritmos. También
son ampliamente utilizadas por la comunidad científica a la hora de demostrar la
validez de nuevos operadores genéticos. Así, cuando un investigador propone un
nuevo mecanismo de cruce o mutación, lo prueba utilizando funciones de
benchmark para validar su hipótesis.
La librería deap pone a nuestra disposición un amplio abanico de funciones de
benchmark que podemos importar fácilmente en nuestro código. Estas funciones
están clasificadas en cuatro grupos, en función de la naturaleza del problema de
optimización que permiten estudiar: continuas con un objetivo, continuas
multiobjetivo, binarias y de regresión simbólica. En la Tabla 3.1 se listan las
funciones disponibles1. Es importante elegir adecuadamente la/s función/es que
usemos para testear nuestros algoritmos, pues cada una de ellas tiene ciertas
características que las hacen relevantes para evaluar ciertas capacidades del
algoritmo, por ejemplo, tener un gran número de óptimos locales o ser muy
planas en el entorno del óptimo global.
Cabe indicar que de la Tabla 3.1 nos vamos a centrar en el estudio de las función
continuas con un solo objetivo (la primera columna). Las funciones
multiobjetivo se verán en el siguiente capítulo. En cuanto a las funciones de
regresión simbólica, se utilizan para algoritmos de programación genética, los
cuales son otra rama de la computación evolutiva, que queda fuera del alance de
este libro (Koza, 1992).
Tabla 3.1. Funciones de benchmark implementadas en el módulo deap
Continuas
Monoobjetivo
Binarias
Multiobjetivo
Regresión simbólica
cigar()
plane()
sphere()
rand()
ackley
bohachevsky()
griewank()
h1()
himmelblau()
rastrigin()
rastrigin_scaled()
rastrigin_skew()
rosenbrock()
schaffer()
schaffer()
schwefel()
shekel()
fonseca()
kursawe()
schaffer_mo()
dtlz1()
dtlz2()
dtlz3()
dtlz4()
zdt1()
zdt2()
zdt3()
zdt4()
zdt6()
chuang_f1()
chuang_f2()
chuang_f3()
royal_road1()
royal_road2()
kotanchek()
salustowicz_1d()
salustowicz_2d()
unwrapped_ball()
rational_polynomial()
rational_polynomial2()
sin_cos()
sipple()
3.2 Aprendiendo a usar las funciones de benchmark : Formulación del
problema
En esta sección vamos a formular el problema de hasta tres funciones de
benchmark: un problema de maximización y dos de minimización. Una
propiedad que tienen los tres problemas que veremos a continuación, es que los
tres tienen variables continuas comprendidas en un determinado intervalo.
3.2.1 Función h1
La función h1, representada en la Figura 3.1, es una función continua en dos
dimensiones, con un máximo global y varios máximos locales. Observando la
figura, podemos comprobar que aparecen máximos locales en las proximidades
del máximo global, lo cual constituye un enorme inconveniente para los métodos
analíticos basados en gradiente. Aunque la función h1 se puede usar con más de
dos variables, nosotros nos ceñiremos al caso de dos dimensiones.
Figura 3.1. Representación de la función de benchmark h1 para dos dimensiones
sin_cos()
3.2.2 Función Ackley
La función Ackley (representada en la Figura 3.2) es continua en N dimensiones,
con un mínimo global y una gran cantidad de mínimos locales. Nosotros la
estudiaremos con N = 4.
Figura 3.2. Representación de la función de benchmark Ackley para dos
dimensiones.
3.2.3 Función Schwefel
La función Schwefel (representada en la Figura 3.3) es una función continua con
un alto número de mínimos y máximos locales. Se puede definir para N
dimensiones y, en nuestro caso, vamos a llegar hasta N = 40, para tratar un
problema con un número alto de variables.
Figura 3.3. Representación de la función de benchmark Schwefel para dos
variables
Una vez formulados los tres problemas que se abordarán en este capítulo,
seguiremos los mismos pasos que en los anteriores capítulos: definir el problema
en deap y crear la población inicial.
3.3 Definición del problema y generación de la
población inicial
Aunque en este capítulo se abordarán tres problemas distintos, los tres
comparten el tipo de variables, esto es, variables continuas. Tendremos que
distinguir en cada caso si el problema es de maximización o minimización.
En el caso de la función h1, el problema se define de la siguiente forma:
maximización:
Mediante el atributo weights definimos el problema como de único objetivo y de
maximización. Tanto para la función Ackely como para Schwefel, debemos
proceder de la siguiente forma:
En ambos casos estamos interesados en minimizar la función objetivo. A
continuación, debemos crear la plantilla para almacenar nuestro individuos. En
este caso, para los tres problemas podemos proceder de la siguiente forma:
Estamos creando una nueva clase Individual que hereda de la clase array del
módulo array2 (ver Apéndice A). Los objetos de tipo array tienen el atributo
typecode que define el tipo de datos que contienen. En este caso, el valor ” f ”
indica que los elementos del vector o array son de tipo flotante.
El siguiente paso es crear la caja herramientas o toolbox y registrar las funciones
que nos permitirán crear individuos aleatorios para la población inicial.
En primer lugar, vamos a definir una función que nos genere un lista de números
flotantes entre dos límites, y la usaremos para los tres casos que estamos
estudiando. Mediante el siguiente código, la función creará una muestra de un
tamaño especificado por tam, cuyos genes tendrán valores uniformemente
aleatorios entre los valores min y max:
Esta función es muy versátil y nos será útil para la mayoría de problemas de
benchmark de variables continuas de la Tabla 3.1. Sin embargo, antes debemos
definir unas variables que dependerán de cada función de benchmark, ya que
determinan el rango de las variables del problema.
Esas tres variables definen:
■minimo : Valor mínimo del rango de las variables del problema en el intervalo
de generación.
■maximo : Valor máximo del rango de las variables del problema en el intervalo
de generación.
■tamaño : Número de variables del problema.
Los valores anteriores son los rangos de la función h1. Para la función Ackely y
Schewel, únicamente debemos cambiar los valores de las variables con respecto
a la formulación de dichos problemas. Pasamos ahora a registrar dicha función
en el objeto toolbox. La función se ha registrado con el alias attr:
Es importante observar que la función crea_individuo crea el cromosoma
completo del individuo, por lo tanto, debemos utilizar la función initIterate para
registrar la función que nos permite crear individuos aleatorios.
Como ejemplo de creación de individuos, en el Resultado 3.1 se muestra la
generación de un individuo para la función de benchmark h1 utilizando la
función que acabamos de registrar.
Resultado 3.1. Ejemplo de individuo para el problema h1.
Para registrar la función que nos genera la población inicial, procedemos como
hemos hecho en capítulos anteriores.
3.4 Función objetivo y operadores genéticos
Una vez hemos definido los problemas que queremos resolver y sabemos cómo
crear la población inicial, pasamos a describir el procedimiento para acceder a
las funciones de benchmark desde deap y a describir algunos operadores
genéticos nuevos.
3.4.1 Función objetivo
En primer lugar, es indispensable que importemos el módulo3 de la librería
correspondiente:
Todas las funciones son accesibles a través de dicho módulo; por lo tanto, para
registrar la función de evaluación debemos proceder de la siguiente forma:
En este caso, estamos registrando la función h1, pero el procedimiento sería
análogo para cualquier otra función de benchmark; simplemente debemos
modificar el último parámetro. Para la función de Ackley debemos utilizar
benchmarks.Ackley, y benchmarks.Schwefel para la función de Schewefel.
Otra forma de utilizar las funciones de benchmark, sería construir la función de
fitness llamando a la función de benchmark correspondiente, donde debemos
sustituir funcion por el nombre de la función, tal y como aparece en la Tabla 3.1:
El registro de dicha función se haría de la siguiente forma:
Una cosa importante que debemos tener en cuenta en este caso, es que ahora no
se devuelve el objetivo como una tupla, ya que la función dentro del módulo
benchmark se encarga de ello. Así, el script anterior devuelve fitness y no
fitness, como hemos visto en capítulos anteriores.
3.4.2 Operadores genéticos
Veamos ahora las operaciones genéticas de cruce, mutación y selección. En este
capítulo vamos a aprovechar para definir nuevos operadores genéticos que no se
han utilizado hasta ahora.
Con respecto a los operadores de cruce, vamos a introducir dos nuevos
mecanismos: (i) Simulated Binary Crossover (SBX) y (ii) Blend Crossover
(Blend). El primero de estos mecanismos, SBX, se basa en el operador
cxSimulatedBinaryBounded, y es un poco más sofisticado que el Blend, como
veremos a continuación. Este operador se basa en el operador
cxSimulatedBinary, con la consideración adicional de establecer unos límites,
low y up, para los individuos. Para registrar dichos operadores se procedería de
la siguiente forma:
En este caso, se ha elegido un valor de eta(η) igual a 2, pero dicho valor podría
ser ajustado libremente. Más adelante veremos cómo afecta al funcionamiento
de este operador de cruce.
Por otro lado, también tenemos disponible en deap el operador de Blend. Para
registrarlo en la caja de herramientas debemos proceder de la siguiente manera:
En este caso, se ha elegido un parámetro alpha igual a cinco (α = 0.5). No
obstante, ese valor se puede ajustar según nos interese, como veremos más
adelante.
Ambas técnicas de cruce se pueden utilizar para variables continuas. Es decir, se
pueden utilizar en los tres problemas que se abordarán en este capítulo. Por el
contrario, el SBX y la operación de Blend no pueden usarse en problemas con
variables discretas. Por último, es importante añadir que si queremos que los
progenitores no se salgan de los intervalos establecidos para las variables,
tenemos que tener cuidado con el parámetro α en la operación de blend.
A diferencia de lo que hemos visto en capítulos anteriores, aquí vamos a definir
nosotros un mecanismo de mutación. Hasta ahora hemos utilizado métodos de
mutación que vienen implementados en deap. No obstante, la librería permite
definir nuevos métodos, tanto para el cruce como para la mutación, y registrarlos
en la caja de herramientas.
Para la mutación vamos a crear un operador similar a mutGauss (estudiado
previamente), al que llamaremos mutTriangular.
En cuanto al mecanismo de selección, en este capítulo introduciremos otro
procedimiento de selección muy popular, como es la selección mediante ruleta.
Se propone como ejercicio evaluar el funcionamiento del algoritmo utilizando
operadores de mutación alternativos, como por ejemplo el operador de mutación
Gaussiana, introducido en el Capítulo 1.
Por último, configuraremos nuestro algoritmo para utilizar unas probabilidades
de cruce y mutación de 0.7 y 0.3, respectivamente, así como un tamaño de
población de 100 individuos y un total de 100 generaciones. Esto se hace
fácilmente en una sola línea:
Antes de continuar, nos gustaría recalcar las diferencias entre la selección por
ruleta y la selección mediante torneo:
■La selección mediante ruleta, en teoría, solo se puede utilizar para problemas
de maximización y nunca si la función de fitness devuelve valores negativos, ya
que daría lugar a probabilidades negativas. Una posibilidad es cambiar el signo
al fitness de los individuos.
■La selección mediante torneo siempre se puede utilizar y, en general, es más
elitista que la selección mediante ruleta.
■El principal problema que tiene la selección por ruleta es que, a medida que se
avancen las generaciones del algoritmo genético, los individuos comenzarán a
parecerse entre sí, en términos de cromosoma y fitness , por lo que todos los
individuos de la población tendrán prácticamente la misma probabilidad de ser
seleccionados como progenitores. Ese problema no ocurre con la selección
mediante torneo, ya que un individuo que tenga un mayor fitness siempre tendrá
mayor probabilidad de ser seleccionado, por muy pequeña que sea la mejora en
su fitness . Para ilustrar el problema, consideremos el siguiente ejemplo:
tenemos un algoritmo genético en un problema de maximización con una
población de cuatro individuos. En un determinado momento de la evolución del
algoritmo, el fitness f de los cuatro individuos es f 1 = 10.1, f 2 = 10.2, f 3 = 10.3
y f 4 = 10.4. Aplicando la selección mediante ruleta, la probabilidad p de
seleccionar cada individuo es p 1 = 0.246, p 2 = 0.248, p 3 = 0. , 251 y p 4 =
0.253. Podemos comprobar que son probabilidades muy parecidas, todas rondan
el 0.25, lo que significa tienen casi las mismas probabilidades de ser
seleccionadas, ya que hay muy poca diferencia entre la mejor solución ( p 4 =
0.253) y la peor ( p 1 = 0.246). Por el contrario, si utilizamos la selección
mediante torneo, si consideramos un torneo de tamaño dos, la mejor solución
tiene una probabilidad de ganar p 4 = 0.437 4. Por lo tanto, el mejor individuo ha
pasado de tener un 25.3% a tener un 43.7% de posibilidades de ser seleccionado.
3.5 Código completo
Puesto que vamos a utilizar algoritmos muy similares para evaluar las funciones
de benchmark, en este capítulo haremos una excepción e introduciremos primero
el código completo, sobre el cual realizaremos pequeñas modificaciones para
utilizarlo con las diferentes funciones propuestas. Así, el algoritmo completo se
muestra en el Código 3.2. Este código se desarrolla de la siguiente manera:
■Líneas 1-10: En primer lugar, importamos las librerías necesarias.
■Líneas 12-13: Creamos los objetos para definir el problema y el tipo de
individuo.
■Línea 16: Creamos el toolbox donde incluiremos todas las herramientas
necesarias.
■Líneas 18-20: Aquí, definiremos el dominio del problema (el rango de valores
posibles de los genes), y el tamaño del individuo.
■Líneas 22-24: Creamos la función de generación de individuos, en este caso
mediante siguiendo una probabilidad uniforme entre los límites indicados.
■Líneas 26-33: Registramos todas las funciones necesarias, tanto las relativas a
la creación de individuos y de la población inicial (líneas 26 a 28) como los
operadores de cruce, mutación y selección (líneas 30 a 32) y la función de fitness
(línea 33).
■Líneas 35-51: Función principal para lanzar el algoritmo genético.
■Líneas 53-55: Ejecución de la función principal.
Código 3.2. Código final para testear las funciones de benchmark.
3.6 Evaluación de algunas funciones de benchmark
Utilizaremos el código anterior, con pequeñas modificaciones que detallaremos,
para evaluar algunas de las funciones que hemos introducido al principio de este
capítulo.
Para cada una de las ejecuciones, analizaremos gráficamente la evolución de los
individuos a lo largo de las generaciones utilizando la función plot_evolucion
descrita en capítulos anteriores.
Y evaluaremos el fitness del mejor individuo:
3.6.1 Función h1
Para evaluar este problema, deberemos hacer las siguientes modificaciones en
nuestro código:
Configuramos nuestro problema como maximizacion:
Elegimos la función h1 como fitness:
Con respecto al rango de las variables y al tamaño del problema, en este caso
generaremos individuos de dimensión 8, cuyos genes tendrán valores entre -100
y 100:
Tras 100 generaciones, usando una población de 100 individuos, el mejor
individuo obtenido es el obtenido en el Resultado 3.3:
Resultado 3.3. Obtención del mejor individuo para el problema h1.
Su evaluación en la función de fitness es 1.9979077811118524, muy próxima al
máximo teórico. La evolución del fitness a lo largo de las generaciones se
muestra en la Figura 3.4.
3.6.2 Función Ackley
Para evaluar este problema, deberemos hacer una serie de modificaciones en
nuestro código. En primer lugar configuraremos nuestro problema como
minimización:
Elegimos la función Ackley como función de fitness:
Figura 3.4. Evolución del fitness para el problema h1.
Elegimos adecuadamente los individuos. En este caso, generaremos individuos
de dimensión 8, cuyos genes tendrán valores entre -15 y 30:
Por último, puesto que el problema es de minimización, no podemos utilizar el
mecanismo de selección de ruleta, de manera que recurriremos a un proceso de
selección por torneo (operador selTournament5) con tres individuos:
Tras 100 generaciones, usando una población de 100 individuos, se obtiene el
siguiente mejor individuo:
Resultado 3.4. Obtención del mejor individuo para el problema Ackley.
Su evaluación en la función de fitness es 0.0005428258269102315, muy
próxima al máximo teórico (0). Para mejorar el resultado podríamos incrementar
el número de individuos que componen la población (en este caso eran 100). La
evolución del fitness a lo largo de las generaciones se muestra en la Figura 3.5.
3.6.3 Función Schwefel
Estudiaremos este problema en 40 dimensiones. Así, empezaremos configurando
nuestro problema como minimización:
Figura 3.5. Evolución del fitness para el problema Ackley.
Elegimos la función schwefel como fitness, indicando las matrices comentadas
anteriormente:
Vamos a poner a prueba nuestro algoritmo aumentando notablemente la
complejidad del problema. Para ello, estudiaremos el problema con dimensión
40; es decir, cada individuo estará compuesto por 40 genes, que tendrán valores
entre 0 y 500:
Para este caso, además, incrementaremos el número de individuos a 1000 y el
número de generaciones a 300. Para ello basta utilizar:
Por último, volvemos a utilizar de nuevo el mecanismo de selección por torneo,
pero esta vez el torneo será de 4 individuos:
Tras las 150 generaciones se obtiene el siguiente mejor individuo:
Resultado 3.5. Obtención del mejor individuo para el problema Schwefel.
cuya evaluación en al función de fitness es 0.0187036032184551, bastante
próxima al mínimo teórico. Un paso adecuado sería calibrar adecuadamente las
probabilidades de cruce y mutación para comprobar su influencia. La evolución
del fitness a lo largo de las generaciones se muestra en la Figura 3.6.
Figura 3.6. Evolución del fitness para el problema Schwefel.
3.7 Ajuste de los hiperparámetros de los operadores
genéticos
En esta sección vamos a analizar la influencia de los hiperparámetros de ajuste
en algunos de los operadores genéticos estudiados en este capítulo.
En primer lugar, vamos a empezar analizando el parámetro η del operador SBX.
Este parámetro controla la distancia ente los hijos y los padres. Como regla, un
valor alto de η crea hijos muy cercanos a los padres. La mejor forma de ver esta
regla es mediante un ejemplo gráfico. Imaginemos que tenemos un problema con
dos variables independientes, como el problema h1, representadas como [x1,x2].
En la Figura 3.7, se puede observar la distancia en el plano x1,x2 de los
descendiente frente a los progenitores. Las coordenadas de los progenitores en el
plano x1,x2 son [0.3,0.3] y [0.7,0.7]6. Se han generado hasta 10 descendientes
distintos y en cada operación los padres son siempre los mismos. Se puede
observar que conforme disminuimos el valor de η, los hijos se separan más de
los padres. Para valores altos, la descendencia que se crea se parece mucho y
está muy cerca en distancia a los padres. Otro aspecto interesante es observar el
cruce de información genética entre las variables x1 y x2. Se pueden observar
hasta cuatro conjuntos de soluciones. Dos de ellas se corresponden con los genes
originales, y las otras dos, son el intercambio de genes entre los progenitores. Es
por ello, que este operador genético realiza una función parecida al cruce de un
punto, pero añadiendo un desplazamiento con respecto a los genes originales.
Por último, hay que tener en cuenta que la Figura 3.7 es solo una representación
de un caso particular, ya que en cada caso las distribuciones serán distintas. Este
ejemplo solo intenta ilustrar el efecto de η en la creación de la descendencia con
respecto a los progenitores. Sería interesante poder variar el valor de η a lo largo
de las generaciones, pero desgraciadamente, la librería deap no tiene
mecanismos para hacerlo; tendríamos que modificar nosotros el operador
genético y la implementación del algoritmo.
Figura 3.7. Descendencia creada con el operador SBX en función del
hiperparámetro η.
A continuación, vamos a estudiar el hiperparámetro α en el operador de Blend.
Lo vamos a hacer con el mismo ejemplo, pero en este caso modificando el valor
de α para generar la descendencia. La Figura 3.8 muestra los resultados
obtenidos. Se puede ver cómo a medida que aumenta el valor de α aumenta la
distancia entre los padres y los hijos. Si α = 0, los hijos están confinados entre
los genes de los padres. Un aspecto importante en la operación de Blend es que,
a partir de ciertos valores de α, es muy probable que los genes se salgan fuera de
los rangos de las variables, en caso de que estos existan.
En la operación de Blend no hay intercambio de genes tal y como se produce en
el operador SBX. Esta es, sin duda, la mayor diferencia entre los dos operadores.
Al igual que en la operación de SBX, sería interesante disminuir el valor de α a
medida que avanzamos en las generaciones del algoritmo genético7.
En resumen, en esta sección se ha analizado la influencia de los hiperparámetros
en dos populares operadores genéticos para problemas con variables continuas.
En este momento el lector se estará preguntando cómo elegir los parámetros
óptimos para su problema en cuestión. Desgraciadamente, no existe una
respuesta universal a dicho problema, ya que el hiperparámetro óptimo
dependerá del problema en cuestión. En general, se recomienda hacer un barrido
con distintos valores para ver los resultados. Si no es posible realizar un barrido
porque requiera mucho tiempo, se recomienda no utilizar valores extremos. Por
ejemplo, η = 2 y α = 1 pueden ser dos buenos puntos de partida.
3.8 Lecciones aprendidas
A modo de resumen, podemos destacar las siguientes lecciones aprendidas:
■Que nuestros algoritmos genéticos converjan correctamente no es suficiente
para garantizar su buen rendimiento a la hora de resolver diferentes problemas
de ingeniería, por lo que antes de utilizarlos para aplicaciones de ingeniería
debemos garantizar que son capaces de resolver problemas tipo o funciones de
benchmark .
Figura 3.8. Descendencia creada con el operador Blend en función del
hiperparámetro α.
■Para ello, tenemos a nuestra disposición un amplio abanico de funciones,
denominadas funciones de benchmark , implementadas en la librería de deap .
Estas funciones tienen soluciones conocidas y, además, constituyen diferentes
retos para testear diferentes aspectos de nuestros algoritmos como, por ejemplo,
evitar óptimos locales.
■Como estas funciones ya están implementadas, simplemente tenemos que
importar la librería de benchmark y definir la función con la que queramos
trabajar como función de fitness .
■Analizando los resultados de la aplicación de nuestros algoritmos con
funciones de benchmark podemos identificar rápidamente potencialidades y
vulnerabilidades de nuestros algoritmos, lo cual será de vital importancia para su
aplicación en problemas de ingeniería.
■Las funciones de benchmark devuelven el resultado como una tupla, aunque
sea una función mono objetivo.
■Se han presentado dos nuevos operadores de cruce adecuados para problemas
con variables continuas: SBX y Blend . Con respecto al primero, se ha utilizado
su versión bounded , que impide que los valores de las variables se salgan de
cierto intervalo. En ambos casos, tenemos parámetros de ajuste que afectan a la
distancia de los descendientes con respecto a los progenitores.
■En este capítulo hemos visto cómo se puede crear un operador de mutación
propio, no incluido en la librería. No debemos olvidar el alias con el que se debe
registrar la función en el objeto toolbox . Para registrar operadores de cruce
debemos utilizar el alias mate ; para el caso de mutación, debemos utilizar el
alias mutation y, finalmente, para la selección debemos utilizar select .
■Hemos presentado la selección mediante ruleta. Este mecanismo solo funciona
en problemas de maximimición 8. La selección mediante ruleta es menos elitista
que que la selección mediante torneo.
■Por último, hemos estudiado la influencia de los parámetros de ajuste en los
operadores genéticos.
3.9 Para seguir aprendiendo
A continuación, se detallan algunas referencias para seguir profundizando en los
conceptos aprendidos en este capítulo.
■Una comparación entre distintas técnicas de selección se puede encontrar en
(Goldberg y Deb, 1991).
■El artículo original donde se describe el método de cruce Simulated Binary
Crossover puede ser consultado en (Deb et al., 1995). Con respecto al operador
Blend , el trabajo original se puede encontrar en (Eshelman y Schaffer, 1993).
■En (Herrera et al., 2003) se puede encontrar una taxonomía de métodos de
cruce para variables continuas.
■La función de benchmark h1 se propuso en (Van Soest y Casius, 2003) donde,
además, se puede encontrar una interesante comparación entre distintas técnicas
de optimización.
Como ejercicios se plantean los siguientes:
■Utilice otras funciones de benchmark para variables continuas disponibles en
deap , y compruebe que se consigue alcanzar el máximo o mínimo global
correspondiente.
■Utilice funciones de variables binarias disponibles en deap 9, y compruebe que
sea capaz de alcanzar los máximos y mínimos correspondientes.
_________________
1https://deap.readthedocs.io/en/master/api/benchmarks.html
2https://docs.python.org/3/library/array.html
3shttps://www.overleaf.com/project/5e00d3f97979160001d2b7f6
4La probabilidad de no seleccionar ninguna de las dos veces al mejor es q = 0.75
× 0.75, ya que cada intento es independiente, por lo que la probabilidad de
seleccionar el mejor es p = 1 – q = 0.43.
5https://deap.readthedocs.io/en/master/api/tools.html#deap.tools.selTournament
6Se han elegido esos valores simplemente para mejorar la visualización de los
resultados.
7Se invita a los lectores a que trabajen en ello.
8Aunque siempre se puede multiplicar por -1 el fitness.
9https://deap.readthedocs.io/en/master/api/benchmarks.html#moduledeap.benchmarks.binary
4.1 Introducción a los problemas con múltiples
objetivos
Hasta ahora, los problemas que hemos visto solo tenían un único objetivo. En el
primer capítulo, vimos un problema de maximización con variables continuas
cuyo objetivo era obtener el máximo de una función de dos variables
independientes x e y. En el segundo capítulo, se estudió el popular problema del
viajero o TSP, que consiste en un problema de minimización con un solo
objetivo: optimizar la distancia que recorre el viajero. Este problema se abordó
con variables discretas (las diferentes ciudades que el viajero visita). En el tercer
capítulo, abordamos la optimización de distintos problemas de benchmark.
Aunque estos problemas nos han servido para motivar el uso de los algoritmos
genéticos, en un gran número de problemas reales que aparecen en el ámbito de
la ingeniería los ingenieros nos enfrentamos a problemas con múltiples
objetivos. Además, en la mayoría de los casos los objetivos son opuestos, es
decir, mejorar un objetivo implica empeorar otros. Un ejemplo claro de objetivos
opuestos es el de la potencia y el coste en ingeniería. En cualquier problema de
ingeniería, aumentar la potencia requiere un aumento del coste, y viceversa. Es
por ello que si nuestro algoritmo genético se centra en optimizar la potencia,
siempre estaremos aumentando el coste, y lo mismo ocurre a la inversa. Parece
lógico pensar que necesitamos un mecanismo específico que nos permita poder
balancear los dos objetivos para tener un visión conjunta de ambos en función de
las variables de nuestro problema. Una técnica muy usada para el primer
propósito se basa en asignar pesos a cada uno de los objetivos y agregarlos en
uno único (función de utilidad), convirtiendo así el problema en un solo objetivo.
Por ejemplo, imaginemos un problema de optimización de una planta industrial,
ilustrado en la Figura 4.1. Consideramos la potencia de la planta como P y el
coste como C; el problema multiobjetivo se convierte en monobjetivo de la
siguiente forma: F = w1 × P + w2 × C.
Figura 4.1. Planta industrial con xn parámetros de entradas y dos salidas P y C.
La cuestión, en este caso, es determinar los valores de w1 y w2 para maximizar
F. Si consideramos ambos objetivos igual de importantes, podemos hacer w1 =
w2 = 0.5. Esta técnica es totalmente válida y se utiliza mucho en la práctica; sin
embargo, el valor F puede ser el mismo para un valor de P alto y C bajo, y para
un valor P bajo y C alto. Lo podemos ver dando valores a los pesos, las
potencias y los costes. Si consideramos w1 = w2 = 0.5, podemos tener las
siguientes soluciones al problema:
■La primera solución con P 1 = 0.8 y C 1 = 0.2 nos lleva a F 1 = 0.5 × 0.8 + 0.5
× 0.2 = 0.5. En este caso, tenemos un valor alto de potencia y un valor bajo de
coste.
■La segunda solución con P 2 = 0.2 y C 2 = 0.8 nos lleva a F 2 = 0.5 × 0.2+0.5 ×
0.8 = 0.5. En este segundo caso, tenemos un valor de potencia bajo y un valor de
coste alto.
En este ejemplo, es fácil ver cómo dos soluciones muy distintas dan un mismo
valor de F. Por lo tanto, esta técnica de convertir un problema multiobjetivo es
un problema monobjetivo puede enmascarar los valores de los objetivos. Como
resultado, tenemos un mismo fitness para dos soluciones muy distintas en el
plano de los objetivos P y C. Este mismo problema lo tendríamos aunque
cambiásemos los valores de w1 y w2. En consecuencia, parece más interesante
tener un mecanismo que nos permita obtener un conjunto de soluciones que
representen todas las posibles combinaciones de los objetivos. Con ese conjunto
de soluciones, podemos decidir en función del presupuesto que tenemos. Es
decir, con un determinado coste podemos decir qué potencia podemos alcanzar,
y al contrario. Además, esto nos permite cambiar de solución en el momento en
que nuestro presupuesto cambie (variación en el coste) o nuestra demanda varíe
(modificación en la potencia requerida). Este mecanismo es el frente de Pareto
que veremos con más detalle en la siguiente sección.
4.2 Introducción a la Pareto dominancia
El frente de Pareto se define como el lugar geométrico que forman todas las
soluciones de nuestro problema que no están dominadas. Hay que preguntarse,
pues, qué significa que una solución esté dominada por otra.
Una solución domina a otra si es estrictamente mejor que la otra en todos los objetivos consid
Para entender mejor la dominancia de Pareto, lo mejor es verla con un ejemplo
práctico. La Figura 4.2 muestra cuatro soluciones Si para un problema con dos
objetivos, F1 y F2. En este caso, deseamos minimizar ambos objetivos. Podemos
considerar que esas cuatro soluciones representan cuatro individuos de la
población de un algoritmo genético. No estamos indicando cuántas variables
tiene nuestro problema pero, en general, podemos considerar que tenemos n
variables. Así, F1 = f (xn) e, igualmente, F2 = f (xn). En la Figura 4.2, cada Si
corresponde con unos valores concretos de las variables xn. En este caso, no nos
vamos a centrar en la representación de las variables, ya que estamos interesados
en comparar soluciones en función de los objetivos del problema.
Para saber qué soluciones están dominadas, debemos comparar de dos en dos las
cuatro soluciones.
■Empecemos comparando s 1 y s 2; s 1 es mejor que s 2 en F 2, ya que s 1 tiene
un valor de 10 en F 1, mientras que S 2 tiene un valor de 20. Sin embargo, con
respecto a F 1, ambas soluciones tienen el mismo valor; por lo tanto, no
podemos decir que ninguna de ellas sea estrictamente mejor que la otra en F 1.
Por este motivo, no podemos decir que s 1 domine a s 2. A su vez, s 2 no domina
a s 1.
■Continuamos ahora comparando s 1 y s 3; de nuevo s 1 es mejor que s 3 con
respecto al objetivo F 1, pero no ocurre lo mismo con respecto a F 2, ya que
ambas soluciones obtienen el mismo valor. Por lo tanto, tampoco podemos decir
que s 1 domine a s 3, ni que s 3 domine a s 1.
■Podemos seguir haciendo comparaciones, pero pasemos a realizar la
comparación más interesante para nuestro objetivo de obtener el frente de
Pareto. Esto es, pasemos a comparar s 1 y s 4. En este caso, sí podemos decir
que s 1 es estrictamente mejor que s 4 para los dos objetivos, F 1 y F 2. Por lo
tanto, podemos decir que s 1 domina a s 4 o, lo que es lo mismo, que s 4 está
dominada por s 1.
■Si seguimos haciendo comparaciones, llegaremos a la conclusión de que el
caso de s 1 y s 4 es el único en el que podemos encontrar una solución que
domina a la otra.
Llegado a este punto, se puede afirmar que el frente de Pareto para nuestro
ejemplo (según las cuatro soluciones que estamos considerando) está formado
por las soluciones s1, s2 y s3. Podemos incluso trazar una curva que una esas
tres soluciones y que nos dé la forma que tiene el frente de Pareto según las
soluciones que tenemos. La Figura 4.3 muestra el frente de Pareto para las
soluciones del ejemplo. Se puede observar que el frente de Pareto divide en dos
la gráfica. Todas las soluciones que puedan aparecer en el lado derecho estarán
dominadas por alguna solución del frente de Pareto. Por otro lado, si nos aparece
una nueva solución en el lado izquierdo, esto hará que el frente de Pareto se
actualice para contener dicha solución. Por lo tanto, cada vez que el algoritmo
genético encuentre una nueva solución no dominada, el frente de Pareto deberá
actualizarse.
Figura 4.2. Ejemplo de soluciones para un problema con dos objetivos, F1 y F2.
Figura 4.3. Frente de Pareto.
Parece lógico pensar que un buen algoritmo genético multiobjetivo será aquel
que nos permita obtener el frente de Pareto de una manera eficiente.
El objetivo de un algoritmo genético multiobjetivo será encontrar de manera eficiente el frente
Con el objetivo de ilustrar el concepto de frente de Pareto en un problema más
complejo de dos dimensiones, la Figura 4.4 muestra el frente de Pareto para la
función de benchmark ZDT1. Esa función tiene dos objetivos, ZDT11 y ZDT12,
y el objetivo es minimizar ambos objetivos simultáneamente. En la Figura 4.4 se
pueden ver, de nuevo, las zonas de soluciones que están dominadas y las zonas
que no. Otro apunte que debemos realizar es que cada punto del frente de Pareto
equivale a una solución del problema convertido a monobjetivo, tal y como se
explicó anteriormente. Es decir, si seleccionamos un punto del frente de Pareto
de la Figura 4.4, dicho punto representa la solución del problema equivalente
monobjetivo F = w1 × f ZDT1 + w2 × f ZDT2, para unos valores determinados
de w1 y w2. Por lo tanto, si el frente de Pareto que se obtiene es cercano al
óptimo, este representará las soluciones del problema convertido a monobjetivo
para todos los valores de w1 y w2.
Figura 4.4. Frente de Pareto para la función de optimización ZDT1.
No todos los frentes de Pareto serán como el mostrado en la Figura 4.4, ya que
podemos encontrar de varios tipos. La Figura 4.5 muestra tres tipos de frente que
podemos encontrarnos. El frente para el problema ZDT1 es tipo cóncavo,
mientras que en el caso ZDT2 es de tipo convexo. Por último, el problema ZDT3
presenta un frente de Pareto discontinuo. En los problemas de optimización
planteados en este libro podremos encontrar los tres tipos de frentes de Pareto
mostrados en la Figura 4.5.
Figura 4.5. Tipos de frente de Pareto.
Antes de continuar, debemos destacar que en el caso del ejemplo que hemos
visto, con dos objetivos, el frente de Pareto es una curva. Si tenemos tres
objetivos, el frente de Pareto será una superficie 3D (un plano), y así
sucesivamente conforme vayan aumentando el número de objetivos. Por lo tanto,
para más de tres objetivos no podremos representar gráficamente el frente de
Pareto. En estos casos, lo que se suele hacer es presentar las proyecciones de
frente de Pareto dos a dos.
En la siguiente sección estudiaremos el algoritmo NSGA-II que, sin lugar a
dudas, es uno de los más utilizados en la actualidad para obtener el frente de
Pareto en problemas de optimización con múltiples objetivos.
4.3 Selección del algoritmo genético
Para este problema utilizaremos el algoritmo Non-Sorted Genetic Algorithm II
(NSGA-II), sin lugar a dudas uno de los algoritmos genéticos multiobjetivo más
utilizados en la actualizad (Deb et al., 2002). Las principales características del
algoritmo genético NSGA-II se pueden resumir en dos puntos:
■En primer lugar, establece un mecanismo rápido para comparar soluciones.
Hemos visto en la sección anterior que la comparación de soluciones es muy
importante para obtener el frente de Pareto.
■En segundo lugar, establece una medida de distancia entre las soluciones. La
idea es que las soluciones del Pareto estén bien distribuidas a lo largo de todo el
frente de Pareto y, no acumular muchas soluciones en algunas zonas del frente.
El mecanismo para comparar soluciones es extremadamente relevante para
obtener el frente de Pareto de manera rápida y eficiente. Imaginemos que
debemos comparar todos los individuos de una población de N individuos,
comparando uno por uno con el resto de individuos de la población. Este
procedimiento requeriría N2 operaciones. Si consideramos que tenemos M
objetivos, el número de operaciones necesarios serían M × N2. Utilizando la
notación Big-O, se representa que el algoritmo de comparación tiene una
complejidad O(M × N2). Lo que estamos diciendo básicamente es que: i) el
número de operaciones aumenta de manera cuadrática con el número de
individuos de la población, y ii) que el número de comparaciones aumenta de
manera lineal con el número de objetivos.
Los algoritmos multiobjetivo tradicionales se basan en realizar un ranking de
dominancia entre las soluciones o individuos de la población. Es decir, se
ordenan los individuos de la población en función de su dominancia. En el
primer nivel están las soluciones dominantes o, lo que es lo mismo, no
dominadas por ninguna otra. En el segundo nivel estarían las soluciones que solo
están dominadas por una solución, y así sucesivamente se pueden definir
distintos niveles de dominancia. Este procedimiento utilizando un método de
comparación O(N2) puede alcanzar una complejidad de O(M × N3), ya que en el
peor de los casos tendremos tantos niveles como individuos en la población. El
algoritmo NSGA-II propone un método para obtener el ranking de dominancia
de manera eficiente. Así, para cada individuo p se obtienen dos parámetros: i) en
primer lugar, se obtiene el número de individuos que dominan a la solución p,
denominado np, y ii) se obtiene el conjunto Sp de soluciones que están
dominadas por p. Este procedimiento tiene una complejidad O(M × N2). En el
primer nivel, los individuos tendrán np = 0, y así sucesivamente. Los valores de
np y los elementos Sp se obtienen mediante un doble bucle; es por ello que
hacen falta M × N2 operaciones de comparación. Para más detalles sobre el
procedimiento, se recomienda consultar la publicación original (Deb et al.,
2002).
En cuanto a la medida de distancia entre individuos, la idea es tener un
mecanismo de selección de individuos dentro de un nivel de dominancia. En
general, siempre vamos a preferir soluciones que estén en los primeros niveles
de dominancia, a ser posible en el primer nivel, ya que son soluciones que no
están dominadas (forman parte del frente de Pareto). Sin embargo, a lo largo de
las generaciones del algoritmo genético, necesitamos un procedimiento de
comparación y selección para aquellos individuos que estén en el mismo nivel de
dominancia. El algoritmo NSGA-II define un medida de distancia basada en la
densidad de soluciones en la vecindad de una solución. Para ilustrar el concepto
de distancia, vamos a volver a utilizar nuestro ejemplo de la Figura 4.3. La
Figura 4.6 muestra cómo se mide la distancia de una solución en un nivel de
dominancia. En este caso, estamos considerando el frente de Pareto utilizado
anteriormente; por lo tanto, estamos en el primer nivel de dominancia. En este
caso sería el nivel 0, ya que estamos midiendo distancia entre soluciones del
frente de Pareto. La métrica de distancia de un punto, por ejemplo el S1 en la
Figura 4.6, se mide como la distancia entre las dos soluciones vecinas, en este
caso S2 y S3. En nuestro ejemplo, como tenemos dos objetivos, tenemos una
distancia Euclídea o norma 2, pero el procedimiento es el mismo para más
dimensiones. Para los extremos del frente de Pareto la distancia es infinita. El
cálculo de la distancia se debe realizar para todos los individuos de la población
del algoritmo genético. La idea principal detrás de la métrica de distancia es la
siguiente: si un individuo tiene una distancia pequeña significa que está en una
zona de alta densidad de soluciones. En general, cuanto mayor sea la distancia,
menos poblada será esa zona y más interesante será mantener dicha solución en
la población.
Figura 4.6. Métrica de distancia de una solución basada en densidad.
En las siguientes secciones de este capítulo, se describirán dos problema de
optimización con múltiples objetivos. En primer lugar, abordaremos un
problema clásico como es el problema de la suma de subconjuntos y, en segundo
lugar, trataremos un problema de benchmark. De esta forma abordaremos de
nuevo un problema con variables discretas y otro con variables continuas.
4.4 El problema de la suma de subconjuntos con múltiples objetivos
Como primer caso de problema de optimización con múltiples objetivos, vamos
a resolver el problema de la suma de subcojuntos con dos objetivos.
4.4.1 Formulación del problema
El problema de la suma de subconjuntos con dos objetivos se define de la
siguiente forma:
Como ocurrió con el problema del viajero, aunque la formulación del problema
es simple, se puede demostrar que este problema es de tipo NP-completo; por lo
tanto, no existe en la actualidad un algoritmo determinista que resuelva el
problema en tiempo polinomial. Si quisiéramos resolver el problema probando
todas las posibles soluciones, tendríamos que evaluar 2n combinaciones, siendo
n el número de elementos en el conjunto original. En consecuencia, se necesita
un tiempo exponencial que se hace inabordable para valores de n altos.
En nuestro caso, el conjunto original va a venir dado por una lista de valores
enteros Si comprendidos en un rango de valores. El valor c será un entero
cualquiera comprendido entre cero y la suma de todos los elementos del S, ya
que si el valor de c fuera mayor sería imposible encontrar una solución al
problema. Veremos que tanto el tamaño del conjunto como el rango de valores
será configurable. También será configurable el valor de c, siempre que
cumplamos con la limitación expuesta anteriormente.
En nuestro caso el problema lo vamos a considerar como multiobjetivo, ya que
vamos a considerar los siguientes objetivos:
■Objetivo 1 - N o Elementos: Minimizar el número de elementos que componen
el subconjunto, de manera que aquellos subconjuntos con menos elementos
serán mejores.
■Objetivo 2 - Diferencia: Minimizar la diferencia con respecto al valor de c .
Cuanto menor sea la diferencia, mejor será la solución. En el caso de que la
suma sobrepase el valor de c , se aplicará la pena de muerte para penalizar las
soluciones.
En cuanto a la representación de los individuos, cada solución se representará
como una lista de ceros y unos, de manera que las posiciones que contengan un
uno serán elementos que se consideren en el subconjunto. Es decir, vamos
obtener los xi de la formulación del problema. Hay que destacar que en este caso
estamos trabajando con cromosomas binarios.
4.4.2 Definición del problema y generación de la población inicial
Como en ejemplos anteriores, lo primero que debemos hacer es crear el tipo de
problema y realizar funciones para crear individuos aleatorios. El siguiente script
incluye las líneas de código necesarias. En este capítulo, vamos a centrarnos con
más detalle en aquellos puntos que son nuevos para problemas con múltiples
objetivos.
Se puede ver que, en este caso, antes de definir el problema tenemos que definir
algunas variables relativas al conjunto S:
■LIMITE_INF : Límite inferior del intervalo de los valores del conjunto S .
■LIMITE_SUP : Límite superior del intervalo de los valores del conjunto S .
■TAM_CONJUNTO : Número de elementos del conjunto S .
■SUMA_OBJETIVO : Valor objetivo de la suma c .
■CONJUNTO : Conjunto de valores S .
Es importante indicar que, en este caso, la semilla del problema se ajusta antes
para obtener siempre el mismo conjunto de valores. El Resultado 4.1 muestra los
valores del conjunto S con el que trabajaremos en este capítulo.
Resultado 4.1. Elementos del conjunto de datos del problema de la suma de
subconjuntos.
Si realizamos la suma con la función nativa de Python sum(), o mediante la
función de la librería de numpy np.sum(), podemos ver que la suma total es de
435. Por lo tanto, no debemos elegir un SUMA_OBJETIVO inferior a 435. Se
puede observar, en el script anterior, que se fijará el valor de 333. Hay que
indicar que el conjunto se ha definido como un objeto de tipo ndarray de numpy
para poder filtrar de manera eficiente las posiciones del individuo que contengan
un uno, como veremos más adelante.
4.4.3 Definición del problema y plantilla del individuo
Es muy importante observar que, en este caso, cuando estamos creando el
problema, el atributo weights tiene una tupla con dos valores (–1,–1):
Esa tupla significa que estamos definiendo un problema con dos objetivos y que
queremos minimizar ambos objetivos. Con respecto a la definición de la plantilla
del individuo, procedemos exactamente igual que en problemas anteriores. Así,
el individuo se representará como una lista de valores binarios de longitud
TAM_CONJUNTO. Si en la posición i del individuo se encuentra un 1, el
elemento CONJUNTO[i] se tendrá en cuenta en el subconjunto. Insistiremos
más adelante en esto cuando hablemos de la función objetivo. La Figura 4.7
ilustra la representación de la cadena cromosómica en este problema.
Figura 4.7. Representación del individuo para el problema de suma de
subconjuntos.
A continuación, debemos definir una función que nos permita generar individuos
aleatorios. En este caso necesitamos cadenas binarias de tamaño
TAM_CONJUNTO. El siguiente script muestra una posible implementación. El
parámetro de entrada size de la función crea_individuo será la variable
TAM_CONJUNTO.
De esta forma, la función es escalable a problemas de cualquier tamaño. Se ha
utilizado una list comprenhension junto a la función random.randint(0,1) para
generar una lista de 0s y 1s. Cabe recordar que cada 1 representa la inclusión del
elemento correspondiente en el subconjunto. Para registrar dicha función en la
caja de herramienta procedemos de la siguiente forma:
Por último, las siguientes líneas registran las funciones para crear individuos
aleatorios y la población inicial:
Como la función crea_individuo permite crear cromosomas aleatorios, volvemos
a utilizar tools.initIterate1. Un aspecto diferenciador con respecto a los capítulos
anteriores (Capítulo 1 y Capítulo 2) es que en este caso no hemos definido el
tamaño de la población en este momento en la función population. El tamaño de
la población lo pasaremos más adelante en el main cuando definamos la
población inicial.
4.4.4 Función objetivo y operadores genéticos
A continuación, vamos a centrarnos en la función objetivo que vamos a utilizar
en nuestro problema y en los operadores genéticos. El siguiente script muestra
las líneas de código necesarias que se detallarán a continuación.
Función objetivo
La función objetivo, denominada funcion_objetivo, recibe dos parámetros de
entrada. El primer parámetro es un individuo a evaluar, y el segundo parámetro
es la suma objetivo que se quiere satisfacer. El primer paso de la función es
realizar un filtrado del array CONJUNTO, utilizando para ello aquellas
posiciones del individuo que contienen un uno.
Como resultado, el objeto subconjunto contendrá aquellos elementos
seleccionados para formar el subconjunto. Para obtener el sumatorio de los
elementos seleccionados, se utiliza la función de numpy np.sum, y se almacena
en el objeto suma_conjunto.
El siguiente paso es calcular la diferencia entre la suma objetivo y la suma de los
elementos que forman el subconjunto; el resultado se almacena en el objeto
diferencia. Este es el primer objetivo del problema que queremos minimizar.
En este punto ya podemos calcular el número de elementos que contiene el
subconjunto. Utilizamos la función nativa sum2 para sumar el número de 1s en
el individuo. Este es el segundo objetivo del problema que queremos minimizar.
A continuación, debemos comprobar dos condiciones3:
■Lo primero que debemos comprobar es que la suma de los elementos del
subcobjunto no sobrepasen el valor objetivo.
■En segundo lugar, debemos comprobar que el número de elementos no sea 0 ya
que, entonces, no tendríamos ningún subconjunto.
En el caso de que alguna de las condiciones ocurra, se debe penalizar la
solución. Hemos optado por utilizar la pena de muerte, tal y como se explicó en
el Capítulo 1. Para ello, lo que hacemos es devolver un valor muy alto para
ambos objetivos, ya que el objetivo es minimizar ambos. Se ha elegido un valor
de 10,000, que es mucho mayor que la mayor diferencia que podemos encontrar
entre un subconjunto de valores y el valor objetivo. Por lo tanto, cualquier
solución que no cumpla alguna de las condiciones será invalidada para participar
en la operaciones genéticas. Una cosa importante que debemos destacar es que
estamos devolviendo dos resultados.
En los problemas con múltiples objetivos, la función objetivo deberá devolver tantos objetivo
Operadores genéticos
Una vez explicada la función objetivo, podemos pasar a los operadores
genéticos. Para el cruce utilizaremos el operador cxTwoPoint, que es similar al
cruce de un punto utilizado en el Capítulo 1, el cual debemos registrarlo en la
caja de herramientas.
En cuanto a la mutación, usaremos el operador mutBitFlip. Se ha seleccionado
una probabilidad de 0.05 (indpb) para conmutar cada uno de los genes del
individuo.
De nuevo, es importante recordar que estas operaciones son transparentes para
nosotros, ya que los operadores serán utilizados internamente por el algoritmo
genético utilizado como caja negra.
El siguiente paso es registrar el algoritmo NSGA-II, que se debe registrar como
mecanismo de selección, ya que el NSGA-II realmente define un procedimiento
de selección para generar los individuos de las siguientes generaciones.
Por último, debemos registrar la función objetivo. En el registro indicamos el
valor del parámetro suma_objetivo.
4.4.5 Últimos pasos: Ejecución del algoritmo multiobjetivo
El siguiente script muestra el código necesario para ejecutar el algoritmo
genético multiobjetivo, visualizar el frente de Pareto y almacenar los resultados
en dos archivos de texto.
A continuación, vamos a centrarnos en las partes de código que son nuevas con
respecto a capítulos anteriores.
4.4.6 Configuración del algoritmo genético multiobjetivo
Comenzaremos describiendo el contenido de la función main. En primer lugar,
configuramos los hiperparámetros de los operadores genéticos:
En este caso se ha elegido una probabilidad de cruce de 0.7, una probabilidad de
mutación de 0.3 y un número de generaciones de 300. Se mostrará más adelante
que con estos valores se pueden conseguir resultados satisfactorios. A
continuación, asignamos valores a los parámetros µ y λ del algoritmo. Estos
parámetros definirán el tamaño de la población inicial y de la población
extendida. Como se explicó en el Capítulo 2, µ y λ se eligen con el mismo valor
para que la población siempre tenga el mismo tamaño a lo largo de todas las
generaciones del algoritmo genético. Es importante indicar que en este paso se
está definiendo el tamaño de la población inicial al crearla. Esto es nuevo con
respecto a capítulos anteriores. Por lo tanto, ambas formas son completamente
válidas: i) podemos indicar el tamaño de la población al registrar la función
population o ii) podemos indicar el tamaño de la población cuando se ejecuta el
método toolbox.population. En ambos casos lo que estamos haciendo es asignar
el valor del parámetro n de la función initIterate4.
Otro aspecto que es nuevo para los problemas multiobjetivo es el objeto de tipo
ParetoFront5.
El objeto pareto almacenará todas las soluciones que forman el frente de Pareto.
Internamente el algoritmo en cada generación actualizará (mediante el método
update) los individuos que forman el frente de Pareto; ese proceso es trasparente
para nosotros.
A continuación, podemos lanzar el algoritmo genético µ + λ.
La principal diferencia con respecto a capítulos anteriores es que como objeto
hof se pasa el objeto pareto que se ha creado anteriormente. Al finalizar el
algoritmo, ese objeto contendrá el frente de Pareto definitivo. Por último, es
importante indicar que la función main ahora devuelve tres valores: la población
final pop, el registro de evolución logbook y el frente de Pareto pareto.
En cuanto a los resultados, en este caso se han vuelto a utilizar dos archivos para
almacenar los resultados. El archivo individuosconjuntos.txt contendrá los
individuos que forman el Pareto, y el archivo fitnessconjuntos.txt incluye el
fitness· de los elementos del frente de Pareto. Se utiliza un bucle para guardar el
frente de Pareto en los archivos .txt.
Si echamos un vistazo a los resultados del almacenados en los archivos,
podemos ver que en realidad hay varias soluciones que alcanzan el valor de
suma objetivo. Podemos ver los resultado almacenados en el Texto 4.2. Tenemos
un total de 21 soluciones en el frente de Pareto, aunque realmente como veremos
más adelante en la Figura 4.8 solo se observan 15, ya que hay seis que se solapan
en la gráfica. Esto era de esperar, ya que existen varias combinaciones de
elementos del conjunto que suman el valor objetivo.
Texto 4.2. Resultados almacenados en el archivo fitnessconjuntos.txt.
Como ejemplo de solución que forma parte de Pareto, se muestra en el Texto 4.3
el último individuo del Texto 4.26. Cabe recordar que un 1 significa que el
elemento correspondiente se incluye en el subconjunto.
Texto 4.3. Resultados almacenados en el archivo fitnessconjuntos.txt.
Representación del frente de Pareto
A continuación, pasamos a representar el frente de Pareto obtenido. Para ello,
utilizamos la función plot_frente, la cual lee el achivo de texto
fitnessconjuntos.txt. Al tener soluciones discretas, el frente de Pareto se ha
representado como un diagrama de dispersión utilizando la función scatter7.
La Figura 4.8 muestra el frente de Pareto obtenido. Se puede ver cómo a medida
que el número de elementos aumenta la diferencia con respecto a la suma
objetivo disminuye, y viceversa.
Figura 4.8. Frente de Pareto para el problema suma de subconjunto.
La Figura 4.9 muestra el frente de Pareto con información adicional. En la
gráfica de la izquierda se muestra la suma obtenida por cada conjunto incluido
en el frente de Pareto. Se puede observar como la suma se va incrementando
conforme más elementos se añaden en el subconjunto. Finalmente, en el extremo
inferior derecho, se puede observar que la suma alcanza el valor deseado. En
cuanto a la gráfica de la derecha, el frente de Pareto incluye los elementos
incluidos en cada subconjunto. Se puede observar que hay varios subconjuntos
que alcanzan el valor deseado.
La función plot_frente necesaria para representar la información extra de la
Figura 4.9 se muestra en el siguiente script.
4.4.7 Algunos apuntes sobre los algoritmos genéticos con múltiples objetivos
Antes de terminar este capítulo, deberíamos realizar algunos apuntes con
respecto a la configuración de los algoritmos genéticos multiobjetivo:
■En relación con las probabilidades de los operadores genéticos. En el caso de
los algoritmos genéticos con un solo objetivo, se indicó que es conveniente
realizar un barrido de las probabilidades de cruce y mutación. En el caso de los
algoritmos multiobjetivo, también se puede realizar dicho barrido; no obstante,
no es tan importante como en el caso de los monobjetivo. Se recomienda probar
con probabilidades estándar, tales como probabilidad de cruce de 0.7 y
probabilidad de mutación de 0.3. Si vemos que el frente de Pareto no incluye
suficientes elementos, podemos probar con otras configuraciones.
■En principio, no hemos limitado el tamaño del frente de Pareto. Eso puede
hacer que en algunos casos el frente de Pareto aumente mucho. El objeto Pareto
permite que se defina una función de distancia, mediante el atributo similar ,
para limitar los individuos que se incluyen en el frente 8. El aumento del frente
Pareto tiene también el problema añadido de que cada iteración del algoritmo
tarda más, debido a que el algoritmo debe comparar cada nueva solución con
todos los elementos de frente de Pareto.
■Un aspecto que no se ha tratado es la convergencia del algoritmo. En capítulos
anteriores la convergencia de los algoritmos genéticos se comprobaba mediante
la visualización de la evolución de la población. Cuando no se observaba cierta
mejora en un número consecutivo de generaciones, se consideraba que el
algoritmo había convergido. Sin embargo, en los problemas con múltiples
objetivos no podemos utilizar dicho procedimiento, ya que el objetivo del
conjunto de la población no es evolucionar hacia un mismo objetivo, sino
encontrar soluciones no dominadas. Es por ello que en los problemas con
múltiples objetivos se deben utilizar otro tipo de procedimientos. La forma más
simple es ver que el frente de Pareto tiene suficientes soluciones. Es decir, que el
frente de Pareto incluye un número de soluciones significativo en número y en
cuanto al rango de valores de los objetivos.
■Aunque el número de objetivos no está limitado en principio, hay que tener en
cuenta que con problemas con más de tres objetivos la representación gráfica del
frente de Pareto no es posible. Además, se ha demostrado que para números
elevados de objetivos el frente de Pareto crece muchísimo, ya que es
prácticamente imposible encontrar soluciones dominadas. Los problemas de
optimización con muchos objetivos ( many objectives optimization problems )
son un campo de investigación activo del que se puede encontrar amplia
literatura (Ishibuchi et al., 2008) (Ishibuchi et al., 2014) (Li et al., 2015a).
Figura 4.9. Frente de Pareto con información sobre la suma obtenida por cada
solución y el conjunto resultante.
4.4.8 Código completo
Pasamos a describir brevemente el código completo utilizado en el problema de
la suma de subconjuntos:
■Las líneas 1-7 importan todas las librerías necesarias.
■La semilla de números aleatorios se fija en la línea 9 para obtener el mismo
conjunto de valores.
■En las líneas 12-26 definimos los límites de los valores del conjunto (línea 12),
el tamaño del conjunto (línea 14), la suma objetivo (línea 15), y creamos el
conjunto con valores aleatorios (línea 16). El conjunto se crea como un array de
numpy para facilitar el filtrado.
■En las líneas 20-21 se realiza la creación del problema multiobjetivo (línea 20),
con dos objetivos de minimización. La plantilla del individuo será una lista
(línea 21).
■En la línea 23 se define la caja de herramientas.
■Las líneas 26-27 definen una función para generar individuos aleatorios. Dicha
función recibe como entrada el tamaño del conjunto de valores. Se devuelve una
cadena cromosómica de unos y ceros.
■En las líneas 30-33 se realiza el registro de la función para generar muestras de
los cromosomas de los individuos (línea 30), la función para generar individuos
aleatorios (línea 31), y la función para generar la población inicial (línea 33).
■La función multiobjetivo se define en las líneas 37-54. Hay que tener en cuenta
que tenemos que devolver dos objetivos. Si no se cumplen los objetivos se
penaliza con pena de muerte (líneas 51 y 53). En cuanto a lo que devuelve la
función, en primer lugar se devuelve el número de elementos y, en segundo
lugar, la diferencia con respecto a la suma objetivo (línea 54).
■Los operadores genéticos se registran en las líneas 57-60. El operador de cruce
es un cruce de dos puntos (línea 57), y para la mutación se utiliza el método
mutFlipBit (línea 58). El proceso de selección es el algoritmo NSGA-II (línea
59). Por último, se registra la función de fitness (línea 60).
■Las líneas 62-75 definen la función plot_frente , la cual lee el frente de Pareto
del archivo fitnessconjuntos.txt (línea 66) y representa el frente de Pareto como
un diagrama de dispersión (líneas 67-75).
■La función main se define en las líneas 77-93. En primer lugar, se definen los
hiperparámetros de los operadores genéticos, así como el número de
generaciones del algoritmo (línea 78). A continuación, en la línea 79, se definen
los parámetros µ y λ del algoritmo µ + λ . Con los valores elegidos, el tamaño de
la población permanece constante a lo largo de las generaciones. La población
inicial se genera en la línea 80. La línea 81 crea el objeto para calcular las
estadísticas y, a continuación, en las líneas 82-85 se registran las funciones
estadísticas. La línea 86 define el registro de evolución, aunque en este ejemplo
no se ha utilizado. Es importante no olvidar que, en este caso, el objeto hof tiene
que ser de tipo ParetoFront . Este objeto se define en la línea 87. La línea 88
lanza el algoritmo eaMupPlusLambda del módulo algorithms . El resultado es la
población final pop y el registro de evolución logbook . Para terminar, la línea 93
devuelve la población final, el frente de Pareto y el registro de evolución.
■La líneas 97 y 98 crean archivos de texto para almacenar los resultados. El
archivo individuosconjuntos.txt almacenará los individuos del frente de Pareto.
Por otro lado, el archivo fitnessconjuntos.txt incluirá el fitness de los individuos
del frente.
■El bucle de las líneas 99-105 recorre los elementos de objeto pareto y almacena
los resultados en los archivos de textos creados.
■Las líneas 106 y 107 cierran los archivos para que los cambios se guarden.
■Por último, la línea 108 llama la función plot_frente para representar
gráficamente el frente de Pareto obtenido.
Código 4.4. Código completo para resolver el problema de la suma de
subconjuntos.
Antes de continuar, merece la pena comentar otros operadores genéticos que se
podrían haber utilizado en el problema. En cuanto a la operación de cruce, se
podría haber utilizado el cruce de un punto. Sin embargo, debido al tamaño de
los cromosomas, parece más lógico utilizar el cruce de dos puntos. En cuanto a
la mutación, también se podría haber optado por el método mutShuffleIndexes,
el cual intercambia la información genética de dos genes. No obstante, si los
genes seleccionados para el intercambio contienen la misma información, el
individuo resultante sería el mismo. Así, el operador utilizado parece el más
adecuado de los que tenemos disponibles en deap para cromosomas binarios.
4.5 Funciones de benchmark con múltiples objetivos
En la Figura 4.4 se mostró el frente de Pareto para la función ZDT1; en esta
sección vamos a describir el código necesario para obtener dicho frente de
Pareto. Dicho código nos servirá también para obtener el frente de Pareto de
otras funciones de benchmark, como ZDT2 y ZDT3.
4.5.1 Definición del problema y población inicial
En primer lugar, se definen algunos parámetros del problema ZDT1,
BOUND_LOW, BOUND_UP y NDIM. Los dos primeros son los límites inferior
y superior de las variables. Por otro lado, NDIM representa el número de
variables del problema. Los valores de estas variables se pueden ajustar para
otras funciones de benchmark9. En este caso vamos a resolver el problemas para
diez variables.
A continuación, seguimos por la definición del problema multiobjetivo y la
plantilla de los individuos.
En este caso, como plantilla se utiliza la clase array10. Los objetos de esta clase
son parecidos a los arrays de numpy. Sin embargo, su comportamiento con
respecto a la operación de slicing es diferente (ver Apéndice A). Los array tienen
el atributo typecode, que define el tipo de elementos que almacena. En este
problema, el tipo es double. Por lo tanto, estamos definiendo un array de
flotantes de tamaño doble.
Para definir los individuos que forman la población inicial, se ha definido la
función crea_individuo, que recibe como parámetros de entrada los límites
inferior (low) y superior (up) de las variables y el tamaño de los individuos
(size). Esta función debemos registrarla como siempre en el toolbox.
Debido a que la función crea_individuo, registrada en el toolbox como attr_float,
genera el individuo completo, se ha utilizado la función tools.initIterate para
generar los individuos aleatorios.
Para crear la población, se opera como siempre utilizando la función
tools.initRepeat.
En problemas donde las variables tengan unos rangos distintos, la función
crea_individuo se debe adaptar a esas condiciones. El siguiente script muestra
una posibilidad para generar individuos con variables con distintos rangos. Se
puede observar que, en este caso, se ha dado por hecho que low y up son de tipo
secuencia. Los valores de dichas secuencias podrían ser diferentes, para asignar
un rango de valores distinto a cada variable del problema.
4.5.2 Función objetivo y operadores genéticos
En esta ocasión, vamos a comenzar con el registro de los operadores genéticos,
ya que la función objetivo está definida en el módulo benchmarks, por lo que no
tenemos que definir nada. El cruce utilizado es el cxSimulatedBinaryBounded.
Este mecanismo de cruce, como se explicó anteriormente, se basa en una
operación matemática entre los genes de dos individuos, que obtiene un
resultado parecido al cruce de un punto para cromosomas con variables binarias.
El parámetro η determina cuánto se parecen los descendientes a los progenitores.
En este caso se utiliza la variante Bounded, para limitar los valores que pueden
tomar las variables. Hay que tener en cuenta que aunque en las operaciones de
cruce en general realizan un intercambio de genes, el operador
tools.cxSimulatedBinaryBounded realiza una operación matemática entre genes
que puede dar lugar a nuevos que se salgan de los límites del espacio de
búsqueda. Eso ocurre especialmente para valores de η pequeños. Es por ello que
en este caso se limitan los valores máximos y mínimos de los resultados de
cruce.
En cuanto a la operación de mutación, se utiliza el operador
tools.mutPolynomialBounded.
En cuanto a la selección, se utiliza el algoritmo NSGA-II, basado en la Pareto
dominancia y la métrica de distancia basada en densidad, para seleccionar los
individuos que pasan a la siguiente generación.
Finalmente, al igual que vimos con las funciones de benchmark con un solo
objetivo, podemos registrar la función ZDT1 como benchmark.zdt1.
Es importante destacar que el código mostrado también funcionaría con las
funciones de benchmark ZDT2 y ZDT3; simplemente, debemos cambiar
benchmark.zdt1 por benchmark.zdt2 o benchmark.zdt3.
4.5.3 Ejecución del algoritmo multiobjetivo
El resto del código que nos falta para poder ejecutar el algoritmo se muestra en
el siguiente script. Se puede observar que no hay notables diferencias con
respecto al ejemplo anterior del problema de la suma de subconjuntos.
Se ha elegido una población de 100 individuos (μ=100); teniendo en cuenta que
tenemos 10 variables, es un tamaño adecuado. Si aumentamos el valor de
NDIM, debemos aumentar también los valores de i μ y λ. En este caso, los
archivos para guardar los resultados se denominan individuosmulti.txt y
fitnessmulti.txt. El Texto 4.5 muestra los tres primeros individuos del frente de
Pareto almacenado en fitnessmulti.txt11. Hay que indicar que el algoritmo
encuentra 1948 soluciones que forman el Pareto. Es decir, soluciones que no
están dominadas. Como se verá a continuación, es un número significativo de
soluciones.
Texto 4.5. Resultados almacenados en el archivo fitnessmulti.txt.
Con respecto a los individuos, el Texto 4.6 muestra los cromosomas de las tres
soluciones mencionadas anteriormente. Cabe indicar que esas tres soluciones
corresponden a valores bajos del primer objetivo ZDT11 y valores altos del
segundo objetivo ZDT12. Para las soluciones almacenadas al final del archivo
fitnessmulti.txt ocurrirá al contrario. Esto es así porque los objetivos ZDT11 y
ZDT12 son opuestos.
Texto 4.6. Resultados almacenados en el archivo individuosmulti.txt.
4.5.4 Representación del frente de Pareto
La Figura 4.10 muestra el frente de Pareto obtenido por el algoritmo genético
basado en NSGA-II. Podemos ver que los resultados coinciden prácticamente
con el Pareto óptimo. De hecho, es difícil distinguir uno del otro12.
Figura 4.10. Comparación del frente de Pareto óptimo y obtenido por el
algoritmo NSGA – II.
Para representar dicha figura se ha utilizado de nuevo la función plot_frente, con
algunas modificaciones que se muestran a continuación:
La librería deap tiene disponible el frente de Pareto óptimo para la función
ZDT1. Dichos valores óptimos se encuentran almacenados en un archivo JSON
denominado zdt1_front.json. En el código anterior, en primer lugar, se lee el
archivo fitnessmulti.txt para representar el frente de Pareto obtenido y, a
continuación, se utiliza el archivo JSON para obtener el frente de Pareto óptimo.
Ambos frentes de Pareto se representan con diagramas de dispersión.
4.5.5 Ajuste de los hiperpámetros de los operadores genéticos
Para ilustrar mejor el funcionamiento de la técnica de mutación utilizada en este
ejemplo mutPolynomialBounded, la Figura 4.11 representa 20 hijos creados con
dicho operador. Se ha considerado un individuo original de solo dos
dimensiones, x1 = 0.5 y x2 = 0.5 para poder visualizar mejor los resultados. Se
puede observar cómo a medida que aumenta el valor eta los hijos se alejan del
progenitor. No siempre se alejan de distinta forma ya que, como se detalló
anteriormente, existe cierta aleatoriedad que define el polinomio de separación
entre el padre y los hijos. Además, hay que tener en cuenta que dicha distancia
también dependerá del parámetro indpb. En este ejercicio se ha utilizado el valor
1/NDIM. Por lo tanto, para NDIM = 2, tenemos indpb = 0.5. En el problema
ZDT1 resuelto anteriormente se utilizó NDIM = 10 y, por lo tanto, indpb = 0.1.
Figura 4.11. Individuos generados mediante mutación polinómica e individuo
original para diferentes valores del parámetro η.
Al igual que se ha comentado anteriormente con otros operadores, sería
interesante modificar este operador en función del número de generaciones. No
obstante, esta funcionalidad no está disponible en deap.
4.5.6 Código completo
El siguiente Código 4.7 muestra el código completo del problema ZDT1
planteado en esta sección. A modo de resumen, se describen brevemente las
líneas de código, indicando cómo se deben modificar para utilizar otras
funciones de benchmark:
■Las líneas 1-10 incluyen las librerías necesarias. Cabe destacar el módulo array
que permite definir vectores de manera nativa en Python .
■Las líneas 12 y 13 definen los límites de las variables ( BOUND_LOW y
BOUND_UP ) y el número de variables del problema NDIM . En este caso las
variables están comprendidas en el intervalo [0 , 1], y tenemos diez variables. En
cada problema de benchmark los límites de las variables pueden ser distintos;
por lo tanto, habrá que adaptarlos en cada caso. En cuanto al número de
variables, normalmente este es ajustable, aumentado la complejidad del
problema con el aumento del número de variables.
■Las líneas 16-18 definen, en primer lugar, el problema multiobjetivo, con dos
objetivos de minimización Weights = (–1 ,– 1) (línea 16), y el tipo de individuo,
que en este caso es un array nativo de Python (líneas 17-18). Para utilizar otras
funciones incluidas en deap , se debe tener en cuenta el número de objetivos y el
tipo (maximización o minimización) 13.
■La función crea_individuo (líneas 21-22) permite crear individuos aleatorios
dentro del rango de las variables. A la función se le deben pasar tres argumentos:
el límite inferior low , el límite superior up y el número de variables del
problema size . En problemas en los que las variables tengan distintos rangos, la
función se debe modificar, ya que los argumentos de entrada low y up serán
secuencias con los límites de cada una de las variables. Anteriormente, se ha
mostrado un ejemplo para este tipo de casos.
■El objeto caja de herramientas toolbox se crea en la línea 24.
■Las líneas 27-32 registran en el objeto toolbox las funciones necesarias para
crear individuos aleatorios y la población inicial.
■Las líneas 35-41 registran los operadores genéticos y la función de evaluación.
Para otras funciones de benchmark , la línea 41 se debe modificar, incluyendo la
función que se desee.
■La función plot_frente se implementa en las líneas 43-59. Esta función permite
representar el frente de Pareto obtenido por el algoritmo.
■La función main , que ejecuta el algoritmo genético, se define en las líneas 6178. Las probabilidades de cruce y mutación se definen en la línea 62, así como el
número de generaciones. El tamaño de la población y los progenitores se definen
con las variables µ y λ en la línea 63. La población inicial se crea en la línea 64.
A continuación, el objeto estadística se define y se configura en las líneas 65-69.
El registro de evolución se crea en la línea 70, aunque en este ejemplo no se
utiliza. La línea 71 crea el objeto que almacenará el frente de Pareto. La línea 72
lanza el algoritmo genético y la línea 78 devuelve la población final, el registro
de evolución y el frente de Pareto.
■La línea 81 ajusta el generador de números aleatorios.
■La línea 82 ejecuta la función main . Las líneas 83 y 84 crean los dos archivos
de texto. El archivo individuosmulti.txt almacenará los individuos que forman el
frente de Pareto. Por otro lado, el archivo fitnessmulti.txt almacenará el frente de
Pareto. Ambos archivos se escriben en el bucle de las líneas 85-91. Finalmente,
los archivos se cierran para guardar los cambios en las líneas 92 y 93.
■Por último, la línea 94 lanza la función plot_frente , que permite visualizar el
frente de Pareto y compararlo con el óptimo.
Código 4.7. Código completo para problemas de benchmark multiobjetivos.
Antes de terminar esta sección, nos parece adecuado mencionar otros operadores
genéticos que se podrían haber utilizado en este problema. En el caso del
operador de cruce, se podría haber utilizado el cruce de un punto o de dos
puntos. Sin embargo, para variables continuas suele funcionar mejor el operador
Simulated Binary Crossover. Otra posibilidad es utilizar el operador de Blend;
este caso se deja a los lectores como ejercicio. En cuanto a la mutación, se
podría haber utilizado la mutación Gaussiana mutGaussian; no obstante, habría
que comprobar que los genes de los individuos generados no se salgan de los
límites de las variables.
4.6 Lecciones aprendidas
En este capítulo las secciones aprendidas comprenden los dos ejemplos que se
han visto, tanto el problema de la suma de subconjunto como las funciones de
benchmark. En cuanto a las lecciones aprendidas, podemos citar las siguientes:
■En esta sección hemos estudiado los problemas con múltiples objetivos. Hemos
visto que la conversión de un problema con múltiples objetivos en un problema
con un solo objetivo mediante la utilización de pesos tiene algunas limitaciones.
■Como alternativa, se ha presentado la dominancia de Pareto y los algoritmos
multiobjetivo basados en dicha dominancia.
■Se ha estudiado en profundidad el algoritmo NSGA-II basado en la dominancia
de Pareto y la métrica de distancia basada en densidad. Para demostrar el
funcionamiento del algoritmo NSGA-II se han resuelto dos problemas. En
primer lugar, un problema clásico como es el problema de la suma de conjuntos
(considerando dos objetivos) y, en segundo lugar, una función de benchmark con
múltiples objetivos.
■Se ha mostrado que en problemas con dos objetivos el frente de Pareto es una
curva. Por otro lado, en problemas con tres objetivos el frente de Pareto es una
superficie. El frente de Pareto en problemas con más de tres objetivos no puede
ser representado de manera gráfica (se pueden representar las proyecciones dos a
dos). Además, cuando el número de objetivos es muy elevado no podemos
utilizar el algoritmo NSGA-II , ya que prácticamente todas las soluciones que se
encuentren no serán dominadas. La librería deap incluye el algoritmo NSGA-III
14, el cual es relativamente reciente, y cuyo estudio se sale fuera del alcance de
este libro (Deb y Jain, 2014).
■No todos los frentes de Pareto tienen la misma forma. Podemos encontrar
frentes de Pareto continuos y discontinuos, así como cóncavos, convexos y no
convexos.
■Los problemas con múltiples objetivos siempre devuelven una tupla, con tantos
elementos como objetivos tenga el problema.
■Hemos aprendido que el tamaño de la población se puede pasar de dos
maneras. Si tenemos muy claro el tamaño que queremos utilizar, lo debemos
incluir cuando se registre la función population en la caja de herramientas. Como
segunda opción, podemos incluir el tamaño de la población en la función main
cuando creemos la población inicial, pasando como parámetro a
toolbox.population(N) el tamaño de la población N .
■Si se aplica la pena de muerte en un problema debido a que una solución no
cumple las restricciones, se deben penalizar todos los objetivos.
■Se ha aprendido a utilizar la mutación polinómica con límite
tools.mutPolynomialBounded . Esta técnica de mutación se utiliza con variables
continuas comprendidas entre dos valores límite. La mutación tiene un
parámetro eta que determina la distancia de los mutantes con respecto a los
individuos originales. Cuanto mayor sea el valor de eta , menor será la distancia,
y viceversa.
■En los problemas con múltiples objetivos no se puede estudiar la convergencia
del algoritmo en función del fitness de los individuos, ya que no existe un solo
criterio. Por lo tanto, la configuración de las probabilidades de cruce y mutación
se debe realizar mediante la visualización del frente de Pareto. Por ejemplo, si en
un problema con objetivos continuos vemos discontinuidades, es una muestra de
que el algoritmo genético no ha explorado/intensificado lo suficiente en el
espacio de búsqueda. En estos casos debemos probar con distintas
probabilidades de cruce y mutación.
4.7 Para seguir aprendiendo
La literatura sobre algoritmos genéticos mulitobjetivo es amplia y variada, hasta
el punto de que hoy en día sigue siendo un tema puntero de investigación. A
continuación, se incluyen algunas referencias para seguir profundizando en esta
dirección:
■Más información sobre la resolución del problema de la suma de conjuntos con
algoritmos genéticos se puede encontrar en (Wang, 2004) y (Li et al., 2015b).
■Como bibliografía básica sobre algoritmos genéticos multiobjetivo se
recomienda (Coello, 2006), (Coello et al., 2007) y (Bechikh et al., 2016).
■Para profundizar sobre problemas con muchos objetivos ( many-objective
genetic algorithms ), se puede consultar (Ishibuchi et al., 2008) y (Ishibuchi et
al., 2014).
Como ejercicios se plantean los siguientes:
■Utilizar el operador de cruce cxUniform 15 en el problema de la suma de
conjuntos y comparar los resultados con el operador de cruce de uno y dos
puntos. Realizar un barrido de probabilidades de cruce y mutación para hacer
una comparación justa entre los distintos operadores.
■Cambiar el tamaño del conjunto de datos y la suma objetivo. Comprobar que se
consiguen resultados satisfactorios para varios valores.
■Utilizar el Código 4.7 para optimizar otras funciones de benchmark , por
ejemplo las funciones ZDT2 y ZDT3 .
_________________
1El Capítulo 2 explica las diferencias entre tools.initRepeat y tools.initIterate.
2https://docs.python.org/3.6/library/functions.html#sum
3El orden no tiene por qué ser ese.
4Se recomienda ir al Capítulo 2 si no se tiene claro dicho parámetro.
5https://deap.readthedocs.io/en/master/api/tools.html#deap.tools.ParetoFront
6Para su correcta visualización se ha dividido en dos líneas de texto. En el
archivo original aparece en una sola línea.
7https://matplotlib.org/3.2.1/api/_as_gen/matplotlib.pyplot.scatter.html
8https://deap.readthedocs.io/en/master/api/tools.html#deap.tools.ParetoFront
9https://deap.readthedocs.io/en/master/api/benchmarks.html
10https://docs.python.org/3/library/array.html
11No se muestra todo el archivo ya que es bastante grande.
12Se han utilizado distintos tamaños en los marcadores para poder
diferenciarlos.
13https://deap.readthedocs.io/en/master/api/benchmarks.html
14https://deap.readthedocs.io/en/master/api/tools.html#deap.tools.selNSGA3
15https://deap.readthedocs.io/en/master/api/tools.html#deap.tools.cxUniform
II Parte 2: Algoritmos genéticos para ingeniería
5Funcionamiento óptimo de una microrred
5.1 Introducción
5.2 Formulación del problema
5.3 Problema con un objetivo: Minimizar el coste de operación
5.4 Problema con múltiples objetivos: Minimizando el coste de operación y el
ciclado de la batería
5.5 Código completo y lecciones aprendidas
5.6 Para seguir aprendiendo
6Diseño de planta microhidráulica
6.1 Introducción
6.2 Formulación del problema
6.3 Problema con un objetivo: Minimizando el coste de instalación
6.4 Problema con múltiples objetivos: Minimizando el coste de instalación y
maximizando la potencia generada
6.5 Código completo y lecciones aprendidas
6.6 Para seguir aprendiendo
7Posicionamiento de sensores
7.1 Introducción
7.2 Formulación del problema
7.3 Problema con un objetivo: Maximizando el número de puntos cubiertos
7.4 Problema con múltiples objetivos: maximizando el número de puntos
cubiertos y la redundancia
7.5 Código completo y lecciones aprendidas
7.6 Para seguir aprendiendo
5.1 Introducción
Un sistema eléctrico de potencia se define como una red formada por varios
componentes que permiten generar, transportar, almacenar y usar la energía
eléctrica. Los sistemas eléctricos tradicionales tienen una arquitectura
centralizada, cuyos componentes pueden ser principalmente clasificados como:
■Sistemas de generación: Aportan energía eléctrica al sistema.
■Sistemas de transmisión: Permiten el transporte de esta energía a través de la
red eléctrica hacia los centros de consumo.
■Sistemas de distribución: Se encargan de alimentar a las cargas del sistema.
El auge de las energías renovables y su integración en la red eléctrica está
provocando una evolución de los sistemas eléctricos que, gradualmente, van
transformando su estructura tradicional hacia una arquitectura distribuida. En
esta nueva arquitectura, los generadores renovables (habitualmente de menor
potencia que las máquinas convencionales) se encuentran en cualquier punto de
la red (incluso en el sistema de distribución), disminuyendo así la distancia entre
los puntos de generación y consumo y, con ello, disminuyendo las pérdidas del
sistema y aumentando el rendimiento del mismo.
Así, es posible llevar a cabo una partición del sistema eléctrico que facilite su
análisis y operación. Cada una de estas particiones es lo que denominamos
microrred. Una microrred no es más que un sistema de generación eléctrica
bidireccional, que permite la distribución de electricidad desde los proveedores
hasta los consumidores utilizando tecnología digital y favoreciendo la
integración de las fuentes de generación de origen renovable. Las microrredes
tienen el objetivo fundamental de ahorrar energía, reducir costes e incrementar la
fiabilidad del sistema. Es importante destacar que estas microrredes pueden ser
operadas tanto de forma aislada (utilizando la energía generada localmente para
alimentar las cargas del sistema) como conectadas a una red de mayor potencia
(en este caso es posible cubrir excesos de carga o generación mediante la energía
proveniente de una red de mayor potencia a la que la microrred se encuentra
conectada).
Un aspecto fundamental en el estudio de sistemas eléctricos es que toda la
potencia generada debe ser consumida de forma instantánea por las cargas en la
red o almacenada en sistemas de almacenamiento. La existencia de un desfase
entre la potencia generada y la consumida/almacenada puede provocar
problemas transitorios en la red que comprometan tanto su estabilidad como la
continuidad y calidad del suministro eléctrico.
Figura 5.1. Microrred considerada.
Este capítulo aborda el problema de operación óptima de una microrred
mediante el uso de algoritmos evolutivos, como veremos más adelante. El
problema en cuestión se suele presentar en la literatura bajo el nombre de
«despacho económico». El despacho económico consiste en determinar a corto
plazo la producción óptima de varias instalaciones de generación de electricidad,
de manera que se satisfaga la demanda del sistema optimizando una función
objetivo y cumpliendo las restricciones de operación. En este capítulo trataremos
la optimización desde el punto de vista de un único objetivo, minimizando el
coste de producción necesario para abastecer la demanda eléctrica, y desde un
enfoque multiobjetivo en el que, además, se tendrá en cuenta la posible
degradación por ciclado de un sistema de almacenamiento.
5.2 Formulación del problema
Con el fin de proporcionar un ejemplo que permita poner en pie los conceptos a
explicar, consideraremos la microrred presentada en la Figura 5.1. Esta red se
encuentra operando de forma aislada; es decir, sin conexión con cualquier otra
red de mayor potencia. Esto significa que, para garantizar la estabilidad en el
suministro, toda la potencia producida debe ser consumida o almacenada de
forma instantánea.
La microrred considerada cuenta con los siguientes componentes:
1. Un generador eólico.
2. Una planta de generación fotovoltaica.
3. Un generador diésel.
4. Una microturbina.
5. Un sistema de almacenamiento de energía.
6. Una zona residencial que actúa como carga en nuestro sistema.
En este punto realizaremos una simplificación importante. Dado que la
estabilidad eléctrica de la microrred queda fuera del alcance de este libro,
consideraremos que tanto generadores como cargas y sistemas de
almacenamiento trabajan a la misma tensión y que, además, las pérdidas a través
de las líneas son despreciables. Por lo tanto, el objetivo principal de nuestro
problema será el de cubrir para cada instante de tiempo la demanda de potencia
de la zona residencial mediante una gestión óptima de los cuatro generadores
considerados y del sistema de almacenamiento de energía. Para el ejemplo
objeto de estudio, nos marcaremos como objetivo la optimización del
funcionamiento de la microrred durante un periodo de un día entero, conocidos
los datos de demanda y generación en cada hora.
En cuanto a los generadores, es importante destacar que pueden dividirse en dos
grupos principales: las 5.2 Formulación del problema unidades despachables y
las no despachables. Una unidad despachable es aquella que permite ajustar la
potencia de salida de manera arbitraria, dentro de unos límites de operación. Este
es el caso del generador diésel y la microturbina, ya que permiten regular la
potencia de salida controlando el flujo de entrada de combustible. Por otro lado,
en nuestra microrred existen dos unidades no despachables: el generador eólico
y la planta fotovoltaica. En este caso la producción vendrá determinada por las
condiciones meteorológicas. Aunque hoy en día es posible aplicar estrategias de
control para obtener una potencia de salida controlada, consideraremos que esta
no puede controlarse y que, por tanto, vendrá impuesta por dichas condiciones.
A continuación definiremos cada uno de los componentes de la red por separado,
incluyendo la notación que utilizaremos en el resto del capítulo. Por último,
plantearemos formalmente el problema a resolver.
5.2.1 Recursos renovables
Como ya introdujimos anteriormente, consideraremos dos recursos renovables:
una turbina eólica y una planta de generación fotovoltaica. Ambas unidades se
consideran no despachables; es decir, su generación de potencia no puede
ajustarse libremente sino que viene impuesta por las condiciones meteorológicas
en cada momento. Así, denominaremos PPV,t y PWT,t a las potencias
instantáneas generadas por la planta fotovoltaica (PV) y la turbina eólica (WT)
en el instante t, respectivamente. La Figura 5.2 muestra los perfiles de
generación durante 24 horas de ambas unidades que consideraremos en nuestro
problema. Dichos perfiles han sido generados de forma aleatoria, pero teniendo
en cuenta el funcionamiento habitual de cada uno de estos generadores. Así, se
presentan dos modelos simplificados que nos permiten obtener la potencia de
salida de ambos generadores a partir de la radiación (en el caso de la
fotovoltaica) y de la velocidad del viento (en el caso de la turbina eólica:
■Generador fotovoltaico (Lasnier y Ang, 1990): La potencia de salida en el
instante t , P PV,t , puede ser calculada a partir de comparar la irradiancia y la
temperatura de los módulos fotovoltaicos en dicho instante, ( E M,T y T M,t
respectivamente) respecto a los valores bajo condiciones de testeo estándares: P
STC , E STC , T STC .
donde n representa el número de módulos que componen la instalación.
■Generador eólico (Deshmukh y Deshmukh, 2008): La potencia de salida en el
instante t , P WT,t , se obtiene a partir de la velocidad del viento v , las
velocidades mínimas y máximas ( v ci y V co ) y la potencia y velocidad de
viento nominal del generador ( P r y v r ):
Así, supondremos que el generador eólico se encuentra siempre trabajando con
velocidades de viento entre vci y vco y, por tanto, la potencia aportada es
siempre positiva con unos valores que fluctúan entre 40 y 80kW. Por otro lado,
el generador fotovoltaico no produce potencia durante las horas de noche,
mientras que alcanza su producción máxima en las horas centrales del día. Para
definir los perfiles de potencia y generar la Figura 5.2, basta con utilizar las
siguientes líneas de código:
Figura 5.2. Generación del sistema fotovoltaico y la turbina eólica para el
periodo de tiempo considerado de 24h.
Se puede observar, que en ambos casos se han utilizado arrays de numpy, con los
valores de los perfiles de generación de la Figura 5.2. La longitud de dichos
arrays es de 24 elementos, uno por hora del día.
5.2.2 Unidades despachables
Las unidades despachables (cuya potencia de salida puede ser controlada)
consideradas en nuestro problema son el generador diésel y la microtrubina.
Ambas reciben como entrada un flujo de combustible (diésel y gas,
respectivamente) y como resultado proporcionan una potencia de salida que
denotaremos como PDE,t y PMT,t donde DE hace referencia al generador diésel,
MT a la microturbina y t al instante de tiempo considerado. La potencia de salida
de ambas unidades se encuentra restringida a una banda de operación:
donde son los límites inferiores de operación y son los límites superiores de
operación de cada una de las unidades. Debemos tener en cuenta que los límites
de operación vienen marcados por los límites físicos de funcionamiento del
generador y, por tanto, son independientes del instante de tiempo en el que nos
encontremos. A continuación, definimos los límites de operación en nuestro
programa, así como una función que evalúe si la solución proporcionada por el
algoritmo genético cumple o no las restricciones establecidas. En caso de no
cumplirse, dicha solución será penalizada con pena de muerte. La pena de
muerte, como ya hemos introducido en capítulos anteriores, consiste en la
asignación de un valor elevado a la función objetivo o función de fitness, con el
fin de descartar la solución y que dicho individuo no pueda ser seleccionado para
participar en las operaciones genéticas. Todo esto puede observarse en el
siguiente fragmento de código:
Por otro lado, y a diferencia de lo que ocurre con los recursos renovables, estas
unidades necesitan un combustible como entrada para poder funcionar, cuyo
coste debe ser considerado. Así, consideraremos que el coste de combustible
viene dado por las siguientes expresiones:
donde hemos utilizado CDE,t y CMT,t para expresar el coste de combustible del
generador diésel y la microturbina en el instante t, respectivamente. Hay que
tener en cuenta cómo el coste de combustible es expresado como una función
dependiente de la potencia de salida; tenemos un primer término fijo, un
segundo término proporcional a la potencia y un tercero cuadrático con esta.
Ambos costes los introduciremos en nuestro programa mediante funciones. Estas
funciones recibirán como entrada la potencia entregada por el generador y
devolverán como salida el coste de funcionamiento. Para los parámetros dDE,
eDE, fDE, dMT, eMT y fMT se han tomado los valores presentados en
(Alvarado-Barrios et al., 2019). El código desarrollado es el siguiente:
Observe cómo el coste de operación únicamente actúa si el generador está funcionando, siend
La Figura 5.3 muestra el coste de funcionamiento de ambos generadores para
todos los valores de potencia que pueden ofrecer. Como se puede observar, el
generador diésel es la solución más económica para bajas potencias, mientras
que la microturbina comienza a ser más adecuada cuando se requieren potencias
superiores a los 55kW. El fragmento de código utilizado para generar la Figura
5.3 es el siguiente:
5.2.3 Sistema de almacenamiento de energía
Con el fin de dotar de flexibilidad a nuestra microrred, consideraremos además
un sistema de almacenamiento de energía conectado a la misma. Para este
ejemplo, asumiremos que dicho equipo es una batería que se encuentra
conectada a la red mediante un convertidor de potencia. Sin embargo, para
simplificar el problema intentaremos abstraernos de la configuración adoptada.
Por lo tanto, nuestra batería estará caracterizada por tres parámetros principales:
Figura 5.3. Coste de combustible del generador diésel y la microturbina en
función de la potencia entregada.
1. Capacidad ( SOC max ): Máxima energía que puede ser almacenada en la
batería. Expresaremos dicha energía en kWh .
2. Máxima capacidad de descarga Máximo valor de potencia que puede ser
entregado por la batería en un instante determinado. Dicho valor será expresado
en kW .
3. Máxima capacidad de carga Máximo valor de potencia que puede ser
absorbido por la batería en un instante determinado. Este valor será también
expresado en kW .
Además, es necesario considerar que la batería será tratada como un sistema
dinámico, es decir, un sistema que evoluciona en el tiempo dependiendo de su
estado anterior y la potencia entregada o demandada. Así, definimos el estado de
carga en un instante t (SOCt) como la cantidad de energía almacenada en la
batería en dicho instante expresada en kWh. De esta forma, el estado de carga de
la batería se encuentra acotado en todo momento por la capacidad de la misma:
0 ≤ SOCi ≤ SOCmax,
donde SOCmax es la capacidad de la batería.
Existen multitud de baterías con diferentes características. En nuestro caso, hemos escogido u
Por otro lado, como hemos mencionado, la batería es un sistema dinámico cuyo
estado de carga evoluciona con el tiempo de acuerdo al estado previo y a la
potencia entregada/absorbida en dicho instante por la misma. Así, dicha
evolución viene dada por la siguiente expresión:
SOCt+1 = SOCt – PESS,tΔt,
donde PESS,t es la potencia entregada/demandada por la batería en el instante t y
Δt es el periodo de tiempo expresado en horas entre t y t + 1. Debemos tener en
cuenta que PESS,t puede tomar tanto valores negativos como positivos. En el
caso de que PESS,t > 0 quiere decir que la batería se encuentra suministrando
potencia a la red y, por consiguiente, el estado de carga en el instante siguiente
debe ser inferior al actual (SOCt+1 < SOCt). Por otro lado, si PESS,t < 0 la
batería actúa demandando energía y se carga, es decir, SOCt+1 > SOCt.
Por último, si consideramos la máxima capacidad de carga/descarga de la
batería, podemos establecer los siguientes límites:
Al igual que en secciones anteriores, hemos intentado simplificar el problema lo máximo posi
De acuerdo con las restricciones introducidas anteriormente, desarrollaremos dos
funciones. La primera recibirá como entrada un vector con los valores de
potencia entregados/demandados por la batería durante el periodo considerado,
así como el estado de carga inicial de la batería. A partir de dichos valores, esta
devolverá un vector con los estados de carga de la batería en dicho periodo.
Dado que, como veremos más adelante, la capacidad de la batería se fijará en
280kWh, tomaremos como estado de carga inicial 140kWh con el fin de dar al
problema suficiente flexibilidad como para cargar o descargar la batería en los
primeros instantes de tiempo. A continuación, se muestra el fragmento de código
correspondiente a la definición de dicha función:
Por otro lado, será necesario construir una función que permita al algoritmo
genético evaluar la factibilidad de las soluciones. Esta función deberá evaluar si
las restricciones se cumplen para dicha solución. En caso de cumplirse,
devolverá un cero. En caso contrario, se penalizará dicha solución con pena de
muerte otorgándole un valor muy grande (dicho valor será posteriormente
utilizado como solución de la función objetivo o de fitness, como ya veremos
más adelante).
5.2.4 Balance de potencia
Además de todas las restricciones de operación introducidas en los apartados
anteriores, el balance de potencia se debe cumplir en todo momento. Esto
significa que la cantidad de energía generada debe coincidir con la demandada
más la almacenada en todo instante de tiempo. Dicha afirmación puede
expresarse como:
PDM,t = PPV,t + PWT,t + PMT,t + PDE,t + PESS,t,
donde PDM,t es la potencia demandada por la zona residencial en el instante t.
Consideraremos el siguiente perfil de potencia demandada:
Dicha curva de demanda tiene la forma típica de la curva de consumo diario en
España. Sin embargo, sus valores han sido escalados con el fin de poder ser
cubierta por los generadores considerados en nuestro problema. Debemos
destacar que estos valores son accesibles desde la página web de Red Eléctrica
Española (REE) a través de la siguiente dirección:
https://demanda.ree.es/visiona/home.
5.3 Problema con un objetivo: Minimizar el coste de
operación
Si bien es cierto que las ecuaciones introducidas hasta ahora definen
completamente el problema, es evidente que existen multitud de soluciones para
el mismo, según se controlen las unidades despachables y el sistema de
almacenamiento de energía. Entonces, parece lógico definir una función que
indique el objetivo principal que se quiere cumplir cuando se trata de operar de
forma óptima la microrred.
Para ello, nuestro principal objetivo va a ser reducir el coste de combustible de
las unidades despachables. Es decir, la función objetivo puede ser definida
como:
Así, la finalidad de nuestro algoritmo será obtener una planificación óptima de la
generación tanto de las unidades despachables como del sistema de
almacenamiento, con el fin de minimizar el coste de operación. Para ello, se
tendrán en cuenta todas las restricciones operacionales introducidas
anteriormente.
Con toda la información introducida anteriormente, podemos definir el problema
de optimización como sigue:
Observe la gran cantidad de restricciones que presenta el problema; eso hace que
su resolución a través del uso de herramientas que usen métodos convencionales
para la resolución de problemas de optimización sea muy complicada. De esta
forma, queda aún más justificado el uso de algoritmos genéticos para resolver el
problema.
5.3.1 Definición del problema y generación de la población inicial
Llegados a este punto, es conveniente definir cómo va a ser nuestro individuo.
Como hemos ido razonando a lo largo del capítulo, el algoritmo debe ajustar de
forma óptima los valores de potencia entregados por el generador diésel, la
microturbina y el demandado/aportado por el sistema de almacenamiento. Es
decir, si consideramos un periodo de 24 horas con datos cada hora, se deben
calcular 72 valores.
Sin embargo, si definimos el individuo como un vector de 72 valores, será muy
complicado generar una solución que cumpla el balance de potencias, ya que
deben generarse valores para cada hora que hagan que el sumatorio de potencias
sea igual a la potencia demandada en cada instante. Así, la solución que
adoptaremos será la de definir a cada individuo como un vector de 48 genes,
como se muestra en la Figura 5.4. Los 24 primeros corresponderán a la potencia
entregada por el generador diésel durante las 24 horas, mientras que los genes
desde el 24 en adelante, corresponderán a la potencia aportada por la
microturbina.
Figura 5.4. Representación genética de las soluciones del problema.
Usando esta estrategia para representar a los individuos, la potencia de la batería
podrá ser deducida a partir de las dos anteriores mediante un balance de
potencia. Este razonamiento se refleja en el siguiente código:
donde la función crea_individuo se encarga de generar un individuo de forma
aleatoria, y será introducida a continuación. Pasamos ahora a definir el problema
y la plantilla que utilizaremos para representar los individuos en Python.
Como vemos, se trata de un problema de minimización en el que los individuos
estarán definidos mediante un array de numpy. El siguiente paso es definir la
función crea_individuo. Como ya hemos mencionado, cada uno de los
individuos estará formado por 48 genes (ver Figura 5.4), que se corresponden
con los niveles de generación de potencia de las dos unidades despachables
durante el periodo de 24 horas considerado. Así, es necesario definir una función
que genere individuos de forma aleatoria y que cumplan, en la medida de lo
posible, con las restricciones impuestas en el problema.
Existen multitud de alternativas para llevar a cabo este propósito. En este
apartado introduciremos una posible solución cuyo buen funcionamiento hemos
validado previamente. Así, usaremos el siguiente código:
Observe que se comienza asignando valores de potencia al generador diésel,
dado que corresponde a los primeros genes de cada individuo. Sin embargo, se
podría haber comenzado asignando valores a la microturbina y, posteriormente y
basándonos en dichos valores, asignar las potencias restantes que corresponderán
al generador diésel.
La solución propuesta se basa en:
1. Generamos un perfil de potencia para el generador diésel de forma aleatoria.
Para ello, imponemos que la potencia entregada por dicho generador debe estar
entre el valor mínimo de generación y el mínimo entre el valor máximo y la
potencia demandada una vez considerado el aporte de las unidades renovables.
En caso de que la demanda sea menor que el aporte de las energías renovables,
apagaremos la unidad despachable.
2. A partir del perfil creado para el generador diésel, obtenemos las potencias
entregadas por la microturbina, de tal modo que la batería no entre en
funcionamiento, es decir, que P ESS,t = 0 , t (tengamos en cuenta que
buscamos una solución factible y no óptima).
3. Evaluamos las potencias entregadas por la microturbina de tal manera que
estén dentro de sus límites de operación. En caso de que la potencia entregada se
encuentre por debajo del límite inferior, se fijará en cero y el resto de energía
será suministrada por la batería. Por otro lado, si la potencia es mayor al límite
superior, se fijará en su capacidad máxima y el exceso de potencia será
absorbido por la batería.
Para registrar las funciones que nos permiten crear individuos aleatorios y la
población inicial, procedemos como hemos visto a lo largo del libro.
Debemos indicar que la población inicial es generada mediante tools.iterate, ya
que nuestra función generará individuos completos y no genes del mismo1.
5.3.2 Operadores genéticos
Una vez definido el problema, es hora de desarrollar las funciones que utilizará
nuestro algoritmo para realizar las operaciones genéticas. Comenzaremos por la
operación de cruce. En este caso utilizaremos un cruce de dos puntos. Dicha
operación requiere seleccionar dos puntos en los individuos de la generación
previa. Todos los datos entre los dos puntos se intercambian entre ambos
individuos, creando dos hijos (nueva generación). Tanto la elección de ambos
puntos como la de si llevar a cabo o no la operación genética se realiza de forma
aleatoria.
A continuación, seguimos con el operador de mutación. Esta función realizará la
operación de mutación a los individuos de acuerdo a sucesos aleatorios. Para
evolucionar desde una generación a otra, el algoritmo aplicará operaciones de
mutación en los genes de ciertos individuos. Dichas mutaciones deben realizarse
siguiendo un criterio adecuado con el fin de garantizar pequeños movimientos
que permitan obtener mejores individuos en posteriores generaciones del
algoritmo. En este problema, se plantea una estrategia ajustada al mismo. Es
decir, no se utiliza un función de la librería deap. Esta propuesta se basa en un
análisis previo para el que se han obtenido resultados positivos; no obstante,
como ya se ha comentado anteriormente, puede ser modificada libremente.
■Se propone mutar genes variando su valor de acuerdo a una distribución
normal cuya media es el valor actual del gen y cuya desviación típica es
ajustable (mutación Gaussiana). Por ejemplo, considere que en un instante
determinado el generador diésel está entregando 40 kW . Entonces, parece lógico
variar esta cantidad en el entrono de dicho valor, realizando una pequeña
modificación que repercutirá en la optimalidad del nuevo individuo. La
variación será más acentuada conforme mayores valores se asignen a la
desviación típica. Para nuestro problema consideraremos un valor de desviación
típica igual a 30.
■Por otro lado, existe la posibilidad de que la unidad esté apagada y, por tanto, la
potencia entregada sea nula. Es imposible conseguir a partir de la mutación
anterior un valor igual a cero, por lo que se propone además una mutación que
establezca a cero un determinado gen.
Así, nuestro operador de mutación puede escribirse como se muestra a
continuación:
donde indpb es una tupla de dos componentes que establece la probabilidad en
tanto por uno de que mute un gen (ya sea variando la potencia generada o
apagando el generador). El último paso, con respecto a la operación de
mutación, es registrarla en la caja de herramientas.
En cuanto al mecanismo de selección, se ha utilizado la selección mediante
torneo. El torneo consiste en escoger un número determinado de individuos de la
última generación del algoritmo, comparar sus valores de función de fitness, y
escoger aquella con menor valor.
La última función que nos queda por registrar, es la función de fitness. En este
problema y debido a que esta es más compleja, la veremos en una sección aparte.
5.3.3 Función objetivo
La función objetivo o de fitness recibe como entrada un individuo, lo evalúa y
devuelve un valor de acuerdo a la calidad del mismo. Dicho valor puede ser
igual al coste de combustible (en el caso de que el individuo cumpla todas las
restricciones de operación) o bien puede adoptar el valor de la penalización (en
el caso de que alguna de las restricciones no se cumpla). De este modo,
simplemente hay que evaluar todas las funciones desarrolladas en los apartados
anteriores en el orden adecuado. Es decir, si un individuo no cumple las
restricciones del problema, no es necesario evaluar su valor de coste de
combustible, ya que esa solución será descartada.
Así, la función objetivo será la siguiente:
Observe cómo la función de fitness devuelve el valor de la pena de muerte,
deteniendo la ejecución de la función cuando una restricción no se cumple.
Cuando todas las restricciones son satisfechas, se calcula el coste de
combustible, que es devuelto por la función.
Por último, no debemos olvidar el registro de la función objetivo.
5.3.4 Ejecución del algoritmo
Es el momento de ejecutar el algoritmo y analizar los resultados obtenidos. El
algoritmo utilizado será el µ + λ (dicho algoritmo ya ha sido utilizado
anteriormente, por ejemplo para resolver el problema planteado en el Capítulo
2). Para ejecutar este algoritmo es necesario definir una serie de parámetros que
se exponen a continuación:
■NGEN : En este caso su valor es 500. Este parámetro se puede variar en
función de lo observado en la gráfica de convergencia.
■MU : Fijamos el tamaño de la población. Establecemos 1500 para esta prueba.
En este problema, debido a su complejidad, utilizamos un número elevado de
individuos. Esto hará que el algoritmo tarde un poco en terminar 2.
■LAMBDA : Se fija el tamaño de descendientes en cada nueva generación. Se
fija con un valor igual a 1500. Como el valor de MU y LAMBDA son iguales, el
tamaño de la población permanece constante durante toda la ejecución del
algoritmo genético.
Además, como parámetros de entrada a la función se usarán las probabilidades
de cruce y mutación expresadas en tanto por uno (c y m respectivamente). La
función de llamada al algoritmo queda, por tanto, del siguiente modo:
Como se puede observar, esta función inicializa el problema y realiza una
llamada al algoritmo basado en la configuración adoptada en el toolbox. Se
establece un registro de los valores mínimos, máximos y medios de lo devuelto
por la función de fitness, así como de la desviación típica de dichos valores para
todos los individuos de cada generación. El funcionamiento de los objetos stats,
hof y logbook ha sido explicado en capítulos anteriores. Se recomienda
consultarlos en caso de duda.
Cabe destacar que, en este caso, el algoritmo genético no ha ejecutado desde la
función main; esto es así porque en este caso se definen dos funciones distintas
para ejecutar los dos tipos de problemas que se abordan en esta sección:
problema con un objetivo o problema con dos objetivos.
El script tarda en ejecutarse alrededor de 200 segundos. Este tiempo de
ejecución ha sido medido tras ejecutar el algoritmo en un Intel Core i5-72000U a
2.5 GHz con 8 Gb de memoria RAM.
5.3.5 Resultados obtenidos
En este momento nos encontramos en disposición de ejecutar nuestro algoritmo
genético. Para ello, haremos una llamada a la función unico_objetivo_ga, para lo
cual es necesario pasar como argumentos los parámetros c y m, que definirán la
probabilidad de cruce y mutación, en tanto por uno respectivamente. Con el fin
de explorar los resultados obtenidos para diferentes combinaciones de estos
parámetros, planteamos la llamada al algoritmo bajo tres posibles
configuraciones: (c = 0.6, m = 0.4), (c = 0.7, m = 0.3) y (c = 0.8, m = 0.2). Para
cada una de las configuraciones se lanzará el algoritmo diez veces. Así,
ejecutamos las siguientes líneas de código:
Observe, en el código anterior, la existencia de dos bucles for anidados. El
primer bucle fija la configuración de los parámetros c y m, mientras que el
segundo realiza las 10 llamadas al algoritmo. Con el fin de registrar de forma
adecuada los resultados obtenidos, se generan dos ficheros de texto:
individuos_microrred.txt y fitness_microrred.txt. En el primero, se almacenarán
los mejores individuos obtenidos de cada llamada al algoritmo. El fichero tendrá
una línea por cada llamada con una estructura [número de llamada, c, m,
individuo]. Por otro lado, el segundo archivo almacenará los valores de la
función objetivo obtenidos para cada caso. Su estructura será análoga a la del
fichero anterior: [número de llamada, c, m, fitness].
Antes de lanzar tantas llamadas al algoritmo evolutivo, lo primero que debemos
hacer es lanzar un caso simple con el fin de cerciorarnos de que los valores
fijados para NGEN, MU y LAMBDA son los apropiados para el problema. Para
ello, basta con ejecutar la siguiente línea:
en donde hemos escogido de forma aleatoria c = 0.7 y m = 0.3. Una vez
finalizada su ejecución, podemos visualizar la gráfica de convergencia del
algoritmo. De esta manera, podemos tener la certeza de que la solución obtenida
es definitiva o aún esta sujeta a variaciones y es necesario ampliar el número de
generaciones o el tamaño de la población.
Figura 5.5. Gráfica que muestra la convergencia del algoritmo.
Es lógico que en cada generación exista algún individuo que no cumpla las
restricciones y que, por tanto, tenga un fitness condicionado por la pena de
muerte. Sin embargo, el valor mínimo debe ser decreciente y asintótico al valor
óptimo final. La evolución del algoritmo se muestra en la Figura 5.5. Se puede
observar la total convergencia del algoritmo alrededor de la generación 300.
Una vez comprobada la convergencia del algoritmo, estamos en posición de
lanzar la batería de ejecuciones introducida anteriormente y analizar los
resultados obtenidos. El Texto 5.1 muestra los datos almacenados en el fichero
fitness_microrred.txt, mientras que el Texto 5.2 muestra un fragmento con el
primero de los individuos almacenados en el fichero individuos_microrred.txt.
Texto 5.1. Resultados del algoritmo almacenados en el archivo
fitness_microrred.txt.
Texto 5.2. Fragmento de los resultados del algoritmo almacenados en el archivo
individuos_microrred.txt.
La Tabla 5.1 muestra el valor mínimo, máximo y medio de la función objetivo
para cada configuración de valores de c y m. Como se puede observar, el mejor
resultado se obtiene para una probabilidad de cruce del 70% y una de mutación
del 30%. Sin embargo, los resultados son muy similares para las tres
configuraciones probadas.
Pcx
Pmut
0.6
0.4
0.7
0.3
0.8
0.2
min(CF) 556.63 554.82 559.42
max(CF) 564.43 564.82 571.65
avg(CF) 560.13 560.14 563.64
Tabla 5.1. Resultados de la función de fitness para diferentes configuraciones de
la probabilidad de cruce y mutación.
La mejor solución obtenida, correspondiente a las probabilidades Pcx=0.7 y
Pmut=0.3, se resume en la Tabla 5.2. En esta tabla podemos observar los valores
de la potencia entregada por los diferentes generadores, así como la gestión de la
batería. Por un lado, se puede comprobar que la mayor parte del tiempo el
generador diésel está apagado y, por tanto, no implica coste alguno de
combustible. Por otro lado, la microturbina se encuentra entregando potencia en
las horas de mayor consumo y, además, utiliza el exceso de potencia para cargar
la batería (valores negativos de PESS,t).
A la vista de la Figura 5.3, se puede comprobar que los resultados son lógicos ya
que, según se ha visto, el coste marginal de producir un kW más de potencia por
la microturbina es menor cuanto mayor sea la potencia total producida por la
misma. Así, por ejemplo, tiene un menor coste producir 180kW con la
microturbina y usar 80kW de ellos para cargar la batería (que aportará dicha
energía en instantes posteriores) que producir 100kW y 80kW en dos instantes
consecutivos.
Para un mejor análisis de los datos generaremos dos nuevas figuras. En la
primera de ellas, representaremos el aporte de cada generador en cada uno de los
instantes de tiempo para cubrir la demanda correspondiente. En la segunda,
representaremos la potencia entregada por la batería, así como la evolución del
estado de carga durante el periodo de tiempo considerado.
Tabla 5.2. Resultado obtenido tras ejecutar el algoritmo genético para el
problema de optimización de la microrred.
Para generar la primera figura (Figura 5.6) utilizaremos el siguiente código:
Por otro lado, para generar la segunda de estas figuras (Figura 5.7) basta con
ejecutar:
Las Figuras 5.6 y 5.7 representan ambas gráficas de resultados. Ahora es el
momento de analizar si la solución obtenida es coherente con los objetivos que
nos hemos planteado.
Figura 5.6. Solución del problema planteado. Se muestra la generación de cada
uno de los recursos, así como la gestión de la batería en la microrred.
Por un lado, la Figura 5.6 muestra un gran uso de la microturbina en decremento
del generador diésel, el cual solo entra en servicio en el instante t = 5. Esto tiene
sentido ya que, como se puede observar en la Figura 5.3, el coste de la
microturbina es menor cuando sobrepasamos los 55kW de potencia. De esta
manera, al tener el generador diésel inactivo nos ahorramos su coste de
operación.
Por otro lado, se puede observar en la Figura 5.7 cómo la batería entra
continuamente en funcionamiento. Este hecho también es lógico, dado que su
uso es gratuito y nos permite una mayor flexibilidad en la red. Es importante
apreciar cómo el estado de carga de la batería alcanza sus valores máximos y
mínimos durante el día, siendo operada para aprovechar todo su potencial.
Figura 5.7. Evolución de potencia y estado de carga del sistema de
almacenamiento para la solución obtenida.
5.4 Problema con múltiples objetivos: Minimizando el
coste de operación y el ciclado de la batería
Analizaremos, ahora, la situación en la que nos interesa optimizar más de un
objetivo de forma simultánea. Además, consideraremos que ambos objetivos se
oponen entre sí, es decir, que una mejora en uno implica inevitablemente un
empeoramiento en el otro. En este escenario el algoritmo debe establecer una
solución de compromiso entre ambos.
Así, nos moveremos hacia a un escenario más realista, en el que tendremos en
cuenta la posible degradación de la batería como consecuencia del ciclado de la
misma. El ciclado de la batería no es más que la cuantificación del número de
cargas y descargas que se realizan a lo largo del tiempo. Al aumentar el número
de ciclos de una batería, sus propiedades se ven afectadas (disminución de la
capacidad, disminución de la potencia máxima de carga y descarga, etc.), lo que
en la práctica se traduce en un número limitado de cargas y descargas de la
batería. Así, es evidente que aumentar el ciclado es un factor desfavorable en
términos de vida útil de la batería.
Vamos entonces a definir como un objetivo adicional la minimización del
ciclado de la batería, utilizando el siguiente término de coste:
donde SOCini es el estado de carga inicial del sistema de almacenamiento.
Podemos observar cómo se penaliza el uso de la batería de forma más acentuada
cuando el estado de carga se sitúa en posiciones más lejanas del estado inicial.
Debemos tener en cuenta que, mientras la función objetivo del caso
monoobjetivo penalizaba el coste de combustible (expresado en ), esta función
penaliza la degradación de la batería (expresada en kWh). Así, la formulación
del problema vendrá dada por:
Por lo tanto, ahora tenemos dos objetivos que deseamos minimizar: CF y CESS.
5.4.1 Definición del problema, población inicial y operadores genéticos
Estos pasos son equivalentes a los dados en el caso mono objetivo. Solamente es
necesario realizar algunos cambios tanto en la creación del problema como en el
toolbox. Estos cambios son los siguientes:
Vemos que los dos únicos cambios considerados son, por una parte, que ahora al
definir el problema se especifica que hay dos objetivos (ambos a minimizar) y,
por otra, que la operación de selección se realiza a través del tools.selNSGA2, ya
que nuestro objetivo es obtener el frente de Pareto del problema. No hay ningún
cambio con respecto a los operadores genéticos. La función objetivo que se ha
registrado (fitness_multi) se definirá a continuación.
5.4.2 Función objetivo
La función objetivo o fitness recibe como entrada un individuo, lo evalúa y
devuelve dos valores de acuerdo a la calidad de ambos términos de coste. Dichos
valores adoptan la penalización en el caso de que alguna de las restricciones no
se cumpla. De este modo, se puede construir la siguiente función objetivo:
Podemos comprobar que esta función es idéntica a la del caso de un único
objetivo, con la única diferencia de que, en este caso, se calcula también el valor
de CESS al final del código. De igual forma, se devuelven dos valores en lugar
de uno, que corresponden a los dos objetivos que se desean minimizar.
5.4.3 Ejecución del algoritmo
La función de llamada al algoritmo evolutivo podrá ser definida de igual manera
que en el caso de un único objetivo, ya que las variaciones necesarias han sido
definidas el la caja de herramientas o toolbox:
De nuevo, el toolbox es definido y pasado como argumento a la función, que
inicializa el problema y comienza la rutina.
5.4.4 Resultados obtenidos
Realizamos entonces la llamada a la función multiple_objetivo_ga, fijando los
valores de configuración de la estrategia de crossover, como son c = 0.7 y m =
0.3 (los óptimos obtenidos para el caso de un único objetivo). De nuevo, y para
facilitar el análisis de los datos, crearemos dos ficheros donde iremos
almacenando los resultados entregados por el algoritmo (en este caso
obtendremos más de un individuo como resultado). Este procedimiento se
recoge a continuación:
Ya que hemos utilizado los mismos parámetros que en el caso de un objetivo,
supondremos que la solución aportada por el algoritmo ha convergido. Los
resultados de la Figura 5.8 avalan que esos valores son adecuados, ya que vemos
un amplio número de soluciones en el frente de Pareto. En el Texto 5.3 se puede
observar un fragmento del fichero de texto fitness_microrred_multi.txt. Así
mismo, el Texto 5.4 muestra uno de los individuos almacenados en el fichero
individuos_microrred_multi.txt.
Texto 5.3. Fragmento de los resultados almacenados en el archivo
fitness_microrred_multi.txt.
Texto 5.4. Fragmento de los resultados almacenados en el archivo
individuos_microrred_multi.txt.
La Figura 5.8 muestra el frente de Pareto obtenido como resultado de la
ejecución del algoritmo. El frente Pareto muestra el valor de ambos términos de
coste para diferentes individuos, de forma que ninguna solución es dominante
sobre otra. Este concepto se puede entender de forma análoga al concepto de
coste marginal en economía. Dada una solución con un coste en combustible y
en degradación de la batería, este término indica qué implicaciones tendría en
uno de los dos términos, un incremento en la calidad del otro. Así, si por ejemplo
nos fijamos en la Figura 5.8, podemos tomar como punto de partida el (637, 190)
(punto 6 en la gráfica). Supongamos, entonces, que deseamos disminuir el coste
de combustible; nos desplazaríamos hacia la izquierda en la curva. La pendiente
de dicha curva representa el coste marginal. Si nos movemos al punto (630, 210)
(punto 5 en la gráfica) comprobamos que una mejora de 7 en el coste de
combustible implica un empeoramiento de 20kWh en el término que cuantifica
la degradación de la batería.
Figura 5.8. Frente Pareto del problema abordado.
Por último, la Figura 5.9 muestra la evolución del estado de carga del sistema de
almacenamiento para diferentes puntos del frente de Pareto (los marcados en la
Figura 5.8). Como se puede observar, conforme disminuye el coste en
combustible, aumenta el coste de degradación de la batería y, por consiguiente,
la evolución del estado de carga toma valores más extremos. De esta manera,
cada vez sacamos más partido al sistema de almacenamiento. Sin embargo, esta
mejora afecta a la vida útil de la batería. Así, para los puntos finales en los que
menores valores se obtienen para el objetivo de degradación de la batería, se
observa que el estado de carga de la misma se mantiene constante la mayor parte
del tiempo, únicamente cubriendo el pico de demanda en t = 20h.
Figura 5.9. Evolución del estado de carga para diferentes puntos del frente de
Pareto.
5.5 Código completo y lecciones aprendidas
Antes de finalizar este capítulo repasemos, a modo de resumen, los pasos que
hemos seguido para la resolución del problema propuesto. Para ello nos
apoyaremos en el código completo mostrado en Código 5.5:
1. Líneas 1-8: Primero es necesario incluir todas las librerías que utilizaremos en
nuestro código. En este caso haremos uso de la librería deap para la resolución
de los algoritmos genéticos, la librería numpy para la correcta operación de
vectores y matrices y, por último, la librería matplotlib que nos permitirá
representar gráficamente los resultados obtenidos.
2. Líneas 10-13: Definimos si el problema a resolver tiene un único objetivo o si,
por el contrario, estamos considerando el caso de minimizar los dos objetivos
propuestos. En la línea 13 se ha dejado la variable multi como True por defecto;
por lo tanto, se ejecutaría el algoritmo multiobjetivo. Si se desea ejecutar el
algoritmo con un solo objetivo, se debe cambiar el valor a False .
3. Líneas 15-25: A continuación, es necesario definir los datos del problema. En
nuestro caso serán los perfiles de potencia aportados por el generador eólico y la
planta fotovoltaica, así como la potencia demandada por el centro de consumo.
4. Líneas 27-43: Definimos los parámetros de las unidades del problema, así
como el valor de penalización para cuando se aplique la pena de muerte. Como
en casos anteriores, dicho valor se puede cambiar, siempre y cuando sea un valor
que penalice mucho las soluciones inválidas.
5. Líneas 45-113: Después, implementamos unas funciones que nos permitan
evaluar si las restricciones de operación se cumplen. En caso de no hacerlo,
dichas funciones deberán devolver un valor que indique la pena de muerte.
Elaboramos funciones para evaluar los límites de operación de las unidades
despachables, así como del estado de carga y potencia demandada/suministrada
por la batería.
6. Líneas 115-141: Se definen las funciones de generación de individuos y de
mutación.
7. Líneas 143-163 y líneas 192-213: Ahora, estamos en posición de definir la
función objetivo de nuestro problema, que devolverá un valor que el algoritmo
intentará minimizar. En el caso de un único objetivo será el coste de
combustible, mientras que en el caso multiobjetivo se considerará también el
coste de ciclado de la batería.
8. Líneas 165-190 y líneas 217-248: Generamos una función que haga la llamada
al algoritmo genético una vez se hayan definido todos los parámetros de
configuración del mismo. Esta función es diferente para el caso de un único
objetivo o de dos.
9. Líneas 251-282: Finalmente, configuraremos el algoritmo evolutivo mediante
el toolbox y haremos la llamada a la resolución del problema. Para ello, nos
apoyaremos en la elección tomada respecto al problema a través de la variable
booleana multi .
Código 5.5. Código final para el problema de gestión óptica de la microrred.
Por otro lado, podemos resumir el trabajo de este capítulo como la aplicación de
los algoritmos genéticos sobre un problema real de optimización de una
microrred. El problema consiste en la gestión óptima de los generadores de la
microrred para cubrir un determinado nivel de demanda, para lo que se han
considerado datos horarios durante un periodo de 24 horas. Dos objetivos han
sido abordados:
■Por un lado, se han calculado los puntos de funcionamiento óptimos de los
generadores y el sistema de almacenamiento con el fin de minimizar el coste de
combustible.
■Por otro lado, hemos considerado un escenario en el que la degradación de la
batería es tomada en consideración. Así, a la vez que se intenta minimizar el uso
de combustible, también se pretende minimizar el ciclado de la batería con el fin
de alargar tanto como sea posible su vida útil.
Se ha introducido una metodología de actuación adecuada para resolver este tipo
de problemas y garantizar una solución aceptable. Así, factores como analizar la
convergencia del algoritmo han sido destacados. Como principales lecciones
aprendidas destacan las siguientes:
■En este capítulo hemos aprendido cómo aplicar los algoritmos genéticos a
través de la librería deap a un problema real de ingeniería eléctrica.
■Hemos visto cómo dotar de mayor flexibilidad al problema mediante una
adecuada definición de los individuos. Si recordamos, nuestro objetivo es
conocer las potencias entregadas por el generador diésel, la microturbina y el
sistema de almacenamiento; sin embargo, solo los datos correspondientes a los
dos primeros han sido incluidos en los genes del individuo. De esta forma, las
potencias de la batería se derivan de los otros generadores, haciendo el problema
más flexible y eficiente, al asegurar que se cumple el estado de carga.
■Se han introducido recursos para obtener datos reales de demanda eléctrica a
través de la página web de Red Eléctrica Española.
■Hemos observado el frente de Pareto de un problema real, poniendo en
relevancia la necesidad de lograr un compromiso entre ambos objetivos.
■Por último, hemos realizado una batería de llamadas al algoritmo y hemos
analizado los resultados obtenidos para diferentes probabilidades de cruce y
mutación, lo que nos ha permitido encontrar la combinación de parámetros más
adecuada.
5.6 Para seguir aprendiendo
Con el fin de lograr un mayor conocimiento sobre este tipo de problemas, se
recomienda la lectura de los siguientes artículos:
■Un resumen amplio sobre optimización de microrredes se puede encontrar en
(Li et al., 2015b). (Khan et al., 2016).
■En (Nemati et al., 2018) se realiza una comparación entre algoritmos genéticos
y técnicas de programación lineal mixta o MILP.
■En (Alvarado-Barrios et al., 2020) se aborda un problema parecido al planteado
en este capítulo pero considerando incertidumbre en la demanda. El problema se
resuelve mediante programación entera no lineal.
■En (Alvarado-Barrios et al., 2019) el problema planteado en (Alvarado-Barrios
et al., 2020) se resuelve mediante algoritmos genéticos. Además, se proponen
distintos modos de funcionamiento de la microrred, lo que da lugar a varios
problemas de optimización. Por ejemplo, teniendo en cuenta el coste ecológico
de funcionamiento. En (Rodríguez del Nozal et al., 2020) se abordan distintos
tipos de baterías.
■El problema se amplía en (Rodríguez del Nozal et al., 2019), aplicando en este
caso Model Predictive Control (MPC) (Camacho y Alba, 2013), el cual se basa
en resolver el problema considerando un ventana temporal en la optimización
del funcionamiento de la microrred.
Como ejercicios se plantean los siguientes:
■Realice una comparación de los resultados para distintos operadores de cruce
(cruce un punto, dos puntos, uniforme, etc.).
■Una vez obtenido el operador de cruce más adecuado, modifique el valor de σ
del operador de mutación propuesto en el problema. Pruebe distintos valores e
intente ajustar su valor.
■Introduzca rendimientos de carga y descarga en la batería y obtenga los
resultados que minimizan tanto el problema con un único objetivo como el
problema multiobjetivo. Esto se logra reescribiendo la función de la evolución
del estado de carga como sigue:
donde η representa el rendimiento de carga y descarga en tanto por uno.
■Resuelva el problema considerando diferentes parámetros de la batería:
capacidad, máxima capacidad de descarga y mínima capacidad de descarga.
Pruebe disminuir la capacidad hasta un punto en el que la factibilidad del
problema se vea comprometida.
_________________
1Si se surge alguna duda sobre estás operaciones, se recomienda consultar
capítulos anteriores.
2El tiempo dependerá del ordenador que se utilice, pero debe estar en el orden
de las decenas de minutos.
6.1 Introducción
En este capítulo se propone la resolución al problema del diseño de una planta
micro-hidráulica, de manera que el emplazamiento de sus elementos más
significativos permita sacar el máximo partido al terreno, satisfaciendo un nivel
mínimo de potencia generada. Las plantas microhidráulicas son plantas de
generación de energía eléctrica a partir de la energía potencial de un flujo natural
de agua. El término micro hace referencia a niveles de potencia instalada
inferiores a 100 kW, lo que se traduce en instalaciones económicas, robustas y
eficientes, generalmente utilizadas para abastecer pequeñas zonas aisladas, con
dificultades para acceder a la red principal.
Aquellos problemas de optimización que tienen asignada una función de
adaptación cuya evaluación supone un alto coste computacional pueden suponer
un serio problema cuando son abordados por un AM. Esto es así porque a la
demanda de evaluación de dicha función por parte de la BG de la EA hay que
añadirle al exigida por la
A pesar de la simplicidad de estas instalaciones, tanto su capacidad como su
rendimiento están fuertemente condicionados por su ubicación y su trazado
sobre el terreno. Por una parte, un trazado que explote una gran diferencia de
altura puede permitir alcanzar niveles de generación más altos. Por otra, un
trazado que abarque una gran distancia puede incrementar demasiado las
pérdidas por fricción en la conducción del agua y afectar negativamente al
rendimiento del sistema.
La complejidad de resolver este problema de optimización no lineal
considerando un terreno cualquiera sobre el que instalar la planta, no solo resulta
inabordable desde un punto de vista analítico, sino que la misma existencia de un
óptimo es, como mínimo, muy complicada de demostrar, si no imposible. Por
este motivo se plantea en este capítulo el diseño de un algoritmo genético para
encontrar una solución óptima.
6.2 Formulación del problema
Para implementar nuestro algoritmo genético, necesitamos en primer lugar
definir un conjunto de ecuaciones que nos permitan entender y predecir el
funcionamiento del sistema, esto es, un modelo de nuestra planta microhidráulica. Este modelo nos permitirá obtener las variables de interés (como la
potencia generada por la planta o cuánto cuesta instalarla) en función de las
variables de diseño o de decisión (como la posición de la turbina y la extracción
de agua). Esta será la herramienta mediante la cual nuestro algoritmo podrá
cuantificar cuán buena es una determinada solución, en base a los criterios que
consideremos oportunos.
En términos generales, una central micro-hidráulica está compuesta por una serie
de elementos destinados a extraer un flujo de agua con una cierta energía
potencial (para garantizar el abastecimiento es habitual instalar en este punto un
reservorio de agua mediante una presa), conducirla aguas abajo (mediante un
conducto denominado tubería forzada) transformando esta energía en presión y,
finalmente, transformarla de nuevo en energía eléctrica mediante un sistema de
generación (generalmente compuesto por una turbina y un generador acoplado),
que suele instalarse dentro de una pequeña construcción denominada casa de
máquinas (ver Figura 6.1).
Figura 6.1. Esquema de una planta micro-hidráulica.
El problema presentado en este capítulo consiste en encontrar el trazado de la
planta (posición de la casa de máquinas, punto de extracción y codos de la
tubería forzada) que minimiza el coste total y que satisface una generación de
potencia mínima. Veamos cómo introducir esta formulación en nuestro
algoritmo. Para ello trabajaremos en dos partes diferenciadas:
1. En primer lugar, desarrollaremos una estrategia matemática para codificar las
posibles soluciones del problema (trazados de la planta sobre el terreno) en un
cromosoma. Esta formulación permitirá obtener, para cada posible solución, las
variables fundamentales de la planta, como son la diferencia de altura bruta H g ,
la longitud de la tubería L o el número total de codos n c . Estas variables se
explicarán en detalle más adelante.
2. En segundo lugar, desarrollaremos un conjunto de ecuaciones matemáticas,
basadas en los principios físicos que rigen el comportamiento de la planta
microhidráulica, que nos permitirá deducir las prestaciones de la planta
(potencia, caudal o coste) en función de las variables fundamentales anteriores.
Estamos, pues, ante un problema combinatorio ya que debemos decidir sobre la
posición óptima de un conjunto de elementos que forman la microplanta.
Perfil del terreno
Para plantear el problema necesitamos, en primer lugar, considerar el perfil del
terreno sobre el que se diseñará la planta. Así, llamaremos ϕ(s) a una función
escalar que define el perfil del río en un plano. Es importante destacar que
hemos considerado que la curvatura del río es suficientemente baja como para
desarrollar su curva de altura en un plano, simplificando así el problema. La
primera consideración importante es que, dada la imposibilidad de conocer una
expresión analítica de ϕ(s) para cualquier terreno, consideraremos una definición
discreta. Para ello se asumirá un conjunto arbitrario de N puntos en la forma
(si,zi), que se pueden obtener mediante un análisis topográfico del terreno o a
partir de sus curvas de nivel.
Si partimos de la información topográfica en un determinado fichero externo
(por ejemplo un archivo PuntosRio.csv con los valores de si y de zi en dos
columnas), podemos construir los vectores s y z mediante los siguientes
comandos:
6.2 Formulación del problema Figura 6.2. Representación del perfil del río.
A continuación, se muestran las primeras líneas del archivo PuntosRio.csv que
se ha utilizado para este problema:
Podemos también comprobar de forma gráfica cómo es el perfil del río,
utilizando una función que represente los puntos z frente a los puntos s. Para esto
podemos crear la función dibujaRio, que se muestra a continuación. El resultado
de esta función se muestra en la Figura 6.2.
Trazado de la planta
Veamos ahora cómo modelar el trazado de la planta sobre el terreno que
acabamos de leer. Tal y como hemos comentado, las partes principales del
sistema son tres: el punto de extracción, la casa de máquinas, y la tubería
forzada. Como únicamente conocemos el terreno en N puntos discretos,
consideraremos estos como puntos candidatos para ubicar uno de los elementos
indicados. Así, codificaremos las soluciones X del problema como un conjunto
de N variables binarias δi, como se muestra en la Figura 6.3: Cualquier
combinación de estos
Figura 6.3. Representación genética de las soluciones del problema.
N bits definirá un posible trazado de la planta. Este será, por tanto, el
cromosoma que definirá a cada uno de los individuos en nuestro algoritmo
genético. Así, la función de interpolación de los nodos, llamémosla Γ(s),
representará el trazado de la planta.
Antes de seguir, vamos a crear una función similar a dibujaRio pero que permita
representar sobre el perfil del río la solución correspondiente a un individuo.
Esta función, que denominaremos dibujaSolucion, debe recibir el individuo
como argumento, y podría escribirse de la siguiente manera:
Para identificar los nodos de la solución hemos utilizado la función np.nonzero1,
que nos devuelve los índices donde aparecen elementos no nulos, es decir, los
puntos del río donde instalamos nodos. Veamos un ejemplo de esta formulación.
Considerando el perfil de río introducido anteriormente, formado por un total de
N=100 puntos, propongamos una posible solución formada por 5 nodos (δi
positivas), situados en los puntos 47, 55, 65 y 85. Podemos crear esta solución de
forma sencilla partiendo de un vector de 200 ceros, y transformar en 1 los
elementos de las posiciones que se acaban de indicar. Para ello, podemos
complementar el código anterior con los siguientes comandos:
Debemos notar que, habiendo definido ϕ(s) en sentido creciente, el primero y el
último de los nodos de este subconjunto representan, respectivamente, la casa de
máquinas y la extracción, mientras que el resto de ellos representan dos codos de
la tubería forzada.
Si extraemos las coordenadas (s y z) de todos los nodos:
podemos determinar fácilmente la información que necesitamos sobre la planta;
por ejemplo, la altura bruta, Hg, restando la altura z del último y el primer nodo:
O la longitud de la tubería forzada, Lt f, sumando la distancia euclídea entre cada
nodo y el siguiente:
O el total de codos de esta última, nc, que no es más que el número de unos del
individuo menos dos (el de la turbina y el de la presa):
Todas estas expresiones las usaremos más adelante para definir la función de
fitness de nuestro algoritmo.
6.2.1 Modelado de la central micro-hidráulica
Estudiaremos ahora cómo podemos determinar las prestaciones de la planta
micro-hidráulica a partir de su trazado. Aunque existe una gran variedad de
equipos y configuraciones, con el propósito de simplificar el estudio, este
capítulo se centrará en el diseño de una planta de microgeneración de agua
fluyente (sin presa ni reservorio de agua) de tipo Pelton. Las turbinas Pelton son
un tipo de turbinas de acción, es decir, el intercambio de energía se produce a
presión atmosférica, para lo cual se requiere la transformación de la presión en
energía cinética mediante un inyector. El intercambio de energía se produce al
impactar un chorro de agua sobre una serie de cucharas dispuestas
circunferencialmente alrededor de un rodete (ver Figura 6.4).
Figura 6.4. Esquema de funcionamiento de una turbina de acción de tipo Pelton.
Prestaciones de la planta
A continuación, es necesario determinar cómo el trazado de la planta (cada
posible solución) condiciona las prestaciones de la misma, de manera que el
algoritmo de optimización pueda evaluar la calidad de los individuos.
Veamos, entonces, las ecuaciones que gobiernan el comportamiento de una
planta de este tipo. Las variables de interés para nuestro problema de
optimización van a ser la potencia generada por la planta, P, y el coste de la
instalación, C. La potencia generada por el conjunto turbina-generador, P, se
puede calcular a partir de la altura (presión del agua expresada en columna de
agua equivalente) neta del agua en la turbina, Ht, y el caudal volumétrico
turbinado, Q, como sigue:
P = ηρgQHt,
donde ρ y g son, respectivamente, la densidad del agua y la aceleración de la
gravedad. Se ha introducido, además, un coeficiente η que representa la
eficiencia del conjunto de generación. En un caso ideal (sin fricción) la altura
neta en la turbina, Ht, sería igual a la altura bruta de la instalación, Hg. No
obstante, en la realidad ocurre que las pérdidas debidas al paso del agua a través
de la tubería provocan una cierta pérdida de altura, que denominaremos ΔHfric.
Así, se puede escribir que:
Ht = Hg – ΔHfric.
Como la turbina es de acción, la transformación de energía en la misma se hace a
presión atmosférica, es decir, toda la energía del agua (que forma un chorro, al
que nos referimos con el subíndice jet) es cinética:
donde, además, dada la incompresibilidad del agua, la velocidad se puede
escribir en términos del caudal y la sección de salida del inyector Siny, según:
siendo CD un coeficiente de descarga que se introduce para modelar la
formación del chorro en el inyector (Thake, 2000) (ver Figura 6.4). Así, la altura
del agua a la entrada de la turbina se puede reescribir como:
En cuanto a las pérdidas en la tubería forzada anteriormente comentadas, ΔHfric,
se pueden aproximar de forma muy sencilla mediante un coeficiente Kt f y la
geometría de la tubería forzada (longitud Lt f y diámetro Dt f), lo cual se puede
escribir:
Combinando estas ecuaciones se puede escribir una expresión para la potencia
generada en la planta:
El coste de la planta, C, se puede estimar como la suma de los costes asociados a
la tubería forzada, Ct f, al equipo de generación, Cgen, y a la red eléctrica, Cre.
En este problema se harán dos consideraciones para simplificar su cálculo: (i) se
considerará un determinado equipo de generación, y (ii) se asumirá además que
el punto de consumo está suficientemente alejado del río, por lo que su longitud
será aproximadamente constante, con independencia de la localización de la
turbina. De esta manera, el coste de la instalación solo será sensible al trazado en
lo que a la tubería respecta, de manera que podemos obviar los términos
constantes y trabajar con el coste de la tubería forzada como variable de interés.
El coste típico de una conducción de este tipo es proporcional a la longitud y al
cuadrado del diámetro, lo cual se puede escribir de la siguiente forma:
Para tener en cuenta el coste de instalar muchos codos sin complicar en exceso la
formulación, haremos la siguiente consideración: cada codo que se instale
equivaldrá a una cierta longitud adicional de tubería, que denominaremos
mediante λ. Así, si llamamos nc al número total de codos, la función de coste se
transforma en:
Por lo tanto, ya podemos introducir estas ecuaciones en nuestro algoritmo para
determinar las prestaciones de nuestra planta en función de las variables que
hemos calculado antes (Hg, Lt f u nc):
Ya podemos escribir una función, que llamaremos validaPlanta, que evalúe las
prestaciones de la planta correspondiente a un individuo. En esta función
incluiremos el modelo que acabamos de presentar, así como todas las constantes
necesarias (diámetro de la tubería, densidad del agua, constante de fricción, etc).
La función devolverá, además del coste y potencia de la planta correspondiente
al individuo, una variable binaria que indicará si la potencia generada cumple o
no con el valor mínimo exigido Pmin:
Factibilidad de la planta
A continuación, plantearemos una estrategia para definir cuándo una solución es
factible y cuándo no, de manera que en la búsqueda de la solución óptima solo
consideremos aquellos trazados cuya construcción sea posible. Así, definiremos
la factibilidad de la planta en función de su adaptación al perfil del río. En
particular, vamos a considerar que una solución es factible si su trazado no se
separa del terreno más de una cierta distancia. Definiremos esta distancia
máxima en función de la altura límite de soportes que podamos instalar en la
tubería forzada, en el caso de que esta discurra por encima del terreno, y de la
profundidad máxima que consideramos que puede realizarse para enterrar la
tubería, en el caso contrario.
Como hemos considerado que conocemos el perfil del río exclusivamente en N
puntos, serán estos en los que verificaremos las condiciones de factibilidad. Así,
consideraremos que una solución es factible si -y solo si- todos los puntos del
trazado que representa cumplen las condiciones de factibilidad. Si llamamos εi a
la diferencia entre el trazado de la tubería forzada, Γ(s), y el terreno, ϕ(s), en el
punto i:
εi = Γ(si) – ϕ(si).
Podemos escribir las condiciones de factibilidad como:
–εexc ≤ εi ≤ εsop,
donde hemos introducido εexc y εsop para referirnos a la máxima profundidad
excavable y a la máxima altura de soportes aceptable, respectivamente. Para
determinar los valores de las diferencias de altura εi en nuestro algoritmo
podemos utilizar:
Cabe destacar que se ha utilizado la función interp1d2 del módulo interpolate de
la librería scipy, para realizar una interpolación lineal de los puntos
seleccionados por los individuos. Como resultado se crea la función de
interpolación trazado.
Ya podemos escribir una función, que llamaremos validaTrazado, que
compruebe si el trazado correspondiente a un individuo cumple correctamente
estas restricciones de factibilidad:
6.3 Problema con un objetivo: Minimizando el coste
de instalación
Puesto que el objetivo principal de nuestro problema es el de encontrar el
trazado de la planta más económico, resulta lógico plantear el coste de la misma,
C, como la función de fitness o función objetivo. Asímismo, se requerirá que la
potencia generada, P, sea superior o igual a un determinado valor mínimo Pmin y
que el trazado correspondiente sea factible:
La complejidad de este problema es NP-duro, ya que no estamos ante un
problema de decisión, en el que comprobar si se dan una serie de condiciones
(potencia mínima requerida, restricciones en los soportes), sino que estamos
buscando la solución óptima en términos de coste. Considerando N puntos
posibles del trazado del río, el número de soluciones posibles a evaluar es 2N, lo
que supone una complejidad exponencial con el número de puntos considerados.
En el caso propuesto, se tendrían 2 **100 posibles soluciones, lo cual es
inabordable mediante un algoritmo exhaustivo3.
En los siguientes epígrafes introduciremos todos los componentes del algoritmo
genético necesarios para resolver el problema.
6.3.1 Definición del problema y generación de la población inicial
Estamos en posición de poder definir el problema en deap. Para ello,
utilizaremos las siguientes líneas de código:
Como se puede observar, hemos definido el problema como uno de
minimización en el que los individuos vendrán estructurados en listas de N
componentes. A continuación, empezaremos definiendo el algoritmo de
generación, que no es más que el conjunto de reglas que permitirán crear de
forma aleatoria los individuos que conformarán la población inicial. Como
hemos visto anteriormente, una estrategia de generación adecuada permitirá al
algoritmo genético realizar una buena exploración en las primeras generaciones.
El objetivo de la generación, no solo consiste en generar individuos de forma
aleatoria, sino que además debe hacerlo, en la medida de lo posible, evitando que
estos violen las restricciones del problema. De esta manera se garantiza una
buena exploración durante las primeras generaciones.
En el problema que estamos estudiando, es evidente que las restricciones de
factibilidad son especialmente severas y, a priori, es bastante improbable que los
individuos generados las satisfagan de forma trivial. Por ello, planteamos la
siguiente estrategia para garantizar la factibilidad de los individuos generados:
1. Generamos un individuo de tamaño N sin ningún nodo; es decir, con todos los
genes a 0.
2. Seleccionamos aleatoriamente dos enteros entre 0 y N – 1, y hacemos 1 el gen
correspondiente a cada una de estas posiciones.
3. Hacemos 1 todos los nodos contenidos en el intervalo que definen los dos
anteriores.
Así, los individuos generados (ver Figura 6.5) cumplirán de forma trivial las
restricciones de factibilidad, ya que todos los valores de ε serán cero. Notemos
que estas soluciones, en general, van a estar lejos del óptimo, ya que consistirán
en instalaciones con un número innecesariamente alto de codos. Por lo tanto,
estamos priorizando partir de soluciones lejanas al óptimo pero válidas. El
trabajo de los operadores genéticos consistirá en guiar a la población inicial
hacia la solución óptima.
Figura 6.5. Estrategia para generar individuos aleatorios que satisfagan las
restricciones de factibilidad.
Así, creamos la caja de herramientas y registramos las funciones necesarias para
generar la población inicial:
Como ha pasado en la mayoría de los problemas que hemos visto, la función
crea_individuo crea el cromosoma completo del individuo. Por ello, la
registramos utilizando tools.initIterate.
6.3.2 Operadores genéticos
En esta sección, definiremos las operaciones de cruce y mutación. Recordemos
que estas son las reglas heurísticas que regulan la modificación de la
información genética de los individuos. A continuación, definiremos una función
de fitness, que no es más que la función que utiliza el algoritmo para cuantificar
la calidad de cada individuo.
Para la operación de cruce, vamos a utilizar un esquema de dos puntos, el cual
ya fue explicado en el Capítulo 1. Recordemos que este método consiste en
seleccionar de forma aleatoria dos puntos entre 0 y N – 1 e intercambiar entre los
dos progenitores el fragmento de información genética contenido en este
intervalo. Para este problema, una posible alternativa a este operador podría ser
el operador de cruce uniforme.
Para la operación de mutación, vamos a utilizar un operador personalizado, que
funcionará igual que el mutFlipBit que estudiamos en el Capítulo 4, pero con
una pequeña modificación que ayude a disminuir el número de nodos de nuestro
problema. Puesto que nuestro propósito es obtener trazados con un bajo número
de nodos, sumado a que hemos propuesto un esquema de generación que
previsiblemente va a crear individuos con un alto número de nodos, nuestro
operador de Bit-Flip Modificado tendrá una probabilidad diferente para mutar un
gen según su valor. En particular, haremos que la probabilidad de que un 1 se
convierta en 0 sea mayor que la probabilidad de que un 0 se convierta en un 1, es
decir:
p0→1 < p1→0
Así registramos dicho operador en nuestra caja de herramientas:
La elección de este operador de mutación se justifica en dos aspectos. En primer
lugar, puesto que el objetivo del problema es minimizar el coste, nos interesa
incentivar la reducción del número de nodos en los individuos. En segundo
lugar, dado el esquema de generación de individuos propuesto, es de esperar que
en las primeras generaciones los individuos tengan un alto número de nodos.
En cuanto al mecanismo de selección, se ha utilizado la selección mediante
torneo. El torneo consiste en escoger un número determinado de individuos de la
última generación del algoritmo, comparar sus valores de función objetivo y, por
último, escoger aquella con menor valor.
6.3.3 Función objetivo o de fitness
La función de fitness recibe un individuo como entrada, lo evalúa y devuelve un
determinado valor de calidad. Como sabemos, para evitar que soluciones con un
buen valor de calidad pero no factibles sean consideradas, la función de fitness
asigna un valor de penalización (pena de muerte). Así, la función objetivo que
vamos a escribir devolverá el coste de la planta cuando el individuo sea factible,
pero devolverá un valor sustituto (un coste muy alto) en el caso de que no lo sea,
con independencia del coste que tenga realmente. Esto se puede implementar
fácilmente mediante:
Podemos ver cómo la función fitness aplica el mecanismo de pena de muerte
(deteniendo la ejecución de la función) cuando alguna de las restricciones no se
cumple para el individuo bajo evaluación. Cuando todas las restricciones son
satisfechas, se calcula el coste de la planta y se devuelve como salida.
Debemos tener en cuenta que, para lograr un correcto funcionamiento posterior de la librería d
Para registrar esta función en la caja de herramientas utilizaremos el siguiente
comando:
Observe que, en este caso, se ha definido el fichero donde se guarda la
información del perfil del río como entrada a la función objetivo.
6.3.4 Ejecución del algoritmo
Llegado a este punto, ya podemos lanzar el algoritmo y empezar a obtener
resultados. Necesitamos definir primero una serie de parámetros relativos a la
ejecución:
■Número de generaciones ( NGEN ): Fijaremos su valor en 100. Este parámetro
se puede variar en función de lo observado en la gráfica de convergencia.
■Número de individuos padres en una generación ( MU ): Fijaremos el tamaño
de la población. Establecemos 4000 para esta prueba.
■Número de individuos descendientes en una generación ( LAMBDA ): Se fija
el tamaño de descendentes en cada nueva generación. Se fija con un valor igual a
4000.
6.3.5 Resultados obtenidos
Ya estamos listos para lanzar nuestro algoritmo genético y obtener resultados.
Para ello llamaremos a la función unico_objetivo_ga, cuyos argumentos son las
probabilidades de cruce, pcx, y de mutación, pmut. Para este problema queremos
estudiar diferentes combinaciones de estas dos probabilidades, por lo que
utilizaremos el siguiente fragmento de código:
Observemos que, análogamente al capítulo anterior, hemos anidado dos bucles
for, con la finalidad de fijar los parámetros c y m por una parte, y llamar 10
veces al algoritmo, por otra. Generamos, además, dos ficheros de texto:
individuos_turbina.txt y fitness_turbina.txt, para almacenar los mejores
individuos de cada llamada al algoritmo, y los valores de la función objetivo
obtenidos, respectivamente.
Por último, antes de ejecutar la secuencia de llamadas a nuestro algoritmo,
lanzaremos un caso simple para asegurarnos de que los valores de NGEN, MU y
LAMBDA son apropiados para el problema (y permiten que algoritmo llegue a
converger) o si, por el contrario, necesita un mayor número de generaciones.
Para ello haremos lo siguiente:
Tras su ejecución, representaremos la gráfica de convergencia del algoritmo.
Así, obtenemos el resultado que se muestra en la Figura 6.6:
Cabe destacar que el máximo en cada iteración es muy elevado debido a la pena
de muerte que se aplica. Una vez comprobamos que se alcanza la convergencia
del algoritmo, ya podemos lanzar la batería de ejecuciones planificada
anteriormente y, después, analizar los resultados obtenidos. La Tabla 6.1 muestra
los resultados obtenidos. Se observa que el mejor resultado se obtiene utilizando
una probabilidad de cruce del 60% y una de mutación del 40%, aunque los
resultados son similares en las cuatro combinaciones.
Figura 6.6. Gráfica que muestra la convergencia del algoritmo.
Tabla 6.1. Resultados obtenidos para el problema de optimización de la planta
micro-hidráulica.
Los Textos 6.1 y 6.2 muestran un fragmento del contenido de los ficheros de
texto individuos_turbina.txt y fitness_turbina.txt tras la ejecución de la batería de
resoluciones respectivamente.
Texto 6.1. Fragmento del archivo individuos_turbina.txt tras ejecutar la batería
de pruebas.
Texto 6.2. Fragmento del archivo fitness_turbina.txt tras ejecutar la batería de
pruebas.
Tras comprobar que la ejecución ha sido exitosa, podemos localizar y estudiar la
mejor solución obtenida, que se ha resumido a continuación:
Tabla 6.2. Mejor solución obtenida para el problema de optimización de la planta
microhidráulica.
Podemos además utilizar la función dibujaSolucion, definida anteriormente, para
representar el individuo, como se muestra en la Figura 6.7. Cabe destacar que la
solución obtenida permite generar una potencia de 7.05 kW (ligeramente
superior a los 7 kW mínimos exigidos), turbinando un caudal de 13.13 L de agua
por segundo. El coste total de la planta es es de aproximadamente 28 k , y la
tubería requerida mide 221.21 metros, con un total de 7 codos. Se comprueba,
además, observando la representación gráfica de la solución, que esta aprovecha
el tramo de máxima pendiente del terreno.
Figura 6.7. Representación del trazado óptimo.
6.4 Problema con múltiples objetivos: Minimizando el
coste de instalación y maximizando la potencia
generada
Una vez resuelto el problema de optimización con un único objetivo (la
minimización del coste de la planta), nos podemos plantear la optimización
multiobjetivo, en la que dos o más objetivos competitivos son considerados
simultáneamente en la optimización.
Así, definiremos el objetivo del problema como la minimización del coste, C, y
la maximización simultánea de la potencia, P. Está claro que, dada la naturaleza
de estas dos variables, la una no puede mejorar sin implicar un empeoramiento
en la otra, por lo que las soluciones que obtendremos representarán un
compromiso entre ambos objetivos. De esta forma obtendremos un análisis más
exhaustivo del diseño de la planta, del cual podremos extraer información
interesante.
6.4.1 Definición del problema, población inicial y operadores genéticos
Analogamente al caso con un único objetivo, definimos el problema en deap y
registramos las funciones necesarias en la caja de herramientas:
Observamos cómo, en este caso, los pesos (weights) definidos en nuestro
problema son (1.0,-1.0,), dado que pretendemos minimizar el coste de la
instalación mientras que maximizamos la potencia aportada por la misma. Por
otro lado, la operación de selección se realiza a través del tools.selNSGA2, ya
que nuestro objetivo es obtener el frente de Pareto del problema. No hay ningún
cambio con respecto a los operadores genéticos. La función objetivo que se ha
registrado (fitness_function_multiobjetivo) se definirá a continuación.
6.4.2 Función objetivo o de fitness
La función de fitness se define ahora como una tupla con dos elementos, la
potencia generada y el coste de la planta. Análogamente al caso de un único
objetivo, se utilizará la pena de muerte para penalizar aquellos individuos que
representen soluciones que no sean factibles. La función
fitness_function_multiobjetivo queda, por tanto:
Podemos ver que esta función es idéntica a la del caso en el que se consideraba
un único objetivo pero, esta vez, devuelve, además del coste de instalación, la
potencia dada por el sistema.
6.4.3 Ejecución del algoritmo
Para llamar al algoritmo utilizaremos multi_objetivo_ga, que definimos de la
siguiente manera:
Es importante mencionar que no se han realizado cambios con respecto al
tamaño de la población y al número de generaciones del algoritmo genético.
6.4.4 Resultados obtenidos
Podemos ejecutar el algoritmo llamando a multi_objetivo_ga; esto es:
Observe cómo hemos establecido las probabilidades de cruce y mutación como c
= 0.6 y m = 0.3, ya que fue la configuración con la que mejores resultados
obtuvimos en el caso de un único objetivo.
En la Figura 6.8 se muestra el frente de Pareto obtenido con el algoritmo
genético de optimización multiobjetivo. Como era de esperar, los individuos
obtenidos se corresponden con diferentes soluciones de compromiso entre la
potencia generada y el coste de la planta, y podemos comprobar que no es
posible incrementar la potencia sin incrementar el coste, y al contrario.
Observando el frente de Pareto, es interesante comprobar que se puede apreciar
una cierta tendencia lineal. Esta puede interpretarse como un coste marginal,
aproximadamente constante, de incrementar la potencia instalada. Este coste
marginal se puede estimar como la pendiente de la recta de tendencia. Si
calculamos Esta mediante mínimos cuadrados para las soluciones más próximas
a la solución de menor coste, obtenemos un valor de m = 2670 por kW adicional.
Figura 6.8. Frente de Pareto Potencia-Coste del problema de optimización (la
tendencia lineal se ha representado en rojo).
6.5 Código completo y lecciones aprendidas
Para finalizar este capítulo, hagamos un repaso del procedimiento realizado para
resolver el problema de diseño de la planta micro-hidráulica utilizando un
algoritmo genético discreto. Usaremos como referencia el programa completo,
que se muestra en el Código 6.3.
1. Líneas 1-10: Primero es necesario incluir todas las librerías que utilizaremos
en nuestro código. Cabe destacar el módulo interpolate para definir la curva que
representa el trazado de la planta.
2. Líneas 12-15: Definimos una variable binaria para indicar si abordamos el
problema en modo multiobjetivo o monoobjetivo, y definimos además las
probabilidades de mutación y cruce.
3. Líneas 17-39: A continuación, definimos la función de generación de
individuos. Habíamos escogido una estrategia de generación que evitase crear
individuos no factibles, para mejorar la exploración durante las primeras
generaciones.
4. Líneas 41-50: Definimos el operador de mutación, consistente en cambiar el
valor binario de ciertos genes.
5. Líneas 52-80: Aquí definimos la función de evaluación del trazado, que
servirá para determinar si el trazado es factible o no.
6. Líneas 82-114: Definimos la función de evaluación de la planta, que nos
permitirá determinar, en caso de que esta sea factible, las variables relativas a sus
prestaciones (potencia y coste).
7. Líneas 116-128: Definimos ahora la función de fitness para el caso
monoobjetivo. Notemos que esta función llama a las dos funciones de
evaluación anteriores.
8. Líneas 130-142: Definimos también la función de fitness para el caso
monoobjetivo. De nuevo podemos comprobar que esta función necesita llamar a
las dos funciones de evaluación anteriormente descritas.
9. Líneas 144-169: Creamos la función de ejecución de nuestro algoritmo
genético monoobjetivo, que requiere la especificación de dos argumentos: la
probabilidad de cruce y la de mutación. Aquí definimos además el número de
generaciones, de mu y de lambda .
10. Líneas 171-187: Aquí hacemos lo mismo para el caso multiobjetivo.
11. Línea 190: Con un condicional de tipo i f determinamos cuál es el modo que
se desea resolver (indicado a través de la variable multi que definimos en la línea
13).
12. Líneas 192-214: Se crea el problema monoobjetivo, declarando todas las
funciones necesarias. Creamos el problema (línea 214) y el individuo (línea
217), y creamos además el toolbox correspondiente, en el que registramos las
operaciones de generación de individuos y población (líneas 223 y 225), la
función de fitness (línea 229), y los operadores de selección, mutación y cruce
(líneas 231-233).
13. Líneas 218-240: Se crea el problema multiobjetivo de forma análoga al
monoobjetivo, declarando las funciones necesarias. Para ello creamos el
problema (línea 240), el individuo (línea 243), y el toolbox (línea 246). Por
último, incorporamos en el toolbox las operaciones de generación de individuos
y población (líneas 249 y 251), la función de fitness multiobjetivo (línea 255), y
los operadores de selección, mutación y cruce (líneas 257-259).
14. Líneas 242-246: Lanzamos la ejecución del problema en el modo
correspondiente.
Código 6.3. Código final desarrollado para el problema de optimización de una
planta microhidráulica.
En cuanto a las lecciones aprendidas, este capítulo ha consistido en la aplicación
de un algoritmo genético para resolver un problema real de ingeniería, basado en
diseñar de forma óptima el trazado de una planta microhidráulica, de forma que
los recursos naturales del terreno se usen de la forma más eficiente posible. El
problema, además, se ha abordado de dos formas:
■En primer lugar, se ha determinado el diseño óptimo de la planta microhidráulica para un nivel de generación fijado, de manera que se ha obtenido el
trazado que resulta en el menor coste posible.
■En segundo lugar, se ha incrementado la complejidad del problema y se ha
resuelto la optimización considerando simultáneamente la maximización de la
potencia. Esto ha permitido evaluar no solo el trazado óptimo para satisfacer la
generación de potencia mínima, sino que además hemos obtenido información
acerca de cómo variaría el coste de la planta si el nivel de potencia requerida se
incrementase, proporcionando un mejor análisis del verdadero potencial del
recurso natural estudiado.
■Con respecto al diseño del algoritmo genético, se ha destacado cómo en
problemas con restricciones complejas, es necesario estudiar una estrategia de
generación de individuos (población inicial) apropiada. En este problema se ha
optado por generar soluciones iniciales válidas aunque poco competitivas en
cuanto al fitness , con la idea de mejorarlas en las distintas generaciones del
algoritmo. Esta técnica puede hacer que el algoritmo necesite más generaciones
para converger, pero en nuestro problema merece la pena.
■Se ha diseñado un método de mutación ajustado al problema, teniendo en
cuenta, además, el punto de partida de las soluciones iniciales. Así, se ha optado
por hacer más probable que un gen se haga 0 que que el mismo gen se haga 1,
considerando que las soluciones iniciales tendrán gran cantidad de unos
consecutivos.
Los resultados han demostrado la capacidad de las estrategias evolutivas para
resolver problemas discretos de ingeniería con difícil o imposible resolución
analítica, utilizando un conjunto de ecuaciones sencillas y tiempos de
computación relativamente bajos.
6.6 Para seguir aprendiendo
El problema estudiado en este capítulo representa una versión sencilla de un
problema de ingeniería complejo y, como tal, puede abordarse desde un punto de
vista más ambicioso. Por ejemplo, podríamos incluir el valor del diámetro de la
tubería como variable de diseño. ¿Cómo podríamos introducir esta nueva
variable en el cromosoma? También podemos considerar que los nodos podrían
ocupar cualquier posición del terreno y no solo en puntos discretos. ¿Podemos
considerar una interpolación de los puntos para formular el problema de forma
continua?
Para profundizar más en este problema y en sus posibles aplicaciones
recomendamos la lectura de los siguientes trabajos:
■En (Tapia et al., 2018) se aborda un problema similar al de este capítulo
utilizando programación entera.
■En (Tapia et al., 2019) se comparan los resultados del algoritmo basado en
programación entera y un algoritmo genético. Los resultados demuestran que el
algoritmo genético es capaz de conseguir mejores resultados.
■El problema se amplía en (Tapia et al., 2020a) considerando el coste de la
instalación eléctrica necesaria.
■Por último, en (Tapia et al., 2020b) se propone un algoritmo genético con
cromosomas de longitud variable, también llamados Messy Algorithms
(Goldberg et al., 1989). Además, el problema se resuelve utilizando variables
continuas en vez de variables discretas, por lo que el problema aumenta en
complejidad, pero también permite obtener mejores resultados. Información
detallada de todos los trabajos anteriores se puede encontrar en (Tapia, 2019).
Como problemas se plantean los siguientes:
■Realice una comparación entre los resultados de aplicar el cruce de un punto, el
cruce de dos puntos y el cruce uniforme.
■Una vez obtenido el operador de cruce adecuado, realice un barrido de las
probabilidades indpb_01 e indpb_10 para obtener los valores más adecuados.
Compare los resultados obtenidos con el operador de mutación mutFlipBit
incluido en deap .
■Aumente la potencia requerida, P min , para que una solución sea válida y
compruebe que, a media que se requiere más potencia, el trazado de las
soluciones aumenta.
_________________
1https://docs.scipy.org/doc/numpy/reference/generated/numpy.nonzero.html
2https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp1d.html
3Realmente, considerando las restricciones impuestas el número total de
posibles soluciones es algo inferior, pero igualmente elevado como para utilizar
un algoritmo de fuerza bruta.
7.1 Introducción
El posicionamiento óptimo de sensores es un problema recurrente en cualquier
tipo de instalación (Chmielewski et al., 2002). Por ejemplo, considere un campo
de cultivo que es regado periódicamente a través de canales de agua. Para
asegurar un correcto funcionamiento del sistema de riego, es necesario tomar
medidas de la presión del agua en diferentes puntos de la instalación. Dichas
medidas pueden ser tomadas mediante sensores inalámbricos. Los sensores
envían las medidas a puntos inalámbricos de conexión que las almacenan y, a su
vez, envían las medidas a un sistema de monitorización y control centralizado.
Como se puede observar, el posicionamiento de los puntos inalámbricos de
conexión tiene una importancia notable para el correcto funcionamiento del
sistema. Su mal posicionamiento puede ocasionar la falta de medidas de alguno
de los sensores y, consecuentemente, el posible fallo del sistema de riego sin la
correspondiente percepción del mismo.
Otro problema similar se da en relación al posicionamiento de torres de
telecomunicaciones para dar cobertura a los usuarios. De la misma forma,
podemos pensar en el posicionamiento óptimo de puntos de acceso Wi-Fi para
dar cobertura de manera eficiente al mayor número de usuarios posible (Eldeeb
et al., 2017; Reina et al., 2013).
Así, este capítulo pretende plantear y resolver el problema del posicionamiento
de un conjunto de puntos de conexión inalámbricos con el fin de cubrir el
máximo número posible de puntos de interés en una superficie. La formulación
formal del problema, así como su planteamiento y su resolución, serán tratados
en los siguientes apartados.
Antes de introducir dichos desarrollos, es importante destacar que este problema
se enmarca, dentro de la teoría de complejidad computacional, como un NPduro. Este concepto ya fue introducido en el Capítulo 2, sección 2.10, donde se
ahondó más en estos conceptos. Aquí, simplemente diremos que un problema
NP-duro es aquel para el cual pueden existir algoritmos o procedimientos que
pueden resolverlo pero que no son deterministas o requieren un tiempo
exponencial (y, por consiguiente, no polinómico).
7.2 Formulación del problema
Consideraremos una superficie en la que existen cierto número de puntos de
interés distribuidos. Para facilitar el problema, supondremos que la superficie
objeto de estudio es rectangular. De tal manera, podemos definirla mediante su
altura y su anchura. En este caso optaremos por una superficie cuadrada de 400
hectáreas, es decir, una superficie de 2000 metros de ancho y 2000 metros de
altura. En dicha superficie, existirán un total de 75 puntos de interés1. Para
generar los puntos de interés utilizaremos el siguiente código:
Cabe destacar un aspecto importante de dicho fragmento de código. Como se
puede observar, utilizamos una semilla de la librería random2. Dicha semilla
permitirá que, al generar los puntos de interés de forma aleatoria, se obtengan los
mismos valores. Así, siempre que ejecutemos nuestro algoritmo para resolver el
problema, estaremos trabajando sobre el mismo escenario; de esta manera,
podremos comparar los resultados obtenidos para distintas versiones o
modificaciones del algoritmo genético (Likas et al., 2003).
Ejecutando el script anterior obtenemos la Figura 7.1. Se puede ver que tenemos
una distribución uniforme de puntos de interés en el escenario. Otro problema
distinto, que no se abordará en el libro, es considerar distintos clusters de puntos
de interés en el escenario. Si el lector no se encuentra familiarizado con el
concepto de cluster, diremos que es un conjunto de nodos agrupados por una
métrica de similitud, que en este caso y por ejemplo puede ser la distancia entre
ellos.
Con el fin de cubrir en cobertura los puntos de interés generados, contamos con
varios puntos de conexión inalámbrica. Dichos puntos de conexión, tienen una
cobertura circular que les permiten cubrir puntos de interés situados a una
distancia igual o menor a 100 metros a su alrededor. De esta manera, si un punto
de interés se sitúa a una distancia menor o igual a 100 metros respecto al punto
de conexión, diremos que este se encuentra cubierto. En caso contrario, diremos
que está fuera del alcance. Este modelo para definir el alcance del punto de
conexión (también denominado modelo de propagación) se conoce como
modelo de disco y, aunque simple, se utiliza en un gran número de casos. Es
posible utilizar modelos de propagación más complejos, como por ejemplo el
modelo de dos rayos (Bacco et al., 2014) o modelos multitrayectos (Tsai, 2008).
Dichos modelos solo variarían el área de cobertura de los nodos inalámbricos,
pero el problema sería idéntico al expuesto en este capítulo3. En la Figura 7.2 se
muestran los puntos de interés considerados y una posible localización de un
punto de conexión con su correspondiente área de cobertura. Para generar dicha
figura basta con incluir al anterior script las siguientes líneas:
Figura 7.1. Superficie considerada y puntos de interés.
Figura 7.2. Cobertura cubierta por un punto de conexión situado en (500,1000).
Así, si nos fijamos en la Figura 7.2 podemos observar cómo conseguimos cubrir
dos puntos de interés mediante la introducción de un único punto de conexión
inalámbrico en las coordenadas x = 500 e y = 1000.
Una vez introducidos los conceptos necesarios para plantear el problema,
procedemos a formularlo matemáticamente. Para ello, es necesario definir la
condición que indica si un punto de interés dista menos de 100 metros de uno de
conexión. Así, serán (xi,yi) las coordenadas de un punto de interés cualesquiera
y serán las coordenadas de un punto de conexión inalámbrica. Entonces, el punto
de interés se encuentra cubierto por dicho punto de conexión si -y solo si-:
donde di–c representa la distancia euclídea entre ambos puntos. Así, podemos
generar una función en Python que nos permita determinar si un punto de interés
se encuentra cubierto o no por uno de conexión. Para ello, utilizaremos el
siguiente fragmento de código:
Nuestro problema puede ser definido como, dados 75 puntos de interés (xi,yi
para i = {1,...,75}), determinar las coordenadas de los 50 puntos de conexión
inalámbrica para c = {1,...,50}), de tal manera que se cubra en cobertura el
mayor número posible.
Para ello, cada uno de los individuos considerados estará compuesto por 100
genes que corresponderán a las coordenadas (x e y) de cada uno de los puntos de
conexión inalámbrica. Una representación gráfica de la estructura del individuo
considerado se puede observar en la Figura 7.3.
Figura 7.3. Estructura de cada individuo considerado en el problema.
Es importante destacar que la única restricción de nuestro problema será el
posicionamiento de los puntos de conexión dentro del área objeto de estudio.
Así, podemos definir una función que evalúe la posición de un punto de
conexión y devuelva un valor indicando si se encuentra dentro o fuera de dicha
área. El código presentado a continuación evalúa la pertenencia o no del punto
de conexión al área definida.
En las siguientes secciones se definirá la estrategia que adoptaremos para la
resolución del problema, así como las consideraciones más importantes que hay
que tener en cuenta con el fin de obtener una solución apropiada que cumpla
nuestras expectativas.
7.3 Problema con un objetivo: Maximizando el
número de puntos cubiertos
En este apartado resolveremos el problema planteado a lo largo del capítulo. Sin
embargo, aún es necesario definir formalmente cuál es el objetivo de nuestro
algoritmo. Como hemos mencionado anteriormente, la finalidad del algoritmo
será la de cubrir en cobertura el máximo número posible de los 75 puntos de
interés considerados mediante el posicionamiento óptimo de 50 puntos de
conexión inalámbricos. Así, se podrá cuantificar la optimalidad de la solución
mediante un número entero que variará entre 0 y 75 atendiendo al número de
puntos de interés cubiertos.
El problema con un único objetivo que queremos resolver se puede definir
matemáticamente como:
Es importante destacar la imposibilidad de resolver dicho problema mediante
métodos de resolución convencionales, debido al carácter altamente no lineal y
al uso de variables tanto discretas como continuas en nuestro problema.
7.3.1 Definición del problema y generación de la población inicial
Una vez definido el problema que deseamos resolver, estamos en posición de
desarrollar las funciones que necesitará nuestro algoritmo para llevar a cabo las
operaciones genéticas pertinentes. Así, será necesario definir una función que
provea al algoritmo de individuos iniciales, es decir, individuos que defina una
distribución inicial de los puntos de conexión inalámbricos. Posteriormente,
mediante operaciones genéticas, dichos individuos irán cambiando hasta lograr
una configuración óptima. Estos cambios se producirán como consecuencia de
operaciones genéticas de mutación y cruce o crossover.
De esta manera, lo primero que debemos hacer para poner en marcha lo que
serán las bases del algoritmo es definir el problema y la caja de herramientas o
toolbox en la cual registraremos las operaciones genéticas a utilizar:
Como podemos observar, se trata de un problema de maximización; de ahí el
peso positivo en el atributo weights en la creación del problema. Como siguiente
paso, registramos en la caja de herramienta las funciones que realizarán las
operaciones genéticas definidas anteriormente.
Definir la población inicial que considerará el algoritmo evolutivo, es un punto
crítico que marcará la calidad de la solución otorgada por el mismo. En este
caso, y a diferencia de problemas introducidos en capítulos anteriores, el
problema se encuentra poco restringido. La única restricción a considerar es que
todos los puntos deben estar incluidos en la superficie considerada.
De esta manera, una posible función para crear individuos es la que se presenta
en el siguiente fragmento de código:
En este caso hemos optado por asignar valores aleatorios siguiendo una
distribución uniforme para cada valor de x y de y de cada punto de conexión.
Cabe recordar que una distribución uniforme es aquella definida por dos
extremos a y b en la que la variable aleatoria solo puede tomar valores
comprendidos entre a y b, de manera que todos los intervalos de una misma
longitud (dentro de a y b) tienen la misma probabilidad.
Al igual que hemos comentado en otros capítulos, esta función se presenta como
una posible candidata y no como la única posible para el problema. De hecho, si
se tomara en consideración más información sobre la localización de los puntos
de interés, tal vez se podría optar por otra manera de generar la población inicial,
tratando de situar los puntos de conexión lo más cerca posible de los puntos de
interés (veremos un ejemplo de esta posibilidad más adelante), por ejemplo, si
consideramos que los puntos de interés están agrupados en clusters. Otra técnica
posible es utilizar diagramas de Voronoi para dividir el escenario. Estos
conceptos no se explicarán en el libro y simplemente se mencionan con el fin de
despertar la curiosidad del lector.
Así, registramos las funciones pertinentes para generar la población inicial de
nuestro problema:
Observe que para la creación de individuos se utiliza la herramienta
tools.initIterate, ya que generaremos individuos de forma aleatoria y no
únicamente genes.
7.3.2 Operadores genéticos
Una vez definidos el problema y la función que nos proveerá de individuos
iniciales, es el momento de declarar las operaciones genéticas que utilizaremos.
Primero, registramos la función de cruce que, al igual que en capítulos
anteriores, se tratará del cruce cxBlend:
Con el fin de realizar pequeñas modificaciones en un individuo para evaluar si
mejora o no la función objetivo a minimizar, utilizamos un operador de mutación
Gaussiana acotado. Este operador es similar al de mutación Gaussiana que
hemos visto anteriormente, pero trunca el valor del gen mutado si se excede de
un rango.
Registramos el operador de mutación en nuestro toolbox:
En cuanto al algoritmo de selección, nuevamente utilizaremos la selección por
torneo:
A continuación, será declarada la función objetivo de nuestro problema, y se
mostrarán y analizarán los resultados obtenidos.
7.3.3 Función objetivo
Para registrar la función objetivo en nuestro toolbox utilizaremos la siguiente
línea de código:
Para generar la función objetivo o de fitness será necesario evaluar cada una de
las funciones presentadas en los fragmentos de código introducidos a lo largo del
capítulo en el orden adecuado. Así, con el fin de que la función sea lo más
eficiente posible, parece lógico evaluar primero si el individuo cumple las
restricciones del problema. De esta forma, en caso de no cumplir, sería posible
descartar directamente la solución sin necesidad de evaluar el número de puntos
de interés cubiertos. En consecuencia, parece claro que la evaluación de estas
funciones de forma ordenada nos permitirá un mayor rendimiento computacional
del algoritmo. De esta manera, se propone la siguiente función objetivo:
Es importante observar la estructura de la función de fitness. Así, primero se
evalúa si el punto de conexión se encuentra dentro de la región admisible. En
caso de que lo esté, se procede a evaluar qué puntos de interés cubre. Entonces,
para cada punto de interés, primero se observa si ya ha sido cubierto y, en caso
de que no lo haya sido, evaluamos su proximidad. En caso de que cubra un
punto de interés, escribirá en el vector pdi_vector un uno en la componente
correspondiente.
Hay que tener en cuenta que la variable penaliza hace referencia a la pena de
muerte cuando se desea descartar una de las soluciones. En este caso,
planteamos un problema de maximización ya que deseamos maximizar el
número de puntos de interés cubiertos por los puntos de conexión. De esta
manera, el valor de la variable penaliza debe ser negativo. Observe que un valor
negativo de la función de fitness es peor que cualquier solución válida al
problema. Por ejemplo, la situación más desfavorable sería no cubrir ningún
punto de interés, en cuyo caso obtendríamos un cero.
No hay que olvidar que para un correcto funcionamiento posterior de la librería deap, es un re
A continuación, veamos cómo llamar al algoritmo genético que resolverá nuestro
problema.
7.3.4 Ejecución del algoritmo
Una vez definidas todas las partes necesarias del programa, estamos en situación
de poder ejecutar el algoritmo y analizar los resultados obtenidos. En este caso, y
al igual que realizamos en casos anteriores, optaremos por utilizar un algoritmo
µ +λ. Para ejecutar el algoritmo es necesario definir una serie de parámetros que
se encuentran expuestos a continuación:
■NGEN : Fijaremos su valor en 700. Este parámetro se puede variar en función
de lo observado en la gráfica de convergencia.
■MU : Fijamos el tamaño de la población. Establecemos 300 para esta prueba.
Teniendo en cuenta que el cromosoma tiene una longitud de 50, es un valor
razonable para empezar.
■LAMBDA : Se fija el tamaño de descendientes en cada nueva generación. Se
fija con un valor igual a 300. Como hemos fijado µ = λ , el tamaño de la
población permanece constante a lo largo de las generaciones.
Como parámetros de entrada a la función se usará la probabilidad de cruce (c) y
mutación (m). Además, se utilizará la caja de herramientas o toolbox, que es
necesario definirla como una variable global. La función de llamada al algoritmo
se presenta a continuación:
Como se puede observar, esta función inicializa el problema y realiza una
llamada al algoritmo basado en la configuración adoptada en el toolbox. Se
establece un registro de los valores mínimos, máximos y medios de lo devuelto
por la función de fitness, así como de la desviación típica de dichos valores para
todos los individuos de la cada generación. Observe, además, que al usar un
indviduo de tipo np.ndarray, en el objeto HallOfFame, es necesario introducir la
sentencia similar = np.array_equal. Esto es así porque la comparación entre listas
y arrays de numpy no se realizan de igual manera (ver Apéndice A).
El script tarda en ejecutarse alrededor de 1000 segundos. Este tiempo de
ejecución ha sido medido tras ejecutar el algoritmo en un Intel Core i5-72000U a
2.5 GHz con 8 Gb de memoria RAM.
7.3.5 Resultados obtenidos
Podemos ejecutar nuestro algoritmo mediante una llamada a la función
unico_objetivo_ga a través de la siguiente línea de código:
En este ejemplo, hemos tomado como valores de probabilidad una c =0.7 y una
m =0.3 pero, al igual que hicimos en capítulos anteriores, realizaremos un
barrido para explorar la calidad de las soluciones para diferentes configuraciones
de los parámetros c y m. Una vez finalizada la ejecución, debemos analizar la
gráfica de convergencia del algoritmo para evaluar si la solución otorgada por el
mismo es óptima o si aún existe margen de mejora. Así, generamos la gráfica
representada en la Figura 7.4. Como se puede observar, conforme avanzan las
generaciones, el valor de la función de fitness aumenta hasta estabilizarse
asintóticamente en 64. Dicho valor, es el óptimo obtenido mediante el uso del
algoritmo. Se observa que el algoritmo converge aproximadamente en la
generación 600.
Una vez comprobada la convergencia del algoritmo, observamos que se cubren
un total de 64 puntos de interés mediante el correcto posicionamiento de 50
puntos de conexión. La representación de los puntos de interés junto con los de
conexión, así como su cobertura, son representados en la Figura 7.5. Para
generar dicha figura hemos utilizado las siguientes líneas de código:
Figura 7.4. Gráfica que muestra la convergencia del algoritmo.
Como se puede observar, la solución obtenida mediante el algoritmo evolutivo es
bastante buena ya que se cubre el 85.33% de los puntos de interés. Sin embargo,
a la vista de los resultados, se observa que la solución puede mejorar aún más
dado que existen puntos de conexión que no cubren ningún punto de interés. Por
ejemplo, considere los puntos de conexión centrados en torno a las coordenadas
(x = 600, y = 900). Estos puntos se solapan, cubriendo los mismos puntos de
interés -e incluso no cubriendo ninguno de ellos-. Por tanto, deja en entredicho la
optimalidad de la solución.
En este simple ejemplo se muestra cómo la solución del algoritmo evolutivo no
tiene por qué ser óptima; solo debe mejorar cualquier solución inicial válida.
Así, dado que el problema planteado es altamente no lineal, al mezclarse tanto
variables continuas como discretas, es muy difícil su resolución mediante
métodos convencionales de resolución de problemas de optimización. Los
algoritmos genéticos son una alternativa válida y eficiente para obtener
soluciones.
Del mismo modo, es posible mejorar la solución obtenida, ya sea mediante la
reformulación de las funciones genéticas (por ejemplo la operación de mutación
y/o cruce), o mediante la creación de individuos factibles iniciales más
adaptados al problema objeto de estudio (por ejemplo teniendo en cuenta la
posición de los puntos de interés para generar los de conexión). Así, por
ejemplo, considere en este caso la siguiente función que nos permite generar los
individuos iniciales:
Figura 7.5. Resultados obtenidos tras ejecutar el algoritmo con un único
objetivo.
Como se puede observar, en este caso, cada punto de conexión se situará en la
misma posición que un punto de interés. Dicho punto de interés se escoge de
forma aleatoria. Por lo tanto, inicialmente, se contará con un mayor número de
puntos de interés cubiertos y el algoritmo deberá adaptarse para conseguir cubrir
los restantes. En este caso, se logra cubrir un total de 72 puntos de interés, es
decir, un 96% del total. La solución obtenida es la presentada en la Figura 7.6.
Por último, realizamos un barrido del resultado del problema para diferentes
configuraciones de las probabilidades c y m. Una comparación de dichos
resultados pueden observarse en la Tabla 7.1. Como se puede observar, se han
probado las siguientes configuraciones: (c = 0.6, m = 0.4), (c = 0.7, m = 0.3) y (c
= 0.8, m = 0.2) y para cada una de ellas se ha resuelto el problema diez veces.
Para llevar a cabo esta batería de pruebas hemos utilizado el siguiente código:
Figura 7.6. Resultados obtenidos tras ejecutar el algoritmo con un único objetivo
con la nueva función para generar individuos.
Observe cómo hemos generado dos archivos de texto para almacenar,
respectivamente, los valores de la función objetivo y los mejores individuos
obtenidos como solución.
Pcx
Pmut
0.6
0.4
min(CF) 70
max(CF) 74
avg(CF) 72.1
0.7
0.3
0.8
0.2
70
75
72.7
68
73
71.4
Tabla 7.1. Resultados de la función objetivo para diferentes configuraciones de
la probabilidad de cruce y mutación
La mejor solución se obtiene para el segundo caso que corresponde a una
probabilidad de c = 0.7 y m = 0.3 para la cual se cubren, en el mejor de los
escenarios, 75 puntos de interés, es decir, el 100% del total. La mejor solución se
representa en la Figura 7.7. Se puede observar que todos los puntos de interés
están cubiertos.
Figura 7.7. Resultados obtenidos tras ejecutar el algoritmo con un único objetivo
con la nueva función para generar individuos y unos valores de probabilidad de c
= 0.7 y m = 0.43.
Los Textos 7.1 y 7.2 muestran fragmentos de los archivos de texto
individuos_sensores.txt y fitness_sensores.txt, respectivamente.
Texto 7.1. Fragmento del archivo de texto individuos_sensores.txt con los
resultados obtenidos tras el barrido.
Texto 7.2. Fragmento del archivo de texto fitness_sensores.txt con los resultados
obtenidos tras el barrido.
En las siguientes secciones, se planteará la situación en la que existan dos
objetivos a optimizar. Dichos objetivos serán opuestos.
7.4 Problema con múltiples objetivos: maximizando el
número de puntos cubiertos y la redundancia
Movámonos ahora a la situación en la que existe más de un único objetivo a
optimizar. Supongamos que, en ocasiones, algunos de los puntos de conexión
pueden presentar fallos o retrasos en las comunicaciones. Así, parece lógico
intentar cubrir los mismos puntos de interés con más de un punto de conexión
con el fin de tener redundancia en las medidas. Para ello, en este segundo
objetivo intentaremos maximizar el número de puntos de interés cubiertos por
los puntos de conexión sin tener en cuenta redundancias. Es decir, si dos puntos
de conexión cubren los mismos dos puntos de interés, consideraremos un total
de cuatro puntos cubiertos.
En este caso, se tendrán dos objetivos contrapuestos. Por un lado, el objetivo
original tenderá a distribuir los puntos de conexión de la forma más espaciada
posible, con el fin de cubrir tantos puntos de interés como sea posible. Por otro
lado, el nuevo objetivo priorizará la redundancia de cobertura por parte de los
puntos de interés, en lugar de cubrir el mayor número. De esta forma, tenderá a
focalizar los puntos de conexión en las zonas con mayor concentración de puntos
de interés.
Así, la formulación del problema será la siguiente:
7.4.1 Definición del problema, población inicial y operadores genéticos
Para poder ejecutar el algoritmo, será necesario realizar algunas modificaciones
en el toolbox, así como en la función que llama a la rutina. En concreto, el
código quedaría como sigue:
Como se puede apreciar, en la primera línea de código se ha modificado los
pesos indicando que ahora hay dos parámetros a maximizar. Por otro lado, la
selección se realiza mediante selNSGA2.
7.4.2 Función objetivo
La función objetivo o de fitness de este algoritmo multiobjetivo puede ser escrita
de la siguiente forma:
Observe cómo la función objetivo devuelve una tupla cuya primera componente
cuantifica el número de puntos de interés cubiertos por los puntos de conexión,
mientras que la segunda devuelve el sumatorio del número de puntos de interés
cubiertos por todos los puntos de conexión.
7.4.3 Ejecución del algoritmo
Para poder ejecutar el algoritmo, será necesario realizar algunas modificaciones
en la función que llama a la rutina. En concreto, el código quedaría como sigue:
Finalmente, en la función multiple_objetivo_ga se indica que se desea buscar las
componentes que conformarán el frente Pareto.
7.4.4 Resultados obtenidos
Ejecutamos el algoritmo mediante el siguiente código:
Mediante estas líneas de código, generaremos dos archivos de texto:
individuos_sensores_multi.txt y fitness_sensores_multi.txt. En el primero se
almacenarán los individuos óptimos que conforman el frente Pareto, mientras
que en el segundo se guardarán los datos de su función de fitness
correspondiente. Un fragmento de dichos ficheros puede observarse en los
Textos 7.3 y 7.4.
Texto 7.3. Fragmento del archivo de texto individuos_sensores_multi.txt.
Texto 7.4. Fragmento del archivo de texto fitness_sensores_multi.txt.
Así, la Figura 7.8 muestra el frente de Pareto obtenido como resultado de la
ejecución del algoritmo. Dada una solución que genera una cobertura y un valor
de redundancia, la recta tangente al frente Pareto muestra qué supondría en uno
de los objetivos un incremento en la optimalidad del otro.
Figura 7.8. Frente de Pareto del problema abordado.
Analicemos, pues, el frente Pareto obtenido. Si partimos del punto más hacia la
derecha, vemos que un punto del frente es aquel en el que se cubren 65/75
puntos de interés con una redundancia de 116 (tal y como muestra la Figura 7.9).
Observe que a pesar de cubrir un alto porcentaje de los puntos de interés, existe
una alta concentración de puntos de conexión en torno a las coordenadas (x =
450; y = 1000) y (x = 510; y = 0). Esto se debe a que en esa zona existe una gran
concentración de puntos de interés, y por tanto, se consigue un mayor valor de
redundancia en pequeño decremento del número de puntos cubiertos.
Del mismo modo, si nos desplazamos a través de la curva del frente Pareto hacia
la izquierda, disminuimos los puntos de interés cubiertos pero, por otro lado,
aumenta la redundancia conseguida. De esta forma, los puntos de conexión se
situarán en donde exista una mayor concentración de puntos de interés
alcanzando una situación en la que cubriendo 55/75 puntos de interés se logra
una redundancia de 130 tal y como muestra la Figura 7.10. Observe que en este
caso, existe aún más concentración de puntos de conexión en el área en torno al
punto (x = 510; y = 0) así como enb el (x = 1650; y = 800).
Cabe destacar que la Figura 7.8 ha sido generada de forma similar a las figuras
de resultados anteriores, y que lo único que debe tenerse en cuenta aquí, es
realizar de forma adecuada la lectura de los ficheros de texto para escoger
adecuadamente el individuo a representar. Así, ya que el archivo
fitness_sensores_multi.txt está ordenado de forma creciente respecto al primer
objetivo, hemos optado por representar el individuo correspondiente (del archivo
individuos_sensores_multi.txt) a la primera y última línea. A continuación, se
introduce un breve código para facilitar la lectura de los archivos donde se
almacenan los datos y convertirlos a una lista:
Figura 7.9. Solución obtenida para el punto del frente Pareto que mayor número
de puntos cubre.
Figura 7.10. Solución obtenida para el punto del frente Pareto que mayor
redundancia alcanza.
7.5 Código completo y lecciones aprendidas
Primero, analicemos los pasos que hemos seguido para la resolución de este
problema. Para ello, nos apoyaremos en el código completo mostrado en Código
7.5:
1. Líneas 1-7: Primero, es necesario importar todas las librerías utilizadas. En
nuestro caso, haremos uso de la librería deap 4 para todo lo referente a la
resolución del problema mediante estrategias evolutivas. Las librerías numpy 5 y
random 6 se utilizarán para operar matemáticamente, así como para generar
números aleatorios. Por último, matplotlib 7 nos ayudará a generar gráficas.
2. Cabe destacar en la línea 10 la variable multi que determina si el problema se
va a resolver con un solo objetivo o con dos objetivos. Por defecto se ejecutará el
problema monoobjetivo. Si deseamos ejecutar el problema multiobjetivo, solo
debemos asignar a dicha variable el valor True .
3. Líneas 13-37: Definimos las funciones area y cobertura , que nos servirán para
evaluar si un punto de conexión se encuentra dentro del área de interés, y si un
punto de interés se encuentra cubierto por uno de conexión.
4. Líneas 39-74: Se definen las funciones genéticas de generación de individuos
y mutación.
5. Líneas 76-121: Se declara la función de fitness por la cual se conocerá la
optimalidad de la solución evaluada (tanto para un único objetivo como para
dos).
6. Líneas 123-183: Se define la función que llama al algoritmo. Se definen los
parámetros característicos del mismo.
7. Líneas 187-270: El problema es definido. Se declaran los puntos de interés, el
toolbox y se hace la llamada a la función que recoge el algoritmo evolutivo.
Código 7.5. Código final desarrollado.
Por otro lado, cabe concluir que este capítulo ha presentado la aplicación de los
algoritmos genéticos sobre un problema real. El problema consiste en el reparto
de unos puntos de conexión de manera óptima con el fin de recoger datos de
ciertos puntos de interés. Dos objetivos han sido abordados: la maximización de
los puntos de interés cubiertos y la redundancia en la cobertura de los mismos.
Podemos destacar las siguientes lecciones aprendidas:
■Hemos tratado un problema cuyas variables o genes del individuo son
continuas pero en el que, por el contrario, el valor devuelto por la función de
evaluación tiene un valor discreto entero.
■A diferencia que en capítulos anteriores, hemos planteado el problema como
uno de maximización (y no de minimización) exponiendo las diferencias que son
necesarias.
■Hemos visto la importancia que tiene la población inicial a la hora de alcanzar
resultados positivos. Una buena estrategia para crear la población inicial puede
hacer que el problema converja hacia valores más adecuados.
■En el problema con dos objetivos se ha obtenido un frente de Pareto discreto.
Además, se han analizado las soluciones del frente de Pareto.
■Hemos observado que aun siendo un problema con menor número de
restricciones, al tratarse de un NP-duro su complejidad es elevada y, por
consiguiente, también lo es su tiempo de resolución. Se puede observar que se
necesita un elevado número de generaciones.
7.6 Para seguir aprendiendo
Con el fin de lograr un mayor conocimiento sobre este tipo de problemas, se
recomienda la lectura de los siguientes trabajos:
■En primer lugar, mencionar varios resúmenes sobre problemas de cobertura en
redes inalámbricas: (Wang, 2011) (Ghosh y Das, 2008)(Reina et al., 2016a).
■Un trabajo sobre posicionamiento óptimo de nodos en redes malladas (
Wireless Mesh Networks ) puede encontrarse en (Oda et al., 2013).
■En (Reina et al., 2012) se utiliza un algoritmo genético para el posicionamiento
de sensores en un entorno ferroviario mediante algoritmos genéticos.
■En (Reina et al., 2013) se emplea un algoritmo genético para obtener las
posiciones óptimas de un conjunto de puntos de acceso, que mejoran la
conectividad de un equipo de rescate en un escenario de desastres.
■Los siguientes trabajos están enfocados al posicionamiento óptimo de
vehículos aéreos para dar cobertura a nodos en tierra: (Reina et al., 2018b),
(Reina et al., 2018a), (Reina et al., 2016b).
■Un enfoque multiobjetivo al problema del posicionamiento de sensores para
medidas medio ambientales puede consultarse en (Kim et al., 2008).
Como ejercicios se plantean los siguientes:
■Realice una comparación de los resultados obtenidos por el algoritmo genético
con distintos métodos de cruce y mutación.
■Utilice el método de selección de ruleta y compare los resultados con los
obtenidos mediante torneo.
■Utilice un algoritmo de clustering para mejorar la población inicial. Por
ejemplo, puede utilizar el algoritmo k-means de la librería scikit-learn 8. Utilice
un número de clusters igual al número de puntos de interés. Compruebe si
mejora la convergencia del algoritmo.
■Considere que la cobertura de un sensor es mejor cuanto más cerca se
encuentra el punto de interés de conexión. Así, no se tendrá un uno si se cubre
un punto y un cero en caso contrario, sino que se tendrá un número real que
indique la calidad de dicha cobertura. Maximice la cobertura de la red.
■Considere un tipo diferente de cobertura que no venga dada por un círculo y
observe los resultados.
_________________
1Se puede variar este número si se quiere aumentar la complejidad del problema.
2https://docs.python.org/library/random
3El análisis de los resultados con distintos modelos de propagación está fuera del
alcance de este libro
4https://deap.readthedocs.io/
5https://docs.scipy.org/doc/numpy/reference/
6https://docs.python.org/3/library/random.html
7https://matplotlib.org/
8https://scikit-learn.org/stable/modules/clustering.html#k-means
Epílogo
En esta obra hemos abordado el aprendizaje de algoritmos genéticos para
resolver problemas de ingeniería mediante un enfoque práctico. Se comenzó con
ejemplos sencillos que aumentaron en complejidad conforme avanzábamos. En
la segunda parte del libro, hemos resuelto hasta tres problemas de ingeniería
reales de áreas muy distintas, como la ingeniería eléctrica, hidráulica y redes de
sensores, que sumados al problema del viajero estudiado en la primera parte y
considerado un problema de transporte, representan un amplio espectro de
posibilidades. Se han abordado problemas de distinta topología, incluyendo
variables discretas y continuas. Además, todos los problemas de ingeniería
resueltos se han planteado de dos modos: i) con un solo objetivo y ii) con
múltiples objetivos. Así, esperamos que esta obra haya sido útil para los lectores
y que sirva de referencia para futuros trabajos.
Antes de terminar, nos gustaría mencionar otros campos de la computación
evolutiva para los que la librería deap tiene herramientas disponibles:
■Estrategias evolutivas: Las estrategias evolutivas o evolutionary strategies
nacieron años después de los algoritmos genéticos en la década de los años
60, en la Universidad Técnica de Berlín (Ingo Rechemberg y Hans Schwefel)
(Beyer y Schwefel, 2002). Están más enfocadas a problemas con variables
continuas y su fortaleza está en los métodos de mutación. En las estrategias
evolutivas, no solo mutan las variables del problema, sino que también
sufren modificaciones ciertos parámetros de los métodos de mutación
empleados. La librería deap dispone de un amplio abanico de herramientas
para desarrollar estrategias evolutivas 9.
■Programación genética: La programación genética o genetic programming
es otra rama de la computación evolutiva, nacida a principio de los 90 de la
mano de los trabajos de John Koza (Koza, 1992). El objetivo de la
programación genética es encontrar el programa óptimo para cierta tarea.
Por lo tanto, el algoritmo intenta buscar la relación más adecuada
(operaciones) entre las variables del problema. El abanico de operaciones
debe ser indicado por el programador mediante un set de primitivas donde
el algoritmo elegirá en cada caso. Una de las principales características de
los algoritmos de programación genética es la representación de los
individuos, que se realiza en forma arbórea. Así, mediante un árbol se
determinan las operaciones óptimas que se realizan entre las variables de
entrada (hojas del árbol) hasta el resultado (nodo raíz). La librería deap
dispone también de herramientas y ejemplos para desarrollar
programación genética 10.
■Optimización basada en enjambre: Por optimización basada en enjambre
se entiende un conjunto de técnicas de optimización bioinspiradas. Estas
técnicas no están dentro de la computación evolutiva clásica; sin embargo, sí
son algoritmos metaheurísticos como los algoritmos genéticos. Los
algoritmos bioinspirados emulan el comportamiento social de ciertas
especies para resolver problemas de optimización complejos. En la
actualidad existen numerosos algoritmos de optimización bioinspirados (Ser
et al., 2019), tales como Particle Swarm Optimization (PSO) (Kennedy y
Eberhart, 1995), Firefly Algorithm (FA) (Yang et al., 2008) y Cuckoo
Optimization Algorithm (COA) (Rajabioun, 2011), entre otros muchos. La
librería deap dispone de ejemplos de aplicación del PSO 11.
_________________
9https://deap.readthedocs.io/en/master/examples/es_fctmin.html
10https://deap.readthedocs.io/en/master/examples/gp_symbreg.html
11https://deap.readthedocs.io/en/master/examples/pso_basic.html
En este apéndice se abordan los problemas que surgen al heredar de un array de
numpy a la hora de crear individuos. Imaginemos que definimos un problema de
minimización y queremos utilizar arrays de numpy de la siguiente forma:
La clase Individual hereda de np.ndarray. Desde el punto de vista de las
operaciones que podemos realizar con arrays de numpy, este procedimiento es
muy interesante, ya que podemos sacar provecho de la gran cantidad de
funciones que tenemos disponibles en la librería numpy. Sin embargo, desde el
punto de vista de cómo funciona deap, debemos tener cuidado con dos aspectos:
1. Los operadores genéticos de la librería no funcionarán correctamente debido a
cómo se realiza la operación slicing en los arrays de numpy .
2. La forma de comparar soluciones debe adaptarse, debido al funcionamiento de
las operaciones de comparación en arrays de numpy .
En este apéndice trataremos ambos problemas y expondremos soluciones para
los dos aspectos mencionados.
A.1 Introducción a las secuencias en Python
En Python un objeto de tipo secuencia está formado por un conjunto de
elementos u objetos. A estos elementos se puede acceder de forma independiente
por un índice. A continuación, se describen los objetos tipo secuencia mutables1
que se pueden utilizar como cromosomas de los individuos en un algoritmo
genético en deap:
■Listas: Las listas son una secuencia de objetos cada uno de distinto tipo. Son
objetos nativos de Python . El índice de los elementos es numérico y empieza en
el cero.
■Diccionarios: Los diccionarios son una secuencia muy particular, ya que los
índices no son numéricos. Los índices se definen mediante objetos inmutables,
normalmente de tipo cadena. Cada elemento y cada índice pueden ser de distinto
tipo y son nativos de Python .
■arrays : En esta secuencia todos los elementos tienen que ser del mismo tipo. El
tipo se indica mediante el atributo typecode . Están definidas en el módulo
nativo array 2. El índice de los elementos es numérico y empieza en el cero.
■ndarray : Son secuencias tipo vector definidas en la librería numpy 3. En estas
secuencias todos los elementos deben ser del mismo tipo. El tipo de los objetos
se define con el atributo dtype. Si este parámetro no se especifica, por defecto
considera el tipo flotante. El índice de los elementos es numérico y empieza en
el cero.
A continuación, se muestran algunos ejemplos de cómo crear secuencias de cada
tipo en Python:
Resultado A.1. Definición de una lista en Python.
Resultado A.2. Definición de un diccionario en Python.
Resultado A.3. Definición de arrays del módulo array.
Resultado A.4. Definición de arrays de numpy.
Cualquiera de estas cuatro secuencias puede ser útil para definir el cromosoma
de un individuo en un problema de optimización. No obstante, en el libro -por
simplicidad- se utilizan solo tres tipos: listas y arrays tanto de numpy como del
módulo array. A continuación, se muestra la diferencia entre estos tres tipos en la
operación de slicing o troceado. Dicha diferencia supondrá un problema a
superar en el caso de los arrays de numpy.
A.2 Slicing en secuencias y operadores genéticos de
deap
La operación de slicing consiste en acceder a la vez a varios elementos de una
secuencia mediante los índices. De forma genérica, en una secuencia en Python
la sintaxis para realizar la operación es: secuencia[i:j], accediendo a los
elementos [i, j). La diferencia principal entre las secuencias es el objeto que
devuelve la operación de slicing. En particular, la gran diferencia reside en los
arrays de numpy, ya que estos devuelven una vista de los elementos del array
original. Veamos qué significa esto mediante un ejemplo:
Script A.5. Slicing en arrays de numpy.
En primer lugar, se define el array v1 con seis elementos. A continuación, la
vista1 contiene los tres primeros elementos. Dicha vista está apuntando
directamente a las mismas posiciones de memoria que el array original. Por lo
tanto, cualquier modificación que se haga sobre la vista afecta también al array
original. Lo podemos ver en el resultado del anterior script.
Resultado A.6. Resultado de la operación slicing en arrays de numpy.
Cuando los elementos de la vista se han puesto a cero, se ponen también a cero
las posiciones correspondientes del array original. Esto no ocurre ni en las listas
ni en los arrays del módulo array. Lo podemos ver con dos ejemplos parecidos al
anterior. En el siguiente script se ha creado una lista de seis elementos y se ha
obtenido un "trozo"de la lista mediante slicing. En concreto, se toman los tres
primeros elementos. Al crear l1_trozo se ha creado un objeto totalmente nuevo
que no está enlazado de ninguna manera con l1. Por lo tanto, cualquier
modificación en l1_trozo no afecta a la lista original.
Script A.7. Slicing en listas.
Lo podemos ver en el siguiente resultado.
Resultado A.8. Resultado de la operación slicing en listas.
A continuación, se muestra el mismo ejercicio con un array del módulo array.
Script A.9. Slicing en arrays del módulo array.
Se puede ver que, al igual que con las listas, el fragmento (“trozo") seleccionado
del array no está relacionado de ninguna forma con el original.
Resultado A.10. Resultado de la operación slicing en arrays del módulo array.
Dicho comportamiento de los arrays de numpy hace que ciertos operadores
genéticos implementados en el módulo tools de deap no funcionen
correctamente. Por lo tanto, si al crear las plantillas de los individuos se hereda
de numpy.ndarray hay que tener cuidado a la hora de utilizar los operadores
genéticos del módulo tools. Por ejemplo, los operadores de cruce de un punto,
dos puntos y cruce ordenado no funcionarían correctamente. Existen otros
operadores en los que no existe ningún problema, por ejemplo cruce tipo
uniforme, blend, cruce polinomial o mutación Gaussiana. En general, cuando se
realice un intercambio de genes mediante slicing tendremos que adaptar las
operaciones genéticas. En los operadores genéticos que utilicen operaciones
matemáticas entre los progenitores, no tendremos ningún problema.
Para realizar la operación de slicing sin enlazar el resultado con el array de
numpy original, debemos utilizar el método copy como se muestra a
continuación.
Script A.11. Operación de slicing en arrays de numpy desenlazando el resultado
del original.
Como resultado, se puede ver que ahora la copia y el original no están enlazados.
Resultado A.12. Resultado del slicing en arrays de numpy con el método copy.
Por lo tanto, vemos que es muy fácil adaptar la operación de slicing en arrays de
numpy para que funcione como en las otras dos secuencias. Así, si queremos
utilizar los operadores genéticos de la librería deap que utilizan slicing, lo único
que tenemos que hacer es adaptar el código fuente original. Veamos dicho
procedimiento con un ejemplo: el cruce de un punto. El siguiente código es
exactamente igual que el código fuente original4, con la diferencia de que se ha
utilizado el método copy para el intercambio de elementos entre los arrays. Este
procedimiento se puede realizar para el resto de operadores que presenten el
mismo problema.
Script A.13. Operador de cruce de un punto adaptado para arrays de numpy.
Observando el resultado, se puede ver que los arrays v1 y v2 se han cruzado
correctamente.
Resultado A.14. Resultado de la operación de cruce adaptada para arrays de
numpy.
A.3 Operador de comparación en secuencias
Otro aspecto importante en el que cambia el funcionamiento de las distintas
secuencias es cuando se realizan operaciones de comparación. Por ejemplo,
imaginemos que utilizamos el operador > para comparar si una secuencia es
mayor que otra.
En las operaciones de comparación entre listas, la operación se realiza
comparando cada par de elementos de las listas, empezando desde la primera
posición de cada lista, hasta que la operación devuelve verdadero en alguna de
las comparaciones. Veamos dicho funcionamiento con un ejemplo:
Script A.15. Operaciones de comparación con listas.
El resultado de dicho ejemplo se muestra a continuación.
Resultado A.16. Resultado de la operación de comparación entre listas.
Podemos ver que el resultado es verdadero, ya que el segundo elemento de l2 es
mayor que segundo elemento de l1. Es importante tener en cuenta que en la
comparación entre listas, estas no tienen que ser del mismo tamaño. La
operación funcionará de la misma forma para listas con distintos tamaños.
Pasemos ahora a comparar arrays del módulo array; utilizamos los mismos
elementos que en el ejemplo anterior de las listas para comparar el resultado.
Script A.17. Operaciones de comparación con arrays del módulo array.
Se puede observar que el resultado es el mismo que en el caso de las listas.
Resultado A.18. Resultado de la operación de comparación entre arrays.
Por lo tanto, la operación de comparación funciona exactamente igual para los
arrays del módulo array. Por último, pasemos a realizar el mismo ejercicio pero,
en este caso, utilizando objetos de tipo ndarray de numpy.
Script A.19. Operaciones de comparación con ndarrays de numpy.
En este caso, el resultado es totalmente distinto, ya que la operación de
comparación devuelve un array del mismo tamaño que los originales, indicando
el resultado de la comparación por cada par de elementos. Este funcionamiento
es debido a que las operaciones de comparación en numpy están definidas como
funciones universales5. Por lo tanto, queda claro que, en este caso, los arrays
deben ser del mismo tamaño.
Resultado A.20. Resultado de la operación de comparación entre ndarrays de
numpy.
La pregunta ahora es, ¿cómo afecta este distinto comportamiento de las
secuencias en deap? La respuesta es que afecta en los objetos de tipo
HallofFame y ParetoFront, ya que estos objetos deben realizar comparaciones
entre los fitness de los individuos para actualizarse. Debido al funcionamiento
interno de deap, las funciones de comparación deben devolver un solo resultado
True o False. Así, para que estos objetos funcionen correctamente con arrays de
numpy, se debe proporcionar una función en el atributo similar. A continuación,
se muestra cómo hacerlo en cada caso. Para el caso de los objetos HallofFame:
La función np.array_equal6 tiene un funcionamiento similar a la comparación de
listas mediante el operador ==. Es decir, devuelve True si ambos arrays son
iguales en longitud y los elementos son también iguales. En el caso del objeto
ParetoFront, se debe proceder de la siguiente forma:
En la página oficial de deap se puede encontrar más información sobre la
diferencia de funcionamiento en términos de rendimiento entre los arrays de
numpy y los arrays del módulo array78.
_________________
1Las secuencias inmutables como las tuplas no se consideran, ya que su
inmutabilidad las hace inservibles para los cromosomas de los individuos.
2https://docs.python.org/3/library/array.html
3https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.html
4En Spyder, el código fuente original se puede consultar pulsado el botón
control y haciendo clic en la función correspondiente. No obstante, siempre se
puede consultar el código fuente en github en https://github.com/deap/deap/
5https://docs.scipy.org/doc/numpy/reference/ufuncs.html
6https://docs.scipy.org/doc/numpy/reference/generated/numpy.array_equal.html
7https://deap.readthedocs.io/en/master/tutorials/advanced/numpy.html
8https://deap.readthedocs.io/en/master/examples/ga_onemax_numpy.html
En este apéndice se aborda cómo trabajar con múltiples procesadores con deap1.
La idea principal es distribuir la carga computacional de los algoritmos genéticos
entre varios procesadores. Para ello, la evaluación de cada individuo se puede
hacer en un procesador distinto. Hay que tener en cuenta que el procesamiento
paralelo solamente tiene sentido si la función de evaluación es relativamente
costosa; lo veremos a continuación mediante un ejemplo. Así, debemos tener
siempre presente que si la función de evaluación la podemos evaluar de manera
muy rápida, por ejemplo en cuestión de microsegundos, no nos merece la pena
ejecutar el algoritmo genético con procesamiento paralelo. Es más, como se
demostrará a continuación mediante un ejemplo, en estos casos el
funcionamiento del algoritmo genético será incluso más lento.
Para ilustrar el procesamiento paralelo de un algoritmo genético en deap, vamos
a utilizar el problema del capítulo 2, esto es, el problema del viajero o TSP.
B.1 Procesamiento paralelo con el módulo
multiprocessing
En este caso vamos utilizar el módulo multiprocessing2. Este es un módulo
nativo de Python que se utiliza para lanzar distintos procesos. Para aquellos
lectores que desconozcan el funcionamiento de los procesos, debemos indicar
que cuando utilizamos múltiples procesos, cada uno tiene recursos
independientes. Es decir, los procesos no comparten memoria. Esto no ocurre
con los hilos o threads, en los que sí se comparte memoria. Por lo tanto, cuando
lanzamos un script en Python utilizando el módulo multiprocessing, es como si
cada proceso se ejecutará en un intérprete de Python distinto. Con respecto al
algoritmo genético, cada individuo se ejecutará en un procesador distinto. Por lo
tanto, si tenemos N procesadores, se podrían evaluar N individuos en paralelo.
Ya veremos que, desgraciadamente, esto no es siempre así.
A continuación, detallamos las principales líneas de código que debemos añadir
al código completo del Capítulo 2 para poder ejecutarlo con procesamiento
paralelo. En primer lugar, debemos importar el módulo multiprocessing:
En este caso, también se importa el módulo time3 para poder medir el tiempo de
ejecución del algoritmo. Así, podremos ver la diferencia entre ejecutar el
algoritmo genético en un solo procesador o utilizando múltiples procesadores.
En segundo lugar, debemos realizar algunos cambios en el main.
En t1 medimos el tiempo de comienzo del algoritmo. Con el objeto pool
indicamos en cuántos procesadores vamos a dividir la carga. El atributo
processes indica el número de procesos que se distribuirán entre los
procesadores disponibles. En este ejemplo se ha utilizado el valor de 4 porque el
portátil que se está utilizando para realizar las pruebas dispone de cuatro
procesadores4. A continuación, se registra en la caja de herramientas la función
map que llama a la función pool.map. La función pool.map funciona como la
función nativa de Python map5, pero utilizando múltiples procesadores. Al
registrar esta función, estamos cambiando el funcionamiento interno del
algoritmo genético. Los algoritmos genéticos implementados en deap utilizan la
función map de Python para evaluar los individuos de la población. Al registrar
una nueva función map basada en procesamiento paralelo, estamos sustituyendo
la función map que utiliza el algoritmo internamente. Es importante destacar que
estas dos líneas de código siempre se tienen que ejecutar desde el paraguas de:
En caso contrario, obtendremos un error de ejecución. Por último, en t2
medimos el tiempo al terminar el algoritmo genético. Calculando la diferencia
entre t2 y t1 podemos calcular el tiempo que ha tardado el algoritmo genético.
En este caso el resultado se muestra a continuación6:
El tiempo de ejecución obtenido con el código que se ejecutó en el Capítulo 2:
Se puede apreciar que en este problema no merece la pena paralelizar la
ejecución, ya que el algoritmo genético tarda más. El problema es que la función
de evaluación se ejecuta muy rápido. De hecho, en este caso la función de
evaluación tarda menos de un microsegundo en ejecutarse. En general, el
procesamiento paralelo introduce tiempos de espera para que se terminen de
ejecutar los procesos en los distintos procesadores, además de tiempos de
planificación. Por lo tanto, si la función objetivo se ejecuta muy rápido, dichos
tiempos de gestión del procesamiento paralelo impactarán negativamente en la
eficiencia del algoritmo.
Para ver la utilidad del procesamiento paralelo vamos a realizar una pequeña
modificación en la función objetivo. A continuación, se muestra la nueva
función de evaluación.
Se puede comprobar que lo único que se ha incluido es un pequeño retraso de 1
ms antes de devolver el resultado. Es decir, ahora la función de evaluación tarda
aproximadamente 1 ms en ejecutarse. Para introducir el retraso se ha utilizado la
función time.sleep, indicando que el retraso que se quiere añadir es de 0.001 s.
Durante ese tiempo el programa se queda en ese punto esperando a que
transcurra dicho tiempo (no realiza nada). Si ejecutamos el algoritmo genético de
nuevo sin procesamiento paralelo, el resultado es el siguiente:
Vemos un incremento significativo con respecto al tiempo de ejecución. Era
esperable, ya que hemos añadido un retraso importante en la función objetivo. A
continuación, ejecutamos el algoritmo genético utilizando procesamiento
paralelo.
Vemos que ahora sí hemos mejorado significativamente los resultados. No
obstante, aunque el computador que estamos utilizando dispone de cuatro
procesadores, el rendimiento no es cuatro veces superior al del caso no
paralelizado. Como hemos comentado, paralelizar la ejecución tiene unos costes
de gestión que reducen la eficiencia del método.
En resumen, antes de lanzarnos al procesamiento paralelo, que puede parecer
muy atractivo, hay que considerar si se va a sacar beneficio por ello. Como
consejos:
1. Se recomienda medir el tiempo de ejecución de la función objetivo.
2. Si la función de evaluación se ejecuta rápidamente (tiempos de ejecución
menores de milisegundos), no se recomienda paralelizar el algoritmo genético.
3. Si la función de evaluación tarda del orden de los milisegundos o más, sí es
recomendable paralelizar el algoritmo genético.
Por último, cabe indicar que el campo del procesamiento paralelo es muy
amplio. Por lo que en este apéndice solo se pretende poner de manifiesto que se
puede realizar procesamiento paralelo con deap en Python.
B.2 Procesamiento paralelo con el módulo Scoop
Otra forma de realizar procesamiento paralelo con deap es utilizar el módulo
scoop7. En este caso los cambios que debemos hacer son dos:
■Debemos registrar en la caja de herramienta la función map del submódulo
futures de scoop :
■Debemos llamar al script de Python directamente desde el intérprete:
Por último, en este caso todo lo referente al algoritmo genético debe estar
definido en la función main.
Para finalizar este apéndice, nos gustaría indicar algunas referencias que se
pueden consultar sobre la paralelización del algoritmos genéticos (Alba y
Tomassini, 2002) (Alba, 2005).
_________________
1https://deap.readthedocs.io/en/master/tutorials/basic/part4.html
2https://docs.python.org/3.6/library/multiprocessing.html
3https://docs.python.org/3.6/library/time.html
4Intel Core i7 @ 1.8 GHz
5https://docs.python.org/3.6/library/functions.html#map
6Hay que tener en cuenta que dicho tiempo dependerá del procesador utilizado.
7https://github.com/soravux/scoop/
cxBlend
Operador de cruce borroso
cxOnePoint
Operador de cruce en un punto
cxOrdered
Operador de cruce ordenado
cxPartiallyMatched
Operador de cruce parcialmente emparejado
cxSimulatedBinary
Operador de cruce binario simulado
cxTwoPoint
Operador de cruce en dos puntos
cxUniform
Operador de cruce uniforme
eaMuPlusLambda
Algoritmo genético µ + λ
eaSimple
Algoritmo genético simple
mutFlipBit
Operador de mutación mediante inversión de bit
mutFlipBitAs
Operador de mutación mediante inversión de bit asimétrica
mutGausBounded
Operador de mutación Gaussiana acotada
mutGaussian
Operador de mutación Gaussiana
mutPolynomialBounded Operador de mutación polinomial con límites
mutShuffleIndexes
Operador de mutación mediante mezcla de índices
mutTriangular
Operador de mutación triangular (personalizado)
NSGA-II
Algoritmo genético NSGA-II
selRoulette
Operador de selección por ruleta
selTournament
Operador de selección por torneo
Abdoun, O., Abouchabaka, J., y Tajani, C. (2012). Analyzing the performance of
mutation operators to solve the travelling salesman problem. CoRR,
abs/1203.3099.
Alba, E. (2005). Parallel metaheuristics: a new class of algorithms, volume 47.
John Wiley & Sons.
Alba, E. y Tomassini, M. (2002). Parallelism and evolutionary algorithms. IEEE
transactions on evolutionary computation, 6(5):443–462.
Alvarado-Barrios, L., del Nozal, Á. R., Valerino, J. B., Vera, I. G., y MartínezRamos, J. L. (2020). Stochastic unit commitment in microgrids: Influence of the
load forecasting error and the availability of energy storage. Renewable Energy,
146:2060–2069.
Alvarado-Barrios, L., Rodríguez del Nozal, A., Tapia, A., Martínez-Ramos, J. L.,
y Reina, D. G. (2019). An evolutionary computational approach for the problem
of unit commitment and economic dispatch in microgrids under several
operation modes. Energies, 12(11).
Arzamendia, M., Espartza, I., Reina, D. G., Toral, S. L., y Gregor, D. (2019a).
Comparison of eulerian and hamiltonian circuits for evolutionary-based path
planning of an autonomous surface vehicle for monitoring ypacarai lake. Journal
of Ambient Intelligence and Humanized Computing, 10(4):1495–1507.
Arzamendia, M., Gregor, D., Reina, D. G., y Toral, S. L. (2019b). An
evolutionary approach to constrained path planning of an autonomous surface
vehicle for maximizing the covered area of ypacarai lake. Soft Comput.,
23(5):1723–1734.
Arzamendia, M., Gregor, D., Reina, D. G., Toral, S. L., y Gregor, R. (2016).
Evolutionary path planning of an autonomous surface vehicle for water quality
monitoring. In 2016 9th International Conference on Developments in eSystems
Engineering (DeSE), pages 245–250. IEEE.
Arzamendia, M., Reina, D. G., Toral, S., Gregor, D., Asimakopoulou, E., y
Bessis, N. (2019). Intelligent online learning strategy for an autonomous surface
vehicle in lake environments using evolutionary computation. IEEE Intelligent
Transportation Systems Magazine, 11(4):110–125.
Arzamendia, M., Reina, D. G., Toral, S. L., Gregor, D., y Tawfik, H. (2018).
Evolutionary computation for solving path planning of an autonomous surface
vehicle using eulerian graphs. In 2018 IEEE Congress on Evolutionary
Computation (CEC), pages 1–8. IEEE.
Bacco, M., Ferro, E., y Gotta, A. (2014). Uavs in wsns for agricultural
applications: An analysis of the two-ray radio propagation model. In SENSORS,
2014 IEEE, pages 130–133. IEEE.
Bechikh, S., Datta, R., y Gupta, A. (2016). Recent advances in evolutionary
multi-objective optimization, volume 20. Springer.
Beyer, H. y Schwefel, H. (2002). Evolution strategies–a comprehensive
introduction. Natural computing, 1(1):3–52.
Camacho, E. F. y Alba, C. B. (2013). Model predictive control. Springer Science
& Business Media.
Chmielewski, D. J., Palmer, T., y Manousiouthakis, V. (2002). On the theory of
optimal sensor placement. AIChE journal, 48(5):1001–1012.
Coello, C. A. (2006). Evolutionary multi-objective optimization: a historical
view of the field. IEEE computational intelligence magazine, 1(1):28–36.
Coello, C. A. et al. (2007). Evolutionary algorithms for solving multi-objective
problems, volume 5. Springer.
Darwin, C. (1859). On the Origin of Species by Means of Natural Selection Or
the Preservation of Favoured Races in the Struggle for Life. H. Milford; Oxford
University Press.
Deb, K. et al. (1995). Simulated binary crossover for continuous search space.
Complex systems, 9(2):115–148.
Deb, K. y Jain, H. (2014). An evolutionary many-objective optimization
algorithm using reference-point-based nondominated sorting approach, part i:
Solving problems with box constraints. IEEE Transactions on Evolutionary
Computation, 18(4):577–601.
Deb, K., Pratap, A., Agarwal, S., y Meyarivan, T. (2002). A fast and elitist
multiobjective genetic algorithm: Nsga-ii. IEEE Transactions on Evolutionary
Computation, 6(2):182–197.
Deshmukh, M. y Deshmukh, S. (2008). Modeling of hybrid renewable energy
systems. Renewable and sustainable energy reviews, 12(1):235–249.
Edmonds, J. y Johnson, E. L. (1973). Matching, euler tours and the chinese
postman. Math. Program., 5(1):88–124.
Eldeeb, H., Arafa, M., y Saidahmed, M. T. F. (2017). Optimal placement of
access points for indoor positioning using a genetic algorithm. In 2017 12th
International Conference on Computer Engineering and Systems (ICCES), pages
306–313. IEEE.
Eshelman, L. J. y Schaffer, J. D. (1993). Real-coded genetic algorithms and
interval-schemata. In Foundations of genetic algorithms, volume 2, pages 187–
202. Elsevier.
Fortin, F. et al. (2012). Deap: Evolutionary algorithms made easy. Journal of
Machine Learning Research, 13(70):2171–2175.
Fortnow, L. (2009). The status of the p versus np problem. Communications of
the ACM, 52(9):78–86.
Ghosh, A. y Das, S. K. (2008). Coverage and connectivity issues in wireless
sensor networks: A survey. Pervasive and Mobile Computing, 4(3):303–334.
Goldberg, D. E. (2006). Genetic algorithms. Pearson Education India.
Goldberg, D. E. y Deb, K. (1991). A comparative analysis of selection schemes
used in genetic algorithms. In Foundations of genetic algorithms, volume 1,
pages 69–93. Elsevier.
Goldberg, D. E. et al. (1985). Alleles, loci, and the traveling salesman problem.
In Proceedings of an international conference on genetic algorithms and their
applications, volume 154, pages 154–159. Lawrence Erlbaum, Hillsdale, NJ.
Goldberg, D. E. et al. (1989). Messy genetic algorithms: Motivation, analysis,
and first results. Complex systems, 3(5):493–530.
Herrera, F., Lozano, M., y Sánchez, A. M. (2003). A taxonomy for the crossover
operator for real-coded genetic algorithms: An experimental study. International
Journal of Intelligent Systems, 18(3):309–338.
Holland, J. (1965). Some practical aspects of adaptive systems theory,".
Electronic Information Handling, pages 209–217.
Holland, J. H. (1962). Outline for a logical theory of adaptive systems. J. ACM,
9:297–314.
Holland, J. H. et al. (1992). Adaptation in natural and artificial systems: an
introductory analysis with applications to biology, control, and artificial
intelligence. MIT press.
Ishibuchi, H., Akedo, N., y Nojima, Y. (2014). Behavior of multiobjective
evolutionary algorithms on many-objective knapsack problems. IEEE
Transactions on Evolutionary Computation, 19(2):264–283.
Ishibuchi, H., Tsukamoto, N., y Nojima, Y. (2008). Evolutionary many-objective
optimization: A short review. In 2008 IEEE Congress on Evolutionary
Computation (IEEE World Congress on Computational Intelligence), pages
2419–2426. IEEE.
Kennedy, J. y Eberhart, R. (1995). Particle swarm optimization. In Proceedings
of ICNN’95-International Conference on Neural Networks, volume 4, pages
1942–1948. IEEE.
Khan, A. A. et al. (2016). A compendium of optimization objectives, constraints,
tools and algorithms for energy management in microgrids. Renewable and
Sustainable Energy Reviews, 58:1664–1683.
Kim, K., Murray, A. T., y Xiao, N. (2008). A multiobjective evolutionary
algorithm for surveillance sensor placement. Environment and Planning B:
Planning and Design, 35(5):935–948.
Koza, J. R. (1992). Genetic Programming: On the Programming of Computers
by Means of Natural Selection. MIT Press, Cambridge, MA, USA.
Lasnier, F. y Ang, T. (1990). Photovoltaic engineering handarticle.
Li, B. et al. (2015a). Many-objective evolutionary algorithms: A survey. ACM
Computing Surveys (CSUR), 48(1):1–35.
Li, H. et al. (2015b). A genetic algorithm-based hybrid optimization approach
for microgrid energy management. In 2015 IEEE International Conference on
Cyber Technology in Automation, Control, and Intelligent Systems (CYBER),
pages 1474–1478. IEEE.
Likas, A., Vlassis, N., y Verbeek, J. J. (2003). The global k-means clustering
algorithm. Pattern recognition, 36(2):451–461.
Lones, M. (2011). Sean luke: essentials of metaheuristics.
Nemati, M., Braun, M., y Tenbohlen, S. (2018). Optimization of unit
commitment and economic dispatch in microgrids based on genetic algorithm
and mixed integer linear programming. Applied energy, 210:944–963.
Oda, T. et al. (2013). Wmn–ga: a simulation system for wmns and its evaluation
considering selection operators. Journal of Ambient Intelligence and Humanized
Computing, 4(3):323–330.
Rajabioun, R. (2011). Cuckoo optimization algorithm. Applied soft computing,
11(8):5508–5518.
Reina, D. G., Camp, T., Munjal, A., y Toral, S. L. (2018a). Evolutionary
deployment and local search-based movements of 0th responders in disaster
scenarios. Future Generation Computer Systems, 88:61–78.
Reina, D. G. et al. (2016a). A survey on the application of evolutionary
algorithms for mobile multihop ad hoc network optimization problems.
International Journal of Distributed Sensor Networks, 12(2):2082496.
Reina, D. G., Marin, S. L. T., Bessis, N., Barrero, F., y Asimakopoulou, E.
(2013). An evolutionary computation approach for optimizing connectivity in
disaster response scenarios. Applied Soft Computing, 13(2):833–845.
Reina, D. G., Tawfik, H., y Toral, S. L. (2018b). Multi-subpopulation
evolutionary algorithms for coverage deployment of uav-networks. Ad Hoc
Networks, 68:16–32.
Reina, D. G., Toral, S. L., Johnson, P., y Barrero, F. (2012). An evolutionary
computation approach for designing mobile ad hoc networks. Expert systems
with applications, 39(8):6838–6845.
Reina, D. G., Toral, S. L., y Tawfik, H. (2016b). Uavs deployment in disaster
scenarios based on global and local search optimization algorithms. In 2016 9th
International Conference on Developments in eSystems Engineering (DeSE),
pages 197–202. IEEE.
Rodríguez del Nozal, A., Reina, D. G., Alvarado-Barrios, L., Tapia, A., y
Escaño, J. M. (2019). A mpc strategy for the optimal management of microgrids
based on evolutionary optimization. Electronics, 8(11):1371.
Rodríguez del Nozal, A., Tapia, A., Alvarado-Barrios, L., y Reina, D. G. (2020).
Application of genetic algorithms for unit commitment and economic dispatch
problems in microgrids. In Nature Inspired Computing for Data Science, pages
139–167. Springer.
Ser, J. D. et al. (2019). Bio-inspired computation: Where we stand and what’s
next. Swarm and Evolutionary Computation, 48:220 – 250.
Smith, J. (2012). Book title, volume 3 of 2. Publisher, City, 1 edition.
Tapia, A. (2019). Optimization strategies of micro-hydro power systems to
supply remote isolated communities. PhD thesis, Universidad Loyola Andalucía.
Tapia, A., Millán, P., y Gómez-Estern, F. (2018). Integer programming to
optimize micro-hydro power plants for generic river profiles. Renewable Energy,
126:905–914.
Tapia, A., Reina, D. G., del Nozal, A. R., y Millán, P. (2020a). Application of
genetic algorithms for designing micro-hydro power plants in rural isolated areas
—a case study in san miguelito, honduras. In Nature Inspired Computing for
Data Science, pages 169–200. Springer.
Tapia, A., Reina, D. G., y Millán, P. (2019). An evolutionary computational
approach for designing micro hydro power plants. Energies, 12(5):878.
Tapia, A., Reina, D. G., y Millán, P. (2020b). Optimized micro-hydro power
plants layout design using messy genetic algorithms. Expert Systems with
Applications, page 113539.
Ter-Sarkisov, A. y Marsland, S. (2011). Convergence properties of two (µ+λ)
evolutionary algorithms on onemax and royal roads test functions. arXiv preprint
arXiv:1108.4080.
Thake, J. (2000). The micro-hydro pelton turbine manual: Design, manufacture
and installation for small-scale hydropower. Technical report, ITDG publishing.
Tsai, Y. (2008). Sensing coverage for randomly distributed wireless sensor
networks in shadowed environments. IEEE Transactions on Vehicular
Technology, 57(1):556–564.
Van Soest, A. K. y Casius, L. R. (2003). The merits of a parallel genetic
algorithm in solving hard optimization problems. J. Biomech. Eng., 125(1):141–
146.
Van Veldhuizen, D. A. y Lamont, G. B. (1998). Evolutionary computation and
convergence to a pareto front. In Late breaking papers at the genetic
programming 1998 conference, pages 221–228. Citeseer.
Črepinšek, M., Liu, S. H., y Mernik, M. (2013). Exploration and exploitation in
evolutionary algorithms: A survey. ACM Comput. Surv., 45(3).
Wang, B. (2011). Coverage problems in sensor networks: A survey. ACM
Computing Surveys (CSUR), 43(4):1–53.
Wang, R. (2004). A genetic algorithm for subset sum problem. Neurocomputing,
57:463–468.
Yang, X. et al. (2008). Firefly algorithm. Nature-inspired metaheuristic
algorithms, 20:79–90.
Zames, G. et al. (1981). Genetic algorithms in search, optimization and machine
learning. Information Technology Journal, 3(1):301–302.
Descargar