English 中文 Español Deutsch 日本語 Português
Еще раз о картах Кохонена

Еще раз о картах Кохонена

MetaTrader 5Примеры | 20 ноября 2015, 08:44
5 358 42
Mykola Demko
Mykola Demko

Введение

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


Теория карт Кохонена

Карты Кохонена — это однослойная сеть, каждый нейрон которой соединен со всеми компонентами n-мерного входного вектора (паттерна). Входной вектор (паттерн) — это описание одного из объектов, подлежащих кластеризации.

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

Формула 1

где:

  • n — количество нейронов,
  • j — номер нейрона победителя,
  • d(x,w) — расстояние между векторами x и w.

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

Формула 2

В данной реализации поиск нейрона-победителя происходит в функции BestMatchingNode класса CSOM_Net_Base.

Вокруг нейрона-победителя образуется окружение (neighborhood) или радиус обучения (radius of learning). Радиус обучения определяет, какие нейроны подвергаются обучению на данной итерации. Он максимален в начале обучения и уменьшается с ростом количества итераций обучения так, что на последнем этапе в радиус обучения попадает лишь нейрон-победитель.

Окрестности нейрона победителя

Веса нейронов, лежащих в пределах радиуса обучения, адаптируются по правилу Кохонена:

Правило Кохонена

где:

  • x — входной паттерн,
  • k — номер итерации обучения,
  • ni(k) — коэффициент обучения i-го нейрона из радиуса обучения в k-й итерации обучения.

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

Коэффициент скорости обучения ni(k) разбивается на две части:

  • на функцию соседства ni (d,k)

функция соседства

  • и функцию скорости обучения a(k)

функция скорости обучения

где A и B — подбираемые константы.

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

В целом динамику адаптации конкретного нейрона можно представить как схождение по градиенту:

Градиентный спуск

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

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

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


Реализация сети Кохонена и примеры использования

Теперь разберем программную реализацию карт Кохонена. Сама реализация происходит на основе двух измерений: размерности паттернов m_dimension и количества узлов m_total_nodes. Однако визуально карты представляются как трехмерные, где количество узлов m_total_nodes сворачивается в прямоугольник m_xcells * m_ycells. Поэтому узлы nodes хранят не только информацию о весах, но и местоположении на плоскости.

