preview
Реализация модели гауссовской смеси в MQL5

Реализация модели гауссовской смеси в MQL5

MetaTrader 5Статистика и анализ |
69 0
Evgeniy Chernish
Evgeniy Chernish

Введение

Модель смеси нормальных распределений (Gaussian Mixture Model, GMM) — один из самых ярких и классических примеров машинного обучения без учителя (unsupervised learning). Это параметрическая вероятностная модель, предполагающая, что наблюдаемые данные порождены взвешенной суммой нескольких гауссовых распределений.

GMM

где:

  • πk — коэффициенты смешивания k-го компонента смеси (∑πk = 1),
  • μk — вектор математического ожидания k-го компонента,
  • Σk — ковариационная матрица k-го компонента,
  • N — многомерная нормальная плотность.

В отличие от классических алгоритмов кластеризации (например, k-means), GMM не просто делит данные на группы, а моделирует вероятностную структуру данных. Благодаря этому каждая точка может в разной степени принадлежать сразу нескольким кластерам.

Реальные данные не всегда укладываются в одно нормальное распределение. Часто они образуют несколько групп (кластеров) с разными центрами, формами и ориентацией в пространстве. В качестве примера на рис. 1 схематически изображены три нормальные компоненты в двухмерном пространстве.

Mixture 2D

Рис. 1 Смесь трех нормальных распределений в двухмерном пространстве: a) изолинии постоянной плотности для каждой из компонент смеси (значения коэффициентов смешивания показаны под каждым компонентом), b) изолинии маргинальной плотности вероятности p(x) распределения смеси

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

На сегодняшний день в открытом доступе нет реализации модели гауссовской смеси на языке MQL5. В данной статье мы попытаемся разобраться в самом устройстве модели, рассмотрим заложенные в нее предпосылки и напишем класс CGMM на языке MQL5. Это позволит решать задачи кластеризации, многомерного моделирования плотности вероятности и генерации данных непосредственно в MetaTrader 5, без необходимости постоянно возвращаться к Python.

Ключевые особенности реализации:

  • Поддержка двух режимов обучения: MLE (максимальное правдоподобие) и MAP (максимальная апостериорная вероятность),
  • Инициализация параметров с помощью K-Means++ (через интеграцию с библиотекой Alglib) или случайным способом,
  • Работа с полными ковариационными матрицами, что позволяет улавливать любые линейные взаимосвязи между признаками,
  • По аналогии с библиотекой scikit-learn реализованы методы жесткой (Predict) и мягкой кластеризации (PredictProba), генерации синтетических выборок (Sample), расчета логарифмического правдоподобия (ScoreSamples), а также информационных критериев (AIC/BIC) для подбора оптимального числа кластеров.

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


Смесь нормальных распределений в терминах латентных переменных

Предположим, у нас есть выборка из N независимых и одинаково распределенных наблюдений {х, …, xN}, которую можно представить в виде матрицы X размера N×D, где n-я строка — это вектор xn. Наша задача описать эти данные с помощью модели смеси нормальных распределений. Геометрическая форма и свойства такой смеси полностью определяются набором параметров θ = (π, μ, Σ):

  • π = (π1, …, πk),
  • μ = (μ1, …, μk),
  • Σ = (Σ1, …, Σk).

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

LL

Из-за суммы по компонентам внутри логарифма аналитически получить оценки параметров невозможно. Чтобы обойти эту трудность,  модель GMM представляют в терминах латентных переменных. Это позволяет использовать для обучения ЕМ-алгоритм и находить удобные аналитические формулы для вычисления параметров. Для этого вводят К-мерную бинарную переменную z (вектор), имеющую представление 1 из К, где znk = 1, если n-е наблюдение порождено k-м компонентом, и 0 — в противном случае.

Такое представление модели удобно изобразить в виде вероятностного графа (рис.2)

Graf GMM

Рис. 2 GMM, представленная в виде графа для набора из N данных x с соответствующими скрытыми переменными z

На графе:

  • Светлый кружок - скрытые переменные zn,
  • Черный кружок - наблюдаемые переменные xn,
  • Параметры модели показаны без кружков (это не случайные величины, а константы, хоть и не известные).

Модель предполагает, что все наблюдения независимы (i.i.d.). Это хорошо видно на графе: каждое наблюдение xn зависит только от своей скрытой переменной zn и не зависит от других наблюдений.

Примечание:Если скрытая переменная zn зависит от своего предыдущего значения zn-1, то такая модель называется Скрытой Марковской Моделью (HMM). О том, что из себя представляет эта модель, вы можете прочитать в моей предыдущей статье, где рассматривается важный частный случай категориальной (дискретной) модели наблюдений.


EM – алгоритм для GMM

EM-алгоритм (Expectation-Maximization) — это итеративный метод нахождения оценок параметров (MLE/MAP) в вероятностных моделях со скрытыми переменными. Основная идея заключается в переходе от оптимизации сложного маргинального правдоподобия (прямое дифференцирование которого аналитически невозможно) к максимизации математического ожидания логарифма совместного распределения p(x, z), что существенно упрощает расчеты.

Рассмотрим ключевые распределения, необходимые для понимания работы EM-алгоритма в частном случае модели GMM.

Априорное распределение скрытой переменной z:

p(z)

Условное распределение наблюдений х при известной скрытой переменной z:

p(x | z)

Совместное распределение р(х, z) задается выражением р(х, z) = p(x|z)p(z), а маргинальное распределение х (то, что мы и называем GMM) получается суммированием совместного распределения по всем возможным состояниям z:

p(x)

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

Еще одно важное распределение — это апостериорные вероятности принадлежности каждой точки к компонентам — так называемые ответственности (responsibilities) γ(znk). Их можно найти, используя теорему Байеса:

responsibilities

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

Теперь введем понятие полного множества данных — {X, Z}. Если бы мы знали истинные значения скрытых переменных, то логарифм правдоподобия полных данных (complete-data log-likelihood) имел бы простой вид:

Complete Data LL

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

Q_ELBO

Отсюда и название первого шага EM-алгоритма - expectation (математическое ожидание). Подставив апостериорные вероятности в эту формулу, получаем:

Q GMM

Вспомогательную функцию Q еще называют нижней границей маргинального правдоподобия (ELBO – evidence lower bound). Доказывается, что если максимизировать эту нижнюю границу, то тем самым мы максимизируем и маргинальное правдоподобие.

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

Таким образом EM-алгоритм состоит из следующих повторяющихся шагов:

  • Выбираем начальные значения параметров θ_old,
  • E-шаг (Expectation) — вычисляем апостериор скрытых переменных,
  • M-шаг (Maximization) — максимизируем функцию Q и получаем обновленные параметры θ_new,
  • Проверяем сходимость логарифмической функции правдоподобия. Если критерий сходимости не выполняется, то θ_new → θ_old и возвращаемся к шагу 2.


Оценка параметров по методу максимального правдоподобия ( MLE )

