Universidad Simón Bolívar
Laboratorio de Algoritmos y Estructuras I CI2691
Trimestre Enero-Marzo 2021
Profesora: Carolina Chang
Alumno: Miguel Cordero
Carnet: 15-10326
Proyecto I




Introducción a los Algoritmos Genéticos

¿Qué son los Algoritmos Géneticos?

Los Algoritmos Genéticos son métodos que pueden usarse para resolver problemas de búsqueda, y optimización del resultado. Su nombre deriva de la similitud con el proceso evolutivo de los seres vivos donde las poblaciones evolucionan según los principios de Selección Natural y Supervivencia del más Fuerte como estableció Charles Darwin.

Estos algoritmos genéticos necesitan una población inicial de posibles soluciones al problema que irán evolucionando hacia valores más próximos al óptimo dependiendo de cuál sea el problema planteado y la programación respectiva.

Ese es el objetivo principal de implementar los Algoritmos Genéticos, obtener la mejor solución a un problema que por otros métodos pudiera resultar más complicado

¿Cómo funcionan?

Fase de Codificación

Al igual que en la evolución de los seres vivos, lo primero que se necesita es representar cual sería una posible solución al problema. Es decir, establecer como serán los Individuos con los cuales se trabajaran.

Para ello, según el problema que se vaya a resolver se define el Cromosoma, en nuestro caso la representación en forma computacional de cada individuo para que el algoritmo lo puede interpretar, generalmente se trata de una matriz, una lista, tuplas etc. (si en la película The Matrix éramos 0s y 1s no es muy distinto a lo que se hace con los algoritmos genéticos).

Por su parte, cada entrada de nuestro cromosoma va a representar lo que a nivel biológico se conoce como Gen y al conjunto de genes se le conoce como Genotipo mientras que su representación visual es el Fenotipo de dicho individuo.

Definidos a los individuos se necesita crear una Población Inicial o conjunto al azar de individuos, cada uno de los cuales representa una posible solución al problema dado.

Fase de Evaluación

Asi como en la naturaleza las características de cada individuo determinan que tan buena será su adaptación a las condiciones del ambiente en el que se desarrolla. En los algoritmos genéticos también es posible determinar que tan bueno es cada individuo como solución al problema y se realiza mediante un función llamada Fitness que permite cuantificar cuan optima es la solución generalmente en comparación con una referencia dada. Es ¡La clave! del algoritmo génetico porque es la base en la toma de decisiones de las siguientes fases.

Dependiendo de como se programe, lo que hace es asignar un rango de valores a cada individuo y que por ejemplo puede estar entre 0 y 1, en porcentaje de 0 a 100 etc, tal que que si el valor del Fitness es cercano a 0 mejor es la solución y mientras mas cercano a 1 menos optima es (o viceversa).

Lo importante es que una vez definido la función fitness podemos ver si deseamos Maximizar o Miniminzar nuestra función, es decir, si buscaremos aquellas soluciones cuyo fitness sea el mas alto o aquellas que tengan el fitness mas bajo, respectivamente. Ejemplo de gráfico de minimización:

Cabe destacar que todos los problemas tienen una función fitness propia y que depende de lo complejo de la solución o del problema a resolve.

Fase de Selección

El siguiente paso es el proceso de Selección, durante esta fase se realiza la evaluación de cada individuo para determinar cuales son los más aptos para Reproducirse y generar nuevos individuos. Esto lo determina el fitness que cada individuo presenta, como bien ocurre en la naturaleza el mejor adaptado tiene más posibilidades de reproducirse.

En los algoritmos genéticos hay varios métodos de selección que permiten imitar este comportamiento de selección “aleatoria”, se conocen como Roulette Wheel o Rueda de la Ruleta, Tournament Selection o Selección por Torneo, Elitism o Elitismo, entre otros.

Como este proyecto es una versión "simple" de un algoritmo genético implementaremos sólo dos métodos selección por Torneo y Elitismo

Selección por Torneo

Como su nombre lo indica se trata de elegir al azar una cantidad finita de individuos de la población, entre los seleccionados se compara su fitness y el que mayor fitness tenga es el que resulta seleccionado para reproducir una nueva generación, es una especie de competencia.

Este proceso se repite hasta tener o bien un conjunto variado de individuos seleccionados o hasta tener un par de individuos que serán los que representen a los padres de la nueva generación.

Elitismo

El elitismo, se basa en la teoría de la Supervivencia del Más Fuerte, en este técnica sólo los mejores individuos de la población sobreviven hasta la próxima generación y sin sufrir ningún tipo de cambio.

La utilidad de esta técnica es que permite que las mejores soluciones (la élite) no se pierdan durante el proceso de Cruce y Mutación pero aún tienen la posibilidad de ser elegidos para reproducirse, sin embargo, es importante que la elite no sea muy grande para garantizar la diversidad de la población.

Fase de Reproducción, Cruce o Crossover

Durante la fase de Cruce los padres seleccionados combinan su características genéticas para producir una Descendencia que conformorá una nueva población de soluciones que en promedio será mejor que la anterior generación porque obtiene las mejores características de cada padre.

Las técnicas para realizar Cruce en algoritmos genéticos son variadas al menos nueve (9), algunas son Cruce por un Único Punto o Single Point Crossover, Cruce por dos puntos o Two Points Crossover, entre otros. Pero el método puede variar de acuerdo al problema y la especificación del individuo, lo importante es garantizar que la descendencia sea variada para asegurar que se cubra todas las posibles soluciones al problema.

A diferencia de la reproducción natural, el cruce no se aplica a todos los pares de individuos que han sido seleccionados para emparejarse, sino que se aplica de manera aleatoria con un alta probabilidad de que ocurra, pero en caso de no ocurrir, la descendencia se obtiene simplemente duplicando los dos padres seleccionados en ese momento en la siguiente generación.

