English Русский 中文 Deutsch 日本語 Português
preview
Redes neuronales: así de sencillo (Parte 23): Creamos una herramienta para el Transfer Learning

Redes neuronales: así de sencillo (Parte 23): Creamos una herramienta para el Transfer Learning

MetaTrader 5Sistemas comerciales | 8 noviembre 2022, 09:21
337 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Contenido


Introducción

Continuamos nuestra inmersión en el mundo de la inteligencia artificial. Hoy queremos presentarles el Aprendizaje por Transferencia. De un modo u otro, hemos mencionado esta tecnología más de una vez, pero nunca la hemos usado. Sin embargo, es una herramienta bastante potente que puede mejorar la eficacia del desarrollo de redes neuronales y reducir los costes de entrenamiento.


1. Para qué necesitamos el aprendizaje por transferencia

¿Qué es el Transfer Learning y para qué se necesita? En términos generales, el Aprendizaje por Transferencia es un método de aprendizaje automático en el que el conocimiento de un modelo entrenado para resolver determinados problemas se reutiliza como base para resolver nuevos problemas. Obviamente, para resolver nuevos problemas, el modelo es reentrenado previamente con los nuevos datos. En general, con el modelo donante adecuado, el reentrenamiento es mucho más rápido y ofrece mejores resultados que el entrenamiento de un modelo similar «partiendo de cero».

En este caso, el modelo donante puede usarse total o parcialmente.

Similar a esta tecnología es el caso cuando usamos los resultados de la compresión de datos y la clusterización para preprocesar los datos de origen para la red neuronal. En este caso concreto, usaremos plenamente el modelo preentrenado, pero al construir el modelo para resolver nuevos problemas, no realizaremos un entrenamiento adicional del modelo donante. Solo lo utilizaremos para preprocesar los datos de origen "en bruto" y usaremos estos para entrenar el nuevo modelo.

Si recordamos, cuando empezamos a aprender sobre los autocodificadores, también hablamos de la posibilidad de utilizar el Aprendizaje por Transferencia después de entrenar el modelo, pero en este caso no podemos usar el autocodificador completamente como modelo donante, porque lo hemos entrenado para comprimir los datos de origen y luego reconstruir estos a partir de la representación comprimida. En consecuencia, no tendría sentido utilizar el autocodificador completo como modelo donante. Resultaría mucho más eficiente utilizar solo el codificador para el preprocesamiento. En este caso, el modelo global será más pequeño y la eficiencia de las capas posteriores resultará mayor, ya que necesitaremos menos pesos de entrenamiento para procesar la misma cantidad de información.

Hay que decir que el uso del Aprendizaje por Transferencia va mucho más allá del simple uso de los resultados del aprendizaje no supervisado. Recuerde cuántas veces ha vuelto a entrenar su modelo, cuando lo único que tenía que hacer era añadir o eliminar una sola capa neuronal. Y sin embargo, algunas de las capas neuronales podrían utilizarse nuevamente.

Existe otro uso para esta tecnología. Debido al problema del gradiente de amortiguación, resulta casi imposible entrenar por completo un modelo profundo. En cambio, con el Aprendizaje por Transferencia podemos entrenar las capas neuronales por bloques y aumentar gradualmente el tamaño del modelo.

Obviamente, podemos encontrar otras aplicaciones para esta tecnología. Le sugerimos proceder al análisis de una herramienta para su aplicación.


2. Creando la herramienta

Al empezar a construir una herramienta, le sugerimos definir antes sus objetivos. En primer lugar, debemos recordar cómo guardar nuestros modelos entrenados. Todos ellos se guardarán en un único archivo binario. De esta forma, cada objeto de nuestro modelo tendrá una estructura propia y estricta para el registro de datos. Y, como consecuencia, resultará difícil eliminar simplemente algunos de los datos del archivo en el editor. Así que tendremos que cargar todo el modelo entrenado desde el archivo, realizar las manipulaciones necesarias con él, y luego guardar el nuevo modelo en un nuevo archivo o sobrescribir el anterior. Le daremos preferencia al nuevo archivo, ya que el modelo donante podremos seguir utilizándolo para las tareas en las que fue entrenado.

El segundo momento a considerar es que nuestras redes neuronales solo funcionarán bien con los datos en los que han sido entrenadas. Con datos completamente nuevos, podremos obtener resultados imprevisibles. Esto también se aplica a las capas neuronales individuales. Por lo tanto, para el Aprendizaje por Transferencia, solo podremos usar capas neuronales consecutivas, empezando por la capa de datos de origen. No podremos sacar ningún bloque del centro o del final del modelo, es decir, podremos tomar el modelo donante completo o algunas de sus primeras capas, añadir algunas capas neuronales diferentes y guardar el nuevo modelo.

Para ello, deberemos asegurarnos de que el nuevo modelo sea plenamente funcional, tanto en el modo de entrenamiento como en su uso comercial. Obviamente, el nuevo modelo deberá ser entrenado antes de su funcionamiento comercial.

Hay otro punto que cabe señalar aquí: las capas neuronales del modelo donante conservan sus coeficientes de peso, y con ellos, todos los conocimientos adquiridos durante la fase de entrenamiento previo del modelo. Las nuevas capas neuronales recibirán pesos aleatorios, como en la inicialización del modelo. Si empezamos a entrenar el nuevo modelo como hacíamos antes, junto con el entrenamiento de las nuevas capas, desequilibraremos las capas neuronales previamente entrenadas. Por ello, primero deberemos bloquear el entrenamiento de las capas neuronales del modelo donante, y solo entrenar las capas nuevas.


2.1 Diseño

Debemos entender que no solo necesitaremos un producto de software que tome el modelo donante original, procesándolo de alguna forma y luego guardándolo otra vez en un nuevo archivo. Al fin y al cabo, el número de capas copiadas y la arquitectura del modelo creado son siempre individuales. Por eso necesitaremos una herramienta que permita al usuario configurar rápida y cómodamente cada modelo de forma individual. Es decir, necesitaremos una herramienta con una interfaz fácil de usar. Comenzaremos el trabajo por el diseño de la interfaz de usuario.

Bien, tenemos tres bloques que destacan claramente. En el primero, trabajaremos con un modelo donante. Aquí necesitaremos una opción para seleccionar el archivo con el modelo entrenado. Después de cargar el modelo desde un archivo, la herramienta deberá ofrecernos una descripción de la arquitectura del modelo cargado. Al fin y al cabo, el usuario deberá entender qué modelo se ha cargado y qué capas neuronales va a copiar. Aquí también le indicaremos a la herramienta el número de capas a copiar. Como hemos mencionado antes, copiaremos las capas neuronales en una fila, empezando por la capa de datos de origen.

En el segundo bloque, organizaremos la adición de capas neuronales. Aquí crearemos campos para introducir información sobre la capa neuronal que vamos a crear. Al igual que sucede con el código del software, describiremos cada capa neuronal una por una y la añadiremos a la arquitectura del nuevo modelo.

El tercer bloque mostrará la arquitectura completa del modelo que estamos creando, con la opción de especificar un archivo para guardarlo. A continuación, mostraremos un ejemplo de diseño de la herramienta a crear.

Diseño de la herramienta

Tanto el diseño de la herramienta como su aplicación se presentan únicamente para demostrar sus capacidades. Siempre podrá modificarlos como mejor le parezca para adaptarlos a sus necesidades.


2.2 Implementando la interfaz de usuario 

