Articulo_Geneticos_en_Haskell

Anuncio
Librería de Funciones abstractas para la construcción de
Algoritmos Genéticos con Programación Funcional
Jorge Mario García Usuga, Cesar Augusto Acosta Minoli, Efraín Alberto Hoyos
Resumen
Este artículo tiene por objeto, presentar el potencial que ofrece la programación funcional como
alternativa para el diseño e implementación de algoritmos genéticos (A.G.), aprovechando las
bondades de este paradigma de programación. Se logró mostrar que es posible implementar una
librería de funciones abstractas para la construcción de algoritmos genéticos, generando un código
claro, simple, corto y evitando el uso de arreglos e índices en comparación con los lenguajes
imperativos. El estudio realizado con una serie de problemas clásicos en la literatura de A.G. mostró
el buen desempeño de la librería, como también su utilidad con fines académicos, experimentales y
educativos en la búsqueda de soluciones a problemas complejos.
Palabras Claves: Programación Funcional, Algoritmos Genéticos,
Lenguajes funcionales.
Diseño e Implementación,
Abstract
The goal of this paper is to show the functional programming potential as a good choice to design
and implementing Genetic Algorithms, with all the features and advantages that this paradigm can
offer. We find that functional languages can be useful to develop an Abstract Functions Library to
help programmers to solve problems with Genetic Algorithms in a easy way due to its syntactic
similarity to mathematics. Good results are obtained when we use the library to solve some
benchmark problems. The library in this proposal can be used for academic, educational and
experimental purposes as well.
Keywords: Functional programming, Genetic Algorithms, design and implementation, functional
language.
Introducción
Desde sus comienzos, la computación blanda ha tomado el camino de la programación imperativa
para la implementación de sus algoritmos y métodos de cálculo en la búsqueda de soluciones a
problemas complejos. Sin embargo, la programación funcional presenta una nueva perspectiva en el
campo de la computación blanda y específicamente en los Algoritmos Genéticos (A.G.)
Los A.G. se han convertido en una poderosa herramienta para la búsqueda y optimización de
soluciones en problemas de tipo NP1, sobretodo en el área de la planeación de redes eléctricas y de

