Нейронная сеть на практике: Ручное обратное распространение
Введение
В предыдущей статье «Нейронная сеть на практике: Начинаем строить цепочку» мы начали создавать нашу первую сеть перцептронов более общего назначения. На самом деле это пока ещё не совсем сеть, а скорее попытка последовательно соединить два перцептрона. Цель заключалась в том, чтобы показать, как работает механизм, позволяющий, казалось бы, изолированным перцептронам работать совместно, как единое целое. Хотя мы можем использовать произвольное количество перцептронов, на данном этапе я предлагаю ограничиться только двумя, поскольку большее их число внесло бы ненужные сложности. Даже количество входов и выходов следует свести к необходимому минимуму, просто потому, что чем больше данных нам приходится анализировать, тем сложнее понять, как всё работает.
Возможно, вы заметили, что перцептроны пытались сходиться к решению. Однако они пытались делать это совершенно самостоятельно. Это связано с тем, что реализованное нами на данный момент позволяет построить лишь нечто вроде того, что показано ниже. 
Изображение 01
Цель этой статьи — показать, как заставить работать связку двух или более перцептронов и они начали действовать совместно, а не как независимые элементы.
Это сеть перцептронов или один перцептрон?
Название этого раздела может показаться несколько экстравагантным. Это связано с тем, что многие могут подумать, что нет смысла сравнивать перцептрон с сетью перцептронов. В какой-то мере это может быть верно. Но это перестаёт быть верным, когда нам нужно придумать, как заставить несколько перцептронов работать совместно.
Возможно, чтобы понять смысл этого заголовка, сначала нам следует задать себе другой вопрос: действительно ли то, что мы видим на Изображении 01, работает? В некотором смысле — да, но не так, как можно было бы ожидать. Это связано с тем, что перцептроны не взаимодействуют друг с другом или, точнее, не обмениваются между собой данными, и каждый из них принимает решения совершенно независимо. Для наших целей это не даст ничего действительно полезного. И здесь возникает вопрос: «как заставить перцептроны работать совместно?»
Что ж, это, по сути, и есть главный вопрос. Однако у него нет единственного ответа. Вопреки тому, что многие говорят или, возможно, ошибочно представляют, ответом не обязательно является обратное распространение. Хотя обратное распространение и решает большую часть проблем, на практике мы применяем его не всегда. Бывают ситуации, когда оно не используется. Таким образом, всё зависит от конкретного случая и конкретной цели, для которой создаётся рассматриваемая сеть перцептронов.
Но прежде чем углубиться в эту тему, я хочу предложить вам кое-что, что мы уже рассматривали в начале этой серии статей, когда я объяснял, как использовать уравнение прямой, чтобы сходиться к лучшим значениям используемых констант. По сути, это приводит к линейной регрессии. По мере продвижения вперёд мы в итоге разработали простой перцептрон, но вполне функциональный в рамках первоначальной цели. По мере продвижения вперёд в какой-то момент мы дойдём до сети перцептронов. Однако прежде чем это произойдёт, я хочу сделать небольшую паузу и опробовать одну модификацию, которая поможет вам понять, как можно реализовать обмен данными между перцептронами. Это позволит объяснить различные вопросы, связанные с необходимостью каким-то образом обеспечить сходимость в сети перцептронов.
Я знаю, что это может показаться весьма странным, особенно тем, кто пытается понять, как всё работает, но в большинстве случаев сеть перцептронов не будет сильно отличаться от отдельного перцептрона. Бывают случаи, когда сеть перцептронов совсем не похожа на то, что будет объяснено здесь. Но в других случаях она будет очень похожа на то, что мы увидим реализованным позже.
Нужно понять, что мы будем делать и, главное, почему, нам придётся вернуться на несколько шагов назад. Это связано с тем, что в том виде, в каком сейчас реализован наш перцептрон довольно трудно ясно объяснить, зачем нам нужна другая модель реализации и почему мы собираемся отказаться от той, которую используем сейчас. Важно, чтобы это было совершенно ясно, прежде чем мы продолжим. Наша текущая реализация вовсе не является неправильной. Просто в таком виде с её помощью немного трудно дать ясное объяснение, как несколько перцептронов могут работать совместно.
Хорошо, для начала нам нужно обратиться к математике. Но вам не стоит пугаться: то, что мы здесь рассмотрим, довольно просто, это базовый уровень.
Любая система, использующая перцептрон, опирается на математику для поиска уравнения, представляющего набор данных. По сути, используется следующее выражение: 
Изображение 02
Здесь у нас только один вход и только один выход, которым как раз и является значение < x >. Также < a >, которое является наклоном прямой и которое, когда речь идёт о перцептронах, обычно называют весом, и < b >, являющийся свободным членом, который здесь мы называем смещением, имеют фиксированные значения. Это верно как при использовании одного перцептрона, так и любой сети перцептронов. Всё можно свести к выражению, показанному на изображении 02.
А теперь обратите внимание. Если значение < x > равно 10, а значение f(x) равно 20, вы легко можете увидеть, что < a > должно быть равно двум, а < b > — нулю. Это в случае только одного входа. По мере изменения значения < x > становится всё очевиднее, что < a > должно быть равно двум, а < b > — нулю, пока мы наконец не приходим к выводу, что выражение, представляющее наш набор данных, будет следующим: 
Изображение 03
Хорошо, но к чему я веду? Если вы поняли предыдущую идею и то, что было объяснено в предыдущих статьях, то, наверное, заметили, что перцептрон может найти выражение, представляющее набор данных, просто сравнивая выходное значение с тем, которое мы задаем как ожидаемое. И он может сделать это, внутренне изменяя значения веса и смещения. Но что произошло бы, если бы мы соединили два перцептрона, как видели в предыдущей статье и как показано на Изображении 01? Какое выражение в действительности должен использовать каждый перцептрон? Итак, выражение будет выглядеть примерно так, как показано ниже.