En este proyecto se aplicará el Cruce por dos puntos, que consiste en seleccionar dos puntos al azar en el cromosoma del padre dividiéndolo asi en tres secciones. Luego, se copia una seccion de sus genes en las respectivas posiciones de uno de los dos hijos que se generan y de forma intercalada (una seccion es del primer padre, la segunda seccion es del segundo padre, y asi hasta completar todos los genes).

Fase de Mutación

Teniendo a la nueva descendencia, se realiza el proceso de Mutación.

La mutación no es mas que alterar uno o varios de los genes del individuo de forma aleatoria y con una muy baja probabilidad de que ocurra, en la naturaleza suele ocurrir en el momento de la reproducción por lo que sólo afecta a la descendencia y el cambio producido puede ser o no beneficioso para el individuo, no hay forma de garantizarlo pero siempre se espera que sea un cambio favorable porque el cambio busca mejorar su adaptabilidad al ambiente que lo rodea.

En los algoritmos genéticos el procedimiento no es muy diferente, sin embargo, se realiza luego de tener a la descendencia, y no necesariamente se mejora al individuo luego de la mutación.

Fase de creación de una nueva generación

Finalmente, la combinación de la elite en caso de existir y la descendencia luego de la mutación, conforma un nueva población de posibles soluciones que debe ser igual en tamaño a la población inicial.

Esta nueva generación reemplaza la población anterior y se repite el ciclo desde la evaluación del fitness en adelante y si el proceso ha sido ejecutado de manera adecuada la nueva población presentará mejores características en comparación con la anterior.

Y esto se debe a que con cada generación las buenas características se propagan a traves de la descendencia y la elite asegurando que cada generación se acerque más y más a la solución optima es lo que se conoce como Convergencia del Algoritmo.

Este ciclo se repite o bien hasta alcanzar un determinado número de generaciones o hasta llegar a la solución óptima.

Aplicacion del Algoritmo Genético en la Reconstrucción de Imágenes

Problema a resolver

El objetivo es el siguiente: recrear por medio de polígonos, formas o figuras, una imagen dada por el usuario.

Si bien existen programas desarrollados en otros lenguajes de programación, e incluso librerías especificamente diseñadas para resolver este tipo de problemas utilizando algoritmos genéticos, se quiere en esta ocasion desarrollar las funciones desde cero, siguiendo la teoría previamente dada y limitando el uso de las librerías a lo estrictamente necesario.

En este caso al procesamiento de imágenes, matrices, gráficas y números aleatorios que es lo que manejaremos durante el proyecto.

Adicionalmente, se aplicaran tecnicas de análisis descendente, correctitud de las funciones y procedimientos, y la documentación adecuada. Temas vistos y evaluados en el desarrollo del curso de Algoritmos y Estructuras I CI2691 de la Universidad Simón Bolívar.

Las imágen que se quiere reconstruir es:

La Mona Lisa, que sirvió de ejemplo inicial y motivación.

Y estás dos son de amigos que ofrecieron sus trabajos para la prueba de este algoritmo:

Imagen, por Castellano, Pedro.

Imagen, por Pinto, Luis.

Librerías utilizadas

Numpy y Copy permiten manipular las matrices, como ordernar sus elementos o copiar por completo toda una matriz en otra variable

import numpy as np
import copy

La librería Random permite generar números o listas con relativa aleatoriedad será útil para incluir cierto grado de probabilidad en el algoritmo

import random

Librerías especializadas en el manejo de imágenes y gráficos

Matplolib, Ipython y Time permite generar gráficas de los datos generados y se vayan actualizando conforme avancen las generaciones

import matplotlib.pyplot as plt
from matplotlib import colors
%matplotlib inline
from IPython.display import clear_output
from time import sleep

Skimage nos permitirá leer imágenes desde una URL guardar esa imagen como una matriz de números reales, junto con Pil podremos ver las gráficas de cada individuo o su fenotipo. Skimage nos ofrece dos funciones esenciales para el algoritmo: Structural Similarity y Mean Squared Error que serán dos opciones para evaluar como se diferencia una imagen de la otra, es decir,nos dará nuestra función fitness.

Imageio nos permitirá guardar las imágenes como gif.

from skimage.metrics import structural_similarity as ssim, mean_squared_error as mse
from skimage import io , color, img_as_float
from skimage.transform import resize
from PIL import Image, ImageDraw
import imageio

Os es una librería opcional, que se implementa por las limitaciones de Google Colaboratory (COLAB) para almacenar la informacion, con mkdir se creará una carpeta local donde se guardaran las imágenes temporalmente mientra dura la sesión.

import os
from os import mkdir

Aplicación y sus respectivas fases

Codificacion

Genes, Genotipo e Individuo

Como buscamos recrear una imagen utilizando triángulos de diferentes tamaños, colores y transparencias.

Los individuos o posibles soluciones serán imágenes de 200x200 pixeles conformadas por un número finito de triángulos.

En esta representación el Individuo tendrá por cromosoma una lista de triángulos creados de manera aleatoria. Siendo los Genes, cada uno de los triángulos.

Y como para representar visualmente a un triángulo se necesita de tres vértices y un color con transparencia, es decir, en formato RGBA, cada triángulo será a su vez una lista de 10 elementos a saber:

$$ triángulo=[x_1, x_2 , y_1, y_2, z_1 ,z_2, R, G, B, A]$$

Donde:
$x_i,y_i$ con $0\leq x_i, y_i\leq200$ representan las coordenadas de un vértice del triángulo en un cuadro de 200x200 unidades. $R,G,B,A$ representan un color y su respectiva intesidad de colores Rojo (Red), Verde (Green), Azúl (Blue), Transparencia (Alpha)

El Genotipo en este caso será la lista completa de todos los triángulos en forma matricial.

