Практическое тестирование сверточных моделей

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

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

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

Геометрический смысл градиента — это наклон касательной к графику функции в текущей точке

Геометрический смысл градиента — это наклон касательной к графику функции в текущей точке

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

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

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

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

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

bool CreateNet(CNet &net)
  {
   CArrayObj *layers = new CArrayObj();
   if(!layers)
     {
      PrintFormat("Error creating CArrayObj: %d"GetLastError());
      return false;
     }
//--- слой исходных данных
   CLayerDescription *descr = new CLayerDescription();
   if(!descr)
     {
      PrintFormat("Error creating CLayerDescription: %d"GetLastError());
      delete layers;
      return false;
     }
   descr.type = defNeuronBase;
   int prev_count = descr.count = BarsToLine;
   descr.window = 0;
   descr.activation = AF_NONE;
   descr.optimization = None;
   if(!layers.Add(descr))
     {
      PrintFormat("Error adding layer: %d"GetLastError());
      delete layers;
      delete descr;
      return false;
     }

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

//--- Сверточный слой
   descr = new CLayerDescription();
   if(!descr)
     {
      PrintFormat("Error creating CLayerDescription: %d"GetLastError());
      delete layers;
      return false;
     }
   descr.type = defNeuronConv;
   int m_iWindow = descr.window = 2;
   int prev_wind_out = descr.window_out = 2;
   int m_iStep = descr.step = 1;
   prev_count=descr.count=(prev_count-descr.window+2*descr.step-1)/descr.step;
   descr.activation = AF_SWISH;
   descr.optimization = Adam;
   descr.activation_params[0] = 1;
   if(!layers.Add(descr))
     {
      PrintFormat("Error adding layer: %d"GetLastError());
      delete layers;
      delete descr;
      return false;
     }

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

//--- Подвыборочный слой
   descr = new CLayerDescription();
   if(!descr)
     {
      PrintFormat("Error creating CLayerDescription: %d"GetLastError());
      delete layers;
      return false;
     }
   descr.type = defNeuronProof;
   descr.window = 2;
   descr.window_out = prev_wind_out;
   descr.step = 1;
   descr.count = (prev_count - descr.window + 2 * descr.step - 1) / descr.step;
   descr.activation = (ENUM_ACTIVATION_FUNCTION)AF_AVERAGE_POOLING;
   descr.optimization = None;
   descr.activation_params[0] = 1;
   if(!layers.Add(descr))
     {
      PrintFormat("Error adding layer: %d"GetLastError());
      delete layers;
      delete descr;
      return false;
     }

Далее код скрипта остается неизменным.

Подготовленный скрипт запускаем в двух режимах: с использованием технологии OpenCL и без. В результате тестирования получили довольно приемлемые результаты. В обоих случаях получили отклонения в 11-м знаке после запятой.

Результаты проверки отклонений градиента ошибки между аналитическим и экспериментальным методами определения

Результаты проверки отклонений градиента ошибки между аналитическим и экспериментальным методами определения

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

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

  • RSI,
  • гистограмма MACD,
  • сигнальная линия MACD,
  • отклонение между сигнальной линией и гистограммой MACD.

Каждый из вас может сам провести ряд тестов и определить свой подход к использованию сверточных моделей. Для меня наиболее очевидным является два варианта использования.

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

Но при анализе временных рядов порой бывает динамика изменения показателя имеет большее значение, чем его абсолютное значение. Мы можем использовать свертку для определения паттернов динамики показателей. Для этого нам потребуется немного переставить показатели, выстроив в один ряд последовательно значения каждого показателя. К примеру, сначала выстроим в буфере исходных данных все показатели индикатора RSI. Затем — все элементы последовательности гистограммы MACD, за ними — данные сигнальной линии. Завершим буфер данными отклонений между сигнальной линией и гистограммой MACD. Конечно, было бы нагляднее расположить данные в табличной форме, где каждая строка представляла бы значения отдельного индикатора. Но, к сожалению, в контексте OpenCL используются только одномерные буферы. Поэтому мы воспользуемся виртуальным делением буфера на блоки.

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

Тестирование свертки в рамках одной свечи

Для проведения тестирования работы сверточных моделей нейронных сетей создадим копию скрипта perceptron_test.mq5 с именем convolution_test.mq5. В начале скрипта по-прежнему мы указываем параметры для работы скрипта.

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

bool CreateLayersDesc(CArrayObj &layers)
  {
   CLayerDescription *descr;
//--- создаем слой исходных данных
   if(!(descr = new CLayerDescription()))
     {
      PrintFormat("Error creating CLayerDescription: %d"GetLastError());
      return false;
     }
   descr.type         = defNeuronBase;
   descr.count        = NeuronsToBar * BarsToLine;
   descr.window       = 0;
   descr.activation   = AF_NONE;
   descr.optimization = None;
   if(!layers.Add(descr))
     {
      PrintFormat("Error adding layer: %d"GetLastError());
      return false;
     }

Обратите внимание, что для сверточного слоя в поле count объекта описания слоя мы указываем не общее количество нейронов, а количество элементов в одном фильтре. В поле window_out указываем количество используемых фильтров. В полях window и step укажем количество элементов на один бар. С такими параметрами мы получим свертку без перекрытия, и каждый фильтр будет сравнивать состояние индикаторов на каждом баре с неким паттерном. Функцию активации я указал Swish, а метод оптимизации — Adam. Данный метод оптимизации мы будем использовать и для всех последующих слоев. Кроме, конечно, подвыборочного, который не содержит матрицы весов.

//--- Сверточный слой
   if(!(descr = new CLayerDescription()))
     {
      PrintFormat("Error creating CLayerDescription: %d"GetLastError());
      return false;
     }
   descr.type = defNeuronConv;
   descr.count = BarsToLine;
   descr.window = NeuronsToBar;
   descr.window_out = 8;
   descr.step = NeuronsToBar;
   descr.activation = AF_SWISH;
   descr.optimization = Adam;
   descr.activation_params[0] = 1;
   if(!layers.Add(descr))
     {
      PrintFormat("Error adding layer: %d"GetLastError());
      delete descr;
      return false;
     }

За сверточным слоем следует подвыборочный слой. В данной реализации я использовал Max Pooling — выбор максимального элемента в пределах входного окна. Мы используем скользящее окно из двух элементов и шагом в один элемент. С таким набором параметров количество элементов в одном фильтре уменьшится на один. Функцию активации на данном слое мы не используем. Количество фильтров равно аналогичному параметру предыдущего слоя.

//--- Подвыборочный слой
   if(!(descr = new CLayerDescription()))
     {
      PrintFormat("Error creating CLayerDescription: %d"GetLastError());
      return false;
     }
   descr.type = defNeuronProof;
   descr.count = BarsToLine - 1;
   descr.window = 2;
   descr.window_out = 8;
   descr.step = 1;
   descr.activation = (ENUM_ACTIVATION_FUNCTION)AF_MAX_POOLING;
   descr.optimization = None;
   descr.activation_params[0] = 0;
   if(!layers.Add(descr))
     {
      PrintFormat("Error adding layer: %d"GetLastError());
      delete descr;
      return false;
     }

Далее идет массив скрытых полносвязных слоев. Их мы будем создавать в цикле с одинаковыми параметрами. Количество создаваемых скрытых слоев задается в параметрах скрипта. Все скрытые слои будут иметь одинаковое количество элементов, которое указывается в параметрах скрипта. Функцию активации будем использовать Swish, а метод оптимизации параметров матрицы весов — Adam, как и для сверточного слоя.

//--- Блок скрытых полносвязных слоев
   if(!(descr = new CLayerDescription()))
     {
      PrintFormat("Error creating CLayerDescription: %d"GetLastError());
      return false;
     }
   descr.type         = defNeuronBase;
   descr.count        = HiddenLayer;
   descr.activation   = AF_SWISH;
   descr.optimization = Adam;
   descr.activation_params[0] = 1;
   for(int i = 0i < HiddenLayersi++)
      if(!layers.Add(descr))
        {
         PrintFormat("Error adding layer: %d"GetLastError());
         return false;
        }

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

//---  Слой результатов
   if(!(descr = new CLayerDescription()))
     {
      PrintFormat("Error creating CLayerDescription: %d"GetLastError());
      return false;
     }
   descr.type         = defNeuronBase;
   descr.count        = 2;
   descr.activation   = AF_LINEAR;
   descr.optimization = Adam;
   descr.activation_params[0] = 1;
   if(!layers.Add(descr))
     {
      PrintFormat("Error adding layer: %d"GetLastError());
      return false;
     }
   return true;
  }

Весь остальной код скрипта остался без изменений.

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

Сравнительный график обучения перцептрона и свёрточной нейронной сети

Сравнительный график обучения перцептрона и свёрточной нейронной сети

При увеличении масштаба можно заметить тенденцию и к потенциальному снижению значения функции потерь при продолжении обучения.

Сравнительный график обучения перцептрона и свёрточной нейронной сети

Сравнительный график обучения перцептрона и свёрточной нейронной сети

 

Тестирование свертки скользящего окна по значениям индикатора

Для эксперимента с поиском паттернов динамики значений индикаторов нам необходимо немного изменить ранее созданный скрипт convolution_test.mq5. Создадим его копию с именем convolution_test2.mq5. Первые изменения мы внесем в объявление сверточного слоя. На этот раз мы создаем слой с окном свертки равным трем элементам и шагом в один элемент. С такими параметрами количество элементов в одном фильтре будет на два элемента меньше предыдущего слоя, но при этом общее количество элементов выходного буфера увеличится кратно количеству используемых фильтров. Функция активации и метод оптимизации остаются без изменений.

//--- Сверточный слой
   int prev_count = descr.count;
   if(!(descr = new CLayerDescription()))
     {
      PrintFormat("Error creating CLayerDescription: %d"GetLastError());
      return false;
     }
   descr.type = defNeuronConv;

  prev_count = descr.count = prev_count - 2;
   descr.window = 3;
   descr.window_out = 8;
   descr.step = 1;
   descr.activation = AF_SWISH;
   descr.optimization = Adam;
   descr.activation_params[0] = 1;
   if(!layers.Add(descr))
     {
      PrintFormat("Error adding layer: %d"GetLastError());
      delete descr;
      return false;
     }

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

//--- Подвыборочный слой
   if(!(descr = new CLayerDescription()))
     {
      PrintFormat("Error creating CLayerDescription: %d"GetLastError());
      return false;
     }
   descr.type = defNeuronProof;
   descr.count = prev_count - 1;
   descr.window = 2;
   descr.window_out = 8;
   descr.step = 1;
   descr.activation = (ENUM_ACTIVATION_FUNCTION)AF_MAX_POOLING;
   descr.optimization = None;
   descr.activation_params[0] = 0;
   if(!layers.Add(descr))
     {
      PrintFormat("Error adding layer: %d"GetLastError());
      delete descr;
      return false;
     }

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

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

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

bool LoadTrainingData(string pathCArrayObj &dataCArrayObj &result)
  {
   CBufferType *pattern;
   CBufferType *target;
//--- открываем файл с обучающей выборкой
   int handle = FileOpen(pathFILE_READ | FILE_CSV | FILE_ANSI | FILE_SHARE_READ,
                                                                     ","CP_UTF8);
   if(handle == INVALID_HANDLE)
     {
      PrintFormat("Error opening study data file: %d"GetLastError());
      return false;
     }
//--- выводим прогресс загрузки данных обучения в комментарий чарта
   uint next_comment_time = 0;
   uint OutputTimeout = 250; // не чаще 1 раза в 250 миллисекунд

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

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

//--- организовываем цикл загрузки обучающей выборки
   while(!FileIsEnding(handle) && !IsStopped())
     {
      if(!(pattern = new CBufferType()))
        {
         PrintFormat("Error creating Pattern data array: %d"GetLastError());
         return false;
        }
      if(!pattern.BufferInit(NeuronsToBarBarsToLine))
        {
         delete pattern;
         return false;
        }
      if(!(target = new CBufferType()))
        {
         PrintFormat("Error creating Pattern Target array: %d"GetLastError());
         delete pattern;
         return false;
        }
      if(!target.BufferInit(12))
        {
         delete pattern;
         delete target;
         return false;
        }

Для загрузки данных мы по прежнему используем динамические массивы:

  • data — массив паттернов исходных данных;
  • result — массив паттернов целевых значений для каждого паттерн;
  • pattern — буфер элементов одного паттерна;
  • target — буфер целевых значений одного паттерна.

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

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

      for(int i = 0i < BarsToLinei++)
         for(int y = 0y < NeuronsToBary++)
            pattern.m_mMatrix[yi] = (TYPE)FileReadNumber(handle);

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

      if(!pattern.Reshape(1BarsToLine * NeuronsToBar))
        {
         delete pattern;
         delete target;
         return false;
        }

Дальнейший процесс загрузки обучающей выборки перенесен без изменений.

      for(int i = 0i < 2i++)
         target.m_mMatrix[0i] = (TYPE)FileReadNumber(handle);
      if(!data.Add(pattern))
        {
         PrintFormat("Error adding study data to array: %d"GetLastError());
         delete pattern;
         delete target;
         return false;
        }
      if(!result.Add(target))
        {
         PrintFormat("Error adding study data to array: %d"GetLastError());
         delete target;
         return false;
        }
      //--- выводим прогресс загрузки в комментарий чарта (не чаще 1 раза в 250 миллисекунд)
      if(next_comment_time < GetTickCount())
        {
         Comment(StringFormat("Patterns loaded: %d"data.Total()));
         next_comment_time = GetTickCount() + OutputTimeout;
        }
     }
   FileClose(handle);
   return true;
  }

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

Сравнительный график обучения перцептрона и двух моделей свёрточных нейронных сетей

Сравнительный график обучения перцептрона и двух моделей свёрточных нейронных сетей

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

Увеличение масштаба графика подтверждает сделанные выше выводы.

Сравнительный график обучения перцептрона и двух моделей свёрточных нейронных сетей

Сравнительный график обучения перцептрона и двух моделей свёрточных нейронных сетей

Комбинированная модель

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

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

Для построения такой модели нам нет необходимости транспонировать данные. Поэтому новую модель будем строить на базе скрипта convolution_test.mq5. В начале мы создадим его копию с именем convolution_test3.mq5. В нем мы изменим параметры сверточного слоя. В обучающей выборке данные идут в хронологическом порядке, следовательно, окно свертки из полных трех баров будет равно 3 * NeuronsToBar. Тогда шаг окна свертки в размере одного бара будет равен NeuronsToBar. С такими параметрами количество элементов в одном фильтре составит BarsToLine - 2. Функцию активации и метод оптимизации параметров оставляем без изменений.

//--- Сверточный слой
   if(!(descr = new CLayerDescription()))
     {
      PrintFormat("Error creating CLayerDescription: %d"GetLastError());
      return false;
     }
   descr.type = defNeuronConv;
   descr.count = BarsToLine - 2;
   descr.window = 3 * NeuronsToBar;
   descr.window_out = 8;
   descr.step = NeuronsToBar;
   descr.activation = AF_SWISH;
   descr.optimization = Adam;
   descr.activation_params[0] = 1;
   if(!layers.Add(descr))
     {
      PrintFormat("Error adding layer: %d"GetLastError());
      delete descr;
      return false;
     }

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

//--- Подвыборочный слой
   if(!(descr = new CLayerDescription()))
     {
      PrintFormat("Error creating CLayerDescription: %d"GetLastError());
      return false;
     }
   descr.type = defNeuronProof;
   descr.count = BarsToLine - 3;
   descr.window = 2;
   descr.window_out = 8;
   descr.step = 1;
   descr.activation = (ENUM_ACTIVATION_FUNCTION)AF_MAX_POOLING;
   descr.optimization = None;
   descr.activation_params[0] = 0;
   if(!layers.Add(descr))
     {
      PrintFormat("Error adding layer: %d"GetLastError());
      delete descr;
      return false;
     }

Весь остальной код скрипта остался без изменений.

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

Сравнительный график обучения перцептрона и трех моделей свёрточных нейронных сетей

Сравнительный график обучения перцептрона и трех моделей свёрточных нейронных сетей

Увеличение масштаба графика только подтверждает сделанные выше выводы.

Сравнительный график обучения перцептрона и трех моделей свёрточных нейронных сетей

Сравнительный график обучения перцептрона и трех моделей свёрточных нейронных сетей

Тестирование моделей Python

В предыдущем разделе мы создали скрипт с тремя моделями нейронных сетей на языке Python. Напомню, первая модель перцептрона имеет три скрытых слоя, во второй мы добавили сверточный нейронный слой Conv1D перед моделью перцептрона, а в третьей модели заменили сверточный слой Conv1D на Conv2D. При этом в каждой последующей модели увеличивалось число параметров. По логике работы созданные нами модели на языке Python повторяют проведенные выше эксперименты с моделями нейронных сетей, созданных средствами MQL5. Поэтому результаты тестирования были вполне ожидаемые и полностью подтвердили сделанные ранее выводы. Для нас это является дополнительным подтверждением корректности работы нашей библиотеки, написанной на языке MQL5. А значит, мы вполне можем использовать ее в нашей дальнейшей работе. Кроме того, получение схожих результатов при тестировании моделей, полностью созданных различными средствами, исключает случайность полученных результатов и минимизирует вероятность допущения ошибки в процессе создания моделей.

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

Сравнительный график обучения перцептрона и 2-х моделей свёрточных нейронных сетей (Python)

Сравнительный график обучения перцептрона и 2-х моделей свёрточных нейронных сетей (Python)

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

Сравнительный график обучения перцептрона и двух моделей свёрточных нейронных сетей (Python zoom)

Сравнительный график обучения перцептрона и двух моделей свёрточных нейронных сетей (Python zoom)

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

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

Сравнительный график обучения перцептрона и двух моделей свёрточных нейронных сетей (Python)

Сравнительный график обучения перцептрона и двух моделей свёрточных нейронных сетей (Python)

На валидационной выборке графики всех трех моделей тесно переплетены в диапазоне 0,71–0,73. На графике видно пересечение графиков обучающей и валидационной выборки после 400.

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

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

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

Сравнение ошибки моделей на тестовой выборке

Сравнение ошибки моделей на тестовой выборке

Сравнение Accuracy моделей на тестовой выборке

Сравнение Accuracy моделей на тестовой выборке

Сравнение результатов по метрике Accuracy, в противоречие только что рассмотренного графика MSE, демонстрирует наилучшие результаты у модели Conv1D. Модель анализирует паттерны каждой отдельной свечи, наименьший результат — у перцептрона. Но, как и по MSE, разрыв между результатами небольшой.

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

Точные значения проверки моделей на тестовой выборке

Точные значения проверки моделей на тестовой выборке

Выводы

По результатам проведенных тестов можно сказать:

  • Построенные нами модели средствами MQL5 при обучении демонстрируют результаты аналогичные моделям построенными с помощью библиотеки Keras на языке Python. Данный факт подтверждает корректность работы создаваемой нами библиотеки. Мы уверенно можем продолжить нашу работу.
  • В общем итоге сверточные модели позволяют улучшить результаты работы модели на той же обучающей выборке.
  • Подходы к свертке исходных данных могут быть различные, и от выбранного подхода могут зависеть результаты работы модели.
  • Объединение различных подходов в рамках одной модели не всегда позволяет улучшить результаты работы модели.
  • Не бойтесь экспериментировать. Создавая свою модель, попробуйте различные архитектуры и различные варианты обработки данных.

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