5.Методы работы с файлами

Мы уже далеко продвинулись в работе над реализацией алгоритма Multi-Head Self-Attention. В предыдущих разделах мы уже реализовали стандартными средствами MQL5 операции прямого и обратного прохода нашего класса CNeuronMHAttention. Теперь для возможности полноценного его использования в наших моделях необходимо дополнить его методами работы с файлами. Как бы вам ни казалось, корректная работа этих методов для промышленного использования не менее важна корректной работы методов прямого и обратного проходов.

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

Вначале посмотрим на структуру нашего класса многоголового внимания CNeuronMHAttention.

class CNeuronMHAttention    :  public CNeuronAttention
  {
protected:
   CNeuronConv       m_cW0;
   int               m_iHeads;
 
public:
                     CNeuronMHAttention(void);
                    ~CNeuronMHAttention(void);
   //---
   virtual bool      Init(const CLayerDescription *descoverride;
   virtual bool      SetOpenCL(CMyOpenCL *opencloverride;
   virtual bool      FeedForward(CNeuronBase *prevLayeroverride;
   virtual bool      CalcHiddenGradient(CNeuronBase *prevLayeroverride;
   virtual bool      CalcDeltaWeights(CNeuronBase *prevLayerbool readoverride;
   virtual bool      UpdateWeights(int batch_sizeTYPE learningRate,
                                   VECTOR &BetaVECTOR &Lambdaoverride;
   //--- методы работы с файлами
   virtual bool      Save(const int file_handleoverride;
   virtual bool      Load(const int file_handleoverride;
   //--- метод идентификации объекта
   virtual int       Type(voidoverride const { return(defNeuronMHAttention);  }
  };

На первый взгляд нет ничего сложного. В теле класса объявляется лишь один сверточный слой m_cW0 и одна переменная m_iHeads, указывающая на количество используемых голов внимания. Основное количество объектов наследуется от родительского класса CNeuronAttention. Мы уже создали аналогичный метод при работе над родительским классом, а сейчас можем им воспользоваться. Я советую еще раз заглянуть в метод родительского класса CNeuronAttention::Save и убедиться, что в нем есть сохранение всех необходимых нам данных. Только после этого можно приступать к работе над методом сохранения данных текущего класса. На этот раз здесь все действительно очень просто.

В параметрах метод CNeuronMHAttention::Save получает хендл файла для записи данных. В теле метода мы сразу передаем полученный хендл в аналогичный метод родительского класса, в котором уже реализованы все контроли. Помимо контролей в методе родительского класса реализовано сохранение унаследованных объектов и их данных. Таким образом, проверяя результат работы метода родительского класса, мы сразу получаем консолидированный результат прохождения блока контролей и сохранения унаследованных объектов. Нам остается лишь сохранить количество используемых голов внимания и данные сверточного слоя m_cW0.

bool CNeuronMHAttention::Save(const int file_handle)
  {
//--- вызов метода родительского класса
   if(!CNeuronAttention::Save(file_handle))
      return false;
//--- сохраняем константы
   if(FileWriteInteger(file_handlem_iHeads) <= 0)
      return false;
//--- вызываем аналогичный метод для всех внутренних слоев
   if(!m_cW0.Save(file_handle))
      return false;
//---
   return true;
  }

Метод загрузки данных CNeuronMHAttention::Load строится по правилу загрузки данных из файла в строгом соответствии с последовательностью их записи. Поэтому в теле метода полученный в параметрах хендл файла мы сразу передаем в аналогичный метод родительского класса и проверяем результат его работы.

bool CNeuronMHAttention::Load(const int file_handle)
  {
//--- вызов метода родительского класса
   if(!CNeuronAttention::Load(file_handle))
      return false;

После выполнения операций метода родительского класса мы считываем из файла количество используемых голов внимание и данные внутреннего сверточного слоя m_cW0. С загрузкой константы все очень просто: мы просто читаем значение из файла и сохраняем в нашу переменную m_iHeads. Но перед обращением к методу загрузки мы должны проверить тип загружаемого объекта. Только при совпадении типов объектов мы вызываем метод загрузки данных и проверяем результат выполнения операций.

   m_iHeads = FileReadInteger(file_handle);
   if(CheckPointer(m_cW0) == POINTER_INVALID)
     {
      m_cW0 = new CNeuronConv();
      if(CheckPointer(m_cW0) == POINTER_INVALID)
         return false;
     }
   if(FileReadInteger(file_handle)!=defNeuronConv ||
      !m_cW0.Load(file_handle))
      return false;

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

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

bool CNeuronAttention::Load(const int file_handle)
  {
  ......
   m_iUnits = FileReadInteger(file_handle);
  ......
   if(!m_cScores.BufferInit(m_iUnitsm_iUnits0))
      return false;
  ......
//---
   return true;
  }

В них инициализируется буфер матрицы коэффициентов зависимости m_cScores. Как можно заметить, инициализация проходит нулевыми значениями в размере достаточном только для одной головы внимания. А это не соответствует требованиям нашего алгоритма Multi-Head Self-Attention. Логично будет добавить в метод загрузки нашего класса повторную инициализацию буфера с приданием ему нужного размера.

//--- инициализируем Scores
   if(!m_cScores.BufferInit(m_iHeadsm_iUnits * m_iUnits))
      return false;
//---
   return true;
  }

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

На этом мы завершаем работу стандартными средствами MQL5 над классом CNeuronMHAttention, в котором мы реализовали алгоритм Multi-Head Self-Attention. В следующем разделе мы дополним его функционал возможностью совершения многопоточных операций с помощью технологии OpenCL.