Una vez que hemos decidido el diseño de nuestra herramienta, podemos empezar a implementarla. Para ello, crearemos la nueva clase CNetCreatorPanel como heredera de la clase básica de aplicación de diálogo CAppDialog.

Debemos decir que cada control de nuestro panel se creará como un objeto independiente. Por ello, el número de objetos que declararemos en nuestra nueva clase será bastante grande. Para mayor comodidad, los dividiremos en bloques.

En el primer bloque, declararemos los objetos relacionados con la visualización del modelo previamente entrenado:

  • m_edPTModel — elemento para introducir el nombre del archivo del modelo preentrenado;
  • m_edPTModelLayers — elemento para mostrar el número total de capas neuronales en el modelo preentrenado;
  • m_spPTModelLayers — número de capas neuronales copiadas al nuevo modelo;
  • m_lstPTMode — elemento para representar la arquitectura del modelo preentrenado.
class CNetCreatorPanel : protected CAppDialog
  {
protected:
   //--- pre-trained model
   CEdit             m_edPTModel;
   CEdit             m_edPTModelLayers;
   CSpinEdit         m_spPTModelLayers;
   CListView         m_lstPTModel;
   CNetModify        m_Model;   
   CArrayObj*        m_arPTModelDescription;

Aquí también declararemos objetos para trabajar con el modelo preentrenado:

  • m_Model — objeto del propio modelo preentrenado;
  • m_arPTModelDescription — array dinámico que describe la arquitectura del modelo preentrenado.

Debemos prestar atención a dos puntos. Todos los objetos se declaran estáticos, excepto el array dinámico que describe la arquitectura del modelo. El uso de objetos estáticos nos permitirá transferir el trabajo de memoria al sistema. La creación y eliminación de objetos estáticos se realiza junto con el objeto que los contiene, por lo que no requerirá ningún trabajo adicional por parte del programador. Sin embargo, de esta forma, solo podemos crear objetos en la estructura de nuestra clase. La descripción de la arquitectura la obtendremos de nuestro modelo preentrenado. Por ello, este objeto ha sido declarado a través de un puntero dinámico.

El segundo punto: para declarar el objeto del modelo preentrenado, antes utilizábamos la clase CNetModify, mientras que creábamos una clase CNet para los modelos de redes neuronales. Esto se debe a que necesitaremos una funcionalidad adicional de nuestra red neuronal. Y para implementar esta, crearemos una nueva clase CNetModify como descendiente de nuestra clase CNet. Pero volveremos a ello cuando describamos la funcionalidad de nuestra herramienta.

El siguiente bloque contendrá los objetos para describir la nueva capa neuronal que estamos creando. Todos ellos coinciden con los elementos de nuestra clase de descripción de la arquitectura de la capa neural CLayerDescription. Por ello, no nos detendremos en la descripción de cada elemento. Lo único que destacaremos es la creación de dos botones para añadir una nueva capa neuronal y eliminar una capa ya creada. Debemos decir que solo podremos eliminar las capas neuronales añadidas. Para controlar el número de capas neuronales a copiar, utilizaremos los elementos del bloque anterior,

   //--- add layers
   CComboBox         m_cbNewNeuronType;
   CEdit             m_edCount;
   CEdit             m_edWindow;
   CEdit             m_edWindowOut;
   CEdit             m_edStep;
   CEdit             m_edLayers;
   CEdit             m_edBatch;
   CEdit             m_edProbability;
   CComboBox         m_cbActivation;
   CComboBox         m_cbOptimization;
   CButton           m_btAddLayer;
   CButton           m_btDeleteLayer;

así como el último bloque de objetos del nuevo modelo. Solo contiene tres elementos. Estos incluyen un objeto para mostrar la arquitectura general del modelo creado, un botón para guardar el nuevo modelo y un array dinámico para describir la arquitectura de las capas neuronales a añadir. En este caso, hemos creado m_arAddLayers, un objeto estático de array dinámico que describe la arquitectura de las capas neuronales a añadir. La arquitectura de las capas neuronales añadidas la crearemos dentro de nuestra herramienta. Y este objeto se puede crear perfectamente como estático.

   //--- new model
   CListView         m_lstNewModel;
   CButton           m_btSave;
   CArrayObj         m_arAddLayers;

La lista de métodos públicos de nuestra clase será bastante básica. Estos incluirán el constructor y destructor de la clase, el método de creación del panel de herramientas y el manejador de eventos.

También hemos sobrescrito 3 métodos de la clase padre, pero esto podría haberse evitado con una herencia pública.

public:
                     CNetCreatorPanel();
                    ~CNetCreatorPanel();
   //--- main application dialog creation and destroy
   virtual bool      Create(const long chart, const string name, const int subwin, const int x1, const int y1);
   //--- chart event handler
   virtual bool      OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam);
 
   virtual void      Destroy(const int reason = REASON_PROGRAM) override { CAppDialog::Destroy(reason); }
   bool              Run(void) { return CAppDialog::Run();}
   void              ChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
     {               CAppDialog::ChartEvent(id, lparam, dparam, sparam); }
  };

Gracias al uso de objetos estáticos, el constructor y el destructor de nuestra clase estarán prácticamente vacíos.

El trabajo principal de creación de nuestros elementos de interfaz y la organización de los mismos se realiza en el método Create, encargado de crear los cuadros de diálogo. Pero antes de describir este método, haremos un pequeño trabajo preparatorio.

En primer lugar, definiremos una serie de constantes que nos ayudarán a organizar correctamente el interior de nuestra interfaz. En el anexo encontrará una lista con las mismas.

También debemos señalar que, además de los elementos de entrada, nuestra interfaz también contiene una serie de etiquetas de texto, para las que no hemos declarado objetos. Esto se hace deliberadamente para simplificar la estructura de nuestra clase. Al fin y al cabo, las constantes solo serán necesarias para la visualización y no se utilizarán al crear la funcionalidad de nuestra herramienta. No obstante, tendremos que crear estos objetos, y el proceso de creación de dichos objetos se repetirá, con la excepción de algunos datos, como el texto del objeto y su posición en el panel. Por consiguiente, para estructurar nuestro código, implementaremos un método CreateLabel aparte para crear dichas etiquetas.

En los parámetros del método, transmitiremos el ID del objeto, el texto de la etiqueta y sus coordenadas en el panel.

En el cuerpo del método, primero crearemos un nuevo objeto de etiqueta y luego comprobamos el resultado de la operación. A continuación, crearemos un objeto en el gráfico, y transmitiremos al mismo el contenido requerido. Luego añadiremos al array dinámico de nuestra colección de objetos de interfaz el puntero al objeto creado.

Aquí debemos señalar que hemos creado un nuevo objeto con un puntero en una variable privada. Durante las operaciones de este método, comprobaremos el resultado de las mismas y borraremos el objeto creado en caso de error, pero después de salir del método, no dejaremos el puntero al objeto creado en nuestra clase para su posterior eliminación al cerrar el programa. La cuestión es que hemos transmitido el puntero al objeto creado a nuestra colección de objetos del cuadro de diálogo, cuya funcionalidad completa ya está implementada en la clase padre. Esto incluye la funcionalidad necesaria para eliminar todos los objetos de la colección al final del programa. Así que ahora podremos simplemente transmitir el puntero a la colección y olvidarnos de él.