Grupo GEDES universidad del Quindío. Maestría en Enseñanza de la Matemática, Línea de Profundización
en Matemática Discreta, Universidad Tecnológica de Pereira.
2
presupuestos. Precisamente una de las características más importantes de los A.G es la posibilidad
de encontrar no sólo una solución, sino un conjunto de buenas soluciones aproximadas y
completamente diferentes.
Muchos de los intentos de llevar programas hechos en lenguajes imperativos a lenguajes
funcionales como Haskell, Mozart-oz o Lisp han fracasado, debido a que los programadores recaen
en el error de tratar de implementar índices y arreglos propios del modelo imperativo, en un sistema
completamente diferente como lo es, el modelo funcional. El manejo de las listas en Haskell ofrece
una alternativa para superar este tipo de inconvenientes.
El presente artículo tiene como fin mostrar que es posible construir una librería de funciones
abstractas para la construcción de algoritmos genéticos en Haskell sin recurrir al uso de arreglos e
índices. El objetivo es aprovechar las potencialidades que ofrece la programación funcional como
el manejo de listas, llamados recursivos, evaluación perezosa y funciones de alto orden para el
desarrollo de software con un código corto, seguro, de fácil comprensión y mantenimiento.
En la primera sección se pretende refinar los conceptos necesarios para comprender la filosofía de la
programación funcional. En la sección siguiente, se presenta una discusión sobre algoritmos
genéticos, sus operadores y la manera como se implementan funciones abstractas para sus
operadores en Haskell, con el fin de facilitar la construcción para problemas particulares.
Finalmente, se hace un análisis del desempeño de la librería, a través de un estudio con una serie de
problemas de la literatura en A.G., teniendo en cuenta aspectos como la población, la taza de
mutación y el método de cruzamiento entre otros, mostrando de esta forma, las posibilidades que la
programación funcional ofrece en el diseño e implementación de algoritmos en general.
Haskell: un lenguaje de programación funcional
La gran mayoría de los lenguajes de programación existentes en la actualidad, se pueden clasificar
en dos grandes ramas según la filosofía que los acoge: Los lenguajes imperativos, entre estos C,
Pascal y Fortran y los lenguajes declarativos como Prolog, Oz-Mozart y Lisp. Se dice que un
lenguaje es imperativo si consiste de una secuencia de comandos e instrucciones de asignación las
cuales son ejecutadas de manera estricta, una después de otra. Por su parte, el modelo declarativo se
interesa más por el qué debe ser computado, en vez del cómo debe ser computado; esta filosofía
1
Clase de Problemas no solubles en tiempo polinomial.
3
cambia las secuencias de comandos e instrucciones por declaraciones y definiciones dejando que la
máquina se ocupe del resto; por este motivo los lenguajes declarativos son considerados como
lenguajes de alto nivel.
Haskell es un lenguaje funcional puro ya que su única forma de calcular la realiza a través de la
evaluación de funciones; la computación la realiza a través de definiciones en vez de hacerlo por
asignaciones como en los lenguajes imperativos. No existen estructuras cíclicas, como por ejemplo,
el for y el do-while de C y no existen las variables en el sentido de mutabilidad, ni asignaciones
destructivas, por ejemplo en Haskell es imposible hacer “x:=x+1”, como usualmente alguien lo
haría en Pascal.
Según Richard Bird (BIRD,2000), para comprender esta visión debemos tener en cuenta que: “La
programación desde una perspectiva funcional, consiste en construir definiciones y en usar el
computador para evaluar expresiones, el papel del programador consiste en definir una función que
permita resolver un problema dado".
A continuación se muestran algunas características que hacen de este un lenguaje moderno y
atractivo para ser objeto de estudio:
Polimorfismo de Tipos: Haskell se caracteriza por ser estaticamente tipeado, es decir, los tipos de
las variables y las funciones son detectados en tiempo de compilación. El tipeado estático permite
detectar muchos errores en tiempo de compilación y no en tiempo de ejecución, como suele suceder
en algunos lenguajes. Además, el sistema de tipos de Haskell posee una propiedad muy importante
conocida como polimorfismo. Para entender y apreciar su poder en detalle observe en la figura 1 la
función long definida en Haskell:
long :: [a]->Int
long [] = 0
long (x:xs) = 1 + long xs
Figura 1. Función que calcula la longitud de una lista
La función long calcula la longitud de una lista. La primera línea indica el nombre de la función
(long), en la parte derecha de (::) se observa el tipo de la función ([a]->Int). El polimorfismo radica
4
en que sin importar el tipo de la lista, la función siempre retornará un entero. Por ejemplo al evaluar
cualquiera de las siguientes tres expresiones, se obtiene como respuesta 3:
long [1,5,6]
long [[2,3],[5,8],[9,7]]
long [‘a’,’b’,’z’]
En estos casos los tipos de las expresiones de entrada son respectivamente [Int], [[Int]] y [Char].
Este hecho es muy interesante ya que las funciones se pueden diseñar de forma general y, por lo
tanto, pueden ser re-usadas con gran facilidad.
Funciones de alto nivel: Dentro de la programación funcional algunos autores consideran que las
“funciones son ciudadanos de primera clase”(BIRD,2000). Esta frase tiene un gran significado en
Haskell,, ya que las funciones en sí mismas son valores, que pueden ser almacenados, pasados
como argumentos a otras funciones y, también, retornan como resultado de una función. Una
función que recibe funciones como argumento se denomina de alto nivel. Para ver esta propiedad se
puede considerar un ejemplo clásico de la literatura en programación funcional:
map
:: (a ->b) -> [a] ->[b]
map f [] = []
map f (x:xs) = f x: map f xs
Figura 2. Función map.
La función map, que se describe en la figura 2, recibe como argumentos una función de la forma
(ab), una lista de tipo a ([a]) y retorna una lista de tipo b ([b]), la función map aplica una función
a cada uno de los elementos de una lista. A continuación se presentan algunos ejemplos del uso de
la función map:
map (+3) [1,2,3] = [4,5,6]
map (*3) [1,2,3] = [3,6,9]
map (head) [[3,4,5],[2,5],[6,8,9]] = [3,2,6]
Las funciones de alto nivel son un poderoso mecanismo de abstracción que puede mejorar
sustancialmente la estructura y modularidad de muchos programas.
Ajuste de Patrones y definiciones recursivas: El Ajuste de Patrones y las definiciones son otra
característica fundamental dentro de los lenguajes funcionales; además, sirven como esquema de
pensamiento en el momento de definir funciones. Por ejemplo, las definiciones de la función take
(ver figura 3) son:
5
take
take 0
:: Int -> [a] -> [a]
xs
= []
take (n+1) []
= []
take (n+1) (x:xs)
= x: take n xs
Figura 3. Función que toma los n primeros elementos de una lista.
La función take toma como argumento un número entero n y una lista, y retorna los n primeros
elementos de la lista en un proceso recursivo a través de sus definiciones. Nótese que las
definiciones de la función take cubren todos los posibles casos, tanto para los números naturales
como para la lista de entrada. En este sentido, para realizar la evaluación de la función, Haskell
ajusta los valores de entrada (en este caso el número entero y la lista) a una de las definiciones de la
función; si los valores no se ajustan a ninguna de ellas, se produce un error. Dado que hay dos
argumentos sobre los que se lleva el ajuste de patrones, es necesario saber en qué orden se realiza
dicho ajuste. Este se lleva a cabo de izquierda a derecha dentro de cada definición y de arriba hacia
abajo en el conjunto de definiciones. Finalmente, observe en la tercera definición que cuando el
número es diferente de cero y la lista es no vacía, esta hace un llamado recursivo de sí misma. En
caso de que el número sea cero, el ajuste de patrones lo lleva a la primera definición y el proceso
termina.
De acuerdo con lo anterior, Haskell ofrece una serie de ventajas interesantes en el momento de
desarrollar software:

