Нейросеть: Самооптимизирующийся советник

Jose Miguel Soriano | 3 октября, 2016


Введение

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

  • Какие значения задать входным параметрам?
  • До каких пор эти значения будут надежными? Когда необходимо проводить повторную оптимизацию?
Очевидно, что, в отличие от параметров, которые задаются заранее (рабочая пара, таймфрейм и т.д.), есть и другие, которые будут изменяемыми: период расчета индикаторов, уровни покупки и продажи, уровни TP/SL и им подобные. Это может вызвать определенные проблемы в использовании эксперта.

Возможно ли создать советника, который с заданной периодичностью самостоятельно оптимизировал бы критерии открытия и закрытия позиций?

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

Это не первый раз, когда тема "MQL и нейросеть" поднимается в сообществе трейдеров. Однако обсуждение чаще всего сводится к тому, чтобы использовать информацию, предоставляемую внешней нейросетью советнику (иногда даже вручную), или же к случаям, когда нейросеть оптимизируется трейдером (в режиме «бесконтрольного обучения») с использованием оптимизатора МetaTrader4/МetaTrader5. В конечном итоге происходит замена входных параметров советника на входные параметры сети, как, например в этой статье.

В данной статье не будет описываться торговый робот. Мы будем разрабатывать и реализовывать помодульно макет советника, который при помощи многослойного персептрона (MCP, реализованный в mql5 с помощью библиотеки ALGLIB) будет выполнять алгоритм, упомянутый в предыдущих абзацах. Затем зададим нейросети две математические задачи, результат которых легко проверяется при помощи другого алгоритма. Это нам позволит анализировать решения и задачи, которые планирует МСР согласно своей внутренней структуре, и получить критерий для того, чтобы впоследствии реализовывать советника, только изменяя модуль ввода данных. Создаваемый нами макет должен решить вопрос самооптимизации.

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


1. Базовый алгоритм

  1. Создание нейросети.
  2. Подготовка входных (и соответствующих им выходных) данных при помощи загрузки в массив данных.
  3. Нормализация данных в определенном диапазоне (как правило [0, 1] или [-1, 1]).
  4. Обучение и оптимизация нейросети.
  5. Расчет и использование прогноза сети согласно стратегии советника.
  6. Самооптимизация: возврат к пункту 2 и реитерация процесса с его включением в функцию OnTimer().

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

 

2. Библиотека ALGLIB

Эта библиотека публиковалась и обсуждалась в публикации Сергея Бочканова и на сайте проекта ALGLIB, http://www.alglib.net/, где была описана как гибрид платформы числового анализа и библиотеки обработки данных. Она совместима с различными языками программирования (C++, C#, Pascal, VBA) и операционными системами (Windows, Linux, Solaris). ALGLIB располагает обширной функциональностью. В ее распоряжении:

  • Линейная алгебра (direct algorithms, EVD/SVD)
  • Мастера решения уравнений (линейных и нелинейных)
  • Интерполяция
  • Оптимизация
  • Быстрое преобразование Фурье
  • Числовая интеграция
  • Линейные и нелинейные минимумы квадратов
  • Обычные дифференциальные уравнения
  • Специальные функции
  • Статистика (описательная статистика, проверка гипотез)
  • Анализ данных (классификация / регрессия, включая нейросети)
  • Реализацию алгоритмов линейной алгебры, интерполяции и т.д. в арифметике высокой точности (с использованием MPFR)

Для работы с библиотекой используются статические функции класса CAlglib, в который были переведены все функции библиотеки.

Содержит тестовые скрипты testclasses.mq5 и testinterfaces.mq5 вместе с простым демонстрационным скриптом usealglib.mq5. Одноименные подключаемые файлы (testclasses.mqh и testinterfaces.mqh) используются для запуска тестовых случаев. Их необходимо поместить в \MQL5\Scripts\Alglib\Testcases\.

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

Пакеты 
Описание 
alglib.mqh
Основной пакет библиотеки включает пользовательские функции. Эти функции необходимо вызывать для работы с библиотекой.
dataanalysis.mqhКлассы анализа данных:
  1. CMLPBase — многослойный персептрон.
  2. CMLPTrain — формирование многослойного персептрона.
  3. CMLPE — наборы нейросетей.

При скачивании библиотеки путь инсталляции отправит файлы в "MQL5\Include\Math\Alglib\". Для ее использования достаточно включить в код программы команду

#include <Math\Alglib\alglib.mqh>

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

//--- create neural networks
static void    MLPCreate0(const int nin,const int nout,CMultilayerPerceptronShell &network);
static void    MLPCreate1(const int nin,int nhid,const int nout,CMultilayerPerceptronShell &network);
static void    MLPCreate2(const int nin,const int nhid1,const int nhid2,const int nout,CMultilayerPerceptronShell &network);
static void    MLPCreateR0(const int nin,const int nout,double a,const double b,CMultilayerPerceptronShell &network);
static void    MLPCreateR1(const int nin,int nhid,const int nout,const double a,const double b,CMultilayerPerceptronShell &network);
static void    MLPCreateR2(const int nin,const int nhid1,const int nhid2,const int nout,const double a,const double b,CMultilayerPerceptronShell &network);
static void    MLPCreateC0(const int nin,const int nout,CMultilayerPerceptronShell &network);
static void    MLPCreateC1(const int nin,int nhid,const int nout,CMultilayerPerceptronShell &network);
static void    MLPCreateC2(const int nin,const int nhid1,const int nhid2,const int nout,CMultilayerPerceptronShell &network)

Функции "MLPCreate" создадут нейросеть с линейным выходом. В примерах, приведенных в этой статье, мы создадим этот тип сети.

Функции "MLPCreateR" создадут нейросеть с выходом в пределах интервала [a, b].

Функции "MLPCreateC" создадут нейросеть с выходом, классифицируемым по «классам» (например, 0 или 1; -1, 0 или 1). 

//--- Properties and error of the neural network
static void    MLPProperties(CMultilayerPerceptronShell &network,int &nin,int &nout,int &wcount);
static int     MLPGetLayersCount(CMultilayerPerceptronShell &network);
static int     MLPGetLayerSize(CMultilayerPerceptronShell &network,const int k);
static void    MLPGetInputScaling(CMultilayerPerceptronShell &network,const int i,double &mean,double &sigma);
static void    MLPGetOutputScaling(CMultilayerPerceptronShell &network,const int i,double &mean,double &sigma);
static void    MLPGetNeuronInfo(CMultilayerPerceptronShell &network,const int k,const int i,int &fkind,double &threshold);
static double  MLPGetWeight(CMultilayerPerceptronShell &network,const int k0,const int i0,const int k1,const int i1);
static void    MLPSetNeuronInfo(CMultilayerPerceptronShell &network,const int k,const int i,int fkind,double threshold);
static void    MLPSetWeight(CMultilayerPerceptronShell &network,const int k0,const int i0,const int k1,const int i1,const double w);
static void    MLPActivationFunction(const double net,const int k,double &f,double &df,double &d2f);
static void    MLPProcess(CMultilayerPerceptronShell &network,double &x[],double &y[]);
static double  MLPError(CMultilayerPerceptronShell &network,CMatrixDouble &xy,const int ssize);
static double  MLPRMSError(CMultilayerPerceptronShell &network,CMatrixDouble &xy,const int npoints);


