English Deutsch 日本語
preview
Ансамблевые методы для улучшения численного прогнозирования в MQL5

Ансамблевые методы для улучшения численного прогнозирования в MQL5

MetaTrader 5Статистика и анализ |
290 1
Francis Dube
Francis Dube

Введение

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

Чтобы окончательно сформировать концепцию объединения прогнозов от моделей, давайте примем несколько основных обозначений. Рассмотрим обучающее множество, состоящее из K точек данных, каждая из которых представлена в виде пары (xi,yi), где xi – это прогнозный вектор, а yi – соответствующая скалярная выходная переменная, которую мы хотим спрогнозировать. Предположим у нас есть N обученных моделей, каждая из которых способна делать прогнозы. Когда модель представлена с прогнозным параметром x, модель n генерирует прогноз, обозначаемый как f_n (x). Наша цель состоит в том, чтобы построить консенсусную функцию f(x), которая эффективно комбинирует эти N отдельных прогнозов, давая более точный общий прогноз, чем любая отдельная модель.

Консенсусная функция

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


Ансамбли, основанные на усредненных прогнозах

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

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

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

неравенство Коши

Теперь рассмотрим вектор прогнозных параметров x, используемых для предсказания зависимой переменной y. Заменяя a в неравенстве на ошибки, допускаемые моделью при прогнозировании y в зависимости от x, получим a_n =  f_n(x) - y. Если слагаемые в левой части данного уравнения разделить, предположив, что f(x) является средним значением прогнозов, вынести за скобки N и перенести крайний правый член уравнения в левую часть неравенства Коши, а затем разделить обе его стороны на N^2, мы придем к фундаментальному уравнению, которое лежит в основе усреднения как ансамблевого метода:

Ансамбль усреднения

Суммы в правой части уравнения выше представляют собой квадратические ошибки отдельных моделей. Суммирование этих квадратов ошибок и деление на количество моделей-компонентов дает среднюю квадратическую ошибку (MSE) отдельных моделей. Тем временем, левая часть уравнения представляет собой квадратическую ошибку консенсусной модели, которая получена из среднего арифметического отдельных прогнозов.

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

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

Код, реализующий усреднение для ансамблей, представлен в классе CAvg, который определен в файле ensemble.mqh. Данный класс, как и все остальные классы, реализующие ансамблевые методы, полагается на предоставляемый пользователем набор заранее обученных моделей. Эти модели должны использовать интерфейс IModel, который определяется следующим образом:

//+------------------------------------------------------------------+
//| IModel interface defining methods for manipulation of learning   |
//| algorithms                                                       |
//+------------------------------------------------------------------+
interface IModel
  {
//train a model
   bool train(matrix &predictors,matrix&targets);
//make a prediction with a trained model
   double forecast(vector &predictors);
  };

В интерфейсе IModel содержатся два метода:

  • train() – данный метод содержит логику для обучения модели;
  • forecast() – данный метод определяет операции для составления прогнозов на основе новых входных данных.
