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

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

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

В первую очередь, нужно сохранить внутренние нейронные слои, содержащие матрицы весовых коэффициентов m_cQuerys, m_cKeys, m_cValues, m_cFF1 и m_cFF2. Кроме того, нам нужно сохранить значения переменных, определяющих архитектуру нейронного слоя: m_iWindow, m_iUnits и m_iKeysSize.

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

Внутренний слой m_cAttentionOut не содержит матрицы весовых коэффициентов, и его данные, так же как и данные буфера m_cScores, перезаписываются на каждой итерации прямого и обратного прохода. Но давайте посмотрим на ситуацию с другой стороны. Вспомним процедуру инициализации нейронного слоя:

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

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

class CNeuronAttention    :  public CNeuronBase
  {
protected:
   CNeuronConv       m_cQuerys;
   CNeuronConv       m_cKeys;
   CNeuronConv       m_cValues;
   CBufferType       m_cScores;
   int               m_cScoreGrad;
   int               m_cScoreTemp;
   CNeuronBase       m_cAttentionOut;
   CNeuronConv       m_cFF1;
   CNeuronConv       m_cFF2;
   //---
   int               m_iWindow;
   int               m_iUnits;
   int               m_iKeysSize;
   CBufferType       m_cStd;
   //---
   virtual bool      NormlizeBuffer(CBufferType *bufferCBufferType *std,
                                                                uint std_shift);
   virtual bool      NormlizeBufferGradient(CBufferType *output,
                       CBufferType *gradientCBufferType *stduint std_shift);
 
public:
                     CNeuronAttention(void);
                    ~CNeuronAttention(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 *prevLayeroverride;
   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(defNeuronAttention); }
  };

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

bool CNeuronAttention::Save(const int file_handle)
  {
   if(!CNeuronBase::Save(file_handle))
      return false;

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

   if(!m_cQuerys.Save(file_handle))
      return false;
   if(!m_cKeys.Save(file_handle))
      return false;
   if(!m_cValues.Save(file_handle))
      return false;
   if(!m_cAttentionOut.Save(file_handle))
      return false;
   if(!m_cFF1.Save(file_handle))
      return false;
   if(!m_cFF2.Save(file_handle))
      return false;

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

   if(FileWriteInteger(file_handlem_iUnits) <= 0)
      return false;
   if(FileWriteInteger(file_handlem_iWindow) <= 0)
      return false;
   if(FileWriteInteger(file_handlem_iKeysSize) <= 0)
      return false;
//---
   return true;
  }

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

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

bool CNeuronAttention::Load(const int file_handle)
  {
   if(!CNeuronBase::Load(file_handle))
      return false;

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

   if(FileReadInteger(file_handle) != defNeuronConv || !m_cQuerys.Load(file_handle))
      return false;

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

   if(FileReadInteger(file_handle) != defNeuronConv || !m_cKeys.Load(file_handle))
      return false;
   if(FileReadInteger(file_handle) != defNeuronConv || !m_cValues.Load(file_handle))
      return false;
   if(FileReadInteger(file_handle) != defNeuronBase ||
      !m_cAttentionOut.Load(file_handle))
      return false;
   if(FileReadInteger(file_handle) != defNeuronConv || !m_cFF1.Load(file_handle))
      return false;
   if(FileReadInteger(file_handle) != defNeuronConv || !m_cFF2.Load(file_handle))
      return false;

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

   m_iUnits = FileReadInteger(file_handle);
   m_iWindow = FileReadInteger(file_handle);
   m_iKeysSize = FileReadInteger(file_handle);

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

   if(!m_cScores.BufferInit(m_iUnitsm_iUnits0))
      return false;

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

Мы, конечно, добавим подмену буферов, но предварительно проверим соответствие указателей.

   if(m_cFF2.GetOutputs() != m_cOutputs)
     {
      if(m_cOutputs)
         delete m_cOutputs;
      m_cOutputs = m_cFF2.GetOutputs();
     }

   if(m_cFF2.GetGradients() != m_cGradients)
     {
      if(m_cGradients)
         delete m_cGradients;
      m_cGradients = m_cFF2.GetGradients();
     }
//---
   SetOpenCL(m_cOpenCL);
//---
   return true;
  }

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

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