//--- training neural networks
static void    MLPTrainLM(CMultilayerPerceptronShell &network,CMatrixDouble &xy,const int npoints,const double decay,const int restarts,int &info,CMLPReportShell &rep);
static void    MLPTrainLBFGS(CMultilayerPerceptronShell &network,CMatrixDouble &xy,const int npoints,const double decay,const int restarts,const double wstep,int maxits,int &info,CMLPReportShell &rep);

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

  • nin: количество нейронов слоя входа.
  • nout: слой выхода.
  • nhid1: скрытый слой 1.
  • nhid2: скрытый слой 2.
  • network: объект класса CMultilayerPerceptronShell, который будет включать определение связей и весов между нейронами и их функции активации.
  • xy: объект класса CMatrixDouble, который будет включать данные входа/выхода для выполнения обучения и оптимизации нейросети.

Обучение/оптимизация будут выполняться при помощи алгоритма Levenberg-Marquardt (MLPTrainLM()) или L-BFGS с регуляцией (MLPTrainLBFGS()). Этот последний будет использоваться, если сеть содержит более 500 связей/весов: информация о функции предупреждает «для сетей с сотнями весов». Эти алгоритмы более эффективны, чем так называемый «метод обратного распространения ошибки» (back propagation), который обычно используется в НС. Библиотека предлагает и другие функции оптимизации. Читатель может рассмотреть их, если его цели не достигнуты при помощи двух выше названных функций.

 

3. Реализация в MQL

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

input int nNeuronEntra= 35;      //Кол. нейронов во входном слое 
input int nNeuronSal= 1;         //Кол. нейронов в выходном слое
input int nNeuronCapa1= 45;      //Количество нейронов в скрытом слое 1 (<1 не существует)
input int nNeuronCapa2= 10;      //Количество нейронов в скрытом слое 2 (<1 не существует)
input string intervEntrada= "0;1";        //Нормализация входа: желаемый мин. и макс. (пусто= НЕ нормализует)
input string intervSalida= "";            //Нормализация выхода: желаемый мин. и макс. (пусто= НЕ нормализует)

Другие внешние переменные:

input int velaIniDesc= 15;
input int historialEntrena= 1500;

Они помогут указать номер бара (velaIniDesc), с которого нужно начинать выгрузку исторических данных для обучения сети, и общее количество данных бара для выгрузки (historialEntrena).

В качестве глобальных открытых переменных определим объект сети и объект двойного массива "arDatosAprende". 
CMultilayerPerceptronShell *objRed;
CMatrixDouble arDatosAprende(0, 0);

"arDatosAprende" будет включать строки данных входа/выхода для обучения сети. Это двумерная динамическая матрица типа double (известно, что в mql5 разрешено создавать только одномерные динамические массивы. При создании многомерных массивов обязательно указываются все размерности, кроме первой). 

Пункты 1 — 4 базового алгоритма реализуются в функции "gestionRed()".

//---------------------------------- СОЗДАЕТ И ОПТИМИЗИРУЕТ НЕЙРОСЕТЬ --------------------------------------------------
bool gestionRed(CMultilayerPerceptronShell &objRed, string simb, bool normEntrada= true , bool normSalida= true,
                bool imprDatos= true, bool barajar= true)
{
   double tasaAprende= 0.001;             //Коэффициент обучения сети
   int ciclosEntren= 2;                   //Количество циклов обучения
   ResetLastError();
   bool creada= creaRedNeuronal(objRed);                                //создание нейросети
  if(creada) 
   {
      preparaDatosEntra(objRed, simb, arDatosAprende);                  //загрузка входных/выходных данных в arDatosAprende
      if(imprDatos) imprimeDatosEntra(simb, arDatosAprende);            //вывести данные для оценки достоверности
      if(normEntrada || normSalida) normalizaDatosRed(objRed, arDatosAprende, normEntrada, normSalida); //опционная нормализация входных/выходных данных
      if(barajar) barajaDatosEntra(arDatosAprende, nNeuronEntra+nNeuronSal);    //перебираем строки массива данных
      errorMedioEntren= entrenaEvalRed(objRed, arDatosAprende, ciclosEntren, tasaAprende);      //выполняем обучение/оптимизацию
      salvaRedFich(arObjRed[codS], "copiaSegurRed_"+simb);      //сохраняем сеть в файл на диске
   }
   else infoError(GetLastError(), __FUNCTION__);
   
   return(_LastError==0);
}

Здесь мы создаем НС в функции (creaRedNeuronal(objRed)); затем загружаем данные в "arDatosAprende" при помощи функции preparaDatosEntra(). Можно вывести данные для оценки достоверности при помощи функции imprimeDatosEntra(). Если входные и выходные данные необходимо нормализировать, используем функцию normalizaDatosRed(). Также, если хотим перебрать строки массива данных перед оптимизацией, выполняем barajaDatosEntra(). Выполняем обучение, используя entrenaEvalRed(), которая вернет полученную ошибку оптимизации. В конце сохраняем сеть на диск для ее возможного  восстановления без необходимости создавать и оптимизировать ее повторно.

В начале функции gestionRed() есть две переменные (tasaAprende и ciclosEntrena), которые определяют коэффициент и циклы обучения НС. AlgLib предупреждает, что обычно они используются в значениях, которые отражает функция. Но дело в том, что при проведении мною многочисленных тестов с двумя предложенными алгоритмами оптимизации настройка такова, что изменение значений этих переменных практически не сказывается на результатах. Сначала мы вводили эти переменные в качестве входных параметров, но потом, в силу их малозначимости, я переместил их внутрь функции.  Впрочем, читатель может сам решить, проводить ли их как входные параметры или нет.

Функция normalizaDatosRed() будет применяться для нормализации входных данных обучения НС в пределах определенного диапазона, только если реальные данные для запроса прогноза НС тоже будут находиться в данном диапазоне. В обратном случае нормализацию проводить не нужно. Кроме того, нормализация реальных данных перед запросом прогноза не проводится, если данные обучения уже были нормализованы.

3.1 Создание нейросети (НС) 

//--------------------------------- СОЗДАЕТ НЕЙРОСЕТЬ --------------------------------------
bool creaRedNeuronal(CMultilayerPerceptronShell &objRed)
{
   bool creada= false;
   int nEntradas= 0, nSalidas= 0, nPesos= 0;
   if(nNeuronCapa1<1 && nNeuronCapa2<1) CAlglib::MLPCreate0(nNeuronEntra, nNeuronSal, objRed);   	//ЛИНЕЙНЫЙ ВЫХОД   
   else if(nNeuronCapa2<1) CAlglib::MLPCreate1(nNeuronEntra, nNeuronCapa1, nNeuronSal, objRed);   	//ЛИНЕЙНЫЙ ВЫХОД
   else CAlglib::MLPCreate2(nNeuronEntra, nNeuronCapa1, nNeuronCapa2, nNeuronSal, objRed);   		//ЛИНЕЙНЫЙ ВЫХОД                    
   creada= existeRed(objRed);
   if(!creada) Print("Ошибка создания НЕЙРОСЕТИ ==> ", __FUNCTION__, " ", _LastError);
   else
   {
      CAlglib::MLPProperties(objRed, nEntradas, nSalidas, nPesos);
      Print("Создана сеть из nº слоев", propiedadRed(objRed, N_CAPAS));
      Print("Nº нейронов во входном слое ", nEntradas);
      Print("Nº нейронов в скрытом слое 1 ", nNeuronCapa1);
      Print("Nº нейронов в скрытом слое 2 ", nNeuronCapa2);
      Print("Nº нейронов в выходном слое ", nSalidas);
      Print("Nº весов", nPesos);
   }
   return(creada);
}