//+------------------------------------------------------------------+
//| Compute the simple average of the predictions                    |
//+------------------------------------------------------------------+
class CAvg
  {

public:
                     CAvg(void) ;
                    ~CAvg(void) ;
   double            predict(vector &inputs, IModel* &models[]) ;

  } ;
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CAvg::CAvg(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CAvg::~CAvg(void)
  {
  }
//+------------------------------------------------------------------+
//|  Make a prediction by using consensus from multiple models       |
//+------------------------------------------------------------------+
double CAvg::predict(vector &inputs, IModel* &models[])
  {
   double output = 0.0 ;

   for(uint imodel=0 ; imodel<models.Size() ; imodel++)
     {
      output +=models[imodel].forecast(inputs) ;
     }

   output /= double(models.Size()) ;
   return output;
  }

Класс CAvg содержит метод predict(), который вызывается с вектором входных данных и массивом заранее обученных моделей-компонентов. Данный метод возвращает скалярное значение, представляющее собой консенсусный прогноз. В случае с классом CAvg консенсусный прогноз рассчитывается как среднее арифметическое прогнозов от предоставленных в виде массива моделей. Благодаря такому дизайну класс CAvg обеспечивает гибкость и модульность, что позволяет пользователям без проблем интегрировать различные типы моделей в свои ансамблевые методы.


Неограниченные линейные комбинации прогнозных моделей

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

Ансамбль на основе линейной регрессии

Данный ансамблевый метод реализован в классе CLinReg. Метод конструктора, метод деструктора и метод predict() имеют те же сигнатуры, что и методы класса CAvg, описанного ранее.

//+------------------------------------------------------------------+
//| Compute the linear regression of the predictions                 |
//+------------------------------------------------------------------+
class CLinReg
  {

public:

                     CLinReg(void) ;
                    ~CLinReg() ;
   bool              fit(matrix & train_vars, vector &train_targets,IModel* &models[]);

   double            predict(vector &inputs, IModel* &models[]) ;

private:
   OLS *m_linreg ;   // The linear regression object
  } ;
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CLinReg::CLinReg(void)
  {
   m_linreg = new OLS();
  }
//+------------------------------------------------------------------+
//| Fit the consensus model from saved models                        |
//+------------------------------------------------------------------+
bool CLinReg::fit(matrix &train_vars,vector &train_targets,IModel* &models[])
  {

   matrix independent(train_vars.Rows(),models.Size()+1);

   for(ulong i=0 ; i<independent.Rows() ; i++)     // Build the design matrix
     {
      independent[i][models.Size()] = 1.0;
      vector ins = train_vars.Row(i);
      for(uint imodel=0 ; imodel<models.Size() ; imodel++)
         independent[i][imodel] = models[imodel].forecast(ins) ;

     }
   return m_linreg.Fit(train_targets,independent);
  }
//+------------------------------------------------------------------+
//|  Destructor                                                      |
//+------------------------------------------------------------------+
CLinReg::~CLinReg(void)
  {
   if(CheckPointer(m_linreg)==POINTER_DYNAMIC)
      delete m_linreg ;
  }
//+------------------------------------------------------------------+
//| Predict                                                          |
//+------------------------------------------------------------------+
double CLinReg::predict(vector &inputs, IModel* &models[])
  {
   vector args = vector::Zeros(models.Size());

   for(uint i = 0; i<models.Size(); i++)
      args[i] = models[i].forecast(inputs);

   return m_linreg.Predict(args);
  }

При этом в классе CLinReg вводится также метод fit(), который обозначает операции для обучения консенсусной модели.

Метод fit() принимает на вход:

  • матрицу прогнозных аргументов;
  • целевые векторы;
  • массив моделей-компонентов.

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

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

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

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


Ограниченные линейные комбинации систематически отклоняющихся моделей

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

Данный ансамблевый метод реализован в виде класса Cbiased. Он включает в себя уже знакомые нам методы fit() и predict(), которые можно найти в других реализациях ансамблей.

//+------------------------------------------------------------------+
//|Compute the optimal linear combination of the predictions         |
//|subject to the constraints that the weights are all nonnegative.  |
//|A constant term is also included.                                 |
//|This is appropriate for biased predictors                         |
//+------------------------------------------------------------------+
class Cbiased:public PowellsMethod
  {

public:

                     Cbiased(void) ;
                    ~Cbiased() ;
   bool              fit(matrix & train_vars, vector &train_targets,IModel* &models[]);
   double            predict(vector &inputs,IModel* &models[]) ;

private:
   vector m_coefs ;    // Computed coefficients here
   int biased_ncases ;
   int biased_nvars ;
   matrix biased_x ;
   vector biased_y ;
   virtual double    func(vector &p,int n=0);
  } ;
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
Cbiased::Cbiased(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
Cbiased::~Cbiased(void)
  {
  }
//+------------------------------------------------------------------+
//| Function to be optimized                                         |
//+------------------------------------------------------------------+
double Cbiased::func(vector &p,int n = 0)
  {

   double  err, pred,diff, penalty ;
// Compute criterion
   err = 0.0 ;
   for(int i=0 ; i<biased_ncases ; i++)
     {
      pred = p[p.Size()-1] ;                    // Will cumulate prediction
      for(int j=0 ; j<biased_nvars ; j++)       // For all model outputs
         pred += biased_x[i][j] * p[j] ;        // Weight them per call
      diff = pred - biased_y[i] ;               // Predicted minus true
      err += diff * diff ;                      // Cumulate squared error
     }

   penalty = 0.0 ;
   for(int j=0 ; j<biased_nvars ; j++)
     {
      if(p[j] < 0.0)
         penalty -= 1.e30 * p[j] ;
     }

   return err + penalty ;
  }
//+------------------------------------------------------------------+
//| Fit the consensus model                                          |
//+------------------------------------------------------------------+
bool Cbiased::fit(matrix & train_vars, vector &train_targets,IModel* &models[])
  {
   biased_ncases = int(train_vars.Rows());
   biased_nvars = int(models.Size());
   biased_x = matrix::Zeros(biased_ncases,biased_nvars);

   biased_y = train_targets;

   m_coefs = vector::Zeros(biased_nvars+1);

   for(int i = 0; i<biased_ncases; i++)
     {
      vector ins = train_vars.Row(i);
      for(int j = 0; j<biased_nvars; j++)
         biased_x[i][j] = models[j].forecast(ins);
     }

   m_coefs.Fill(1.0/double(biased_nvars));
   m_coefs[m_coefs.Size()-1] = 0.0;

   int iters = Optimize(m_coefs,int(m_coefs.Size()));

   double sum = m_coefs.Sum();

   m_coefs/=sum;

   return true;
  }
//+------------------------------------------------------------------+
//| Make prediction with consensus model                             |
//+------------------------------------------------------------------+
double Cbiased::predict(vector &inputs,IModel* &models[])
  {
   double output=0.0;
   for(uint imodel=0 ; imodel<models.Size() ; imodel++)
     {
      output += m_coefs[imodel] * models[imodel].forecast(inputs);
     }
   return output;
  }

Тем не менее, ключевым отличием класса Cbiased является способ оптимизации весовых коэффициентов.

Оптимизация весовых коэффициентов осуществляется с помощью метода Пауэлла для минимизации функции. Поэтому класс Cbiased является потомком класса PowellsMethod. Функция критерия реализована в методе func(), которая выполняет итерацию по обучающим данным, накапливая квадратические ошибки с помощью весовых коэффициентов. Для каждого образца в наборе данных:

  • прогнозы от моделей-компонентов взвешиваются в соответствии с текущими весовыми коэффициентами и слагаемым-константой;
  • суммируются квадраты разниц между прогнозами и целевыми значениями.

Функция критерия завершается проверкой того, являются ли какие-либо из пробных весовых коэффициентов отрицательными. Если какие-либо из них отрицательны, применяется так называемый "штраф". Функция возвращает суммарную ошибку плюс любой штраф от отрицательных весовых коэффициентов. Данный тип ансамблевого метода наиболее уместен, когда известно, что некоторые модели-компоненты систематически отклоняются. Систематическое отклонение в данном контексте означает ситуацию, когда модели стабильно выдают прогнозы, которые либо слишком велики, либо слишком малы по сравнению с их соответствующими целевыми значениями, зачастую показывая явную тенденцию. Ограничивая весовые коэффициенты, класс Cbiased эффективно уменьшает влияние систематически отклоняющихся моделей, благодаря чему ансамблевое прогнозирование становится более сбалансированным и точным. В следующем разделе мы представим метод, который подходит для наборов моделей, показывающих лишь небольшое систематическое отклонение либо не имеющих такового, и акцент будет сделан на агрегировании прогнозов от моделей с сопоставимой эффективностью.


Ограниченные комбинации моделей, не имеющих систематических отклонений

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

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

Иллюстрацией этого служит следующее уравнение:

Ограниченная линейная модель

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

//+------------------------------------------------------------------+
//|Compute the optimal linear combination of the predictions         |
//|subject to the constraints that the weights are all nonnegative   |
//|and they sum to one.  This is appropriate for unbiased predictors.|
//+------------------------------------------------------------------+
class CUnbiased:public PowellsMethod
  {

public:

                     CUnbiased(void) ;
                    ~CUnbiased() ;
   bool              fit(matrix & train_vars, vector &train_targets,IModel* &models[]);
   double            predict(vector &inputs,IModel* &models[]) ;

private:
   vector m_coefs ;    // Computed coefficients here
   int unbiased_ncases ;
   int unbiased_nvars ;
   matrix unbiased_x ;
   vector unbiased_y ;
   virtual double    func(vector &p,int n=0);
  } ;
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CUnbiased::CUnbiased(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CUnbiased::~CUnbiased(void)
  {
  }
//+------------------------------------------------------------------+
//| Function to be optimized                                         |
//+------------------------------------------------------------------+
double CUnbiased::func(vector &p,int n = 0)
  {

   double sum, err, pred,diff, penalty ;

// Normalize weights to sum to one
   sum = p.Sum() ;

   if(sum < 1.e-60)    // Should almost never happen
      sum = 1.e-60 ;   // But be prepared to avoid division by zero

   vector   unbiased_work = p / sum ;

// Compute criterion
   err = 0.0 ;
   for(int i=0 ; i<unbiased_ncases ; i++)
     {
      pred = 0.0 ;                                       // Will cumulate prediction
      for(int j=0 ; j<unbiased_nvars ; j++)              // For all model outputs
         pred += unbiased_x[i][j] * unbiased_work[j] ;   // Weight them per call
      diff = pred - unbiased_y[i] ;                      // Predicted minus true
      err += diff * diff ;                               // Cumulate squared error
     }

   penalty = 0.0 ;
   for(int j=0 ; j<unbiased_nvars ; j++)
     {
      if(p[j] < 0.0)
         penalty -= 1.e30 * p[j] ;
     }

   return err + penalty ;
  }
//+------------------------------------------------------------------+
//| Fit the consensus model                                          |
//+------------------------------------------------------------------+
bool CUnbiased::fit(matrix & train_vars, vector &train_targets,IModel* &models[])
  {
   unbiased_ncases = int(train_vars.Rows());
   unbiased_nvars = int(models.Size());
   unbiased_x = matrix::Zeros(unbiased_ncases,unbiased_nvars);

   unbiased_y = train_targets;

   m_coefs = vector::Zeros(unbiased_nvars);

   for(int i = 0; i<unbiased_ncases; i++)
     {
      vector ins = train_vars.Row(i);
      for(int j = 0; j<unbiased_nvars; j++)
         unbiased_x[i][j] = models[j].forecast(ins);
     }

   m_coefs.Fill(1.0/double(unbiased_nvars));

   int iters = Optimize(m_coefs);

   double sum = m_coefs.Sum();

   m_coefs/=sum;

   return true;
  }
//+------------------------------------------------------------------+
//| Make prediction with consensus model                             |
//+------------------------------------------------------------------+
double CUnbiased::predict(vector &inputs,IModel* &models[])
  {
   double output=0.0;
   for(uint imodel=0 ; imodel<models.Size() ; imodel++)
     {
      output += m_coefs[imodel] * models[imodel].forecast(inputs);
     }
   return output;
  }

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


Комбинации прогнозных моделей, взвешенные по дисперсии

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

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

Ансамбль, взвешенный по дисперсии

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

Весовое уравнение

Ансамбль взвешенных по дисперсии моделей реализован в классе CWeighted. В методе fit() для каждого обучающего образца:

  • рассчитывается прогноз каждой модели-компонента;
  • суммируются квадратические ошибки каждого прогноза.

//+------------------------------------------------------------------+
//| Compute the variance-weighted average of the predictions         |
//+------------------------------------------------------------------+
class CWeighted
  {
public:

                     CWeighted(void) ;
                    ~CWeighted() ;
   bool              fit(matrix & train_vars, vector &train_targets,IModel* &models[]);
   double            predict(vector &inputs,IModel* &models[]) ;

private:
   vector m_coefs ;    // Computed coefficients here

  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CWeighted::CWeighted(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CWeighted::~CWeighted(void)
  {
  }
//+------------------------------------------------------------------+
//| Fit a consensus model                                            |
//+------------------------------------------------------------------+
bool CWeighted::fit(matrix &train_vars,vector &train_targets,IModel* &models[])
  {
   m_coefs = vector::Zeros(models.Size());

   m_coefs.Fill(1.e-60);
   double diff = 0.0;
   for(ulong i = 0; i<train_vars.Rows(); i++)
     {
      vector ins = train_vars.Row(i);
      for(ulong j = 0; j<m_coefs.Size(); j++)
        {
         diff = models[j].forecast(ins) - train_targets[i];
         m_coefs[j] += (diff*diff);
        }
     }

   m_coefs=1.0/m_coefs;

   m_coefs/=m_coefs.Sum();

   return true;
  }
//+------------------------------------------------------------------+
//| Make a prediction with the consensus model                       |
//+------------------------------------------------------------------+
double CWeighted::predict(vector &inputs,IModel* &models[])
  {
   double output = 0.0;

   for(uint i = 0; i<models.Size(); i++)
      output+=m_coefs[i]*models[i].forecast(inputs);

   return output;
  }

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


Интерполированные комбинации на основе обобщенно-регрессионных нейронных сетей

Ансамблевые методы, которые мы обсудили на данный момент, отлично работают, когда консенсусная модель обучена на чистых данных. Однако когда обучающие данные зашумлены, модель может быть склонна к плохому обобщению. Чтобы решить эту проблему, существует эффективный метод регрессии – обобщенно-регрессионная нейронная сеть (GRNN). Заметным преимуществом обобщенно-регрессионной нейронной сети над традиционной регрессией является ее сниженная подверженность переобучению. Достигается оно тем, что параметры обобщенно-регрессионных нейронных сетей относительно меньше влияют на модель по сравнению с традиционными методиками регрессии. Хотя такое улучшение в обобщении дается на фоне некоторого снижения точности, обобщенно-регрессионные нейронные сети способны моделировать сложные, нелинейные зависимости, представляя собой полезный инструмент, когда данные демонстрируют подобные характеристики.

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

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

Условное математическое ожидание

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

Формула обобщенно-регрессионной нейронной сети

Перед презентацией кода для ансамблевого метода на базе обобщенной-регрессионной нейронной сети мы должны сперва обсудить реализацию самой обобщенной-регрессионной нейронной сети. Код обобщенной-регрессионной нейронной сети определен в файле grnn.mqh, который содержит определение класса CGrnn.

//+------------------------------------------------------------------+
//| General regression neural network                                |
//+------------------------------------------------------------------+
class CGrnn
  {
public:
                     CGrnn(void);
                     CGrnn(int num_outer, int num_inner, double start_std);
                    ~CGrnn(void);
   bool              fit(matrix &predictors,matrix &targets);
   vector            predict(vector &predictors);
   //double            get_mse(void);
private:
   bool              train(void);
   double            execute(void);
   ulong             m_inputs,m_outputs;
   int               m_inner,m_outer;
   double            m_start_std;
   ulong             m_rows,m_cols;
   bool              m_trained;
   vector            m_sigma;
   matrix            m_targets,m_preds;
  };

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

//+------------------------------------------------------------------+
//| Default constructor                                              |
//+------------------------------------------------------------------+
CGrnn::CGrnn(void)
  {
   m_inner = 100;
   m_outer = 10;
   m_start_std = 3.0;
  }
//+------------------------------------------------------------------+
//| Parametric constructor                                           |
//+------------------------------------------------------------------+
CGrnn::CGrnn(int num_outer,int num_inner,double start_std)
  {
   m_inner = num_inner;
   m_outer = num_outer;
   m_start_std = start_std;
  }

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

//+------------------------------------------------------------------+
//| Fit data to a model                                              |
//+------------------------------------------------------------------+
bool CGrnn::fit(matrix &predictors,matrix &targets)
  {
   m_targets = targets;
   m_preds = predictors;
   m_trained = false;
   m_rows = m_preds.Rows();
   m_cols = m_preds.Cols();
   m_sigma = vector::Zeros(m_preds.Cols());

   if(m_targets.Rows() != m_preds.Rows())
     {
      Print(__FUNCTION__, " invalid inputs ");
      return false;
     }

   m_trained = train();

   return m_trained;
  }

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

//+------------------------------------------------------------------+
//| Make a prediction with a trained model                           |
//+------------------------------------------------------------------+
vector CGrnn::predict(vector &predictors)
  {
   if(!m_trained)
     {
      Print(__FUNCTION__, " no trained model available for predictions ");
      return vector::Zeros(1);
     }

   if(predictors.Size() != m_cols)
     {
      Print(__FUNCTION__, " invalid inputs ");
      return vector::Zeros(1);
     }

   vector output  = vector::Zeros(m_targets.Cols());
   double diff,dist,psum=0.0;

   for(ulong i = 0; i<m_rows; i++)
     {
      dist  = 0.0;
      for(ulong j = 0; j<m_cols; j++)
        {
         diff  = predictors[j]  - m_preds[i][j];
         diff/= m_sigma[j];
         dist += (diff*diff);
        }
      dist  = exp(-dist);
      if(dist< EPS1)
         dist = EPS1;
      for(ulong k = 0; k<m_targets.Cols(); k++)
         output[k] += dist * m_targets[i][k];
      psum += dist;
     }
   output/=psum;
   return output;
  }

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

Ансамбль на основе обобщенно-регрессионной нейронной сети реализован в виде класса CGenReg.

//+------------------------------------------------------------------+
//| Compute the General Regression of the predictions                |
//+------------------------------------------------------------------+
class CGenReg
  {

public:

                     CGenReg(void) ;
                    ~CGenReg(void) ;
   bool              fit(matrix & train_vars, vector &train_targets,IModel* &models[]);
   double            predict(vector &inputs,IModel* &models[]) ;

private:
   CGrnn *grnn ;       // The GRNN object
   vector m_work ;     // Work vector nmodels long
   vector            m_targs;
   matrix            m_vars;
  } ;

В классе CGenReg используется объект CGrnn для моделирования сложных взаимосвязей между прогнозами отдельных моделей и фактическими целевыми значениями. В методе fit() он сначала сохраняет обучающие данные, включая целевые значения (train_targets) и входные переменные (train_vars). Затем он собирает отдельные прогнозы от каждой из моделей, создает матрицу (preds), в которой каждая строка представляет обучающую выборку, а каждый столбец содержит прогноз от соответствующей модели из набора. Объект CGrnn обучается с помощью матрицы отдельных прогнозов (preds) в качестве входных данных и фактических целевых значений (targ) в качестве выходных данных.

//+------------------------------------------------------------------+
//| Fit consensus model                                              |
//+------------------------------------------------------------------+
bool CGenReg::fit(matrix & train_vars, vector &train_targets,IModel* &models[])
  {
   m_targs = train_targets;
   m_vars = train_vars;

   m_work = vector::Zeros(models.Size());

   matrix targ = matrix::Zeros(train_targets.Size(),1);

   if(!targ.Col(train_targets,0))
     {
      Print(__FUNCSIG__, " error adding column ", GetLastError());
      return false;
     }

   matrix preds(m_vars.Rows(),models.Size());
   for(ulong i = 0; i<m_vars.Rows(); i++)
     {
      vector ins = m_vars.Row(i);
      for(uint j = 0; j< models.Size(); j++)
        {
         preds[i][j] = models[j].forecast(ins);
        }
     }

   return grnn.fit(preds,targ);
  }

В методе predict(), класс собирает прогнозы от каждой из моделей для новых входных данных и хранит их в рабочем векторе (m_work). Обученный объект CGrnn затем используется для прогнозирования конечного вывода на основе этих отдельных прогнозов. Метод возвращает первый элемент выходного прогнозного вектора в качестве конечного прогноза.

//+------------------------------------------------------------------+
//| Make a prediction                                                |
//+------------------------------------------------------------------+
double CGenReg::predict(vector &inputs,IModel* &models[])
  {
   vector output;
   for(uint i = 0; i<models.Size(); i++)
      m_work[i] = models[i].forecast(inputs);
   output = grnn.predict(m_work);
   return output[0];
  }


Заключение: Сравнение ансамблевых методов

Были представлены различные ансамблевые методы, и были кратко рассмотрены присущие им преимущества и недостатки. Чтобы завершить данную статью, мы исследуем, каковы эти методы в сравнении при работе с реальными данными. Данное сравнение реализовано в виде скрипта MetaTrader 5 под названием Ensemble_Demo.mq5.

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

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

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

Модели представляют собой нейронные сети с прямой связью, реализованные в виде класса FFNN в файле mlffnn.mqh.

//+------------------------------------------------------------------+
//| Class for a basic feed-forward neural network                    |
//+------------------------------------------------------------------+
class FFNN
  {
protected:

   bool              m_trained;             // flag noting if neural net successfully trained
   matrix            m_weights[];           // layer weights
   matrix            m_outputs[];           // hidden layer outputs
   matrix            m_result;              // training result
   uint              m_epochs;              // number of epochs
   ulong             m_num_inputs;          // number of input variables for nn
   ulong             m_layers;              // number of layers of neural net
   ulong             m_hidden_layers;       // number of hidden layers
   ulong             m_hidden_layer_size[]; // node config for layers
   double            m_learn_rate;          // learning rate
   ENUM_ACTIVATION_FUNCTION m_act_fn;       // activation function
   //+------------------------------------------------------------------+
   //| Initialize the neural network structure                          |
   //+------------------------------------------------------------------+

   virtual bool      create(void)
     {
      if(m_layers - m_hidden_layers != 1)
        {
         Print(__FUNCTION__,"  Network structure misconfiguration ");
         return false;
        }

      for(ulong i = 0; i<m_layers; i++)
        {
         if(i==0)
           {
            if(!m_weights[i].Init(m_num_inputs+1,m_hidden_layer_size[i]))
              {
               Print(__FUNCTION__," ",__LINE__," ", GetLastError());
               return false;
              }
           }
         else
            if(i == m_layers-1)
              {
               if(!m_weights[i].Init(m_hidden_layer_size[i-1]+1,1))
                 {
                  Print(__FUNCTION__," ",__LINE__," ", GetLastError());
                  return false;
                 }
              }
            else
              {
               if(!m_weights[i].Init(m_hidden_layer_size[i-1]+1,m_hidden_layer_size[i]))
                 {
                  Print(__FUNCTION__," ",__LINE__," ", GetLastError());
                  return false;
                 }
              }
        }

      return true;
     }
   //+------------------------------------------------------------------+
   //| Calculate output  from all layers                                |
   //+------------------------------------------------------------------+

   virtual matrix    calculate(matrix &data)
     {
      if(data.Cols() != m_weights[0].Rows()-1)
        {
         Print(__FUNCTION__," input data not compatible with network structure ");
         return matrix::Zeros(0,0);
        }

      matrix temp = data;

      for(ulong i = 0; i<m_hidden_layers; i++)
        {
         if(!temp.Resize(temp.Rows(), m_weights[i].Rows()) ||
            !temp.Col(vector::Ones(temp.Rows()), m_weights[i].Rows() - 1))
           {
            Print(__FUNCTION__," ",__LINE__," ", GetLastError());
            matrix::Zeros(0,0);
           }

         m_outputs[i]=temp.MatMul(m_weights[i]);

         if(!m_outputs[i].Activation(temp, m_act_fn))
           {
            Print(__FUNCTION__," ",__LINE__," ", GetLastError());
            return matrix::Zeros(0,0);
           }

        }

      if(!temp.Resize(temp.Rows(), m_weights[m_hidden_layers].Rows()) ||
         !temp.Col(vector::Ones(temp.Rows()), m_weights[m_hidden_layers].Rows() - 1))
        {
         Print(__FUNCTION__," ",__LINE__," ", GetLastError());
         return matrix::Zeros(0,0);
        }

      return temp.MatMul(m_weights[m_hidden_layers]);
     }
   //+------------------------------------------------------------------+
   //|  Backpropagation method                                          |
   //+------------------------------------------------------------------+

   virtual bool      backprop(matrix &data, matrix& targets, matrix &result)
     {
      if(targets.Rows() != result.Rows() ||
         targets.Cols() != result.Cols())
        {
         Print(__FUNCTION__," invalid function parameters ");
         return false;
        }
      matrix loss = (targets - result) * 2;
      matrix gradient = loss.MatMul(m_weights[m_hidden_layers].Transpose());
      matrix temp;

      for(long i = long(m_hidden_layers-1); i>-1; i--)
        {
         if(!m_outputs[i].Activation(temp, m_act_fn))
           {
            Print(__FUNCTION__," ",__LINE__," ", GetLastError());
            return false;
           }

         if(!temp.Resize(temp.Rows(), m_weights[i+1].Rows()) ||
            !temp.Col(vector::Ones(temp.Rows()), m_weights[i+1].Rows() - 1))
           {
            Print(__FUNCTION__," ",__LINE__," ", GetLastError());
            return false;
           }

         m_weights[i+1] = m_weights[i+1] + temp.Transpose().MatMul(loss) * m_learn_rate;

         if(!m_outputs[i].Derivative(temp, m_act_fn))
           {
            Print(__FUNCTION__," ",__LINE__," ", GetLastError());
            return false;
           }

         if(!gradient.Resize(gradient.Rows(), gradient.Cols() - 1))
           {
            Print(__FUNCTION__," ",__LINE__," ", GetLastError());
            return false;
           }

         loss = gradient * temp;

         gradient = (i>0)?loss.MatMul(m_weights[i].Transpose()):gradient;
        }

      temp = data;
      if(!temp.Resize(temp.Rows(), m_weights[0].Rows()) ||
         !temp.Col(vector::Ones(temp.Rows()), m_weights[0].Rows() - 1))
        {
         Print(__FUNCTION__," ",__LINE__," ", GetLastError());
         return false;
        }

      m_weights[0] = m_weights[0] + temp.Transpose().MatMul(loss) * m_learn_rate;

      return true;
     }

public:
   //+------------------------------------------------------------------+
   //| Constructor                                                      |
   //+------------------------------------------------------------------+

                     FFNN(ulong &layersizes[], ulong num_layers = 3)
     {
      m_trained = false;
      m_layers = num_layers;
      m_hidden_layers = m_layers - 1;

      ArrayCopy(m_hidden_layer_size,layersizes,0,0,int(m_hidden_layers));

      ArrayResize(m_weights,int(m_layers));

      ArrayResize(m_outputs,int(m_hidden_layers));
     }
   //+------------------------------------------------------------------+
   //| Destructor                                                       |
   //+------------------------------------------------------------------+

                    ~FFNN(void)
     {
     }
   //+------------------------------------------------------------------+
   //| Neural net training method                                       |
   //+------------------------------------------------------------------+

   bool              fit(matrix &data, matrix &targets,double learning_rate, ENUM_ACTIVATION_FUNCTION act_fn, uint num_epochs)
     {
      m_learn_rate = learning_rate;
      m_act_fn = act_fn;
      m_epochs = num_epochs;
      m_num_inputs = data.Cols();
      m_trained = false;

      if(!create())
         return false;

      for(uint ep = 0; ep < m_epochs; ep++)
        {
         m_result = calculate(data);

         if(!backprop(data, targets,m_result))
            return m_trained;
        }

      m_trained = true;
      return m_trained;
     }
   //+------------------------------------------------------------------+
   //| Predict method                                                   |
   //+------------------------------------------------------------------+

   matrix            predict(matrix &data)
     {
      if(m_trained)
         return calculate(data);
      else
         return matrix::Zeros(0,0);
     }

  };
//+------------------------------------------------------------------+

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

  • m_trained – булев флаг, указывающий, была ли сеть успешно обучена;
  • m_weights – массив матриц, которые хранят веса между всеми слоями;
  • m_outputs – массив матриц, которые хранят выходные данные из каждого скрытого слоя;
  • m_result – матрица, хранящая конечный вывод нейронной сети после обучения;
  • m_epochs – количество эпох (итераций) обучения;
  • m_num_inputs – количество входных переменных для сети;
  • m_layers – общее количество слоев в нейронной сети, включая входной и выходной слои;
  • m_hidden_layers – количество скрытых слоев в сети;
  • m_hidden_layer_size – массив, который определяет количество узлов в каждом из скрытых слоев;
  • m_learn_rate – скорость обучения, используемая для обновления весов в процессе обучения;
  • m_act_fn – функция активации, используемая в скрытых слоях.

Класс содержит как закрытые, так и открытые методы. Закрытые методы включают, например, следующие:

  • create(), который инициализирует структуру сети путем выделения памяти для весовых матриц и выводов скрытых слоев на основе определенной конфигурации;
  • calculate(), который распространяет входные данные по сети, используя веса и функции активации для расчета вывода;
  • backprop(), который реализует алгоритм обратного распространения, корректируя веса на основании погрешности между прогнозным и фактическим выводами.

Открытые методы включают:

  • FFNN() (конструктор), который инициализирует сеть с указанными числом слоев и размерами скрытых слоев;
  • ~FFNN() (деструктор), который высвобождает ресурсы, выделенные для сети;
  • fit(), который обучает сеть на представленном наборе данных, корректируя веса посредством обратного распространения в рамках указанного числа эпох;
  • predict(), который использует обученную сеть для генерации прогнозов для новых входных данных, эффективно реализуя прямое распространение.

В скрипте класс CMlfn реализует интерфейс IModel на основе экземпляра FFNN. Далее кратко представлен запуск скрипта в различных конфигурациях.

//+------------------------------------------------------------------+
//|                                                Ensemble_Demo.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property script_show_inputs
#include<mlffnn.mqh>
#include<ensemble.mqh>
#include<np.mqh>
//--- input parameters
input int      NumGoodModels=3;
input int      NumBiasedModels=7;
input int      NumBadModels=5;
input int      NumSamples=20;
input int      NumAttempts=1;
input double   VarParam=3.0;//variance parameter
input bool     TrainCombinedModelsOnCleanData = true;
//+------------------------------------------------------------------+
//| Clean up dynamic array pointers                                  |
//+------------------------------------------------------------------+
void cleanup(IModel* &array[])
  {
   for(uint i = 0; i<array.Size(); i++)
      if(CheckPointer(array[i])==POINTER_DYNAMIC)
         delete array[i];
  }
//+------------------------------------------------------------------+
//| IModel implementation of Multilayered iterative algo of GMDH     |
//+------------------------------------------------------------------+
class CMlfn:public IModel
  {
private:
   FFNN              *m_mlfn;
   double             m_learningrate;
   ENUM_ACTIVATION_FUNCTION    m_actfun;
   uint               m_epochs;
   ulong              m_layer[3];

public:
                     CMlfn();
                    ~CMlfn(void);
   void              setParams(double learning_rate, ENUM_ACTIVATION_FUNCTION act_fn, uint num_epochs);
   bool              train(matrix &predictors,matrix&targets);
   double            forecast(vector &predictors);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CMlfn::CMlfn(void)
  {
   m_learningrate=0.01;
   m_actfun=AF_SOFTMAX;
   m_epochs= 100;
   m_layer[0] = 2;
   m_layer[1] = 2;
   m_layer[2] = 1;
   m_mlfn = new FFNN(m_layer);
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CMlfn::~CMlfn(void)
  {
   if(CheckPointer(m_mlfn) == POINTER_DYNAMIC)
      delete m_mlfn;
  }
//+------------------------------------------------------------------+
//| Set other hyperparameters of the model                           |
//+------------------------------------------------------------------+
void CMlfn::setParams(double learning_rate, ENUM_ACTIVATION_FUNCTION act_fn, uint num_epochs)
  {
   m_learningrate=learning_rate;
   m_actfun=act_fn;
   m_epochs= num_epochs;
  }
//+------------------------------------------------------------------+
//| Fit a model to the data                                          |
//+------------------------------------------------------------------+
bool CMlfn::train(matrix &predictors,matrix &targets)
  {
   return m_mlfn.fit(predictors,targets,m_learningrate,m_actfun,m_epochs);
  }
//+------------------------------------------------------------------+
//| Make a prediction with the trained model                         |
//+------------------------------------------------------------------+
double CMlfn::forecast(vector &predictors)
  {
   matrix preds(1,predictors.Size());

   if(!preds.Row(predictors,0))
     {
      Print(__FUNCTION__, " error inserting row ", GetLastError());
      return EMPTY_VALUE;
     }
   matrix out = m_mlfn.predict(preds);
   return out[0][0];
  }

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   if(NumSamples<1 || NumAttempts<1 || VarParam<0.0 || NumBadModels<1 || NumGoodModels<1 || NumBiasedModels<1)
     {
      Print(" Invalid User inputs ");
      return;
     }

   int ndone, divisor;
   double diff, std, temp;

   double computed_err_average ;
   double computed_err_unconstrained ;
   double computed_err_unbiased ;
   double computed_err_biased ;
   double computed_err_weighted ;
   double computed_err_bagged ;
   double computed_err_genreg ;

   CAvg average;
   CLinReg unconstrained;
   CUnbiased unbiased;
   Cbiased biased;
   CWeighted weighted;
   CGenReg genreg;

   vector computed_err_raw = vector::Zeros(NumBadModels+NumGoodModels+NumBiasedModels);

   std  =  sqrt(VarParam);

   divisor = 1;

   IModel* puremodels[];
   matrix xgood[],xbad[],xbiased[],test[10];

   if(ArrayResize(puremodels,NumBadModels+NumGoodModels+NumBiasedModels)<0 ||
      ArrayResize(xgood,NumBadModels+NumGoodModels+NumBiasedModels)<0 ||
      ArrayResize(xbad,NumBadModels+NumGoodModels+NumBiasedModels)<0 ||
      ArrayResize(xbiased,NumBadModels+NumGoodModels+NumBiasedModels)<0)
     {
      Print(" failed puremodels array resize ", GetLastError());
      return;
     }

   for(uint i = 0; i<puremodels.Size(); i++)
      puremodels[i] = new CMlfn();

   for(uint i = 0; i<xgood.Size(); i++)
      xgood[i] = matrix::Zeros(NumSamples,3);

   for(uint i = 0; i<xbad.Size(); i++)
      xbad[i] = matrix::Zeros(NumSamples,3);

   for(uint i = 0; i<xbiased.Size(); i++)
      xbiased[i] = matrix::Zeros(NumSamples,3);

   for(uint i = 0; i<test.Size(); i++)
      test[i] = matrix::Zeros(NumSamples,3);

   computed_err_average = 0.0 ;
   computed_err_unconstrained = 0.0 ;
   computed_err_unbiased = 0.0 ;
   computed_err_biased = 0.0 ;
   computed_err_weighted = 0.0 ;
   computed_err_bagged = 0.0 ;
   computed_err_genreg = 0.0 ;

   vector t,v;
   matrix d;

   ndone  = 1;
   for(uint i = 0; i<xgood.Size(); i++)
     {
      xgood[i].Random(0.0,1.0);
      if(!xgood[i].Col(sin(xgood[i].Col(0)) - pow(xgood[i].Col(1),2.0) + std*xgood[i].Col(2),2))
        {
         Print(" column insertion error ", GetLastError());
         cleanup(puremodels);
         return;
        }
     }
   matrix xb(xgood[0].Rows(),1);
   for(uint i = 0; i<xbad.Size(); i++)
     {
      xbad[i] = xgood[0];
      xb.Random(0.0,1.0);
      if(!xbad[i].Col(xb.Col(0),2))
        {
         Print(" column insertion error ", GetLastError());
         cleanup(puremodels);
         return;
        }
     }
   for(uint i = 0; i<xbiased.Size(); i++)
     {
      xbiased[i] = xgood[0];
      if(!xbiased[i].Col(xgood[0].Col(2)+1.0,2))
        {
         Print(" column insertion error ", GetLastError());
         cleanup(puremodels);
         return;
        }
     }
   for(uint i = 0; i<test.Size(); i++)
     {
      test[i].Random(0.0,1.0);
      if(!test[i].Col(sin(test[i].Col(0)) - pow(test[i].Col(1),2.0) + std * test[i].Col(2),2))
        {
         Print(" column insertion error ", GetLastError());
         cleanup(puremodels);
         return;
        }
     }

   for(uint imodel=0; imodel<puremodels.Size(); imodel++)
     {
      if(imodel < xgood.Size())
        {
         t=xgood[imodel].Col(2);
         d=np::sliceMatrixCols(xgood[imodel],0,2);
        }
      else
         if(imodel >= xgood.Size() && imodel<(xgood.Size()+xbiased.Size()))
           {
            t=xbiased[imodel-xgood.Size()].Col(2);
            d=np::sliceMatrixCols(xbiased[imodel-xgood.Size()],0,2);
           }
         else
           {
            t=xbad[imodel - (xgood.Size()+xbiased.Size())].Col(2);
            d=np::sliceMatrixCols(xbad[imodel - (xgood.Size()+xbiased.Size())],0,2);
           }

      matrix tt(t.Size(),1);

      if(!tt.Col(t,0) || !puremodels[imodel].train(d,tt))
        {
         Print(" failed column insertion ", GetLastError());
         cleanup(puremodels);
         return;
        }

      temp  = 0.0;

      for(uint i = 0; i<test.Size(); i++)
        {
         for(int j = 0; j<NumSamples; j++)
           {
            t  = test[i].Row(j);
            v  = np::sliceVector(t,0,2);
            diff = puremodels[imodel].forecast(v) - t[2];
            temp += diff*diff;
           }
        }
      computed_err_raw[imodel] += temp/double(test.Size()*NumSamples);
     }
//average
   matrix tdata;
   if(TrainCombinedModelsOnCleanData)
      tdata = xgood[0];
   else
     {
      tdata = matrix::Zeros(NumSamples*3,3);
      if(!np::matrixCopyRows(tdata,xgood[0],0,NumSamples) ||
         !np::matrixCopyRows(tdata,xbad[0],NumSamples,NumSamples*2) ||
         !np::matrixCopyRows(tdata,xbiased[0],NumSamples*2))
        {
         Print(" failed to create noisy dataset");
         cleanup(puremodels);
         return;
        }
     }
   temp = 0.0;
   for(uint i  = 0; i<test.Size(); i++)
     {
      for(int j = 0; j<NumSamples; j++)
        {
         t = test[i].Row(j);
         v = np::sliceVector(t,0,2);
         diff = average.predict(v,puremodels) - t[2];
         temp += diff*diff;
        }
     }
   computed_err_average += temp/double(test.Size()*NumSamples);
//unconstrained
   temp = 0.0;
   t = tdata.Col(2);
   d = np::sliceMatrixCols(tdata,0,2);
   if(!unconstrained.fit(d,t,puremodels))
     {
      Print(" failed to fit unconstrained model ");
      cleanup(puremodels);
     }

   for(uint i  = 0; i<test.Size(); i++)
     {
      for(int j = 0; j<NumSamples; j++)
        {
         t = test[i].Row(j);
         v = np::sliceVector(t,0,2);
         diff = unconstrained.predict(v,puremodels) - t[2];
         temp += diff*diff;
        }
     }
   computed_err_unconstrained += temp/double(test.Size()*NumSamples);
//unbiased
   temp = 0.0;
   t = tdata.Col(2);
   d = np::sliceMatrixCols(tdata,0,2);
   if(!unbiased.fit(d,t,puremodels))
     {
      Print(" failed to fit unbiased model ");
      cleanup(puremodels);
     }

   for(uint i  = 0; i<test.Size(); i++)
     {
      for(int j = 0; j<NumSamples; j++)
        {
         t = test[i].Row(j);
         v = np::sliceVector(t,0,2);
         diff = unbiased.predict(v,puremodels) - t[2];
         temp += diff*diff;
        }
     }
   computed_err_unbiased += temp/double(test.Size()*NumSamples);
//biased
   temp = 0.0;
   t = tdata.Col(2);
   d = np::sliceMatrixCols(tdata,0,2);
   if(!biased.fit(d,t,puremodels))
     {
      Print(" failed to fit biased model ");
      cleanup(puremodels);
     }

   for(uint i  = 0; i<test.Size(); i++)
     {
      for(int j = 0; j<NumSamples; j++)
        {
         t = test[i].Row(j);
         v = np::sliceVector(t,0,2);
         diff = biased.predict(v,puremodels) - t[2];
         temp += diff*diff;
        }
     }
   computed_err_biased += temp/double(test.Size()*NumSamples);
//weighted
   temp = 0.0;
   t = tdata.Col(2);
   d = np::sliceMatrixCols(tdata,0,2);
   if(!weighted.fit(d,t,puremodels))
     {
      Print(" failed to fit weighted model ");
      cleanup(puremodels);
     }

   for(uint i  = 0; i<test.Size(); i++)
     {
      for(int j = 0; j<NumSamples; j++)
        {
         t = test[i].Row(j);
         v = np::sliceVector(t,0,2);
         diff = weighted.predict(v,puremodels) - t[2];
         temp += diff*diff;
        }
     }
   computed_err_weighted += temp/double(test.Size()*NumSamples);
//gendreg
   temp = 0.0;
   t = tdata.Col(2);
   d = np::sliceMatrixCols(tdata,0,2);
   if(!genreg.fit(d,t,puremodels))
     {
      Print(" failed to fit generalized regression model ");
      cleanup(puremodels);
     }
   for(uint i  = 0; i<test.Size(); i++)
     {
      for(int j = 0; j<NumSamples; j++)
        {
         t = test[i].Row(j);
         v = np::sliceVector(t,0,2);
         diff = genreg.predict(v,puremodels) - t[2];
         temp += diff*diff;
        }
     }
   computed_err_genreg += temp/double(test.Size()*NumSamples);

   temp = 0.0;
   PrintFormat("\n\n\nRandom DataSet%5d    Raw errors:", ndone);
   for(uint imodel  = 0; imodel<puremodels.Size() ; imodel++)
     {
      PrintFormat("  %.8lf", computed_err_raw[imodel] / ndone) ;
      temp += computed_err_raw[imodel] / ndone ;
     }
   PrintFormat("\n       Mean raw error = %8.8lf", temp / double(puremodels.Size())) ;

   PrintFormat("\n        Average error = %8.8lf", computed_err_average / ndone) ;
   PrintFormat("\n  Unconstrained error = %8.8lf", computed_err_unconstrained / ndone) ;
   PrintFormat("\n       Unbiased error = %8.8lf", computed_err_unbiased / ndone) ;
   PrintFormat("\n         Biased error = %8.8lf", computed_err_biased / ndone) ;
   PrintFormat("\n       Weighted error = %8.8lf", computed_err_weighted / ndone) ;
   PrintFormat("\n         GenReg error = %8.8lf", computed_err_genreg / ndone) ;

   cleanup(puremodels);
  }
//+------------------------------------------------------------------+

Запуск скрипта со значениями по умолчанию дает следующий вывод.

MR      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)       Random DataSet    1    Raw errors:
KI      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.38602529
HP      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.36430552
CK      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.36703202
OS      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.51205057
EJ      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.57791798
HE      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.66825953
FL      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.65051234
QD      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.57403745
EO      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.71593174
PF      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.62444495
NQ      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.77552594
KI      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.75079339
MP      0       15:56:41.914    Ensemble_Demo (BTCUSD,D1)         0.78851743
CK      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)         0.52343272
OR      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)         0.70166082
EK      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)       
RE      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)              Mean raw error = 0.59869651
QL      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)       
DE      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)               Average error = 0.55224337
ML      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)       
QF      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)         Unconstrained error = 10.21673109
KL      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)       
RI      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)              Unbiased error = 0.55224337
GL      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)       
PH      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)                Biased error = 0.48431477
CL      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)       
HH      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)              Weighted error = 0.51507522
OM      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)       
LK      0       15:56:41.915    Ensemble_Demo (BTCUSD,D1)                GenReg error = 0.33761372
KM      0       15:57:11.108    Ensemble_Demo (BTCUSD,D1)       
GG      0       15:57:11.108    Ensemble_Demo (BTCUSD,D1)       
CQ      0       15:57:11.108    Ensemble_Demo (BTCUSD,D1)       

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