Цель метода максимального правдоподобия — найти такие параметры модели θ = {π, μ, Σ}, при которых наблюдаемые данные X обладают наибольшей вероятностью. Математически эта задача сводится к максимизации логарифма функции правдоподобия.

После инициализации начальных параметров и выполнения Е-шага — расчета апостериорных вероятностей скрытых состояний γ(z), которые из-за дискретности латентных переменных удобно представлять в виде матрицы (таблицы чисел), — алгоритм переходит к М-шагу. На этом этапе оптимизация параметров существенно упрощается, так как мы работаем со вспомогательной функцией Q, чьи производные легко вычисляются в явном аналитическом виде.

Математическое ожидание μk:

MLE mean_k

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

Ковариация  Σk:

MLE sigma_k

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

Коэффициенты смешивания:

MLE pi_k

Это просто отношение числа точек, распределенных в k-й кластер, к общему количеству наблюдений в выборке.


Оценка параметров по методу максимальной апостериорной вероятности (MAP)

Метод максимального правдоподобия часто сталкивается с численной нестабильностью, особенно при высокой размерности данных (D > 20) или небольших выборках. Основная проблема — появление вырожденных или плохо обусловленных ковариационных матриц.

Хорошим решением является переход от MLE к MAP-оценке. При этом мы остаёмся в рамках того же EM-алгоритма, но добавляем априорные распределения на параметры модели. Поскольку главная нестабильность возникает именно в ковариационных матрицах, изменения затрагивают только формулу их обновления на M-шаге. E-шаг остаётся полностью без изменений.

В качестве сопряженного априорного распределения для математического ожидания и ковариации рассматривают нормальное обратное распределение Вишарта (NIW). Благодаря свойству сопряженности априора, нет необходимости программировать эту сложную плотность распределения. Весь байесовский вывод на практике сводится к слегка модифицированной оценке MLE на M-шаге:

MAP sigma_k

где,

  • S – априорная ковариация нормального обратного распределения Вишарта. Задавать её можно по-разному, но в качестве надежного и адаптивного варианта предлагается использовать масштабированную диагональную матрицу дисперсий исходных признаков:

S prior NIW distribution

где, sd – эмпирическая дисперсия признака d, вычисленная по всей обучающей выборке.

  • η (eta) — параметр информативности априорного распределения Вишарта. Самое слабое информативное распределение получается, если положить η = D + 2 (чем больше η, тем сильнее регуляризация).
  • Nk - фактическое количество точек, назначенных кластеру k,
  • D - размерность пространства признаков,
  • K - количество компонентов смеси.

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


Класс CGMM

Вся описанная выше математика реализована в виде  класса CGMM.

//+------------------------------------------------------------------+
//|                                                          GMM.mqh |
//|                                                           Eugene |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Eugene"
#property link      "https://www.mql5.com"
#include <Math\Stat\Math.mqh>
#include <Math\Stat\Normal.mqh>
#include <Math\Alglib\alglib.mqh>

//--- тип оценки параметров модели
enum ENUM_EM_MODE
  {
   MLE,
   MAP   
  };

enum ENUM_INIT_MODE
  {
   KMEANS,  // Инициализация центров с помощью K-Means++ (Alglib)
   RANDOM   // Инициализация центров случайными числами 
  };

//+------------------------------------------------------------------+
//| Класс Смеси Гауссовых Распределений (Gaussian Mixture Model)     |
//+------------------------------------------------------------------+
class CGMM
  {
private:
   int               m_k;             // Количество кластеров (K)
   ulong             m_dim;           // Размерность признаков (D)
   bool              m_is_fitted;     // Флаг успешного обучения модели

   vector            m_pi;            // Веса кластеров (априоры) [K]
   matrix            m_mu;            // Средние значения кластеров [K][D]
   matrix            m_sigma[];       // Матрицы ковариации [K][D x D]

   ENUM_EM_MODE      m_estimate;      // Режим оценки параметров (MLE или MAP)
   ENUM_INIT_MODE    m_init_mode;     // Режим инициализации средних (KMEANS или RANDOM)
   double            m_threshold;     // Порог схождения EM
   int               m_max_iters;     // Максимальное число итераций

   //--- Гиперпараметры для MAP оценки 
   matrix            m_S;             // Масштабная матрица априорного распределения
   double            m_eta;           // Степени свободы априорного распределения

   void              Initialize_parameters(const matrix &X);

   bool              e_step(const matrix &X, matrix &responsibilities);       // Е-шаг EM алгоритма
   bool              m_step(const matrix &X, const matrix &responsibilities); // М-шаг EM алгоритма
   double            log_likelihood(const matrix &X); // Функция правдоподобия данных X

public:

   vector            logliks; // история правдоподобия
   int               n_iter_; // количество итераций выполненных EM алгоритмом

                     CGMM(int components, ENUM_EM_MODE estimate=MLE, double tol=1e-5, int max_iter=100,ENUM_INIT_MODE mode = KMEANS);
                    ~CGMM(void) {};

   // Методы позволяющие установить стартовые значения параметров перед обучением модели
   bool              SetInitialWeights(const vector &pi);
   bool              SetInitialMeans(const matrix &means);
   bool              SetInitialCovars(const matrix &covars[]);

   // Главные методы
   bool              Fit(const matrix &X,bool verbose=false); // Обучение модели
   vector            Predict(const matrix &X);                // Вектор меток кластеров [N]
   matrix            PredictProba(const matrix &X);           // Матрица апостериорных вероятностей кластеров [N x K]
   bool              Sample(const int n_samples, matrix &X, vector &labels); // Генерация выборок
   vector            ScoreSamples(const matrix &X);           // Вектор логарифма плотности вероятности [N]
   double            AIC(const matrix &X);                    // Информационный критерий Акаике
   double            BIC(const matrix &X);                    // Байесовский информационный критерий

   // Геттеры для доступа к параметрам модели
   vector            GetWeights(void)       { return m_pi; }
   matrix            GetMeans(void)         { return m_mu; }
   void              GetCovars(matrix &out_sigma[]);
  };
Основные методы класса:
  • Fit(const matrix &X, bool verbose) — Обучает модель на данных X с помощью EM-алгоритма. Параметр verbose позволяет выводить ход обучения (значения логарифма правдоподобия),
  • Predict(const matrix &X) — Выполняет жёсткую кластеризацию (Hard Clustering). Возвращает вектор размера N с индексами кластеров (от 0 до K-1) для каждой точки,
  • PredictProba(const matrix &X) — Выполняет мягкую кластеризацию (Soft Clustering). Возвращает матрицу размером N×K с апостериорными вероятностями принадлежности каждой точки к каждому компоненту. Сумма по каждой строке равна 1,
  • ScoreSamples(const matrix &X) — Возвращает вектор логарифма маргинального правдоподобия для каждого наблюдения,
  • Sample(const int n_samples, matrix &X, vector &labels) — Генерирует синтетические данные вместе с истинными метками компонентов,
  • AIC(const matrix &X) и BIC(const matrix &X) — информационные критерии Акаике и Байесовский. Используются для выбора оптимального количества кластеров.