Вышеприведенная функция создает НС с необходимым количеством слоев и нейронов (nNeuronEntra, nNeuronCapa1, nNeuronCapa2, nNeuronSal), а затем проверяет правильность создания сети при помощи функции:

//--------------------------------- СУЩЕСТВУЕТ СЕТЬ --------------------------------------------
bool existeRed(CMultilayerPerceptronShell &objRed)
{
   bool resp= false;
   int nEntradas= 0, nSalidas= 0, nPesos= 0;
   CAlglib::MLPProperties(objRed, nEntradas, nSalidas, nPesos);
   resp= nEntradas>0 && nSalidas>0;
   return(resp);
}

Если сеть была создана правильно, функция сообщит пользователю ее параметры, в свою очередь, используя функцию MLPProperties() класса CAlglib, который содержится в библиотеке AlgLib.

Как уже было сказано в разделе 2, ALGLIB  располагает другими функциями, при помощи которых можно создать НС, предназначенную для классификации (на выходе мы получаем метку класса), или сеть для решения задач регресии (на выходе у нее — конкретное числовое значение).

После создания НС, для получения некоторых из ее параметров в других частях советника, можно определить функцию "propiedadRed()":

enum mis_PROPIEDADES_RED {N_CAPAS, N_NEURONAS, N_ENTRADAS, N_SALIDAS, N_PESOS};

//---------------------------------- СВОЙСТВА СЕТИ  -------------------------------------------
int propiedadRed(CMultilayerPerceptronShell &objRed, mis_PROPIEDADES_RED prop= N_CAPAS, int numCapa= 0)
{           //если запрашивается количество нейронов N_NEURONAS, нужно указать номер слоя numCapa
   int resp= 0, numEntras= 0, numSals= 0, numPesos= 0;
   if(prop>N_NEURONAS) CAlglib::MLPProperties(objRed, numEntras, numSals, numPesos);    
   switch(prop)
   {
      case N_CAPAS:
         resp= CAlglib::MLPGetLayersCount(objRed);
         break;
      case N_NEURONAS:
         resp= CAlglib::MLPGetLayerSize(objRed, numCapa);
         break;
      case N_ENTRADAS:
         resp= numEntras;
         break;
      case N_SALIDAS:
         resp= numSals;
         break;
      case N_PESOS:
         resp= numPesos;
   }
   return(resp);
}   

3.2  Подготовка входных/выходных данных 

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

//---------------------------------- ПОДГОТАВЛИВАЕТ ВХОДНЫЕ/ВЫХОДНЫЕ ДАННЫЕ --------------------------------------------------
void preparaDatosEntra(CMultilayerPerceptronShell &objRed, string simb, CMatrixDouble &arDatos, bool normEntrada= true , bool normSalida= true)
{
   int fin= 0, fila= 0, colum= 0,
       nEntras= propiedadRed(objRed, N_ENTRADAS),
       nSals= propiedadRed(objRed, N_SALIDAS);
   double valor= 0, arResp[];   
   arDatos.Resize(historialEntrena, nEntras+nSals);
   fin= velaIniDesc+historialEntrena;
   for(fila= velaIniDesc; fila<fin; fila++)
   {                   
      for(colum= 0; colum<NUM_INDIC;  colum++)
      {
         valor= valorIndic(codS, fila, colum);
         arDatos[fila-1].Set(colum, valor);
      }
      calcEstrat(fila-nVelasPredic, arResp);
      for(colum= 0; colum<nSals; colum++) arDatos[fila-1].Set(colum+nEntras, arResp[colum]);
   }
   return;
}

В описанном процессе мы проходим по всей истории от "velaIniDesc" до "velaIniDesc+historialEntrena" и на каждом баре получаем значение каждого используемого в стратегии индикатора (NUM_INDIC), после чего загружаем его в соответствующую колонку двумерной матрицы CMatrixDouble. Также вводим для каждого бара результат стратегии ("calcEstrat()"), который соответствует указанным значениям индикаторов. Переменная "nVelasPredic" позволяет экстраполировать эти значения индикаторов на n свечей вперед. Обычно "nVelasPredic" будет определяться как внешний параметр.

То есть, в каждой строке массива "arDatos" класса CMatrixDouble будет столько же колонок, сколько и входных данных или индикаторных значений используется в стратегии, и сколько выходных данных ею определяется. В "arDatos" будет столько строк, сколько определено значением в "historialEntrena".

3.3 Печать массива входных/выходных данных

Если необходимо напечатать содержание двумерной матрицы для проверки точности входных и выходных данных, можно воспользоваться функцией "imprimeDatosEntra()".

