5.Метод прямого прохода Multi-Head Self-Attention

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

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

bool CNeuronMHAttention::FeedForward(CNeuronBase *prevLayer)
  {
//--- проверяем актуальность всех объектов
   if(!prevLayer || !prevLayer.GetOutputs() ||
      !m_cAttentionOut.GetOutputs())
      return false;

После успешного прохождения блока контролей мы формируем конкатенированные тензоры запросов Query, ключей Key и значений Value. Для этого мы вызываем методы прямого прохода внутренних сверточных слоев m_cQuerys, m_cKeys и m_cValues. Созвучность тензоров в архитектуре Multi-Head Self-Attention и вызываемых объектов неслучайна: это делает код более читабельным и позволяет отслеживать выстраиваемый алгоритм.

   if(!m_cQuerys.FeedForward(prevLayer))
      return false;
   if(!m_cKeys.FeedForward(prevLayer))
      return false;
   if(!m_cValues.FeedForward(prevLayer))
      return false;

Обязательно контролируем процесс выполнения операций.

Далее, согласно алгоритму Multi-Head Self-Attention, нам предстоит определить коэффициенты зависимости между элементами последовательности и вывести конкатенированный результат работы всех голов внимания. Данный функционал является связующим между внутренними нейронными слоями. Он не покрывается использование других объектов и будет полностью выстраиваться внутри данного метода.

Как вы помните, при построении всех процессов в методах классов нашей библиотеки мы создаем две ветки алгоритма: стандартными средствами MQL5 и многопоточные вычисления на GPU с использованием технологии OpenCL. Как всегда, в данном разделе мы рассмотрим реализацию алгоритма стандартными средствами MQL5. А к реализации многопоточных вычислений с использованием технологии OpenCL мы вернемся позже.

И тут перед нами встает вопрос, как организовать работу. У нас есть три измерения:

  • головы внимания,
  • элементы последовательности,
  • вектор описания одного элемента последовательности.

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

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

//--- разветвление алгоритма по вычислительному устройству
   MATRIX out;
   if(!m_cOpenCL)
     {
      if(!out.Init(m_iHeadsm_iUnits * m_iKeysSize))
         return false;
      MATRIX querys[], keys[], values[];
      if(!m_cQuerys.GetOutputs().m_mMatrix.Vsplit(m_iHeadsquerys))
         return false;
      if(!m_cKeys.GetOutputs().m_mMatrix.Vsplit(m_iHeadskeys))
         return false;
      if(!m_cValues.GetOutputs().m_mMatrix.Vsplit(m_iHeadsvalues))
         return false;

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

Напомню, алгоритм Multi-Head Self-Attention предусматривает деление коэффициентов зависимости на квадратный корень из размерности вектора ключа Key и последующую нормализацию полученных значений функцией Softmax в разрезе элементов запросов Query.

Следуя алгоритму, мы перемножаем матрицы querys и транспонированную keys, делим на квадратный корень их размерности и сразу вычисляем экспоненциальное значение. В полученной матрице берем построчные суммы значений и организуем вложенный цикл нормализации данных.

      for(int head = 0head < m_iHeadshead++)
        {
         //--- определяем Scores
         MATRIX sc = exp(querys[head].MatMul(keys[head].Transpose()) /
                                                                sqrt(m_iKeysSize));
         VECTOR sum = sc.Sum(1);
         for(uint r = 0r < sc.Rows(); r++)
            if(!sc.Row(sc.Row(r) / sum[r], r))
               return false;

Как можно заметить, алгоритм полностью повторяет аналогичные операции родительского класса.

Теперь, когда у нас уже есть посчитанная матрица коэффициентов зависимостей между элементами, мы можем двигаться дальше по алгоритму Multi-Head Self-Attention и определить значения конкатенированного тензора результатов работы в части анализируемой головы внимания. Для этого нам достаточно вычислить произведения двух матриц — коэффициентов зависимости и значений Values.

         //--- выход блока внимания
         MATRIX temp = sc.MatMul(values[head]).Transpose();

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

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

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

         if(!temp.Reshape(1m_iUnits * m_iKeysSize))
            return false;
         if(!sc.Reshape(1m_iUnits * m_iUnits))
            return false;
         if(!m_cScores.m_mMatrix.Row(sc.Row(0), head))
            return false;
         if(!out.Row(temp.Row(0), head))
            return false;
        }

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

      if(!out.Reshape(m_iHeads * m_iKeysSizem_iUnits))
         return false;
      m_cAttentionOut.GetOutputs().m_mMatrix = out.Transpose();
     }
   else // Блок OpenCL
     {
      return false;
     }

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

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

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

   if(!m_cW0.FeedForward(GetPointer(m_cAttentionOut)))
      return false;

После успешного завершения операций метода в буфере результатов нашего нейронного слоя будет результат работы блока Multi-Head Self-Attention. Согласно алгоритму трансформера нам предстоит сложить полученный результат с исходными данными в единый тензор и нормализовать полученный результат по формулам:

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

//--- суммируем с исходными данными и нормализуем
   if(!m_cW0.GetOutputs().SumArray(prevLayer.GetOutputs()))
      return false;
   if(!NormlizeBuffer(m_cW0.GetOutputs(), GetPointer(m_cStd), 0))
      return false;

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

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

На этом завершается блок Multi-Head Self-Attention в алгоритме энкодера трансформера. Далее идет второй его блок — Feed Forward. В рамках этого блока нам предстоит провести сигнал через два нейронных слоя, что мы и сделаем, последовательно вызвав методы прямого прохода каждого нейронного слоя.

//--- FeedForward
   if(!m_cFF1.FeedForward(GetPointer(m_cW0)))
      return false;
   if(!m_cFF2.FeedForward(GetPointer(m_cFF1)))
      return false;

В заключение алгоритма прямого прохода нам предстоит повторить процедуру нормализации данных. Но теперь мы складываем буферы результатов блоков Multi-Head Self-Attention и Feed Forward.

//--- суммируем с выходом внимания и нормализуем
   if(!m_cOutputs.SumArray(m_cW0.GetOutputs()))
      return false;
   if(!NormlizeBuffer(m_cOutputsGetPointer(m_cStd), 1))
      return false;
//---
   return true;
  }

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