Перед запуском EM-алгоритма необходимо задать начальные значения параметров. Инициализация параметров, если она не была задана пользователем вручную, происходит автоматически с помощью приватного метода Initialize_parameters(const matrix &X).

//+------------------------------------------------------------------+
//| Инициализация параметров                                         |
//+------------------------------------------------------------------+
void CGMM::Initialize_parameters(const matrix &X)
  {
   m_dim = X.Cols();

   // --- 1. Инициализация весов (pi) ---
   //--- Если веса не заданы пользователем заранее, заполняем их равномерно
   if(m_pi.Size() != (ulong)m_k)
     {
      m_pi.Init(m_k);
      m_pi.Fill(1.0 / m_k);
     }

   // --- 2. Инициализация средних (mu) ---
   //--- Если центры кластеров не заданы, вычисляем их через K-Means++ или RANDOM
   if(m_mu.Rows() != (ulong)m_k || m_mu.Cols() != m_dim)
     {
      m_mu.Init((ulong)m_k, m_dim);

      if(m_init_mode == KMEANS)
        {
         CClusterizerState s;
         CKmeansReport rep;
         CMatrixDouble x;
         x = X; 

         CAlglib::ClusterizerCreate(s);
         CAlglib::ClusterizerSetPoints(s, x, 2);        // 2 - Евклидова метрика
         CAlglib::ClusterizerSetKMeansLimits(s, 1, 0);  // 1 рестарт, без лимита итераций
         CAlglib::ClusterizerRunKMeans(s, m_k, rep);    // Запуск на m_k кластеров

         if(rep.m_terminationtype > 0)
            m_mu = rep.m_c.ToMatrix(); // скопировали центры из K-Means++
         else
            m_init_mode = RANDOM; // Если Alglib выдал ошибку, переключаемся на случайный старт
        }

      if(m_init_mode == RANDOM)
        {
         vector zero_mean = vector::Zeros(m_dim);
         matrix identity = matrix::Identity(m_dim, m_dim);
         CMVN::rnd(zero_mean, identity, m_k, m_mu); // Случайные центры
        }
     }

   // --- 3. Инициализация матриц ковариации (Sigma) ---
   // Если матрицы не заданы, берем дисперсию признаков всей выборки
   if(ArraySize(m_sigma) != m_k)
     {
      ArrayResize(m_sigma, m_k);
      vector var_ = X.Var(0);
      matrix diag_variance;
      diag_variance.Diag(var_); // Создаем диагональную матрицу дисперсий     
      for(int i = 0; i < m_k; i++)
         m_sigma[i] = diag_variance;
     }
  }
  • Коэффициенты смешивания - инициализируются равномерно,
  • Центры кластеров

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

  1. KMEANS (рекомендуемый) — центры вычисляются с помощью алгоритма K-Means++ (библиотека Alglib). Этот метод значительно снижает вероятность попадания в неудачный локальный максимум и ускоряет обучение модели.
  2. RANDOM — центры генерируются случайно из многомерного нормального распределения с нулевым математическим ожиданием и единичной ковариационной матрицей.
Если при работе K-Means++ произошла ошибка, класс автоматически переключается на случайную инициализацию.
  • Ковариационные матрицы - инициализируются одинаково для всех компонентов как диагональные матрицы, где на главной диагонали стоят эмпирические дисперсии каждого признака, рассчитанные по всей выборке X.

Метод обучения модели — Fit()

//+------------------------------------------------------------------+
//| Обучение модели                                                  |
//+------------------------------------------------------------------+
bool CGMM::Fit(const matrix &X, bool verbose=false)
  {
   m_dim = X.Cols();
   m_is_fitted = false;

// --- расчет гиперпараметров S и eta при использовании оценки MAP ---
   if(m_estimate == MAP)
     {
      m_eta = (double)m_dim + 2.0;
      vector var = X.Var(0);
      m_S.Init(m_dim, m_dim);
      m_S.Fill(0.0);
      double k_correction = MathPow((double)m_k, 1.0 / (double)m_dim);
      for(ulong d = 0; d < m_dim; d++)
        {
         m_S[d, d] = var[d] / k_correction;
        }
     }

// --- Инициализация параметров---
   Initialize_parameters(X);

// --- Основной цикл EM-алгоритма ---
   logliks.Init(m_max_iters);
   logliks.Fill(0.0);
   double LL = log_likelihood(X);
   double prev_LL = 0.0;
   matrix responsibilities;
   int iter = 0;

   if(verbose)
      Print("   Iteration      LogLikelihood");

   while(iter < m_max_iters)
     {
      prev_LL = LL;
      if(!e_step(X, responsibilities)) // E-шаг
         return false;
      if(!m_step(X, responsibilities)) // M-шаг
         return false;

      LL = log_likelihood(X);
      logliks[iter] = LL;

      if(verbose)
         PrintFormat("     %6d   %15.5f", iter + 1, LL);

      iter++;
      n_iter_ = iter;

      if(MathAbs(LL / prev_LL - 1.0) < m_threshold)
        {
         if(verbose)
            PrintFormat("EM converged at %d iterations. Final Log-L value: %.5f", iter, LL);
         m_is_fitted = true;
         break;
        }
     }

   if(iter >= m_max_iters)
     {
      m_is_fitted = true;
      if(verbose)
         Print("Max number of EM iterations reached. Try different init parameters or increase max_iter.");
     }

   logliks.Resize(iter);

   return m_is_fitted;
  }

Если пользователем выбран режим оценки параметров MAP, автоматически рассчитываются гиперпараметры S и eta. После инициализации параметров запускается цикл EM-алгоритма. Проверяется условие остановки: если относительное изменение логарифма правдоподобия становится меньше порога m_threshold (по умолчанию 1e-5), обучение завершается. E и M шаги вынесены в отдельные функции.

//+------------------------------------------------------------------+
//| Е-Шаг EM-алгоритма  (E-step)                                     |
//+------------------------------------------------------------------+
bool CGMM::e_step(const matrix &X, matrix &responsibilities)
  {
   ulong N = X.Rows();
   responsibilities.Init(m_k, N); // Инициализируем матрицу апостериорных вероятностей [K x N]

// Шаг 1: считаем числитель в формуле Байеса для всех K компонентов (pi_k * PDF_k)
   for(int k = 0; k < m_k; k++)
     {
      vector pdf_k;
      if(!CMVN::pdf(X, m_mu.Row(k), m_sigma[k], pdf_k))
         return false;
      responsibilities.Row(pdf_k * m_pi[k], k); 
     }

// Шаг 2: суммируем по столбцам, получая знаменатель формулы Байеса для каждой точки
   vector likelihood_old = responsibilities.Sum(0);
   likelihood_old.Clip(1e-300, DBL_MAX); // Защита от деления на ноль для аномальных точек данных


// Шаг 3: Нормировка
   for(int k = 0; k < m_k; k++)
      responsibilities.Row(responsibilities.Row(k) / likelihood_old, k);

   return true;
  }