//---------------------------------- ВЫВОДИТ ВХОДНЫЕ/ВЫХОДНЫЕ ДАННЫЕ --------------------------------------------------
void imprimeDatosEntra(string simb, CMatrixDouble &arDatos)
{
   string encabeza= "indic1;indic2;indic3...;resultEstrat",     //имена индикаторов, разделенные ";"
          fichImprime= "dataEntrenaRed_"+simb+".csv";
   bool entrar= false, copiado= false;
   int fila= 0, colum= 0, resultEstrat= -1, nBuff= 0,
       nFilas= arDatos.Size(),
       nColum= nNeuronEntra+nNeuronSal,
       puntFich= FileOpen(fichImprime, FILE_WRITE|FILE_CSV|FILE_COMMON);
   FileWrite(puntFich, encabeza);
   for(fila= 0; fila<nFilas; fila++)
   {
      linea= IntegerToString(fila)+";"+TimeToString(iTime(simb, PERIOD_CURRENT, velaIniDesc+fila), TIME_MINUTES)+";";                
      for(colum= 0; colum<nColum;  colum++) 
         linea= linea+DoubleToString(arDatos[fila][colum], 8)+(colum<(nColum-1)? ";": "");
      FileWrite(puntFich, linea);
   }
   FileFlush(puntFich);
   FileClose(puntFich);
   Alert("Download file= ", fichImprime);
   Alert("Path= ", TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\Files");
   return;
}

Функция построчно перебирает матрицу, создавая на каждом шаге строку "línea"со всеми значениями колонок в строке, разделенными ";". Затем эти данные выводятся в файл типа .csv, который создается при помощи функции FileOpen(). Для темы данной статьи это второстепенная функция, поэтому здесь не будем ее комментировать. Для проверки файла типа .csv можно использовать Excel.

3.4  Нормализация данных в определенном интервале

Обычно, перед тем как приступить к оптимизации сети, считается целесообразным, чтобы входные данные располагались в рамках определенного диапазона, т.е., были нормализованы. Для этого предлагается следующая функция, которая проведет нормализацию входных или выходных данных  (на выбор), находящихся в массиве "arDatos" класса CMatrixDouble

//------------------------------------ НОРМАЛИЗУЕТ ВХОДНЫЕ/ВЫХОДНЫЕ ДАННЫЕ-------------------------------------
void normalizaDatosRed(CMultilayerPerceptronShell &objRed, CMatrixDouble &arDatos, bool normEntrada= true, bool normSalida= true)
{
   int fila= 0, colum= 0, maxFila= arDatos.Size(),
       nEntradas= propiedadRed(objRed, N_ENTRADAS),
       nSalidas= propiedadRed(objRed, N_SALIDAS);
   double maxAbs= 0, minAbs= 0, maxRel= 0, minRel= 0, arMaxMinRelEntra[], arMaxMinRelSals[];
   ushort valCaract= StringGetCharacter(";", 0);
   if(normEntrada) StringSplit(intervEntrada, valCaract, arMaxMinRelEntra);
   if(normSalida) StringSplit(intervSalida, valCaract, arMaxMinRelSals);
   for(colum= 0; normEntrada && colum<nEntradas; colum++)
   {
      maxAbs= arDatos[0][colum];
      minAbs= arDatos[0][colum];
      minRel= StringToDouble(arMaxMinRelEntra[0]);
      maxRel= StringToDouble(arMaxMinRelEntra[1]); 
      for(fila= 0; fila<maxFila; fila++)                //определяем maxAbs и minAbs каждой колонки данных
      {
         if(maxAbs<arDatos[fila][colum]) maxAbs= arDatos[fila][colum];
         if(minAbs>arDatos[fila][colum]) minAbs= arDatos[fila][colum];            
      }
      for(fila= 0; fila<maxFila; fila++)                //устанавливаем новое нормализованное значение
         arDatos[fila].Set(colum, normValor(arDatos[fila][colum], maxAbs, minAbs, maxRel, minRel));
   }
   for(colum= nEntradas; normSalida && colum<(nEntradas+nSalidas); colum++)
   {
      maxAbs= arDatos[0][colum];
      minAbs= arDatos[0][colum];
      minRel= StringToDouble(arMaxMinRelSals[0]);
      maxRel= StringToDouble(arMaxMinRelSals[1]);
      for(fila= 0; fila<maxFila; fila++)
      {
         if(maxAbs<arDatos[fila][colum]) maxAbs= arDatos[fila][colum];
         if(minAbs>arDatos[fila][colum]) minAbs= arDatos[fila][colum];            
      }
      minAbsSalida= minAbs;
      maxAbsSalida= maxAbs;
      for(fila= 0; fila<maxFila; fila++)
         arDatos[fila].Set(colum, normValor(arDatos[fila][colum], maxAbs, minAbs, maxRel, minRel));
   }
   return;
}

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

Необходимо помнить, что "intervEntrada" и "intervSalida" — это переменные строкового типа, определенные как входные параметры (см. начало раздела «Реализация в MQL5»). Они могут иметь форму, например, "0;1" или "-1;1", т.е., содержат относительные максимумы и минимумы. Функция "StringSplit()" передает строку в массив, который будет содержать эти относительные экстремумы. Для каждой колонки необходимо:

  1. Определить абсолютный максимум и минимум (переменные "maxAbs" и "minAbs").
  2. Пройтись по всей колонке, нормализуя значения между "maxRel" и "minRel": см. ниже функцию "normValor()".
  3. Установить в "arDatos" новое нормализованное значение, используя метод .set класса CMatrixDouble.

//------------------------------------ ФУНКЦИЯ НОРМАЛИЗАЦИИ ---------------------------------
double normValor(double valor, double maxAbs, double minAbs, double maxRel= 1, double minRel= -1)
{
   double valorNorm= 0;
   if(maxAbs>minAbs) valorNorm= (valor-minAbs)*(maxRel-minRel))/(maxAbs-minAbs) + minRel;
   return(valorNorm);
} 
3.5 Перебор входных/выходных данных

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

//------------------------------------ ПЕРЕБИРАЕТ ВХОДНЫЕ/ВЫХОДНЫЕ ДАННЫЕ ПОСТРОЧНО -----------------------------------
void barajaDatosEntra(CMatrixDouble &arDatos, int nColum)
{
   int fila= 0, colum= 0, filaDestino= 0, nFilas= arDatos.Size();
   double filaTmp[];
   ArrayResize(filaTmp, nColum);
   MathSrand(GetTickCount());          //перезагружает произвольную дочернюю серию
   while(fila<nFilas)
   {
      filaDestino= randomEntero(0, nFilas-1);   //получает произвольно новую целевую строку
      if(filaDestino!=fila)
      {
         for(colum= 0; colum<nColum; colum++) filaTmp[colum]= arDatos[filaDestino][colum];
         for(colum= 0; colum<nColum; colum++) arDatos[filaDestino].Set(colum, arDatos[fila][colum]);
         for(colum= 0; colum<nColum; colum++) arDatos[fila].Set(colum, filaTmp[colum]);
         fila++;
      }
   }
   return;
}

После перезагрузки произвольной дочерней серии "MathSrand(GetTcikCount())", за то, куда именно будут произвольно перемещаться строки, будет отвечать функция "randomEntero()".

//---------------------------------- ПРОИЗВОЛЬНОЕ ПЕРЕМЕЩЕНИЕ -----------------------------------------------
int randomEntero(int minRel= 0, int maxRel= 1000)
{
   int num= (int)MathRound(randomDouble((double)minRel, (double)maxRel));
   return(num);
}

3.6  Обучение/оптимизация нейросети
Библиотека AlgLib позволяет использовать алгоритмы настройки сети, которые значительно сокращают время обучения и оптимизации по сравнению с традиционной системой, применяемой в многослойном персептроне: «метод обратного распространения ошибки» или «back propagation». Как было сказано вначале, будем использовать:

  • алгоритм Levenberg-Marquardt с регуляризацией и точным расчетом гессианы (MLPTrainLM()), или
  • алгоритм L-BFGS с регуляризацией (MLPTrainLBFGS()).

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

//---------------------------------- ОБУЧЕНИЕ СЕТИ-------------------------------------------
double entrenaEvalRed(CMultilayerPerceptronShell &objRed, CMatrixDouble &arDatosEntrena, int ciclosEntrena= 2, double tasaAprende= 0.001)
{
   bool salir= false;
   double errorMedio= 0; string mens= "Entrenamiento Red";
   int k= 0, i= 0, codResp= 0,
       historialEntrena= arDatosEntrena.Size();
   CMLPReportShell infoEntren;
   ResetLastError();
   datetime tmpIni= TimeLocal();
   Alert("Начало оптимизации нейросети...");
   Alert("Подождите несколько минут, в соответствии с количеством задействованной истории.");
   Alert("...///...");
   if(propiedadRed(objRed, N_PESOS)<500)
      CAlglib::MLPTrainLM(objRed, arDatosEntrena, historialEntrena, tasaAprende, ciclosEntrena, codResp, infoEntren);
   else
      CAlglib::MLPTrainLBFGS(objRed, arDatosEntrena, historialEntrena, tasaAprende, ciclosEntrena, 0.01, 0, codResp, infoEntren);
   if(codResp==2 || codResp==6) errorMedio= CAlglib::MLPRMSError(objRed, arDatosEntrena, historialEntrena);
   else Print("Cod entrena Resp: ", codResp);
   datetime tmpFin= TimeLocal();
   Alert("NGrad ", infoEntren.GetNGrad(), " NHess ", infoEntren.GetNHess(), " NCholesky ", infoEntren.GetNCholesky());
   Alert("codResp ", codResp," Средняя ошибка обучения "+DoubleToString(errorMedio, 8), " ciclosEntrena ", ciclosEntrena);
   Alert("tmpEntren ", DoubleToString(((double)(tmpFin-tmpIni))/60.0, 2), " min", "---> tmpIni ", TimeToString(tmpIni, _SEG), " tmpFin ", TimeToString(tmpFin, _SEG));
   infoError(GetLastError(), __FUNCTION__);
   return(errorMedio);
}

