Redes neuronales: de la teoría a la práctica

Dmitriy Parfenovich | 8 mayo, 2014

Introducción

Hoy en día, cualquier operador ha oído hablar de las redes neuronales y conoce las ventajas de su utilización. La mayoría de ellos creen que quien puede trabajar con redes neuronales es una especie de superman. En este artículo intentaré explicarle la arquitectura de la red neuronal, describir sus aplicaciones y dar ejemplos de su uso práctico.

 

Concepto de redes neuronales

Las redes neuronales artificiales pertenecen a una de esas áreas de la investigación en inteligencia artificial basadas en los intentos de simular el sistema nervioso del ser humano y su capacidad para aprender y adaptarse, lo que nos permitirá crear una simulación muy genérica del funcionamiento del cerebro humano.

Lo más curioso es que las redes neuronales artificiales están formadas por neuronas artificiales.

Fig. 1. Modelo de neurona artificial
Fig. 1. Modelo de neurona artificial

La estructura de una neurona puede ser representada como una composición de las siguientes unidades:

  1. Entradas Entradas;
  2. Pesos Pesos;
  3. Función de transferencia Función de transferencia y entrada neta Entrada neta de una neurona;
  4. Función de activación Función de activación;
  5. Salida Salida.

Las redes neuronales tienen muchas propiedades y la capacidad de aprender es la más destacada. El proceso de aprendizaje se reduce a cambiar los pesos Pesos.

Cálculo de la entrada neta de la neurona

Entrada neta de una neurona esta es la entrada neta de la neurona.

Fórmula de la función de activación

La entrada neta se transforma luego en la salida mediante la función de activación que veremos más tarde. En pocas palabras, puede considerarse una red neuronal como una "caja negra" que recibe señales como entradas y proporciona resultados como salidas.

Fig. 2. Modelo de una red neuronal multicapa
Fig. 2. Modelo de una red neuronal multicapa

Este es el aspecto de una red neuronal multicapa. Comprende:

Este ejemplo muestra la red neuronal con dos capas ocultas. Pero puede haber redes neuronales con más capas ocultas.

 

Normalización de los datos de entrada

La normalización de los datos de entrada es el proceso por el se normalizan todos los datos de entrada, es decir, se reducen a los rangos [0,1] o [-1,1]. Si no se realiza la normalización los datos de entrada tendrán un efecto adicional sobre la neurona, dando lugar a decisiones incorrectas. En otras palabras, ¿cómo podemos comparar los valores con diferentes órdenes de magnitud?

La normalización, en su forma estándar, es como sigue:

Fórmula de la normalización

donde:

Permítanme explicarlo usando un ejemplo:

Supongamos que tenemos n datos de entrada desde el rango [0, 10], luego Valor mínimo de x = 0 y Valor máximo de x = 10. Reduciremos los datos al rango [0, 1], luego d1 = 0 y d2 = 10. Ahora, una vez introducidos los datos en la fórmula podemos calcular los valores normalizados para cualquier x a partir de n datos de entrada.

Así es queda cuando se implementa en MQL5:

double d1=0.0;
double d2=1.0;
double x_min=iMA_buf[ArrayMinimum(iMA_buf)];
double x_max=iMA_buf[ArrayMaximum(iMA_buf)];
for(int i=0;i<ArraySize(iMA_buf);i++)
  {
   inputs[i]=(((iMA_buf[i]-x_min)*(d2-d1))/(x_max-x_min))+d1;
  }

Primero especificamos los límites superiores e inferiores del valor de salida y luego obtenemos los valores máximos y mínimos del indicador (se ha obviado el copiado de datos del indicador, aunque pueden ser, por ejemplo, los últimos 10 valores). Por último, normalizamos cada elemento de entrada (valores del indicador en barras distintas) y guardamos los resultados en una matriz para usarlos luego.

 

Funciones de activación

La función de activación calcula la información de salida de una neurona. La entrada que recibe representa la suma de todos los productos de las entradas y sus respectivos pesos (en adelante "suma ponderada"):

Fig. 3. Modelo de neurona artificial con la descripción de la función de activación
Fig. 3. Modelo de neurona artificial con la descripción de la función de activación

La función de activación, en su forma estándar, es como sigue:

Fórmula de la función de activación

donde:

Los principales tipos de funciones de activación son:

  1. El paso unitario o función de umbral dura.
    Gráfico del paso unitario o función de umbral dura.
    La función se describe en la siguiente fórmula:
    Fórmula de la función
    Si la suma ponderada es menor que el valor especificado, la función de activación devuelve cero. Si la suma ponderada es mayor, la función de activación devuelve uno.

  2. La función sigmoidea
    Gráfico de la función sigmoidea
    La fórmula que describe la función sigmoidea es la siguiente:
    Fórmula que describe la función sigmoidea
    Se usa a menudo en redes neuronales multicapa y otras redes con señales continuas. La suavidad y continuidad de la función son propiedades muy positivas.

  3. La tangente hiperbólica
    Gráfico de la función tangente hiperbólica
    Fórmula:
    Fórmula que describe la función tangente hiperbólicao Fórmula que describe la función tangente hiperbólica
    También se usa a menudo en redes con señales continuas. Tiene de especial que puede devolver valores negativos.

 

Cambiar la forma de la función de activación

En la sección previa hemos trabajado con los distintos tipos de funciones de activación. Ahí hay otra importante cuestión a considerar: la pendiente de la función (excepto para la función umbral dura). Veamos con mayor detenimiento la función sigmoidea.

Al observar el gráfico de la función, uno puede ver fácilmente que la función es suave a lo largo del rango [-5, 5]. Supongamos que tenemos una red con una solo neurona con 10 entradas y una salida. Vamos ahora a intentar calcular los valores superiores e inferiores de la variable Entrada neta de una neurona. Cada entrada tomará un valor normalizado (como ya se mencionó en la normalización de datos de entrada), por ejemplo, desde el rango [-1,1].

Usaremos los valores de entrada negativos ya que la función no es diferenciable ni siquiera en un argumento negativo. También se elegirán los pesos a partir del mismo rango. Con todas las combinaciones posibles de entradas y pesos, obtendremos los valores extremos Entrada neta de una neurona en el rango [-10,10] como:

Cálculo de la entrada neta de la neurona

En MQL5, la fórmula será:

for(int n=0; n<10; n++) 
  {
   NET+=Xn*Wn;
  }

Ahora necesitamos trazar la función de activación en el rango como identificada. Vamos a tomar como ejemplo la función sigmoidea. La forma más sencilla de hacer esto es utilizar Excel.

Fig. 4. Gráfico Excel de la función sigmoidea
Fig. 4. Gráfico Excel de la función sigmoidea

Aquí podemos ver claramente que los valores del argumento fuera del rango [-5,5] no tienen ningún efecto sobre los resultados. Esto sugiere que el rango de valores es incompleto. Vamos a intentar arreglar esto. Añadiremos al argumento un coeficiente adicional d que nos permitirá ampliar el rango de valores.

Fig. 5. Gráfico Excel de la función sigmoidea con el coeficiente adicional aplicado.
Fig. 5. Gráfico Excel de la función sigmoidea con el coeficiente adicional aplicado.

Vamos a ver una vez más los gráficos. Hemos añadido un coeficiente adicional d=0,4 que ha cambiado la forma de la función. La comparación de los valores en la tabla sugiere que ahora están distribuidos más uniformemente. Los resultados pueden ser expresados de la siguiente forma:

for(int n=0; n<10; n++) 
  {
   NET+=Xn*Wn;
  }
NET*=0.4;

Vamos ahora a revisar la función de activación de la tangente hiperbólica. Si no tenemos en cuenta la teoría que vimos con la función previa llegamos a la aplicación práctica de inmediato. La única diferencia aquí es que la salida puede caer en el rango [-1,1]. La suma ponderada puede tomar también valores en el rango [-10, 10].

Fig. 6. Gráfico Excel de la función hiperbólica con el coeficiente adicional aplicado.
Fig. 6. Gráfico Excel de la función hiperbólica con el coeficiente adicional aplicado.

El gráfico muestra que la forma de la función ha sido mejorada debido al uso del coeficiente adicional d=0,2. Los resultados pueden ser expresados de la siguiente forma:

for(int n=0;n<10;n++) 
  {
   NET+=Xn*Wn;
  }
NET*=0.2;

De este modo podemos cambiar y mejorar la forma de cualquier función de activación.

 

Aplicación