bool CNetCreatorPanel::CreateLabel(const int id, const string text, const int x1, const int y1, const int x2, const int y2)
  {
   CLabel *tmp_label = new CLabel();
   if(!tmp_label)
      return false;
   if(!tmp_label.Create(m_chart_id, StringFormat("%s%d", LABEL_NAME, id), m_subwin, x1, y1, x2, y2))
     {
      delete tmp_label;
      return false;
     }
   if(!tmp_label.Text(text))
     {
      delete tmp_label;
      return false;
     }
   if(!Add(tmp_label))
     {
      delete tmp_label;
      return false;
     }
//---
   return true;
  }

De la misma forma, implementaremos un método para crear los objetos de la entrada de datos. Sin embargo, no creamos nuevos objetos en este, sino que utilizaremos los previamente creados en la clase. Los punteros a dichos objetos se transmitirán en los parámetros del método.

bool CNetCreatorPanel::CreateEdit(const int id,
                                  CEdit& object,
                                  const int x1,
                                  const int y1,
                                  const int x2,
                                  const int y2,
                                  bool read_only)
  {
   if(!object.Create(m_chart_id, StringFormat("%s%d", EDIT_NAME, id), m_subwin, x1, y1, x2, y2))
      return false;
   if(!object.TextAlign(ALIGN_RIGHT))
      return false;
   if(!object.ReadOnly(read_only))
      return false;
   if(!Add(object))
      return false;
//---
   return true;
  }

Además, utilizaremos enumeraciones y constantes para describir la arquitectura de las capas neuronales creadas. Para reducir a "0" la probabilidad de que el usuario introduzca valores incorrectos en dichos elementos, crearemos controles especiales. En ellas, el usuario solo podrá seleccionar un elemento de una lista sugerida. Necesitaremos varios de elementos de este tipo. Primero crearemos un elemento para especificar el tipo de capa neuronal. La implementación de esta funcionalidad la asignaremos al método CreateComboBoxType. Como este método está diseñado para crear un elemento concreto, no necesitaremos transmitir un puntero al objeto en los parámetros. En este caso solo tendremos que especificar las coordenadas del elemento a crear.

En el cuerpo del método, crearemos un elemento en el gráfico en las coordenadas especificadas y comprobaremos el resultado de la operación.

A continuación, deberemos rellenar el elemento con una descripción de texto de cada elemento y su identificador numérico. Si bien podemos usar el identificador del tipo de capa neuronal como identificador numérico, no hemos dado previamente una descripción textual en ningún sitio. Por consiguiente, crearemos el método aparte LayerTypeToString para convertir el identificador numérico en una descripción de texto. Su algoritmo es bastante simple. Le propongo estudiarlo por sí mismo en el archivo adjunto. Aquí solo utilizaremos la llamada a este método para cada tipo de capa neuronal,

y al final del método, añadiremos a la colección de objetos de nuestra interfaz el puntero a nuestro objeto.

Querríamos señalar que hemos añadido a la colección tanto objetos dinámicos como estáticos. Esto se relaciona con el hecho de que la funcionalidad de la colección es mucho más amplia que el control de la eliminación de los objetos después de la finalización. No obstante, los elementos de la colección también participan en la determinación de las coordenadas para ubicar los objetos en el gráfico, y en el procesamiento de los eventos. El propósito general de dicha colección es hacer posible el funcionamiento de todos los objetos como un organismo completo.

bool CNetCreatorPanel::CreateComboBoxType(const int x1, const int y1, const int x2, const int y2)
  {
   if(!m_cbNewNeuronType.Create(m_chart_id, "cbNewNeuronType", m_subwin, x1, y1, x2, y2))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronBaseOCL), defNeuronBaseOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronConvOCL), defNeuronConvOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronProofOCL), defNeuronProofOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronLSTMOCL), defNeuronLSTMOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronAttentionOCL), defNeuronAttentionOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronMHAttentionOCL), defNeuronMHAttentionOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronMLMHAttentionOCL), defNeuronMLMHAttentionOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronDropoutOCL), defNeuronDropoutOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronBatchNormOCL), defNeuronBatchNormOCL))
      return false;
   if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronVAEOCL), defNeuronVAEOCL))
      return false;
   if(!Add(m_cbNewNeuronType))
      return false;
//---
   return true;
  }

Del mismo modo, crearemos objetos para enumerar las funciones de activación y los métodos de optimización de los parámetros. Solo debemos señalar que utilizaremos la función estándar EnumToString para convertir la enumeración a su forma textual. Por lo tanto, podremos añadir elementos a la lista en un ciclo. El código completo del método se encuentra en el anexo.

Con esto podemos dar por concluido el trabajo preparatorio y pasar directamente a la creación de nuestra interfaz de usuario. Esta funcionalidad se implementará en el método Create. De entrada, le advertiremos que los parámetros del método solo nos darán las coordenadas de la esquina superior derecha de nuestro panel de interfaz en el gráfico. Sin embargo, también necesitaremos las dimensiones de nuestro panel para crear los objetos. Para facilitar el funcionamiento y una rápida modificación posterior en caso necesario, hemos fijado las dimensiones del panel usando constantes predefinidas. El panel se crea utilizando el mismo método de la clase padre. Esto es lo primero que llamaremos en el cuerpo de nuestro método.

bool CNetCreatorPanel::Create(const long chart, const string name, const int subwin, const int x1, const int y1)
  {
   if(!CAppDialog::Create(chart, name, subwin, x1, y1, x1 + PANEL_WIDTH, y1 + PANEL_HEIGHT))
      return false;

A continuación, aplicaremos los objetos de la interfaz al panel creado. Haremos esto de forma secuencial, empezando por la esquina superior izquierda. En este caso, vincularemos las coordenadas de cada nuevo objeto con las coordenadas del objeto anterior. Este enfoque nos permitirá alinear los objetos en una estructura uniforme.

Siguiendo la lógica anterior, empezaremos a crear los objetos en el grupo de trabajo con el modelo preentrenado. Primero crearemos una etiqueta de grupo. Para ello, definiremos las coordenadas de la etiqueta y llamaremos al método CreateLabel creado anteriormente. En este método, transmitiremos el texto de la etiqueta y sus coordenadas. Y, obviamente, no nos olvidaremos de añadir un identificador de etiqueta único.

   int lx1 = INDENT_LEFT;
   int ly1 = INDENT_TOP;
   int lx2 = lx1 + LIST_WIDTH;
   int ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateLabel(0, "PreTrained model", lx1, ly1, lx2, ly2))
      return false;

A continuación, crearemos el campo de entrada del nombre del archivo con un modelo preentrenado. Para ello, desplazaremos verticalmente las coordenadas del objeto creado y dejaremos las coordenadas horizontales sin modificar. De esta forma, los 2 objetos se situarán rigurosamente uno debajo del otro.

Aquí debemos añadir que no permitiremos que el usuario introduzca el nombre del archivo manualmente. En su lugar, pediremos al usuario que elija un archivo entre los ya existentes. Veremos la funcionalidad de esta acción un poco más adelante, pero por ahora haremos que el campo del nombre del archivo sea solo de lectura. Crearemos el objeto llamando al método CreateEdit que creamos antes. Una vez creado el campo, le añadiremos un mensaje informativo.

   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateEdit(0, m_edPTModel, lx1, ly1, lx2, ly2, true))
      return false;
   if(!m_edPTModel.Text("Select file"))
      return false;

A continuación, indicaremos el número total de capas neuronales del modelo entrenado. Para ello, crearemos una etiqueta de texto y un campo de entrada (en este caso de salida) para el número de capas neuronales. Este campo también será solo de lectura.

   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateLabel(1, "Layers Total", lx1, ly1, lx1 + EDIT_WIDTH, ly2))
      return false;
