Implementación de Redes Neuronales Artificiales en Haskell

Anuncio
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
(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. 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
Descargar