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

Redes neuronales: así de sencillo (Parte 24): Mejorando la herramienta para el Transfer Learning

MetaTrader 5Sistemas comerciales | 15 noviembre 2022, 13:41
362 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Contenido

Introducción

En el anterior artículo de esta serie, creamos una herramienta para poder aprovechar la tecnología del Aprendizaje por Transferencia. Como resultado de un copioso trabajo, ahora tenemos una herramienta que permite editar modelos ya entrenados. Al hacerlo, podemos tomar cualquier número de capas neuronales de un modelo preentrenado. Obviamente, existen condiciones limitantes. Solo tomaremos capas consecutivas a partir de la capa de datos de origen. La razón de este enfoque parte de la naturaleza de las redes neuronales. Estas solo funcionan bien con datos de entrada similares a los utilizados en el entrenamiento del modelo.

Además, la herramienta creada permite no solo editar modelos entrenados, sino también crear modelos totalmente nuevos. Esto nos ahorra la necesidad de describir la arquitectura del modelo en el código del programa. Lo único que debemos hacer es crear un modelo con la herramienta, y luego entrenar y utilizar el modelo cargando la red neuronal creada desde un archivo. De esta forma, podremos experimentar con diferentes arquitecturas sin cambiar el código del programa. Ni siquiera tendremos que recompilar el mismo. Lo único que deberemos hacer es sustituir el archivo del modelo.

Obviamente, queremos que una herramienta tan útil resulte lo más fácil posible de usar. En este artículo, intentaremos hacer más cómodo su uso.


1. Mostrando la información completa de la capa neuronal

Comenzaremos a mejorar la usabilidad de nuestra herramienta aumentando la cantidad de información sobre cada capa neuronal. Como recordará, en el último artículo recopilamos toda la información que pudimos sobre la arquitectura de cada capa neuronal del modelo entrenado, pero solo mostramos al usuario el tipo de capa neuronal y el número de neuronas en la salida. Esto puede aceptarse al trabajar con un modelo cuya arquitectura se recuerda, pero cuando experimentamos con más modelos, esta cantidad de información obviamente no será suficiente.

No obstante, al mismo tiempo, más información requiere más espacio en el tablero de información. Añadir un desplazamiento horizontal a la ventana de información del modelo probablemente no sea la mejor solución. Así que hemos decidido mostrar la información sobre cada capa de neuronas en varias líneas. Y aquí resulta importante que la información mostrada sea fácil de leer, en lugar de fusionarse en una enorme prueba difícil de percibir. Podemos dividir el texto en bloques insertando separadores visuales entre las descripciones de dos capas neuronales consecutivas.

La solución aparentemente sencilla de dividir el texto en varias líneas durante el proceso de aplicación también ha requerido enfoques poco convencionales. La cuestión es que para mostrar la información sobre la arquitectura del modelo, usaremos la clase de lista CListView, en la que cada línea representa un elemento de la lista por separado. Al mismo tiempo, no existe la posibilidad de mostrar un elemento en varias líneas y agrupar varios elementos en una sola entidad. Añadir esta funcionalidad requeriría la introducción de cambios en el algoritmo y la arquitectura de la clase. En la práctica, esto resultaría en la creación de una nueva clase de objeto de control. En esta variante, podemos heredar de la clase CListView o crear un elemento completamente nuevo. Como podrá imaginar, hablamos de un trabajo bastante voluminoso, y esto no entra en mis planes.

Así que hemos decidido usar una clase existente, pero con algunos trucos sin realizar ningún cambio en el código de la clase en sí. Como hemos mencionado antes, usaremos elementos separadores para dividir visualmente las capas neuronales individuales en bloques. Precisamente estos bloques nos permitirán dividir el cuerpo total del texto que describe la arquitectura del modelo en bloques separados de capas neuronales, agrupando visualmente la información de cada capa neuronal.

Pero además de la agrupación visual, también necesitaremos una comprensión a nivel programático sobre la capa neuronal a la que pertenece un elemento concreto de la lista. Como recordará, en el último artículo implementamos el cambio del número de capas neuronales replicadas seleccionando con el ratón una sola capa neuronal del modelo entrenado y eliminando la capa seleccionada de la lista de capas neuronales a añadir al nuevo modelo. En ambos casos, necesitaremos comprender claramente la correspondencia entre el elemento asignado y la capa neuronal específica.

Y aquí hay que recordar que al añadir cada elemento a la lista, deberemos especificar su texto y algún valor numérico. Este valor numérico es el que suele usarse para identificar rápidamente el elemento destacado. Antes, aquí especificábamos algún valor individual para cada elemento, pero nadie nos impide utilizar un único valor para varios elementos. Obviamente, este enfoque dificultará la identificación de cada elemento de la lista, cosa que no necesitamos en estos momentos. Solo tendremos que identificar nuestro grupo de elementos. Por lo tanto, podemos usar esta función para identificar un grupo entero de elementos en lugar de uno solo.

bool  AddItem( 
   const string  item,     // text 
   const long    value     // value 
   )

En la práctica, esta solución nos ofrece otra ventaja. La clase CListView tiene el método SelectByValue. La funcionalidad principal de este método es seleccionar un elemento según su valor numérico. Su algoritmo está construido de tal forma que encuentra el primer elemento con el valor numérico especificado entre todos los elementos de la lista y lo selecciona. Es decir, organizando el procesamiento del evento de cambio en la selección de la lista, podremos leer el valor de un elemento seleccionado por el usuario y pedir a la clase que seleccione el primer elemento de la lista con ese valor. Con ello, haremos visible para el usuario el inicio del grupo que le interese. A nuestro juicio, se trata de una función muy útil.

bool  SelectByValue( 
   const long  value     // value 
   )