//---
   if(!CreateEdit(1, m_edPTModelLayers, lx2 - EDIT_WIDTH, ly1, lx2, ly2, true))
      return false;
   if(!m_edPTModelLayers.Text("0"))
      return false;

Del mismo modo, crearemos una etiqueta y varios campos para introducir el número de capas neuronales a copiar. Aquí necesitaremos un mecanismo que limite al usuario a la hora de seleccionar el número de capas neuronales. No deberá ser inferior a "0" ni superior al número total de capas neuronales del modelo. Podremos organizar esto de forma bastante sencilla usando un ejemplar del objeto clase CSpinEdit. Esta clase nos permitirá indicar un rango de valores admisibles. El resto ya está implementado en la clase.

   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateLabel(2, "Transfer Layers", lx1, ly1, lx1 + EDIT_WIDTH, ly2))
      return false;
//---
   if(!m_spPTModelLayers.Create(m_chart_id, "spPTMCopyLayers", m_subwin, lx2 - 100, ly1, lx2, ly2))
      return false;
   m_spPTModelLayers.MinValue(0);
   m_spPTModelLayers.MaxValue(0);
   m_spPTModelLayers.Value(0);
   if(!Add(m_spPTModelLayers))
      return false;

Todo lo que deberemos hacer a continuación es mostrar la descripción de la arquitectura del modelo preentrenado. Tenga en cuenta que antes de ello, siempre hemos desplazado las coordenadas de los objetos creados un nivel hacia abajo. Sin embargo, en este caso, solo hemos desplazado el límite superior hacia abajo desde el objeto anterior. El límite inferior del objeto lo hemos definido a la distancia de separación respecto a la altura de nuestra ventana. De este modo, hemos estirado el objeto hasta el tamaño de la ventana y hemos conseguido un borde uniforme en la parte inferior de la interfaz creada.

   lx1 = INDENT_LEFT;
   lx2 = lx1 + LIST_WIDTH;
   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ClientAreaHeight() - INDENT_BOTTOM;
   if(!m_lstPTModel.Create(m_chart_id, "lstPTModel", m_subwin, lx1, ly1, lx2, ly2))
      return false;
   if(!m_lstPTModel.VScrolled(true))
      return false;
   if(!Add(m_lstPTModel))
      return false;

Con esto hemos completado el bloque del modelo preentrenado y podemos pasar al segundo bloque de objetos para describir la arquitectura de la capa neuronal que vamos a añadir. Los objetos de este bloque también los crearemos desde arriba hacia abajo. Al definir las coordenadas para el nuevo objeto, desplazaremos las coordenadas horizontalmente y definiremos el límite superior en el nivel de separación desde el borde superior de la ventana.

   lx1 = lx2 + CONTROLS_GAP_X;
   lx2 = lx1 + ADDS_WIDTH;
   ly1 = INDENT_TOP;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateLabel(3, "Add layer", lx1, ly1, lx2, ly2))
      return false;

Abajo, en la distancia de separación, crearemos un cuadro combinado para seleccionar el tipo de capa neuronal que vamos a crear. Para ello, usaremos el método creado anteriormente. La anchura de este objeto será igual a la de todo el bloque.

   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateComboBoxType(lx1, ly1, lx2, ly2))
      return false;

A esto le seguirán los elementos para describir la arquitectura de la capa neuronal que vamos a crear. Para cada elemento de la clase de descripción de la arquitectura de la capa neural CLayerDescription, crearemos 2 objetos: una etiqueta de texto con el nombre del elemento y un campo para introducir valores. Para que nuestros elementos queden alineados en la barra de la interfaz, alinearemos las etiquetas de texto a la izquierda y los campos de entrada a la derecha de nuestro bloque. Todos los campos de entrada tendrán el mismo tamaño. Este enfoque nos ayudará a diseñar una apariencia de tabla.

No vamos a mostrar ahora el código idéntico para los nueve elementos. A continuación, mostraremos como ejemplo el código para crear 2 filas de nuestra tabla. Encontrar el código completo en el archivo adjunto.

   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateLabel(4, "Neurons", lx1, ly1, lx1 + EDIT_WIDTH, ly2))
      return false;
//---
   if(!CreateEdit(2, m_edCount, lx2 - EDIT_WIDTH, ly1, lx2, ly2, false))
      return false;
   if(!m_edCount.Text((string)DEFAULT_NEURONS))
      return false;
   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   if(!CreateLabel(5, "Activation", lx1, ly1, lx1 + EDIT_WIDTH, ly2))
      return false;
//---
   if(!CreateComboBoxActivation(lx2 - EDIT_WIDTH, ly1, lx2, ly2))
      return false;

Después de crear los elementos para describir la arquitectura de la capa neuronal a añadir, añadiremos 2 botones: uno para añadir una capa neuronal y otro para eliminarla. A continuación, dispondremos los botones en una sola fila, dividiendo entre ellos la anchura del bloque por la mitad.

   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + BUTTON_HEIGHT;
   if(!m_btAddLayer.Create(m_chart_id, "btAddLayer", m_subwin, lx1, ly1, lx1 + ADDS_WIDTH / 2, ly2))
      return false;
   if(!m_btAddLayer.Text("ADD LAYER"))
      return false;
   m_btAddLayer.Locking(false);
   if(!Add(m_btAddLayer))
      return false;
//---
   if(!m_btDeleteLayer.Create(m_chart_id, "btDeleteLayer", m_subwin, lx2 - ADDS_WIDTH / 2, ly1, lx2, ly2))
      return false;
   if(!m_btDeleteLayer.Text("DELETE"))
      return false;
   m_btDeleteLayer.Locking(false);
   if(!Add(m_btDeleteLayer))
      return false;

Después pasaremos al tercer y último bloque para describir la arquitectura completa del modelo a crear. Todas las técnicas usadas anteriormente se podrán encontrar aquí.

Una vez creados todos los elementos, saldremos del método con el resultado true. Encontrará el código completo del asesor en el archivo adjunto.

Con esto, podemos dar por completa la disposición de nuestros elementos de interfaz, así que ya podemos añadirla al asesor. Sin embargo, tal como está, solo supondrá una bonita imagen en un gráfico de instrumentos. El siguiente paso consistirá en proporcionar a nuestro formulario la funcionalidad que necesita.


2.3 Rellenando la herramienta con funcionalidad

Vamos a seguir construyendo nuestra herramienta: la siguiente etapa de nuestro trabajo consistirá en dotar a la interfaz de las funcionalidades necesarias. Antes de empezar, repasaremos una vez más el algoritmo necesario para nuestra herramienta.

  1. Primero deberemos abrir el archivo con el modelo entrenado guardado. Para ello, el usuario deberá clicar con el ratón en un objeto para seleccionar el archivo. Esto abrirá una ventana de diálogo en la que el usuario seleccionará un archivo existente con la extensión especificada.
  2. Una vez seleccionado el archivo, nuestra herramienta deberá cargar el modelo desde el archivo indicado y mostrar información sobre el modelo cargado (el tipo y el número de capas neuronales, el número de neuronas en cada capa).
  3. Junto con la muestra de información por defecto sobre el modelo cargado, en el nuevo modelo se copiarán todas sus capas neuronales. La información sobre ellos se copiará en el bloque de descripción del modelo a crear.
  4. El usuario debería poder cambiar manualmente el número de capas neuronales a copiar. De forma sincrónica con el cambio en el número de capas neuronales replicadas, deberemos hacer cambios en la arquitectura del modelo que estamos creando. Esto se reflejará en el bloque de descripción de la arquitectura del modelo creado.
  5. Después de seleccionar el número de capas neuronales a copiar, el usuario podrá indicar manualmente el tipo y la arquitectura de la nueva capa neuronal y añadir esta al modelo a crear pulsando el botón "ADD LAYER".
  6. Si se ha añadido por error una capa neuronal al modelo, el usuario podrá seleccionar esa capa en el bloque de descripción del modelo que está creando y eliminarla pulsando el botón "DELETE". Tenga en cuenta que solo se podrán eliminar las capas neuronales añadidas. Para eliminar capas del modelo donante, deberemos utilizar la herramienta para cambiar el número de capas neuronales a copiar.
  7. Tras implementar la arquitectura de la red neuronal a crear, el usuario deberá pulsar el botón "SAVE MODEL". Luego se abrirá una ventana de diálogo en la que podremos seleccionar un archivo existente o especificar un nuevo nombre de archivo.