In [ ]:
#Demostración del genotipo de un individuo con 10 triángulos aleatorios
#Correr varias veces esta celda para obtener diversos resultados
individuo_de_prueba=genes(10)
#Para visualizar mejor al individuo se utiliza numpy
individuo_de_prueba=np.asarray(individuo_de_prueba)

print(individuo_de_prueba)
[[ 31  83  18   3  45 181 222 241 128 213]
 [167  89 152 122   3 120 216   1 205  93]
 [192 137 198 144 197  58 184   0 170  95]
 [ 88  89  85  39 191 161 241 186  32 228]
 [ 11  81  10  94 146  63 191 182  56  51]
 [199   0 145  79 158 134 106 249 235 182]
 [ 38   4  54 138   2 189 148  46 170 146]
 [ 55 169  22  15  25 197  96 176 118  79]
 [142 173 193 193 164 117 172 161  81  24]
 [141  23  74 142 110 129 157  35  93  57]]

Fenotipo

Para representar visualmente a un individuo es necesario leer la información del cromosoma triángulo por triángulo para ir contruyendo la imagen final que representa al individuo. Esto es, pasar de una matriz a un gráfico que permita luego la comparación entre la imágen original y la población.

Cabe destacar que estos triangulos se grafican sobre un fondo negro para facilitar el contraste de los colores pero no es determinante para el resultado del algoritmo ya que los triangulos adoptadoran los colores de la imagen de referencia (si el algoritmo es correcto).

In [ ]:
# Demostración del fenotipo del individuo de prueba obtenido anteriormente
fenotipo_de_prueba=fenotipo_transparencia(individuo_de_prueba)
plt.axis('off')
plt.imshow(fenotipo_de_prueba)
Out[ ]:
<matplotlib.image.AxesImage at 0x7efcdac1c0d0>

Población Inicial

La población incial dependerá del numero de individuos o posibles soluciones con las que cuales el algoritmo iniciará la búsqueda de la imagen mas óptima.

Como se sugiere contar con poblaciones realtivamente grandes, se inicia por defecto con 200 individuos para asegurar que se cubra una buena cantidad de soluciones. Cada individuo contará con un número entre 20 y 100 triángulos, ya que si son muy pocos se puede perder un poco la definición de la imagen e incluso llegar muy rápido a un mínimo o máximo local (ya no se consegueriran mejores individuos en las próximas generaciones), pero si son muchos el algoritmo puede tardar en alcanzar una solución aceptable.

La población inicial será por defecto una lista de 200 individuos, cada uno con 50 triángulos para evitar que converga aceleradamente y le tome menos tiempo modificar a la población.

In [ ]:
# Demostración de una población de 10 individuos y 10 triangulos
poblacion_de_prueba=crear_poblacion(10,10)
#Para visualizar mejor el genotipo de la población (lista de individuos)
poblacion_de_prueba=np.asarray(poblacion_de_prueba)
print(poblacion_de_prueba)

#Para visualizar el fenotipo de la poblacion (lista de imágenes, una por individuo)
mostrar_poblacion(poblacion_de_prueba)
[[[115  86 118  74 157   7 247  85 215  29]
  [ 25 105  25  55  69 165  93  85  61 221]
  [ 65 122 101  63  61  98  39   7 150  65]
  [103 108  56  27 199  26  36 160  91 139]
  [ 71 161  42 143 165 158 248  23 216 243]
  [158 176 118 173  44 116  77 178  39 125]
  [ 66   4 190  87  86 133 119 106 255 124]
  [132 143 180 129  34 184  87  19  91  76]
  [108 181  83  99 155  60 234  58  69 153]
  [128 175  81   4  71  70  63  43 246  11]]

 [[ 47 157 172  36 174  77 170 183  32  15]
  [142 164 129 153 154  51  32  95 212 172]
  [ 40  83  72 199  34 137 255 165 167 233]
  [140  26  11  63  91 138 158 143 207  83]
  [ 86  66  16 200  44  47  22  84 208  95]
  [ 80 198 127  68   7  72  34 148  95 211]
  [ 92  29  15 157   4 177  29 239 219  17]
  [ 53 119 182  58  55 200 233  34 123 247]
  [ 34  27  63 125  28   4 233 172  99  94]
  [145  28 138  64  99  19  49  96  97  89]]

 [[125  92 160  83 171 118  33  58  45 135]
  [163 146   2 120  90  73  93  68 209 243]
  [ 35 117  77 110  86 155  94 110  23 222]
  [ 35  97   1 193 138 151  74 241  20  29]
  [199 106  42  20 160  33 170 221  37  69]
  [107 151 164 187 177 138 138  47  52  94]
  [ 99   8  13 139 144  66  20 164 216 212]
  [ 40  64  12 187  61 149 142 179 220  71]
  [169  65 145 157 199  92  75  83 140 145]
  [ 83 195   0  79 145   9 118  15  57 182]]

 [[118 190  85  31 181 130   5 127 192 201]
  [ 25 106 127  29  46 177 121  74 214  53]
  [ 57  68  29  15 182  29  40 158  83  29]
  [165 108  27  99 183 156  84 180 124   7]
  [164  42 147 142 186  64 248   1 122 176]
  [119 138  68 196 153 112 245   5 135 181]
  [106   1 187 157 153 200 119 156  41 193]
  [ 41 111 199  50  35  90 254 242 213 157]
  [ 12 161 150 182  87 179  70  35  35 105]
  [ 59 178  31 120  19  79  43 101 111  75]]

 [[119 138  13  97  35 140 155 125  23  28]
  [148 126 132  41   8  29  42 244  81 227]
  [156   3 149  65   2  74 213  17 179 165]
  [176 192  37 197 151  83  45  82 211  86]
  [108 141  94  81  85 141 193 210 246 150]
  [127   9 148 125   6 116 224 122  55   9]
  [  7 135  16 176 196   4 138  55   3 117]
  [159  46 138  66  87 174 173 254 177 247]
  [165 111 107  33  40 146 134 159  42  83]
  [198 141  36 109  90  99 245 136  88  65]]

 [[ 10  17  94 193  84 144 132   7 248 105]
  [ 99 173  83 116 193 154 199  18  12 203]
  [ 82  82 182 157 152  41 238 184 128 149]
  [169  49  25  84  66  29 199 122 111  23]
  [132  17 129 176  91  43  44 247  76 164]
  [100 140 188  95 161 123 106 243 101 243]
  [151  68 144   4 191  99  71 203 226  52]
  [ 79 114 118  79  52 170 188 236 199 211]
  [179 118 198 125  10 193 209 173 215  84]
  [ 18  12 183 199  19  47  55 179  87   7]]

 [[101   5   1 121  64  93 207 136  24  78]
  [134 151 134 130  22  51 224  38  17 250]
  [ 65 137  34 105  85   5  11  79  28   7]
  [ 29 141 102 124 145 107 205 104  56 185]
  [ 62  51  82 152  69 139 238  67 254  82]
  [ 38  12 103 116  70 110 147 179 240 235]
  [145 132 113 189  52 145  37 234 128 207]
  [160  90   6   1  15  51 239 204   3 100]
  [ 81 192  25 161 104   8 221 163 219  72]
  [152  53  39 175   7 145   1  56  65 232]]

 [[  6 178  43  47 187 124 111  65 188  12]
  [118  23 149  97  82  70 121  67 104 105]
  [116  39 121 115  14 163 111 241  98  53]
  [134 122  34 143 128 121 127  62  67  39]
  [177  52   4 171  66  10 134 235 200 152]
  [114  31 186 120   9 148 182 134 231 127]
  [128   0 157  65  72  48 126 213 225 227]
  [ 15  32  58   8  40   9  25 179 140 221]
  [132 106  74   0 118   1  24  69  67 243]
  [ 65 179  88  89 123  56  70  28 247  23]]

 [[ 25 108 104 135 172  92  99 133 218 143]
  [139  73   7 169 140  32 231  67 227 172]
  [145 124  84  81 113 195  58 220 220 223]
  [166 150 101  64  43  69  11 249 166 136]
  [194  79  72 135  65 193 232  25  19 197]
  [ 60  33  43  31 150  40  46  23 113 110]
  [168  42  40  54 186 127 231  95 201 118]
  [ 24 194  11   3 123   1 192 223 186 161]
  [113 149  19  27 149  80 180 132 131   5]
  [129 140 145  21  56 119  95  33 237 179]]

 [[103  25 109 108  21  89 137  38 199 115]
  [ 46 176  92 157 170  39  18 103 185 228]
  [128 197  84 175  74  97 167  46 209 207]
  [174  52  96 109 114  24  39  87 215  28]
  [192 189  61  66 149  14 252  71 132 209]
  [  0  36  90  45 178  98 245 207 187  74]
  [130 119 151 192 157 159  87 100  73 106]
  [ 88  95 138  92 145  31  88  97 182 238]
  [ 87 136 140 115  79 170 174 145 119 189]
  [189  10  25 110  28 129 191 229 155   3]]]