Результатом E-шага является матрица ответственностей размером K × N.

//+------------------------------------------------------------------+
//| M-Шаг EM-алгоритма (M-step)                                      |
//+------------------------------------------------------------------+
bool CGMM::m_step(const matrix &X, const matrix &responsibilities)
  {
   ulong N = X.Rows();

   for(int k = 0; k < m_k; k++)
     {
      vector resp_k = responsibilities.Row(k);
      double Nk = resp_k.Sum();

      //--- 1. Обновляем средние
      m_mu.Row((resp_k @ X) / Nk, k);

      //--- 2. Считаем взвешенную Sigma_k
      matrix W_dk(N, m_dim);
      for(ulong n = 0; n < N; n++)
         W_dk.Row((X.Row(n) - m_mu.Row(k))* MathSqrt(resp_k[n]), n);

      matrix Sigma_k = W_dk.Transpose() @ W_dk;

      //--- 3. Обновляем матрицы ковариации
      if(m_estimate == MLE)
        {
         m_sigma[k] = Sigma_k / Nk;
        }
      else
         if(m_estimate == MAP)
           {
            m_sigma[k] = (m_S + Sigma_k) / (m_eta + Nk + m_dim + 2.0);
           }

      //--- 4. Обновляем веса
      m_pi[k] = Nk / (double)N;
     }
   return true;
  }

Результат M-шага – это обновленные параметры модели.

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

//+------------------------------------------------------------------+
//| Предсказание кластеров (Hard Clustering)                         |
//+------------------------------------------------------------------+
vector CGMM::Predict(const matrix &X)
  {
   vector labels(X.Rows());
   if(!m_is_fitted)
     {
      Print("Ошибка: Модель не обучена!");
      return labels;
     }

   matrix resp = PredictProba(X); // матрица вероятностей размера [N x K]
   labels = resp.ArgMax(1); // ArgMax(1) находит индекс максимального значения в каждой строке

   return labels; // Возвращает вектор размера [N], содержащий номера кластеров (0, 1, ... K-1)
  }

//+------------------------------------------------------------------+
//| Мягкое предсказание кластеров (Soft Clustering)                  |
//| Возвращает матрицу размера [N x K], где:                         |
//| N - количество точек данных (строк)                              |
//| K - количество кластеров (вероятности для каждого компонента)    |
//+------------------------------------------------------------------+
matrix CGMM::PredictProba(const matrix &X)
  {
   matrix resp(X.Rows(),m_k);
   if(!m_is_fitted)
     {
      Print("Ошибка: Модель не обучена!");
      return resp;
     }
     
   e_step(X, resp); // [K x N]

   return resp.Transpose(); //  [N x K]
  }

//+------------------------------------------------------------------+
//|Маргинальное правдоподобие                                        |
//+------------------------------------------------------------------+
double CGMM::log_likelihood(const matrix &X)
  {
  //--- Возвращает логарифм плотности вероятности для всех данных выборки X
   return ScoreSamples(X).Sum(); // суммируем вектор логарифмов
  }

//+------------------------------------------------------------------+
//| Log-Likelihood для каждого наблюдения                            |
//+------------------------------------------------------------------+
vector CGMM::ScoreSamples(const matrix &X)
  {
   ulong N = X.Rows();
   matrix joint_density(m_k, N); // Матрица совместной плотности p(x,z) размера [K x N]

//--- Шаг 1: Вычисляем совместную плотность p(x,z) = p(x|z) * p(z)
   for(int k = 0; k < m_k; k++)
     {
      vector pdf_k;
      if(!CMVN::pdf(X, m_mu.Row(k), m_sigma[k], pdf_k))
         Print("Ошибка в CMVN::pdf при расчете компонента ", k);
         
      //--- Умножаем условную плотность p(x|z) на априорную вероятность скрытого состояния p(z)
      joint_density.Row(pdf_k * m_pi[k], k);
     }
//--- Шаг 2: Маргинализация — суммируем по всем состояниям z, получая маргинальную плотность p(x)
   vector likelihood = joint_density.Sum(0); // p(xn) = p(xn,z=0) + p(xn,z=1) + ⋯ + p(xn,z=K−1)
  
   likelihood.Clip(1e-300, DBL_MAX);// Защита от получения логарифма бесконечности для аномальных точек

//--- Шаг 3: Берем натуральный логарифм от плотности вероятности
   likelihood = MathLog(likelihood);

   return likelihood; // Возвращает вектор логарифмов правдоподобия размера [N]
  }

Функция маргинального правдоподобия вычисляется для контроля сходимости EM-алгоритма. Для удобства и лучшей совместимости с интерфейсом библиотеки scikit-learn вычисление правдоподобия разделено на два метода. ScoreSamples возвращает вектор логарифмических плотностей вероятности для каждого наблюдения, а метод log_likelihood — суммарное маргинальное логарифмическое правдоподобие по всей выборке.


Реализация многомерного нормального распределения (Класс CMVN)

Реализация модели смеси нормальных распределений была бы технически невозможна без методов для работы с многомерным нормальным распределением (Multivariate Normal Distribution, MVN). Поскольку встроенных средств для работы с многомерными распределениями в MQL5 пока нет, был написан  класс CMVN.

