Определяем константы и перечисления

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

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

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

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

Работа, проделанная на данном этапе, будет нашей «дорожной картой» в создании проекта. Детальное рассмотрение организации интерфейсов обмена данными позволяет оценить необходимость наличия той или иной информации на каждом этапе. Также это дает возможность определить источники информации и выявить возможный дефицит данных. А ликвидировать дефицит данных на стадии проектирования будет гораздо легче, чем уже в процессе реализации, ведь тогда нам придется возвращаться на стадию проектирования для поиска источников необходимых данных. Затем необходимо будет продумывать возможные варианты передачи информации от источника к месту ее обработки и попытаться «впихнуть» в уже выстроенную архитектуру с минимальными доработками. Это повлечет за собой непредсказуемое количество правок в уже выстроенных процессах, при этом необходимо будет оценить влияние этих правок на смежные процессы.

Все файлы выстраиваемой библиотеки мы соберем в подкаталоге NeuroNetworksBook\realization в соответствие со структурой файлов.

Все глобальные константы нашего проекта соберем в один файл defines.mqh.

Какие же константы мы будем определять?

Давайте посмотрим на архитектуру проекта. Мы уже проговорили, что результатом нашей работы будет некий класс, охватывающий полную организацию работы нейронной сети. В архитектуре MQL5 все объекты наследуются от базового класса CObject. В нем определен виртуальный метод для идентификации класса Type, который возвращает целочисленное значение. Следовательно, для однозначной идентификации нашего класса мы должны определить некую константу, желательно отличную от констант уже имеющихся классов. Это будет прототипом визитной карточки нашего класса внутри программы. Для создания именованных констант воспользуемся механизмом макроподстановки.

#define defNeuronNet             0x8000

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

#define defArrayLayers           0x8001

Дальше по структуре будет создаваться отдельный класс для нейронного слоя. Позже, когда мы подойдем к вопросам реализации алгоритмов вычислений с использованием технологии OpenCL, мы поговорим об организации векторных вычислений и средствах передачи данных в память GPU. В данном ключе будет не очень удобно создавать классы для каждого нейрона, но потребуется класс для хранения информации и буферной организации обмена данными. Таким образом, мы должны создать «визитные карточки» и для этих объектов.

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

#define defBuffer                0x8002
#define defActivation            0x8003
#define defLayerDescription      0x8004
#define defNeuronBase            0x8010
#define defNeuronConv            0x8011
#define defNeuronProof           0x8012
#define defNeuronLSTM            0x8013
#define defNeuronAttention       0x8014
#define defNeuronMHAttention     0x8015
#define defNeuronGPT             0x8016
#define defNeuronDropout         0x8017
#define defNeuronBatchNorm       0x8018

С константами идентификаторов объектов определились, двигаемся дальше. Давайте вспомним, с чего начинается данная книга. В самом начале книги мы рассмотрели математическую модель нейрона. Каждый нейрон имеет функцию активации. Мы рассмотрели несколько вариантов функций активации, и все они имеют право на жизнь. Ввиду отсутствия производной мы исключим из списка пороговую функцию, но остальные рассмотренные функции реализуем с использование технологии OpenCL. В случае работы на CPU воспользуемся векторными операциями, в которых уже реализованы функции активации. Чтобы сохранить единство подходов, для указания используемой функции активации воспользуемся стандартным перечислением указания функции ENUM_ACTIVATION_FUNCTION.

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

//--- функции активации подвыборочного слоя
enum ENUM_PROOF
  {
   AF_MAX_POOLING,
   AF_AVERAGE_POOLING
  };

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

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

enum ENUM_OPTIMIZATION
  {
   None=-1,
   SGD,   
   MOMENTUM,
   AdaGrad,
   RMSProp,
   AdaDelta,
   Adam
  };

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

После методов обучения мы с вами говорили о приемах повышения сходимости нейронных сетей. Здесь все довольно просто. Нормализация и dropout будут организованы отдельными слоями — для них мы уже задали константы при рассмотрении нейронных слоев. Регуляризацию предлагаю реализовать одну — Elastic Net. Управлять процессом будем через переменные λ1 и λ2. Если обе переменные равны нулю, регуляризация отключена. В случае, когда один из параметров равен нулю, получим L1 или L2-регуляризацию, в зависимости от ненулевого параметра.

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

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

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

#define TYPE                      double
#define MATRIX                    matrix<TYPE>
#define VECTOR                    vector<TYPE>

Аналогичную макроподстановку организуем и для OpenCL-программы.

#resource "opencl_program.clas string OCLprogram
//---
#define LOCAL_SIZE                256
const string ExtType=StringFormat("#define TYPE %s\r\n"
                                  "#define TYPE4 %s4\r\n"
                                  "#define LOCAL_SIZE %d\r\n",
                                   typename(TYPE),typename(TYPE),LOCAL_SIZE);
#define cl_program                ExtType+OCLprogram

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

#define defLossSmoothFactor       1000
#define defLearningRate           (TYPE)3.0e-4
#define defBeta1                  (TYPE)0.9
#define defBeta2                  (TYPE)0.999
#define defLambdaL1               (TYPE)0
#define defLambdaL2               (TYPE)0

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