class CSOMNode
  {
protected:
   
   //--- координаты зоны узла
   int               m_x1;
   int               m_y1;
   int               m_x2;
   int               m_y2;
   
   //--- координаты центра узла
   double            m_x;
   double            m_y;
   
   //--- веса узла
   double            m_weights[];
   ...

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

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

class CSOM_Net_Base
class CSOM_Net_Data   : public CSOM_Net_Base
class CSOM_Net_Train  : public CSOM_Net_Data
class CSOM_Net_Img    : public CSOM_Net_Train
class CSOM_Net_Demo   : public CSOM_Net_Img

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

Перечень публичных функций приведен ниже:

class CSOM_Net_Base
  {
public:
   //--- публичный узел сети Кохонена
   CSOMNode         *public_node;
   //--- функция передает заданный ind узел в публичную часть public_node
   void              GetNode(int ind){ public_node=m_som_nodes[ind].GetObjPointer(); };
   //--- функция возвращает размерность паттернов
   int               GetDimension(){return(m_dimension);};
   //--- функция загрузки сети из файла                 
   bool              DownloadNet(string file_name);
   //--- функция нахождения наилучшего узла сети по заданному вектору с применением маски
   int               BestMatchingNode(const double &vector[]);
   //--- функция нахождения наилучшего узла сети по заданному вектору с обрезкой по размеру dimension
   int               BestMatchingNode(const double &vector[],int dimension);
   //--- функция заполнения битной маски для поиска узлов, похожих на паттерны (маска определяет, по каким полям искать)
   bool              InitSetByteMap(int num,bool value);
  };
  
class CSOM_Net_Data : public CSOM_Net_Base
  {
public:
   //--- функция загрузки данных для обучения из заданного файла
   bool              LoadPatternDataFromFile(string filename);
   //--- функция добавления вектора в обучающее множество
   void              AddVectorToPatternsSet(double &vector[],string title);
   //--- функция копирования паттерна из обучающего мн-ва    
   bool              GetPatterns(int ind_pattern,double &vector[]);
   //--- функция возврата титла паттерна из обучающего мн-ва  
   string            GetTitlesPatterns(int ind_pattern);
   //--- функция возврата количества паттернов
   int               GetTotalsPatterns();
  }; 
  
class CSOM_Net_Train : public CSOM_Net_Data
  {
public:
   //--- функция инициализации сети, получения параметров
   void              InitParameters(int iterations,int xcells,int ycells);
   //--- функция обучения сети
   void              Train();
   //--- функция сохранения сети в файл 
   void              SaveNet(string file_name);
   //--- виртуальные функции (тело описывается в потомке CSOM_Net_Img) 
   virtual void      Render(){};
   virtual void      ShowBMP(bool back){};
  }; 
  
class CSOM_Net_Img : public CSOM_Net_Train
  {
public:
   //--- функция инициализации сети, получения параметров
   void              InitParameters(int iterations,int xcells,int ycells,
                                    int bmpwidth,int bmpheight,
                                    bool p_HexagonalCell,bool p_ShowBorders,bool p_ShowTitles,
                                    int p_ColorScheme,int p_MaxPictures);
   //--- функция отображения состояния сети
   void              Render();
   //--- функция показа bmp-образа на графике  
   void              ShowBMP(bool back);
   //--- функция сохранения ресурса в bmp-файл
   void              SaveBMP();
   //--- функция деинициализации, удаляет BMP-картинку с графика
   void              NetDeinit();
  };
  
class CSOM_Net_Demonstration : public CSOM_Net_Img
  {
public:
   //--- функция - пример использования потомка для расширения возможностей
   void              ShowTrainPatterns();
  };

В прикрепленных файлах имеется пять примеров с вариантами использования карт Кохонена. Примеры пронумерованы по порядку каскада классов. Рассмотрим примеры по порядку.


Создание сети, базовый класс

Первый пример для своей работы требует файл с расширением somnet, он используется для сохранения обученной ранее сети. В примере рассмотрен вариант подключения обученной сети к советнику для распознавания паттернов, подаваемых из программной реализации. Для простоты данные файла получаются с помощью функции FileReadArray. Это не требует сложных систем для чтения и распознавания строк (парсеров). К тому же сброс данных и получение данных с помощью массивов происходит быстрее.

В примере Sample1_SOM_Net_Base загружается обученная сеть из двоичного файла, указанного в параметре (без расширения):

input string SOM_Net="SOM\\SOM_Net";

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

Загрузка сети происходит в функции DownloadNet(string file_name). Рассмотрев детально тело функции, становится понятен формат файла somnet. В начале файла записана заглавная часть файла (HEADER), состоящая из трех ячеек памяти типа double (как и весь получаемый из файла массив). Первое поле хранит размерность паттернов плюс шесть. Шесть полей нужны для хранения координат узла. Но поскольку шесть — константное значение, мы легко можем получить размерность паттернов вычитанием шести из первого поля шапки массива. Второе и третье поле хранит переменные m_xcells и m_ycells — размеры по X и Y соответственно в визуальном представлении сети. Из них путем перемножения можно получить количество узлов в сети.

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

Продолжим рассмотрение примера. После того как сеть загружена, заполняется константами паттерн vector[]. Инициализация константами приведена для примера. Функция BestMatchingNode находит индекс наиболее похожего узла. После этого узел передается в публичную часть для безопасного доступа к функциям класса CSOMNode.

Остановимся подробнее на функции BestMatchingNode. Функция имеет три варианта использования. При вызове перегрузки BestMatchingNode(const double &vector[]) без инициализации маски будет произведен поиск по всем полям вектора, так как функция сама проинициализирует маску единицами, то есть разрешит поиск по всем полям. Если же предварительно вызвать инициализацию маски InitSetByteMap(int num,bool value) для каждого поля, то включится фильтрация, по какому полю искать, а по какому нет. Таким образом достигается возможность поиска узла по неполной информации. Если же используется другая перегрузка BestMatchingNode(const double &vector[],int dimension), то фильтрация будет по порядку до поля под номером, переданным в функцию параметром dimension.


Загрузка данных паттернов

Во втором примере Sample2_SOM_Net_Data показано подключение следующего расширения функционала — класса CSOM_Net_Data. Класс объявлен как потомок класса CSOM_Net_Base, поэтому ему доступны все функции предка плюс свои функции. Класс создан для введения в функционал парсера для загрузки паттернов. Паттерны, получаемые парсером, в последующем могут быть использованы как для загрузки паттернов обучения, так и паттернов для распознавания.

Имя файла с паттернами задается параметром:

input string DataFileName="SOM\\optim.csv";

Обратите внимание — имя файла указано с расширением, то есть полное имя файла. Сам файл парсером открывается как файл с флагом FILE_CSV.

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

Тут стоит рассмотреть формат файла с паттернами, на который настроен парсер.

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

Файл с паттернами

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

Парсер при своей работе не только помещает данные паттернов во встроенный массив m_patterns_sets_array, но и распознает заголовки полей, сохраняет их в массив m_som_titles (объявленный в классе-предке), а также заполняет номерами строк массив m_patterns_titles. Таким образом, найти нужный паттерн в файле по его строке не составляет труда.


Обучение сети

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

Для последующих классов необходимо, чтобы в функции обучения сети Train() были вызваны функции обсчета и рисования bmp-картинки, изображающие этапы обучения. Но поскольку в третьем примере графики нет, то тела данных функций здесь не нужны. Чтобы разрешить это противоречие, функции Render() и ShowBMP(bool back) объявлены как виртуальные (virtual) и имеют пустые тела, а требуемый код определен в потомке CSOM_Net_Img.

Теперь перейдем непосредственно к обучению. Перед обучением сетке требуется передать параметры. Для этого есть сервисная функция InitParameters(int iterations,int xcells,int ycells), в которую передаются параметры: количество итераций обучения — iterations и размер сети, разделенный на два параметра — xcells, ycells (размер по X и Y соответственно).

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

Сама итерация обучения специально выведена в отдельную функцию TrainIterations(int &p_iter) для более простого понимания кода и легкого доступа при доработке. Ведь данная реализация написана для того, чтобы другие программисты легко могли понять суть и сделать при необходимости свои правки. В функции реализован алгоритм, описанный в разделе "Теория карт Кохонена", поэтому останавливаться на ней не будем.

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

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

void CSOM_Net_Train::SaveNet(string file_name)
bool CSOM_Net_Base::DownloadNet(string file_name)

Бинарное представление файла somnet

Среди данных сети тип double преобладает. Поэтому сеть сохраняется в одномерный массив типа double. Остальные данные приводятся к этому типу.

Для начала в сеть сохраняется шапка. Это пять первых полей double, в которые сохраняются необходимые данные о сети.

  1. 6+dimension_node — длина паттерна плюс 6 (шесть дополнительных полей в узле необходимы для хранения координат).
  2. m_xcells — количество узлов по горизонтали.
  3. m_ycells — количество узлов по вертикале.
  4. m_xsize — размер bmp по горизонтали.
  5. m_ysize — размер bmp по вертикале.

Далее идет копирование в сеть узлов. Сначала 6 данных о координатах узла, затем — веса узла. Так, узел за узлом, копируются все данные.

В конце к сети добавляется строка с перечислением Titles, бинарно переведенная в формат double.

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


Графическая оболочка

В четвертом примере Sample4_SOM_Net_Img показано подключение графической оболочки. Для ее вызова в предыдущей статье было необходимо подключение графической библиотеки cintbmp.mqh, написанной Дмитрием Федосеевым. Но мне пришлось переработать ее так, чтобы убрать все вызовы WinAPI DLL. Это позволит использовать код в Маркете. В обновленном файле сохранена информация об авторе, но я сменил имя на cintbmp2.mqh.

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

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

void   InitParameters(int iterations,int xcells,int ycells,
                       int bmpwidth,int bmpheight,
                       bool p_HexagonalCell,bool p_ShowBorders,bool p_ShowTitles,
                       int p_ColorScheme,int p_MaxPictures);

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

Далее следует вызов функции обучения Train() из экземпляра класса CSOM_Net_Train. Но поскольку теперь в телах функций Render() и ShowBMP(bool back) есть код работы с графикой, это приводит к отображению процесса обучения на каждой сотой итерации. После выхода из Train() эти функции вызываются для отображения последних изменений.

Окончание обучения


Расширение функционала

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

Для чего же нужно писать класс-потомок? А для того чтобы в нем были доступны все скрытые командой protected функции всех классов, от которых наш класс наследуется. Например, подключившись как потомок класса CSOM_Net_Img, мы получим доступ ко всем функциям и данным, объявленным как protected и public всех предыдущих классов. Таким образом, можно компоновать программу по своему усмотрению, в зависимости от того, что вы хотите получить.

Итак, расширение класса CSOM_Net_ Player — графическая панель управления нейросетью Кохонена. Для ее написания я подключил файл IncGUI_v3.mqh с графической библиотекой Дмитрия Федосеева, лишь слегка ее доработав. Доработка коснулась вывода графических меток (не сбоку, а сверху объектов) и цвета меток. Я не делал правки в оригинальном файле, а лишь объявил наследников от классов библиотеки.

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

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

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

Важно: переключение режимов сбрасывает режим в нулевое состояние.

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

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

Таким образом, если не было прерывания (если состояние кнопок "Старт" и "Обучение" не изменилось, и у нас все так же доступен вход на обучение), происходит новая итерация обучения и отправка нового сообщения 333. И так далее, пока сеть не обучится и сама не прервет цикл, или пользователь кнопками не остановит обучение.

Такая же схема прерывания реализована и в режиме поиска паттернов.

Я старался сделать интерфейс панели интуитивно понятным. В основном он повторяет input переменные из предыдущих примеров. Но все же покажем значение некоторых кнопок и полей ввода:

Кнопки плеера

Для запуска панели требуется:

  • Разложить файлы из zip-архива по директориям.
  • Файлы с паттернами — в директорию MQL5\Files\SOM\.
  • Файлы с bmp-картинками кнопок — в директорию MQL5\Images\SOM\.
  • Файлы с программами — в директорию MQL5\Projects\SOM\.
  • Провести компилирование всех mq5-файлов.


Заключение

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

Прикрепленные файлы |
project_som.zip (267.78 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (42)
fxsaber
fxsaber | 16 окт. 2017 в 14:10
Andrey Litvichenko:

А оператор _W  будет работать для обратного преобразования, то есть для

?

Да, в обе стороны. Здесь примеры.

Andrey Litvichenko
Andrey Litvichenko | 16 окт. 2017 в 14:19
fxsaber:

Да, в обе стороны. Здесь примеры.


Спасибо, все заработало

Oleg Mironov
Oleg Mironov | 15 дек. 2018 в 18:35

Добрый день,

К сожалению обратился к статье поздно, сейчас в архиве явно не хватает файлов

CSOM_Net_Base,
CSOM_Net_Train

Это сбой или ограничение доступа?

Stanislav Korotky
Stanislav Korotky | 16 дек. 2018 в 14:54
Олег:

Добрый день,

К сожалению обратился к статье поздно, сейчас в архиве явно не хватает файлов

Это сбой или ограничение доступа?

Прикладываю файлы - вроде их уже спрашивали и выкладывали ранее.

Готовятся новые статьи по теме.

Oleg Mironov
Oleg Mironov | 17 дек. 2018 в 08:54
Stanislav Korotky:

Прикладываю файлы - вроде их уже спрашивали и выкладывали ранее.

Готовятся новые статьи по теме.

Спасибо за файлы, с удовольствием ознакомлюсь с новой статьей.

Защита от ложных срабатываний торгового робота Защита от ложных срабатываний торгового робота
Прибыльность торговых систем определяется не только логикой и точностью анализа динамики финансовых инструментов, но и качеством алгоритма исполнения этой логики. Характерным проявлением некачественного исполнения основной логики торгового робота являются ложные срабатывания. В статье рассмотрены варианты решения указанной проблемы.
MQL5 для начинающих: Антивандальная защита графических объектов MQL5 для начинающих: Антивандальная защита графических объектов
Что должна делать ваша программа, если графические панели управления были удалены или изменены кем-то еще? В этой статье мы покажем, как после удаления приложения не иметь на графике "бесхозные" объекты, и как не потерять над ними контроль в случае переименования или удаления созданных программно объектов.
Индикатор "Канат" Эрика Наймана Индикатор "Канат" Эрика Наймана
В статье описывается построение индикатора «Канат» по книге Эрика Л. Наймана «Малая энциклопедия трейдера». Этот индикатор показывает направление тренда на основе расчетных величин быков и медведей за указанный период. В статье изложены принципы построения и расчета индикатора с примерами кода, на основе индикатора построен эксперт и произведена оптимизация внешних параметров.
Обработка ошибок и логирование в MQL5 Обработка ошибок и логирование в MQL5
В статье рассматриваются общие вопросы обработки ошибок в программном обеспечении. Кроме того, затрагивается тема логирования и демонстрируется пример реализации логгера средствами MQL5.