6.Метод прямого прохода пакетной нормализации

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

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

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

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

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

bool CNeuronBatchNorm::FeedForward(CNeuronBase *prevLayer)
  {
//--- блок контролей
   if(!prevLayer || !prevLayer.GetOutputs() || !m_cOutputs ||
      !m_cWeights || !m_cActivation)
      return false;

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

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

//--- проверка размера пакета нормализации
   if(m_iBatchSize <= 1)
     {
      m_cOutputs.m_mMatrix = prevLayer.GetOutputs().m_mMatrix;
      if(m_cOpenCL && !m_cOutputs.BufferWrite())
         return false;
      if(!m_cActivation.Activation(m_cOutputs))
         return false;
      return true;
     }

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

//--- разветвление алгоритма по вычислительному устройству
   if(!m_cOpenCL)
     {

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

      MATRIX inputs = prevLayer.GetOutputs().m_mMatrix;
      if(!inputs.Reshape(1prevLayer.Total()))
         return false;

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

      VECTOR mean = (m_cBatchOptions.Col(0) * ((TYPE)m_iBatchSize - 1.0) + 
                     inputs.Row(0)) / (TYPE)m_iBatchSize;

После определения скользящей средней находим среднюю дисперсию.

      VECTOR delt = inputs.Row(0) - mean;
      VECTOR variance = (m_cBatchOptions.Col(1) * ((TYPE)m_iBatchSize - 1.0) +
                         MathPow(delt2)) / (TYPE)m_iBatchSize;

Когда значения средней и дисперсии найдены, довольно легко вычислить нормализованное значение текущего элемента последовательности.

      VECTOR std = sqrt(variance) + 1e-32;
      VECTOR nx = delt / std;

Обратите внимание, что к дисперсии мы прибавляем небольшую константу для исключения ошибки деления на ноль.

Следующий шаг алгоритма пакетной нормализации — сдвиг и масштабирование.

      VECTOR res = m_cWeights.Col(0) * nx + m_cWeights.Col(1);

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

      if(!m_cOutputs.Row(res0) ||
         !m_cBatchOptions.Col(mean0) ||
         !m_cBatchOptions.Col(variance1) ||
         !m_cBatchOptions.Col(nx2))
         return false;
     }
   else  // Блок OpenCL
     {
      return false;
     }

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

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

   if(!m_cActivation.Activation(m_cOutputs))
      return false;
//---
   return true;
  }

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