English Русский 中文 Deutsch 日本語 Português
preview
Redes neuronales: así de sencillo (Parte 19): Reglas asociativas usando MQL5

Redes neuronales: así de sencillo (Parte 19): Reglas asociativas usando MQL5

MetaTrader 5Sistemas comerciales | 16 septiembre 2022, 10:27
549 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Contenido


Introducción

En el artículo anterior, empezamos a familiarizarnos con los algoritmos de búsqueda de reglas asociativas relacionados con los métodos de aprendizaje no supervisado. Entonces analizamos dos algoritmos para resolver este tipo de problemas: Apriori y FP-Grows, El cuello de botella para el algoritmo Apriori se encuentra en el gran número de llamadas a la base de datos para identificar los soportes de los candidatos a patrones frecuentes. El método FP-Growth lo resuelve construyendo un árbol en el que se comprima toda la base de datos. Y todo el trabajo posterior se realiza con el árbol FP, sin la referencia a la base de datos. Esto aumenta la velocidad a la que se podemos completar la tarea, dado que el árbol FP cabe en la RAM, y se puede acceder a ella mucho más rápido que realizando una consulta completa de la base de datos.


1. Variante de uso del método en el trading

Antes de comenzar la creación del algoritmo para buscar las reglas asociativas usando MQL5, deberemos pensar en cómo podemos utilizarlas en nuestro comercio.

Los algoritmos de minería de reglas asociativas han sido diseñados para encontrar relaciones estables entre características binarias en una base de datos. Por lo tanto, con la ayuda de estos algoritmos, podremos encontrar algún tipo de relación estable entre las distintas características. Pueden ser, por ejemplo, diversas pautas compuestas por varios indicadores y/o instrumentos. Al algoritmo no le importa que cada característica individual suponga diferentes lecturas o que sean valores del mismo indicador en diferentes intervalos de tiempo. El algoritmo valora cada atributo como independiente. Así que, ¿por qué no tratamos de cruzar este algoritmo con la experiencia de los métodos de aprendizaje supervisado? Podemos tomar algunas características objetivo y añadirlas a la muestra de entrenamiento con datos históricos, pidiendo luego al algoritmo que encuentre para nosotros las reglas asociativas por las que se generarán nuestros valores objetivo. 

Tenemos un algoritmo y la idea de usarlo para resolver nuestros problemas prácticos. Vamos a ver la posibilidad de implementarlo con la ayuda de MQL5, y luego poner a prueba nuestra idea.


2. Implementando el algoritmo FP-Growth

Mientras comenzamos a implementar el algoritmo FP-Growth, analizado en el artículo anterior, vamos a recordar que este se basa en un árbol de decisión. La biblioteca estándar MQL5 ofrece la clase CTree para construir un árbol binario. Lamentablemente, no nos sentimos del todo cómodos con la opción del árbol binario, ya que el número de ramas de un solo nodo puede ser superior a 2 (proporcionado por la implementación binaria), mientras se construye el árbol FP. Por ello, antes de construir el algoritmo en sí, vamos a crear la clase CMyTreeNode para implementar un nodo de árbol con múltiples ramas.

2.1. Implementando la clase de nodo de árbol

Crearemos esta clase como heredera de la clase MQL5 CArrayObj estándar del array de objetos dinámicos. Así, la elección de esta clase como clase padre se debe a la funcionalidad disponible para crear y mantener un array dinámico de objetos, que serán nuestros nodos de rama.

Además, hemos añadido 3 variables a la nueva clase para implementar la funcionalidad necesaria dentro del algoritmo que estamos construyendo:

  • m_cParent — es un puntero al objeto del nodo padre anterior. El puntero se dejará en blanco para la raíz del árbol;
  • m_iIndex — índice de características en la base de datos de origen, lo usaremos para identificar las características;
  • m_dSupport — variable para registrar el soporte de la función.

class CMyTreeNode : public CArrayObj
  {
protected:
   CMyTreeNode      *m_cParent;
   ulong             m_iIndex;
   double            m_dSupport;

public:
                     CMyTreeNode();
                    ~CMyTreeNode();
  }

En el constructor de la clase, estableceremos los valores iniciales de las variables y borraremos el array dinámico. El destructor de la clase lo dejaremos vacío.

CMyTreeNode::CMyTreeNode() :  m_iIndex(ULONG_MAX),
                              m_dSupport(0)
  {
   Clear();
  }

Para tratar con las variables de clase ocultas, crearemos una serie de métodos que usaremos al construir el algoritmo FP-Growth. Iremos viendo la finalidad de los métodos a medida que los utilicemos.

class CMyTreeNode : public CArrayObj
  {
   ........
public:
   ........
   //--- methods of access to protected data
   CMyTreeNode*      Parent(void)           const { return(m_cParent); }
   void              Parent(CMyTreeNode *node)  {  m_cParent = node; }
   void              IncreaseSupport(double support)  { m_dSupport += support; }
   double            GetSupport(void)       {  return m_dSupport;  }
   void              SetSupport(double support)       { m_dSupport = support;  }
   ulong             ID(void)             {  return m_iIndex;  }
   void              ID(ulong ID)         {  m_iIndex = ID; }
  };

Para calcular el nivel de confianza, crearemos el método GetConfidence, en el que primero comprobaremos el puntero al nodo anterior y, si es válido, dividiremos el soporte del nodo actual por el soporte del nodo padre.