Ahora vamos a examinar la aplicación práctica de los planteamientos descritos anteriormente. En primer lugar, necesitaremos implementar una representación textual de la descripción de la arquitectura de la capa neuronal para mostrarla en el panel. Para ello, crearemos el método LayerDescriptionToString. Este método tomará como parámetros el puntero al objeto de descripción de la arquitectura de la capa neuronal y el puntero a un array dinámico de líneas en el que escribiremos la descripción textual de la capa neuronal. En este caso, además, cada elemento del array representará una línea distinta en la lista de descripción de la arquitectura del modelo. Es decir, para referirnos a la terminología usada anteriormente, cada una de estos arrays será un grupo separado de elementos en una lista para describir una sola capa neuronal. El uso de un array dinámico nos permitirá organizar grupos de elementos de diferentes tamaños, dependiendo de la necesidad que tengamos de describir una capa neuronal en particular.

Nuestro método retornará el número de elementos del array.

int CNetCreatorPanel::LayerDescriptionToString(const CLayerDescription *layer, string& result[])
  {
   if(!layer)
      return -1;

En el cuerpo del método, primero comprobaremos la validez del puntero resultante a la descripción de la arquitectura de la capa neuronal.

A continuación, prepararemos una variable local y borraremos el array dinámico resultante,

   string temp;
   ArrayFree(result);

creando luego una descripción textual de la capa neuronal según su tipo. Hay que decir que no vamos a trabajar directamente con un array dinámico de líneas. En su lugar, escribiremos toda la descripción en una sola línea. En este caso, insertaremos un carácter separador en las separaciones de línea. En el ejemplo anterior, usamos la barra invertida "\". Para facilitar la composición del texto con la marca establecida, hemos utilizado la función StringFormat, lo cual nos permite producir texto formateado con un mínimo de esfuerzo.

Después de compilar la descripción en forma de línea de la arquitectura de la capa neural, utilizaremos la función StringSplit y dividiremos nuestro texto en líneas. Esta función dividirá el texto en líneas en las que estará presente el elemento separador especificado que añadimos cuidadosamente al texto en la etapa anterior. La comodidad de esta función reside en que también aumenta el tamaño del array dinámico hasta alcanzar el requerido, lo cual significa que no tendremos que controlarlo.

   switch(layer.type)
     {
      case defNeuronBaseOCL:
         temp = StringFormat("Dense (outputs %d, \activation %s, \optimization %s)", 
                layer.count, EnumToString(layer.activation), EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;
      case defNeuronConvOCL:
         temp = StringFormat("Conolution (outputs %d, \window %d, step %d, window out %d, \activation %s, \optimization %s)",
                layer.count * layer.window_out, layer.window, layer.step, layer.window_out, EnumToString(layer.activation),

                EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;
      case defNeuronProofOCL:
         temp = StringFormat("Proof (outputs %d, \window %d, step %d, \optimization %s)",
                layer.count, layer.window, layer.step, EnumToString(layer.activation), EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;
      case defNeuronAttentionOCL:
         temp = StringFormat("Self Attention (outputs %d, \units %s, window %d, \optimization %s)",
                layer.count * layer.window, layer.count, layer.window, EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;
      case defNeuronMHAttentionOCL:
         temp = StringFormat("Multi-Head Attention (outputs %d, \units %s, window %d, heads %s, \optimization %s)",
                layer.count * layer.window, layer.count, layer.window, layer.step, EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;
      case defNeuronMLMHAttentionOCL:
         temp = StringFormat("Multi-Layer MH Attention (outputs %d, \units %s, window %d, key size %d, \heads %s, layers %d,
                              \optimization %s)",
                layer.count * layer.window, layer.count, layer.window, layer.window_out, layer.step, layer.layers,

                EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;
      case defNeuronDropoutOCL:
         temp = StringFormat("Dropout (outputs %d, \probability %d, \optimization %s)",
                layer.count, layer.probability, EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;
      case defNeuronBatchNormOCL:
         temp = StringFormat("Batchnorm (outputs %d, \batch size %d, \optimization %s)",
                layer.count, layer.batch, EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;
      case defNeuronVAEOCL:
         temp = StringFormat("VAE (outputs %d)", layer.count);
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;
      case defNeuronLSTMOCL:
         temp = StringFormat("LSTM (outputs %d, \optimization %s)", layer.count, EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;
      default:	
         temp = StringFormat("Unknown type %#x (outputs %d, \activation %s, \optimization %s)",
                layer.type, layer.count, EnumToString(layer.activation), EnumToString(layer.optimization));
         if(StringSplit(temp, '\\', result) < 0)
            return -1;
         break;
     }

Tras compilar la descripción para todas las capas neuronales conocidas, no nos olvidaremos de añadir una descripción estándar para los tipos desconocidos. Haciendo esto, informaremos al usuario de que se ha detectado una capa neuronal desconocida y evitaremos comprometer deliberadamente la integridad del modelo.

Al final del método, retornaremos el tamaño del array resultante al programa que ha realizado la llamada.

//---
   return ArraySize(result);
  }

A continuación, pasaremos al método LoadModel, que ya hemos visto en el artículo anterior. No modificaremos el método completo: solo cambiaremos el cuerpo del ciclo para añadir elementos a la lista. Como antes, primero recuperaremos en el cuerpo del ciclo el puntero al objeto que describe la siguiente capa neuronal desde un array dinámico, y comprobaremos directamente la validez del puntero resultante.

   for(int i = 0; i < total; i++)
     {
      CLayerDescription* temp = m_arPTModelDescription.At(i);
      if(!temp)
         return false;

A continuación, prepararemos un array dinámico de líneas y llamaremos al método anterior para generar una descripción textual de la capa neuronal LayerDescriptionToString. Cuando el método se complete, obtendremos un array de descripciones de líneas y el número de elementos que contiene. Si se produce un error, el método retornará un array vacío y "-1" en lugar del tamaño del array. A continuación, notificaremos al usuario sobre el error y finalizaremos el método.

      string items[];
      int total_items = LayerDescriptionToString(temp, items);
      if(total_items < 0)
        {
         printf("%s %d Error at layer %d: %d", __FUNCSIG__, __LINE__, i, GetLastError());
         return false;
        }

Si la composición de la descripción textual de la capa neural tiene éxito, primero añadiremos un elemento separador de bloques, y luego, en un ciclo anidado, mostraremos todo el contenido del array de descripción textual de la capa neuronal.

      if(!m_lstPTModel.AddItem(StringFormat("____ Layer %d ____", i + 1), i + 1))
         return false;
      for(int it = 0; it < total_items; it++)
         if(!m_lstPTModel.AddItem(items[it], i + 1))
            return false;
     }

Tenga en cuenta que al especificar un identificador de grupo, añadiremos 1 al número ordinal de la capa neuronal en la descripción del array dinámico de la arquitectura del modelo. Esta es una acción necesaria, ya que la indexación de los elementos del array comienza con "0". Al indicar "0" como identificador numérico, la clase CListView lo sustituirá automáticamente por el número total de elementos de la lista, y no querríamos obtener un valor aleatorio en lugar del ID de grupo.

El código del método LoadModel no se modifica, y su código completo se puede encontrar en el archivo adjunto. Ahí también podrá encontrar los códigos de todos los métodos y clases utilizados en el programa descrito. En concreto, podrá ver adiciones similares en el método de muestra de la nueva descripción del modelo ChangeNumberOfLayers.

Tenga en cuenta que en el método ChangeNumberOfLayers la información sobre el modelo a crear se genera a partir de los 2 arrays dinámicos que describen la arquitectura del modelo. La primera describe la arquitectura del modelo donante. De esta tomaremos la descripción de las capas neuronales que se van a copiar. La segunda matriz, entre tanto, contiene una descripción de las redes neuronales que se van a añadir.

Después de mostrar la descripción de la arquitectura del modelo, pasaremos a los métodos de procesamiento de los eventos de cambio de estado de las listas creadas.

ON_EVENT(ON_CHANGE, m_lstPTModel, OnChangeListPTModel)
ON_EVENT(ON_CHANGE, m_lstNewModel, OnChangeListNewModel)

Como hemos descrito antes, cuando el usuario seleccione cualquier línea de la lista, desplazaremos la selección a la primera línea del bloque indicado. Para ello, simplemente obtendremos el identificador del grupo del elemento seleccionado por el usuario y le diremos al programa que seleccione el primer elemento con el identificador recibido. Esta operación la realizará el método SelectByValue.

bool CNetCreatorPanel::OnChangeListNewModel(void)
  {
   long value = m_lstNewModel.Value();
//---
   return m_lstNewModel.SelectByValue(value);
  }

De este modo, hemos ampliado la cantidad de información mostrada sobre la arquitectura del modelo. Al hacerlo, hemos logrado que la cantidad de información mostrada sea la mínimamente suficiente y dependa del tipo de capa neuronal. Es decir, al usuario solo se le presentará la información relevante para describir la arquitectura de cada capa neuronal específica, mientras que la información innecesaria no abarrotará la ventana de información.


2. Activando los campos de entrada utilizados/desactivando los no utilizados

Nuestra próxima mejora se referirá a los campos de entrada de datos. Por extraño que parezca, aquí hay mucho margen para la imaginación. Seguramente, lo primero que le llame la atención sea la cantidad de información introducida. En el panel hemos creado campos de entrada para todos los elementos de la clase de descripción de la arquitectura de la capa neural CLayerDescription. No podemos decir que sea algo malo. De esta forma, el usuario verá todos los datos especificados y podrá cambiarlos en cualquier orden y cualquier número de veces antes de añadir una capa. Pero usted y yo sabemos que no todos estos campos resultan relevantes para todas las capas neuronales.

Por ejemplo, para indicar los parámetros de una capa neuronal totalmente conectada, solo hay que especificar 3 de ellos: el número de neuronas, la función de activación y el método de optimización de los parámetros. Los demás parámetros son irrelevantes para ella. Al mismo tiempo, para una capa neuronal convolucional, hay que especificar el tamaño de la ventana de datos de entrada y su paso, mientras que el número de elementos en la salida de la capa neuronal dependerá del tamaño del búfer de datos de entrada y de los dos parámetros mencionados anteriormente.

Ya en el bloque recursivo LSTM, las funciones de activación estarán definidas por la arquitectura del bloque y no será necesario especificarlas. 

Claro que el usuario podrá conocer todos esos matices, pero una herramienta bien diseñada deberá advertir al usuario de posibles errores "mecánicos". Aquí hay dos opciones preventivas. Podemos eliminar los elementos irrelevantes del panel o simplemente hacer que no estén disponibles para su edición.

Cada opción tiene sus ventajas y sus desventajas. La primera opción tiene la ventaja de reducir el número de campos de entrada en el panel. Esto significa que el panel puede ser más compacto. La desventaja sería su aplicación, más complicada, ya que cada vez deberemos desplazar elementos por el panel. Al mismo tiempo, el movimiento constante de los objetos podría confundir al usuario y provocar errores de funcionamiento.

A nuestro juicio, el uso de este método está justificado cuando debemos introducir grandes cantidades de datos. La eliminación de objetos innecesarios hará que el panel resulte más compacto y evitará el desorden de los elementos.

La segunda opción resultará aceptable para un número reducido de elementos, cuando podemos organizar fácilmente todos los elementos de un panel a la vez. Es una opción cómoda de usar. De esta forma, no confundiremos al usuario moviéndolo innecesariamente por el panel. Como resultado, después de unas pocas iteraciones, el usuario podrá recordar su ubicación visualmente y será capaz de trabajar con los elementos de forma casi automática, lo cual aumentará su productividad en general.

Ya hemos colocado todos los campos de entrada en el panel de la interfaz de nuestra herramienta, por lo que consideraremos la segunda opción como aceptable.

Pues bien, ahí tenemos la primera solución arquitectónica, pero vamos a ir un poco más allá. Nuestro panel tiene tanto un campo de lista desplegable como un campo de introducción directa de valores, y si el campo con la lista desplegable permite al usuario seleccionar solo una de las opciones posibles, resultará físicamente posible que el usuario introduzca cualquier texto en los campos de introducción directa de valores.

Esperamos obtener un valor entero allí, así que tendrá sentido añadir la comprobación de la información introducida antes de transferir esta al objeto que describe la arquitectura de la capa neuronal a crear. Y para que el usuario pueda comprobar por sí mismo cómo de bien percibe la información nuestra herramienta, implementaremos la verificación de la corrección de la información introducida en cuanto el usuario introduzca el texto. Después de realizar la comprobación en el campo de entrada, sustituiremos la información introducida por el usuario por la información percibida por la herramienta. Así, el usuario podrá ver la diferencia entre la información introducida y la leída, y, si fuera necesario, realizará las correcciones correspondientes.

Una cosa más. Al describir la arquitectura de la capa neuronal en la clase CLayerDescription, tendremos elementos de doble propósito. Así es como el elemento step para las capas convolucional y de submuestreo indicará el paso de la ventana de datos de origen. También se usará para indicar el número de cabezas de atención al describir las capas neuronales de la atención.

Del mismo modo, el parámetro window_out indicará el número de filtros en la capa de convolución y el tamaño de la capa interna de claves en el bloque de atención.

Para que nuestra interfaz sea más fácil de usar, lo correcto será realizar cambios en las etiquetas de texto al seleccionar el tipo de capa neuronal correspondiente.

Esto no deberá confundirse con el problema de la reordenación de los elementos en la ventana de la interfaz descrito anteriormente. En este caso, el campo de entrada no se modificará. Solo cambiará el mensaje informativo junto a la ventana. Si el usuario no presta atención al cambio de rótulo e introduce automáticamente los datos en el campo correspondiente, no se producirá ningún error en la organización del modelo, ya que la información caerá en todo caso en el elemento correcto de la descripción de la arquitectura de la capa neuronal a crear.

Para poner en práctica las soluciones descritas anteriormente, deberemos dar un paso atrás y realizar un trabajo preparatorio.

En primer lugar, al crear etiquetas de texto en el panel de la interfaz de nuestra herramienta, no guardaremos los punteros a los objetos correspondientes. Ahora, cuando necesitemos cambiar el texto de algunos de ellos, deberemos buscarlos en un array común de objetos. Para evitar esto, volveremos al método CreateLabel y cuando las operaciones del método se completen, retornaremos el puntero al objeto creado en lugar del resultado lógico.

CLabel* 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 NULL;
   if(!tmp_label.Create(m_chart_id, StringFormat("%s%d", LABEL_NAME, id), m_subwin, x1, y1, x2, y2))
     {
      delete tmp_label;
      return NULL;
     }
   if(!tmp_label.Text(text))
     {
      delete tmp_label;
      return NULL;
     }
   if(!Add(tmp_label))
     {
      delete tmp_label;
      return NULL;
     }
//---
   return tmp_label;
  }

Obviamente, no guardaremos los punteros a todas las etiquetas, solo guardaremos los 2 objetos de interés. Para ello, declararemos dos variables adicionales. Aunque utilizaremos punteros dinámicos a objetos, no los añadiremos al destructor de nuestra clase de herramienta. Estos objetos serán, como antes, borrados en un array de todos los objetos de la herramienta, pero al hacerlo, tendremos acceso directo a los objetos que necesitamos.

   CLabel*           m_lbWindowOut;
   CLabel*           m_lbStepHeads;

La escritura de los punteros a las nuevas variables se realizará en el método Create de nuestra clase, donde haremos los pequeños cambios que se indican a continuación. El resto del código del método no se modificará. Podrá encontrar el código completo de los métodos en el archivo adjunto.

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;
//---
...............
...............
//---
   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   m_lbStepHeads = CreateLabel(8, "Step", lx1, ly1, lx1 + EDIT_WIDTH, ly2);
   if(!m_lbStepHeads)
      return false;
//---
...............
...............
//---
   ly1 = ly2 + CONTROLS_GAP_Y;
   ly2 = ly1 + EDIT_HEIGHT;
   m_lbWindowOut = CreateLabel(9, "Window Out", lx1, ly1, lx1 + EDIT_WIDTH, ly2);
   if(!m_lbWindowOut)
      return false;
//---
...............
...............
//---
   return true;
  }

El siguiente paso en nuestro trabajo preparatorio consistirá en crear un método para cambiar el estado del campo de entrada. Aquí debemos decir que la clase estándar CEdit ya tiene un método ReadOnly para cambiar el estado del objeto, pero este método no posibilita la visualización del estado del objeto, solo bloquea la posibilidad de introducir datos. Y lo que a nosotros nos gustaría es que los objetos visualmente separables estuvieran disponibles para la entrada de datos y no. Ciertamente, no vamos a inventar nada nuevo aquí. Resaltaremos los objetos con el color de fondo. Para los campos editables, haremos que el color de fondo del objeto sea blanco, mientras que para los objetos inmutables, haremos que el fondo sea del mismo color que el panel.

Esta funcionalidad se implementará en el método EditReedOnly. En los parámetros del método, transmitiremos el puntero al objeto y una nueva bandera de estado. En el cuerpo del método, transmitiremos la bandera obtenida al método ReadOnly del objeto de entrada y estableceremos el fondo del objeto para que coincida con la bandera indicada.

bool CNetCreatorPanel::EditReedOnly(CEdit& object, const bool flag)
  {
   if(!object.ReadOnly(flag))
      return false;
   if(!object.ColorBackground(flag ? CONTROLS_DIALOG_COLOR_CLIENT_BG : CONTROLS_EDIT_COLOR_BG))
      return false;
//---
   return true;
  }

Ahora nos centraremos en las funciones de activación. Más concretamente, en la lista desplegable de funciones de activación disponibles. En esta cuestión, deberemos señalar que dicha lista desplegable no resulta relevante para todos los tipos de capas neuronales. Como en algunas arquitecturas, el tipo de función de activación está claramente prescrito y no es modificado por esta lista. Ejemplos de ello son el bloque LSTM, la capa de submuestreo, o los bloques de atención. No obstante, la clase CComboBox no ofrece un método para bloquear la funcionalidad de la clase de ninguna manera. Por ello, buscaremos otra solución y modificaremos la lista de funciones de activación disponibles en cada caso. Para ello, crearemos métodos separados para rellenar la lista de funciones de activación disponibles,

de hecho, solo habrá dos métodos de este tipo. Una, por así decirlo, general, con especificación de las funciones de ActivationListMain. El segundo será una ActivationListEmpty «vacía» en la que solo habrá una opción, «None», disponible.

Para entender el algoritmo de construcción del método, analizaremos el código del método ActivationListMain. Al inicio del método, eliminaremos completamente la lista existente de elementos disponibles para la activación, y luego rellenaremos la lista de elementos en un ciclo usando el método ItemAdd y la función EnumToString.

Tenga en cuenta que la codificación de los elementos en la lista de funciones de activación comienza con "-1" para None, mientras que la siguiente función de tangente hiperbólica TANH de la lista tiene el índice "0". Esto no es bueno por la razón dada anteriormente para la descripción del rellenado de la lista de la arquitectura de la red neuronal. Después de todo, la lista desplegable es una clase CListView que ya conocemos. Por ello, al igual que antes, para evitar la presencia del valor nulo del identificador de la lista, simplemente añadiremos una pequeña constante al identificador de enumeración.

Tras rellenar la lista de funciones de activación disponibles, estableceremos el valor por defecto y saldremos del método.

bool CNetCreatorPanel::ActivationListMain(void)
  {
   if(!m_cbActivation.ItemsClear())
      return false;
   for(int i = -1; i < 3; i++)
      if(!m_cbActivation.ItemAdd(EnumToString((ENUM_ACTIVATION)i), i + 2))
         return false;
   if(!m_cbActivation.SelectByValue((int)DEFAULT_ACTIVATION + 2))
      return false;
//---
   return true;
  }

Otro método que vamos a necesitar, nos permitirá automatizar un poco más la experiencia del usuario. Ya hemos mencionado que en el caso de los modelos convolucionales o de los bloques de atención, el número de elementos en la salida del modelo dependerá del tamaño de la ventana de datos de origen analizados y de su paso de desplazamiento. Y aquí, para evitar posibles errores y reducir el trabajo manual del usuario, hemos decidido cerrar el campo de entrada del recuento de bloques y rellenarlo con el método independiente SetCounts.

En los parámetros de este método, transmitiremos el tipo de capa neuronal a crear, y luego retornaremos el resultado lógico de la operación.

bool CNetCreatorPanel::SetCounts(const uint position, const uint type)
  {
   const uint position = m_arAddLayers.Total();

Ya en el cuerpo del método, primero determinaremos el número de elementos en la salida de la capa anterior. Aquí debemos entender que la capa anterior puede encontrarse en uno de los dos arrays dinámicos siguientes: la descripción de la arquitectura del modelo donante o la descripción de la arquitectura de adición de nuevas capas neuronales. Determinar de dónde tenemos que sacar la última capa neuronal resulta bastante sencillo. Siempre añadiremos una capa neural al final de la lista. En consecuencia, solo tomaremos una capa del modelo donante si el array de nuevas capas neuronales está vacío. Siguiendo esta lógica, comprobaremos el tamaño del array dinámico de las nuevas capas neuronales, y, en función de su tamaño, consultaremos el array correspondiente para obtener el puntero a la capa neuronal anterior.

   CLayerDescription *prev;
   if(position <= 0)
     {
      if(!m_arPTModelDescription || m_spPTModelLayers.Value() <= 0)
         return false;
      prev = m_arPTModelDescription.At(m_spPTModelLayers.Value() - 1);
      if(!prev)
         return false;
     }
   else
     {
      if(m_arAddLayers.Total() < (int)position)
         return false;
      prev = m_arAddLayers.At(position - 1);
     }
   if(!prev)
      return false;

A continuación, contaremos el número de elementos del búfer de resultados de la capa anterior según su tipo. Si, por cualquier motivo, el tamaño del búfer no es superior a "0", saldremos del método con el resultado false.

   int outputs = prev.count;
   switch(prev.type)
     {
      case defNeuronAttentionOCL:
      case defNeuronMHAttentionOCL:
      case defNeuronMLMHAttentionOCL:
         outputs *= prev.window;
         break;
      case defNeuronConvOCL:
         outputs *= prev.window_out;
         break;
     }
//---
   if(outputs <= 0)
      return false;

A continuación, leeremos de la interfaz los valores del tamaño de la ventana de datos de origen analizados y su paso, y también prepararemos una variable para registrar el resultado del cálculo.

   int counts = 0;
   int window = (int)StringToInteger(m_edWindow.Text());
   int step = (int)StringToInteger(m_edStep.Text());

Después calcularemos directamente el número de elementos según el tipo de la capa neuronal a crear. Para calcular el número de elementos de las capas convolucionales y de submuestreo, necesitaremos el tamaño de la ventana de datos de origen analizados y su paso.

   switch(type)
     {
      case defNeuronConvOCL:
      case defNeuronProofOCL:
         if(step <= 0)
            break;
         counts = (outputs - window - 1 + 2 * step) / step;
         break;

Al utilizar bloques de atención, el tamaño del paso será el mismo que el tamaño de la ventana, y, según las reglas matemáticas, la fórmula se reducirá ligeramente.

      case defNeuronAttentionOCL:
      case defNeuronMHAttentionOCL:
      case defNeuronMLMHAttentionOCL:
         if(window <= 0)
            break;
         counts = (outputs + window - 1) / window;
         break;

En el caso de la capa latente del autocodificador variacional, el tamaño de la capa será exactamente la mitad del tamaño de la capa anterior.

      case defNeuronVAEOCL:
         counts = outputs / 2;
         break;

Para todos los demás casos, fijaremos un tamaño para la capa neuronal igual al tamaño de la capa anterior. Podemos valernos de ello al declarar una capa de normalización por lotes o Dropout.

      default:
         counts = outputs;
         break;
     }
//---
   return m_edCount.Text((string)counts);
  }

El valor resultante se transmitirá al elemento de interfaz correspondiente.

Ahora disponemos de medios suficientes para organizar los cambios de la interfaz según la elección del tipo de capa neuronal a crear. Así que vamos a ver cómo podemos hacer esto. La presente funcionalidad se implementa en el método OnChangeNeuronType. El nombre del método no es casual. Al fin y al cabo, lo llamaremos cada vez que el usuario cambie el tipo de capa neuronal a añadir.

El método indicado no tiene parámetros y retorna el resultado lógico de las operaciones. En el cuerpo del método, primero definiremos el tipo de capa neuronal a crear seleccionado por el usuario.

bool CNetCreatorPanel::OnChangeNeuronType(void)
  {
   long type = m_cbNewNeuronType.Value();

Nuestro algoritmo se ramificará entonces según el tipo de capa neuronal elegido. El algoritmo para cada capa neuronal resultará similar, pero casi todas las capas neuronales estarán matizadas. Para una capa neuronal completamente conectada, dejaremos solo un campo para introducir el número de neuronas activas y cargaremos una lista completa de posibles funciones de activación.

   switch((int)type)
     {
      case defNeuronBaseOCL:
         if(!EditReedOnly(m_edCount, false) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, true) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!ActivationListMain())
            return false;
         break;

En el caso de las capas de convolución, los otros 3 campos de entrada estarán activos. El tamaño de la ventana de datos de origen a analizar, su paso, y también el tamaño de la ventana de resultados (número de filtros). Al hacerlo, actualizaremos el valor de las dos etiquetas de texto y empezaremos a recalcular el número de elementos de la capa neuronal según el tamaño de la ventana de datos de origen y su paso. Debemos decir que estamos contando el número de elementos para un filtro. Por ello, el resultado del cálculo será independiente del número de filtros utilizados.

      case defNeuronConvOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, false) ||
            !EditReedOnly(m_edWindow, false) ||
            !EditReedOnly(m_edWindowOut, false))
            return false;
         if(!m_lbStepHeads.Text("Step"))
            return false;
         if(!m_lbWindowOut.Text("Window Out"))
            return false;
         if(!ActivationListMain())
            return false;
         if(!SetCounts(defNeuronConvOCL))
            return false;
         break;

Para la capa de submuestreo, no especificaremos el número de filtros ni la función de activación. En nuestra implementación, siempre utilizaremos el valor máximo como función de activación de la capa de submuestreo. Por lo tanto, limpiaremos la lista de funciones de activación disponibles, pero, al igual que sucede con la capa convolucional, iniciaremos el cálculo del número de elementos de la capa a crear.

      case defNeuronProofOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, false) ||
            !EditReedOnly(m_edWindow, false) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!m_lbStepHeads.Text("Step"))
            return false;
         if(!SetCounts(defNeuronProofOCL))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;

La lista de funciones de activación tampoco se usará al declarar el bloque LSTM, por lo que la borraremos. En este caso, solo el campo para el número de elementos en la capa de neuronas estará abierto para la introducción de datos.

      case defNeuronLSTMOCL:
         if(!EditReedOnly(m_edCount, false) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, true) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;

Para inicializar la capa Dropout, solo deberemos especificar los valores de la probabilidad de valores atípicos de las neuronas. La función de activación no se utilizará, y el número de elementos será igual al tamaño de la capa neuronal anterior.

      case defNeuronDropoutOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, false) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, true) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!SetCounts(defNeuronDropoutOCL))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;

El enfoque es similar al considerado al declarar una capa de normalización por lotes, solo que, en este caso, especificaremos el tamaño del lote.

      case defNeuronBatchNormOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, false) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, true) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!SetCounts(defNeuronBatchNormOCL))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;

Dependiendo del método de atención, haremos que los campos de entrada para el número de cabezas de atención y capas neuronales en el bloque estén activos. Al hacerlo, cambiaremos las etiquetas de texto a los campos de entrada correspondientes.

      case defNeuronAttentionOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, false) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!SetCounts(defNeuronAttentionOCL))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;
      case defNeuronMHAttentionOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, false) ||
            !EditReedOnly(m_edWindow, false) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!m_lbStepHeads.Text("Heads"))
            return false;
         if(!SetCounts(defNeuronMHAttentionOCL))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;
      case defNeuronMLMHAttentionOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, false) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, false) ||
            !EditReedOnly(m_edWindow, false) ||
            !EditReedOnly(m_edWindowOut, false))
            return false;
         if(!m_lbStepHeads.Text("Heads"))
            return false;
         if(!m_lbWindowOut.Text("Keys size"))
            return false;
         if(!SetCounts(defNeuronMLMHAttentionOCL))
            return false;
         if(!ActivationListEmpty())
            return false;
         break;

Para la capa latente del autocodificador variacional, no será necesario introducir ningún dato. Solo deberemos seleccionar el tipo de capa y añadirla al modelo.

      case defNeuronVAEOCL:
         if(!EditReedOnly(m_edCount, true) ||
            !EditReedOnly(m_edBatch, true) ||
            !EditReedOnly(m_edLayers, true) ||
            !EditReedOnly(m_edProbability, true) ||
            !EditReedOnly(m_edStep, true) ||
            !EditReedOnly(m_edWindow, true) ||
            !EditReedOnly(m_edWindowOut, true))
            return false;
         if(!ActivationListEmpty())
            return false;
         if(!SetCounts(defNeuronVAEOCL))
            return false;
         break;

Si no encontramos el tipo de capa neuronal indicado en los parámetros, el método finalizará con false.

      default:
         return false;
         break;
     }
//---
   return true;
  }

Si todas las operaciones del método tienen éxito, saldremos de él con un resultado positivo.

Ahora solo deberemos hacer que el método descrito se ejecute en el momento adecuado. Para ello, usaremos el evento de cambio de valor del elemento de selección del tipo de capa neural y añadiremos el manejador de eventos correspondiente.

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)
ON_EVENT(ON_CHANGE, m_lstNewModel, OnChangeListNewModel)
ON_EVENT(ON_CHANGE, m_cbNewNeuronType, OnChangeNeuronType)
EVENT_MAP_END(CAppDialog)

Al implementar los métodos anteriores, hemos organizado la activación y desactivación de los campos de entrada según el tipo de capa neuronal seleccionada. Pero antes también hemos hablado del control de la introducción de datos.

Si se fija bien, verá que en todos los campos de entrada de datos esperamos obtener número enteros estrictamente mayores que cero. La única excepción es el valor de la probabilidad de valores atípicos de los elementos en la capa Dropout. Aquí se permite un valor real entre 0 y 1. En consecuencia, necesitaremos 2 métodos para verificar los datos introducidos. Uno para la probabilidad y otro para el resto de los elementos.

El algoritmo de ambos métodos será bastante sencillo. Primero leeremos el valor textual introducido por el usuario, convirtiéndolo a continuación a un valor numérico. Luego comprobaremos que se cumpla el rango de valores permitidos, y retornaremos este valor a la ventana de la interfaz correspondiente. El usuario deberá asegurarse de que los datos se interpreten correctamente.

bool CNetCreatorPanel::OnEndEditProbability(void)
  {
   double value = StringToDouble(m_edProbability.Text());
   return m_edProbability.Text(DoubleToString(fmax(0, fmin(1, value)), 2));
  }
bool CNetCreatorPanel::OnEndEdit(CEdit& object)
  {
   long value = StringToInteger(object.Text());
   return object.Text((string)fmax(1, value));
  }

Tenga en cuenta que si bien identificamos claramente el campo de entrada al comprobar si el valor de la probabilidad se introduce correctamente, para identificar el objeto en el segundo método, en cambio, transmitiremos el puntero al objeto correspondiente en los parámetros del método. Y ahí se encuentra el siguiente reto. En las macros de procesamiento de eventos sugeridas no existe una macro adecuada para transmitir al método de procesamiento de eventos el puntero al objeto que activa el evento. Así que hemos tenido que añadir una macro como esta.

#define ON_EVENT_CONTROL(event,control,handler)          if(id==(event+CHARTEVENT_CUSTOM) && lparam==control.Id()) \
                                                              { handler(control); return(true); }

Aquí debemos recordar que los campos de entrada a comprobar pueden incluir el tamaño y el paso de la ventana de datos de origen analizados, y como recordará, estos parámetros determinarán el número de elementos de la capa neuronal. Esto significa que si cambiamos sus valores, deberemos recalcular el tamaño de la capa neuronal que estamos creando, salvo que el modelo de procesamiento de eventos que utilizamos solo permitirá un manejador para cada evento. Al mismo tiempo, podemos usar un manejador para atender diferentes eventos, así que crearemos otro método que primero comprobará los valores de los campos de entrada para el tamaño de la ventana y el paso, y luego llamaremos al método para recalcular el tamaño de la capa neuronal considerando el tipo de capa neuronal seleccionado.

bool CNetCreatorPanel::OnChangeWindowStep(void)
  {
   if(!OnEndEdit(m_edWindow) || !OnEndEdit(m_edStep))
      return false;
   return SetCounts((uint)m_cbNewNeuronType.Value());
  }

Ahora, todo lo que deberemos hacer es completar nuestro mapa de manejadores de eventos. Esto nos permitirá iniciar el manejador de eventos correcto en el momento adecuado.

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)
ON_EVENT(ON_CHANGE, m_lstNewModel, OnChangeListNewModel)
ON_EVENT(ON_CHANGE, m_cbNewNeuronType, OnChangeNeuronType)
ON_EVENT(ON_END_EDIT, m_edWindow, OnChangeWindowStep)
ON_EVENT(ON_END_EDIT, m_edStep, OnChangeWindowStep)
ON_EVENT(ON_END_EDIT, m_edProbability, OnEndEditProbability)
ON_EVENT_CONTROL(ON_END_EDIT, m_edCount, OnEndEdit)
ON_EVENT_CONTROL(ON_END_EDIT, m_edWindowOut, OnEndEdit)
ON_EVENT_CONTROL(ON_END_EDIT, m_edLayers, OnEndEdit)
ON_EVENT_CONTROL(ON_END_EDIT, m_edBatch, OnEndEdit)
EVENT_MAP_END(CAppDialog)


3. Añadiendo el procesamiento de eventos de teclado

Ya hemos hecho un buen trabajo para que nuestra herramienta de Transfer Learning sea mucho más fácil de usar, pero todas nuestras mejoras se relacionan con la interfaz y facilitan su uso con la ayuda del ratón o de forma táctil, si nuestra computadora es compatible con esta función. No obstante, no hemos implementado en absoluto la posibilidad de utilizar un teclado para trabajar con nuestra herramienta. Por ejemplo, podría resultar cómodo utilizar las flechas hacia arriba y hacia abajo para cambiar el número de capas neuronales a copiar, o bien, al pulsar la tecla Delete del teclado, activar un método para eliminar la capa neuronal seleccionada del modelo a crear.

No vamos a profundizar demasiado en el tema por el momento. Solo mostraremos cómo añadir el procesamiento de teclas a nuestros manejadores de eventos existentes en solo unas pocas líneas de código.

Como habrá notado, las tres funcionalidades anteriores ya están implementadas en el código de nuestra herramienta, y se ejecutan al producirse un determinado evento. Para eliminar la capa neuronal seleccionada, hemos creado un botón independiente en el panel de la interfaz; el cambio del número de capas neuronales copiadas se realiza pulsando los botones del objeto CSpinEdit.

Desde el punto de vista técnico, pulsar los botones del teclado es un evento tan importante como pulsar los botones del ratón o mover este. La pulsación también es procesada por la función OnChartEvent de nuestro programa, lo cual significa que llamará al método ChartEvent de nuestra clase.