Evaluación

Para la evaluación del fitness de cada individuo necesitamos comparar dos imagenes, la de referencia y el fenotipo del individuo.

Existen varias formas y/o fórmulas que permiten comparar dos imágenes. Dos de ellas, las mas comunes son, Error de la Media Cuadrática-MSE por sus siglas en ingles de Mean Squared Error y Similitud Estructural-SSIM por sus siglas en ingles de Structural Similarity.


El MSE mide la diferencia entre dos imágenes pixel por pixel, por lo que requiere que ambas imágenes sean de las mismas dimensiones. Si ambas imágenes son identicas su valor será $0.0$, y será $1.0$ si son totalmente diferentes. La fórmula está definada por la siguiente expresión: $$MSE=1/n \sum\ (x-\bar{x})^2$$ Donde $x$ representa los valores $(R,G,B)$ de la imágen original para un pixel, $\bar{x}$ los valores del pixel correspondiente del individuo, y $n$ es el número de pixeles a comparar.

Está será la función que se implementará para medir el fitness de los individuos. Que nos permitirá minimzar los valores del fitness hasta llegar a 0.


En cambio, el SSIM considera los cambios en la información estructural de cada imagen, junto con el contraste y el brillo, entre porciones de igual tamaño entre las imágenes a comparar. El SSIM asigna valores entre $[-1,1]$ siendo 1 si ambas imágenes son iguales. Y su fórmula está dada por: $$SSIM(x,\bar{x})=\frac{(2 \mu_{x}\mu_{\bar{x}}+c_{1})(2\sigma_{x\bar{x}}+c_{2})}{(2 \mu_{x}^2+\mu_{\bar{x}}^2+c_{1})(\sigma_{x}^2+\sigma_{\bar{x}}^2+ c_{2})}$$

Donde:
$\mu_{i}$ es el valor promedio para $x$ y $\bar{x}$;
$\sigma_{i}^2$ es la varianza respecto de $x$ y $\bar{x}$;
$\sigma_{x\bar{x}}$ es la covarianza de $x$ y $\bar{x}$;
$c_{i}$ son constantes que dependen de los pixeles en las imágenes.

Fitnes Poblacional y por Individuo

In [ ]:
# Se escoge como demostración la imagen del primer individuo
imagen_a_comparar=fenotipo_transparencia(poblacion_de_prueba[0])
# La funcion fitness debe retornar 0.0 cuando se compara una imagen consigo misma
fitness_de_prueba=fitness(imagen_a_comparar,imagen_a_comparar)
print(f'La comparación de una imágen consigo misma dio como fitnes: {fitness_de_prueba}')

# Se comparan los individuos de la población anterior con el seleccion:
fitness_de_la_poblacion_de_prueba=fitness_poblacion(imagen_a_comparar,poblacion_de_prueba)

