¿Cómo conseguir un objetivo por los mismos medios por los que una especie sobrevive? La respuesta es, mutando y cruzándo sus individuos para obtener ese objetivo.
Los algoritmos genéticos desde el punto de vista del desarrollo de software, tienen casi todos un patrón único y si tienen un patrón único atienden a una estructura común, por tanto es como cualquier interface, atienden siempre a la misma composición, de modo que ¿Por qué no generamos una interface que implemente este comportamiento? ¿Por qué crear una interface? Generamos una interface porque esperamos que cualquier clase que implemente esta interface haga lo mismo y la interface es el elemento adecuado. (¿he entrado en bucle? uhmmm). Para saber más acerca de interfaces, aquí tiene un artículo de Wakicode.
Bien, la verdad que he buscado y «requetebuscado» (que dicen en mi tierra) y no he visto nada parecido acerca de una interface de algoritmo genéticos.
Un algoritmo genético tiene los siguientes elementos:
- Un conjunto de genes en los que se basan cada uno de objetos generados
- Un patrón inicial o digamos un Adán o una Eva
- Un candidato a ser el mejor de la especie
- Un indicador métrico de aptitud del mejor de la especie
y efectua las siguientes tareas:
- Inicia el algoritmo
- Crea el patrón inicial
- Obtiene la métrica de cualquier objeto de la especie. Función de Aptitud
- Ejecuta un cruce entre individuos de la especie. Cruce
- Ejecuta una mutación sobre un objeto. Mutación
- Comprueba si se ha cumplido con el objetivo. Criterio de parada
- Si no se ha conseguido el objetivo, volvemos a realizar la tarea
- Obtención de la solución
Además un algoritmo genético no tiene porque realizar un cruce porque a lo mejor nuestro problema lo podemos solucionar mutando solamente sus genes sin realizar cruce. Cada algoritmo hay que estudiarlo
Dicho esto nos ponemos manos a la obra y vamos a desarrollar una interfaz que tenga estos elementos y que nos permita realizar estas acciones con diferentes tipos. En principio voy a crear una interface del tipo Mutable y luego heredaré de esta para crear una nueva Crossover con funciones de cruce.
En Kotlin
/** * @author Joaquin Martinez Rus © * @since 15AGO20 * version 1.1 */ interface IMutable { val geneset: T val target: F var bestParent: T var bestFitness: F /** * Return true when target is reached * @return Boolean */ fun isSuccess(): Boolean /** * Start algorithm * @sample * CreateParent * Mutate * GetFitness * Compare * IsSuccess */ fun start() /** * Return a new parent object * @param Parameter of generation * @return Type T */ fun createParent(param: Any): T /** * Get object fitness * @return F value */ fun getFitness(obj: T): F /** * Return a mutation of a parent object * @param Parent element * @return type T mutated */ fun mutate (parent: T): T } /** * @author Joaquin Martinez Rus © * @since 15AGO20 * version 1.1 */ interface ICrossOver : IMutable { /** * Return a crossover of two objects of same type * @param Parent element * @return type T crossed over */ fun crossOver(father: T, mother: T): T }
y en C#
/* ************************************************************************************************************************************************ * © JOAQUIN MARTINEZ RUS 2020 * Archivo: Imutable.cs * Descripción: Interface que implementa un algoritmo genético de mutación * Historial: * 1. Joaquin Martínez Rus - 15 ago 2020. Creación * * Comentarios: * * **************************************************************************************************************************************************/ public interface IMutable where F : IComparable { public T Geneset { get; set; } public F Target { get; set; } public T BestParent { get; set; } public F BestFitness { get; set; } /// /// Return true when target is reached /// /// Boolean bool IsSuccess(); /// /// Start algorithm /// Example /// CreateParent /// Mutate /// Getfitness /// Compare /// IsSuccess /// void Start(); /// /// Return a new parent object /// /// Parameter of generation /// Type T T CreateParent(object param); /// /// Get object fitness /// /// Object from which the fitness is obtained /// F Value F GetFitness(T obj); /// /// Return a mutation of a parent object /// /// Parent element /// T mutated T Mutate(T parent); } /* ************************************************************************************************************************************************ * © JOAQUIN MARTINEZ RUS 2020 * Archivo: ICrossOver.cs * Descripción: Interface que implementa un algoritmo genético de mutación y cruce * Historial: * 1. Joaquin Martínez Rus - 15 ago 2020. Creación * * Comentarios: * * **************************************************************************************************************************************************/ public interface ICrossOver : IMutable where F: IComparable { /// /// Return a crossover of two objects of same type /// /// Fateher /// Mother /// type T crossed over T CrossOver(T father, T mother); }
Analicemos la interface.Lo primero. La interface es una interface genérica con dos tipos, T
es el tipo del objeto del algoritmo y F
es el tipo de la propiedad Fitness
que obligatoriamente debe ser comparable. Este será el cromosoma.
Cuatro propiedades, un geneset
o conjunto de genes, el objetivo que pretenderá alcanzar el algoritmo (target
), un objeto genérico como mejor individuo de su especie (bestParent
) y un indicador de aptitud del mejor de los individuos (bestFitness
).
Luego tenemos 5 métodos, método start
para iniciar el algoritmo, un método createParent
que creará el primero de la especie al cual le he incluido un parámetro por si quisiéramos determinar alguna particularidad sobre él, un método mutate
que mutará un objeto en otro, un método getFitness
que obtiene la aptitud y por último otro método booleano que determina si el algoritmo ha cumplido su objetivo (isSuccess
).La interface ICrossOver
hereda de esta añadiéndole un método más llamado crossOver
con dos parámetros o padre y madre del objeto resultante.Si ahora creamos una clase que implemente esta interface, nos aseguramos que como mínimo tendremos que desarrollar los elementos de la interface, más fácil imposible. ¿Dónde tenemos que estrujarnos el cerebro? en diseñar el tipo de cromosoma, el objetivo como debe alcanzarse o en como debe mutar. No es poco, pero si tienes poca experiencia con algoritmos genéticos, esta es tu interface.
En la práctica, vamos a generar un algoritmo sencillo que adivine una frase. Esto tan fácil como crear una clase que implemente IMutable
y en nuestro IDE nos aparecerá en rojo indicándonos que algo mal. La mayoría de los IDE,s tiene un asistente, una bombilla, una herramienta, un algo que al pulsar sobre él nos indica que algo está pasando y este dirá «implement as constructor parameters», seleccionamos las propiedades y et voilá, propiedades en la clase, pulsamos de nuevo y nos dirá implement members.
Al pulsar, seleccionamos todos lo métodos y clase lista. Ahora el código
Y aquí la clase con su código, esta solo mutará por tanto implementa IMutable
, donde el algoritmo intentará adivinar unos preciosos versos de Mario Benedetti «Corazón coraza» con 334 caracteres.
El algoritmo creará una cadena aleatoria inicialmente e irá mutando hasta llegar al objetivo deseado.
class GuessString (override val target: Double, override val geneset: String) : IMutable{ val guess = "Porque te tengo y no porque te pienso\n" + "porque la noche está de ojos abiertos\n" + "porque la noche pasa y digo amor\n" + "porque has venido a recoger tu imagen\n" + "y eres mejor que todas tus imágenes\n" + "porque eres linda desde el pie hasta el alma\n" + "porque eres buena desde el alma a mí\n" + "porque te escondes dulce en el orgullo\n" + "pequeña y dulce\n" + "corazón coraza" override var bestParent: String = createParent(guess.length) override var bestFitness: Double = getFitness(bestParent) private var attempts = 0 init { start() } override fun start() { var childFitness: Double var child: String do { do { child = mutate(bestParent) childFitness = getFitness(child) } while (bestFitness >= childFitness) bestFitness = childFitness bestParent = child attempts ++ } while (!isSuccess()) println("Intentos: $attempts") } override fun isSuccess(): Boolean { val value = (bestFitness.toString().toDouble() / guess.length) return value >= target } override fun createParent(param: Any) : String { val genes = StringBuilder() val length = geneset.length (0 until param.toString().toInt()).forEach { genes.append(geneset[Random.nextInt(length)]) } return genes.toString() } override fun getFitness(obj: String): Double = guess.mapIndexed { i, c -> if (c==obj[i]) 1.0 else 0.0}.sum() override fun mutate(parent: String): String { var index = 0 val parentFitness: Double = getFitness(parent) var childFitness = 0.0 var newGene: Char var alternate: Char var definitive: Char var indexGeneset: Int var child: String var length = geneset.length do { index = Random.nextInt(parent.length) indexGeneset = Random.nextInt( length) newGene = geneset[indexGeneset] indexGeneset = Random.nextInt( length) alternate = geneset[indexGeneset] //definitivo definitive = if (newGene == parent[index]) alternate else newGene child = parent.replaceRange(index,index+1, definitive.toString()) childFitness = getFitness(child) } while (parentFitness >= childFitness) return child } } fun main() { val guess =GuessString( 1.0, " áéíóúabcdefghijklmnñopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,\n") println(guess.bestParent) }
Y el resultado
Intentos: 328
Porque te tengo y no porque te pienso
porque la noche está de ojos abiertos
porque la noche pasa y digo amor
porque has venido a recoger tu imagen
y eres mejor que todas tus imágenes
porque eres linda desde el pie hasta el alma
porque eres buena desde el alma a mí
porque te escondes dulce en el orgullo
pequeña y dulce
corazón coraza
Pues aquí tenéis mi interface para algoritmos genéticos.
En el próximo post, veremos un algoritmo genético que posicionará las piezas de ajedrez en su sitio.
Pd: En realidad, la clase de adivinar no es gran cosa, porque se podría haber conseguido por iteración, pero entonces no habriáis aprendido el algortimo genético. Entonces, espero que os haya servido. 😉
Podía haber elegido a Antonio Machado a Calderón de la Barca o a cualquier otro poeta o poema, pero, este me parece especialmente bello. Leedlo completo, es corto, tres estrofas.
Esta obra está bajo una Licencia Creative Commons Atribución-CompartirIgual 4.0 Internacional.