Tenga en cuenta que el algoritmo del árbol FP está organizado de tal forma que el soporte de cualquier nodo no podrá ser mayor que el soporte del nodo padre. Por consiguiente, el resultado de las operaciones de este método será siempre positivo y no mayor que "1".

Tampoco realizaremos una comprobación de división por cero, ya que los nodos se añaden al árbol a partir de las transacciones existentes. Por ello, si un nodo está en el árbol, significará que su característica ha aparecido al menos una vez en la base de datos y tiene un soporte mínimo.

double CMyTreeNode::GetConfidence(void)
  {
   CMyTreeNode *parent = Parent();
   if(!parent)
      return 1;
//---
   double result = m_dSupport / parent.GetSupport();
   return result;
  }

También añadiremos el método AddNode para crear un nuevo nodo de ramificación. En los parámetros del método, transmitiremos el identificador de la característica en la base de datos de la muestra de entrenamiento original y el soporte del nodo creado. Retornará a nuestro método el puntero al objeto creado.

En el cuerpo del método, creamos un nuevo ejemplar de nodo de nuestro árbol y comprobamos inmediatamente el resultado de la operación. Si se produce un error, retornaremos un puntero de objeto no válido.

A continuación, especificaremos el identificador del nodo creado y le transmitiremos un puntero al objeto actual como objeto padre.

Después añadiremos un nuevo objeto al array dinámico del nodo actual y comprobaremos el resultado. Si se produce un error al añadir un objeto al array, eliminaremos el objeto creado y saldremos del método retornando un puntero no válido.

Al finalizar el método, guardaremos el soporte indicado en los parámetros en el nuevo objeto y saldremos del método.

CMyTreeNode *CMyTreeNode::AddNode(const ulong ID, double support = 0)
  {
   CMyTreeNode *node = new CMyTreeNode();
   if(!node)
      return node;
   node.ID(ID);
   if(!Add(node))
     {
      delete node;
      return node;
     }
   node.Parent(GetPointer(this));
//---
   if(support > 0)
      node.SetSupport(support);
   return node;
  }

Una vez hayamos creado un objeto nuevo, también deberíamos ser capaces de eliminarlo. El método encargado de eliminar un objeto según su número ordinal en un array dinámico ya existe en la clase padre. Para ampliar la funcionalidad, crearemos un método para eliminar un nodo según el identificador de la característica DeleteNode.

En los parámetros, transmitiremos al método el identificador de la característica a eliminar, y retornaremos el resultado lógico de la operación.

En el cuerpo del método, organizaremos un ciclo para encontrar el nodo con el identificador dado en el array dinámico del nodo actual. Este ciclo recorrerá los elementos en el rango de 0 hasta el valor de la variable m_data_total. Esta variable contiene el número de elementos activos en el array dinámico y es controlada por la clase padre.

En el cuerpo del método, extraeremos el siguiente elemento del array dinámico y comprobaremos la validez del puntero. El elemento con un puntero no válido se eliminará inmediatamente llamando al método Delete de la clase padre, indicando el número de secuencia del elemento a eliminar.

Tenga en cuenta que el método Delete retornará el resultado lógico de la operación, y cuando eliminamos con éxito un elemento de un array dinámico, reducimos el contador de iteraciones del ciclo y pasamos al siguiente array. Reduciremos solo el contador de iteraciones del ciclo y no cambiaremos el valor de la variable m_data_total, pues su valor ya ha sido cambiado en el método de la clase padre Delete.

Si se produce un error al eliminar un elemento no válido de un array dinámico, simplemente pasaremos al siguiente elemento del array. No finalizaremos el método con el resultado false, porque la tarea del método no consiste en limpiar el array dinámico de objetos no válidos. Esta es solo una función auxiliar. El objetivo principal del método es eliminar un elemento concreto, así que seguiremos ejecutando el método hasta que encontremos el elemento que queremos.

Cuando encontremos el elemento que buscamos en un array dinámico, llamaremos al ya mencionado método Delete de la clase padre. Y en esta ocasión, saldremos del método retornando el resultado de la eliminación del objeto buscado.

Si la búsqueda completa de todos los elementos de un array dinámico no encuentra el elemento necesario, saldremos del método con el resultado false.

bool CMyTreeNode::DeleteNode(const ulong ID)
  {
   for(int i = 0; i < m_data_total; i++)
     {
      CMyTreeNode *temp = m_data[i];
      if(!temp)
        {
         if(!Delete(i))
            continue;
         return DeleteNode(ID);
        }
      if(temp.ID() != ID)
         continue;
      return Delete(i);
     }
//---
   return false;
  }

Siguiendo con los métodos de la nueva clase de nodo de árbol CMyTreeNode, querríamos centrarnos en el método Mining. Este método se encarga de encontrar los caminos en nuestro árbol FP hasta la característica a analizar. Antes de pasar a la descripción del algoritmo del método, debemos decir que dicho algoritmo se ha construido pensando en las particularidades de su próxima utilización en un entorno comercial. Así que nos hemos desviado ligeramente del algoritmo básico.