# Para visualizar mejor los resultados, note que el primer valor es 0. por comparse
# el primer individuo consigo mismo. El resto de los valores está entre 0.0 y 1.0
fitness_de_la_poblacion_de_prueba=np.asarray(fitness_de_la_poblacion_de_prueba)
print(fitness_de_la_poblacion_de_prueba)
La comparación de una imágen consigo misma fue dio como fitnes: 0.0
[0.         0.05864636 0.04312095 0.06217108 0.05488719 0.05423293
 0.06679199 0.06818574 0.09903872 0.06593631]

Selección

Para selección se utilizaran las técnicas de Elitismo y Selección por Torneo.

Con el elitismo se busca garantizar que las mejores soluciones de cada generación se mantengan en la siguiente, para garantizar que siempre se mejore el fitness y el algoritmo converga. Por defecto el valor escogido es de un $20\%$ de la poblacion, para dejar paso a una descendencia variada.

Con la selección por torneo, se busca que individuos al azar puedan ser elegidos para el cruce. El tamaño del torneo es 3, para mejorar el tiempo de ejecución del algoritmo ya que es un método lento de selección pero que se compensa con la variedad génetica que produce.

Elite y Torneo

In [ ]:
# Obtenemos a los mejores 3 fitness de la población anterior
elite_de_prueba, fitness_obtenido =orden_elite(poblacion_de_prueba,fitness_de_la_poblacion_de_prueba,3)
elite_de_prueba=np.asarray(elite_de_prueba)
print(elite_de_prueba)
print(f'\n Y sus fitness fueron: {fitness_obtenido}')
[[[115  86 118  74 157   7 247  85 215  29]
  [ 25 105  25  55  69 165  93  85  61 221]
  [ 65 122 101  63  61  98  39   7 150  65]
  [103 108  56  27 199  26  36 160  91 139]
  [ 71 161  42 143 165 158 248  23 216 243]
  [158 176 118 173  44 116  77 178  39 125]
  [ 66   4 190  87  86 133 119 106 255 124]
  [132 143 180 129  34 184  87  19  91  76]
  [108 181  83  99 155  60 234  58  69 153]
  [128 175  81   4  71  70  63  43 246  11]]

 [[125  92 160  83 171 118  33  58  45 135]
  [163 146   2 120  90  73  93  68 209 243]
  [ 35 117  77 110  86 155  94 110  23 222]
  [ 35  97   1 193 138 151  74 241  20  29]
  [199 106  42  20 160  33 170 221  37  69]
  [107 151 164 187 177 138 138  47  52  94]
  [ 99   8  13 139 144  66  20 164 216 212]
  [ 40  64  12 187  61 149 142 179 220  71]
  [169  65 145 157 199  92  75  83 140 145]
  [ 83 195   0  79 145   9 118  15  57 182]]

 [[ 10  17  94 193  84 144 132   7 248 105]
  [ 99 173  83 116 193 154 199  18  12 203]
  [ 82  82 182 157 152  41 238 184 128 149]
  [169  49  25  84  66  29 199 122 111  23]
  [132  17 129 176  91  43  44 247  76 164]
  [100 140 188  95 161 123 106 243 101 243]
  [151  68 144   4 191  99  71 203 226  52]
  [ 79 114 118  79  52 170 188 236 199 211]
  [179 118 198 125  10 193 209 173 215  84]
  [ 18  12 183 199  19  47  55 179  87   7]]]

 Y sus fitness fueron: [0.0, 0.04312094579008075, 0.054232929514289374]
In [ ]:
# Para demostrar la selección por torneo puede correr esta celda varias veces
padre_1_de_prueba=seleccion_torneo(poblacion_de_prueba,fitness_de_la_poblacion_de_prueba)

print(padre_1_de_prueba)
[[119 138  13  97  35 140 155 125  23  28]
 [148 126 132  41   8  29  42 244  81 227]
 [156   3 149  65   2  74 213  17 179 165]
 [176 192  37 197 151  83  45  82 211  86]
 [108 141  94  81  85 141 193 210 246 150]
 [127   9 148 125   6 116 224 122  55   9]
 [  7 135  16 176 196   4 138  55   3 117]
 [159  46 138  66  87 174 173 254 177 247]
 [165 111 107  33  40 146 134 159  42  83]
 [198 141  36 109  90  99 245 136  88  65]]

Cruce

El cruce para cada generación se realiza por medio de Cruce por dos Puntos, estos puntos son seleccionados al azar en un rango que va desde $1/3$ a un $2/3$ de la longitud del cromosoma de cada individuo. Los puntos seleccionados no pueden ser los mismo porque se trataría del cruce por un punto. Y tampoco pueden coincidir con los entremos del cromosoma para evitar que se copie por completo a uno de los padres.

Se fija una probabilidad por defecto de $0.9$ o $90\%$, donde si un valor arbitrario entre $0.0$ y $1.0$ es menor que $0.9$ el cruce ocurre, en caso contrario, los padres son copiados directamente a la nueva generación, siguiendo la teoría dada anteriormente.

Los individuos que se combinaran en cada cruce son seleccionados cada uno por torneo, para aumentar la variación génetica de cada generación.

El cruce se realiza hasta alcanzar el tamaño maximo de la población.

Cruce por dos puntos

In [ ]:
# Para probar esta función el radio de cruce está fijo 0.99 de probabilidad
# Como la descendencia complementa a la elite debe obtenerse 6 nuevos individuos
# Para completar los 10 que conforman a cada población.

