English 中文 Español Deutsch 日本語 Português
preview
Нейросети — это просто (Часть 23): Создаём инструмент для Transfer Learning

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

MetaTrader 5Торговые системы | 3 августа 2022, 17:17
1 352 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Содержание


Введение

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


1. Зачем нужен Transfer Learning

Что же такое Transfer Learning и зачем он нужен? В общем понятии Transfer Learning — это метод машинного обучения, при котором знания модели, обученной для решения одних задач, повторно используются в качестве базы для решения новых задач. Разумеется, для решения новых задач предварительно осуществляется дообучение модели на новых данных. И в общем случае, при правильно подобранной модели-доноре, дообучение осуществляется гораздо быстрее и с лучшими результатами, чем обучение аналогичной модели с "чистого листа".

При этом модель-донор может быть использована как полностью, так и частично.

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

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

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

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

Вы, конечно, можете найти и другие области применения технологии. А я предлагаю перейти к рассмотрению инструмента для её применения.


2. Создание инструмента

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

Второй момент, что наши нейронные сети хорошо работаю только с теми данными, на которых обучались. На абсолютно новых данных мы можем получить непредсказуемый результат. Это касается и отдельных нейронных слоёв. Поэтому для Transfer Learning мы можем использовать только идущие подряд нейронные слои, начиная со слоя исходных данных. Нельзя выдернуть какой-то блок из середины или конца модели. То есть, мы можем взять модель-донор полностью или несколько её первых слоёв. Добавить к ней несколько различных нейронных слоёв и сохранить новую модель.

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

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


2.1 Дизайн

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

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

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

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

Дизайн инструмента

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


2.2 Реализация пользовательского интерфейса 

Определившись с дизайном нашего инструмента, мы можем приступать к его реализации. Для этого мы создадим новый класс CNetCreatorPanel наследником базового класса диалоговых приложение CAppDialog.

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

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

  • m_edPTModel — элемент для ввода имя файла предварительно обученной модели;
  • m_edPTModelLayers — элемент отображения общего количества нейронных слоёв в предварительно обученной модели;
  • m_spPTModelLayers — количество нейронных слоёв, копируемых в новую модель;
  • m_lstPTMode — элемент отображения архитектуры предварительно обученной модели.