En primer lugar, no vamos a definir las reglas asociativas para todas las características, sino solo para nuestros valores objetivo. Por consiguiente, durante la construcción de los árboles de reglas, es probable que nos encontremos con una situación en la que la característica que buscamos no es una hoja del árbol, sino un nodo que contiene elementos posteriores en el camino. Y aquí tenemos que entender que no podemos ignorar los nodos subsiguientes, ya que pueden aumentar la probabilidad de que aparezca el resultado deseado. Por ello, deberemos tenerlos en cuenta a la hora de muestrear los caminos hasta la característica que vamos a analizar.

Otro punto al que hemos prestado atención al construir este método. Según el algoritmo, primero deberemos encontrar todos los caminos hacia la característica que se va a analizar en el árbol FP. Y ya luego calcularemos el soporte para cada característica en los caminos seleccionados. Hemos decidido realizar estas dos subtareas en un solo método.

Debemos decir que, para construir el árbol FP, hemos planeado usar solo ejemplares de la clase CMyTreeNode. En consecuencia, usaremos una llamada recursiva al método en cuestión para realizar la búsqueda en profundidad.

Veamos ahora la implementación de estas tareas en el método Mining de nuestra clase. En los parámetros del método, transmitiremos los punteros al vector de soporte, la matriz para registrar los caminos, el identificador de la característica a analizar y el nivel de confianza mínimo. Nuestro método retornará el valor booleano de la ejecución de la operación.

En el cuerpo del método, primero comprobaremos si el nodo analizado es la característica que se está buscando. Para ello, compararemos el identificador de nuestro nodo con el identificador del nodo que estamos buscando, obtenido en los parámetros. Si los identificadores son iguales, comprobaremos el nivel de confianza del nodo en la rama actual, que definimos mediante el método GetConfidence, comentado anteriormente. El grado de confianza no deberá ser inferior al mínimo. En caso contrario, saldremos del método con el resultado true.

bool CMyTreeNode::Mining(vector &supports, matrix &paths, const ulong ID, double min_conf)
  {
   if(ID == m_iIndex)
      if(GetConfidence() < min_conf)
         return true;

El siguiente bloque organizará el avance de la búsqueda en la profundidad del árbol. Aquí, almacenaremos primero en una variable local el valor de soporte del nodo actual. Luego organizaremos un ciclo para buscar todas las ramas desde el nodo actual hasta la profundidad del árbol. En este caso, realizaremos una llamada recursiva a este método para todas las ramas.

Una cosa que debemos entender aquí es que con el método recursivo, solo transmitiremos el identificador que buscamos hasta encontrar el nodo correspondiente. Entonces transmitiremos la constante ULONG_MAX a la profundidad del árbol en lugar del identificador que estamos buscando. Esto se debe a que, a causa de la naturaleza de la construcción del árbol FP, es probable que el nivel de confianza del patrón sea inferior al 100% antes de encontrar el camino hacia el elemento buscado. A medida que se avanza en el camino, la probabilidad de tener la característica buscada será del 100%. Porque, de lo contrario, habríamos construido un camino distinto, pasando por alto el nodo que buscábamos.

Obviamente, tal situación quedará excluida si usamos plenamente el algoritmo del autor. Y es que, al fin y al cabo, si definimos las reglas para todas las características, en el momento en que se empiece a trabajar en alguna de ellas en nuestro árbol FP, ésta será una hoja sin más nodos, ya que todas las características con menos soporte ya habrán sido procesadas y eliminadas del árbol.

Por consiguiente, para cualquier desviación del algoritmo, deberemos evaluar el impacto de los cambios realizados en todo el proceso y efectuar los ajustes adecuados en el algoritmo. En este caso, deberemos enumerar todas las rutas que incluyan la característica que buscamos. Y este será el camino desde la característica buscada a la raíz del árbol y todas las variaciones de caminos desde cualquiera de los nodos posteriores a la raíz del árbol que pasarán por la característica buscada. Para lograr esto, deberemos decir a los nodos subsiguientes que la característica que buscamos ya se ha encontrado entre ellos y la raíz del árbol. Esta será la bandera de señal para cambiar el identificador de la característica buscada a la constante ULONG_MAX.

Después de que el método recursivo haya funcionado positivamente para el nodo siguiente, restaremos el valor de soporte de la variable local creada antes del ciclo al soporte del nodo actual. Y si el identificador del nodo posterior es el mismo que el buscado, eliminaremos el nodo procesado.

   double support = m_dSupport;
   for(int i = 0; i < m_data_total; i++)
     {
      CMyTreeNode *temp = m_data[i];
      if(!temp)
        {
         if(Delete(i))
            i--;
         continue;
        }
      if(!temp.Mining(supports, paths, (ID == m_iIndex ? ULONG_MAX : ID), min_conf))
         return false;
      support -= temp.GetSupport();
      if(temp.ID() == ID)
         if(Delete(i))
            i--;
     }

Podrá notar que en el bloque anterior solo llamamos al mismo método de forma recursiva para los nodos siguientes, pero no hemos guardado los caminos encontrados. Implementaremos esta funcionalidad en el siguiente bloque del método. Este bloque se ejecutará para los nodos con la característica buscada y más allá. Para ello, implementaremos la comprobación del identificador del nodo actual y el obtenido en los parámetros. Además, el soporte para el nodo actual que queda después de que los métodos recursivos hayan sido procesados deberá ser mayor que "0". Y el nodo actual no puede ser la raíz. Es decir, deberá tener al menos un nodo precedente. Después de todo, tendremos que poner algo en el mensaje de la norma.

Después de pasar con éxito los controles anteriores, aumentaremos el tamaño de la matriz de caminos en 1 línea y rellenaremos la línea añadida con valores cero.

A continuación, organizaremos un ciclo de ascenso desde el nodo actual hasta la raíz del árbol. Al nodo actual y a todos los nodos precedentes, les asignaremos el soporte restante para el nodo actual en la línea de nuestro camino, y también añadiremos el mismo valor en el vector de soporte acumulado para los elementos correspondientes.

Cuando el ciclo del elemento padre se complete, saldremos del método con un resultado positivo.

   if(ID == m_iIndex || ID == ULONG_MAX)
      if(support > 0 && !!m_cParent)
        {
         CMyTreeNode *parent = m_cParent;
         ulong row = paths.Rows();
         if(!paths.Resize(row + 1, paths.Cols()))
            return false;
         if(!paths.Row(vector::Zeros(paths.Cols()), row))
            return false;
         supports[m_iIndex] += support;
         while(!!parent)
           {
            if(parent.ID() != ULONG_MAX)
              {
               supports[parent.ID()] += support;
               paths[row, parent.ID()] = support;
              }
            parent = parent.Parent();
           }
        }
//---
   return true;
  }