NL      0       15:59:51.502    Ensemble_Demo (BTCUSD,D1)       Random DataSet    1    Raw errors:
OS      0       15:59:51.502    Ensemble_Demo (BTCUSD,D1)         0.72840629
GJ      0       15:59:51.502    Ensemble_Demo (BTCUSD,D1)         0.63345953
PE      0       15:59:51.502    Ensemble_Demo (BTCUSD,D1)         0.68442450
JL      0       15:59:51.502    Ensemble_Demo (BTCUSD,D1)         0.91936106
OD      0       15:59:51.502    Ensemble_Demo (BTCUSD,D1)         0.75230667
LO      0       15:59:51.502    Ensemble_Demo (BTCUSD,D1)         0.88366446
PF      0       15:59:51.502    Ensemble_Demo (BTCUSD,D1)         0.78226316
CQ      0       15:59:51.502    Ensemble_Demo (BTCUSD,D1)         0.87140196
II      0       15:59:51.502    Ensemble_Demo (BTCUSD,D1)         0.58672356
KP      0       15:59:51.502    Ensemble_Demo (BTCUSD,D1)         1.09990815
MK      0       15:59:51.502    Ensemble_Demo (BTCUSD,D1)         0.92548778
OR      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)         1.03795716
GJ      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)         0.80684429
GE      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)         1.24041209
GL      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)         0.92169606
NF      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)       
CS      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)              Mean raw error = 0.85828778
RF      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)       
DS      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)               Average error = 0.83433599
FF      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)       
FP      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)         Unconstrained error = 23416285121251567120416768.00000000
DS      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)       
JR      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)              Unbiased error = 0.83433599
HS      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)       
PP      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)                Biased error = 0.74321307
LD      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)       
GQ      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)              Weighted error = 0.83213118
PD      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)       
FR      0       15:59:51.503    Ensemble_Demo (BTCUSD,D1)                GenReg error = 0.78697882

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

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

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