Esto me parece un escenario lógico a la hora de trabajar con la herramienta, pero para ponerlo en práctica, tendremos que trabajar un poco. En primer lugar, necesitaremos implementar la funcionalidad necesaria para recuperar la información sobre el modelo guardado. Antes, no proporcionábamos al usuario información sobre el modelo descargado, y para implementar esta funcionalidad, deberemos realizar cambios en la clase de red neuronal. Pero como esta funcionalidad no influye sobre el modelo en sí, la añadiremos a la nueva clase CNetModify, que será heredera directa de la clase de modelo de red neuronal CNet creada anteriormente.

No esperamos que la nueva clase cree ningún objeto nuevo. Por consiguiente, el constructor y el destructor de la clase permanecerán vacíos. El método LayersTotal retorna el número de capas neuronales del modelo, y su algoritmo no tiene nada de complicado, pues simplemente devuelve el tamaño del array. Encontrará su código en el archivo adjunto.

class CNetModify :  public CNet
  {
public:
                     CNetModify(void) {};
                    ~CNetModify(void) {};
   //---
   uint              LayersTotal(void);
   CArrayObj*        GetLayersDiscriptions(void);
  };

No obstante, me propongo centrarme un poco en el método GetLayersDiscriptions para obtener información sobre las redes neuronales en uso. Como resultado de la ejecución de este método, esperamos obtener un array dinámico que describa la arquitectura de la red neuronal, similar a la descripción del modelo transmitida en los parámetros del método de construcción del modelo. La organización de este proceso, ya compleja de por sí, se ve agravada por el hecho de que no hayamos creado previamente métodos para obtener los hiperparámetros de las capas neuronales. Como consecuencia de ello, también deberemos añadir el método correspondiente a las clases de capas neuronales. Para empezar, añadiremos un método GetLayerInfo de este tipo a la clase básica de la capa neuronal CNeuronBaseOCL.

El nuevo método no tendrá parámetros, y devolverá el objeto CLayerDescription tras su ejecución. En el cuerpo del método, primero crearemos un ejemplar del objeto de descripción de la capa neuronal, y luego lo rellenaremos con los hiperparámetros de la capa neuronal actual. A continuación, saldremos del método y retornaremos el puntero al objeto creado al programa que lo llama.

CLayerDescription* CNeuronBaseOCL::GetLayerInfo(void)
  {
   CLayerDescription* result = new CLayerDescription();
   if(!result)
      return result;
//---
   result.type = Type();
   result.count = Output.Total();
   result.optimization = optimization;
   result.activation = activation;
   result.batch = (int)(optimization == LS ? iBatch : 1);
   result.layers = 1;
//---
   return result;
  }

Añadiendo el método a la clase básica de la capa neuronal, esencialmente hemos añadido un método a todos sus herederos. Es decir, todas nuestras capas neuronales han recibido este método, y ahora podemos obtener información similar de cualquier capa neuronal. Si no dispone de suficientes datos, podrá terminar aquí con la capa neuronal y pasar al método de recopilación de información sobre el modelo,

pero si necesita información específica para cada capa neuronal, deberá redefinir este método en todas las capas neuronales. A continuación, mostramos un ejemplo de redefinición del método en una capa de submuestra que recupera los datos sobre el tamaño de la ventana analizada y su paso de movimiento. En el cuerpo de este método, primero llamaremos al método de la clase padre para obtener los hiperparámetros básicos, y luego aumentaremos el objeto de descripción de la capa neuronal resultante con los parámetros específicos, saliendo finalmente del método y retornando el puntero al objeto de descripción de la capa neuronal al programa que lo llama.

CLayerDescription* CNeuronProofOCL::GetLayerInfo(void)
  {
   CLayerDescription *result = CNeuronBaseOCL::GetLayerInfo();
   if(!result)
      return result;
   result.window = (int)iWindow;
   result.step = (int)iStep;
//---
   return result;
  }

En el archivo adjunto podrá encontrar métodos similares para todos los tipos de capas neuronales anteriormente analizados.

Ahora tendremos la posibilidad de obtener información sobre los hiperparámetros de cada capa neuronal, y podremos combinarla en una estructura global. Vamos a regresar a nuestro método CNetModify::GetLayersDiscriptions y a crear un array dinámico en él para escribir los punteros a los objetos de descripción de las capas neuronales.

A continuación, crearemos un ciclo de enumeración de todas las capas neuronales, y en el cuerpo del mismo, consultaremos a cada capa neuronal para obtener un objeto que describa su arquitectura, llamando al método que hemos creado anteriormente. Los objetos resultantes se añadirán a nuestro array dinámico.

Después de todas las iteraciones del ciclo, obtendremos un array dinámico que describirá la arquitectura completa del modelo cargado. Esto es lo que retornaremos al programa que realiza la llamada cuando el método se complete.

CArrayObj* CNetModify::GetLayersDiscriptions(void)
  {
   CArrayObj* result = new CArrayObj();
   for(uint i = 0; i < LayersTotal(); i++)
     {
      CLayer* layer = layers.At(i);
      if(!layer)
         break;
      CNeuronBaseOCL* neuron = layer.At(0);
      if(!neuron)
         break;
      if(!result.Add(neuron.GetLayerInfo()))
         break;
     }
//---
   return result;
  }

En este punto, hemos implementado la posibilidad de obtener la descripción de la arquitectura de un modelo previamente creado, y podemos pasar a implementar el método para cargar un modelo preentrenado desde un archivo indicado por el usuario. Para implementar esta funcionalidad, se creará el método CNetCreatorPanel::LoadModel. En los parámetros, el método recuperará el nombre del archivo para cargar el modelo.

En el cuerpo del método, primero cargaremos el modelo desde el archivo especificado. Tenga en cuenta que antes de llamar al método de carga del modelo Load no comprobaremos el valor del parámetro, pues todos los controles están ya implementados en el método de carga. Una vez más, comprobaremos el resultado de la operación, y si obtenemos un error de carga del modelo, mostraremos la información al respecto en el bloque de descripción del modelo cargado.