Изображение 04
Теперь всё становится немного интереснее. Обратите внимание на следующее: f(xout) — это оценочный результат. В свою очередь, < xin > — это входное значение. И здесь у нас, так сказать, два перцептрона, соединенных последовательно. То есть вход одного является выходом предыдущего. Поэтому у нас есть < a1 > и < b1 >, которые представляют входной перцептрон. Аналогично, < a2 > и < b2 > представляют выходной перцептрон.
Теперь уже не так просто понять, какие значения присвоить этим константам, верно? Это связано с тем, что изменение одной из них влечет за собой изменение всего набора, поэтому для того же набора данных, который использовался для построения Изображения 03, мы уже не можем использовать значения два и ноль в качестве веса и смещения соответственно.
И, как я только что упоминал, именно здесь всё становится по-настоящему интересным. Помните, что это самая простая модель после той, в которой используется один перцептрон. Еще один момент: здесь мы игнорируем любую функцию активации, поскольку ее цель — изменить уравнение прямой. Но это будет объяснено позже.
А теперь обратите внимание на одну деталь в этом выражении на Изображении 04: заметьте, что оно очень похоже на предыдущие. И приведённые выше выражения, по сути, соответствуют модели перцептрона, которую мы уже рассмотрели. Таким образом, мы начинаем сводить задачу к той, с которой уже умеем справляться. То есть мы используем перцептрон для получения результата и, в зависимости от ошибки между оценочным значением и значением, возвращаемым перцептроном, корректируем значения весов и смещения. Благодаря этому наша сеть будет вести себя так, как будто это один-единственный перцептрон.
Я стараюсь делать всё как можно более постепенно. Так сохраняется довольно высокий дидактический уровень, и всем становится легче понять материал. Дойдя до этого момента, те, кто уже обладает хорошими знаниями в программировании, сразу начинают представлять себе решение и могут быстро реализовать всё необходимое, даже подготовив реализацию для моделирования более сложной топологии, в которой мы не будем ограничены одной-единственной последовательностью перцептронов.
Однако здесь мы будем двигаться медленнее, так как я не знаю, каков уровень подготовки каждого в программировании. Поэтому, если уже достаточно ясно, что модель класса CNeuron вовсе не ошибочна и нуждается лишь в адаптации для достижения наших целей, позволяя нам контролировать, как будет выполняться обратное распространение, мы можем начать думать о необходимых изменениях. Но, чтобы чётко разграничить эти вещи, давайте начнём новую тему.Небольшое изменение, но с глубокими последствиями
Хотя результаты будут интересными, в целом то, с чего мы здесь начнём, может показаться несколько скучным. На самом деле это не так уж сложно и не требует какого-то особенного опыта. Мы просто изменим код, который рассматривали в предыдущей статье, чтобы применить то, что было объяснено в предыдущем разделе. И это как не что-то захватывающее, потому что сделать это очень просто.
Для начала нужно удалить код обучения, который мы используем. Мы перейдём на другой код, чтобы модель стала больше похожа на ту, что была описана в начале этой статьи. То есть мы не будем использовать функцию Learning класса C_Neuron. Мы создадим локальную функцию, которая будет выполнять ту же задачу. Но не волнуйтесь, мы будем действовать не спеша, шаг за шагом. Так вы сможете следить за этим и понимать, как всё возникает естественным образом. Таким образом, код из предыдущей статьи будет изменён и примет вид, показанный ниже.
//+------------------------------------------------------------------+ #property copyright "Daniel Jose" #property description "Experiencing a simple chain of neurons" //+------------------------------------------------------------------+ #define def_NO_MSG //+------------------------------------------------------------------+ #include <Neural Network\C_Neuron.mqh> //+------------------------------------------------------------------+ //Выражение для обучения: f(x) = (w0 * 2) //+------------------------------------------------------------------+ double Train[] { 10, 20 }; //+------------------------------------------------------------------+ #define nColumns 2 #define nLines Train.Size() / nColumns //+------------------------------------------------------------------+ void SimpleChain(const uchar nNeurons) { C_Neuron *neuron[]; double tmp[], in, out; if ((!nNeurons) || (Train.Size() > 2)) return; ArrayResize(neuron, nNeurons); for (uchar c = 0; c < nNeurons; c++) neuron[c] = new C_Neuron(nColumns - 1, C_Neuron::Identity); ArrayResize(tmp, Train.Size()); in = out = Train[0]; for (uchar c = 0; c < nNeurons; c++) { tmp[0] = (c > 0 ? out : in); PrintFormat("Input value in the neuron #%02d: %.8f", c, tmp[0]); out = (*neuron[c]).Perceptron(tmp); } PrintFormat("Final exit: %.8f || Expected: %.8f", out, Train[1]); for (uchar c = 0; c < nNeurons; c++) delete neuron[c]; ArrayFree(tmp); ArrayFree(neuron); } //+------------------------------------------------------------------+ void OnStart() { Print("************************************"); Print("A simple chain of neurons..."); Print("************************************"); SimpleChain(2); } //+------------------------------------------------------------------+
Код 01
Конечно, если вы попытаетесь запустить его, то не получите ничего впечатляющего. Вы увидите только то, что показано на следующем изображении, то есть то же самое, что и раньше.