Имя файла
Описание
MQL5/include/mlffnn.mqh
Содержит определение класса FFNN, реализующего базовый многослойный персептрон
MQL5/include/grnn.mqh
Определяет класс CGrnn, который реализует обобщенно-регрессионную нейронную сеть, использующей имитацию отжига
MQL5/include/OLS.mqh
Определяет класс OLS, который реализует регрессию обычным методом наименьших квадратов
MQL5/include/ensemble.mqh
Содержит определение шести ансамблевых методов, реализованных в виде классов CAvg, CLinReg, Cbiased, CUnbiased, CWeighted и CGenReg
MQL5/include/np.mqh
Содержит матрицу отклонений и функцию полезности вектора
MQL5/scripts/Ensemble_Demo.mq5
Этот скрипт демонстрирует работу ансамблевых классов, определенных в файле ensemble.mqh

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/16630

Прикрепленные файлы |
ensemble.mqh (50.08 KB)
grnn.mqh (6.6 KB)
mlffnn.mqh (8.39 KB)
np.mqh (74.16 KB)
OLS.mqh (13.34 KB)
Ensemble_Demo.mq5 (13.03 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Roman Shiredchenko
Roman Shiredchenko | 14 июл. 2025 в 08:28

суппер статья - спасибо. Тема актуальна -изучаю данные  - буду смотреть у себя в терминале и исследовать.....

дам обратную связь тут.

Эта информация у меня в очереди к рассмотрению......

Прогнозирование трендов с помощью LSTM для стратегий следования за трендом Прогнозирование трендов с помощью LSTM для стратегий следования за трендом
Долгая кратковременная память (LSTM) - это тип рекуррентной нейронной сети (RNN), предназначенной для моделирования последовательных данных путем эффективного учета долгосрочных зависимостей и решения проблемы исчезающего градиента. В настоящей статье мы рассмотрим, как использовать LSTM для прогнозирования будущих тенденций, повышая эффективность стратегий следования за трендами. В статье будет рассказано о внедрении ключевых концепций и стоящей за разработкой мотивации, извлечении данных из MetaTrader 5, использовании этих данных для обучения модели на Python, интеграции модели машинного обучения в MQL5, а также о результатах и перспективах на будущее на основании статистического бэк-тестирования.
Оценка качества торговли спредами по факторам сезонности на рынке Форекс в терминале MetaTrader 5 Оценка качества торговли спредами по факторам сезонности на рынке Форекс в терминале MetaTrader 5
В статье рассматривается оценка качества сезонного торгового подхода на дневном таймфрейме — как для отдельных символов, так и для спредов. Особое внимание уделяется выявлению повторяющихся месячных циклов и возможностям их применения в торговле в рамках текущего года.
Тестирование надежности торговых советников Тестирование надежности торговых советников
При разработке стратегии необходимо учитывать множество сложных деталей, на многие из которых не обращают особого внимания начинающие трейдеры. В результате многим трейдерам, включая меня, пришлось усвоить эти уроки на собственном горьком опыте. Данная статья основана на моих наблюдениях за распространенными подводными камнями, с которыми сталкивается большинство начинающих трейдеров при разработке стратегий на MQL5. В ней представлен ряд советов, хитростей и примеров, которые помогут определить причину дисквалификации советника и протестировать надежность наших собственных советников простым в применении способом. Цель состоит в том, чтобы обучить читателей, помогая им избежать мошенничества в будущем при покупке советников, а также предотвратить ошибки при разработке собственной стратегии.
Нейросети в трейдинге: Вероятностное прогнозирование временных рядов (Окончание) Нейросети в трейдинге: Вероятностное прогнозирование временных рядов (Окончание)
Приглашаем вас познакомиться с фреймворком K²VAE и вариантом интеграции предложенных подходов в торговую систему. Вы узнаете, как гибридный подход Koopman–Kalman–VAE помогает строить адаптивные и интерпретируемые модели. А в завершении статьи представлены практические результаты использования реализованных решений.