//+------------------------------------------------------------------+
//| Многомерное нормальное распределение                             |
//+------------------------------------------------------------------+
class CMVN
  {
public:
   //+------------------------------------------------------------------+
   //| Генерация случайных векторов (генерация выборки)                 |
   //| INPUT:                                                           |
   //|   Mu    - вектор математических ожиданий размера [d]             |
   //|   Sigma - ковариационная матрица размера [d x d]                 |
   //|   N     - количество генерируемых наблюдений                     |
   //| OUTPUT:                                                          |
   //|   R     - матрица сгенерированных данных [N x d]                 |
   //+------------------------------------------------------------------+
   static bool       rnd(const vector &Mu, const matrix &Sigma, const int N, matrix &R)
     {
      ulong d = Sigma.Rows();

      if(N < 1 || d < 1)
         return false;
      if(Mu.Size() != d || Sigma.Cols() != d || !Sigma.IsSymmetric())
         return false;

      //--- Разложение Холецкого
      matrix L;
      if(!Sigma.FactorizationCholesky(L))
        {
         Print("Ошибка mvnrnd: Матрица ковариации не является положительно определенной");
         return false;
        }

      int total_elements = (int)(N * d);
      double rand_array[];

      if(!MathRandomNormal(0.0, 1.0, total_elements, rand_array))
         return false;

      matrix Z(N, d);
      for(int n = 0; n < N; n++)
        {
         vector row_vec(d);
         for(ulong j = 0; j < d; j++)
           {
            row_vec[j] = rand_array[n * d + j];
           }
         Z.Row(row_vec, n);
        }

      //--- Линейное преобразование и добавление математического ожидания
      R = Z @ L.Transpose();
      for(int n = 0; n < N; n++)
         R.Row(R.Row(n) + Mu, n);

      return true;
     }

   //+------------------------------------------------------------------+
   //| Расчет плотности вероятности (Probability Density Function)      |
   //| INPUT:                                                           |
   //|   X     - матрица данных размера [N x D]                         |
   //|   Mu    - вектор математических ожиданий размера [D]             |
   //|   Sigma - ковариационная матрица размера [D x D]                 |
   //| OUTPUT:                                                          |
   //|   y     - вектор плотности вероятности размера  [N]              |
   //+------------------------------------------------------------------+
   static bool       pdf(const matrix &X, const vector &Mu, const matrix &Sigma, vector &y)
     {
      ulong N = X.Rows();
      ulong D = X.Cols();

      // --- Проверки размеров ---
      if(N < 1 || D < 1)
        {
         Print("Ошибка mvnpdf: Матрица X пуста");
         return false;
        }
      if(Mu.Size() != D)
        {
         Print("Ошибка mvnpdf: Размер вектора средних должен соответствовать количеству столбцов X");
         return false;
        }
      if(Sigma.Rows() != D || Sigma.Cols() != D)
        {
         Print("Ошибка mvnpdf: Матрица ковариации должна быть квадратной размера D x D");
         return false;
        }

      // --- Проверка на симметричность ---
      if(!Sigma.IsSymmetric())
        {
         Print("Ошибка mvnpdf: Матрица ковариации должна быть симметричной");
         return false;
        }

      // --- Разложение Холецкого 
      matrix L; // Нижнетреугольная матрица L (Sigma = L * L^T)
      if(!Sigma.FactorizationCholesky(L))
        {
         Print("Ошибка mvnpdf: Матрица ковариации должна быть положительно определенной");
         return false;
        }

      // --- Центрирование данных (X0 = X - Mu) 
      matrix X0(N, D);
      for(ulong n = 0; n < N; n++)
         X0.Row(X.Row(n) - Mu, n);

      // --- Решение треугольной системы уравнений (LAPACK TRTRS) 
      matrix B = X0.Transpose(); // Размер D x N
      matrix X_sol;              // Решение размера D x N

      if(!L.LinearEquationsSolutionTriangular(EQUATIONSFORM_N, B, X_sol))
        {
         Print("Ошибка mvnpdf: Не удалось решить треугольную систему уравнений");
         return false;
        }

      // --- Вычисление логарифма квадратного корня из определителя 
      vector diagL = L.Diag();
      double logSqrtDetSigma = 0.0;
      logSqrtDetSigma =  MathLog(diagL).Sum();

      // --- Вычисление квадратичной формы 
      vector quadform = (X_sol * X_sol).Sum(0);

      // --- Итоговый расчет плотности---
      //--- Считаем вектор логарифмов плотностей
      vector logPdf = -0.5 * quadform - logSqrtDetSigma - (D * MathLog(2.0 * M_PI)) / 2.0;
      //--- Берем экспоненту от каждого элемента вектора logPdf
      y = MathExp(logPdf);
      return true;
     }
  };

Класс содержит две функции, необходимые для работы GMM:

  • pdf — расчёт плотности вероятности многомерного нормального распределения,
  • rnd — генерация случайных векторов из многомерного нормального распределения.

При разработке класса CMVN основной акцент был сделан на производительности, поскольку функция плотности вероятности вызывается многократно на каждой итерации EM-алгоритма. Для этого используются методы из высокопроизводительной библиотеки линейной алгебры OpenBLAS:

  • FactorizationCholesky — разложение Холецкого матрицы ковариации и проверки ее на положительную определенность (аналог LAPACK POTRF),
  • LinearEquationsSolutionTriangular — решение треугольных систем линейных уравнений(аналог LAPACK TRTRS) для ускоренного расчета квадратичной формы без явного вычисления обратной матрицы.
Класс CMVN можно использовать независимо от GMM, в задачах, где требуется вычисление нормальной многомерной плотности или генерация зависимых (коррелированных) случайных величин.


Сравнение результатов обучения с моделью scikit-learn

Для проверки корректности реализации класса CGMM проведено сравнение с моделью GaussianMixture из библиотеки scikit-learn (Python).

В качестве тестового набора использован классический датасет Old Faithful («Старый Служака») — данные об извержениях гейзера в Йеллоустонском национальном парке США. Набор содержит 272 наблюдения и две переменные:

  • длительность извержения (в минутах),
  • время до следующего извержения (в минутах).

На рис. 3 хорошо видно, что данные имеют два чётко выраженных кластера.

Old Faithful

Рис. 3 Зависимость времени до следующего извержения от продолжительности текущего извержения (датасет Old Faithful)

Данные были загружены, стандартизированы и переданы в модель с числом компонентов K = 2. Обучение происходило в режиме MLE.

//+------------------------------------------------------------------+
//|                                                     Learning.mq5 |
//|                                                           Eugene |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Eugene"
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <GMM\GMM.mqh>
#include <Graphics\Graphic.mqh>


//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
// 1. Загрузка датасета Old Faithful (272 наблюдения, 2 признака)
   matrix X;
   CSVtoMatrix("GMM/OldFaithful.csv",X);
   ulong N = X.Rows();
   ulong D = X.Cols();
   
// 2. Нормализация данных
   vector mean_ = X.Mean(0);
   vector std_ = X.Std(0);
   matrix X_norm(N, D);
   for(ulong i = 0; i < N; i++)
      X_norm.Row((X.Row(i) - mean_) / std_, i);

// 3. Создаем модель
   int components = 2;
   CGMM gmm_model(components,MLE);
// CGMM gmm_model(components,MAP, 1e-5, 100);

// 4. Инициализация параметров через сеттеры
// --- Начальные веса ---
   vector init_pi = {0.5, 0.5};
   gmm_model.SetInitialWeights(init_pi);

// --- Начальные центры средних ---
   matrix init_means = {{-1.5, 1.5}, {1.5, -1.5}};
   gmm_model.SetInitialMeans(init_means);

// --- Начальные матрицы ковариации (0.1 * Identity) ---
   matrix m = {{0.1, 0.0}, {0.0, 0.1}};
   matrix init_covars[2];
   init_covars[0] = m;
   init_covars[1] = m;
   gmm_model.SetInitialCovars(init_covars);

