Redes neuronales recurrentes y series temporales
Las redes neuronales recurrentes constituyen una herramienta muy apropiada para modelar series temporales. Se trata de un tipo de redes con una arquitectura que implementa una cierta memoria y, por lo tanto, un sentido temporal. Esto se consigue implementando algunas neuronas que reciben como entrada la salida de una de las capas e inyectan su salida en una de las capas de un nivel anterior a ella. En este artículo voy a mostrar cómo utilizar de una forma sencilla dos redes neuronales de este tipo, las de Elman y las de Jordan, utilizando el programa R.
En las redes de Elman, las entradas de estas neuronas, se toman desde las salidas de las neuronas de una de las capas ocultas, y sus salidas se conectan de nuevo en las entradas de esta misma capa, lo que proporciona una especie de memoria sobre el estado anterior de dicha capa. El esquema es como en esta figura, donde X es la entrada, S la salida y el nodo amarillo es la neurona de la capa de contexto:
En las redes de Jordan, la diferencia está en que la entrada de las neuronas de la capa de contexto se toma desde la salida de la red:
Precisamente por esta característica de memoria son apropiadas para modelar series temporales. Vamos a ver cómo utilizarlas con el programa R. En primer lugar, se deben cargar los paquetes que implementan estas redes neuronales. Yo voy a utilizar el paquete RSNNS para las redes y, de manera secundaria, el paquete quantmod para algunas operaciones.
Como serie temporal de ejemplo, utilizaré en primer lugar una serie generada por medio de la función logística, en la zona de dinámica caótica. La dinámica caótica hace que la serie tenga una estructura compleja, haciendo que la predicción de valores futuros que podemos hacer sea muy complicada o imposible. Esta serie se obtiene a partir de la siguiente función:
Xn+1 = µXn(1-Xn)
Cuando el parámetro µ toma valores a de aproximadamente 3,5, la dinámica de la serie generada se vuelve caótica. En este enlace podéis descargar un fichero csv con 1000 valores de la serie logística, con el valor inicial 0,1 y un valor de µ de 3,95.
Empezamos cargando los paquetes y los datos de la serie:
require(RSNNS)
require(quantmod)
slog<-as.ts(read.csv("logistic-x.csv",F))
Los valores de la ecuación logística están todos en el rango (0,1) y no hace falta preprocesarlos, de lo contrario resulta conveniente escalarlos. Como tenemos 1000 valores, vamos a utilizar 900 para entrenar la red neuronal. Para ello definimos la variable train:
train<-1:900
Vamos a definir, como variables de entrenamiento de la serie, los n valores anteriores de la serie. La elección de n es arbitraria, yo he seleccionado 10 valores, pero es posible que, dependiendo del problema que estemos tratando, sea más conveniente otro valor. Por ejemplo, si tenemos valores mensuales de una variable, es posible que 12 sea un valor mejor para n. Lo que haremos será crear un data frame con n columnas, cada una de las cuales adelantada un valor de la serie en el futuro, a través de una variable de tipo zoo:
y<-as.zoo(slog)
x1<-Lag(y,k=1)
x2<-Lag(y,k=2)
x3<-Lag(y,k=3)
x4<-Lag(y,k=4)
x5<-Lag(y,k=5)
x6<-Lag(y,k=6)
x7<-Lag(y,k=7)
x8<-Lag(y,k=8)
x9<-Lag(y,k=9)
x10<-Lag(y,k=10)
slog<-cbind(y,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10)
Eliminamos los valores NA producidos al desplazar la serie:
slog<-slog[-(1:10),]
Y definimos, por comodidad, los valores de entrada y salida de la red neuronal:
inputs<-slog[,2:11]
outputs<-slog[,1]
Ahora ya podemos crear una red de Elman y entrenarla:
fit<-elman(inputs[train],
outputs[train],
size=c(3,2),
learnFuncParams=c(0.1),
maxit=5000)
El tercer parámetro indica que hemos creado dos capas ocultas, una de tres neuronas y otra de una, hemos indicado un ritmo de aprendizaje de 0,1 y también un número máximo de iteraciones de 5000. Con la función plotIterativeError podemos ver cómo ha ido evolucionando el error de la red con el número de iteraciones:
Como podemos ver, el error converge a cero muy rápidamente. Ahora vamos a realizar una predicción con el resto de los términos de la serie, que tiene el siguiente aspecto gráfico:
y<-as.vector(outputs[-train])
plot(y,type="l")
pred<-predict(fit,inputs[-train])
Si superponemos la predicción sobre la serie original, vemos que la aproximación es muy buena:
lines(pred,col="red")
En este punto, puede surgir la siguiente pregunta: ¿hemos realizado una predicción perfecta de una serie caótica?, pero ¿no es esto imposible? La respuesta es que lo que realmente ha pasado aquí es que la red neuronal ha “aprendido” perfectamente la función logística, y es capaz de predecir bastante bien su siguiente valor.
De hecho, todos los valores predichos lo han sido a partir de 10 valores reales previos de la serie, si tratas de predecir nuevos términos utilizando como valores de entrada valores predichos previamente, debido a la naturaleza caótica de la serie y la sensibilidad a las condiciones iniciales, rápidamente se pierde precisión, lo que concuerda con la teoría del caos en cuanto a que los fenómenos caóticos solo son predecibles en un plazo muy corto. En cualquier caso, gracias al efecto memoria podemos adelantarnos a la serie al menos en un valor con una precisión muy buena, lo cual puede ser muy útil en algunas aplicaciones.
Si queremos realizar la misma prueba con una red de Jordan, el comando a utilizar es el siguiente:
fit<-jordan(inputs[train],
outputs[train],
size=4,
learnFuncParams=c(0.01),
maxit=5000)
Con este comando hemos indicado 4 capas ocultas y un factor de ritmo de aprendizaje de 0,01. El resultado se ajusta también bastante bien a la serie original:
pred<-predict(fit,inputs[-train])
plot(y,type="l")
lines(pred,col="red")
Como la serie generada por la ecuación logística, en el dominio caótico, presenta sensibilidad a las condiciones iniciales, puedes generar otra serie con el mismo parámetro pero empezando por un valor diferente a 0,1, dentro del intervalo (0,1). Esto generará una serie totalmente distinta a la que hemos utilizado para entrenar la red, y el modelo será capaz de predecir sin problemas el tramo de la nueva serie que queramos, lo que indica que ha “aprendido” perfectamente la ecuación logística que la genera.
La función logística genera una serie de una sola variable. Vamos a poner a prueba a la red con un sistema de dos ecuaciones con dos variables, el sistema de Henon, que también genera series de tiempo con dinámica caótica, estas son las ecuaciones:
Xn+1=1+Yn-1.4Xn2
Yn+1=0.3Xn
En este enlace podéis descargar la serie de 1000 términos de la variable X del sistema de Henon, en formato csv. Ahora tenemos una serie que depende de dos variables, generada por un sistema del que tenemos información incompleta, ya que solo disponemos de la serie de una de las dos variables. Podemos repetir el procedimiento anterior:
shen<-as.ts(read.csv("henon-x.csv",F))
y<-as.zoo(shen)
…
inputs<-(inputs - min(y))/(max(y)-min(y))
outputs<-(outputs - min(y))/(max(y)-min(y))
En este caso los valores de la serie están entre -1 y 1, por lo que los estandarizamos para que tomen valores entre 0 y 1, ya que así funciona mejor el modelo.
Y, de nuevo, realizamos una predicción de los valores de la serie restantes:
pred<-predict(fit,inputs[-train])
y<-as.vector(outputs[-train])
plot(y,type="l")
Podemos comprobar que el ajuste también es casi perfecto:
lines(pred,col="red")
Para terminar, os voy a recomendar un libro sobre el trabajo con redes neuronales con el programa R, se trata de Deep learning made easy with R, de N.D. Lewis, en el se explica de una forma sencilla y muy práctica los principales tipos de redes neuronales y sus aplicaciones.