Probablemente merezca la pena explicar cómo funciona el método con un pequeño ejemplo. Después de todo, su construcción excede un poco los límites del algoritmo FP-Growth descrito anteriormente. Supongamos que tenemos 2 veces la transacción "AB", 1 vez la transacción "ABC", 3 veces la transacción "ABCD" y 1 vez la transacción "ABCDE" en la base de datos de origen. Como resultado, se ha formado el camino "A7-B7-C5-D4-E1" en el árbol FP. Al analizar la característica "C", deberemos restablecer del árbol todos los caminos que contengan esa característica.

Comenzaremos llamando al método para el elemento raíz "A" y le indicaremos que busque la característica "C". En él, llamaremos recursivamente al método para el nodo "B". Y así sucesivamente hasta la hoja "E". Como la hoja "E" no tiene más nodos, empezaremos a trabajar en el bloque 2 de nuestro método y registramos los caminos. Aquí guardaremos primero la ruta "ABCDE" y escribiremos el soporte 1 en todos los nodos. Esto significará que esta ruta ha estado en la base de origen 1 vez y saldremos del método pasando el control al nivel superior.

En el nivel del nodo "D", mantendremos el camino "ABCD". En este caso, del soporte del nodo "D", restaremos el soporte de la hoja "E" (4-1=3), y escribiremos este valor como soporte de todos los elementos del camino. Como podemos ver, esto es coherente con los datos originales sobre la existencia de 3 transacciones similares en la muestra de entrenamiento con este conjunto de características. Solo que no hemos repetido la transacción 3 veces, sino que hemos expresado esto a través del soporte de las características.

Del mismo modo, guardaremos el camino "ABC" con un soporte igual a 1. El camino "AB" no se conserva, ya que no contiene la característica que debe analizarse.

Podrá encontrar el código completo de todos los métodos de la clase en el archivo adjunto "MyTreeNode.mqh".

2.2. Implementando la clase de búsqueda de reglas asociativas

Vamos a seguir con la construcción del algoritmo de búsqueda de reglas asociativas FP-Growth. La funcionalidad básica se describe en otra clase CAssocRules. A continuación, le mostramos la estructura de la clase que estamos creando. Como podrá ver, la mayoría de los métodos están ocultos "bajo el capó", pero lo primero es lo primero.

class CAssocRules : public CObject
  {
protected:
   CMyTreeNode       m_cRoot;
   CMyTreeNode       m_cBuyRules;
   CMyTreeNode       m_cSellRules;
   vector            m_vMin;
   vector            m_vStep;
   int               m_iSections;
   matrix            m_mPositions;
   matrix            m_BuyPositions;
   matrix            m_SellPositions;
   //---
   bool              NewPath(CMyTreeNode *root, matrix &path);
   CMyTreeNode      *CheckPath(CMyTreeNode *root, vector &path);
   //---
   bool              PrepareData(matrix &data, matrix &bin_data, 
                                 vector &buy, vector &sell,
                                 const int sections = 10, const double min_sup = 0.03);
   matrix            CreatePath(vector &bin_data, matrix &positions);
   matrix            CreatePositions(vector &support, const double min_sup = 0.03);
   bool              GrowsTree(CMyTreeNode *root, matrix &bin_data, matrix &positions);
   double            Probability(CMyTreeNode *root, vector &data, matrix &positions);

public:
                     CAssocRules();
                    ~CAssocRules();
   //---
   bool              CreateRules(matrix &data, vector &buy, 
                                 vector &sell, int sections = 10, 
                                 double min_freq = 0.03,
                                 double min_prob = 0.3);
   bool              Probability(vector &data, double &buy, double &sell);
   //--- methods for working with files
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual bool      Save(const string file_name);
   virtual bool      Load(const string file_name);
  };

Entre las variables de la clase, podemos ver los 3 ejemplares de los nodos del árbol descritos anteriormente:

  • m_cRoot — para registrar nuestro árbol FP;
  • m_cBuyRules — para registrar las reglas de compra;
  • m_cSellRules  — para registrar las reglas de venta.