Изображение 05
Теперь я хочу, чтобы вы подумали вот о чём: если мы попросим перцептроны скорректировать свои веса и смещения, что произойдёт? Очевидно, на выходе каждого из перцептронов мы получим разные значения. Но давайте подумаем об этом немного глубже. А что, если вместо того, чтобы изменять значения по отдельности, заставить оба перцептрона изменять веса и смещения одновременно и на одну и ту же величину? Что произойдёт в таком случае? Хм, не знаю, как на это ответить. Итак, для этого нам нужно добавить в класс C_Neuron новую процедуру, поскольку в его нынешнем виде мы пока не можем делать такие вещи.
Эту новую процедуру можно увидеть в следующем фрагменте.
//+------------------------------------------------------------------+ inline void BackPropagation(const double errBias, const double errWeight) { m_Infos.Bias -= errBias; for (uint c = 0, m = m_Infos.Weight.Size(); c < m; c++) m_Infos.Weight[c] -= errWeight; } //+------------------------------------------------------------------+
Фрагмент 01
Название этой процедуры довольно красноречиво, но на самом деле здесь мы указываем перцептрону, насколько он должен изменить веса и смещение. Но как мы будем к этому обращаться? Итак, вернувшись к основному коду, мы можем увидеть, как именно должна выполняться эта настройка. Поскольку нам не нужно слишком сильно изменять код, мы сосредоточимся только на основном фрагменте, показанном ниже.
17. //+------------------------------------------------------------------+ 18. void SimpleChain(const uchar nNeurons, const double errBias, const double errWeight) 19. { 20. C_Neuron *neuron[]; 21. double tmp[], in, out; 22. 23. if ((!nNeurons) || (Train.Size() > 2)) 24. return; 25. 26. ArrayResize(neuron, nNeurons); 27. for (uchar c = 0; c < nNeurons; c++) 28. neuron[c] = new C_Neuron(nColumns - 1, C_Neuron::Identity); 29. 30. ArrayResize(tmp, Train.Size()); 31. in = out = Train[0]; 32. for (uchar count = 1; count < 3; count++) 33. { 34. PrintFormat("====== Interaction #%02d ======", count); 35. for (uchar c = 0; c < nNeurons; c++) 36. { 37. tmp[0] = (c > 0 ? out : in); 38. PrintFormat("Input value in the neuron #%02d: %.8f", c, tmp[0]); 39. out = (*neuron[c]).Perceptron(tmp); 40. } 41. PrintFormat("Final exit: %.8f || Expected: %.8f", out, Train[1]); 42. for (uchar c = 0; c < nNeurons; c++) 43. (*neuron[c]).BackPropagation(errBias, errWeight); 44. } 45. 46. for (uchar c = 0; c < nNeurons; c++) 47. delete neuron[c]; 48. 49. ArrayFree(tmp); 50. ArrayFree(neuron); 51. } 52. //+------------------------------------------------------------------+
Фрагмент 02
Хорошо, идея заключается в том, чтобы заставить оба перцептрона одновременно корректировать значения смещения и веса. Это будет достигнуто путём передачи значений errBias и errWeight. Поскольку мы выполним всего две итерации, цель будет не в том, чтобы получить идеальное значение, а просто в том, чтобы проверить гипотезу. Обратите внимание, что в первой итерации у нас есть значения, псевдослучайно сгенерированные перцептроном. Однако в строке 42 мы входим в цикл, цель которого — заставить все перцептроны одинаково скорректировать свои значения. То есть все они будут корректироваться с помощью новой процедуры, которую мы добавили в класс C_Neuron, независимо от ошибки каждого перцептрона.
Таким образом, во второй итерации перцептрон вернёт другое значение в строке тридцать девять. Наблюдая за этим значением, вы сможете увидеть, как перцептроны пытаются правильно скорректировать веса и смещение. Ниже приведён типичный результат выполнения кода. 
Изображение 06
Наша главная цель — сделать так, чтобы значения, отображаемые в области, выделенной зелёным цветом, были как можно ближе друг к другу. Обратите внимание, что нам нужно скорректировать значение смещения, поскольку, согласно обучающим данным, мы имеем: f(x) = 2 * x + 0. То есть смещение должно быть как можно ближе к нулю. В свою очередь, значение веса должно быть как можно ближе к двум. Но как я это сделал на Изображении 06? Что ж, если присмотреться, то можно заметить, что значения результата инвертированы и при этом очень близки к идеальным значениям. Это означает, что значения нужно инвертировать. Таким образом, поменяв их местами и немного скорректировав, терпеливо и спокойно, мы можем получить результат, показанный на следующем изображении.

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

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