bool CNetCreatorPanel::LoadModel(string file_name)
  {
   float error, undefine, forecast;
   datetime time;
   ResetLastError();
   if(!m_Model.Load(file_name, error, undefine, forecast, time, false))
     {
      m_lstPTModel.ItemsClear();
      m_lstPTModel.ItemAdd("Error of load model", 0);
      m_lstPTModel.ItemAdd(file_name, 1);
      int err = GetLastError();
      if(err == 0)
         m_lstPTModel.ItemAdd("The file is damaged");
      else
         m_lstPTModel.ItemAdd(StringFormat("error id: %d", GetLastError()), 2);
      m_edPTModel.Text("Select file");
      return false;
     }

Una vez que el modelo haya sido cargado con éxito, mostraremos el nombre del archivo cargado y el número de capas neuronales en los elementos correspondientes de la interfaz creada con anterioridad.

Luego eliminaremos la descripción del modelo cargado anteriormente, si la hay, y llamaremos al método para obtener información sobre la arquitectura del modelo cargado.

   m_edPTModel.Text(file_name);
   m_edPTModelLayers.Text((string)m_Model.LayersTotal());
   if(!!m_arPTModelDescription)
      delete m_arPTModelDescription;
   m_arPTModelDescription = m_Model.GetLayersDiscriptions();

Después de obtener la información sobre el modelo cargado, crearemos un ciclo en cuyo cuerpo mostraremos en el bloque correspondiente de nuestra interfaz la información recibida.

   m_lstPTModel.ItemsClear();
   int total = m_arPTModelDescription.Total();
   for(int i = 0; i < total; i++)
     {
      CLayerDescription* temp = m_arPTModelDescription.At(i);
      if(!temp)
         return false;
      //---
      string item = StringFormat("%s (units %d)", LayerTypeToString(temp.type), temp.count);
      if(!m_lstPTModel.AddItem(item, i))
         return false;
     }

Al final del método, cambiaremos el rango de valores del número de capas neuronales que se permite copiar al tamaño total del modelo cargado, e indicaremos a la herramienta que copie el modelo cargado en su totalidad. A continuación, saldremos del método.

   m_spPTModelLayers.MaxValue(total);
   m_spPTModelLayers.Value(total);
//---
   return true;
  }

Como podemos ver en el método anterior, el nombre del archivo para cargar los datos se encuentra en los parámetros del programa que realiza la llamada. Debemos permitir que el usuario seleccione el archivo del modelo por sí mismo.

A continuación, crearemos otro método OpenPreTrainedModel. En el cuerpo de este, solo organizaremos una llamada a la función estándar FileSelectDialog, en la que ya se ha implementado la interfaz de la ventana de diálogo de los archivos. Cuando se llama a esta función, solo se especificarán las extensiones de archivo necesarias y la bandera FSD_FILE_MUST_EXIST, que indica que solo se puede indicar un archivo existente.

Si se realizan ciertos ajustes en la bandera, esta función permitirá seleccionar varios archivos. Por ello, como resultado de la ejecución, FileSelectDialog retornará el número de archivos seleccionados, y sus nombres estarán contenidos en un array; el puntero al mismo lo recibirá la función en los parámetros.

Así, cuando el usuario seleccione un archivo, transmitiremos su nombre en los parámetros al método anterior. En caso contrario, mostraremos un mensaje en nuestra interfaz indicándonos que deberemos seleccionar un archivo para cargar los datos.

bool CNetCreatorPanel::OpenPreTrainedModel(void)
  {
   string filenames[];
   if(FileSelectDialog("Выберите файлы для загрузки", NULL,
                       "Neuron Net (*.nnw)|*.nnw|All files (*.*)|*.*",
                       FSD_FILE_MUST_EXIST, filenames, NULL) > 0)
     {
      if(!LoadModel(filenames[0]))
         return false;
     }
   else
      m_edPTModel.Text("Files not selected");
//---
   return true;
  }

Estamos avanzando poco a poco, y ya hemos logrado implementar la visualización de la interfaz de nuestra herramienta, así como una cadena de métodos para seleccionar un archivo y cargar un modelo preentrenado a partir de él. Pero hasta ahora, estos dos bloques de nuestro programa no se han combinado en un programa orgánico. Sí, en el método de carga de datos hemos logrado mostrar en el panel la información sobre el modelo de datos cargado, pero por ahora se trata de un "camino de ida". Ahora tenemos que señalar el camino de vuelta. Aquí es donde nuestro software obtendrá información sobre las acciones del usuario y su reacción a la información mostrada.

Para ello, usaremos el manejador de eventos. Los herederos de CAppDialog implementan este mecanismo a través de macrosustituciones. Para ello, se creará un bloque de macros en el código del programa que comenzará con la macro EVENT_MAP_BEGIN y terminará con la macro EVENT_MAP_END. Entre ellas hay una serie de macros que se corresponderán con diferentes eventos. En nuestro caso, usaremos la macro ON_EVENT, que supone que el evento será procesado por un identificador numérico. En concreto, para procesar el evento de clic de ratón sobre el objeto de nombre de archivo para cargar el modelo, en el cuerpo de la macro, indicaremos el evento ON_CLICK, el puntero del objeto m_edPTModel y el nombre del método a llamar cuando se produzca el evento OpenPreTrainedModel. Así, al clicar en el objeto m_edPTModel que se corresponde con el campo para introducir el nombre del archivo de carga del modelo, el programa llamará al método OpenPreTrainedModel y, por tanto, iniciará la cadena de métodos creada anteriormente para cargar el modelo preentrenado.

EVENT_MAP_BEGIN(CNetCreatorPanel)
ON_EVENT(ON_CLICK, m_edPTModel, OpenPreTrainedModel)
ON_EVENT(ON_CLICK, m_btAddLayer, OnClickAddButton)
ON_EVENT(ON_CLICK, m_btDeleteLayer, OnClickDeleteButton)
ON_EVENT(ON_CLICK, m_btSave, OnClickSaveButton)
ON_EVENT(ON_CHANGE, m_spPTModelLayers, ChangeNumberOfLayers)
ON_EVENT(ON_CHANGE, m_lstPTModel, OnChangeListPTModel)
EVENT_MAP_END(CAppDialog)

Del mismo modo, describiremos los otros eventos y los métodos llamados:

  • OnClickAddButton — método para procesar la pulsación del botón ADD LAYER;
  • OnClickDeleteButton  — método para procesar la pulsación del botón "DELETE";
  • OnClickSaveButton  — método para procesar la pulsación del botón "SAVE MODEL";
  • ChangeNumberOfLayers — método para procesar el evento de cambio del número de capas neuronales copiadas;
  • OnChangeListPTModel — método para procesar un evento de clic del ratón en una capa neuronal en la lista de descripción de la arquitectura del modelo cargado.

El código de todos los métodos anteriores se encuentra en el archivo adjunto. Proponemos al lector centrarnos en el método de almacenamiento del nuevo modelo. Su implementación resulta bastante compleja y requiere la creación de métodos adicionales en la clase del modelo de red neuronal CNetModify.

A grandes rasgos, el algoritmo de este método puede dividirse en 3 bloques:

  • copiado de las capas neuronales del modelo preentrenado;
  • adición de las nuevas capas neuronales al modelo;
  • almacenamiento del modelo en un archivo.

Por el momento, solo el último punto está implementado en nuestra clase de red neuronal. No disponemos de los métodos necesarios para copiar las capas neuronales de otro modelo, ni para añadir nuevas capas neuronales a un modelo existente.

Vamos a seguir punto por punto. Lo primero que haremos es crear un mecanismo para copiar las capas neuronales. Sabemos que, dependiendo de la arquitectura de la capa neuronal, esta puede contener un número diferente de objetos. Al mismo tiempo, necesitaremos un algoritmo universal que pueda copiar todos los tipos de capas neuronales con diferentes técnicas de optimización de parámetros. Al hacerlo, el copiado del modelo entrenado implicará la transferencia no solo de la arquitectura, sino también de todos los coeficientes de peso. Entonces nos surgirá una pregunta sensata: ¿por qué querríamos replicar completamente todos los elementos de cada capa neuronal? ¿Qué nos impide copiar simplemente un puntero al objeto de la capa neuronal necesaria? Como sabemos, el uso de punteros nos permite acceder al mismo objeto desde diferentes partes del código de nuestro programa. Precisamente esta propiedad es la que vamos a aprovechar. Bien, comenzaremos creando dos métodos. Uno de ellos retornará un puntero al objeto de capa neuronal según su número en la estructura del modelo. El segundo añadirá un puntero al objeto de la capa neuronal en la arquitectura del modelo.