Está abierto hacia la enseñanza, la investigación y el desarrollo de aplicaciones,
incluyendo la construcción de sistemas robustos.

Es de dominio público, cualquier persona puede usarlo libremente.

Presenta pocos errores en tiempo de ejecución.

Permite re-usar el código gracias a la generalidad que ofrece el polimorfismo. Por lo tanto,
es posible adaptarlo con facilidad a una gran cantidad de aplicaciones.

Su mecanismo de evaluación perezosa y las funciones de alto orden permiten construir
programas más modulares, donde cada parte (función) tiene su propio significado y es
independiente del programa.
Sin embargo, es imposible dar todos los créditos a Haskell pues existen algunas desventajas:
6

Ofrece al programador menos control sobre la máquina (programación de alto nivel), esto
puede traer ciertos inconvenientes cuando se quiere diseñar software donde el máximo
desempeño se requiere a cualquier costo (programación de bajo nivel).

El recolector de basura a pesar de librar al programador del manejo de memoria tiene un
costo en tiempo de ejecución, que puede repercutir en la eficiencia del programa.
A pesar de todo, los autores e investigadores de Haskell (HUGHES ,1990) argumentan que el
sacrificio es mínimo comparado con las ventajas del lenguaje. Los beneficios de un modelo de
programación que ofrezca más soporte valen más que los modestos costos de ejecución.
Algoritmos Genéticos y su implementación:
Una de las tendencias mundiales para resolver problemas de optimización consiste en desarrollar
técnicas basadas en la naturaleza; Ejemplo de ello son: las redes neuronales, Ant-Q y los
algoritmos genéticos, entre otros. Estas técnicas utilizan métodos heurísticos no analíticos y se usan
en una serie de problemas que no tienen una forma directa de resolverse o en los cuales no es
necesario obtener la mejor solución sino una serie de soluciones muy próximas a esta.
Los Algoritmos Genéticos son una de las técnicas más utilizadas hoy en día para tratar problemas
de optimización. Según David E. Goldberg
“Los Algoritmos Genéticos son algoritmos de
búsqueda basados en los mecanismos de selección natural y genética natural. Combinan la
supervivencia de los más compatibles entre las estructuras de cadenas, con una estructura de
información ya aleatorizada, intercambiada para construir un algoritmo de búsqueda con algunas de
las capacidades de innovación de la búsqueda humana”.
Desde un punto de vista biológico los Algoritmos Genéticos (A.G) se basan en el modelo del
Neodarwinismo, el cual plantea que los organismos de una población conviven de manera
armoniosa con su medio ambiente, pero que al presentarse un problema, una situación en la cual se
ven afectados de alguna manera; bien sea por un depredador, una enfermedad o un cambio
climático, estos evolucionan para adaptarse a la nueva situación. Esta evolución se logra, por o
ejemplo, a través de mutaciones, es decir, cambios inesperados en su estructura corporal, (piernas
más largas, resistencia a una enfermedad entre otros). Esta adaptación, da como resultado la
solución al problema inicial. Los individuos adaptados tendrán más posibilidades de sobrevivir
que sus compañeros para las nuevas condiciones del medio.
7
Los individuos que gracias a un proceso de mutación cambian su estructura genética para adaptarse
mejor a las nuevas condiciones del medio tienen la ventaja de pasar su nueva información genética
a sus descendientes por medio de un proceso de cruzamiento. Este proceso permite a las especies
pasar de una generación a otra, las características especiales que les permitieron sobrevivir a la
situación o problema que se planteó inicialmente.
Esta estrategia de la naturaleza fue modelada en un sistema computacional por John Holland entre
los años 60 y 70, quien gracias a un grupo de estudiantes logró resolver algunos problemas
utilizando esta nueva técnica de optimización
(conocida, desde entonces, como Algoritmos
Genéticos (A.G)). Veamos ahora como se puede implementar un A.G y qué detalles debemos tener
en cuenta para su desarrollo.
Con el Algoritmo Genético debe hacer lo siguiente:

Representar adecuadamente una configuración y codificación del problema. Con frecuencia
se utiliza la codificación binaria donde son fácilmente simulados los operadores genéticos
de cruzamiento y mutación.

Encontrar una forma adecuada para evaluar la función-objetivo o su equivalente (fitness).
Así, se pueden identificar las configuraciones de mejor calidad (fitness value).

Escoger una estrategia de selección de configuraciones con derecho a participar en la
formación (construcción) de las configuraciones de la nueva población (nueva generación).

Escoger un mecanismo que permita implementar el operador genético de cruzamiento
(Crossover).