descendencia_de_prueba=cruce(poblacion_de_prueba,elite_de_prueba,fitness_de_la_poblacion_de_prueba,0.99)
descendencia_de_prueba=np.asarray(descendencia_de_prueba)
print(f'Se obtuvieron {len(descendencia_de_prueba)} descendientes.')
print(descendencia_de_prueba)
Se obtuvieron 6 descendientes.
[[[125  92 160  83 171   7  33  58  45 135]
  [163 146   2  55  69  73  93  68 209 243]
  [ 35 117  77  63  61  98  94 110  23 222]
  [ 35  97   1  27 199 151  74 241  20  29]
  [199 106  42 143 165  33 170 221  37  69]
  [107 151 164 173  44 116 138  47  52  94]
  [ 99   8  13 139 144 133  20 164 216 212]
  [ 40  64  12 187  61 184 142 179 220  71]
  [169  65 145 157 199  60  75  83 140 145]
  [ 83 195   0   4 145   9 118  15  57 182]]

 [[115  86 118  74 157 118 247  85 215  29]
  [ 25 105  25 120  90 165  93  85  61 221]
  [ 65 122 101 110  86 155  39   7 150  65]
  [103 108  56 193 138  26  36 160  91 139]
  [ 71 161  42  20 160 158 248  23 216 243]
  [158 176 118 187 177 138  77 178  39 125]
  [ 66   4 190  87  86  66 119 106 255 124]
  [132 143 180 129  34 149  87  19  91  76]
  [108 181  83  99 155  92 234  58  69 153]
  [128 175  81  79  71  70  63  43 246  11]]

 [[ 47 157 172  36 157  77 170 183  32  15]
  [142 164 129 153  69  51  32  95 212 172]
  [ 40  83  72  63  34 137 255 165 167 233]
  [140  26  11  27 199  26 158 143 207  83]
  [ 86  66  16 200 165  47  22  84 208  95]
  [ 80 198 127 173   7  72  34 148  95 211]
  [ 92  29  15  87   4 177  29 239 219  17]
  [ 53 119 182  58  55 184 233  34 123 247]
  [ 34  27  63  99 155   4 233 172  99  94]
  [145  28 138   4  71  19  49  96  97  89]]

 [[115  86 118  74 174   7 247  85 215  29]
  [ 25 105  25  55 154 165  93  85  61 221]
  [ 65 122 101 199  61  98  39   7 150  65]
  [103 108  56  63  91 138  36 160  91 139]
  [ 71 161  42 143  44 158 248  23 216 243]
  [158 176 118  68  44 116  77 178  39 125]
  [ 66   4 190 157  86 133 119 106 255 124]
  [132 143 180 129  34 200  87  19  91  76]
  [108 181  83 125  28  60 234  58  69 153]
  [128 175  81  64  99  70  63  43 246  11]]

 [[118 190  85  31 181   7   5 127 192 201]
  [ 25 106 127  55  46 177 121  74 214  53]
  [ 57  68  29  63  61  29  40 158  83  29]
  [165 108  27  99 199 156  84 180 124   7]
  [164  42 147 143 186  64 248   1 122 176]
  [119 138  68 196 153 116 245   5 135 181]
  [106   1 187  87 153 200 119 156  41 193]
  [ 41 111 199 129  34 184 254 242 213 157]
  [ 12 161 150  99  87 179  70  35  35 105]
  [ 59 178  31 120  71  79  43 101 111  75]]

 [[115  86 118  74 157 130 247  85 215  29]
  [ 25 105  25  29  69 165  93  85  61 221]
  [ 65 122 101  15 182  98  39   7 150  65]
  [103 108  56  27 183  26  36 160  91 139]
  [ 71 161  42 142 165 158 248  23 216 243]
  [158 176 118 173  44 112  77 178  39 125]
  [ 66   4 190 157  86 133 119 106 255 124]
  [132 143 180  50  35  90  87  19  91  76]
  [108 181  83 182 155  60 234  58  69 153]
  [128 175  81   4  19  70  63  43 246  11]]]

Mutación

En la mutación generalmente los cambios son pequeños pero significativos. Sin embargo, debido a la complejidad del problema los pequeños cambios alargan la convergencia del algoritmo y prolonga la duración del proceso para alcanzar una optima solución.

Por esta razón, la mutación evalua a cada uno de los genes (triágulos) de la descendencia y modifica una de sus características dependiendo de un número aleatorio entre $0.0$ y $1.0$ y un valor por defecto del $2\%$ de radio de mutación, es decir, si el número aleatorio es menor a $0.02$, la mutación ocurre.

Para evitar el estancamiento de las generaciones, producto de haber alcanzado el mayor grado de diversidad genética, el radio de mutación irá disminuyendo en $0.000001\%$ o $0.00000001$ por cada generación, esta pequeña variación asegura que el redio de mutación siga siendo una probalidad pequeña pero que asegure que el cambio ocurra cada vez con menor frecuencia.

In [ ]:
# Esta funcion genera pequeños cambios en la descendencia, a fines de la demostracion
# se considerá una población de dos individuos como descendencia y un radio de mutación
# fijo en 0.02, se incluye la intruccion print('Ha ocurrido una mutación') para
# representar que hubo un cambio

mutacion(descendencia_de_prueba[0:1],0.02)
print(descendencia_de_prueba[0:1])
Ha ocurrido una mutación
[[[185  92 160  68 171 115 202  23 199 116]
  [163  63   2  55 184   7  69  68 209 237]
  [ 35 117  83  63  80  29  94  60 108 222]
  [ 35  71   1  30 181 151 187 241 244  29]
  [ 93 130  42 143  74  76 170 221  82  69]
  [107 151 199  11  44  13   4  47  52 177]
  [ 73 106  13 139  58 187  20 164 216 212]
  [181  52 181  67  61  99 142 138 220  71]
  [169 132 145 157 142 130  75  83 128 145]
  [ 83 121 189 142 126   9 118  15  94 182]]]

Complemento

Una parte importante del proceso pero que no forma parte de los algoritmos genéticos es la visualización del avance desde la población inicial hasta la solución más óptima, y que a su vez permita llevar un registro de la convergencia del algoritmo.

La función de complemento Plot permitirá representar, la imagen de referencia a la izquierda, el mejor individuo en el centro y la gráfica de los mejores fitness generación por generación (en este caso por ser un problema de minimización de la funcón fitness deberá decrecer poco a poco hasta 0.0).