Изображение 10
Сейчас мы используем пять перцептронов, соединённых последовательно. Однако на этот раз мы используем функцию eLU в качестве функции активации. Важно отметить, что выбранная функция будет применяться ко всем перцептронам. Позже мы рассмотрим, как сделать так, чтобы функция активации каждого перцептрона отличалась от функций активации остальных. Подробное объяснение оставим на другой раз.
Впрочем, вы, возможно, думаете, что для достижения такого уровня взаимодействия пришлось значительно изменить код. Однако посмотрите ниже и ответьте: действительно ли пришлось изменять код так сильно, как вы предполагали?
//+------------------------------------------------------------------+ #property copyright "Daniel Jose" #property script_show_inputs #property description "Experiencing a simple chain of neurons" //+------------------------------------------------------------------+ #define def_NO_MSG //+------------------------------------------------------------------+ #include <Neural Network\C_Neuron.mqh> //+------------------------------------------------------------------+ //Выражение для обучения: f(x) = (w0 * 2) //+------------------------------------------------------------------+ double Train[] { 10, 20 }; //+------------------------------------------------------------------+ #define nColumns 2 #define nLines Train.Size() / nColumns //+------------------------------------------------------------------+ input C_Neuron::eFnActivate user00 = C_Neuron::Identity; //Функция активации input uchar user01 = 2; //Количество нейронов input double user02 = 1.0; //Ошибка смещения input double user03 = 1.0; //Ошибка веса //+------------------------------------------------------------------+ void SimpleChain(const uchar nNeurons, const double errBias, const double errWeight) { C_Neuron *neuron[]; double tmp[], in, out; if ((!nNeurons) || (Train.Size() > 2)) return; ArrayResize(neuron, nNeurons); for (uchar c = 0; c < nNeurons; c++) neuron[c] = new C_Neuron(nColumns - 1, user00); ArrayResize(tmp, Train.Size()); in = out = Train[0]; for (uchar count = 1; count < 3; count++) { PrintFormat("====== Interaction #%02d ======", count); for (uchar c = 0; c < nNeurons; c++) { tmp[0] = (c > 0 ? out : in); PrintFormat("Input value in the neuron #%02d: %.8f", c, tmp[0]); out = (*neuron[c]).Perceptron(tmp); } PrintFormat("Final exit: %.8f || Expected: %.8f", out, Train[1]); for (uchar c = 0; c < nNeurons; c++) (*neuron[c]).BackPropagation(errBias, errWeight); } for (uchar c = 0; c < nNeurons; c++) delete neuron[c]; ArrayFree(tmp); ArrayFree(neuron); } //+------------------------------------------------------------------+ void OnStart() { Print("************************************"); Print("A simple chain of neurons..."); Print("************************************"); Print("Parameters:"); Print("Activation function: ", EnumToString(user00)); Print("Bias Error: ", user02); Print("Weight Error:", user03); Print("************************************"); SimpleChain(user01, user02, user03); } //+------------------------------------------------------------------+
Код 02
Отлично. Но прежде чем завершить статью, я хочу предложить ещё одно изменение, которое ещё больше повысит уровень взаимодействия. Я хочу немного подробнее остановиться на этой теме, ведь чем лучше вы поймёте, что здесь происходит, тем проще будет понять то, что мы увидим дальше. Давайте поступим следующим образом: откроем новую тему, чтобы должным образом разделить вопросы.Чем интерактивнее, тем лучше
Хотя реализация с помощью скрипта в какой-то мере удобна, рано или поздно она становится несколько однообразной. Мы можем сделать её немного удобнее для практического использования. Для этого мы добавим новый способ взаимодействия, при котором можно будет использовать клавиши со стрелками для настройки значений. Сам по себе код несложный; на самом деле это модификация более старого кода, который мы уже рассматривали в этой серии. Однако на этот раз мы воспользуемся этой идеей немного по-другому, именно для того, чтобы использование приложения было не таким однообразным.
Код будет состоять из двух частей. Одна будет находиться в заголовочном файле, а другая — в индикаторе. Часть, относящаяся к заголовочному файлу и показанная в следующем фрагменте, будет включена в файл C_SimpleChain, с полным содержимым которого вы можете ознакомиться в приложенном файле. Однако нам нужно объяснить следующий фрагмент.
33. //+------------------------------------------------------------------+ 34. bool Interaction(const double errBias, const double errWeight, string &arr[]) 35. { 36. double tmp[], in, out; 37. uchar cArr = 0; 38. 39. if ((m_Info.nNeurons == 0) || (m_Info.nColumns == 0)) 40. return false; 41. 42. ArrayResize(neuron, m_Info.nNeurons); 43. for (uchar c = 0; c < m_Info.nNeurons; c++) 44. neuron[c] = new C_Neuron(m_Info.nColumns - 1, m_Info.fn); 45. ArrayResize(arr, (m_Info.nNeurons + 2) * 2); 46. ArrayResize(tmp, m_Info.Train.Size()); 47. in = out = m_Info.Train[0]; 48. for (uchar count = 1; count < 3; count++) 49. { 50. arr[cArr++] = StringFormat("====== Interaction #%02d ======", count); 51. for (uchar c = 0; c < m_Info.nNeurons; c++) 52. { 53. tmp[0] = (c > 0 ? out : in); 54. arr[cArr++] = StringFormat("Input value in the neuron #%02d: %.8f", c, tmp[0]); 55. out = (*neuron[c]).Perceptron(tmp); 56. } 57. arr[cArr] = StringFormat("Final exit: %.8f || Expected: %.8f", out, m_Info.Train[1]); 58. for (uchar c = 0; c < m_Info.nNeurons; c++) 59. (*neuron[c]).BackPropagation(errBias, errWeight); 60. } 61. 62. for (uchar c = 0; c < m_Info.nNeurons; c++) 63. delete neuron[c]; 64. ArrayFree(neuron); 65. 66. return true; 67. } 68. //+------------------------------------------------------------------+
Фрагмент 03
Обратите внимание, что в строке 34 у нас есть возвращаемый массив типа string. Это позволит нам впоследствии направить собранные данные в более подходящее место. Я рекомендую не пытаться изменять этот код, пока вы действительно не поймёте, как он работает. Я не выполняю проверок границ, чтобы гарантировать отсутствие попыток доступа к недопустимой области памяти. Следовательно, любая нестандартная ситуация приведёт к сбою кода, поскольку он очень чувствителен к малейшим изменениям своей структуры. Тем не менее, я хочу обратить ваше внимание на цикл в строке 48. Здесь мы делаем то же самое, что и в скрипте из предыдущей темы. Однако, если вы обратите внимание на строку 43, то увидите, что при каждом вызове взаимодействия мы будем заново перестраивать сеть перцептронов.
Обратите внимание на эту деталь. Созданная сеть перцептронов будет существовать только внутри этой функции. Это связано с тем, что мы будем использовать эту сеть в индикаторе. И поскольку индикатор не будет заново размещаться на графике при каждом новом взаимодействии, если бы сеть не уничтожалась, мы не смогли бы задать нужный тип взаимодействия. Обратите внимание, что в конце, в строке 58, мы выполняем то, что можно назвать обратным распространением, точно так же, как это делалось в скрипте. Однако, поскольку здесь взаимодействие будет постоянным, всё может показаться несколько запутанным, если не понимать, что сеть существует только в рамках процедуры фрагмента 03. В строке 62 мы входим в цикл, который уничтожит сеть, построенную в рамках этой процедуры.
Возможно, эта система заслуживает отдельной статьи, целиком посвящённой её объяснению. Однако, поскольку большая часть кода уже была должным образом объяснена в начале этой статьи, я не вижу необходимости посвящать отдельную статью исключительно объяснению всего кода. Это не означает, что нам не следует немного подробнее объяснить код индикатора, поскольку в нём есть некоторые части, которые могут вызвать вопросы. Однако у тех, кто уже знаком с программированием на MQL5, таких вопросов не возникнет. По этой причине приведённое здесь объяснение предназначено для тех, кто только начинает программировать и хочет понять, как работает этот индикатор.
Ниже приведён полный код. Поэтому, если вы не уверены, как это работает, внимательно следите за объяснением.
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. #property indicator_chart_window 004. #property indicator_plots 0 005. //+------------------------------------------------------------------+ 006. #define def_NO_MSG 007. //+------------------------------------------------------------------+ 008. #include <Canvas\Canvas.mqh> 009. #include <Neural Network\C_SimplesChain.mqh> 010. //+------------------------------------------------------------------+ 011. input C_Neuron::eFnActivate user00 = C_Neuron::Identity; //Функция активации 012. input uchar user01 = 2; //Количество нейронов 013. input double user02 = 1e-3; //Скорость обучения 014. //+------------------------------------------------------------------+ 015. CCanvas *canvas; 016. C_SimpleChain *gl_Chain; 017. //+------------------------------------------------------------------+ 018. double Train[] { 019. 10, 20 020. }; 021. //+------------------------------------------------------------------+ 022. #define nColumns 2 023. #define nLines Train.Size() / nColumns 024. //+------------------------------------------------------------------+ 025. struct st_00 026. { 027. int x, 028. y, 029. sizeLine; 030. uint height; 031. double errBias, errWeight; 032. }global; 033. //+------------------------------------------------------------------+ 034. void NewPoint(const char lr, const char ud) 035. { 036. #define macroScale(a, b, c) MathAbs(MathAbs(a/b) > c ? c/a : 1/b) 037. 038. double sx, sy; 039. string SzArr[]; 040. 041. (*canvas).Erase(ColorToARGB(clrWhite, 255)); 042. (*canvas).LineVertical(global.x, global.y - global.sizeLine, global.y + global.sizeLine, ColorToARGB(clrRoyalBlue, 255)); 043. (*canvas).LineHorizontal(global.x - global.sizeLine, global.x + global.sizeLine, global.y, ColorToARGB(clrRoyalBlue, 255)); 044. 045. global.errBias += (user02 * lr); 046. global.errWeight += (user02 * ud); 047. 048. if (!(*gl_Chain).Interaction(global.errBias, global.errWeight, SzArr)) return; 049. 050. sx = macroScale(global.errBias, user02, global.sizeLine); 051. sy = macroScale(global.errWeight, user02, global.sizeLine); 052. 053. (*canvas).FillCircle(global.x + (int)(global.errBias * sx), global.y - (int)(global.errWeight * sy), 5, ColorToARGB(clrRed, 255)); 054. (*canvas).FillCircle(global.x + (int)(global.errBias * sx), global.y - (int)(global.errWeight * sy), 3, ColorToARGB(clrBlack, 255)); 055. 056. (*canvas).TextOut(5, global.height * 0, StringFormat("Activation function: %s", EnumToString(user00)), ColorToARGB(clrDarkSlateGray)); 057. (*canvas).TextOut(5, global.height * 2, StringFormat("Bias Error : %.8f", global.errBias), ColorToARGB(clrMediumBlue)); 058. (*canvas).TextOut(5, global.height * 3, StringFormat("Weight Error : %.8f", global.errWeight), ColorToARGB(clrMediumBlue)); 059. for(uint c = 0, l = 5, m = SzArr.Size(), t = m - 2; c < m; c++, l+=(c == t ? 2 : 1)) 060. (*canvas).TextOut(5, global.height * l, SzArr[c], ColorToARGB(c == t ? clrPurple : clrBlack)); 061. 062. (*canvas).Update(true); 063. 064. ArrayFree(SzArr); 065. 066. #undef macroScale 067. } 068. //+------------------------------------------------------------------+ 069. int OnInit() 070. { 071. uint w; 072. 073. ZeroMemory(global); 074. ChartSetInteger(0, CHART_SHOW_DATE_SCALE, false); 075. ChartSetInteger(0, CHART_SHOW_PRICE_SCALE, false); 076. TextGetSize("W", w, global.height); 077. 078. canvas = NULL; 079. 080. gl_Chain = new C_SimpleChain(user00, (uchar)(user01 < 1 ? 1 : user01), Train, nColumns); 081. 082. return INIT_SUCCEEDED; 083. } 084. //+------------------------------------------------------------------+ 085. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 086. { 087. return rates_total; 088. } 089. //+------------------------------------------------------------------+ 090. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 091. { 092. switch (id) 093. { 094. case CHARTEVENT_CHART_CHANGE: 095. if (canvas != NULL) 096. { 097. (*canvas).Destroy(); 098. delete canvas; 099. } 100. 101. canvas = new CCanvas; 102. 103. global.x = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0); 104. global.y = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, 0); 105. 106. (*canvas).CreateBitmapLabel("BL", 0, 0, global.x, global.y, COLOR_FORMAT_ARGB_NORMALIZE); 107. global.x /= 2; 108. global.y /= 2; 109. global.sizeLine = MathMin(global.y, global.x) - 10; 110. 111. NewPoint(0, 0); 112. break; 113. case CHARTEVENT_KEYDOWN: 114. if (TerminalInfoInteger(TERMINAL_KEYSTATE_LEFT)) 115. NewPoint(-1, 0); 116. if (TerminalInfoInteger(TERMINAL_KEYSTATE_RIGHT)) 117. NewPoint(1, 0); 118. if (TerminalInfoInteger(TERMINAL_KEYSTATE_UP)) 119. NewPoint(0, 1); 120. if (TerminalInfoInteger(TERMINAL_KEYSTATE_DOWN)) 121. NewPoint(0, -1); 122. break; 123. } 124. } 125. //+------------------------------------------------------------------+ 126. void OnDeinit(const int reason) 127. { 128. delete gl_Chain; 129. (*canvas).Destroy(); 130. delete canvas; 131. ChartSetInteger(0, CHART_SHOW_DATE_SCALE, true); 132. ChartSetInteger(0, CHART_SHOW_PRICE_SCALE, true); 133. } 134. //+------------------------------------------------------------------+
Код 03
При выполнении этот код даст результат, показанный в следующем видео.
Видео 01
Обратите внимание, что уровень взаимодействия здесь значительно выше, чем раньше, когда мы использовали скрипт. Однако этот индикатор не следует использовать сам по себе. Вам следует использовать его как дополнение, чтобы понять, что происходит внутри сети перцептронов.
Итак, в строке 6 у нас есть определение, которое уже объяснялось в другой статье этой серии. В строке 18 находится обучающий массив. Одно уточнение: вы можете изменять значения в этом массиве, но НЕ МЕНЯЙТЕ КОЛИЧЕСТВО ЗНАЧЕНИЙ. В противном случае у вас возникнут проблемы с этим индикатором. Это связано с тем, что он не реализован так, чтобы допускать более одного входа в создаваемой сети. Кроме того, он также не рассчитан на более чем один выход.
По сути, независимо от того, являетесь ли вы пользователем или начинающим программистом, вы можете изменять только те значения, которые появляются при тестировании этого индикатора в MetaTrader 5. Посмотрите видео, чтобы лучше это понять. Помимо деталей, показанных в видео, вернёмся к коду: в строке сто тринадцать у нас обрабатывается нажатие некоторых клавиш. Они указывают, насколько следует скорректировать смещение и веса. Кроме того, в строке девяносто четыре у нас есть событие, которое помогает адаптировать элементы к размеру графика. Это довольно просто, и я думаю, что вы сможете понять, как это работает, даже если только начинаете.
А теперь посмотрите на строку восемьдесят. В ней мы создаём цепочку перцептронов, формируя таким образом небольшую простую сеть. На этом этапе мы выполняем проверку, чтобы пользователь не ввёл отрицательное значение или даже ноль, поскольку сеть с такими значениями не имела бы смысла. Поэтому, если это происходит, мы принудительно создаём сеть как минимум с одним перцептроном.
Теперь мы можем перейти к ядру этого индикатора — а именно к процедуре в строке тридцать четыре. Это довольно простая процедура, без лишних сложностей. В строке сорок один мы очищаем bitmap, который будет отображаться. Строки сорок два и сорок три создают декартовы оси. В строках сорок пять и сорок шесть корректируются значения ошибки, то есть смещения и веса. Обратите внимание, что шаг определяется значением, которое вы можете настроить при добавлении индикатора на график. Это и есть так называемая скорость обучения. В строке сорок восемь мы выполняем цикл из двух итераций с сетью перцептронов.
Первая итерация показывает начальные значения. Во второй итерации мы видим значения, уже изменённые в результате выполняемой нами настройки. Об этом уже говорилось по ходу статьи. После этого следующие строки — с пятидесятой по шестидесятую — создадут все данные, которые будут показаны. В строке 62 мы отображаем данные на графике символа. В строке 64 мы освобождаем память, выделенную классом C_SimpleChain.
Большая часть этого кода довольно проста. Но обратите внимание на строку 59, где у нас есть цикл for и мы получаем некоторые данные, которые C_SimpleChain поместил в массив типа string. На этом этапе мы можем показать эти данные пользователю. Таким образом, классу C_SimpleChain не нужно будет знать, что мы будем с ними делать. Мы можем отправить их куда угодно или даже преобразовать, чтобы получить другие данные, которые можно будет использовать позже.
Заключительные замечания
Цель этой статьи состояла в том, чтобы очень кратко и просто представить механизм, позволяющий дискретному набору перцептронов работать совместно. В общих чертах этот механизм можно было бы назвать обратным распространением. Однако на самом деле это не его цель; в действительности это лишь интерактивный способ однократно настроить веса и смещения всех перцептронов.
Здесь важно понять, что простое изменение в коде позволило нам уйти от того, что было на Изображении 01, и перейти к чему-то похожему на то, что показано на следующем изображении. 
Изображение 11
Это простое изменение в структуре, которое на первый взгляд может показаться чем-то тривиальным, позволяет увидеть, что мы можем реализовать любую функциональность, если знаем, что делаем. В следующей статье мы рассмотрим, как разработанный здесь механизм может помочь нам лучше понять, как на самом деле работает обратное распространение.| Файл MQ5 | Описание |
|---|---|
| Скрипты\Пример 01 | Базовая демонстрация |
| Индикаторы\Пример 01 | Базовая демонстрация |
Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/13913
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Альтернатива временным барам (Часть 3): Индикатор в подокне для отображения альтернативных баров в MQL5
Нейронная сеть на практике: Начинаем строить цепочку
Создание объектно-ориентированного движка инференса ONNX на языке MQL5
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования