Нейросети — это просто (Часть 24): Совершенствуем инструмент для Transfer Learning

10 августа 2022, 13:52
Dmitriy Gizlyk
3
701

Содержание

Введение

В предыдущей статье данной серии был создан инструмент, позволяющий нам воспользоваться технологией Transfer Learning. В результате большой проделанной работы мы получили инструмент, который позволяет редактировать уже обученные моделям. При этом мы можем из предварительно обученной модели взять любое количество нейронных слоёв. Конечно, есть ограничивающие условия. Мы берем только последовательные слои начиная со слоя исходных данных. Причина такого подхода кроется в природе нейронных сетей. Они хорошо работают только с исходными, данными аналогичными используемым при обучении модели.

Кроме того, созданный инструмент позволяет не только редактировать обученные модели. Но и создавать полностью новые модели. Что позволяет отказаться от описания архитектуры модели в коде программы. Достаточно лишь создать модель с помощью инструмента. А затем обучать и использовать модель, загрузив созданную нейронную сеть из файла. Тем самым мы можем экспериментировать с различными архитектурами без изменения кода программы. Нам не потребуется даже перекомпилировать программу. Достаточно лишь заменить файл модели.

И конечно, столь полезный инструмент хочется сделать максимально удобным для пользователей. В этой статье мы и попробуем повысить комфорт его использования.


1. Вывод полной информации о нейронном слое

И начнем мы повышение юзабилити нашего инструмента с увеличения количества информации о каждом нейронном слое. Как вы помните, в прошлой статье мы собрали всю возможную информации об архитектуре каждого нейронного слоя обученной модели. Но пользователю мы показали только тип нейронного слоя и количество нейронов на выходе. С этим можно смириться, когда мы работаем с одной моделью и помним её архитектуру. Но когда вы будете экспериментировать с большим количеством моделей, такого количества информации явно будет недостаточно.

Но вместе с тем, больший объем информации требует и больше места на информационном табло. Добавление горизонтального скроллинга для окна вывода информации о модели, наверное, будет не лучшим решением. Поэтому я решил выводить информацию о каждом нейронном слое в несколько строк. И здесь важно, чтобы выводимая информация была легко читаема. А не слилась в один огромный тест трудный для восприятия. Разделить текст на блоки нам поможет вставка визуальных разделителей между описаниями двух последовательных нейронных слоёв.

Казалось бы, простое решение разделить текст на несколько строк в процессе реализации тоже потребовало нестандартных подходов. Дело в том, что для вывода информации об архитектуре модели мы используем класс списка CListView. В котором каждая строка представляет собой отдельный элемент списка. При этом отсутствует возможность как вывода одного элемента в несколько строк, так и группировки нескольких элементов в одну сущность. Добавление подобного функционала потребует внесение изменений в алгоритм и архитектуру класса. А на практике это выльется в создание нового класса управляющего объекта. В таком варианте можно наследоваться от класса CListView или создать полностью новый элемент. Как вы понимаете, это довольно объемная работа и она не входила в мои планы.

Поэтому я решил использовать уже существующий класс, но с небольшими ухищрениями без внесения изменений в сам код класса. Как уже было сказано выше, для визуального разделения на блоки отдельных нейронных слоёв будут использоваться элементы-разделители. Именно они позволят нам разделить общий массив текста описания архитектуры модели на отдельные блоки нейронных слоёв. И тем самым визуально сгруппируют информацию по каждому нейронному слою.

Но помимо визуальной группировки нам нужно еще и понимание на уровне программы к какому нейронному слою относится тот или иной элемент в списке. Как вы помните, в прошлой статье мы реализовали изменения числа копируемых нейронных слоёв по выделению мышью отдельного нейронного слоя обученной модели и удаление выделенного слоя из списка добавляемых в новую модель нейронных слоев. И в том и в другом случае нам необходимо четкое понимание соответствия выделенного элемента и конкретного нейронного слоя.

И тут давайте вспомним, что при добавлении каждого элемента в список мы указывали его текст и некое числовое значение. Именно это числовое значение, как правило, используется для быстрой идентификации выделенного элемента. Ранее мы здесь указывали некое индивидуальное значение для каждого элемента. Но нам никто не запрещает использовать одно значение для нескольких элементов. Конечно, такой подход затруднит идентификацию каждого элемента списка. А в данный момент нам этого и не надо. Нам достаточно идентифицировать нашу группу элементов. Следовательно, с помощью данного функционала мы можем идентифицировать не отдельный элемент, а целую группу элементов.

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

На практике такое решение дало нам ещё один плюс. Класс CListView имеет метод SelectByValue. Основной функционал данного метода — выделение элемента по его числовому значению. Его алгоритм построен таким образом, что среди всех элементов списка он находит первый элемент с указанным числовым значением и выделяет его. То есть, организовав обработку события изменения выделения списка, мы можем прочитать значение выделенного пользователем элемента и попросить класс выделить первый элемент из списка с данным значением. Тем самым визуализируем для пользователя начало интересующей группы. По-моему, это довольно удобный функционал.

bool  SelectByValue( 
   const long  value     // value 
   )

А теперь предлагаю посмотреть на практическую реализацию описанных выше подходов. И прежде всего, нам необходимо реализовать текстовое представление описания архитектуры нейронного слоя для его вывода на панель. Для этого мы создадим метод LayerDescriptionToString. Данный метод в параметрах получает указатель на объект описания архитектуры нейронного слоя и указатель на динамический массив строк, в который мы запишем текстовое описание нейронного слоя. При этом каждый элемент массива представляет собой отдельную строку в списке описания архитектуры модели. То есть, если обратиться к используемой выше терминологии, то каждый такой массив представляет собой отдельную группу элементов в списке для описания одного нейронного слоя. Использование динамического массива позволяет нам организовывать группы элементов различного размера. В зависимости от необходимости для описания того или иного нейронного слоя.

Возвращать наш метод будет количество элементов в массиве.

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

В теле метода мы сначала проверяем действительность полученного указателя на описание архитектуры нейронного слоя.

Далее мы подготовим локальную переменную и очистим полученный динамический массив.

   string temp;
   ArrayFree(result);

И далее мы будем создавать текстовое описание нейронного слоя в зависимости от его типа. Надо сказать, что мы не будем сразу работать с динамическим массивом строк. Вместо этого мы все описание запишем в одну строку. При этом в местах разделения строк вставим символ-разделитель. В приведенном примере я использовал обратный слеш "\". А для удобного составления текста с заданной разметкой я воспользовался функцией StringFormat. Которая позволяет получить отформатированный текст с минимальными трудозатратами.

После составления форматированного строкового описания архитектуры нейронного слоя мы воспользуемся функцией StringSplit и разобьем наш текст на строки. Данная функция делит текст на строки в местах присутствия указанного элемента-разделителя. Который мы аккуратно добавили в текст на предыдущем этапе. Удобство использования данной функции еще и в том, что она увеличивает размер динамического массива до необходимых размеров. А значит нам не придется это контролировать.

   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;
     }

После составления описания для всех известных нейронных слоёв не забудем добавить одно стандартное описание для неизвестных типов. Тем самым мы сообщим пользователю об обнаружении неизвестного нейронного слоя и обезопасим себя не преднамеренного нарушения целостности модели.

В завершении метода мы вернем вызывающей программе размер полученного массива результатов.

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

Далее мы переходим к методу LoadModel, с которым мы уже познакомились в предыдущей статье. Мы не будем менять весь метод, а лишь изменим тело цикла добавления элементов в список. Как и ранее, в теле цикла мы сначала получаем из динамического массива указатель на объект описания очередного нейронного слоя. И сразу проверяем действительность полученного указателя.

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

Затем мы подготовим динамический массив строк и вызовем вышеописанный метод генерации текстового описания нейронного слоя LayerDescriptionToString. После завершения работы метода мы получим массив строковых описаний и количество элементов в нем. В случае возникновения ошибки метод вернет пустой массив и "-1" вместо размера массива. О возникновении ошибки мы сообщим пользователю и завершим работу метода.

      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;
        }

При удачном составлении текстового описания нейронного слоя мы сначала добавим элемент разделителя блоков. А затем во вложенном цикле выведем все содержание текстового массива-описания нейронного слоя.

      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;
     }

Здесь надо обратить внимание, что при указании идентификатора группы мы прибавляем 1 к порядковому номеру нейронного слоя в динамическом массиве описания архитектуры модели. Это необходимое действие, так как индексация элементов в массиве начинается с "0". А при указании "0" в качестве числового идентификатора класс CListView автоматически заменит его на общее количество элементов в списке. А нам бы не хотелось получить случайное значение вместо идентификатора группы.

Далее код метода LoadModel остался без изменений. А с его полным кодом можно ознакомиться во вложении. Там же можно найти коды всех методов и классов, используемых в описываемой программе. В частности, вы сможете увидеть аналогичные дополнения в методе вывода описания новой модели ChangeNumberOfLayers.

Обратите внимание, что в методе ChangeNumberOfLayers информация о создаваемой модели собирается из 2-х динамических массивов описания архитектуры моделей. Первый описывает архитектуру модели-донора. Из неё мы берем описание копируемых нейронных слоёв. А второй массив содержит описание добавляемых нейронных сетей.

После вывода описания архитектуры моделей мы обращаемся к методам обработки событий изменения состояния созданных списков.

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

Как было описано выше, при выделении пользователем любой строки в списке мы будем перемещать выделение к первой строке указанного блока. Для этого мы просто получим идентификатор группы выделенного пользователем элемента и скажем программе выделить первый элемент с полученным идентификатором. Эта операция выполняется методом SelectByValue.

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

Таким образом мы расширили объем отображаемой информации об архитектуре модели. При этом мы сделали объем выводимой информации минимально-достаточным и зависимым от типа нейронного слоя. То есть пользователю выводится только информация релевантная для описания архитектуры каждого конкретного нейронного слоя. А лишняя информация не загромождает информационное окно.


2. Активация используемых/деактивация не используемых полей ввода

Следующая наша доработка будет касаться полей ввода данных. Как бы ни казалось странным, но здесь довольно большое поле для фантазии. Наверное, первое, что бросается в глаза это объем вводимой информации. На панели мы создали поля ввода для всех элементов класса описания архитектуры нейронного слоя CLayerDescription. Не могу сказать, что это плохо. Так пользователь видит все указанные данные и может их изменять в любом порядке и любое количество раз, прежде чем добавить слой. Но ведь мы с вами знаем, что не все эти поля релевантные для всех нейронных слоёв.

К примеру, для указания параметров полносвязного нейронного слоя достаточно указать только 3 из них: количество нейронов, функция активации и метод оптимизации параметров. Остальные параметры для него не релевантные. В то же время, для свёрточного нейронного слоя необходимо указать размер окна исходных данных и его шага. А количество элементов на выходе нейронного слоя будет зависеть от размера буфера исходных данных и двух указанных выше параметров.

А в рекуррентном блоке LSTM функции активации определены архитектурой блока и нет необходимости их указывать. 

Конечно, пользователь может знать все подобные нюансы. Но хорошо продуманный инструмент должен предостеречь пользователя от возможных "механических" ошибок. И здесь есть 2 превентивных варианта. Мы можем убрать не релевантные элементы с панели или просто сделать их недоступными для редактирования.

Каждый вариант имеет свои плюсы и минусы. К достоинствам первого варианта можно отнести снижение количества полей ввода на панели. А значит панель можно сделать более компактной. Недостатком же будет более сложная реализация. Так как нам нужно будет каждый раз перемещать элементы по панели. В то же время постоянное перемещение объектов может запутать пользователя и привести к ошибкам в процессе эксплуатации.

На мой взгляд, использование этого метода оправдано при необходимости ввода большого количества данных. Тогда удаление лишних объектов позволит сделать панель более компактной и избежать излишнего нагромождения элементов.

Второй вариант приемлем для небольшого количества элементов. Когда мы можем без труда расположить сразу все элементы на панели и это удобно в использовании. При этом мы не запутываем пользователя излишним их перемещением по панели. В результате, после нескольких итераций пользователь зрительно запоминает их расположении и, практически, машинально может с ними работать. Что в целом повышает производительность.

Мы уже расположили все поля ввода на панели интерфейса нашего инструмента. Поэтому я считаю приемлемым второй вариант реализации.