Veamos ahora una aplicación práctica. Primero, intentaremos implementar el cálculo de la entrada neta de la neurona, seguido por la adición de la función de activación. Vamos a recuperar la fórmula para el cálculo de la entrada neta de la neurona:

Cálculo de la entrada neta de la neurona

double NET;
double x[3];
double w[3];
int OnInit()
  {
   x[0]=0.1; // set the input value х1
   x[1]=0.8; // set the input value х2
   x[2]=0.5; // set the input value х3

   w[0]=0.5; // set the weight value w1
   w[1]=0.6; // set the weight value w2
   w[2]=0.3; // set the weight value w3

   for(int n=0;n<3;n++)
     {
      NET+=x[n]*w[n]; // add the weighted net input values together
     }
  }

Examinémoslo más detenidamente:

  1. Hemos empezado declarando una variable para almacenar la entrada neta de la neurona Entrada neta de una neurona y dos matrices: entradas Entradas y pesos Pesos;
  2. Estas variables han sido declaradas al inicio, fuera de todas las funciones para darle un alcance global (para que sean accesibles desde cualquier punto del programa);
  3. En la función de inicialización OnInit() (puede ser realmente cualquier otra función) hemos completado la matriz de entrada y la matriz de pesos;
  4. A continuación se ha realizado el bucle de suma, n<3 ya que solo tenemos tres entradas y tres pesos respectivos;
  5. Luego hemos añadido los valores de entrada ponderados y los hemos almacenado en la variable Entrada neta de una neurona.

De esta forma hemos completado la primera tarea y hemos obtenido la suma. Ahora es el turno de la función de activación. A continuación se muestran los códigos para el cálculo de las funciones de activación que hemos visto en la sección funciones de activación.

Paso unitario o función de umbral dura.

double Out;
if(NET>=x) Out=1;
else Out=0;

La función sigmoidea

double Out = 1/(1+exp(-NET));

La función tangente hiperbólica

double Out = (exp(NET)-exp(-NET))/(exp(NET)+exp(-NET));

 

Reuniéndolo todo

Para hacer más fácil la implementación, tomaremos una red formada por una sola neurona. Sería algo exagerado llamarlo una red, pero lo importante es comprender el principio. Después de todo, una red neuronal multicapa está formada por las mismas neuronas que en la capa anterior de neuronas servían como entrada para la siguiente capa.

Vamos a utilizar una versión ligeramente modificada del asesor experto desarrollado en el artículo "Un inicio rápido o una breve guía para principiantes". Por tanto, vamos a reemplazar el indicador de la media móvil por el oscilador del índice de resistencia relativo. La información sobre los parámetros del indicador y su serie se encuentra disponible en la ayuda.

//+------------------------------------------------------------------+
//|                                                neuro-example.mq5 |
//|                        Copyright 2012, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2012, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>        //include the library for execution of trades
#include <Trade\PositionInfo.mqh> //include the library for obtaining information on positions

//--- weight values
input double w0=0.5;
input double w1=0.5;
input double w2=0.5;
input double w3=0.5;
input double w4=0.5;
input double w5=0.5;
input double w6=0.5;
input double w7=0.5;
input double w8=0.5;
input double w9=0.5;

int               iRSI_handle;  // variable for storing the indicator handle
double            iRSI_buf[];   // dynamic array for storing indicator values

double            inputs[10];   // array for storing inputs
double            weight[10];   // array for storing weights

double            out;          // variable for storing the output of the neuron

string            my_symbol;    // variable for storing the symbol
ENUM_TIMEFRAMES   my_timeframe; // variable for storing the time frame
double            lot_size;     // variable for storing the minimum lot size of the transaction to be performed

CTrade            m_Trade;      // entity for execution of trades
CPositionInfo     m_Position;   // entity for obtaining information on positions
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- save the current chart symbol for further operation of the EA on this very symbol
   my_symbol=Symbol();
//--- save the current time frame of the chart for further operation of the EA on this very time frame
   my_timeframe=PERIOD_CURRENT;
//--- save the minimum lot of the transaction to be performed
   lot_size=SymbolInfoDouble(my_symbol,SYMBOL_VOLUME_MIN);
//--- apply the indicator and get its handle
   iRSI_handle=iRSI(my_symbol,my_timeframe,14,PRICE_CLOSE);