Как можно увидеть, в качестве входных параметров функция получает «объект сети» и матрица входных/выходных данных, которая к данной стадии уже была нормализована. Также мы определяем циклы, или эпохи обучения ("ciclosEntrena"; или количество раз, которое алгоритм проведет подгонку в поиске наименее возможной «ошибки обучения»); в документации рекомендуется 2. Проведенные мною тесты не показали улучшения результатов при увеличении количества эпох обучения.  Также мы сказали о параметре «Коэффициент обучения» ("tasaAprende").

Определяем в начале функции объект "infoEntren" (класса CMLPReportShell) , который соберет информацию о результате обучения и который затем мы получим при помощи методов GetNGrad() и GetNCholesky(). Средняя ошибка обучения (среднеквадратическая ошибка всех исходных выходных данных по отношению к выходным данным, полученным после обработки алгоритмом) получается с помощью функции "MLPRMSError()". Кроме того, мы информируем пользователя о затраченном на оптимизацию времени. Для этого берется начальное и конечное время в переменных tmpIni и tmpFin.

Эти функции оптимизации возвращают код ошибки выполнения («codResp»), который может принимать следующие значения:

  • -2, если образец обучения имеет большее количество выходных данных, чем количество нейронов в выходном слое.
  • -1, если какой-нибудь входной параметр функции некорректен.
  • 2, правильное выполнение,  масштаб ошибки меньше, чем критерий остановки ("MLPTrainLM()").
  • 6, то же самое для функции "MLPTrainLBFGS()".

Таким образом, правильное выполнение вернет 2 или 6 в соответствии с количеством весов оптимизированной сети. 

Эти алгоритмы выполняют настройку таким образом, что реитерация циклов обучения (переменная "ciclosEntrena") практически не влияет на полученную ошибку, в отличие от алгоритма "back propagation", где реитерация может существенно изменить полученную точность. Сеть из 4 слоев с 35, 45, 10 и 2 нейронами и входной матрицей из 2000 строк можно оптимизировать при помощи вышеописанной функции за 4 - 6 минут (I5, core 4, RAM 8 gb) с ошибкой порядка 2 — 4 стотысячных (4x10^-5).

3.7 Сохранение сети в текстовом файле или восстановление из него

На данном этапе статьи мы создали НС, приготовили входные/выходные данные и обучили сеть. В целях предосторожности необходимо сохранить сеть на жесткий диск, на случай непредвиденных ошибок во время работы советника. Для этого нам нужно воспользоваться функциями, которые предоставляет AlgLib, для того, чтобы получить характеристики и внутренние значения сети (количество слоев и нейронов в каждом слое, значение весов и т.д.) и записать эти данные в текстовый файл, расположенный на жестком диске.

//-------------------------------- СОХРАНИТЬ СЕТЬ НА ДИСК -------------------------------------------------
bool salvaRedFich(CMultilayerPerceptronShell &objRed, string nombArch= "")
{
   bool redSalvada= false;
   int k= 0, i= 0, j= 0, numCapas= 0, arNeurCapa[], neurCapa1= 1, funcTipo= 0, puntFichRed= 9999;
   double umbral= 0, peso= 0, media= 0, sigma= 0;
   if(nombArch=="") nombArch= "copiaSegurRed";
   nombArch= nombArch+".red";
   FileDelete(nombArch, FILE_COMMON);
   ResetLastError();
   puntFichRed= FileOpen(nombArch, FILE_WRITE|FILE_BIN|FILE_COMMON);
   redSalvada= puntFichRed!=INVALID_HANDLE;
   if(redSalvada)
   {
      numCapas= CAlglib::MLPGetLayersCount(objRed);   
      redSalvada= redSalvada && FileWriteDouble(puntFichRed, numCapas)>0;
      ArrayResize(arNeurCapa, numCapas);
      for(k= 0; redSalvada && k<numCapas; k++)
      {
         arNeurCapa[k]= CAlglib::MLPGetLayerSize(objRed, k);
         redSalvada= redSalvada && FileWriteDouble(puntFichRed, arNeurCapa[k])>0;
      }
      for(k= 0; redSalvada && k<numCapas; k++)
      {
         for(i= 0; redSalvada && i<arNeurCapa[k]; i++)
         {
            if(k==0)
            {
               CAlglib::MLPGetInputScaling(objRed, i, media, sigma);
               FileWriteDouble(puntFichRed, media);
               FileWriteDouble(puntFichRed, sigma);
            }
            else if(k==numCapas-1)
            {
               CAlglib::MLPGetOutputScaling(objRed, i, media, sigma);
               FileWriteDouble(puntFichRed, media);
               FileWriteDouble(puntFichRed, sigma);
            }
            CAlglib::MLPGetNeuronInfo(objRed, k, i, funcTipo, umbral);
            FileWriteDouble(puntFichRed, funcTipo);
            FileWriteDouble(puntFichRed, umbral);
            for(j= 0; redSalvada && k<(numCapas-1) && j<arNeurCapa[k+1]; j++)
            {
               peso= CAlglib::MLPGetWeight(objRed, k, i, k+1, j);
               redSalvada= redSalvada && FileWriteDouble(puntFichRed, peso)>0;
            }
         }      
      }
      FileClose(puntFichRed);
   }
   if(!redSalvada) infoError(_LastError, __FUNCTION__);
   return(redSalvada);
} 

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

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

//-------------------------------- ВОССТАНАВЛИВАЕТ СЕТЬ С ДИСКА -------------------------------------------------
bool recuperaRedFich(CMultilayerPerceptronShell &objRed, string nombArch= "")
{
   bool exito= false;
   int k= 0, i= 0, j= 0, nEntradas= 0, nSalidas= 0, nPesos= 0,
       numCapas= 0, arNeurCapa[], funcTipo= 0, puntFichRed= 9999;
   double umbral= 0, peso= 0, media= 0, sigma= 0;
   if(nombArch=="") nombArch= "copiaSegurRed";
   nombArch= nombArch+".red";
   puntFichRed= FileOpen(nombArch, FILE_READ|FILE_BIN|FILE_COMMON);
   exito= puntFichRed!=INVALID_HANDLE;
   if(exito)
   {
      numCapas= (int)FileReadDouble(puntFichRed);
      ArrayResize(arNeurCapa, numCapas);
      for(k= 0; k<numCapas; k++) arNeurCapa[k]= (int)FileReadDouble(puntFichRed); 
      if(numCapas==2) CAlglib::MLPCreate0(nNeuronEntra, nNeuronSal, objRed);
      else if(numCapas==3) CAlglib::MLPCreate1(nNeuronEntra, nNeuronCapa1, nNeuronSal, objRed);
      else if(numCapas==4) CAlglib::MLPCreate2(nNeuronEntra, nNeuronCapa1, nNeuronCapa2, nNeuronSal, objRed);
      exito= existeRed(arObjRed[0]);
      if(!exito) Print("ошибка создания нейросети ==> ", __FUNCTION__, " ", _LastError);
      else
      {
         CAlglib::MLPProperties(objRed, nEntradas, nSalidas, nPesos);
         Print("Восстановлена сеть из nº слоев", propiedadRed(objRed, N_CAPAS));
         Print("Nº нейронов во входном слое ", nEntradas);
         Print("Nº нейронов в скрытом слое 1 ", nNeuronCapa1);
         Print("Nº нейронов в скрытом слое 2 ", nNeuronCapa2);
         Print("Nº нейронов в выходном слое ", nSalidas);
         Print("Nº веса", nPesos);
         for(k= 0; k<numCapas; k++)
         {
            for(i= 0; i<arNeurCapa[k]; i++)
            {
               if(k==0)
               {
                  media= FileReadDouble(puntFichRed);
                  sigma= FileReadDouble(puntFichRed);
                  CAlglib::MLPSetInputScaling(objRed, i, media, sigma);
               }
               else if(k==numCapas-1)
               {
                  media= FileReadDouble(puntFichRed);
                  sigma= FileReadDouble(puntFichRed);
                  CAlglib::MLPSetOutputScaling(objRed, i, media, sigma);
               }
               funcTipo= (int)FileReadDouble(puntFichRed);
               umbral= FileReadDouble(puntFichRed);
               CAlglib::MLPSetNeuronInfo(objRed, k, i, funcTipo, umbral);
               for(j= 0; k<(numCapas-1) && j<arNeurCapa[k+1]; j++)
               {
                  peso= FileReadDouble(puntFichRed);
                  CAlglib::MLPSetWeight(objRed, k, i, k+1, j, peso);
               }
            }      
         }
      }
   }
   FileClose(puntFichRed);
   return(exito);
} 