Что ж, есть первое архитектурное решение. Но мы пойдем немного дальше. На нашей панели реализованы как поля с выпадающим списком, так и поле непосредственного ввода значений. И если в после с выпадающим списком пользователь может выбрать только один из возможных вариантов. То в поля непосредственного ввода значения физически пользователь может ввести любой текст.

Мы же ожидаем там получить целочисленное значение. И вполне логично добавить проверку введенной информации до передачи её в объект описания архитектуры создаваемого нейронного слоя. А чтобы пользователь мог воочию оценить корректность восприятия информации нашим инструментом мы будем осуществлять проверку корректности вводимой информации сразу после ввода текста пользователем. И после проверки в поле ввода заменим информацию, введенную пользователем на воспринятую инструментом. Таким образом, пользователь сможет увидеть разницу межу введенной и считанной информацией. И, при необходимости, совершить корректирующие действия.

И ещё один момент. При описании архитектуры нейронного слоя в классе CLayerDescription у нас есть элементы двойного назначения. Так элемент step для свёрточного и подвыборочного слоёв указывает на шаг окна исходных данных. При этом он же используется для указания количества голов внимания при описании нейронных слоёв внимания.

Аналогично параметр window_out указывает на количество фильтров в свёрточном слое и размер внутреннего слоя ключей в блоке внимания.

Чтобы сделать наш интерфейс более дружелюбным к пользователю правильно сделать изменение текстовых меток при выборе соответствующего типа нейронного слоя.

Здесь не надо путать с описанной выше проблемой перестановки элементов в окне интерфейса. В данном случае поле ввода остаётся неизменным. Меняется лишь информационное сообщение рядом с окном. Если пользователь не обратит внимание на изменение надписи и машинально введет данные в соответствующее поле, то это не приведет к каким-либо ошибкам в организации модели. Так как информация в любом случае попадет в нужный элемент описания архитектуры создаваемого нейронного слоя.

Для реализации вышеописанных решений нам необходимо сделать шаг назад и провести некоторую подготовительную работу.

Прежде всего, при создании текстовых меток на панели интерфейса нашего инструмента мы не сохраняли указатели на соответствующие объекты. И теперь, когда нам нужно будет изменить текст некоторых из них нам придется их искать в общем массиве объектов. Чтобы этого не делать мы вернемся к методу создания текстовой метки CreateLabel и по завершении операций метода вместо логического результата вернем указатель на созданный объект.

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;
  }

Конечно, мы не будем сохранять указатели на все метки. Мы сохраним лишь 2 интересующих нас объекта. Для этого мы объявим две дополнительных переменных. Несмотря на то, что используются динамические указатели на объекты, мы не будем их добавлять в деструктор класса нашего инструмента. Данные объекты, как и раньше, будут удаляться в массиве всех объектов инструмента. Но при этом мы получим прямой доступ к нужным нам объектам.

   CLabel*           m_lbWindowOut;
   CLabel*           m_lbStepHeads;

Запись указателей в новые переменные мы будем осуществлять в методе Create нашего класса. Куда мы внесем небольшие изменения, приведенные ниже. В остальном код метода остался без изменений. С полным кодом метода можно ознакомиться во вложении.

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;
  }

Следующим этапом нашей подготовительной работы мы создадим метод для изменения статуса поля ввода. Здесь надо сказать, что стандартный класс CEdit уже имеет метод ReadOnly для изменения статуса объекта. Но данным методом не предусмотрена визуализация статуса объекта. Он лишь блокирует возможность ввода данных. А мы бы хотели сделать визуально разделимыми объекты доступные для ввода данных и нет. Мы здесь не будем изобретать чего-то нового. И выделим объекты цветом фона. Для редактируемых полей мы сделаем белый цвет фона объекта. А для неизменяемых объектов сделаем фон аналогичный цвету панели.

Данный функционал будет выполняться в методе EditReedOnly. В параметрах метода мы будем передавать указатель на объект и новый флаг статуса. В теле метода мы передадим полученный флаг в метод ReadOnly объекта ввода и установим фон объекта в соответствии с указанным флагом.

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;
  }