// 5. Запуск обучения
   if(gmm_model.Fit(X_norm,true))
     {
      Print("=== The model has been successfully trained! ===");

      // 1. Веса после обучения
      vector final_pi = gmm_model.GetWeights();
      Print("Final weights (pi): ", final_pi);

      // 2. Средние и матрицы ковариации
      matrix final_means = gmm_model.GetMeans();
      matrix final_covars[];
      gmm_model.GetCovars(final_covars);

      for(int k = 0; k < components; k++)
        {
         PrintFormat("--- Cluster %d ---", k);
         Print("Mean (mu): ", final_means.Row(k));
         Print("Covariance (Sigma):\n", final_covars[k]);
        }

      Print("------------------------------------------------");

      //--- Нарезаем подвыборку из первых 5 точек
      matrix first_five(5, D);
      for(ulong i=0; i<5; i++)
         first_five.Row(X_norm.Row(i), i);

      // --- Hard clustering 
      vector predicted_labels = gmm_model.Predict(first_five);
      Print("Predicted clusters for the first 5 points:\n ", predicted_labels);

      // --- Soft clustering 
      matrix predicted_probas = gmm_model.PredictProba(first_five);
      Print("Probability matrix [5 x 2] (rows - data, columns - cluster probabilities):\n", predicted_probas);

      vector score_samples = gmm_model.ScoreSamples(X_norm);
      //Print("Сумма лог-правдоподобия (Total Log-L): ", score_samples.Sum());

      // --- Информационные критерии 
      Print("================================================");
      Print("=== INFORMATION CRITERIA OF THE MODEL ===");

      double aic_value = gmm_model.AIC(X_norm);
      double bic_value = gmm_model.BIC(X_norm);

      PrintFormat("AIC (Akaike Information Criterion): %.5f", aic_value);
      PrintFormat("BIC (Bayesian Information Criterion): %.5f", bic_value);
      Print("================================================");
     }
   else
     {
      Print("Error: Failed to train GMM model");
     }
   VisualizeGMMTraining(gmm_model);
  }
//+------------------------------------------------------------------+
//| Визуализация кривой обучения GMM                                 |
//+------------------------------------------------------------------+
void VisualizeGMMTraining(CGMM &gmm_model)
  {

   int plotLimit = (int)gmm_model.logliks.Size();
   double x[], y_[];
   ArrayResize(x, plotLimit);
   ArrayResize(y_, plotLimit);

   for(int i = 0; i < plotLimit; i++)
     {
      x[i] = i;
      y_[i] = gmm_model.logliks[i];
     }
   ChartSetInteger(0, CHART_SHOW, false);
   CGraphic graphic;
   ulong width = ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   ulong height = ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   graphic.Create(0, "GMM_Training_Plot", 0, 0, 0, int(width), int(height));
   CCurve *curve = graphic.CurveAdd(x, y_, ColorToARGB(clrDodgerBlue, 255), CURVE_LINES, "LogLikelihood");
   graphic.BackgroundMainColor(ColorToARGB(clrBlack, 255));
   graphic.BackgroundMainSize(24);
   graphic.BackgroundMain("GMM EM Training Progress");
   graphic.XAxis().Name("Iteration");
   graphic.XAxis().NameSize(18);
   graphic.YAxis().Name("Log Likelihood");
   graphic.YAxis().NameSize(18);
   graphic.CurvePlotAll();
   graphic.Update();
   Sleep(10 * 1000);
   ChartSetInteger(0, CHART_SHOW, true);
   graphic.Destroy();
   ChartRedraw(0);
  }
//+------------------------------------------------------------------+

После установки начальных параметров (в данном примере они были заданы вручную) запускаем обучение с помощью метода Fit(). Для вывода в журнал значений маргинального правдоподобия в ходе обучения, установим аргумент verbose = true.

На рис. 4 представлены результаты обучения

EM training

Рис. 4 Кривая обучения модели GMM для данных Old Faithful

После обучения скрипт выводит в журнал:

  • оптимизированные значения параметров,
  • финальное значение функции правдоподобия,
  • метки кластеров для первых 5 точек (Hard Clustering),
  • матрицу вероятностей для первых 5 точек (Soft Clustering),
  • значение информационных критериев AIC/BIC.

Для проверки корректности работы нашего класса, мы сравним полученные результаты с эталонной библиотекой scikit-learn. Для этого был создан скрипт scikit.py, в котором использовались те же данные, то же количество компонентов, аналогичная инициализация и те же настройки обучения.

import numpy as np
from sklearn.mixture import GaussianMixture
import matplotlib.pyplot as plt

# 1. Читаем датасет Old Faithful 
file_path = r"C:\Program Files\MetaTrader 5\MQL5\Files\GMM\OldFaithful.csv"
X = np.genfromtxt(file_path, delimiter=',')

# 2. Нормализация данных
X_mean = X.mean(axis=0)
X_std = X.std(axis=0)
X_norm = (X - X_mean) / X_std

# 3. Стартовые параметры 
init_pi = np.array([0.5, 0.5])
init_mu = np.array([[-1.5, 1.5], [1.5, -1.5]])
init_Sigma = np.array([np.identity(2) * 0.1, np.identity(2) * 0.1])

# 4. Настройка и обучение Scikit-Learn GMM
gmm = GaussianMixture(
    n_components=2,
    covariance_type="full",
    tol=1e-5,
    max_iter=100,
    weights_init=init_pi,
    means_init=init_mu,
    precisions_init=np.linalg.inv(init_Sigma),  # Scikit требует матрицы точности
    verbose=0
)

gmm.fit(X_norm)

# Переводим средний Log-L в суммарный
total_log_likelihood = gmm.score(X_norm) * X_norm.shape[0]

print(f"=== Scikit-Learn: EM сошелся за {gmm.n_iter_} итераций ===")
print("Финальный Log-L:", total_log_likelihood)
print("Финальные веса (pi):", gmm.weights_)

for i in range(2):
    print(f"\n--- Кластер {i} ---")
    print("Среднее (mu):", gmm.means_[i])
    print("Ковариация (Sigma):\n", gmm.covariances_[i])

print("\n" + "-"*48)

# 5. Срез первых 5 точек для предсказания
first_five = X_norm[:5]

# Получаем жесткие метки (кластеры 0 или 1)
predicted_labels = gmm.predict(first_five)
print("Предсказанные кластеры для первых 5 точек:")
print(predicted_labels)

# Получаем вероятности (размер [5 x 2])
predicted_probas = gmm.predict_proba(first_five)
print("\nМатрица вероятностей [5 x 2] (строки - данные, столбцы - кластеры):")
print(np.round(predicted_probas, 10)) 

score_samples = gmm.score_samples(X_norm) 
#print("score_samples:")
#print(score_samples)

# --- Информационные критерии ---
print("\n" + "="*48)
print("=== ИНФОРМАЦИОННЫЕ КРИТЕРИИ МОДЕЛИ  ===")

aic_value = gmm.aic(X_norm)
bic_value = gmm.bic(X_norm)

print(f"AIC (Akaike Information Criterion): {aic_value:.5f}")
print(f"BIC (Bayesian Information Criterion): {bic_value:.5f}")
print("="*48)