class CNetCreatorPanel : protected CAppDialog
  {
protected:
   //--- pre-trained model
   CEdit             m_edPTModel;
   CEdit             m_edPTModelLayers;
   CSpinEdit         m_spPTModelLayers;
   CListView         m_lstPTModel;
   CNetModify        m_Model;   
   CArrayObj*        m_arPTModelDescription;

Здесь же мы объявим объекты для работы с предварительно обученной моделью:

  • m_Model — объект самой предварительно обученной модели;
  • m_arPTModelDescription — динамический массив описания архитектуры предварительно обученной модели.

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

И второй момент. Для объявления объекта предварительно обученной модели мы воспользовались классом CNetModify. В то время как для моделей нейронной сети ранее мы создавали класс CNet. Это связано с тем, что нам потребуется дополнительный функционал от нашей нейронной сети. И для его реализации мы создадим новый класс CNetModify наследником нашего класса CNet. Но к этому мы вернемся при описании функционала нашего инструмента.

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

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

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

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

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

Ещё мы переопределили 3 метода родительского класса. Но этого можно было избежать при публичном наследовании.

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

Благодаря использованию статических объектов конструктор и деструктор нашего класса практически пустые.

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

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

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

В параметрах метода мы будем передавать идентификатор объекта, текст метки и её координаты на панели.

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

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

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

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

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

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

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

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

И в завершении метода мы добавим указатель на наш объект в коллекцию объектов нашего интерфейса.

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

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

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

На этом мы завершаем подготовительную работу и переходим непосредственно к созданию нашего пользовательского интерфейса. Этот функционал выполняется в методе 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;

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

Следуя выше указанной логике, мы начнем создание объектов группы работы с предварительно обученной моделью. И первой мы создадим метку группы. Для этого мы определим координаты метки и вызовем ранее созданный метод CreateLabel. В данный метод мы передадим текст метки и её координаты. И, конечно, не забудем добавить уникальный идентификатор метки.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Я не буду сейчас приводить идентичный код для всех 9 элементов. Ниже для примера представлен код создания 2-х строк из нашей таблицы. С полным кодом вы можете познакомиться во вложении.

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

После создания элементов для описания архитектуры добавляемого нейронного слоя мы добавим 2 кнопки: для добавления и удаления нейронного слоя. Кнопки мы расположим в один ряд, разделив между ними пополам ширину блока.

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

И мы переходим к 3-му заключительному блоку описания полной архитектуры создаваемой модели. Здесь можно найти все используемые выше приёмы.

После создания всех элементов мы выходим из метода с результатом true. А с полным кодом метода можно познакомиться во вложении.

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


2.3 Наполнение инструмента функционалом

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

  1. Вначале нам необходимо открыть файл с сохраненной обученной моделью. Для этого пользователь мышкой нажимаем на объект для выбора файла. При этом открывается диалоговое окно, в котором пользователь выбирает существующий файл с заданным расширением.
  2. После выбора файла пользователем, наш инструмент должен загрузить модель из указанного файла и вывести информацию о загруженной модели (тип и количество нейронных слоев, количество нейронов в каждом слое).
  3. Вместе с выводом информации о загруженной модели по умолчанию все её нейронные слои устанавливаются к копированию в новую модель. При этом информация о них копируется в блок описания создаваемой модели.
  4. У пользователя должна быть возможность вручную поменять количество копируемых нейронных слоёв. Синхронно с изменением числа копируемых нейронных слоёв должны быть внесены изменения в архитектуру создаваемой модели. Что отразится в блоке описания архитектуры создаваемой модели.
  5. После выбора числа копируемых нейронных слоёв пользователь может вручную указать тип и архитектуру нового нейронного слоя и добавить его в создаваемую модель путем нажатия кнопки "ADD LAYER".
  6. Если какой-то нейронный слой был добавлен в модель ошибочно, пользователь может выбрать такой нейронный слой в блоке описания архитектуры создаваемой модели и удалить его путем нажатия кнопки "DELETE". Здесь надо обратить внимание, что удаление возможно только добавленных нейронных слоёв. Для удаления слоев модели-донора нужно будет воспользоваться инструментом изменения числа копируемых нейронных слоев.
  7. После создания архитектуры создаваемой нейронной сети пользователь нажимает кнопку "SAVE MODEL". Перед ним открывается диалоговое окно, в котором можно выбрать существующий или указать имя нового файла.

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

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

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

Я же предлагаю немного остановиться на методе получения информации об используемых нейронных сетях GetLayersDiscriptions. В результате выполнения этого метода ожидается получить динамический массив описания архитектуры нейронной сети, аналогичный описанию модели, передаваемому в параметрах метода конструктора модели. Сложность организации данного процесса заключается ещё и в том, что ранее мы не создавали методов для получения гиперпараметров нейронных слоёв. Следовательно, нам нужно добавить соответствующий метод и в состав классов нейронных слоев. Для начала мы добавим такой метод GetLayerInfo в базовый класс нейронных слоёв CNeuronBaseOCL.

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

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

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

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

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

Во вложении вы можете найти аналогичные методы для всех ранее рассмотренных типов нейронных слоёв.

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

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

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

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

На данном этапе мы реализовали возможность получения описания архитектуры ранее созданной модели. И можем перейти к реализации метод загрузки предварительно обученной модели из указанного пользователем файла. Для реализации указанного функционала мы создадим метод CNetCreatorPanel::LoadModel. В параметрах метод получит имя файла для загрузки модели.

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

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

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

Удалим описание ранее загруженной модели, если таковое имеется. И вызовем метод сбора информации об архитектуре загруженной модели.

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

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

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

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

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

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

Создадим еще один метод OpenPreTrainedModel. В теле данного метода мы лишь организуем вызов стандартной функции FileSelectDialog, в которой уже реализован интерфейс диалогового окна работы с файлами. При вызове данной функции мы лишь укажем необходимые расширения файлов и флаг FSD_FILE_MUST_EXIST, который указывает на возможность указания только существующего файла.

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

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

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

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

Для этого мы воспользуемся средством обработки событий. В наследниках класса CAppDialog этот механизм реализован посредствам макроподстановок. Для этого в коде программы создается блок макросов, который начинается макросом EVENT_MAP_BEGIN и завершается макросом EVENT_MAP_END. Между ними размещаются ряд макросов, соответствующих различным событиям. В нашем случае мы будем использовать макрос ON_EVENT, который предполагает обработку события по числовому идентификатору. В частности, для обработки события нажатия клавиши мышки на объекте названия файла для загрузки модели в теле макроса мы укажем событие ON_CLICK, указатель объекта m_edPTModel и имя метода, который необходимо вызвать при наступлении события OpenPreTrainedModel. Таким образом, при нажатии клавиши мыши на объекте m_edPTModel, который соответствует полю для ввода имени файла загрузки модели, программа вызовет метод OpenPreTrainedModel и тем самым запустит цепочку созданных выше методов загрузки предварительно обученной модели.

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

Аналогичным образом мы опишем другие события и вызываемые при этом методы:

  • OnClickAddButton — метод обработки нажатия кнопки "ADD LAYER";
  • OnClickDeleteButton  — метод обработки нажатия кнопки "DELETE";
  • OnClickSaveButton  — метод обработки нажатия кнопки "SAVE MODEL";
  • ChangeNumberOfLayers — метод обработки события изменения количества копируемых нейронных слоёв;
  • OnChangeListPTModel — метод обработки события нажатия кнопки мыши на нейронном слое в списке описания архитектуры загруженной модели.

С кодом всех указанных методов можно ознакомиться во вложении. Я предлагаю остановиться на методе сохранения новой модели. Так как его реализация довольно сложна и требует создания дополнительных методов в классе модели нейронной сети CNetModify.

Алгоритм данного метода можно условно разделить на 3 блока:

  • копирование нейронных слоёв из предварительно обученной модели;
  • добавление в модель новых нейронных слоёв;
  • сохранение модели в файл.

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

Будем двигаться по пунктам. И сначала создадим механизм копирования нейронных слоев. Мы прекрасно знаем, что в зависимости от архитектуры нейронного слоя он может содержать различное количество объектов. Вместе с тем нам нужен универсальный алгоритм, позволяющий копировать все типы нейронных слоёв с различными методами оптимизации параметров. При этом копирование обученной модели предполагает перенос не только архитектуры, но и всех весовых коэффициентов. И тут в голову приходит разумный вопрос зачем нам копировать полностью все элементы каждого нейронного слоя? Что нам мешает просто скопировать указатель на необходимый объект нейронного слоя? Как вы знаете, использование указателей позволяет обращаться к одному и тому же объектов из разных участков программного кода. Именно этим свойством мы и воспользуемся. Создадим два метода. Один будет возвращать указатель на объект нейронного слоя по его номеру в структуре модели. А второй будет добавлять указатель на объект нейронного слоя в архитектуру модели.

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

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

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

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

Вначале мы добавим в класс CNeuronBaseOCL метод, который будет корректировать матрицу весовых коэффициентов в соответствии с указанным количеством нейронов в последующем слое CNeuronBaseOCL::numOutputs.

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

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

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

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

После создания матрицы весовых коэффициентов создадим буферы данных, используемых в процессе оптимизации весовых коэффициентов.

Если же нет необходимости в матрице весовых коэффициентов и сопутствующих буферов, то удалим их за ненадобностью. И выходим из метода.

С полным кодом метода можно познакомиться во вложении.

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

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

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

Если же нам предстоит добавлять нейронные слои в существующую модель, то сначала мы объявим локальные переменные.

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

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

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

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

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

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

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

Далее мы создадим новый экземпляр класса нейронной сети и проверим результат выполнения операции.

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

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

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

После завершения итераций копирования нейронных слоёв мы вызываем вышеописанный метод добавления нейронных слоёв, чем и завершаем создание новой модели.

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

После этого нам остаётся лишь сохранить созданную модель.

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

После сохранения модели мы можем удалить её, так как обучение будет осуществляться уже в другой программе.

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

На этом мы завершаем работу с нашим классом и переходим к тестированию проделанной работы.


3. Тестирование

Для тестирования созданного инструмента мы создадим советник NetCreator.mq5. Код советника довольно прост и содержит лишь подключение выше созданного класса CNetCreatorPanel. По существу, интеграция класса в советник осуществляется в 3-х точках. Инициализация и запуск работы модели в функции OnInit. Уничтожение класса в функции OnDeinit. И передача событий в класс в методе OnChartEvent. Код всех точек интеграции приведен ниже.

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

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


Заключение

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

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


Ссылки

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

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

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


Прикрепленные файлы |
MQL5.zip (71.47 KB)
DoEasy. Элементы управления (Часть 14): Новый алгоритм именования графических элементов. Продолжаем работу над WinForms-объектом TabControl DoEasy. Элементы управления (Часть 14): Новый алгоритм именования графических элементов. Продолжаем работу над WinForms-объектом TabControl
В статье создадим новый алгоритм именования всех графических элементов для построения пользовательской графики и продолжим разработку WinForms-объекта TabControl.
Разработка торговой системы на основе индикатора объемов Volumes Разработка торговой системы на основе индикатора объемов Volumes
Представляю вашему вниманию новую статью из серии, в которой мы учимся создавать торговые системы на основе популярных технических индикаторов. Данная статья будет посвящена индикатору Volumes. Объем как понятие является важным факторов в торговле на финансовых рынках, и поэтому обязательно надо его учитывать. В этой статье узнаем, как разработать торговую систему на основе показателей от индикатора объемов Volumes.
Машинное обучение и Data Science (Часть 03): Матричная регрессия Машинное обучение и Data Science (Часть 03): Матричная регрессия
В этот раз мы будем создавать модели с помощью матриц — они дают большую гибкость и позволяют создавать мощные модели, которые могут обрабатывать не только пять независимых переменных, но и множество других, насколько позволяют пределы вычислительных возможностей компьютера. Статья будет очень интересной, это точно.
Разработка торговой системы на основе индикатора MFI Разработка торговой системы на основе индикатора MFI
Это новая статья из серии, в которой мы учимся создавать торговые системы на основе популярных технических индикаторов. На этот раз она посвящена Индексу денежного потока MFI. Мы подробно изучим этот индикатор и разработаем простые торговые системы на MQL5 для исполнения в MetaTrader 5.