Las matrices m_mPositions, m_BuyPositions y m_SellPositions, por otro lado, contendrán las características y sus soportes, clasificados en orden descendente.

No olvidemos que al probar todos los patrones anteriores, hemos puesto a prueba la capacidad de identificar un fractal antes de la formación de la tercera vela del patrón. Por lo tanto, definiremos 2 árboles de reglas de búsqueda para identificar los fractales de compra y venta. Si los requisitos de su tarea consisten en definir las reglas para más características objetivo, entonces tendrá que crear más árboles de reglas privados.

Por ejemplo, podrá tener varios niveles objetivo para comprar y varios para vender. Desgraciadamente, los algoritmos de búsqueda de reglas asociativas solo funcionan con tablas binarias. En consecuencia, para cada nivel objetivo tendrá que crear una característica aparte y encontrar reglas asociativas para ella. Para eliminar un gran número de variables privadas, podrá utilizar arrays dinámicos.

Hemos dejado el constructor y el destructor del método vacíos, ya que no hemos usado punteros dinámicos a los objetos de construcción de árboles en esta clase. 

Ya hemos mencionado que los algoritmos de búsqueda de reglas asociativas solo funcionan con matrices binarias, mientras que los datos sobre el estado de la situación del mercado son difíciles de clasificar como tales. Por lo tanto, estos necesitarán un procesamiento previo antes de su uso.

Para simplificar el uso posterior de la clase, el algoritmo no requiere que el usuario preprocese los datos; en lugar de ello, esto se ha implementado en el método PrepareData. El método obtiene en los parámetros los punteros a 2 matrices, 2 vectores y 2 constantes. Una matriz contiene los datos originales, mientras que la segunda se usa para registrar los datos binarios procesados. Los vectores contienen los valores objetivo. En nuestro caso, estarán representados por datos binarios y no requerirán de preprocesamiento. No podemos decir lo mismo de los datos originales.

Para convertir las unidades escalares de los datos originales en unidades binarias, tomaremos el rango de valores de cada atributo y lo dividiremos en varios intervalos. El número de estos intervalos se especificará usando el parámetro de Sections. Guardaremos los valores mínimos y el tamaño del paso para cada característica en los vectores correspondientes m_vMin y m_vStep. Los necesitaremos para poder convertir los datos originales en datos binarios durante el uso práctico del modelo.

Aquí es donde prepararemos nuestra matriz binaria, dándole el tamaño requerido y rellenándola con valores cero. Podemos especificar directamente los identificadores de las características objetivo, que luego añadiremos como últimas columnas de la matriz.

bool CAssocRules::PrepareData(matrix &data,
                              matrix &bin_data,
                              vector &buy, 
                              vector &sell,
                              const int sections = 10,
                              const double min_sup = 0.03)
  {
//---
   m_iSections = sections;
   m_vMin = data.Min(0);
   vector max = data.Max(0);
   vector delt = max - m_vMin;
   m_vStep = delt / sections + 1e-8;
   m_cBuyRules.ID(data.Cols() * m_iSections);
   m_cSellRules.ID(m_cBuyRules.ID() + 1);
   bin_data = matrix::Zeros(data.Rows(), m_cSellRules.ID() + 1);

A continuación, organizaremos un ciclo para recorrer todas las filas de la matriz de datos originales. En el cuerpo del ciclo, restaremos a cada fila el vector de valores mínimos y dividiremos la diferencia resultante por el valor del paso. Para evitar las "caídas", limitaremos los valores superior e inferior de los elementos del vector. Como resultado de estas operaciones, la parte entera del número de cada elemento del vector nos dirá a qué rango de valores pertenece el elemento correspondiente en los datos originales. Y cada rango de nuestra matriz binaria supondrá una característica independiente.

Organizamos un ciclo anidado y rellenamos la fila correspondiente de la matriz binaria. Si el atributo está activo, su valor cambiará a "1". Las características inactivas permanecerán con valores cero.

   for(ulong r = 0; r < data.Rows(); r++)
     {
      vector pos = (data.Row(r) - m_vMin) / m_vStep;
      if(!pos.Clip(0, m_iSections - 1))
         return false;
      for(ulong c = 0; c < pos.Size(); c++)
         bin_data[r, c * sections + (int)pos[c]] = 1;
     }
   if(!bin_data.Col(buy, m_cBuyRules.ID()) ||
      !bin_data.Col(sell, m_cSellRules.ID()))
      return false;

Después de rellenar la matriz binaria, calcularemos directamente los soportes para cada característica y los clasificaremos en orden descendente en el método CreatePositions. Cuando la clasificación de las características esté completa, saldremos del método con un resultado positivo.

   vector supp = bin_data.Sum(0) / bin_data.Rows();
   m_mPositions = CreatePositions(supp, min_sup);
//---
   return true;
  }

Ya que hemos tocado el método de clasificación de características CreatePositions, le sugerimos ver también su algoritmo. El método recibe en los parámetros el vector de soportes y el nivel de soporte mínimo.

Primero, realizaremos un pequeño trabajo preparatorio. Esto se debe a que los valores de soporte obtenidos están representados por un vector en el que los valores de los elementos contienen valores de soporte. Y los números ordinales de los elementos indican las características. Al clasificar simplemente los elementos del vector, perderemos la conexión con las características de los datos originales. Por ello, tendremos que crear pares «identificador de características - soporte». Los datos de los pares los guardaremos en una matriz.