Теперь давайте обратим внимание на функции активации. А точнее на выпадающий список доступных функций активации. В данном вопросе надо обратить внимание, что не все для всех типов нейронных слоёв актуален данный выпадающий список. Так как в некоторых архитектурах четко прописан тип функции активации и не изменяется данным списком. Примером тому можно назвать LSTM-блок, подвыборочный слой, блоки внимания. Однако классом CComboBox не предусмотрено метода, который бы блокировал функционал класса каким-либо образом. Поэтому мы воспользуемся обходным путем и будем изменять список доступных функций активации в каждом конкретном случае. Для этого мы создадим отдельные методы наполнения списка доступных функций активации.

На самом деле, таких методов будет только два. Один, так сказать общий, с указанием функций активации ActivationListMain. И второй — "пустой" ActivationListEmpty, в котором доступен только один выбор "None".

Для понимания алгоритма построения методов рассмотрим код метода ActivationListMain. В начале метода мы полностью очистим существующий список элементов доступных функций активации. А затем наполним список элементов в цикле с использованием метода ItemAdd и функции EnumToString.

Здесь надо обратить внимание, что кодирование элементов в перечислении функций активации начинается с "-1" для None. И следующая в перечислении функции гиперболического тангенса TANH имеет индекс "0". А это не совсем хорошо по причине, указанной выше при описании наполнения списка описания архитектуры нейронной сети. Ведь выпадающий список представляет собой уже известный нам класс CListView. Поэтому, как и ранее для исключения нулевого значения идентификатора списка мы просто добавим небольшую константу к идентификатору перечисления.

После заполнения списка доступных функций активации мы установим значение по умолчанию и выйдем из метода.

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;
  }

И ещё один метод, который нам потребуется, поможет нам немного автоматизировать работу пользователя. Выше мы уже упоминали, что в случае свёрточных моделей или блоков внимания количество элементов на выходе модели является зависимым от размера окна анализируемых исходных данных и шага его перемещения. И тут, для исключения возможных ошибок и снижения ручного труда пользователя я решил закрыть поле ввода значения количества блоков и заполнять его отдельным методом SetCounts.

В параметрах данного метода мы передаем тип создаваемого нейронного слоя. Возвращать метод будет логический результат выполнения операций.

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

А в теле метода мы сначала определим количество элементов на выходе предыдущего слоя. И тут надо понимать, что предыдущий слой у нас может находиться в одном из двух динамических массивов: описания архитектуры модели донора или описания архитектуры добавления новых нейронных слоев. Определить откуда нам нужно брать последний нейронный слой довольно просто. Добавлять мы всегда будем нейронный слой в конец списка. Следовательно, из модели-донора мы будем брать слой только в том случае, если массив новых нейронных слоёв пуст. И, следуя этой логике, мы проверяем размер динамического массива новых нейронных слоев. И в зависимости от его размера запросим у соответствующего массива указатель на предыдущий нейронный слой.

   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;

Далее мы посчитаем количество элементов в буфере результатов предыдущего слоя в соответствии с его типом. Если вдруг размер буфера не больше "0" мы выходим из метода с результатом 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;

Затем мы считаем с интерфейса значения размера окна анализируемых исходных данных и его шага. А также подготовим переменную для записи результата расчета.

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

Непосредственно расчет количества элементов мы будем осуществлять в зависимости от типа создаваемого нейронного слоя. Для расчета количества элементов свёрточного и подвыборочного слоёв нам потребуются размер анализируемого окна исходных данных и его шага.

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

При использовании блоков внимания размер шага равен размеру окна. И по правилам математики формула немного сокращается.

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

А в случае использования латентного слоя вариационного автоэнкодера размер слоя будет ровно в 2 раза меньше предыдущего.

      case defNeuronVAEOCL:
         counts = outputs / 2;
         break;

Для всех прочих случаев мы установим размер нейронного слоя равным размеру предыдущего слоя. Этим можно воспользоваться при объявлении слоя пакетной нормализации или Dropout.

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

Полученное значение мы перенесем в соответствующий элемент интерфейса.

Теперь у нас есть достаточно средств, чтобы организовать изменения интерфейса в зависимости от выбора создаваемого типа нейронного слоя. Так давайте посмотрим, как можно это сделать. Данный функционал реализован в методе OnChangeNeuronType. Название метода не случайно. Ведь вызывать мы его будем каждый раз при изменении пользователем типа добавляемого нейронного слоя.