Por otra parte, leer_imagen() permitirá escoger imágenes de internet escogidas por el usuario.

Las sentencias try-except se implementan como parte de la programación robusta con el objetivo de facilitar la interacción con el usuario final.

In [ ]:
# Para demostrar esta función se utilizan ejes vacios 
plot_(imagen_a_comparar,imagen_a_comparar,eje_x=[],eje_y=[],i=1,imagenes=[])
In [ ]:
# Para probar esta función utilice la URL de una imagen en linea
# O use la imagen de la Mona Lisa por defecto copie y pegue el siguiente Link 
# https://upload.wikimedia.org/wikipedia/commons/thumb/5/55/Mona_Lisa_headcrop.jpg/481px-Mona_Lisa_headcrop.jpg

image_a_procesar=leer_imagen()
Inserte la URL de la imagen que desea procesar: https://upload.wikimedia.org/wikipedia/commons/thumb/5/55/Mona_Lisa_headcrop.jpg/481px-Mona_Lisa_headcrop.jpg

Algoritmo Principal

La prueba de este algoritmo está en la sección de implementación

# Función that implement Genetics Algorithm to recreate images using triangles
# in an interactive way to the user.
# Generate a final gif with the process and save pictures every 50th generation.

def algoritmo_genetico():
    '''
    Genetic algorithm to reconstruct an online picture input by the user.
    This use polygons of three side to recreate the image. 
    Implement: MSE as Evaluation Fitness
               Selection by tournament
               Two Points Crossover
               Random Mutation
    return: A gif with the process and save the pictures in a folder called 
            AlgoritmoGenetico.         
    '''
    x_data=[]
    y_data=[]
    imagenes=[]
    answer='n'

    ######this avoid that user input wrong values to the algorithm#####
    #Read the reference image to compare
    while answer=='n':
      try:
        imagen_referencia=leer_imagen()
        sleep(2)
        #clear_output(wait=True)
        answer=input('¿Desea inciar el Algoritmo Genético con esta imagen? y/n: ')
        # Give the chance to user to interrupt the process
        print('El Algoritmo Genético va a iniciar si desea detenerlo utilice la combinación ctrl+M+I\n')
      except KeyboardInterrupt:
        print('Ha detenido el programa')
        return
    # Read the number of initial solutions to evaluate
    while True:
      try:
        num_individuos=int(input('Ingrese el número de individuos con los que desea iniciar \n (Sugerencia 200 individuos): '))
        assert (num_individuos>0)
        break
      except KeyboardInterrupt:
        print('Ha detenido el programa')
        return
      else:
        print('El valor ingresado no es un entero')
        print('Vuelva a intentarlo') 
    # Read the number of triangles for every individual in population
    while True:
      try:
        num_genes=int(input('Ingrese el número de genes (triángulos) que desea para cada individuo\n (Sugerencia 75 triángulos o más): '))
        assert (num_genes>0)
        break
      except KeyboardInterrupt:
        print('Ha detenido el programa')
        return
      else:
        print('El valor ingresado no es un entero')
        print('Vuelva a intentarlo')     
    # Read the number of generations or iterations for the algorithm
    while True:
      try:
        num_generaciones=int(input('Ingrese el número de generaciones que desea evaluar\n (Sugerencia 5000 0 6000 Generaciones): '))
        assert (num_generaciones>0)
        break
      except KeyboardInterrupt:
        print('Ha detenido el programa')
        return
      else:
        print('El valor ingresado no es un entero')
        print('Vuelva a intentarlo') 
    # Read the probability to mutate a gen in individual
    while True:
      try:
        mutacion_rate=float(input('Ingrese un valor entre 0.0 y 1.0 para la probabilidad con la que desea que ocurran las mutaciones\n (Sugerencia 0.01): '))
        assert (0.0<=mutacion_rate<=1.0)
        break
      except KeyboardInterrupt:
        print('Ha detenido el programa')
        return
      else:
        print('El valor ingresado no es un número real entre 0.0 y 1.0')
        print('Vuelva a intentarlo')
    # Read the probability to crossover two individual
    while True:
      try:
        cruce_rate=float(input('Ingrese un valor entre 0.5 y 1.0 para la probabilidad con la que desea que ocurran los cruces\n (Sugerencia 0.9): '))
        assert (0.0<=cruce_rate<=1.0)
        break
      except KeyboardInterrupt:
        print('Ha detenido el programa')
        return
      else:
        print('El valor ingresado no es un número real entre 0.0 y 1.0')
        print('Vuelva a intentarlo')       
    ##########
    ### Genetic Algorithm
    try:
      # Create the first generation acording to the user input
      generacion=crear_poblacion(num_individuos,num_genes)   
      i=0
      while i <= num_generaciones:
          # Evaluation of population
          fit=fitness_poblacion(imagen_referencia, generacion)
          # Selection by elitism
          elite=orden_elite(generacion,fit,int(0.2*num_individuos))
          # Show the fitness individual and fitness by generations 
          ejemplo=fenotipo_transparencia(elite[0])
          if i%50=0:
            plot_(imagen_referencia,ejemplo,x_data,y_data,i,imagenes)
          # Crossover the population
          descendencia=cruce(generacion,elite,fit,cruce_rate)
          # Mutate the offspring
          mutacion(descendencia,mutacion_rate-(i*0.00000001))
          # Create a new generation
          elite.extend(descendencia)
          generacion=elite
          # Repeat the process
          i=i+1
      # Save the process as a gif
      imageio.mimwrite(f'/content/AlgoritmoGenetico/algoritmo_prueba{i}.gif', imagenes, fps=1)
      return generacion,imagen_referencia
    except KeyboardInterrupt:
      print('Ha detenido el programa')
      return

Implementación

In [ ]:
# Test del algoritmo génetico. Corra la celda anterior aunque haya corrido las celdas anteriores

# El algoritmo retorna la última generación creada y por las limitaciones de colab trabaje con generaciones
# menores a 1000 y luego con el resultado corra la celda siguiente si desea continuar evaluando generaciones.
# Puede utilizar la Mona Lisa como ejemplo: 
# https://upload.wikimedia.org/wikipedia/commons/thumb/5/55/Mona_Lisa_headcrop.jpg/481px-Mona_Lisa_headcrop.jpg

# Puede utilizar la imagen de Castellano Pedro: 
# https://pbs.twimg.com/media/EyVab7zXMAACokK?format=jpg&name=medium

# Puede utilizar la imagen de Pinto Luis: 
# https://pbs.twimg.com/media/EzN-wDIUcAIi_C8?format=jpg&name=small

mejor_poblacion_encontrada, imagen_de_referencia=algoritmo_genetico()
In [ ]:
# Test del algoritmo génetico. Corra la celda anterior aunque haya corrido las celdas anteriores

# El algoritmo retorna la última generación creada y por las limitaciones de colab trabaje con generaciones
# menores a 1000 y luego con el resultado corra la celda siguiente si desea continuar evaluando generaciones.
# Puede utilizar la Mona Lisa como ejemplo: 
# https://upload.wikimedia.org/wikipedia/commons/thumb/5/55/Mona_Lisa_headcrop.jpg/481px-Mona_Lisa_headcrop.jpg

# Puede utilizar la imagen de Castellano Pedro: 
# https://pbs.twimg.com/media/EyVab7zXMAACokK?format=jpg&name=medium

# Puede utilizar la imagen de Pinto Luis: 
# https://pbs.twimg.com/media/EzN-wDIUcAIi_C8?format=jpg&name=small

mejor_poblacion_encontrada, imagen_de_referencia=algoritmo_genetico_anvil()

Imágenes de la implementación

Todas las imágenes y gif se encuentran disponibles en la cuenta de twitter del proyecto EDUGAP_USB por las siglas en ingles de Evolutionary Development Using Genetics Algorithms in Python (Desarrollo Evolutivo Usando Algoritmos Genéticos en Python)

IMAGEN DE LA MONA LISA

Algoritmo Genético Mejor Individuo de la Generación 0

Algoritmo Genético Mejor Individuo de la Generación 500

Algoritmo Genético Imágenes del Proceso desde la Generación 0 a la Generación 500

Gif del proceso


IMAGEN DE LUIS PINTO

Algoritmo Genético Mejor Individuo Generación 0

Algoritmo Genético Mejor Individuo Generación 10000

Algoritmo Genético Imágenes del Proceso desde la Generación 0 a la Generación 10000

Gif del proceso

Pruebas implementadas para definir la modificación

Para las pruebas de mutación se eligión la imagen de Pedro Castellano, por se la más sencilla en cuanto a colores y forma, pero con una cantidad de detalle considerable para evaluar la actuación del algoritmo para detalles como: el color de la la oreja, los rasgos faciales y las líneas del contorno.

Las condiciones de cada prueba fueron las siguientes:
Población: 200 individuos
Triángulos: 75 triangulos
Probabilidad de mutación: 0.01 o 1%
Probabilidad de cruce: 0.9 o 90%
Constante de mutación a disminuir (propia del algoritmo): 0.00000001

RESULTADOS:

Prueba utilizando la versión original de la función Mutación y aumentando generación a generación la probabilidad de que un gen mute:

Gif del proceso de esta prueba

Prueba utilizando la versión original de la función Mutación y disminuyendo generación a generación la probabilidad de que un gen mute:

Gif del proceso de esta prueba

Prueba utilizando la versión original de la función Mutación SIN disminuir generación a generación la probabilidad de que un gen mute:.

Gif del proceso de esta prueba


En el desarrollo de estas pruebas se optó por implementar una nueva función de mutación que en lugar de generar valores aleatorios en el rango completo del gen, es decir, $[0, 200]$ para los vértices y $[0,256]$ para los colores, se generara un valor al azar dependiendo del valor de cada gen en el rango de $[gen-25,gen+25]$. Esta función de mutación tiene por nombre $mutacion\_suave$.

Como en ese rango existe la posibilidad que el valor generado se salga de los limites, se aplicó la condición de que si es superior a 200 el nuevo valor del gen será 200, y de ser menor a 0, el nuevo valor del gen será 0.

A excepción de que el valor original del gen a mutar fuera 200 y 0 respectivamente.

Se realizaron las mismas pruebas que la versión original y estos fueron los resultados:

Prueba utilizando la versión "suave" de la función Mutación y disminuyendo generación a generación la probabilidad de que un gen mute

Gif del proceso de esta prueba

Prueba utilizando la versión "suave" de la función Mutación SIN disminuir generación a generación la probabilidad de que un gen mute:

Gif del proceso de esta prueba

Una última prueba se realizó utilizando un rango de $[gen-5,gen+5]$ y disminuyendo la probabilidad de mutación. Con las mismas condiciones descritas anteriormente.

Gif del proceso de esta prueba

CONCLUSIÓN

En vista de los resultados obtenidos para unas 10 mil generaciones, parece que la versión original del algoritmo arrojó el resultado óptimo del fitness $0.01453$ con un semejanza bastante cercana en cuanto a forma y color con el original.

Por su parte, la versiones original y "suave" disminuyendo por una constante arrojaron valores de fitness de $0.01492$ y $0.01473$ respectivamente, pero el mejor individuo para la generación 10 mil en cuanto a forma y color no llegó a imitar o mejorar al de la versión inicial.

Sin embargo, por calidad de detalle del rostro y por el fenómeno de estancamiento, se eligió la versión original disminuyendo la probabilidad de mutación. Y continuar las pruebas variando la probabilidad de mutación hasta obtener mejores resultados.

Referencias y fuentes consultadas