Para ello, primero crearemos una matriz unitaria de 2 columnas y un número de filas igual al número de características de la muestra original. A continuación, calcularemos las sumas acumuladas de los elementos de las columnas y reduciremos los valores de la matriz resultante en "1". De esta forma, obtendremos una matriz en la que las columnas tendrán valores en orden ascendente desde "0" y se corresponderán con el índice de la fila. Lo único que tendremos que hacer es sustituir la columna por el vector de soporte obtenido. Y nosotros ya tenemos una matriz en la que cada fila contiene un identificador de característica y su correspondiente valor de soporte.

matrix CAssocRules::CreatePositions(vector &support, const double min_sup = 0.03)
  {
   matrix result = matrix::Ones(support.Size(), 2);
   result = result.CumSum(0) - 1;
   if(!result.Col(support, 1))
      return matrix::Zeros(0, 0);

Ahora solo deberemos clasificar las filas de la matriz en orden descendente de soporte de las características. Para ello, organizaremos un ciclo en el que estableceremos un algoritmo de clasificación de burbuja.

   bool change = false;
   do
     {
      change = false;
      ulong total = result.Rows() - 1;
      for(ulong i = 0; i < total; i++)
        {
         if(result[i, 1] >= result[i + 1, 1])
            continue;
         if(result.SwapRows(i, i + 1))
            change = true;
        }
     }
   while(change);

Tras salir del ciclo, tendremos una matriz con las características clasificadas. Lo único que tendremos que hacer es eliminar las características que no cumplan el requisito del soporte mínimo. Para ello, encontraremos la primera característica por debajo del nivel de soporte mínimo y "cortaremos" todas las características por debajo de este nivel.

   int i = 0;
   while(result[i, 1] >= min_sup)
      i++;
   if(!result.Resize(i, 2))
      return matrix::Zeros(0, 0);
//---
   return result;
  }

Después de redimensionar la matriz con éxito, saldremos del método retornando la matriz obtenida.

Antes de pasar a ver los métodos públicos, veremos algunos métodos más en los que implementaremos algunas de las funciones repetitivas. En primer lugar, implementaremos la creación de un camino a partir de datos binarios para su transferencia al árbol FP. Esta funcionalidad se realizará en el método CreatePath. A continuación, transmitiremos los punteros a un vector de datos binarios y a una matriz de características ordenadas como parámetros del método. El método retornará una matriz de caminos que contendrá los pares «identificador de características - soporte» que se añadirán al árbol FP.

Debemos prestar intención de inmediato a la diferencia entre la matriz de características clasificada que obtuvimos al preparar los datos y la matriz para añadir un camino al árbol FP. Ambas matrices contendrán pares "identificador de características - soporte". Solo la primera matriz contendrá todas las características presentes en los datos originales y su soporte en la muestra de entrenamiento. La segunda matriz (de camino) contendrá solo las características presentes en la transacción que se está analizando y los soportes de esa transacción que se especifican en la matriz binaria.

Sí, se trata de una matriz binaria y los soportes de las características en cada transacción deben ser los mismos, pero más adelante utilizaremos el mismo método para construir árboles de reglas privados. Ya ofrecimos un ejemplo al describir el método CMyTreeNode::Mining, y allí usamos los niveles de soporte varias veces en lugar de repetir un camino. Precisamente para unificar las operaciones utilizaremos 1 método en 2 subprocesos, y la introducción de niveles de soporte nos resultará muy útil.

Al principio del método, guardaremos en las variables locales el tamaño del vector de datos originales y el número de características a analizar, que será inferior al tamaño del vector de datos originales en el número de características aleatorias con soporte inferior al mínimo.

Aquí también prepararemos una matriz para registrar los resultados, que no podrá ser mayor que la matriz de las características analizadas. Asimismo, introduciremos una variable que indicará el tamaño de nuestro camino. En esta etapa, será "0".

A continuación, estableceremos un ciclo en el que recorreremos todas las características analizadas en orden descendente de soporte. En el cuerpo del ciclo, recuperaremos el identificador de la siguiente característica a comprobar, y comprobaremos su valor en el vector binario de los datos originales. Si la característica no está activa, pasaremos a la siguiente característica.

Si la característica está activa, añadiremos el identificador de la característica y su soporte del vector binario de los datos originales a la matriz de caminos; y a continuación, aumentaremos inmediatamente la variable de tamaño del camino.

Después de salir del ciclo, reduciremos el tamaño de la matriz de caminos hasta el número de elementos rellenados y saldremos del método.

matrix CAssocRules::CreatePath(vector &bin_data, matrix &positions)
  {
   ulong size = bin_data.Size();
//---
   ulong total = positions.Rows();
   int vect_pos = 0;
   matrix path = matrix::Zeros(2, total);
   for(ulong c = 0; c < total; c++)
     {
      ulong pos = (ulong)positions[c, 0];
      if(pos >= size)
         continue;
      if(bin_data[pos] == 0)
         continue;
      path[0, vect_pos] = (double)pos;
      path[1, vect_pos] = bin_data[pos];
      vect_pos++;
     }
   if(!path.Resize(2, vect_pos))
      return matrix::Zeros(0, 0);
//---
   return path;
  }