# ==============================================================================
# ГРАФИК: Исходные данные
# ==============================================================================
plt.figure(figsize=(6, 4))
plt.scatter(X[:, 0], X[:, 1], c='blue', s=35, alpha=0.7, edgecolors='k', label='Data Old Faithful')
plt.title("Old Faithful")
plt.xlabel('Eruption Duration (minutes)')
plt.ylabel('Waiting Time (minutes)')
plt.grid(True, linestyle=':', alpha=0.6)
plt.legend()
plt.tight_layout()
plt.show()

# ==============================================================================
# ГРАФИК: Мягкая кластеризация (PredictProba) 
# ==============================================================================
# Получаем вероятности для всей выборки
all_probas = gmm.predict_proba(X_norm)

proba_cluster_1 = all_probas[:, 1]
plt.figure(figsize=(6, 4))
scatter = plt.scatter(X[:, 0], X[:, 1], c=proba_cluster_1, cmap='bwr', s=35, alpha=0.8, edgecolors='k')
plt.title("Soft Clustering of Old Faithful Dataset (PredictProba)")
plt.xlabel('Eruption Duration (minutes)')
plt.ylabel('Waiting Time (minutes)')
plt.grid(True, linestyle=':', alpha=0.6)

cbar = plt.colorbar(scatter)
cbar.set_label('Probability of Cluster 1', rotation=270, labelpad=15)

plt.tight_layout()
plt.show()

Параметры моделей, правдоподобие, предсказания, информационные критерии полностью совпали с реализацией на MQL5, с погрешностью в пределах 5–6 знаков после запятой.

Результат мягкой кластеризации данных с помощью метода PredictProba представлен на рис. 5

Soft Clustering OF

Рис. 5 Мягкая кластеризация с помощью метода PredictProba


Генерация выборок

Модель GMM является порождающей. Поэтому ее можно использовать не только для кластеризации или оценки плотности, но и для генерации новых данных. Для этого в классе CGMM реализован метод Sample().

//+------------------------------------------------------------------+
//| Генерация случайных выборок (траекторий) из обученной модели GMM |
//| INPUT:                                                           |
//|   n_samples - количество данных, которые нужно сгенерировать     |
//| OUTPUT:                                                          |
//|   sample_X  - матрица сгенерированных данных  [n_samples x D]    |
//|   labels    - вектор меток (индексов кластеров) [n_samples]      |
//+------------------------------------------------------------------+
bool CGMM::Sample(const int n_samples, matrix &sample_X, vector &labels)
  {
   if(n_samples <= 0)
     {
      Print("Ошибка: Количество выборок должно быть больше 0");
      return false;
     }

   int K = m_k;
   int D = (int)m_mu.Cols();

// --- Шаг 1: выбор кластеров для всех N точек  ---
   int cluster_indices[];
   ArrayResize(cluster_indices, K);
   for(int k = 0; k < K; k++)
      cluster_indices[k] = k;

   double pi[];
   VectorToArray(m_pi,pi);

   int z[];
   ArrayResize(z, n_samples);

   if(!MathSample(cluster_indices, pi, n_samples, true, z))
     {
      Print("Ошибка : Сбой при вызове MathSample");
      return false;
     }

   sample_X.Init(n_samples, D);
   labels.Init(n_samples);

   for(int s = 0; s < n_samples; s++)
      labels[s] = (double)z[s]; //--- переносим метки в выходной вектор

// --- Шаг 2: Генерация данных пакетами для каждого кластера с помощью mvnrnd 
   for(int k = 0; k < K; k++)
     {
      //--- Считаем, сколько точек данных выпало для текущего кластера k
      int samples_in_cluster = 0;
      for(int s = 0; s < n_samples; s++)
        {
         if(z[s] == k)
            samples_in_cluster++;
        }

      //--- Если для этого кластера есть точки, генерируем их одним пакетом
      if(samples_in_cluster > 0)
        {
         vector current_mu = m_mu.Row(k);
         matrix cluster_samples;

         if(!CMVN::rnd(current_mu, m_sigma[k], samples_in_cluster, cluster_samples))
           {
            PrintFormat("Ошибка: Сбой при генерации mvnrnd для кластера %d.", k);
            return false;
           }

         //--- Распределяем сгенерированный пакет по исходным индексам
         ulong current_row_idx = 0;
         for(int s = 0; s < n_samples; s++)
           {
            if(z[s] == k)
              {
               sample_X.Row(cluster_samples.Row(current_row_idx), s);
               current_row_idx++;
              }
           }
        }
     }

   return true;
  }

Процесс сэмплирования состоит из двух последовательных этапов:

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

  • Пакетная генерация координат: Для каждого компонента подсчитывается количество точек, которые должны быть из него сгенерированы. Затем с помощью метода CMVN::rnd() генерируется пакет векторов из соответствующего многомерного нормального распределения. Полученные точки затем распределяются по своим индексам в итоговой матрице sample_X.

На рис. 6 показана выборка 1000 точек на основе параметров полученных после обучения модели GMM на данных Old Faithful (скрипт Sample.mq5).

//+------------------------------------------------------------------+
//|                                                       Sample.mq5 |
//|                                                           Eugene |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Eugene"
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <GMM\GMM.mqh>
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   // 1. Создаем объект GMM (2 компонента)
   CGMM gmm(2);
   
   // 2. Настраиваем  параметры 
   vector init_pi = {0.35592792679548113, 0.6440720732045188};
   gmm.SetInitialWeights(init_pi);
   
   matrix init_means = {
      {-1.27384982, -1.20981836}, 
      {0.70395651,  0.66857136}   
   };
   gmm.SetInitialMeans(init_means);
   
   matrix m0 = {{0.05337263, 0.02822063}, {0.02822063, 0.18303629}};      
   matrix m1 = {{0.13083678, 0.06071860}, {0.06071860, 0.19563396}};   
   matrix init_covars[2];
   init_covars[0] = m0;
   init_covars[1] = m1;
   gmm.SetInitialCovars(init_covars);
   
   // 3. Вызываем генерацию 
   matrix sample_X;
   vector sample_z;    
   if(gmm.Sample(1000, sample_X, sample_z))
     {
      Print("Выборка успешно сгенерирована");     
      MatrixToCSV("GMM_Samples.csv", sample_X);
      VectorToCSV("GMM_Labels.csv", sample_z); 
     }
  }

Synthetic Data

Рис. 6 Синтетическая выборка из 1000 точек, сгенерированная моделью GMM

Как видно на рисунке, синтетические данные хорошо воспроизводят структуру исходного датасета Old Faithful (два выраженных кластера с характерной формой и ориентацией). Обратите внимание, что визуализация выполнена в стандартизированном масштабе.


Выбор оптимального количества компонентов смеси (Model Selection)

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

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

  • AIC (Akaike Information Criterion) штрафует модель пропорционально количеству свободных параметров,
  • BIC (Bayesian Information Criterion) накладывает более строгий штраф, который также учитывает объём выборки.