Чтобы получить прогноз сети при загрузке в нее данных на настоящий момент, вызываем функцию "respuestaRed()":

//--------------------------------------- ЗАПРАШИВАЕТ ОТВЕТ СЕТИ ---------------------------------
double respuestaRed(CMultilayerPerceptronShell &ObjRed, double &arEntradas[], double &arSalidas[], bool desnorm= false)
{
   double resp= 0, nNeuron= 0;
   CAlglib::MLPProcess(ObjRed, arEntradas, arSalidas);   
   if(desnorm)             //Если нужно поменять нормализацию выходных данных
   {
      nNeuron= ArraySize(arSalidas);
      for(int k= 0; k<nNeuron; k++)
         arSalidas[k]= desNormValor(arSalidas[k], maxAbsSalida, minAbsSalida, arMaxMinRelSals[1], arMaxMinRelSals[0]);
   }
   resp= arSalidas[0];
   return(resp);
}

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

 

4.  Самооптимизация

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

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

Зададим тип перечисления "mis_PLAZO_OPTIM", описывающий временные интервалы, которые пользователь сможет выбрать для повторения базового алгоритма (ежедневно, выборочно или в выходные дни). Зададим другое перечисление, чтобы пользователь мог решить, будет ли советник действовать как «оптимизатор» сети или как «исполнитель» стратегии.

enum mis_PLAZO_OPTIM {_DIARIO, _DIA_ALTERNO, _FIN_SEMANA};
enum mis_TIPO_EAred {_OPTIMIZA, _EJECUTA};

Сейчас напомним, что МetaTrader 5 позволяет одновременное выполнение советника на каждом открытом графике. Таким образом, на первый график загрузим советник в режиме исполнения, а на второй — в режиме оптимизации. На первом графике советник будет заниматься контролем стратегии, на втором — только оптимизацией нейросети. Таким образом, решается вторая описанная проблема. На первом графике советник «использует» нейросеть, «считывая» ее из текстового файла, который он генерирует в режиме «оптимизатора» каждый раз, когда оптимизирует НС.

Ранее было сказано, что на тесты оптимизации затрачивалось от 4 до 6 минут вычислительного времени. Применение данного метода несколько увеличивает длительность процесса: до 8 и 15 минут, в зависимости от азиатского или европейского времени рыночной активности, но контроль над стратегией не прекращается.

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

input mis_TIPO_EAred tipoEAred            = _OPTIMIZA;        //Тип выполняемого задания
input mis_PLAZO_OPTIM plazoOptim          = _DIARIO;          //Временной интервал для оптимизации сети
input int horaOptim                       = 3;                //Местное время для оптимизации сети

Параметр "horaOptim" сохраняет местное время, по которому должна производиться оптимизация. Время должно соответствовать низкой или нулевой рыночной активности: в Европе, например, ранним утром (03:00 h как значение по умолчанию) или в выходные. Если есть желание всегда проводить оптимизацию, когда запускается советник, не ожидая установленного времени и дней, также нужно задать следующее:

input bool optimInicio                    = true;         //Оптимизировать нейросеть при запуске советника

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

double fechaUltLectura;
bool reOptimizada= false;

Для решения первой проблемы блок обработки указанного метода прописывается в функции OnTimer(), которая будет выполняться в соответствии с периодом "tmp", в свою очередь, установленным при помощи EventSetTimer(tmp) в OnInit(), по крайней мере каждый час. Таким образом, каждые tmp секунд советник-«оптимизатор» будет проверять, нужно ли переоптимизировать сеть, а советник-«исполнитель» проверит, нужно ли снова читать файл сети, потому что его обновил советник «оптимизатор».

/---------------------------------- ON TIMER --------------------------------------
void OnTimer()
{
   bool existe= false;
   string fichRed= "";
   if(tipoEAred==_OPTIMIZA)            //советник работает в режиме «оптимизатор»
   {
      bool optimizar= false;
      int codS= 0,
          hora= infoFechaHora(TimeLocal(), _HORA);    //получаем полное текущее время
      if(!redOptimizada) optimizar= horaOptim==hora && permReoptimDia();
      fichRed= "copiaSegurRed_"+Symbol()+".red";      //определяет имя файла нейросети
      existe= buscaFich(fichRed, "*.red");            //ищет на диске файл, где была сохранена нейросеть
      if(!existe || optimizar)
         redOptimizada= gestionRed(objRed, simb, intervEntrada!="", intervSalida!="", imprDatosEntrena, barajaDatos);
      if(hora>(horaOptim+6)) redOptimizada= false;    //по прошествии 6 часов с предполагаемого времени, настоящая оптимизированная сеть считается устаревшей
      guardaVarGlobal(redOptimizada);                 //сохраняет на диске значение "reoptimizada»( переоптимизирована)
   }
   else if(tipoEAred==_EJECUTA)        //советник работает в режиме «исполнитель»
   {
      datetime fechaUltOpt= 0;
      fichRed= "copiaSegurRed_"+Symbol()+".red";      //определяет имя файла нейросети
      existe= buscaFich(fichRed, "*.red");            //ищет на диске файл, где была сохранена нейросеть
      if(existe)
      {
         fechaUltOpt= fechaModifFich(0, fichRed);     //определяет дату последней оптимизации (модификация файла сети)
         if(fechaUltOpt>fechaUltLectura)              //если дата оптимизации позже последнего чтения
         {
            recuperaRedFich(objRed, fichRed);         //читает и генерирует новую нейросеть
            fechaUltLectura= (double)TimeCurrent();
            guardaVarGlobal(fechaUltLectura);         //сохраняет на диске новую дату прочтения
            Print("Сеть восстановлена после оптимизации... "+simb);      //выводит на экран сообщение 
         }
      }
      else Alert("tipoEAred==_EJECUTA --> Файл нейросети не наден: "+fichRed+".red");
   }
   return;
}