//--- check the availability of the indicator handle
   if(iRSI_handle==INVALID_HANDLE)
     {
      //--- no handle obtained, print the error message into the log file, complete handling the error
      Print("Failed to get the indicator handle");
      return(-1);
     }
//--- add the indicator to the price chart
   ChartIndicatorAdd(ChartID(),0,iRSI_handle);
//--- set the iRSI_buf array indexing as time series
   ArraySetAsSeries(iRSI_buf,true);
//--- place weights into the array
   weight[0]=w0;
   weight[1]=w1;
   weight[2]=w2;
   weight[3]=w3;
   weight[4]=w4;
   weight[5]=w5;
   weight[6]=w6;
   weight[7]=w7;
   weight[8]=w8;
   weight[9]=w9;
//--- return 0, initialization complete
   return(0);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- delete the indicator handle and deallocate the memory space it occupies
   IndicatorRelease(iRSI_handle);
//--- free the iRSI_buf dynamic array of data
   ArrayFree(iRSI_buf);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- variable for storing the results of working with the indicator buffer
   int err1=0;
//--- copy data from the indicator array to the iRSI_buf dynamic array for further work with them
   err1=CopyBuffer(iRSI_handle,0,1,10,iRSI_buf);
//--- in case of errors, print the relevant error message into the log file and exit the function
   if(err1<0)
     {
      Print("Failed to copy data from the indicator buffer");
      return;
     }
//---
   double d1=0.0;                                 //lower limit of the normalization range
   double d2=1.0;                                 //upper limit of the normalization range
   double x_min=iRSI_buf[ArrayMinimum(iRSI_buf)]; //minimum value over the range
   double x_max=iRSI_buf[ArrayMaximum(iRSI_buf)]; //maximum value over the range

//--- In the loop, fill in the array of inputs with the pre-normalized indicator values
   for(int i=0;i<ArraySize(inputs);i++)
     {
      inputs[i]=(((iRSI_buf[i]-x_min)*(d2-d1))/(x_max-x_min))+d1;
     }
//--- store the neuron calculation result in the out variable
   out=CalculateNeuron(inputs,weight);
//--- if the output value of the neuron is less than 0.5
   if(out<0.5)
     {
      //--- if the position for this symbol already exists
      if(m_Position.Select(my_symbol))
        {
         //--- and this is a Sell position, then close it
         if(m_Position.PositionType()==POSITION_TYPE_SELL) m_Trade.PositionClose(my_symbol);
         //--- or else, if this is a Buy position, then exit
         if(m_Position.PositionType()==POSITION_TYPE_BUY) return;
        }
      //--- if we got here, it means there is no position; then we open it
      m_Trade.Buy(lot_size,my_symbol);
     }
//--- if the output value of the neuron is equal to or greater than 0.5
   if(out>=0.5)
     {
      //--- if the position for this symbol already exists
      if(m_Position.Select(my_symbol))
        {
         //--- and this is a Buy position, then close it
         if(m_Position.PositionType()==POSITION_TYPE_BUY) m_Trade.PositionClose(my_symbol);
         //--- or else, if this is a Sell position, then exit
         if(m_Position.PositionType()==POSITION_TYPE_SELL) return;
        }
      //--- if we got here, it means there is no position; then we open it
      m_Trade.Sell(lot_size,my_symbol);
     }
  }
//+------------------------------------------------------------------+
//|   Neuron calculation function                                    |
//+------------------------------------------------------------------+
double CalculateNeuron(double &x[],double &w[])
  {
//--- variable for storing the weighted sum of inputs
   double NET=0.0;
//--- Using a loop we obtain the weighted sum of inputs based on the number of inputs
   for(int n=0;n<ArraySize(x);n++)
     {
      NET+=x[n]*w[n];
     }
//--- multiply the weighted sum of inputs by the additional coefficient
   NET*=0.4;
//--- send the weighted sum of inputs to the activation function and return its value
   return(ActivateNeuron(NET));
  }
//+------------------------------------------------------------------+
//|   Activation function                                            |
//+------------------------------------------------------------------+
double ActivateNeuron(double x)
  {
//--- variable for storing the activation function results
   double Out;
//--- sigmoid
   Out=1/(1+exp(-x));
//--- return the activation function value
   return(Out);
  }
//+------------------------------------------------------------------+