Указанный метод не содержит параметров и возвращает логический результат выполнения операций. В теле метода мы сначала определяем тип создаваемого нейронного слоя, который выбрал пользователь.

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

Далее наш алгоритм разветвляется в зависимости от выбранного типа нейронного слоя. Алгоритм для каждого нейронного слоя будет похож. Но практически каждый нейронный слой имеет свои нюансы. Для полносвязного нейронного слоя мы оставляем активным только одно поле ввода числа нейронов и загружаем полный список возможных функция активации.

   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;

В случае свёрточного слоя активными будут 3 других поля ввода. Размер анализируемого окна исходных данных, его шаг. А также размер окна результатов (количество фильтров). При этом мы актуализируем значение двух текстовых меток и запустим пересчет количества элементов нейронного слоя в зависимости от размера окна исходных данных и его шага. Надо сказать, что мы считаем число элементов для одного фильтра. Поэтому результат расчета не зависит от количества используемых фильтров.

      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;

Для подвыборочного слоя мы не указываем количество фильтров и функцию активации. В нашей реализации мы всегда используем максимальное значение в качестве функции активации подвыборочного слоя. Поэтому мы очищаем список доступных функций активации. Но, как и в случае сверточного слоя, запускаем расчет количества элементов создаваемого слоя.

      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;

При объявлении LSTM-блока также не используется список функций активации, и мы его очищаем. При этом для ввода данных открыто только поле количества элементов в нейронном слое.

      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;

Для инициализации слоя Dropout достаточно указать только значения вероятности выброса нейронов. Функция активации не используется. А количество элементов рано размеру предыдущего нейронного слоя.

      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;

Аналогичный подход при объявлении слоя пакетной нормализации. Только в данном случае мы указываем размер пакета.

      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;

В зависимости от метода внимания мы делаем активными поля ввода количества голов внимания и нейронных слоев в блоке. При этом изменяем текстовые метки к соответствующим полям ввода.

      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;

А для латентного слоя вариационного автоэнкодера и вовсе не нужно вводить данных. Достаточно лишь выбрать тип слоя и добавить его в модель.

      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;

Если же указанный в параметрах тип нейронного слоя не найден, то завершаем метод с результатом false.

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

При успешном выполнении всех операций метода мы выходим из него с положительным результатом.

Теперь нам остается лишь организовать запуск описанного метода в нужный момент. Для этого мы воспользуемся событием изменением значения элемента выбора типа нейронного слоя и добавим соответствующий обработчик событий.

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)

Реализацией вышеописанных методов мы организовали включение и отключение полей ввода в зависимости от типа выбранного нейронного слоя. Но выше мы еще обсуждали контроль ввода данных.

При внимательном рассмотрении можно заметить, что во всех полях ввода данных мы ожидаем получить целые числа строго больше ноля. Исключение составляет лишь значение вероятности выброса элементов в слое Dropout. Здесь допускается вещественное значение в диапазоне от 0 до 1. Следовательно, для проверки введенных данных нам потребуется 2 метода. Один для вероятности и второй для всех остальных элементов.

Алгоритм обоих методов довольно прост. Сначала мы считываем текстовое значение, введенное пользователем. Преобразуем в числовое значение. Проверяем на соответствие диапазону допустимых значений. И выводим полученное значение обратно в соответствующее окно интерфейса. Контроль за корректностью трактовки данных оставляем за пользователем.

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));
  }

Следует обратить внимание, что, если при проверке корректности введения значения вероятности мы четко идентифицируем поле ввода. То для идентификации объекта во втором методе мы будем передавать указатель на соответствующий объект в параметрах метода. И тут кроется очередная задача. В предложенных макросах обработки событий отсутствует подходящий макрос, который бы передавал указатель на вызвавший событие объект в метод обработки события. Поэтому нам пришлось добавить такой макрос.

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

И тут надо вспомнить, что среди проверяемых полей ввода могут быть размер анализируемого окна исходных данных и его шаг. А как вы помните, от этих параметров будет зависеть количество элементов в нейронном слое. А значит при изменении их значений нам нужно будет пересчитать размер создаваемого нейронного слоя. Только вот используемая нами модель обработки событий позволяет только один обработчик для каждого события. В то же время мы можем использовать один обработчик для обслуживания разных событий. Поэтому мы создадим еще один метод, который сначала проверит значения в полях ввода размера окна и его шага. А затем вызовет метод пересчета размера нейронного слоя с учетом выбранного типа нейронного слоя.

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

