Построение класса пакетной нормализации средствами MQL5

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

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

class CNeuronBatchNorm    :  public CNeuronBase
  {
protected:
   CBufferType       m_cBatchOptions;
   uint              m_iBatchSize;       // размер пакета
 
public:
                     CNeuronBatchNorm(void);
                    ~CNeuronBatchNorm(void);
   //---
   virtual bool      Init(const CLayerDescriptiondescriptionoverride;
   virtual bool      SetOpenCL(CMyOpenCL *opencloverride;
   virtual bool      FeedForward(CNeuronBaseprevLayeroverride;
   virtual bool      CalcHiddenGradient(CNeuronBaseprevLayeroverride;
   virtual bool      CalcDeltaWeights(CNeuronBaseprevLayerbool readoverride;
   //--- методы работы с файлами
   virtual bool      Save(const int file_handleoverride;
   virtual bool      Load(const int file_handleoverride;
   //--- метод идентификации объекта
   virtual int       Type(void)  override   const { return(defNeuronBatchNorm); }
  };

Переопределять мы будем все тот же набор основных методов:

  • Init — метод инициализации экземпляра класса;
  • FeedForward — метод прямого прохода;
  • CalcHiddenGradient — метод распределения градиентов ошибки через скрытый слой;
  • CalcDeltaWeights — метод распределения градиентов ошибки до матрицы весовых коэффициентов;
  • Save — метод сохранения параметров нейронного слоя;
  • Load — метод восстановления работоспособности нейронного слоя из сохраненных данных.

Начнем работу над классом с его конструктора. В данном методе мы лишь задаем начальное значение для размера пакета нормализации. Деструктор класса остается пустым.

CNeuronBatchNorm::CNeuronBatchNorm(void)  :  m_iBatchSize(1)
  {
  }

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

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

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

При расчете нормируемой величины используются только среднее значение и среднеквадратичное отклонение, которые рассчитываются по выборке и не имеют настраиваемые параметры. У нас появляются только два настраиваемых параметра в процессе сдвига и масштабирования значения γ и β. Оба параметра подбираются индивидуально для каждого значения из тензора исходных данных.

А теперь давайте вспомним математическую формулу нейрона со смещением.

Не кажется ли вам, что при N = 1 формулы примут идентичный вид? Этим сходством мы и воспользуемся.

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

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

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

bool CNeuronBatchNorm::Init(const CLayerDescription *description)
  {
   if(!description ||
      description.window != description.count)
      return false;
   CLayerDescription *temp = new CLayerDescription();
   if(!temp || !temp.Copy(description))
      return false;
   temp.window = 1;
   if(!CNeuronBase::Init(temp))
      return false;
   delete temp;

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

//--- инициализируем буфер обучаемых параметров
   if(!m_cWeights.m_mMatrix.Fill(0))
      return false;
   if(!m_cWeights.m_mMatrix.Col(VECTOR::Ones(description.count), 0))
      return false;

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

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

  1. μ — среднее значение от предыдущих итераций прямого прохода.
  2. σ2 — дисперсия выборки за предыдущие итерации прямого прохода.
  3. — нормализованная величина до масштабирования и сдвига.

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

//--- инициализируем буфер параметров нормализации
   if(!m_cBatchOptions.BufferInit(description.count30))
      return false;
   if(!m_cBatchOptions.Col(VECTOR::Ones(description.count), 1))
      return false;

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

   m_iBatchSize = description.batch;
//---
   return true;
  }

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