Lo primero que necesitamos hacer es entrenar nuestra red. Vamos a optimizar los pesos.

Fig. 7. El probador de estrategias con el conjunto de parámetros requeridos.

Fig. 7. El probador de estrategias con el conjunto de parámetros requeridos.

Ejecutaremos la optimización usando los siguientes parámetros:

Fig. 8. Establecer los parámetros y sus respectivos rangos a optimizar

Fig. 8. Establecer los parámetros y sus respectivos rangos a optimizar

Se ejecutará la optimización respecto a todos los pesos y sus rangos. Iniciamos la optimización volviendo a la pestañas Settings (ajustes) y haciendo clic en el botón Start (inicio).

Fig. 9. Datos obtenidos después de la optimización

Fig. 9. Datos obtenidos después de la optimización

Después de completar la optimización, seleccionamos la pasada con el mayor beneficio (para ordenarla por uno de los parámetros hacemos clic en el encabezado de la columna correspondiente) en la pestaña Optimization Results (resultados de la optimización). Luego podemos evaluar otros parámetros y seleccionar las pasadas deseadas si el necesario.

Un doble clic en la pasada necesaria inicia la prueba de los resultados mostrados en las pestañas Results (resultados) y Graph (gráfico).

Fig. 10. Informe de prueba

Fig. 10. Informe de prueba

Fig. 11. Gráfico de saldo

Fig. 11. Gráfico de saldo

Fig. 12. Funcionamiento del trading del asesor experto

Fig. 12. Funcionamiento del trading del asesor experto

Finalmente obtenemos los resultados y para ser el comienzo no está mal del todo. Tenga en cuenta que solo teníamos una neurona. Este ejemplo es muy primitivo pero hemos de admitir que incluso él solo puede conseguir beneficios.

 

Ventajas de las redes neuronales

Vamos ahora a intentar comparar un asesor experto basado en la lógica estándar con un asesor experto según una red neuronal. Compararemos los resultados de la optimización y las pruebas del asesor experto de la muestra MACD que viene junto con el terminal con los del asesor experto según una red neuronal basado en MACD.

Los valores de Take Profit y Trailing Stop no estarán implicados en la optimización ya que no se encuentran en el asesor experto basado en una red neuronal. Los dos asesores expertos que vamos a probar están basados en MACD con los siguientes parámetros:

También puede establecer el par de divisas requerido y el marco temporal, pero en nuestro caso los dejaremos como están: EURUSD y H1, respectivamente. El periodo de prueba es el mismo en ambos casos: desde el principio del año usando precios de apertura.

Muestra de MACD macd-neuro-examle
Probador de estrategias con los parámetros para la muestra MACD
Probador de estrategias con los parámetros para la muestra macd-neuro-example
Establecer los parámetros y sus respectivos rangos a optimizar
Establecer los parámetros y sus respectivos rangos a optimizar
Datos obtenidos después de la optimización
Datos obtenidos después de la optimización
Informe de prueba
Informe de prueba
Gráfico de saldo
Gráfico de saldo


Vamos a comparar los parámetros clave de los asesores expertos probados:

Parámetro Muestra de MACD macd-neuro-examle
Beneficio neto total 733,56 2.658,29.
Valor absoluto de reducción del saldo 0,00 534,36
Valor máximo de la reducción del capital 339,50 (3,29%) 625,36 (6,23%)
Factor de beneficio 4,72 1,55
Factor de recuperación 2,16 4,25
Retribución esperada 30,57 8,08
Ratio de Sharpe 0,79 0,15
Transacciones totales 24 329
Contratos totales 48 658
Transacciones con beneficio (% del total) 21 (87,50%) 187 (56,84%)
Transacción con beneficio promedio 44,33 39,95
Transacciones con ganancias promedio 5 2

 

Fig. 13. Comparación de los parámetros clave

Fig. 13. Comparación de los parámetros clave

 

Conclusión

Este artículo ha tratado sobre los aspectos que debemos conocer al diseñar asesores expertos usando redes neuronales. Nos ha mostrado la estructura de una neurona y la arquitectura de una red neuronal, ha descrito las funciones de activación y los métodos para cambiar la forma de la función de activación, así como el proceso de optimización y normalización de datos de entrada. Además, hemos comparado un asesor experto basado en la lógica estándar con un asesor experto según una red neuronal.