Теперь нам остаётся лишь дополнить нашу карту обработчика событий. Что позволит запустить нужный обработчик события в нужное время.

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. Добавляем обработку событий клавиатуры

Мы довольно хорошо потрудились и сделали наш инструмент для Transfer Learning намного удобнее и более дружелюбным для пользователя. Но все наши улучшения касались интерфейса и облегчили его использования с помощью мыши или сенсорного ввода. Если ваш компьютер поддерживает такую возможность. Но мы абсолютно не реализовали возможность использования клавиатуры для работы с нашим инструментом. К примеру, может быть удобно использовать стрелки вверх и вниз для изменения числа копируемых нейронных слоев. Или нажатие клавиши Delete на клавиатуре вызовет метод удаления выделенного нейронного слоя из создаваемой модели.

Я не буду сейчас сильно глубоко погружаться в эту тему. Лишь покажу каким образом всего лишь в несколько строк кода добавить обработку клавиш уже существующими обработчиками событий.

Как вы могли заметить все три предложенных выше функционала уже реализованы в коде нашего инструмента. И выполняются при наступлении определенного события. Для удаления выделенного нейронного слоя мы сделали отдельную кнопку на панели интерфейса. А изменение числа копируемых нейронных слоёв осуществляется путем нажатия кнопок объекта CSpinEdit.

С технической стороны, нажатие кнопок клавиатуры является таким же событием, как и нажатие кнопок мыши или её перемещение. Оно также обрабатывается функцией OnChartEvent вашей программы. А значит вызывает метод ChartEvent нашего класса.

При наступлении события нажатия клавиши клавиатуры мы получим идентификатор события CHARTEVENT_KEYDOWN и в переменной lparam будет храниться идентификатор нажатой клавиши.

Воспользовавшись этим свойством, мы можем поиграться с клавиатурой и определить идентификаторы всех интересующих нас клавиш. К примеру, вот коды упомянутых выше клавиш.

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

Теперь мы перейдем к методу ChartEvent нашего класса. Как вы помните в нем мы вызывали аналогичный метод родительского класса. Сейчас мы добавим в него проверку идентификатора события и видимости нашего инструмента. Запускать обработчик событий мы будем только при видимости интерфейса нашего инструмента. Ведь пользователь должен видеть, что происходит на панели, и визуально контролировать процесс.

Если мы прошли первый этап проверки, то проверяем код нажатой клавиши. При наличии соответствующей клавиши в нашем списке мы генерируем пользовательское событие, которое соответствуют аналогичному действию на панели нашего интерфейса.

К примеру, при нажатии на клавиатуре клавиши Delete мы генерируем событие нажатия кнопки DELETE на панели нашего интерфейса. 

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;
        }
     }
  }

И после этого выходим из метода. Далее мы позволяем программе обработать сгенерированное событие уже существующими обработчиками событий и методами.

Конечно, такой подход возможен только при наличии соответствующих обработчиков в программе. Тем не менее, нам никто не мешает создать новые обработчики событий и генерировать для них уникальные события.


Заключение

В данной статье мы рассмотрели различные варианты повышения юзабилити пользовательского интерфейса. Качество работы используемых подходов вы можете проверить при тестировании инструмента во вложении к статье. Я же надеюсь, что вы найдете полезным для себя предложенный инструмент. И буду признателен, если в соответствующей ветке форума вы поделитесь своим впечатлением и пожеланиями по совершенствованию инструмента.

Ссылки

  1. Нейросети — это просто (Часть 20): Автоэнкодеры
  2. Нейросети — это просто (Часть 21): Вариационные автоэнкодеры (VAE)
  3. Нейросети — это просто (Часть 22): Обучение без учителя рекуррентных моделей
  4. Нейросети — это просто (Часть 23): Создаём инструмент для Transfer Learning

Программы, используемые в статье