Оптимальным считается число компонентов, при котором значение соответствующего критерия минимально.

Скрипт ModelSelection.mq5 демонстрирует процесс выбора оптимального количества компонентов (от 1 до 5) на данных Old Faithful.

Model Selection

Рис. 7 Зависимость логарифма правдоподобия, AIC и BIC от количества компонентов модели

Как видно на рис. 7:

  • Логарифм правдоподобия монотонно растёт с увеличением числа компонентов,
  • Критерий BIC достигает минимума при двух компонентах,
  • Критерий AIC минимален при трёх компонентах.

Такое поведение типично: BIC чаще выбирает более экономные модели, в то время как AIC может допускать чуть более сложные. Финальный выбор оптимального количества компонентов в GMM — это в значительной степени эвристический процесс, опирающийся на экспертную оценку, а не строгая автоматическая процедура. AIC и BIC — это полезные эвристики, а не абсолютные критерии истины. На практике рекомендуется рассматривать результаты обоих критериев одновременно и анализировать поведение модели при разном числе кластеров.

Чтобы запустить скрипт scikit.py, вам понадобится интерпретатор Python и стандартный стек статистических пакетов. О том, как их установить, рассказывается в статье Python + MetaTrader 5: быстрый исследовательский контур для данных, признаков и прототипов.

После установки пакетов поместите файл scikit.py в папку (например, Scripts/GMM).

Запускайте скрипт через MetaEditor:

  • в MetaEditor нажмите Файл → Открыть и выберите файл scikit.py из папки GMM,
  • после открытия файла нажмите клавишу F7 (или кнопку «Компилировать» на панели инструментов),
  • результаты вычислений должны появиться во вкладке Журнал.


Заключение

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

Основные преимущества модели GMM:

  • Мягкая кластеризация — модель выдаёт не просто номер кластера, а вероятности принадлежности каждой точки ко всем компонентам смеси, что позволяет делать более обоснованные выводы,
  • Гибкость — благодаря полным ковариационным матрицам модель способна описывать кластеры любой формы, размера и ориентации в пространстве,
  • Качественная инициализация — по умолчанию используется алгоритм K-Means++ (Alglib), который существенно ускоряет сходимость EM-алгоритма, снижая вероятность попадания в неудачные локальные максимумы,
  • Генеративность — обученную модель можно использовать для генерации новых синтетических данных, близких по статистическим свойствам к оригинальным,
  • Статистическая обоснованность — встроенная поддержка информационных критериев AIC и BIC позволяет объективно выбирать оптимальное количество компонентов и бороться с переобучением.
  • Численная устойчивость — поддержка MAP-оценки с априорным нормальным обратным распределением Вишарта значительно повышает стабильность обучения при малых выборках и высокой размерности данных.

Особенно важно, что класс CGMM показывает результаты, полностью сопоставимые с GaussianMixture из scikit-learn, при этом работая непосредственно внутри MetaTrader 5, без внешних зависимостей. Это позволяет переобучать модель тогда, когда вы посчитаете нужным, не фиксируя один и тот же набор параметров на все случаи жизни.

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


Программы, используемые в статье

# Имя Тип Описание
1 GMM.mqh Включаемый файл Содержит класс модели гауссовской смеси и класс многомерного нормального распределения
2 Learning.mq5 Скрипт Пример обучения модели
3 Sample.mq5 Скрипт Пример генерации выборок
4 ModelSelection.mq5 Скрипт Пример выбора количества компонентов модели
5 scikit.py Скрипт Пример обучения модели GaussianMixture из библиотеки sklearn
6 OldFaithful.csv CSV Данные в формате csv
7 MQL5.zip Архив Все вышеперечисленные файлы можно найти в архиве

Прикрепленные файлы |
MQL5.zip (15.76 KB)
Создание пользовательских индикаторов в MQL5 (Часть 2): Разработка индикатора RSI в виде шкалы со стрелкой на Canvas Создание пользовательских индикаторов в MQL5 (Часть 2): Разработка индикатора RSI в виде шкалы со стрелкой на Canvas
В этой статье мы разработаем индикатор RSI в формате круговой шкалы на MQL5, использующий Canvas и механику стрелочного указателя для отображения значений индекса относительной силы с помощью динамической стрелки, цветовых зон перекупленности и перепроданности и настраиваемых текстовых обозначений. Мы используем класс Canvas для отрисовки таких элементов, как дуги, деления и секторы, обеспечивая плавное и эффективное обновление при поступлении новых данных RSI.
Создание пользовательских индикаторов в MQL5 (Часть 1): Построение трендового индикатора на основе пивотов с градиентной заливкой на Canvas Создание пользовательских индикаторов в MQL5 (Часть 1): Построение трендового индикатора на основе пивотов с градиентной заливкой на Canvas
В этой статье мы создадим трендовый индикатор на основе пивотов в MQL5. Он рассчитывает быстрые и медленные пивотные линии за заданные пользователем периоды, определяет направление тренда по положению цены относительно этих линий и отмечает начало тренда стрелками, при необходимости продлевая линии за пределы текущего бара. Индикатор поддерживает динамическое отображение отдельных восходящих и нисходящих линий настраиваемых цветов, пунктирных быстрых линий, которые меняют цвет при смене тренда, а также дополнительную градиентную заливку между линиями с использованием объекта класса CCanvas для более наглядного выделения трендовой области.
Встраивание торговой дисциплины в код (Часть 3): Принудительное ограничение торговли на уровне символов с помощью белого списка в MQL5 Встраивание торговой дисциплины в код (Часть 3): Принудительное ограничение торговли на уровне символов с помощью белого списка в MQL5
В этой статье подробно рассматривается фреймворк на MQL5, который ограничивает торговлю утвержденным набором символов. Это решение сочетает в себе общую библиотеку, дашборд управления конфигурацией и советника-принудителя, проверяющего каждую сделку на соответствие белому списку символов и логирующего заблокированные попытки. Оно включает в себя полностью функциональные примеры кода, четкое объяснение решений по структурному проектированию и тесты проверки, подтверждающие надежную фильтрацию символов, контролируемая рыночная экспозиция и прозрачный мониторинг соблюдения правил.
Моделирование рынка: Position View (VII) Моделирование рынка: Position View (VII)
В данной статье мы начнем вносить некоторые улучшения в индикатор позиции, чтобы иметь возможность взаимодействовать с ним и изменять ценовые линии или закрывать позицию напрямую через взаимодействие с индикатором позиции. Прежде чем мы перейдем к реализации, стоит кое-что уточнить, особенно для тех, кто об этом не знает. Индикатор никак не может использоваться для изменения чего-либо на торговом сервере. Это связано с тем, что в MetaTrader 5 действует система безопасности, которая позволяет воздействовать на ордера и позиции только советникам. Никакое другое приложение, кроме советника, не сможет манипулировать ордерами или позициями.