Следующие дополнительные функции, которые здесь не комментируются:

//--------------------------------- РАЗРЕШЕНИЕ НА ПОВТОРНУЮ ОПТИМИЗАЦИЮ ---------------------------------
bool permReoptimDia()
{
   int diaSemana= infoFechaHora(TimeLocal(), _DSEM);
   bool permiso= (plazoOptim==_DIARIO && diaSemana!=6 && diaSemana!=0) ||     //оптимизирует [каждый день, со вторника по субботу]
                 (plazoOptim==_DIA_ALTERNO && diaSemana%2==1) ||              //оптимизирует [вторник, четверг и суббота]
                 (plazoOptim==_FIN_SEMANA && diaSemana==5);                   //оптимизирует [суббота]
   return(permiso);
}

//-------------------------------------- ИЩЕТ ФАЙЛ --------------------------------------------
bool buscaFich(string fichBusca, string filtro= "*.*", int carpeta= FILE_COMMON)
{
   bool existe= false;
   string fichActual= "";
   long puntBusca= FileFindFirst(filtro, fichActual, carpeta);
   if(puntBusca!=INVALID_HANDLE)
   {
      ResetLastError();
      while(!existe)
      {
         FileFindNext(puntBusca, fichActual);
         existe= fichActual==fichBusca;
      }
      FileFindClose(puntBusca);
   }
   else Print("Файл не найден!");
   infoError(_LastError, __FUNCTION__);
   return(existe);

Описанный алгоритм в настоящее время используется в советнике, который мы тестируем. Это позволяет нам полностью контролировать стратегию, в то время как каждую ночь, с 3:00 ч по местному времени, сеть переоптимизируется с данными Н1 за 3 предыдущих месяца: 35 нейронов во входном слое, 45 в первом скрытом слое, 8 во втором скрытом слое и 2 в выходном слое; оптимизация продолжается 35-45 минут.

5. Задача 1: двоично-десятичный конвертер

Чтобы проверить данную систему, решим задачу, из которой мы заранее узнаем точное решение (существует алгоритм, который его дает) и сравним его с тем, которое предоставляет нейросеть. Сделаем двоично-десятичный конвертер. Для теста предлагается следующий скрипт:

#property script_show_confirm
#property script_show_inputs

#define FUNC_CAPA_OCULTA   1  
#define FUNC_SALIDA        -5
            //1= гиперболический тангенс; 2= e^(-x^2); 3= x>=0 raizC(1+x^2) x<0 e^x; 4= сигмоидная функция;
            //5= биноминальная x>0.5? 1: 0; -5= линейная функция
#include <Math\Alglib\alglib.mqh>

enum mis_PROPIEDADES_RED {N_CAPAS, N_NEURONAS, N_ENTRADAS, N_SALIDAS, N_PESOS};
//---------------------------------  Входные параметры  ---------------------
sinput int nNeuronEntra= 10;                 //Кол. нейронов входного слоя 
                                             //2^8= 256 2^9= 512; 2^10= 1024; 2^12= 4096; 2^14= 16384 
sinput int nNeuronCapa1= 0;                  //Кол. нейронов в первом скрытом слое (<1 не существует)
sinput int nNeuronCapa2= 0;                  //Кол. нейронов во втором скрытом слое (<1 не существует)                                             //2^8= 256 2^9= 512; 2^10= 1024; 2^12= 4096; 2^14= 16384 
sinput int nNeuronSal= 1;                    //Кол. нейронов в выходном слое

sinput int    historialEntrena= 800;         //История обучения
sinput int    historialEvalua= 200;          //История оценки
sinput int    ciclosEntrena= 2;              //Циклы обучения
sinput double tasaAprende= 0.001;            //Уровень обучения сети
sinput string intervEntrada= "";             //Нормализация входа: желаемый мин. и макс. (пусто= НЕ нормализует)
sinput string intervSalida= "";              //Нормализация выхода: желаемый мин. и макс. (пусто= НЕ нормализует)
sinput bool   imprEntrena= true;             //Вывести данные обучения/оценки
      
// ------------------------------ ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ -----------------------------     
int puntFichTexto= 0;
ulong contFlush= 0; 
CMultilayerPerceptronShell redNeuronal;
CMatrixDouble arDatosAprende(0, 0);
CMatrixDouble arDatosEval(0, 0);
double minAbsSalida= 0, maxAbsSalida= 0;
string nombreEA= "ScriptBinDec";

//+------------------------------------------------------------------+
void OnStart()              //Двоично-десятичный конвертер
{
   string mensIni= "Script conversor BINARIO-DECIMAL",
          mens= "", cadNumBin= "", cadNumRed= "";
   int contAciertos= 0, arNumBin[],
       inicio= historialEntrena+1,
       fin= historialEntrena+historialEvalua;
   double arSalRed[], arNumEntra[], salida= 0, umbral= 0, peso= 0;
   double errorMedioEntren= 0;
   bool normEntrada= intervEntrada!="", normSalida= intervSalida!="", correcto= false,
        creada= creaRedNeuronal(redNeuronal);        
   if(creada) 
   {
      iniFichImprime(puntFichTexto, nombreEA+"-infRN", ".csv",mensIni);
      preparaDatosEntra(redNeuronal, arDatosAprende, intervEntrada!="", intervSalida!="");
      normalizaDatosRed(redNeuronal, arDatosAprende, normEntrada, normSalida);
      errorMedioEntren= entrenaEvalRed(redNeuronal, arDatosAprende);
      escrTexto("-------------------------", puntFichTexto);
      escrTexto("RESPUESTA RED------------", puntFichTexto);
      escrTexto("-------------------------", puntFichTexto);
      escrTexto("numBinEntra;numDecSalidaRed;correcto", puntFichTexto);
      for(int k= inicio; k<=fin; k++)
      {
         cadNumBin= dec_A_baseNumerica(k, arNumBin, 2, nNeuronEntra);
         ArrayCopy(arNumEntra, arNumBin);
         salida= respuestaRed(redNeuronal, arNumEntra, arSalRed);
         salida= MathRound(salida);
         correcto= k==(int)salida;
         escrTexto(cadNumBin+";"+IntegerToString((int)salida)+";"+correcto, puntFichTexto);
         cadNumRed= "";
      }
   }      
   deIniFichImprime(puntFichTexto);
   return;
}

Создаем НС и обучаем ее с первыми 800 натуральными числами в бинарном виде (10 знаков, 10 входных нейронов, 1 выходной). Затем преобразуем следующие 200 натуральных чисел в двоичный вид (с 801 до 1000 в двоичном виде) и сравниваем реальный результат с тем, который предсказала НС. Например, если мы зададим сети 1100110100 (820 в бинарном виде; 10 знаков, 10 входных нейронов), сеть должна получить на выходе 820 или близкую к нему цифру. Метод For, описанный выше, отвечает за получение прогноза сети для этих 200 чисел и за сравнение ожидаемого результата с прогнозируемым.

После выполнения скрипта с заданными параметрами (НС без скрытых слоев, 10 входных нейронов и 1 выходной) получаем прекрасный результат. Файл "ScriptBinDec-infRN.csv" сгенерированный в папке "Terminal\Common\Files" дает нам следующую информацию:


 

На рисунке видно, что скрипт напечатал матрицу обучения до числа 800 в бинарном виде (на входе) и десятичном виде (на выходе). НС обучилась, и мы напечатали ответ, начиная с числа 801. В третьей колонке мы получили true. Это итог сравнения между ожидаемым и полученным результатом. Как уже было сказано, это хороший результат.

Тем не менее, если мы определим структуру НС как «10 входных  нейронов, 20 нейронов скрытого слоя 1, 8 нейронов второго скрытого слоя, 1 выходной нейрон», то получим следующее:


 

Это неприемлемый результат! Дело в том, что здесь мы сталкиваемся с огромной проблемой при обработке нейросети: какая внутренняя конфигурация (количество слоев, количество нейронов и функций активации) наиболее подходящая? Как известно, эту проблему решает только опыт, тысячи тестов со стороны пользователя и прочтение таких статей как, к примеру, «Оценка и выбор переменных в моделях машинного обучения». Кроме того, мы использовали данные матрицы обучения в программе статистического анализа Rapid Miner, чтобы попытаться найти наиболее эффективную структуру, прежде чем реализовывать ее в mql5.

 

6. Задача 2: определитель простых чисел

Рассмотрим сейчас похожую задачу, только в этот раз НС будет определять, простое ли это число или нет. Обучающая матрица будет содержать 10 колонок с 10 знаками каждого натурального числа в бинарном виде до 800 и одну колонку, где указывается, простое ли это число ("1") или нет ("0"). Т.е., у нас будет 800 строк и 11 колонок. Затем зададим, чтобы НС проанализировала следующие 200 натуральных чисел в бинарном виде (с 801 до 1000) и определила, какое число простое, а какое — нет. В конце, осознавая, что эта задача более трудная, распечатаем статистику полученных совпадений.

#include <Math\Alglib\alglib.mqh>

enum mis_PROPIEDADES_RED {N_CAPAS, N_NEURONAS, N_ENTRADAS, N_SALIDAS, N_PESOS};
//---------------------------------  Входные параметры  ---------------------
sinput int nNeuronEntra= 10;                 //Кол. нейронов входного слоя
                                             //2^8= 256 2^9= 512; 2^10= 1024; 2^12= 4096; 2^14= 16384 
sinput int nNeuronCapa1= 20;                 //Количество нейронов в скрытом слое 1 (<1 не существует)
sinput int nNeuronCapa2= 0;                  //Кол. нейронов скрытый слой 2 (<1 не существует)                                             //2^8= 256 2^9= 512; 2^10= 1024; 2^12= 4096; 2^14= 16384 
sinput int nNeuronSal= 1;                    //Кол. нейронов в выходном слое

sinput int    historialEntrena= 800;         //История обучения
sinput int    historialEvalua= 200;          //История прогноза
sinput int    ciclosEntrena= 2;              //Циклы обучения
sinput double tasaAprende= 0.001;            //Коэффициент обучения сети
sinput string intervEntrada= "";             //Нормализация входа: желаемый мин. и макс. (пусто= НЕ нормализует)
sinput string intervSalida= "";              //Нормализация выхода: желаемый мин. и макс. (пусто= НЕ нормализует)
sinput bool   imprEntrena= true;             //Вывести данные обучения/оценки
      
// ------------------------------ ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ -----------------------------     
int puntFichTexto= 0;
ulong contFlush= 0; 
CMultilayerPerceptronShell redNeuronal;
CMatrixDouble arDatosAprende(0, 0);
double minAbsSalida= 0, maxAbsSalida= 0;
string nombreEA= "ScriptNumPrimo";

//+----------------------- Определитель простых чисел -------------------------------------------+
void OnStart()
{
   string mensIni= "Script comprobación NÚMEROS PRIMOS", cadNumBin= "", linea= "";
   int contAciertos= 0, totalPrimos= 0, aciertoPrimo= 0, arNumBin[],
       inicio= historialEntrena+1,
       fin= historialEntrena+historialEvalua;
   double arSalRed[], arNumEntra[], numPrimoRed= 0;
   double errorMedioEntren= 0;
   bool correcto= false,
        esNumPrimo= false, 
        creada= creaRedNeuronal(redNeuronal);        
   if(creada) 
   {
      iniFichImprime(puntFichTexto, nombreEA+"-infRN", ".csv",mensIni);
      preparaDatosEntra(redNeuronal, arDatosAprende, intervEntrada!="", intervSalida!="");
      normalizaDatosRed(redNeuronal, arDatosAprende, normEntrada, normSalida);
      errorMedioEntren= entrenaEvalRed(redNeuronal, arDatosAprende);
      escrTexto("-------------------------", puntFichTexto);
      escrTexto("RESPUESTA RED------------", puntFichTexto);
      escrTexto("-------------------------", puntFichTexto);
      escrTexto("numDec;numBin;numPrimo;numPrimoRed;correcto", puntFichTexto);
      for(int k= inicio; k<=fin; k++)
      {
         cadNumBin= dec_A_baseNumerica(k, arNumBin, 2, nNeuronEntra);
         esNumPrimo= esPrimo(k);
         ArrayCopy(arNumEntra, arNumBin);
         numPrimoRed= respuestaRed(redNeuronal, arNumEntra, arSalRed);
         numPrimoRed= MathRound(numPrimoRed);
         correcto= esNumPrimo==(int)numPrimoRed;
         if(esNumPrimo)
         {
            totalPrimos++;
            if(correcto) aciertoPrimo++;  
         }
         if(correcto) contAciertos++;
         linea= IntegerToString(k)+";"+cadNumBin+";"+esNumPrimo+";"+(numPrimoRed==0? "false": "true")+";"+correcto;
         escrTexto(linea, puntFichTexto);
      }
   }     
   escrTexto("porc Aciertos / total;"+DoubleToString((double)contAciertos/(double)historialEvalua*100, 2)+" %", puntFichTexto); 
   escrTexto("Aciertos primos;"+IntegerToString(aciertoPrimo)+";"+"total primos;"+IntegerToString(totalPrimos), puntFichTexto); 
   escrTexto("porc Aciertos / total primos;"+DoubleToString((double)aciertoPrimo/(double)totalPrimos*100, 2)+" %", puntFichTexto); 
   deIniFichImprime(puntFichTexto);
   return;
}

После выполнения скрипта с заданными параметрами (НС без скрытых слоев, 10 нейронов входа, 20 нейронов в первом скрытом слое и 1 в слое выхода) результат получается хуже, чем в предыдущей задаче. Файл "ScriptNumPrimo-infRN.csv", сгенерированный в папке "Terminal\Common\Files", дает нам следующую информацию:


 

Здесь мы видим, что первое простое число после 800 (809) не было обнаружено сетью (верно = неверно). Статистический обзор:


 

Здесь сообщается, что НС угадала 78% из 200 чисел на интервале оценки (801 до 200). Но из 29 простых чисел, которые существуют в этом интервале, она обнаружила только 13 (44,83 %).

Если мы проведем тест со следующей структурой сети: «10 нейронов во входном слое, 35 в первом скрытом слое, 10 во втором скрытом слое и 1 в выходном слое», скрипт выводит следующую информацию по мере своего выполнения:


 

Как видно на рисунке ниже, за 0,53 минут и со средней ошибкой обучения 0,04208383, результаты получились хуже.


 

Таким образом, мы опять возвращаемся к предыдущему вопросу: как определить внутреннюю структуру сети наилучшим образом?

 

Заключение

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

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

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