# Имя Тип Описание
1 NetCreator.mq5 Советник   Инструмент создания моделей
2 NetCreatotPanel.mqh Библиотека класса Библиотека класса для создания инструмента
3 NeuroNet.mqh Библиотека класса Библиотека классов для создания нейронной сети
4 NeuroNet.cl Библиотека Библиотека кода программы OpenCL

Прикрепленные файлы |
MQL5.zip (74.22 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (3)
Dmitry Iglakov
Dmitry Iglakov | 23 сен 2022 в 19:26
Нейросети -  это просто (часть 500988939928177231827361823461827631827361827361827361827361284762834762834762). Когда вы прочтёте последнюю часть, то вам скорее всего будет уже 89 лет и нейросети уже будут не актуальны.
А если серьёзно, то "нейросети - это просто" это когда максимум две статьи такого объема. А не когда мне кажется, что мт5 завис и мне уже год приходят уведомления о статье  "нейросети - это просто" 
Dmitriy Gizlyk
Dmitriy Gizlyk | 23 сен 2022 в 19:48
Dmitry Iglakov #:
Нейросети -  это просто (часть 500988939928177231827361823461827631827361827361827361827361284762834762834762). Когда вы прочтёте последнюю часть, то вам скорее всего будет уже 89 лет и нейросети уже будут не актуальны.
А если серьёзно, то "нейросети - это просто" это когда максимум две статьи такого объема. А не когда мне кажется, что мт5 завис и мне уже год приходят уведомления о статье  "нейросети - это просто" 

Идея "Нейросети - это просто" показать доступность технологии каждому. Да, серия получилась довольно длинная. Но практическое использование читателю доступно уже со второй статьи. И каждая отдельная статья рассказывает о новых возможностях. И их возможно включить в свои разработки сразу после прочтения статьи. Использовать их или нет, личное дело каждого читателя. При этом нет необходимости дожидаться следующей статьи. А по поводу объемности темы могу сказать, что наука развивается и с каждым днем появляются новые алгоритмы. Вполне возможно, что их применение может дать плоды и в трейдинге.

Ivan Butko
Ivan Butko | 23 сен 2022 в 22:45
НЛО прилетело и опубликовало эту статью. 

И так можно сказать про каждую из цикла. Чем больше погружаешься в тему, тем больше понимаешь, какая глубина и ценность таких статей. Новичкам тут главное не бросать это дело, когда в статье не видишь красивых графиков роста баланса. Автор плавно подводит к этому. Хотя, рабочий экземпляр прикреплён к нескольким статьям, бери-пользуйся. 

Дмитрию спасибо
Популяционные алгоритмы оптимизации Популяционные алгоритмы оптимизации
Вводная статья об алгоритмах оптимизации (АО). Классификация. В статье предпринята попытка создать тестовый стенд (набор функций), который послужит в дальнейшем для сравнения АО между собой, и, даже, возможно, выявления самого универсального алгоритма из всех широко известных.
Машинное обучение и Data Science (Часть 04): Предсказание биржевого краха Машинное обучение и Data Science (Часть 04): Предсказание биржевого краха
В этой статье я попытаюсь использовать нашу логистическую модель, чтобы спрогнозировать крах фондового рынка на основе главнейших акций для экономики США: NETFLIX и APPLE. Мы проанализируем эти акции, будем использовать информацию о предыдущих падениях рынка 2019 и 2020 годов. Посмотрим, как наша модель будет работать в нынешних мрачных условиях.
DoEasy. Элементы управления (Часть 15): WinForms-объект TabControl — несколько рядов заголовков вкладок, методы работы с вкладками DoEasy. Элементы управления (Часть 15): WinForms-объект TabControl — несколько рядов заголовков вкладок, методы работы с вкладками
В статье продолжим работу над WinForm-объектом TabControl — создадим класс объекта-поля вкладки, сделаем возможность расположения заголовков вкладок в несколько рядов и добавим методы для работы с вкладками объекта.
Машинное обучение и Data Science (Часть 03): Матричная регрессия Машинное обучение и Data Science (Часть 03): Матричная регрессия
В этот раз мы будем создавать модели с помощью матриц — они дают большую гибкость и позволяют создавать мощные модели, которые могут обрабатывать не только пять независимых переменных, но и множество других, насколько позволяют пределы вычислительных возможностей компьютера. Статья будет очень интересной, это точно.