CLayer* CNetModify::GetLayer(uint layer)
  {
   if(!layers || LayersTotal() <= layer)
      return NULL;
//---
   return layers.At(layer);
  }
bool CNetModify::AddLayer(CLayer *new_layer)
  {
   if(!new_layer)
      return false;
   if(!layers)
     {
      layers = new CArrayLayer();
      if(!layers)
         return false;
     }
//---
   return layers.Add(new_layer);
  }

Como vamos a copiar un bloque de capas neuronales consecutivas, al transferir los punteros a las capas neuronales en el nuevo modelo preservando plenamente su secuencia, conservaremos todas las relaciones entre dichas capas neuronales.

Ya hemos aclarado el primer punto. Sigamos adelante. Nuestro constructor de modelos es perfectamente capaz de crear un modelo nuevo partiendo de la descripción de la arquitectura que le transmitimos. Añadiendo capas neuronales al modelo, crearemos una descripción similar de las capas neuronales, y parece que lo único que deberemos hacer es añadir nuevas capas, algo que el modelo ya sabe realizar. No obstante, la dificultad radica en la ausencia de un puente entre las capas neuronales copiadas y las recién creadas.

La arquitectura de nuestras capas neuronales es tal que los pesos de una capa neuronal se encuentran en relación directa con los elementos de la otra capa neuronal. Por ello, necesitaremos construir esta relación para respetar el funcionamiento de las pasadas directas e inversas. Si miramos el método de inicialización de nuestra clase básica CNeuronBaseOCL, veremos entre sus parámetros un puntero al número de neuronas de la capa de neuronas subsiguiente. Este parámetro determinará el tamaño de la matriz de coeficientes de peso y los búferes vinculados utilizados en la optimización de los parámetros.

Primero añadiremos un método a la clase CNeuronBaseOCL que se encargará de ajustar la matriz de coeficientes de peso según el número indicado de neuronas en la capa subsiguiente CNeuronBaseOCL::numOutputs.

En los parámetros del método transmitiremos el número de neuronas de la capa subsiguiente y el método de optimización de los parámetros.

En el cuerpo del método, comprobaremos el número de elementos obtenidos en los parámetros de la capa de neuronas subsiguiente y, de ser necesario, crearemos una matriz de coeficientes de peso con las dimensiones correspondientes. En este caso, además, la rellenaremos con coeficientes de peso aleatorios, ya que se relaciona con la nueva capa neuronal a añadir. Para la matriz rellenada, crearemos un búfer en un contexto OpenCL y transmitiremos al mismo el contenido de la matriz.

La transmisión de datos al contexto OpenCL será imprescindible porque el método de nuestra clase intentará cargar los datos desde el contexto antes de guardarlos en un archivo, y en caso de error, interrumpirá el almacenamiento del modelo con un resultado negativo. Obviamente, podríamos introducir cambios en los métodos de nuestras clases de capas neuronales, pero, en mi opinión, este trabajo resultará más laborioso que la transferencia de información al contexto OpenCL y desde el mismo.