Otro método que necesitaremos será el método para añadir un camino a nuestro árbol FP, NewPath. Como parámetros, el método recibirá el puntero al nodo raíz del árbol y la matriz de caminos creada anteriormente, y luego retornará el resultado lógico de la operación. En el cuerpo del método, primero comprobaremos el tamaño del camino resultante. Deberá ser superior a "0". A continuación, aumentaremos el soporte para el nodo raíz e iniciaremos un ciclo de enumeración de todos los elementos del camino.

En el cuerpo del ciclo, comprobaremos si existe el siguiente nodo con el identificador requerido y, si es necesario, crearemos un nuevo nodo. A continuación, aumentaremos el tamaño del soporte del nodo, y pasaremos a la búsqueda del siguiente nodo del camino.

Cuando se hayan completado todos los elementos del camino, saldremos del método. 

bool CAssocRules::NewPath(CMyTreeNode *root, matrix &path)
  {
   ulong total = path.Cols();
   if(total <= 0)
      return false;
   CMyTreeNode *parent = root;
   root.IncreaseSupport(path[1, 0]);
   for(ulong i = 0; i < total; i++)
     {
      CMyTreeNode *temp = parent.GetNext((ulong)path[0, i]);
      if(!temp)
        {
         temp = parent.AddNode((int)path[0, i], 0);
         if(!temp)
            return false;
        }
      temp.IncreaseSupport(path[1, i]);
      parent = temp;
     }
//---
   return true;
  }

Por último, pasaremos al método de crecimiento del árbol FP, GrowsTree. En los parámetros, el método obtendrá el puntero al nodo raíz del árbol, una matriz binaria de datos de entrada y una matriz de atributos analizados clasificados. Dentro del método, estableceremos un ciclo para recorrer todas las filas de datos originales.

En el cuerpo del ciclo, recuperaremos la siguiente transacción de la muestra de entrenamiento y crearemos un camino para añadirlo al árbol usando el método CreatePath. A continuación, comprobaremos el tamaño de la ruta resultante, que deberá ser superior a "0", y llamaremos al método NewPath para añadir la ruta creada a nuestro árbol FP. Al mismo tiempo, no deberemos olvidarnos de comprobar el resultado de la ejecución de las operaciones.

Una vez se hayan enumerado con éxito todas las transacciones de los datos originales, saldremos del método con un resultado positivo.

bool CAssocRules::GrowsTree(CMyTreeNode * root, matrix & bin_data, matrix &positions)
  {
   ulong rows = bin_data.Rows();
   for(ulong r = 0; r < rows; r++)
     {
      matrix path = CreatePath(bin_data.Row(r), positions);
      ulong size = path.Cols();
      if(size <= 0)
         continue;
      if(!NewPath(root, path))
         return false;
     }
//---
   return true;
  }

Ahora, vamos a combinar todos los métodos anteriores en un método público llamado CreateRules. En los parámetros del método, transmitiremos la matriz de datos escalares de entrada (no binarios), los vectores binarios de valores objetivo, el número de intervalos para convertir los valores escalares en binarios, el soporte mínimo y los niveles de confianza mínimos.

En el cuerpo del método, primero comprobaremos los datos originales obtenidos. En primer lugar, tendremos que hacer coincidir la dimensionalidad de los vectores de la matriz obtenida, y el número de intervalos deberá ser mayor que "0".

Después de pasar por el bloque de validación, convertiremos los datos originales escalares en su forma binaria. Para ello, usaremos el método PrepareData, descrito anteriormente.

bool CAssocRules::CreateRules(matrix &data,
                              vector &buy,
                              vector &sell,
                              int sections = 10,
                              double min_sup = 0.03,
                              double min_conf = 0.3)
  {
   if(data.Rows() <= 0 || data.Cols() <= 0 || sections <= 0 ||
      data.Rows() != buy.Size() || data.Rows() != sell.Size())
      return false;
//---
   matrix binary_data;
   if(!PrepareData(data, binary_data, buy, sell, sections))
      return false;

A continuación, para pasar al plano de las unidades relativas, dividiremos los valores de nuestra matriz binaria por el número de transacciones de la muestra de entrenamiento y construiremos nuestro árbol FP con el método GrowsTree.

   double k = 1.0 / (double)(binary_data.Rows());
   if(!GrowsTree(GetPointer(m_cRoot), binary_data * k, m_mPositions))
      return false;

Después de construir el árbol FP, podremos pasar a crear las reglas. Aquí, prepararemos primero un vector para registrar los soportes y una matriz para registrar los caminos. A continuación, llamaremos al método Mining de nuestro árbol FP para encontrar todos los caminos con una característica para comprar.

   vector supports = vector::Zeros(binary_data.Cols());
   binary_data = matrix::Zeros(0, binary_data.Cols());
   if(!m_cRoot.Mining(supports, binary_data, m_cBuyRules.ID(),min_conf))
      return false;

Una vez se hayan extraído todos los caminos con éxito, pondremos a cero el soporte para la característica de compra, eliminándola así de todos los caminos y clasificando los soportes privados en orden descendente. Después, llamaremos al método CreatePositions, y escribiremos el resultado de las operaciones en la matriz m_BuyPositions. Si después de clasificar las características todavía tenemos una oportunidad de construir las reglas (la matriz clasificada tiene características para asignar al antecedente de la regla), llamaremos al método de crecimiento del árbol y le daremos los caminos previamente seleccionados como datos de entrada.

Como resultado de las operaciones anteriores, obtendremos un árbol de reglas parcial con la raíz en el nodo m_cBuyRules.

   supports[m_cBuyRules.ID()] = 0;
   m_BuyPositions = CreatePositions(supports, min_sup);
   if(m_BuyPositions.Rows() > 0)
      if(!GrowsTree(GetPointer(m_cBuyRules), binary_data, m_BuyPositions))
         return false;

De la misma forma, crearemos un árbol de reglas para las características de venta.

   supports = vector::Zeros(binary_data.Cols());
   binary_data = matrix::Zeros(0, binary_data.Cols());
   if(!m_cRoot.Mining(supports, binary_data, m_cSellRules.ID(),min_conf))
      return false;
   supports[m_cSellRules.ID()] = 0;
   m_SellPositions = CreatePositions(supports, min_sup);
   if(m_SellPositions.Rows() > 0)
      if(!GrowsTree(GetPointer(m_cSellRules), binary_data, m_SellPositions))
         return false;
//---
   m_cRoot.Clear();
//---
   return true;
  }