Cuando se produce un evento de pulsación de una tecla, obtendremos el identificador de evento CHARTEVENT_KEYDOWN y la variable lparam almacenará el identificador de la tecla pulsada.

Usando esta propiedad, podremos jugar con el teclado y determinar los identificadores de todas las teclas que nos interesen. Por ejemplo, aquí tenemos los códigos de las teclas mencionadas.

#define KEY_UP                               38
#define KEY_DOWN                             40
#define KEY_DELETE                           46

Ahora pasaremos al método ChartEvent de nuestra clase. Como recordará, en él hemos llamado al mismo método de la clase padre. Ahora añadiremos el identificador del evento y una comprobación de visibilidad para nuestra herramienta. Solo ejecutaremos el manejador de eventos cuando la interfaz de nuestra herramienta resulte visible. Al fin y al cabo, el usuario necesitará poder ver lo que ocurre en el panel y controlar visualmente el proceso.

Si hemos superado la primera etapa de la comprobación, verificaremos el código de la tecla pulsada. Si la tecla correspondiente existe en nuestra lista, generaremos un evento personalizado que se corresponderá con una acción similar en el panel de nuestra interfaz.

Por ejemplo, al pulsar la tecla Delete del teclado, generaremos el evento DELETE en el panel de nuestra interfaz, 

void CNetCreatorPanel::ChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   CAppDialog::ChartEvent(id, lparam, dparam, sparam);
   if(id == CHARTEVENT_KEYDOWN && m_spPTModelLayers.IsVisible())
     {
      switch((int)lparam)
        {
         case KEY_UP:
            EventChartCustom(CONTROLS_SELF_MESSAGE, ON_CLICK, m_spPTModelLayers.Id() + 2, 0.0, m_spPTModelLayers.Name() + "Inc");
            break;
         case KEY_DOWN:
            EventChartCustom(CONTROLS_SELF_MESSAGE, ON_CLICK, m_spPTModelLayers.Id() + 3, 0.0, m_spPTModelLayers.Name() + "Dec");
            break;
         case KEY_DELETE:
            EventChartCustom(CONTROLS_SELF_MESSAGE, ON_CLICK, m_btDeleteLayer.Id(), 0.0, m_btDeleteLayer.Name());
            break;
        }
     }
  }

y después de ello, saldremos del método. A continuación, permitiremos que el programa procese el evento generado con los manejadores de eventos y los métodos existentes.

Obviamente, este enfoque solo será posible si disponemos de los manejadores correspondientes en el programa. No obstante lo dicho, nadie nos impide crear nuevos manejadores de eventos y generar eventos únicos para ellos.


Conclusión

En este artículo, hemos visto varias opciones para mejorar la usabilidad de la interfaz de usuario. Podrá comprobar la calidad de los enfoques utilizados poniendo a prueba la herramienta en el archivo adjunto al artículo. Espero que la herramienta propuesta le resulte útil. Agradecería al lector que compartiera sus impresiones y sugerencias respecto a cualquier mejora en el hilo del foro correspondiente.

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
  4. Redes neuronales: así de sencillo (Parte 23): Creamos una herramienta para el Transfer Learning

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 una 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/11306

Archivos adjuntos |
MQL5.zip (74.22 KB)
DoEasy. Elementos de control (Parte 15): Objeto WinForms TabControl - múltiples filas de encabezados de pestañas, métodos de trabajo con pestañas DoEasy. Elementos de control (Parte 15): Objeto WinForms TabControl - múltiples filas de encabezados de pestañas, métodos de trabajo con pestañas
En este artículo, continuaremos desarrollando el objeto WinForm TabControl: hoy crearemos la clase de objeto de pestaña, haremos posible la disposición de los encabezados de las pestañas en varias filas y añadiremos los métodos para trabajar con las pestañas del objeto.
Aprendiendo a diseñar un sistema de trading con Bears Power Index Aprendiendo a diseñar un sistema de trading con Bears Power Index
Bienvenidos a un nuevo artículo de la serie dedicada a la creación de sistemas comerciales basados en indicadores técnicos populares. En esta ocasión, hablaremos sobre el Bears Power Index y crearemos un sistema comercial basado en sus indicadores.
Aprendiendo a diseñar un sistema de trading con Bulls Power Aprendiendo a diseñar un sistema de trading con Bulls Power
Bienvenidos a un nuevo artículo de la serie dedicada a la creación de sistemas comerciales basados en indicadores técnicos populares. En esta ocasión, hablaremos sobre el índice de fuerza alcista Bulls Power y crearemos un sistema comercial basado en sus indicadores.
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.