bool CNeuronBaseOCL::numOutputs(const uint outputs, ENUM_OPTIMIZATION optimization_type)
  {
   if(outputs > 0)
     {
      if(CheckPointer(Weights) == POINTER_INVALID)
        {
         Weights = new CBufferFloat();
         if(CheckPointer(Weights) == POINTER_INVALID)
            return false;
        }
      Weights.BufferFree();
      Weights.Clear();
      int count = (int)((Output.Total() + 1) * outputs);
      if(!Weights.Reserve(count))
         return false;
      float k = (float)(1 / sqrt(Output.Total() + 1));
      for(int i = 0; i < count; i++)
        {
         if(!Weights.Add((2 * GenerateWeight()*k - k)*WeightsMultiplier))
            return false;
        }
      if(!Weights.BufferCreate(OpenCL))
         return false;

Después de crear la matriz de coeficientes de peso, crearemos los búferes de datos usados en el proceso de optimización de los coeficientes de peso.

Si no necesitamos la matriz de coeficientes de peso y los búferes vinculados, entonces los eliminaremos, y saldremos del método.

El código completo del asesor se encuentra en el archivo adjunto.

Ahora vamos a regresar a la clase CNetModify para crear un método que añada capas neuronales según la descripción dada de AddLayers. En los parámetros, el método recibirá el puntero a un array dinámico que describirá la arquitectura de las capas neuronales a añadir, e inmediatamente, en el cuerpo del método, comprobaremos los datos obtenidos. Ante todo, el puntero resultante deberá ser válido y contener la descripción de al menos una capa neuronal.

bool CNetModify::AddLayers(CArrayObj *new_layers)
  {
   if(!new_layers || new_layers.Total() <= 0)
      return false;
//---
   if(!layers || LayersTotal() <= 0)
     {
      Create(new_layers);
      return true;
     }

A continuación, comprobaremos el número de capas neuronales que existen en el modelo. Si no hay, simplemente llamaremos al constructor de la clase padre. En él se creará un nuevo modelo con una arquitectura determinada.

Si vamos a añadir capas neuronales a un modelo existente, primero deberemos declarar las variables locales.

   CLayerDescription *desc = NULL, *next = NULL;
   CLayer *temp;
   int outputs;

Después realizaremos un pequeño trabajo preparatorio y llamaremos al método creado anteriormente para acoplar las dos capas neuronales.

   int shift = (int)LayersTotal() - 1;
   CLayer* last_layer = layers.At(shift);
   if(!last_layer)
      return false;
//---
   CNeuronBaseOCL* neuron = last_layer.At(0);
   if(!neuron)
      return false;
//---
   desc = neuron.GetLayerInfo();
   next = new_layers.At(0);
   outputs = (next == NULL || (next.type != defNeuron && next.type != defNeuronBaseOCL) ? 0 : next.count);
   if(!neuron.numOutputs(outputs, next.optimization))
      return false;
   delete desc;

Entonces, al igual que se hizo en el constructor de la clase padre, realizaremos un ciclo a través de un array dinámico que describirá la arquitectura del modelo y añadiremos secuencialmente todas las capas neuronales. El código de este bloque es exactamente igual al del constructor de la clase padre, así que el lector me permitirá que no lo repita en este artículo. Encontrará el código completo del asesor en el archivo adjunto.

Volvamos a nuestra clase de herramienta CNetCreatorPanel para crear el método encargado de procesar el evento Save Model Key, que combinará los métodos anteriores para crear un nuevo modelo en una sola secuencia.

Al principio del método OnClickSaveButton, pediremos al usuario que indique el archivo para almacenar el modelo. Para ello, usaremos la función FileSelectDialog, que ya conocemos. Esta vez cambiaremos la bandera que indica la creación del archivo a escribir, y especificaremos el nombre del archivo por defecto.

bool CNetCreatorPanel::OnClickSaveButton(void)
  {
   string filenames[];
   if(FileSelectDialog("Выберите файлы для сохранения", NULL,
                       "Neuron Net (*.nnw)|*.nnw|All files (*.*)|*.*",
                       FSD_WRITE_FILE, filenames, "NewModel.nnw") <= 0)
     {
      Print("File not selected");
      return false;
     }

A continuación, crearemos un nuevo ejemplar de la clase de red neuronal y comprobaremos el resultado de la operación.

   string file_name = filenames[0];
   if(StringLen(file_name) - StringLen(EXTENSION) > StringFind(file_name, EXTENSION))
      file_name += EXTENSION;
   CNetModify* new_model = new CNetModify();
   if(!new_model)
      return false;

Tras crear con éxito el nuevo modelo, organizaremos un ciclo de copiado del número necesario de capas neuronales. Al mismo tiempo, estableceremos para todas las capas neuronales copiadas la bandera de aprendizaje en false. Al hacerlo, desactivaremos el proceso de actualización de los pesos de estas capas durante el aprendizaje posterior. Más tarde, podremos cambiar programáticamente esta bandera para todas las capas neuronales del modelo llamando literalmente a un único método.

   int total = m_spPTModelLayers.Value();
   bool result = true;
   for(int i = 0; i < total && result; i++)
     {
      CLayer* temp = m_Model.GetLayer((uint)i);
      if(!temp)
        {
         result = false;
         break;
        }
      CNeuronBaseOCL* neuron = temp.At(0);
      neuron.TrainMode(false);
      if(!new_model.AddLayer(temp))
         result = false;
     }

Una vez completadas las iteraciones de copiado de las capas neuronales, llamaremos al método anterior de adición de capas neuronales, que completará la creación del nuevo modelo.

   new_model.SetOpenCL(m_Model.GetOpenCL());
   if(result && m_arAddLayers.Total() > 0)
      if(!new_model.AddLayers(GetPointer(m_arAddLayers)))
         result = false;

Después de todo esto, lo único que deberemos hacer es almacenar el modelo creado.

   if(result && !new_model.Save(file_name, 1.0e37f, 100, 0, 0, false))
      result = false;
//---
   if(!!new_model)
      delete new_model;
   LoadModel(m_edPTModel.Text());
//---
   return result;
  }

Tras guardar el modelo, podremos borrarlo, porque el entrenamiento ya se realizará en otro programa.

Aquí debemos recordar que al borrar el modelo también se borrarán las capas neuronales copiadas. Como no hemos copiado los datos en el nuevo modelo, solo habremos transmitido los punteros, y si el usuario quiere crear otro modelo basado en un modelo que ya está en uso, tendrá que cargarlo de nuevo. Vamos a ahorrar al usuario esta tarea innecesaria y llamaremos nosotros mismos al método para recargar el modelo. Solo entonces saldremos del método.

De esta forma habremos concluido el trabajo con nuestra clase y podremos proceder a comprobar el trabajo realizado.


3. Simulación

Para probar la herramienta creada, crearemos el asesor experto NetCreator.mq5. El código del asesor es bastante simple y contiene solo la conexión de la clase CNetCreatorPanel creada anteriormente. En esencia, la integración de la clase en el asesor tiene lugar en 3 puntos. La inicialización y la ejecución del modelo en la función OnInit. La destrucción de la clase en la función OnDeinit. Y la transmisión de los eventos a la clase en el método OnChartEvent. A continuación, mostramos el código de todos los puntos de integración.

#include "NetCreatorPanel.mqh"
CNetCreatorPanel Panel;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   if(!Panel.Create(0, "NetCreator", 0, 50, 50))
      return INIT_FAILED;
   if(!Panel.Run())
      return INIT_FAILED;
//---
   return(INIT_SUCCEEDED);
  }
void OnDeinit(const int reason)
  {
//---
   Panel.Destroy(reason);
  }
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   if(id == CHARTEVENT_OBJECT_CLICK)
      Sleep(0);
   Panel.ChartEvent(id, lparam, dparam, sparam);
  }

Las pruebas prácticas han confirmado nuestras expectativas en cuanto a la transferencia de las capas neuronales de un modelo a otro, con la posibilidad de añadir nuevas capas. Además, la herramienta nos permite crear un modelo completamente nuevo, evitando así describir el modelo a crear en el código del programa.  


Conclusión

En este artículo, hemos creado una herramienta que nos permite transferir algunas de las capas neuronales de un modelo a otro. Al hacer esto, podemos añadir cualquier número de capas nuevas de arquitectura arbitraria. De esta forma, proponemos a todo el mundo experimentar con sus modelos previamente entrenados y ver cómo un cambio de arquitectura puede afectar a la productividad del modelo.

Podrá tratar de combinar diferentes arquitecturas en un modelo, y realizar diversos experimentos para cambiar la arquitectura del modelo. Al mismo tiempo, si conservamos las arquitecturas de las capas de resultados y de datos de origen, podremos intentar "encajar" la arquitectura completamente nueva de un modelo en un asesor existente. Le proponemos entrenar el modelo y comparar el impacto de la arquitectura y el error del modelo.


Enlaces

  1. Redes neuronales: así de sencillo (Parte 20): Autocodificadores
  2. Redes neuronales: así de sencillo (Parte 21): Autocodificadores variacionales (VAE)
  3. Redes neuronales: así de sencillo (Parte 22): Aprendizaje no supervisado de modelos recurrentes

Programas usados en el artículo.

# Nombre Tipo Descripción
1 NetCreator.mq5 Asesor   Herramienta de construcción de modelos
2 NetCreatotPanel.mqh Biblioteca de clases Biblioteca de clases para crear la herramienta
3 NeuroNet.mqh Biblioteca de clases Biblioteca de clases para crear una red neuronal
4 NeuroNet.cl Biblioteca Biblioteca de código de programa OpenCL


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

Archivos adjuntos |
MQL5.zip (71.47 KB)
Aprendizaje automático y Data Science - Redes neuronales (Parte 01): Análisis de redes neuronales con conexión directa Aprendizaje automático y Data Science - Redes neuronales (Parte 01): Análisis de redes neuronales con conexión directa
A muchos les gustan todas las operaciones que hay detrás de las redes neuronales, pero pocos las entienden. En este artículo, intentaremos explicar en términos sencillos lo que ocurre detrás un perceptrón multinivel con conexión Feed Forward.
Creación simple de indicadores complejos usando objetos Creación simple de indicadores complejos usando objetos
El artículo presenta un método para crear indicadores complejos que nos evitará problemas al trabajar con múltiples gráficos y búferes, así como al combinar datos de varias fuentes.
DoEasy. Elementos de control (Parte 14): Nuevo algoritmo de denominación de los elementos gráficos. Continuamos trabajando con el objeto WinForms TabControl DoEasy. Elementos de control (Parte 14): Nuevo algoritmo de denominación de los elementos gráficos. Continuamos trabajando con el objeto WinForms TabControl
En este artículo, crearemos un nuevo algoritmo para nombrar todos los elementos gráficos y construir gráficos personalizados. Asimismo, continuaremos desarrollando el objeto WinForms TabControl.
DoEasy. Elementos de control (Parte 13): Optimizando la interacción de los objetos WinForms con el ratón. Comenzamos el desarrollo del objeto WinForms TabControl DoEasy. Elementos de control (Parte 13): Optimizando la interacción de los objetos WinForms con el ratón. Comenzamos el desarrollo del objeto WinForms TabControl
En el presente artículo, corregiremos y optimizaremos el procesamiento de la apariencia de los objetos WinForms después de mover el cursor del ratón lejos del objeto y comenzaremos a desarrollar el objeto TabControl WinForms.