Construir un mecanismo que permita implementar el operador genético de mutación.

Especificar el tamaño de la población o sea el número de configuraciones en cada
generación.
La población y las listas como su estructura de datos
La población es el conjunto de soluciones (factibles o no) del problema. Dicho conjunto, a través
de un proceso de selección y con la ayuda de operadores genéticos como el de cruzamiento y
mutación, se convertirán en una nueva población con mejores soluciones que su antecesora
8
mediante un proceso iterativo. A la población que aparece en cada iteración también se le denomina
generación.
En el momento de representar a la población y a cada uno de sus individuos haciendo uso de la
programación funcional, es necesario construir una estructura de datos que haga uso de listas en vez
de arreglos e índices por los siguientes motivos:

Las listas son la estructura lineal más importante de Haskell, además cuenta con un gran
número de funciones y operaciones para utilizarlas.

La representación de una matriz indexada en Haskell es poco eficiente, ya que los valores al
ser atrapados en el constructor de datos Array, son de difícil acceso y esto tiene un costo
computacional.
Adicionalmente, si se observa con detalle la forma en la cual se almacena la información y se
operan los individuos con A.G, se hace evidente que ésta consiste en la manipulación de un
conjunto de listas a las que llamamos la población, además los operadores genéticos de
cruzamiento, mutación, y el mecanismo de selección, realizan todo su proceso en el hecho de
manipular listas. Acciones como mutar una posición o intercambiar información
entre las
soluciones es simplemente intercambiar bits de información entre dos o más listas.
Una lista se puede construir como un tipo de dato recursivo mediante la siguiente expresión:
data Lista a=Nil | Cons a (Lista a)
Esta declaración se lee: “Una lista de tipo a es Nil (Vacia) o una constante de tipo a unido con una
lista de tipo a”. En Haskell el tipo polimórfico de lista se denota por [a] y la lista vacía se denota
como [ ].
Algunos ejemplos del Listas son los siguientes:
[1,2,3] :: [Int]
[‘a’,’b’,’c’] :: [Char]
[[1,2],[3,4]] :: [[Int]]
Haskell ofrece un operador infijo (:) para la construcción de listas, que se encarga de unir (al lado
izquierdo) un elemento a la lista (cabeza:cola), por ejemplo la lista [1,2,3] puede ser escrita como:
[1,2,3]=1:(2:(3:[ ]))=1:2:3:[ ]
9
A continuación se aprecia como es posible implementar funciones para la creación de poblaciones
iniciales en Haskell, las cuales son generadas de manera aleatoria. Como ejemplo observe las
figuras 4 y 5:
Primero se genera algunos individuos aleatorios con la siguiente función:
generaIndividuoBin
:: Int ->Int->[Int]
generaIndividuoBin n l = take n (aleatoriosB l)
Figura 4. Función que genera un individuo de tamaño n.
Como se puede observar esta función toma un parámetro llamado semilla (un número que se utiliza
para generar números pseudoaleatorios) y lo convierte en una lista de números binarios aleatorios
de tamaño l. Recuerde que cada individuo de nuestro algoritmo se considerará como una lista de
números. Ahora observe en la figura 5 cómo crear una población con n individuos:
generaPoblacionBin
:: Int ->Int->Int->[[Int]]
generaPoblacionBin 0 _ _ = [ ]
generaPoblacionBin n l p = (generaIndividuoBin l (p-1)):
(generaPoblacionBin (n-1) (l) (p+1) )
Figura 5. Función que genera una población de n elementos y usa la función de la figura 4
Como se aprecia en la figura anterior, la función generaPoblacionBin toma la función que genera
a un individuo y la llama indefinidamente, provocando que Haskell genere una lista infinita de
sublistas, las cuales van a hacer los individuos de la población. El lenguaje funcional puede tratar
fácilmente con el problema del infinito potencial gracias a su método de evaluación perezosa o no
voraz, el cual sólo evalúa las partes necesarias de una expresión para obtener la respuesta, hay
partes de la expresión que no son evaluadas del todo.
La función objetivo
La función objetivo es la encargada de evaluar a los individuos de la población y definir la calidad
de cada uno de ellos, para determinar la cantidad de descendientes de cada solución. El A.G toma
el nivel de desempeño de la solución en particular y determina si es muy buena o regular. En caso
de ser una buena solución, el algoritmo le asigna un número determinado de oportunidades para
tener descendientes. Ahora bien, en el caso de ser regular, el algoritmo le asigna un bajo número de
oportunidades.
10
Sin embargo, la función objetivo es particular en cada problema debido a la naturaleza y
metodología propia de cada situación. Por esta razón no se ha incluido la función objetivo dentro
de la librería de A.G, y debe ser implementada independientemente de la misma.
La selección
Una vez calificados todos los individuos de una generación, el algoritmo debe seleccionar a los
individuos más calificados, mejor adaptados al problema, para que tengan mayor oportunidad de
reproducción. De esta forma se incrementa la probabilidad de tener individuos con alta calificación
en el futuro. Se puede decir, entonces, que la selección es el operador genético que permite
seleccionar las configuraciones de la población actual con derecho a participar en la generación de
la nueva población (nueva generación).
Los procesos de selección han sufrido una serie de cambios desde que fueron concebidos. Estos
cambios implementan, ahora, una serie de estrategias que hacen que las nuevas poblaciones
adquieran una mejor diversidad y eviten la homogenización de la misma.
En la figura 7, se aprecia una forma de implementar la selección por torneo, que es un sistema muy
simple y fácil de adecuar a todo tipo de problema. La selección por torneo consiste en tomar dos
elementos de la población de forma aleatoria y compara sus desempeños en la función objetivo para
determinar cuál es el mejor de los dos. El ganador tendrá derecho a una oportunidad de tener un
descendiente.
Antes de implementar el algoritmo se debe tomar de la población los dos individuos que van a
competir. Para tal propósito se implementa la función sacardos:
sacardos
::[[a]] ->Int->Int ->([a],[a])
sacardos x s r
= (a,b)
where
a=x !! s
b=x !! r
Figura 6. Función que toma dos elementos de una lista y retorna una pareja ordenada con ellos.
Como se observa en la figura 6, la función toma de la población dos individuos, los cuales son
escogidos a través de los parámetros s y r, y retorna una pareja ordenada con los dos individuos
seleccionados. Posteriormente se deben comparar estos dos individuos con respecto a su función de
11
evaluación y aquel que obtenga el mejor rendimiento será el escogido para hacer parte de la nueva
etapa. La función de la figura 7 es la encargada de obtener las dos configuraciones y escoger al
mejor individuo:
torneoIn
::(Fractional a, Ord a)=> ([a]->a)->[[a]]->Int ->[a]
torneoIn f (x:xs) s
= if (f a) > (f b) then a else b
where
i= unaletorio ((length (x:xs))-1) (s+1)
j= unaletorio ((length (x:xs))-1) (s+2)
a= fst (sacardos (x:xs) i j)
b= snd (sacardos (x:xs) i j)
Figura 7. Función que selecciona el mejor individuo
Esta función toma como parámetros la función objetivo llamada f, la lista x, y una semilla s. Toma
dos elementos de la población de forma aleatoria y los compara
a través de la función
f.
Finalmente devuelve al mejor evaluado. Observe que torneoIn es una función de alto nivel ya que
recibe la función objetivo como argumento sin importar el tipo de problema que se esté resolviendo
con A.G.
Este proceso se repite hasta alcanzar el límite de población, por esta razón debemos crear una
función que tome a torneoIn y la aplique el número de veces que sea necesario para llenar el tope
máximo de población. La siguiente función realiza el proceso de selección de torneo a una
población y tomando como parámetros la función objetivo, la población y un valor entero como
semilla:
torneo
::(Fractional a, Ord a) =>([a]->a)->[[a]]->Int->[[a]]
torneo f [] s
= []
torneo f x s
=(torneoIn f x (s+2)):(torneo f x (s+1))
Figura 8. Función que genera una población de n elementos y usa la función de la figura 4
La función torneo devuelve una lista infinita con la nueva población ya seleccionada. En la
implementación de este algoritmo, en una aplicación real, se debe especificar con la función take la
12
cantidad de individuos que se quieren escoger de dicha población. Otros ejemplos de algoritmos de
selección son el de la ruleta y el de truncación.
El cruzamiento
El operador genético de cruzamiento transmite la información genética de una solución padre a sus
descendientes. Para realizar este proceso, primero se toman dos configuraciones o individuos de la
población, luego se selecciona un lugar aleatorio en la configuración interna de los dos individuos
escogidos. Posteriormente, las secciones que se encuentran después de dicha posición son
intercambiadas del primer padre al segundo, produciendo así, dos nuevos descendientes:
Figura 9. Cruzamiento
Estos dos nuevos descendientes son parte de la nueva generación, este tipo de cruzamiento se
conoce como “cruzamiento a un punto” debido a que solo se escoge un punto aleatorio dentro de
la configuración para realizar el intercambio. La figura 10 muestra como se puede implementar este
algoritmo de manera funcional:
cruzaUnPunto
::[a]->[a]->Int ->([a],[a])
cruzaUnPunto x y n
= (p,m)
where
m=(fst c)++(snd d)
p=(fst d)++ (snd c)
d=splitAt n x
c=splitAt n y
Figura 10. Cruzamiento
En esta función se puede apreciar la simplicidad de este operador genético en Haskell. La función
splitAt, propia del lenguaje, se encarga de partir la lista que representa a los padres y la función
cruzaUnPunto, también propia del lenguaje, toma como parámetros dos configuraciones padres y
devuelve los dos hijos en forma de una pareja ordenada.
13
Este tipo de estrategia para realizar el cruzamiento entre soluciones no es la única, existen otros
métodos como la cruza a dos puntos y multipunto. Dichas variaciones se encuentran implementadas
en la librería.
La mutación
La mutación es el operador genético que se encarga de crear nuevas características a la población,
teniendo en cuenta que dichas características no siempre son beneficiosas para obtener mejores
soluciones. Este proceso se realiza tomando un individuo de manera aleatoria en la población, y
luego eligiendo un componente del mismo para cambiarlo por otro valor que también se escoge de
manera aleatoria:
Figura 11. Mutación en un punto
Al igual que con el cruzamiento, la implementación de la función de mutación es sencilla:
mutacionESP
::(Fractional a)=> [a] -> a ->Int-> [a]
mutacionESP [] _ _ =[]
mutacionESP x r i
= w++[r]++c
where
w=fst (splitAt (i-1) x)
c=tail (snd (splitAt (i-1) x))
Figura 12. Función que muta genéticamente a un individuo.
De manera similar a la función de la figura 10, se usa la función splitAt para cortar la lista en sublistas, se modifica la lista donde se debe realizar el cambio y posteriormente se unen en una nueva
lista. La función devuelve el individuo escogido con la mutación ya realizada. Existen otros
esquemas de mutación pero éstos se encuentran más explícitos en la librería.
La incumbente
La incumbente se define como la mejor solución de una generación. En la primera generación se
escoge por primera vez la solución incumbente, en la segunda generación se vuelve a buscar la
mejor solución y se compara con la incumbente de la primera generación, la que tenga mayor
14
desempeño se llamará la incumbente global o general. Este proceso se repite durante todo el ciclo
generacional. Dicha incumbente global es el resultado final del algoritmo, es la solución que mejor
desempeño tendrá frente al problema en todas las generaciones. Observe en la figura 13 cómo
buscar la incumbente de manera funcional:
bI
bI
bI
::(Fractional a, Ord a)=>[[a]]->([a]->a)->[a]
[] _
= []
(x:xs) f = buscaIncumbente xs x
where
buscaIncumbente []
m=m
buscaIncumbente (x:xs) m =if a >= b
then buscaIncumbente xs m
else buscaIncumbente xs x
where
a= f m
b= f x
Figura 13. Función busca la solución incumbente en una población.
La función bI recibe como argumentos una lista de listas que representa a la población y una
función que representa a la función objetivo. Finalmente, la función retorna como resultado al
individuo con mejor desempeño. La metodología es muy simple, lo que hace la función es denotar
la primera solución de población como la incumbente y compararla, posteriormente, con el segundo
componente de la población para determinar cuál es el mejor, y por lo tanto la incumbente. Este
proceso se repite de manera recursiva mediante la sub-función buscaIncumbente hasta evaluar
todos los individuos de la población.
Cabe resaltar que el proceso general se repite en cada generación y, por consiguiente, si se usan n
ciclos generacionales, se obtendrán n incumbentes. Al finalizar los ciclos se escoge la mejor de las
mejores configuraciones encontradas y ésta será la incumbente general.
Desempeño de la librería
Para estudiar el desempeño de la librería de funciones abstractas se utilizaron varios problemas de
referencia que aparecen en la literatura de Algoritmos Genéticos. Estos se encuentran clasificados
según su naturaleza y el tamaño de la información que procesan. Todas las pruebas se realizaron en
un PC con procesador AMD-ATHLON de 850 Mhz con 256 Mb de memoria, con Windows XP
como sistema operativo. El compilador para la librería en Haskell fue GHC compiler versión 5.04.3
para Windows y los tiempos fueron capturados mediante la función getClockTime, una función predefinida de Haskell. Para la comparación con un lenguaje imperativo como Pascal se usó Delphi,
15
versión 6. A continuación se describe el problema de la mochila, pero en el estudio completo se
analizó el desempeño con el problema del agente viajante y el problema de las monedas entre otros.
El problema de la mochila (Knapsack Problem)
Ahora se implementará una aplicación con la librería de A.G. Se trata del problema de la mochila o
Knapsack Problem. Para comprender el problema, suponga que ha ganado un concurso en un
supermercado. El premio consiste en llenar una bolsa o mochila que tiene un volumen específico
con la mayor cantidad de productos posibles de los enumerados en una lista. Ahora bien, si se
desea llevar un buen premio a casa, se debe escoger con mucho cuidado lo que se va a meter en la
bolsa. El concurso tiene una restricción; sólo se puede meter en la bolsa un objeto de cada producto
y la bolsa o mochila tiene un volumen limitado, el cual obviamente no se puede sobrepasar, por
ejemplo: uno de los productos puede ser una caja grande de servilletas, este producto ocupará
mucho espacio y es relativamente barato con relación a otros productos, así que se debe buscar
productos que sean pequeños pero costosos, o grandes y costosos que no sobrepasen el volumen
de la bolsa; por ejemplo: si escogemos un perfume; este producto es muy pequeño y costoso, se
puede llenar la bolsa con otros productos que sean del mismo tipo que el perfume. Al final, la idea
es sacar una bolsa lo más costosa posible, pero que no sobrepase el volumen especificado.
Planteamiento del problema
Suponga que se va a resolver el problema de la mochila sin usar A.G. Para este propósito se deben
considerar todas las posibles soluciones e identificar cuál de ellas es la mejor.
Ahora, el problema es saber cuántas posibles soluciones existen. Como nuestro problema es para
n=20, es decir, cada configuración tiene 20 casillas de 1s y 0s, por tanto:
Si T es el tamaño total de la muestra y n=20, entonces T = 220= 1'048.576 posibles soluciones.
Si se considera que el problema
es relativamente pequeño, se tendrán una gran cantidad de
soluciones para evaluar, si cambiáramos levemente el tamaño del problema para n=30 entonces:
T es el tamaño total de la muestra y n=30, entonces
T = 230= 1073'741.824
posibles soluciones.
De manera General se tiene que para n variables el tamaño T estaría dado por T=2n
Como se puede observar, resolver el problema considerando el espacio total de soluciones es una
tarea ineficiente. Los A.G permiten explorar una parte de población, sin considerar el espacio
total, lo que resulta una tarea eficiente y rápida, además de ofrecer un abanico de soluciones con
diferentes configuraciones.
16
Si se reconsidera resolver el problema con A.G, usando n=20, es decir 20 casillas por individuo, una
población de 50 individuos por ciclo y 25 ciclos generacionales, el A.G sólo exploraría 1250
elementos de la población, lo que indica que sólo exploraremos el 0.1 % de la población total.
Ahora, si la población se incrementa en 200 individuos por ciclo y 30 ciclos generacionales,
entonces se estará explorando 6000 individuos, lo que implica explorar sólo el 0.5% de la población
total. Como ejemplo se considera los siguientes vectores de costo y volumen:
Vector de costo:
[10,25,36,21,2,8,70,50,60,32,58,45,23,1,1,21,23,47,6,20]
Vector de volumen:
[5,1,20,40,60,5,1,100,60,40,56,30,10,10,2,50,45,23,10,3]<300
Estos vectores nos indican que, por ejemplo, el producto representado en la primera casilla tiene un
costo de 10 y un volumen de 5, el segundo tiene un costo de 25 y un volumen de 1.
Desempeño del problema de la mochila utilizando la librería de A.G
A continuación se muestra el desempeño de la librería de A.G desarrollando un algoritmo genético
para el problema de la mochila. Para verificar su comportamiento y desempeño, se debe tener en
cuenta que en todas las mediciones se utilizaron quince ciclos generacionales, de los cuales los
cinco primeros admiten algún grado de infactibilidad, por tal motivo, los cinco primeros ciclos
presentan un desempeño mejor que las restantes diez generaciones, pero teniendo en cuenta que
estas soluciones encontradas en los primeros ciclos pueden llegar a ser infactibles. Recordemos que
usar infactibilidad en los primeros pasos del algoritmo permite crear una mejor población con un
alto índice de calidad. Es posible observar el desempeño en la figura 14.
Figura 14 Desempeño de la librería con respecto a diferentes tamaños de la población
17
Conclusiones
En el transcurso de la investigación, en lo referente a la revisión bibliográfica, el diseño y la
implementación de la librería y el estudio del desempeño de ésta, se destacan las siguientes
conclusiones:
• Haskell presenta una gran cantidad de ventajas a la hora de programar A.G. En primer lugar su
carácter modular permite construir funciones de orden superior que actúan de manera
independiente, las cuales pueden ser mejoradas o reemplazadas si así se requiere. En segundo lugar,
Haskell, por ser un lenguaje puramente funcional se acopla perfectamente a la definición formal del
problema, es decir, casi que al definir matemáticamente el problema,
se tiene parte de la
implementación. Esto permite que nuestros algoritmos presenten un soporte matemático que los
lenguajes imperativos no pueden ofrecer.
• Haskell ofrece una serie de funciones y mecanismos para el manejo de listas de cualquier tipo, esta
es una de las principales características con las cuales se logró implementar A.G., ya que permite
reducir considerablemente el tamaño del código, haciendo más comprensible el problema a tratar.
Es por estas razones, que el manejo de listas en Haskell se presenta como una herramienta
interesente para programar A.G.
• Haskell por ser un lenguaje de alto nivel, presenta varios inconvenientes en el cálculo numérico,
que, aunque son considerables, pueden ser superados con una buena planeación y un buen manejo
del estilo de programación. Uno de los principales inconvenientes es la velocidad, la cual es un
poco más lenta que la de los lenguajes imperativos como Pascal. En comparación podríamos decir
que Pascal toma los dos tercios de tiempo que Haskell en realizar ciertas operaciones, sin embargo
Haskell es mucho más rápido en el manejo de listas.
• En cuanto al desempeño de la librería y, en especial, de los problemas escogidos (El problema de
la mochila, los billetes y cartero viajante), éstos se comportaron de manera muy similar a como lo
sugiere la literatura. Por ejemplo, al tratar el problema con una población muy baja de 20 y 50
individuos, el algoritmo encuentra una incumbente con una función objetivo muy baja, pero si el
problema se trabaja con una población superior a los 100 individuos (200,300,400 y 500), el
algoritmo encuentra mejores soluciones. Sin embargo, en algún momento por más que se aumenta
la población, el algoritmo se estabiliza y no presenta nuevas soluciones, por el contrario, el nivel en
la función objetivo de las soluciones tiende a verse disminuido.
18
• De igual manera se puede observar que la tasa de mutación cuando es muy baja no permite
encontrar buenas soluciones, ahora bien, si la tasa de mutación se mantiene entre el 3% y 5% el
algoritmo logra encontrar buenas configuraciones. Por otra parte, si se aumenta la tasa de mutación,
el algoritmo no converge fácilmente, por lo tanto, las soluciones que se encuentren serán de baja
calidad. En otros casos es necesario tener tasas de mutación más altas o, por el contrario, mucho
más bajas.
Proyecciones y trabajo futuro:
• El potencial de la librería como material pedagógico muestra un horizonte bastante prometedor,
puesto que las características de la programación funcional pueden servir como plataforma para la
iniciación tanto en los A.G como en la programación funcional. El desarrollo de materiales
didácticos que mejoren la comprensión de estos temas es un aspecto prioritario que merece un
estudio muy concienzudo y detallado.
• Existe una serie de posibilidades para continuar con la implementación de A.G. con programación
funcional. Día a día, nuevas características e innovaciones aparecen en el campo de la programación
funcional, así como en los A.G. El trabajo con las mónadas de estado puede brindar nuevos caminos
y formas alternativas de implementación que permitan mejorar el desempeño de los A.G
construidos con Haskell. En los últimos años se han hecho nuevas herramientas para trabajar la
programación distribuida, no solo en lenguajes como Haskell sino también en Mozart-oz, lo que
permitiría el paralelismo, es decir, A.G que corran en paralelo en la misma o en distintas máquinas,
con características de migración de población (Algoritmos Meméticos), comparación de
poblaciones, e intercambio de información genética.
19
Referencias
BANZHAF. W. (1999). Foundations of Genetic Algorithms. Reeves C. Morgan Kaufmann
Publishers.
BAUER. R.J. (1994). Genetic Algorithms and investment strategies. Wiley Finance Edition.
BIRD, R. (2000). Introducción a la Programación Funcional con Haskell. Prentice Hall.
BLICKLE. T y THIELE. L. (1995). A Comparation of Selection Schemes Used in Genetic
Algorithms. TIK- Reports.
COELLO. C. (2002). Introducción a la Computación Evolutiva. Notas de Clase-Departamento
de Ingeniería Eléctrica de CINVESTAV - IPN México DF.
CONTRERAS. A, MALDONADO. O. (2000). El Problema de la Satisfactibilidad.
Universidad de Pamplona, Colombia.
COOK. S. (2002). The P versus NP problem. Clay Mathematics Institute University of Toronto.
DONOSO, Y. (2000). Desarrollo y análisis del rendimiento de la programación funcional para
la solución de ecuaciones diferenciales. Revista Ingeniería y desarrollo. Universidad del
Norte, Barranquilla Colombia.
ESCOBAR A; GALLEGO R, ROMERO
Ingeniería Electrónica UTP Colombia.
R (2000). Algoritmos Genéticos. Maestría en
FOGEL. David B. (1995). Evolutionary Computation. IEEE Press.
GOLDBERG D. (2001). Genetic Algorithms in Search, Optimization and Machine Learning.
Addison - Wesley.
HOLLAND. J, (1992). Adaptation in Natural and Artificial Systems. MIT Press Edition.
KURI. A, GALAVIZ. J. (1999). Algoritmos Genéticos. Centro de Investigación en
Computación I.P.N. - Facultad de Ciencias U.N.A.M.
LABRA, J. (1998). Introducción al Lenguaje Haskell. Universidad de Oviedo Departamento de Informática.
MICHALEWICZ, Z. (1999). Genetic Algorithms + Data Structures = Evolution
Programs. Springer-Verlag.
SAGAN, C. (1980). Cosmos. Planeta E.D.
TOLMOS, P. (2000). Introducción a los Algoritmos Genéticos y sus aplicaciones. Universidad
Rey Juan Carlos, Madrid.
VAN ROY P. y HARIDI S. (2002). Concepts, Techniques, and Models of Computer
Programming.
Descargar