Implementación de Redes Neuronales Artificiales en Haskell César Augusto Acosta Minoli, Efraín Alberto Hoyos, Julián Marín (Paper Publicado en Revista de Investigaciones Universidad del Quindío. Universidad del Quindío, v.14, p.133 - 146, 2004) Resumen En la actualidad, la gran mayoría del software de simulación y entrenamiento de redes neuronales es desarrollado mediante lenguajes imperativos como C, Fortran y Pascal. El presente artículo tiene como fin, presentar al lenguaje de programación funcional Haskell, como alternativa para la implementación de algoritmos de simulación y entrenamiento de redes neuronales aprovechando las potencialidades que este ofrece y sin recurrir al uso de arreglos e índices, los cuales son responsables de la poca eficiencia y expresividad de los algoritmos numéricos implementados en lenguajes funcionales. Se logró mostrar que es posible evitar el uso de arreglos e índices para la implementación de redes neuronales feedforward multicapa, generando un código claro, simple y corto en comparación con los lenguajes imperativos. Se encontró, cómo Haskell puede ser adecuado para la experimentación con nuevos algoritmos de entrenamiento de redes neuronales gracias a su similitud sintáctica con la matemática y las fortalezas del lenguaje. Se desarrolló una interfase en Haskell para que cualquier usuario pueda entrenar redes neuronales feedforward multicapa, sin tener un conocimiento profundo en programación funcional. El estudio comparativo con Matlab mostró que la librería de redes neuronales desarrollada en Haskell tiene un buen desempeño y se puede usar como cualquier otro simulador con fines experimentales y educativos. Palabras Claves: Haskell, Redes Neuronales, Diseño e Implementación, Lenguajes funcionales, feedfordward multilayer. Abstract Nowadays, most of the software to train and simulate neural network is developed in languages like C, FORTRAN and Pascal. This article intends to show how Haskell, a functional programming language, can be used to build a library to train and simulate Feedforward Multilayer Neural Network with all the features and advantages that the language can offer. This implementation doesn't use arrays nor index, which are responsible for low efficiency and little expressiveness for some numeric algorithms implemented in that way when using functional programming. We find that Haskell can be useful to develop new algorithms to train neural networks due to its syntactic similarity to mathematics and its language strengths. Good results are obtained when comparing the Matlab Neural Network toolbox and the Haskell Neural Network library. The library in this proposal can be used for educational and experimental purposes. Keywords: Haskell, Neural Networks, design and implementation, functional language, feedfordward multilayer. 1 "Learn at least one new [programming] language every year. Different languages solve the same problems in different ways. By learning several different approaches, you can help broaden your thinking and avoid getting stuck in a rut." The Pragmatic Programmer Introducción La programación funcional presenta una nueva alternativa para el diseño e implementación de algoritmos; muchos son los aspectos que muestran el poder de este tipo de programación. Actualmente existe una gran comunidad de científicos de la computación de diversas universidades y centros de investigación a nivel mundial mostrando resultados interesantes en diferentes campos 1. Este tipo de programación ofrece una forma diferente de razonar ante un problema computacional; como ejemplo es posible garantizar mediante el rigor matemático, que el algoritmo que usted diseña hace realmente lo que debe hacer, esto a primera vista lo hace más confiable que los lenguajes imperativos y permite hacer un seguimiento más claro del algoritmo en tiempo de diseño. Según Richard Bird (Bird, 2000, 11), 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". Esta posición propone resolver los problemas mediante la definición de funciones explícitas, sin embargo, la práctica nos muestra que en algunos problemas existe tal imposibilidad debido a que hay una gran cantidad de situaciones que son difíciles de modelar y de resolver mediante procesos algorítmicos. Por tal motivo es necesario que este paradigma computacional centre sus ojos en la computación blanda como una alternativa para ampliar su campo de acción. 1 Para una información completa y detallada de este paradigma de programación visite www.haskell.org 2 Dentro de la computación blanda y desde un punto de vista matemático, una red neuronal se comporta como una función entre conjuntos, esta concepción a pesar de ser implementada inicialmente en los lenguajes imperativos es inherentemente funcional. Para resolver un problema mediante redes neuronales se requiere básicamente de construir una función a través de un proceso iterativo, sin que exista una representación explícita o simbólica de la misma. Existen diferentes formas mediante las cuales las redes neuronales son llevadas a la práctica según las especificaciones teóricas, esto es lo que se conoce como ``Implementación''. En la actualidad las redes neuronales se implementan básicamente por software, es decir mediante la simulación de la red en un PC convencional, o por hardware, mediante el uso de dispositivos electrónicos. La implementación por software es la más simple, inmediata y poco costosa. Alrededor de esta tendencia existen en versión comercial y freeware, una gran diversidad de herramientas para entrenar y simular redes neuronales. La gran mayoría de estos desarrollos son realizados por medio de lenguajes imperativos como es el caso de C, FORTRAN y Pascal (HILERA,2000,105). Se han hecho algunos intentos por llevar a la programación funcional las redes neuronales (SERRARENS,1999), sin embargo han existido algunos aspectos que han impedido el avance en esta área: la relativa juventud de los lenguajes funcionales frente a los lenguajes imperativos genera apatía por parte de los programadores convencionales y por otra parte, el cambio de paradigma al cual se tiene que enfrentar un programador que ingresa al mundo de la programación funcional produce como resultado que algunas implementaciones desarrolladas en los lenguajes funcionales luzcan como simples traducciones imperativas, este es el caso de la implementación del Álgebra Lineal mediante el uso de arreglos e índices, la cual es a su vez el soporte matemático de las redes neuronales. Destacadas investigaciones en el área, muestran que el camino menos indicado para la implementación de algoritmos de álgebra lineal en los lenguajes funcionales es mediante el uso de 3 arreglos e índices, estos muestran poca eficiencia y expresividad (SERRARENS,1999) (SKIBINSKY, 1998). Dichos trabajos también muestran que la solución más práctica se obtiene por medio de listas, ya que los lenguajes funcionales son diseñados teniendo en cuenta a las listas como la estructura lineal más importante y porque cuentan con un gran número de funciones y operaciones para utilizarlas. Parece ser, que la ruta de una implementación de redes neuronales sigue en esta dirección, pero ¿cómo se implementan y se diseñan algoritmos para simular y entrenar una red neuronal sin índices? El presente artículo tiene como fin mostrar que es posible construir una librería para la simulación y el entrenamiento de redes neuronales feedforward multicapa en Haskell sin recurrir al uso de arreglos e índices. El objetivo es aprovechar las potencialidades que ofrece la programación funcional como es el caso del 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 muestran los algoritmos de entrenamiento de redes neuronales que son implementados en Haskell y, finalmente se presenta una discusión sobre el desempeño de la librería a través de un estudio comparativo con Matlab y otros lenguajes de programación, mostrando así las posibilidades que Haskell 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 como es el caso de C, Pascal, Fortran y los lenguajes declarativos como por ejemplo Prolog, Oz-Mozart y Lisp. Se dice que un lenguaje es imperativo si consiste de una secuencia de comandos e 4 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 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. La programación funcional forma parte del pensamiento declarativo. Según Peter Van Roy y Seif Haridi (VAN ROY P. y HARIDI S., 2002) la programación funcional consiste de definir funciones las cuales son ejecutadas mediante la evaluación de expresiones, donde las funciones son verdaderas funciones en el sentido matemático. Se dice que un lenguaje funcional cuya única forma de calcular sea mediante la evaluación de funciones es un lenguaje funcional puro. Los lenguajes puramente funcionales se basan en un formalismo llamado el lambda-cálculo una teoría matemática desarrollada inicialmente por Alonzo Church, la cual consiste básicamente en la definición y evaluación de funciones a través de una serie de operaciones abstractas como lo son la unificación, el ajuste de patrones y la evaluación perezosa entre otras. Haskell es un lenguaje de programación funcional. Lleva su nombre en honor al lógico matemático Haskell Brooks Curry (1900-1982). Haskell ha tenido algunas modificaciones desde su creación en 1988, la última versión estable es Haskell 98 (JONES, PETERSON, 1999). Haskell es un lenguaje funcional puro (no posee extensiones imperativas), 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. A continuación veremos algunas características que hacen de él un lenguaje moderno y atractivo para ser objeto de estudio: 5 Polimorfismo de Tipos: Haskell se caracteriza por ser fuertemente tipeado o estáticamente 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 la siguiente función 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 que se muestra en la figura 1 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 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: long [1,5,6] long [[2,3],[5,8],[9,7]] long [‘a’,’b’,’z’] Se obtiene como respuesta 3. En estos casos los tipos de las expresiones de entrada son respectivamente [Int], [[Int]] y [Char]. Este hecho es sumamente 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 primer clase”(BIRD,2000), esta frase tiene un gran significado en Haskell , ya que las funciones en sí mismas son valores, las cuales pueden ser almacenadas, pueden ser pasadas como argumentos a otras funciones y también retornar como resultado de una función. Aquellas funciones que reciben funciones como argumento son consideradas como funciones de 6 alto nivel. Para ver esta propiedad consideremos 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 (ab), 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. Observe una vez más la generalidad que ofrece el polimorfismo de tipos. Veamos algunos ejemplos del uso de 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. Evaluación Perezosa: Haskell proporciona un método para evaluar expresiones conocido como perezoso o no estricto. Este método 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. Ajuste de Patrones : El Ajuste de Patrones es otra característica fundamental dentro de los lenguajes funcionales; sirve como esquema de pensamiento en el momento de definir funciones. Tomemos por ejemplo una función que sume los elementos de una lista de enteros (figura 3): 7 suma :: [Int]->Int->Int suma [] s = s suma (x:xs) s = suma xs (x+s) Figura 3. Función que suma los elementos de una lista. Por ejemplo, si definimos suma [4,5,6] 0 obtendremos como resultado 15. La función suma recibe dos parámetros: la lista de enteros y un parámetro en el cual iremos acumulando la suma dentro de un proceso recursivo. Observe en la figura 3 como la función se define a través de los posibles valores de entrada que pueda tomar, define la función suma en el caso de que una lista sea vacía: suma [] s = s, y posteriormente se define en el caso de una lista cuya cabeza es x y cola es xs suma (x:xs) s = suma xs (x+s). El evaluador busca que el valor de entrada se ajuste a algunos de los argumentos izquierdos de las definiciones y en caso de encontrar alguno entonces su expresión derecha será usada para la llamada de la función. Finalmente note la importancia que tiene el ajuste de patrones a la hora de definir un proceso recursivo en Haskell. De acuerdo con lo anterior Haskell ofrece algunas ventajas que suenan bastante 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. Haskell permite eliminar los paréntesis de las expresiones mediante el mecanismo de ajuste patrones por ejemplo f(x,y,z) puede ser escrito como “f x y z”. De manera similar las definiciones pueden ser escritas sin usar punto y coma (;) para separarlas, solo exige que las expresiones estén escritas en columnas siguiendo la misma sangría. Lo anterior hace que el código sea corto, claro y de fácil mantenimiento. Pocos errores en tiempo de ejecución. 8 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. La 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: Haskell ofrece al programador menos control sobre la maquina (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, lo cual 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 que ofrece el lenguaje. Los beneficios de un modelo de programación que ofrezca más soporte valen más que los modestos costos de ejecución. Construcción de una librería de Redes Neuronales en Haskell Para construir un algoritmo en programación funcional, este debe ser visto como una función explicita, al cual se le ingresa unos valores de entrada para retornar una salida, de manera similar una red neuronal se comporta como una función.. La figura 4 nos presenta de manera esquemática la relación. Función y=f(x) Haskell output = programa (input) Red Neuronal output = red (input) Figura 4. El concepto de Red neuronal y de Algoritmo en Haskell. Ahora bien, para lograr una implementación con éxito es necesario considerar los siguientes aspectos: estructura de datos, las funciones de Álgebra Lineal y Cálculo, el problema de la iteración, 9 como almacenar los pesos y bias de la red, el conjunto de entrenamiento y las funciones de activación. Estructura de datos Al momento de representar una matriz 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. A parte de la poca expresividad, 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. La figura 5 nos muestra como se representa una matriz por medio de listas de listas, donde cada una de ellas representa una fila de la matriz. a b c e f g a , b , c , e , f , g , h , i , j h i j Figura 5. Una matriz como lista de listas. Álgebra Lineal sin índices El siguiente paso consiste en desarrollar una librería de operaciones básicas de álgebra lineal para construir los algoritmos de entrenamiento. Para ver el proceso de construcción observe que en la figura 6, la función zipmatriz se usa para definir funciones que respectivamente sumen, resten y multipliquen elemento a elemento dos matrices, esta abstracción hace uso del concepto de función de alto nivel. 10 zipFila ::(Num a)=>(a->a->a)->[a]->[a]->[a] zipFila f [] [] = [] zipFila f (x:xs)(y:ys) = (f x y):zipFila f xs ys zipMatriz ::(Num a)=>(a->a->a)->[[a]]->[[a]]->[[a]] zipMatriz f [] [] = [] zipMatriz f (x:xs)(y:ys) = zipFila f x y: zipMatriz f xs ys sumaMatriz,restaMatriz, mulmatriz ::(Num a)=>[[a]]->[[a]]->[[a]] sumaMatriz a b = zipMatriz (+) a b restaMatriz a b = zipMatriz (-) a b mulMatriz a b = zipMatriz (*) a b Ejemplo: sumaMatriz [[3,4],[5,6]] [[8,9],[10,2]] = [[11,13],[15,8]] Figura 6. Función zipmatriz. De manera similar es posible construir funciones para desarrollar diferentes tipos de cálculos matriciales sin hacer uso de arreglos e índices, como lo son los productos entre matrices, el cálculo de la inversa, solución de sistemas de ecuaciones, el proceso de ortogonalización de Gram-Schmidt entre otros. Iteración y Recursión El entrenamiento de una red neuronal es un proceso iterativo el cual consiste de la actualización de sus pesos y bias hasta que se cumpla un criterio de parada. El modelo imperativo de programación implementa la iteración y la actualización por medio de ciclos y asignaciones. En el caso de lenguaje C, el proceso iterativo se puede hacer por ejemplo por medio de la estructura dowhile(Condición) la cual se ejecuta una y otra vez mientras la condición sea verdadera. En el caso de la actualización, los lenguajes imperativos proporcionan la posibilidad de que una variable puede cambiar su valor en memoria, esto se puede hacer mediante una asignación. Por su parte, Haskell no ofrece estructuras cíclicas ni asignaciones, esto se debe a la naturaleza misma de la programación funcional la cual actúa por medio de declaraciones y reducción de expresiones. Sin embargo la iteración y la actualización de variables se puede superar por medio de declaraciones 11 recursivas. Es fácil demostrar por inducción matemática que todo proceso iterativo se puede expresar como un proceso recursivo. La figura 7 nos muestra la función fIter la cual realiza un proceso Iterativo mediante la recursión. Observe como fIter se define haciendo uso de sí misma y hace uso de valores enteros para determinar el número de ciclos de la iteración ( init. y final). En el llamado recursivo init aumenta en una unidad y el proceso termina una vez init sea igual a final, de lo contrario sigue modificando el valor g a través de alguna función h. fIter g fIter :: a -> Int -> Int -> b fIter g init final = if (init==final) then g else fIter (h g) (init+1) final h(g) Figura 7. Proceso Iterativo haciendo uso de la recursión. La implementación llevada a cabo se realizó pensando en los algoritmos de entrenamiento de redes feedforward multicapa. La arquitectura de una capa de este tipo de red luce como en la figura 8: Figura 8. Arquitectura de una capa. Por lo tanto es necesario declarar los siguientes tipos como se aprecia en la figura 9: type Input = [[Float]] type Target=[[Float]] type NeuralNetwork = ( [[Float]],[[Float]] ) Figura 9. Función zipmatriz. Los tipos Input y Target no son más que una redeclaración de una matriz de tipo Float. Por su parte, el tipo NeuralNetwork se define como una pareja ordenada donde el primer componente 12 corresponde a la matriz de pesos y la segunda componente corresponde a la matriz de bias de la capa, es decir W , b . Para simular una capa se define la función simlayer: simLayer simLayer pQ (w,b) f :: Input->NeuralNetwork->(Float->Float)->Target = mapMatriz f (sumarVectorColumnas (pMatriz w pQ) b) Figura 10. Función simlayer. Observe en la figura 10 la gran similitud sintáctica con la formulación matemática de la salida de una capa, pues el llamado de esta función seria de la siguiente forma: a=simlayer p (W,b) f Un hecho interesante estriba en que la función de activación f puede ser definida en un módulo anterior y luego ser llamada, de esta forma se puede definir una arquitectura perceptron o una Adalaine respectivamente como: simLayer p (w,b) hardlim simLayer p (w,b) pureline La función anterior nos permite definir una función para el caso en el cual tenemos una arquitectura de cualquier cantidad de capas y neuronas en cada capa (figura 11). simMultilayer :: Input->[NeuralNetwork] ->[(Float->Float,Float->Float)] ->[Target] simMultilayer _ [] _ = [] simMultilayer pQ (g:gs) ((f,f'):fs) = a:simMultilayer a gs fs where a=(simLayer pQ g f) Figura 11. Función simMultilayer. Una vez se ha definido una función de simulación, se necesario considerar toda una serie de funciones auxiliares para el manejo de la información como lo son: Funciones de conversión lista a Neuralnetwork y viceversa Funciones para inicializar los pesos y bias. Finalmente hace falta construir las funciones de entrenamiento y de interfase con el usuario para conformar la primera fase de la librería. Estas son las siguientes: Función trainperceptron para entrenar una red Perceptron 13 Función trainAdaline para entrenar una red Adaline Función trainBPGSD para entrenar una red Backpropagation con gradiente descendente Función trainBPbm para entrenar una red Backpropagation con gradiente descendente en modo cascada. Función trainBPM para entrenar una red Backpropagation con momentum Función trainBPMX para entrenar una red Backpropagation con rata de aprendizaje variable Función trainCGBP para entrenar una red Backpropagation con gradiente conjugado Función trainLMBP para entrenar una red Backpropagation con Levenberg-Marquardt Es de interés notar que la cantidad de código para la creación de la librería es realmente pequeño en comparación con otros lenguajes de programación. (No supera los 70K), para apreciar esto en detalle observe en la figura 12 el código de la función trainperceptron, la cual recibe como argumentos la matriz P de entrada, la matriz de T de supervisión y las matrices de pesos y bias de la red. Esta retorna las matrices de pesos y bias entrenadas. trainPerceptron :: Input->Target->NeuralNetwork->Int->[(Int,NeuralNetwork)] trainPerceptron p t g sh = tPerceptron p' t' g size 0 sh where p' =(headArray p) t' =(headArray t) size =(fromInteger (long (head t))) tPerceptron :: [Input]->[Target]->NeuralNetwork ->Int ->Int ->Int->[(Int,NeuralNetwork)] tPerceptron p t g q epoca sh = let { prueba=trainning p t g 0; next =if ((epoca `mod` sh)==0) then (epoca,([],[])):r else r; r =tPerceptron p t (snd prueba) q (epoca+1) sh } in if (fst(prueba)==q) then [(epoca,g)] else next trainning :: [Input]->[Target]->NeuralNetwork->Int->(Int,NeuralNetwork) trainning [] _ g s = (s,g) trainning (x:xs) (y:ys) g s = if (esCero e 0)==0 then trainning (xs)(ys) g (s+1) else trainning (xs) (ys)(actualizar x e g) s where e = restaMatriz y (simLayer x g hardlim) esCero esCero [] :: [[Float]]->Int->Int s= s 14 esCero (x:xs) s = if x==[0] then esCero xs s else esCero xs (s+1) actualizar :: [[Float]]->[[Float]]->NeuralNetwork->NeuralNetwork actualizar p e (w,b) = (sumaMatriz w (pMatrizC e p) , sumaMatriz b e) Figura 12. La Función trainPerceptron y sus funciones auxiliares. Sesión de Entrenamiento Para entrenar y diseñar una red mediante la librería de Redes Neuronales, se puede usar Hugs 98, un programa con la capacidad de interpretar el lenguaje Haskell 98. Otra alternativa es el compilador GHC (Glasgow Haskell Compiler) con la posibilidad de crear archivos ejecutables. La librería es portable y se puede usar tanto en el interpretador Hugs 98 como en el compilador Glasgow. Como ejemplo, observe la sesión de entrenamiento de la figura 13, la cual puede ser escrita en un fichero de extensión (*.txt) y guardada con extensión (*.hs). Module ParityProblem where import Transfer import NeuralNetwork import Interface p p ::Input =[ [-1,-1,2,2],[0,5,0,5] ] // Matriz de Entrada t t ::Target =[[-1,-1,1,1]] // Matriz de Supervisión brain brain :: [NeuralNetwork] // Lista de Parejas de (W,b) Matriz de Pesos y Bias = [([[-0.27,0.28],[-0.41,0.5],[0.25,0.3] ], [ [-0.48 ],[-0.13],[-0.2] ]),( [[0.09,-0.17,0.1]],[[0.48]])] ft ft :: [(Float->Float,Float->Float)] // Lista de funciones de activación por capas = [(tansig,dvtansig),(purelin,dvpurelin)] main = entrenarBPGSD p t brain ft 0.05 300 50 0.00001 Figura 13. Sesión de Entrenamiento. Al ejecutar este módulo y llamando a la función main 0.01 879 se obtiene el resultado que se muestra en la figura 14: 15 "Epoca 0/300, MSE 1.43410770/0.00001000" "Epoca 50/300, MSE 0.00170720/0.00001000" "Epoca 100/300, MSE 0.00018103/0.00001000" "Epoca 150/300, MSE 0.00004919/0.00001000" "Epoca 200/300, MSE 0.00001671/0.00001000" "Epoca 226/300, MSE 0.00000997/0.00001000" Simulacion : [-1.00466200 -0.99593710 1.00121180 0.99959400] Supervision : [-1.00000000 -1.00000000 1.00000000 1.00000000] Figura 14. Resultados de Entrenamiento. La librería ofrece la posibilidad de almacenar los pesos y bias de la red en un archivo de texto. Estudio Comparativo Para estudiar el desempeño de la implementación se utilizaron varios problemas de referencia que aparecen en la literatura de las redes neuronales. Estos se encuentran clasificados según su naturaleza y el tamaño de la información que procesan. Para revisar de forma relativa la eficiencia en tiempo de entrenamiento de la librería, se realizan 20 corridas de cada función sobre un mismo problema, modificando los pesos iniciales de la red y los parámetros de entrenamiento de cada función. En esta comparación la mejor respuesta se considera aquella que ofrezca el menor error medio cuadrático. Dicha respuesta será comparada con Matlab ejecutándola 10 veces y utilizando los mismos parámetros y pesos iniciales de entrenamiento, el mejor tiempo es tomado en ambos casos. Todas las pruebas se realizaron en un PC con procesador AMD-ATHLON de 850 Mhz con 256 Mb de memoria, utilizando a Windows 98 como sistema operativo. El compilador para la librería en Haskell fué GHC compiler versión 5.04.3 para Windows y los tiempos fueron capturados mediante la función getClockTime, una función pre-definida de Haskell. Para la comparación se usó Matlab versión 6.1.0.450 release 12.1. La siguiente tabla hace un resumen de los problemas de entrenamiento: Problema Seno Encoder Tipo de Problema Configuración Tolerancia (mse) Aprox. Funciones 1-5-1 0.002 Aprox. Funciones 10-5-10 0.01 16 Paridad 3 bits Paridad 6 bits Rec. de Patrones Rec. de Patrones 3-3-1 6-12-1 0.001 0.001 Tabla 1. Problemas de referencia para analizar el desempeño . En las siguientes tablas 2, 3 y 4 se observa la comparación realizada entre la librería de Haskell y Matlab, aquí T(s), significa tiempo en segundos, E significa el número de épocas y mse el error medio cuadrático. Haskell BPbm BPM BPMX BPGC BPLM T(s) 151 25 50 18 3 E 15000 2419 3130 208 25 Mse 0.003 0.0019985 0.00199994 0.00165426 0.00016678 Matlab traingd traingdm traingdx traincgf trainlm T(s) 70.09 75.41 14.94 1.76 0.39 E 15000 15000 3168 115 23 Mse 0.00215947 0.48345 0.00199951 0.00198608 0.001551 Tabla 2. Comparación entre los mejores resultados obtenidos por las funciones de la librería de Haskell y sus respectivas funciones homologas en Matlab para el entrenamiento de la función sen(x) . Haskell BPbm BPM BPMX BPGC BPLM T(s) 23 1 13 3 1 E 8654 459 3120 95 8 Mse 0.0009986 0.0009591 0.00085503 0.0006834 0.00015417 Matlab traingd traingdm traingdx traincgf trainlm T(s) 34.82 2.25 13.73 1.93 0.27 E 8654 459 3230 160 8 Mse 0.000998828 0.000995905 0.000972952 0.000991767 0.000153235 Tabla 3. Comparación entre los mejores resultados obtenidos por las funciones de la librería de Haskell y sus respectivas funciones homologas en Matlab para el problema de la paridad de 3 bits. Haskell BPbm BPM BPMX BPGC BPLM T(s) 53 26 3 31 123 E 3629 1595 103 172 7 Mse 0.00999232 0.00998201 0.00927069 0.0099747 0.00071241 Matlab traingd traingdm traingdx traincgf trainlm T(s) 44.05 47.35 0.94 1.04 1.26 E 10000 10000 146 69 92 Mse 0.0513344 0.0208439 0.00930574 0.0100008 0.0100004 Tabla 4. Comparación entre los mejores resultados obtenidos por las funciones de la librería de Haskell y sus respectivas funciones homologas en Matlab para el problema del Encoder 10-5-10. El estudio comparativo muestra que el desempeño de la librería es bastante bueno teniendo como referente a Matlab para los problemas propuestos. La sección anterior también muestra un código claro, corto en comparación con los lenguajes imperativos y sin la necesidad de usar arreglos e índices. Sin embargo se mostró que se requiere de mucho más tiempo para realizar las operaciones. 17 Lo anterior permite formular la siguiente pregunta: ¿Cuál puede ser el papel de Haskell frente a la computación numérica en la actualidad? Haskell y La computación Numérica La programación funcional ofrece la oportunidad de crear un código mucho más comprensible y fácil de manejar gracias a la similitud sintáctica con la matemática, los altos niveles de abstración permiten crear un código más estructurado y reusable. No se pide obtener una alta eficiencia en tiempo por parte de Haskell, pues su misma naturaleza como lenguaje de alto nivel lo impide, además la relativa juventud de este tipo de programación ofrece un gran campo de investigación en el cual se puede pensar en buscar nuevas alternativas para la construcción de compiladores más eficientes, como también en la formación de alianzas con lenguajes de bajo nivel para la optimización de algunas operaciones, como es el caso del producto de valores de punto flotante. Lo importante a destacar estriba en las posibilidades que tiene Haskell para la creación y experimentación de nuevos algoritmos en el campo de la computación numérica. Como ejemplo es posible, aprovechar el lenguaje para la experimentar modificaciones en los algoritmos objeto de estudio, este es el caso del algoritmo para entrenar una red neuronal por medio del gradiente descendente haciendo uso del momentum para filtrar la oscilación del algoritmo frente a la rata de aprendizaje. La regla para calcular el gradiente es: Una posible modificación de este algoritmo consiste en almacenar el gradiente negativo multiplicado por la rata de aprendizaje de la iteración anterior: Esta modificación no implica un cambio significativo en la estructura del código de la función trainBPM, sin embargo se obtienen interesantes resultados en el caso del problema de la paridad con 3 bits. La tabla 5 contiene la comparación entre trainBPM y la función modificada 18 trainBPMmod, considerando 20 corridas de ambas funciones con diferentes pesos iniciales y diferentes parámetros de entrenamiento. En este caso se registró el número de épocas y el mse de cada sesión, el máximo número de épocas permitidas fué de 15000. Epocas Epocas No. Semilla Lr Mo trainBPM mse trainBPMmod mse mod 1 742 0,4 0,8 2339 0,00098567 1057 0,00090055 2 635 0,6 0,8 1662 0,00099051 617 0,00091584 3 978 0,4 0,9 15000 0,21901823 1710 0,00096925 4 30 0,7 0,9 5066 0,00099667 1469 0,00089077 5 730 0,5 0,9 691 0,0009966 679 0,00096144 6 230 0,5 0,9 768 0,00099915 835 0,000993 7 316 0,6 0,9 15000 0,4999997 804 0,00098025 8 246 0,8 0,9 2470 0,0009996 4248 0,00099187 9 77 0,6 0,9 1216 0,00099865 682 0,00096919 10 508 0,6 0,9 15000 0,5 2698 0,00093841 11 92 0,6 0,9 15000 0,499999 4251 0,00094779 12 513 0,6 0,9 943 0,00099666 1062 0,00094563 13 715 0,6 0,9 1440 0,00099993 814 0,00095915 14 524 0,6 0,9 1007 0,00099917 576 0,00099122 15 301 0,6 0,9 15000 0,16697064 787 0,00098572 16 129 0,6 0,9 15000 0,18768963 822 0,00095913 17 753 0,6 0,9 15000 0,15018547 811 0,00094694 18 171 0.6 0.9 15000 0.18825182 1031 0.00096965 19 94 0,6 0,6 337 0,00099996 657 0,00099954 20 953 0,6 0,9 15000 0.18760048 1054 0.00097735 Tabla 5. Comparación entre los mejores resultados obtenidos por las funciones trainBPM y trainBPMmod de la librería de Haskell. Observe en la tabla 5 como la modificación trainBPMmod permite encontrar resultados en aquellas situaciones donde la función original no converge. El próximo paso consistiría entonces en revisar las propiedades matemáticas de este resultado, las posibilidades que este ofrece, y posteriormente se puede pensar en la implementación de la misma en un lenguaje de bajo nivel. Conclusiones y Trabajo Futuro En el transcurso de la investigación, en lo atinente a la revisión bibliográfica; el diseño y la implementación de la librería; el desarrollo de la interfase y el estudio comparativo con Matlab; se destacan las siguientes conclusiones: 19 Quedó demostrado que no es necesario usar índices y arreglos para el diseño e implementación de algoritmos que simulen y entrenen redes neuronales. Haskell posee grandes posibilidades para el diseño y evaluación experimental de nuevos algoritmos de forma rápida gracias a la cercanía que tiene con la especificación del problema a implementar y su similitud sintáctica con la matemática. Haskell permite expresar algoritmos de forma clara y simple, esto es útil en el momento de desarrollar y de derivar versiones más eficientes a bajo nivel. La librería sirve con fines educativos, se puede usar para que los estudiantes aprendan a diseñar y a entrenar redes neuronales como también la forma como se implementan en lenguajes funcionales. Se desarrolló una técnica efectiva de escribir e implementar algoritmos de redes neuronales considerando todas las ventajas y propiedades esenciales de la programación funcional. Haskell como lenguaje de programación funcional puro, aún no está preparado para competir por la eficiencia en tiempo, su característica de lenguaje de alto nivel le impide tal rapidez, por tal motivo es necesario la generación de alianzas con lenguajes de bajo nivel que se encarguen de hacer el trabajo pesado y menos significativo; en el caso de las redes neuronales el producto punto y el producto de matrices. Como ejemplo de estas alianzas, muchos de los algoritmos de Matlab están desarrollados en una eficiente librería de bajo nivel diseñada para el álgebra lineal numérica conocida como LAPACK. Adicionalmente Matlab hace un uso cuidadoso de C y ensamblador en muchas de sus rutinas. Estas alianzas han logrado optimizar significativamente las operaciones que se pueden realizar en Matlab, como referencia, para ciertas versiones de Matlab, calcular el producto de matrices de tamaño 528 tomaba hasta 25 segundos, las nuevas versiones pueden realizar esta operación en menos de 3.6 segundos. Trabajo Futuro 20 Se hace necesario un estudio sobre las posibilidades de hacer una alianza entre lenguajes de bajo nivel y Haskell para la construcción de algoritmos precompilados de bajo nivel que puedan ser usados por Haskell. A nivel teórico es necesario observar las posibilidades que Haskell ofrece para el diseño de nuevos algoritmos de redes neuronales puramente funcionales, explicando la teoría de las redes desde el lambda cálculo. 21 Referencias 1. BIRD, R. (2000), Introducción a la Programación Funcional con Haskell , Prentice Hall. 2. DEMUTH, H. (2000) Neural Network Matlab Toolbox. TheMathWorks,Inc. 3. 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. 4. FAUSETT, L. (1994) Fundamentals of Neural Networks. Prentice Hall. 5. HAGAN, M. DEMUTH, H. y BEALE, M. (1995) Neural Network Design. Plus Publishing Company. 6. HILERA, J. y MARTINEZ, V.(2000) Redes Neuronales Artificiales. Alfaomega. 7. HUGHES, J. (1990) Why Functional Programming Matters. Tutorial. 8. JONES, M. y PETERSON, J. (1999) Hugs 98. Manual de Usuario 9. MOLER, C. (2000)Matlab Incorporates Lapack. Matlab News And Notes. 10. PEYTON S. (2000), A Gentle Introduction to Haskell , Tutorial. 11. RAO, V. y RAO, H (1993), (1993) C++ Neural Networks and Fuzzy Logic, EditMis Press. 12. RIEDMILER, M. (1994) Advanced Supervised Learning in Multi-Layer Perceptrons from Backpropagation to Adaptive Learning Algorithms. Journal of Computer Standards and Interfaces Special Issue on Neural Networks. 13. RUSELL, S. y NORVIG, P. (1996) Inteligencia Artificial: Un Enfoque Moderno. Prentice Hall. 14. SERRARENS, P. (1999) Implementation the Conjugate Gradient Algorithm in a Functional Language. Computer Science Institute University of Nijmegen 15. SHANG, Y. y WAH, B. (1996) Global Optimization for Neural Network Training. Coordinated Science Laboratory University of Illinois. 16. SKIBINSKY, J. (1998). Indexless Linear Algebra Algorithms. Numeric Quest Inc Hunstsville-Ontario Canada. 17. VAN ROY P. y HARIDI Computer Programming. S., (2002) Concepts, Techniques, and Models of 22