Una vez seleccionadas todas las reglas, eliminaremos el objeto de árbol FP original, liberando así los recursos de nuestra computadora, y saldremos del método con un resultado positivo.

El método de 'Probability' ha sido creado para el uso práctico En los parámetros, el método recibirá un vector escalar de datos originales y los punteros a las dos variables de tipo double en las que se escribirán los niveles de confianza para uno u otro patrón. El algoritmo del método usará todos los métodos mencionados anteriormente. Podrá familiarizarse con ellos en el archivo adjunto.

El código completo de todas las clases y sus métodos se encuentra en el archivo adjunto.


3. Simulación

Para probar el funcionamiento de la clase construida usando datos reales, hemos creado el asesor "assocrules.mq5". Debemos decir que el asesor se ha probado respetando todos los parámetros usados en las pruebas anteriores. No podemos decir que el método haya identificado todos los fractales sin error, pero el asesor creado ha mostrado resultados interesantes, los cuales mostramos en la captura de pantalla siguiente.

Resultados de las pruebas de la clase de reglas asociativas 


Conclusión

En este artículo, hemos presentado otro tipo de problema que se resuelve con métodos de aprendizaje no supervisado: la búsqueda de reglas asociativas. Asimismo, hemos implementado una clase para crear reglas asociativas usando el algoritmo FP-Growth, y hemos creado un asesor para probar el método, que ha mostrado buenos resultados. Como resultado, podemos concluir que dichos algoritmos pueden usarse a la hora de resolver problemas prácticos en el comercio.

Enlaces

  1. Redes neuronales: así de sencillo (Parte 14): Clusterización de datos
  2. Redes neuronales: así de sencillo (Parte 15): Clusterización de datos usando MQL5
  3. Redes neuronales: así de sencillo (Parte 16): Uso práctico de la clusterización
  4. Redes neuronales: así de sencillo (Parte 17): Reducción de la dimensionalidad
  5. Redes neuronales: así de sencillo (Parte 18): Reglas asociativas

Programas usados en el artículo.

# Nombre Tipo Descripción
1 assocrules.mq5 Asesor   Asesor para el entrenamiento y la prueba de modelos
2 AssocRules.mqh Biblioteca de clases
Biblioteca de clases para organizar el algoritmo FP-Growth
3 MyTreeNode.mqh Biblioteca de clases Biblioteca de clases para la organización de los nodos del árbol 


Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/11141

Archivos adjuntos |
MQL5.zip (7.78 KB)
Aprendiendo a diseñar un sistema de trading con el oscilador Chaikin Aprendiendo a diseñar un sistema de trading con el oscilador Chaikin
Aquí tenemos un nuevo artículo de nuestra serie destinada al estudio de indicadores técnicos populares y la creación de sistemas comerciales basados en ellos. En este artículo, trabajaremos con el indicador Chaikin Oscillator, el oscilador de Chaikin.
DoEasy. Elementos de control (Parte 9): Reorganizando los métodos de los objetos WinForms, los controles "RadioButton" y "Button" DoEasy. Elementos de control (Parte 9): Reorganizando los métodos de los objetos WinForms, los controles "RadioButton" y "Button"
En este artículo, ordenaremos los nombres de las clases de objeto WinForms y crearemos los objetos WinForms Button y RadioButton.
Aprendiendo a diseñar un sistema de trading con Force Index Aprendiendo a diseñar un sistema de trading con Force Index
Aquí tenemos un nuevo artículo de nuestra serie destinada a la creación de sistemas comerciales basados en indicadores técnicos populares. En esta ocasión, analizaremos el indicador Force Index y aprenderemos a crear sistemas comerciales basados en él.
Aprendizaje automático y data science (Parte 06): Descenso de gradiente Aprendizaje automático y data science (Parte 06): Descenso de gradiente
El descenso de gradiente juega un papel importante en el entrenamiento de redes neuronales y diversos algoritmos de aprendizaje automático: es un algoritmo rápido e inteligente. Sin embargo, a pesar de su impresionante funcionamiento, muchos científicos de datos todavía lo malinterpretan. Veamos sobre qué tratará este artículo.