Класс функции активации

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

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

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

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

Родительский класс всех функций активации CActivation я решил наследоваться от класса CObject — базового класса всех объектов в MQL5.

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

  • CActivation — конструктор класса;
  • ~CActivation — деструктор класса;
  • Init — передача параметров для расчета функции активации;
  • GetFunction — получение используемой функции активации и ее параметров;
  • Activation — вычисление значения функции активации по исходному значению;
  • Derivative — производная от функции активации;
  • SetOpenCL — запись указателя на объект работы с OpenCL;
  • Save и Load — виртуальные методы для работы с файлами;
  • Type — виртуальный метод идентификации класса.

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

CActivation::CActivation(void) : m_iRows(0),
                                 m_iCols(0),
                                 m_cOpenCL(NULL)
  {
   m_adParams = VECTOR::Ones(2);
   m_adParams[1] = 0;
  }

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

  • m_cInputs
  • m_cOutputs

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

CActivation::~CActivation(void
  {
   if(!!m_cInputs)
      delete m_cInputs;
  }

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

bool CActivation::Init(VECTOR &params)
  {
   m_adParams = params;
//---
   m_cInputs = new CBufferType();
   if(!m_cInputs)
      return false;
//---
   return true;
  }

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

ENUM_ACTIVATION_FUNCTION CActivation::GetFunction(VECTOR &params)
  {
   params = m_adParams;
   return GetFunction();
  }

Метод вычисления значений функции активации Activation в параметрах получает указатель на буфер результатов нейронного слоя. В данном буфере содержатся данные работы нейрона до функции активации. Нам необходимо активизировать полученные значения и перезаписать их в указанный буфер. Но мы же помним, что полученные значения могут понадобиться при вычислении производных некоторых функций. Поэтому мы осуществляем «жонглирование» указателями на объекты буферов, сохранив полученный указатель в переменной m_cInputs. В полученную в параметрах переменную и переменную m_cOutputs сохраним буфер из переменной m_cInputs. Текущий класс соответствует отсутствию функции активации, поэтому мы и не осуществляем никаких операций над полученными данными.

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

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

bool CActivation::Activation(CBufferType *&output)
  {
   if(!output || output.Total() <= 0)
      return false;
   m_cOutputs = m_cInputs;
   m_cInputs = output;
   output = m_cOutputs;
   if(GetFunction() == AF_NONE && output != m_cInputs)
     {
      delete output;
      output = m_cInputs;
     }
//---
   return true;
  }

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

Метод активизации функционала многопоточных вычислений SetOpenCL в параметрах получает указатель на  объект работы с контекстом OpenCL и размеры буфера результатов вызывающего нейронного слоя. Данные размеры нам понадобятся для инициализации и создания буфера в контексте.

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

bool CActivation::SetOpenCL(CMyOpenCL *openclconst ulong rowsconst ulong cols)
  {
   m_iRows = rows;
   m_iCols = cols;
   if(m_cOpenCL != opencl)
     {
      if(m_cOpenCL)
         delete m_cOpenCL;
      m_cOpenCL = opencl;
     }
//---
   if(!!m_cInputs)
     {
      if(!m_cInputs.BufferInit(m_iRowsm_iCols0))
         return false;
      m_cInputs.BufferCreate(m_cOpenCL);
     }//---
   return(!!m_cOpenCL);
  }

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

bool CActivation::Save(const int file_handle)
  {
   if(file_handle == INVALID_HANDLE)
      return false;
   if(FileWriteInteger(file_handleType()) <= 0 ||
      FileWriteInteger(file_handle, (int)GetFunction()) <= 0 ||
      FileWriteInteger(file_handle, (int)m_iRows) <= 0 ||
      FileWriteInteger(file_handle, (int)m_iCols) <= 0 ||
      FileWriteDouble(file_handle, (double)m_adParams[0]) <= 0 ||
      FileWriteDouble(file_handle, (double)m_adParams[1]) <= 0)
      return false;
//---
   return true;
  }

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

bool CActivation::Load(const int file_handle)
  {
   if(file_handle == INVALID_HANDLE)
      return false;
   m_iRows = (uint)FileReadInteger(file_handle);
   m_iCols = (uint)FileReadInteger(file_handle);
   m_adParams.Init(2);
   m_adParams[0] = (TYPE)FileReadDouble(file_handle);
   m_adParams[1] = (TYPE)FileReadDouble(file_handle);
//---
   if(!m_cInputs)
     {
      m_cInputs = new CBufferType();
      if(!m_cInputs)
         return false;
     }
   if(!m_cInputs.BufferInit(m_iRowsm_iCols0))
      return false;
//---
   return true;
  }

Мы рассмотрели все методы базового класса функции активации CActivation. В целом получили следующую структуру класса.

class CActivation : protected CObject
  {
protected:
   ulong             m_iRows;
   ulong             m_iCols;
   VECTOR            m_adParams;
   CMyOpenCL*        m_cOpenCL;
   //---
   CBufferType*      m_cInputs;
   CBufferType*      m_cOutputs;
 
public:
                     CActivation(void);
                    ~CActivation(void) {if(!!m_cInputsdelete m_cInputs; }
   //---
   virtual bool      Init(VECTOR &params);
   virtual ENUM_ACTIVATION_FUNCTION  GetFunction(VECTOR &params);
   virtual ENUM_ACTIVATION_FUNCTION   GetFunction(void) { return AF_NONE; }
   virtual bool      Activation(CBufferType*& output);
   virtual bool      Derivative(CBufferType*& gradient) { return true;    }
   //---
   virtual bool      SetOpenCL(CMyOpenCL *openclconst ulong rows
                                                  const ulong cols);

   //--- методы работы с файлами
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   //--- метод идентификации объекта
   virtual int       Type(void)             const { return defActivation; }
  };

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

class CActivationLine   :  public CActivation
  {
public:
                     CActivationLine(void) {};
                    ~CActivationLine(void) {};
   //---
   virtual ENUM_ACTIVATION_FUNCTION   GetFunction(voidoverride
                                              { return AF_LINEAR; }
   virtual bool      Activation(CBufferType*& outputoverride;
   virtual bool      Derivative(CBufferType*& gradientoverride;
  };

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

  • GetFunction — получение используемой функции активации и ее параметров;
  • Activation — вычисление значения функции активации по исходному значению;
  • Derivative — производная от функции активации.

В методе GetFunction мы лишь меняем тип возвращаемой функции активации на соответствующий данному классу.

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

bool CActivationLine::Activation(CBufferType*& output)
  {
   if(!CActivation::Activation(output))
      return false;
//---
   if(!m_cOpenCL)
     {
      if(!m_cInputs.m_mMatrix.Activation(output.m_mMatrixAF_LINEAR,
                                          m_adParams[0], m_adParams[1]))
         return false;
     }
   else // блок OpenCL
     {
      return false;
     }
//---
   return true;
  }

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

bool CActivationLine::Derivative(CBufferType*& gradient)
  {
   if(!m_cInputs || !m_cOutputs ||
      !gradient || gradient.Total() < m_cOutputs.Total())
      return false;
//---
   if(!m_cOpenCL)
     {
      gradient.m_mMatrix = gradient.m_mMatrix * m_adParams[0];
     }
   else // блок OpenCL
     {
      return false;
     }
//---
   return true;
  }

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

class CActivationLReLU : public CActivation
  {
public:
                     CActivationLReLU(void) { m_adParams[0] = (TYPE)0.3; };
                    ~CActivationLReLU(void) {};
   //---
   virtual ENUM_ACTIVATION_FUNCTION   GetFunction(voidoverride { return AF_LRELU; }
   virtual bool      Activation(CBufferType*& outputoverride;
   virtual bool      Derivative(CBufferType*& gradientoverride;
  };

В функции активации нового класса мы также воспользуемся вызовом матричной функции активации с указанием соответствующего типа функции AF_LRELU.

bool CActivationLReLU::Activation(CBufferType*& output)
  {
   if(!CActivation::Activation(output))
      return false;
//---
   if(!m_cOpenCL)
     {
      if(!m_cInputs.m_mMatrix.Activation(output.m_mMatrixAF_LRELU,m_adParams[0]))
         return false;
     }
   else // блок OpenCL
     {
      return false;
     }
//---
   return true;
  }

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

bool CActivationLReLU::Derivative(CBufferType*& gradient)
  {
   if(!m_cOutputs || !gradient ||
      m_cOutputs.Total() <= 0 || gradient.Total() < m_cOutputs.Total())
      return false;
//---
   if(!m_cOpenCL)
     {
      MATRIX temp;
      if(!m_cInputs.m_mMatrix.Derivative(tempAF_LRELU,m_adParams[0]))
         return false;
      gradient.m_mMatrix *= temp;
     }

   else // блок OpenCL
     {
      return false;
     }
//---
   return true;
  }

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