- Описание архитектуры и принципов реализации
- Построение средствами MQL5
- Класс функции активации
Построение средствами MQL5
Приступая к реализации полносвязного нейронного слоя, следует учесть, что это будет базовый класс для всех последующих архитектурных решений нейронных слоев. Поэтому мы должны сделать его максимально универсальным с возможностью расширения функций. При этом расширения должны максимально легко вписываться в существующее решение.
Начнем с создания нашего базового класса нейронного слоя CNeuronBase с наследованием от класса CObject. Затем определим внутренние переменные класса:
- m_cOpenCL — указатель на экземпляр класса работы с технологией OpenCL;
- m_cActivation — указатель на объект функций активаций;
- m_eOptimization — тип метода оптимизации нейронов при обучении;
- m_cOutputs — массив значений на выходе нейронов;
- m_cWeights — массив весовых коэффициентов;
- m_cDeltaWeights — массив для накопления невыполненных обновлений весовых коэффициентов (суммарный градиент ошибки для каждого весового коэффициента после последнего обновления);
- m_cGradients — градиент ошибки на выходе нейронного слоя в результате последней итерации обратного прохода;
- m_cMomenum — в отличие от остальных переменных это будет массив из двух элементов для записи указателей на массивы накопления моментов.
Для облегчения доступа к переменным из классов-наследников все переменные будут объявлены в блоке protected.
В конструкторе класса проведем инициализацию вышеуказанных переменных параметрами по умолчанию. Я указал метод оптимизации Adam и Swish в качестве функции активации, вы же можете выбрать свой любимый метод оптимизации и функцию активации. Указатель на класс работы с OpenCL оставим пустым, создадим экземпляры для всех остальных используемых классов.
CNeuronBase::CNeuronBase(void) : m_eOptimization(Adam) |
Сразу создадим деструктор класса, чтобы не забыть про очистку памяти после работы класса.
CNeuronBase::~CNeuronBase(void) |
Следующим создадим метода инициализации нейронного слоя. В параметрах метод получает экземпляр класса CLayerDescription с описанием создаваемого слоя. Чтобы не путаться в тонкостях алгоритма метода, предлагаю разбить его на отдельные логические блоки.
Начинается метод с блока проверки входящих параметров. Прежде всего проверяем действительность указателя на объект. Затем проверяем тип создаваемого слоя и количество нейронов в слое: в каждом слое должен быть хотя бы один нейрон, ведь с логической точки зрения построения нейронной сети слой без нейронов блокирует прохождение сигнала и парализует всю сеть. Обратите внимание, при проверке типа создаваемого слоя мы используем виртуальный метод Type, а не возвращаемую им константу defNeuronBase. Это очень важный момент для будущего наследования класса. Дело в том, что при использовании константы вызов такого метода для классов-наследников всегда бы возвращал false при попытке создать слой отличный от базового. Использование виртуального метода позволяет нам получить константу-идентификатор конечного класса-наследника, а проверка даст истинный результат сравнения заданного типа нейронного слоя и создаваемого объекта.
bool CNeuronBase::Init(const CLayerDescription *desc) |
В следующем блоке проверим действительность ранее созданных буферов для записи исходящего из нейронного слоя потока данных и градиента к ним (при необходимости создаем новые экземпляры класса). Инициализируем массивы нулевыми значениями.
//--- создание буфера результатов |
После этого проверим количество элементов входного сигнала. В случае использования нейронного слоя в качестве массива входящего сигнала у нас не будет предшествующих нейронных слоев и не потребуются остальные буферы для данных. Мы можем безболезненно их удалить и очистить память. Затем проверим действительность указателя на объект в m_cOpenCL и при положительном результате проверки создадим копию буфера данных в контексте OpenCL.
//--- удаление не используемых объектов для слоя исходных данных |
Последующий код метода выполняется только при наличии предшествующих нейронных слоев. Создадим и инициализируем экземпляр метода функции активации. Этот процесс мы вынесли в отдельный метод SetActivation, который сейчас просто вызываем. Непосредственно алгоритм метода SetActivation рассмотрим немного позже.
//--- инициализация объекта функции активации |
Следующим шагом проведем инициализацию матрицы весовых коэффициентов. Определим количество элементов в матрице и инициализируем ее случайными значениями по методу Ксавье. В случае использования в качестве функции активации LReLU будем использовать метод Хе.
//--- инициализация объекта матрицы весов |
Нам осталось провести инициализацию буферов дельт и моментов. Размер буферов будет равен размеру матрицы весов, инициализируем мы их нулевыми значениями. Следует помнить, что не все методы оптимизации одинаково используют матрицы моментов. Поэтому матрицы моментов мы будем инициализировать в зависимости от метода оптимизации. Лишние массивы очистим и удалим, чтобы освободить память для полезного использования.
//--- инициализация объекта накопления градиентов на уровне матрицы весов |
case MOMENTUM: |
case AdaDelta: |
default: |
В заключение метода сохраним заданный способ оптимизации весовых коэффициентов.
Метод SetOpenCL служит для сохранения указателя на объект работы с контекстом OpenCL и выглядит проще метода инициализации. Но и в нем есть свои нюансы. В отличие от всех рассмотренных ранее методов, мы не завершаем работу метода при получении недействительного указателя на объект. Это связано с тем, что мы не вводим в каждый класс нейронного слоя флаг использования технологии OpenCL. Вместо этого мы используем один флаг в базовом классе нейронной сети. В свою очередь, для проверки использования технологии внутри класса мы можем проверить действительность указателя в переменной m_cOpenCL.
Следует обратить внимание на тот момент, что все объекты нейронной сети работают в одном контексте OpenCL. И до всех объектов доводится указатель на один и тот же объект класса CMyOpenCL. При подобном подходе, удаление экземпляра класса в одном из объектов нейронной сети сделает недействительным указатель во всех, использующих его, объектах. И флаг может не соответствовать текущему состоянию указателя. К тому же в случае отключения использования технологии мы оставляем возможность указания пустого значения указателя на объект.
Поэтому и код нашего метода можно условно разделить на две части. Первая часть кода будет отрабатывать при получении недействительного указателя на объект. В этом случае нам необходимо очистить все ранее созданные буфера данных в контексте OpenCL.
bool CNeuronBase::SetOpenCL(CMyOpenCL *opencl) |
Вторая часть метода будет выполняться при получении действительного указателя на объект работы с контекстом OpenCL. Здесь мы организуем создание новых буферов данных в указанном контексте OpenCL для всех объектов текущего класса.
if(m_cOpenCL) |
if(m_cActivation) |
Ранее мы говорили о выделении в отдельный метод процедуры инициализации функции активации. Предлагаю рассмотреть данный метод для завершения описания процесса инициализации нового объекта. Это один из немногих методов, в котором мы не организовываем блок проверки полученных данных. Проверка параметров функции активации не предоставляется возможным ввиду различия диапазона допустимых значений при использовании различных функций. В большинстве случаев диапазон их значений ограничивается только здравым смыслом и архитектурными требованиями модели.
Что же касается выбора функции активации, то он существует неявно, в виде перечисления допустимых значений. Но даже если пользователь подставит значение не из перечисления, то создавать объекты функции активации мы будем в теле оператора выбора switch. А значит, у нас будет неявный контроль типа функции активации, и при отсутствии указанного значения в функции выбора мы создадим базовый класс без функции активации.
Необходимость создания базового класса обусловлена сохранением работоспособности класса без использования функции активации в стандартном режиме. Как вы увидите позже, в некоторых случаях мы будем использовать нейронные слои без функций активации.
bool CNeuronBase::SetActivation(ENUM_ACTIVATION_FUNCTION function, VECTOR ¶ms) |
switch(function) |
case AF_SIGMOID: |
case AF_LRELU: |
case AF_TANH: |
case AF_SOFTMAX: |
case AF_SWISH: |
default: |
После создания экземпляра объекта требуемой функции активации мы передаем в новый объект параметры функции и указатель на объект контекста OpenCL.
if(!m_cActivation.Init(params[0], params[1])) |
Операции прямого прохода будут реализованы в методе FeedForward. В параметрах метод получает указатель на объект предыдущего слоя. Благодаря тому, что классы всех нейронных слоев планируется строить на основе одного базового класса, в параметрах метода мы можем использовать класс базового нейронного слоя для получения указателя на предыдущий слой любого типа. При этом использование виртуальных методов доступа к внутренним объектам класса позволяет выстроить универсальный интерфейс без привязки к конкретному типу нейронного слоя.
В начале метода проверим действительность указателей на все используемые в методе объекты. Это наши исходные данные: полученный в параметрах указатель на предыдущий слой, а также содержащийся в нем буфер выходных состояний нейронов. Вместе с ними проверим указатели на матрицу весовых коэффициентов и буфер для записи результатов прямого прохода текущего слоя — буфер выходных состояний нейронов текущего слоя. Нелишним будет проверить и указатель на экземпляр класса для расчета значений функции активации.
bool CNeuronBase::FeedForward(CNeuronBase * prevLayer) |
Затем проверим указатель на объект работы с OpenCL. Если указатель действительный — перейдем к блоку использования данной технологии. О нем мы поговорим чуть позже при рассмотрении организации процесса параллельных вычислений. При недействительном указателе на объект или его отсутствии перейдем к блоку вычислений стандартными средствами MQL5. Здесь мы сначала проверим соответствие размеров матриц и переформатируем матрицу исходных данных в вектор, добавив единичный элемент для bias-смещения. Выполним операцию матричного умножения на матрицу весовых коэффициентов. Результат запишем в буфер исходящего потока. Перед выходом из метода не забудем вычислить значения функции активации на выходе нейронного слоя.
//--- разветвление алгоритма в зависимости от устройства выполнения операций |
else |
За прямым проходом идет обратный проход. Эту процедуру обучения нейронной сети мы разбиваем на составные части и создаем четыре метода:
- метод расчета градиента ошибки на выходе нейронной сети CalcOutputGradient,
- метод распространения градиента через скрытый слой CalcHiddenGradient,
- метод необходимого расчета корректирующих значений для весовых коэффициентов CalcDeltaWeights,
- метод обновления матрицы весовых коэффициентов UpdateWeights.
Мы будем двигаться по пути потока данных и рассмотрим алгоритм каждого метода.
В процессе обучения с учителем после прямого прохода расчетные значения на выходе нейронной сети сравниваются с эталонными значениями. В этот момент определяется отклонение на каждом нейроне выходного слоя. Эту операцию мы выполняем в методе CalcOutputGradient. Алгоритм данного метода довольно прост: в параметрах метод получает массив эталонных значений и тип используемой функции потерь. В начале метода мы проверим действительность указателей на используемые объекты, а также соответствие размеров массивов.
bool CNeuronBase::CalcOutputGradient(CBufferType * target, ENUM_LOSS_FUNCTION loss) |
Далее, как и в методе прямого прохода, создадим разветвление алгоритма в зависимости от устройства проведения вычислений. Алгоритм с использованием технологии OpenCL будет рассмотрен в следующей главе, а сейчас посмотрим на построение процесса средствами MQL5.
Давайте посмотрим на процесс вычисления градиента ошибки на выходе нейронной сети. На первый взгляд, мы должны двигаться в сторону минимизации ошибки на каждом нейроне. То есть вычислять разницу между эталонным и расчетным значениями и минимизировать эту разницу. В таком случае мы получаем линейную зависимость ошибки и градиента. Это справедливо при использовании средней абсолютной ошибки в качестве функции потерь со всеми вытекающими преимуществами и недостатками.
Но говоря о функции потерь, мы с вами рассматривали и другие варианты, обсуждали их преимущества и недостатки. Но как нам воспользоваться их преимуществами? Ответ тут довольно прост. Нужно рассматривать функцию потерь и обучаемую модель как единую сложную функцию. В этом случае мы должны минимизировать не отклонение на каждом нейроне выходного слоя, а непосредственно значение функции потерь. И так же, как и при проведении градиента ошибки через нейронную сеть, мы определяем производную функции потерь и умножаем ее на отклонение значения функции потерь от нуля. При этом если для MAE и MSE мы можем взять в качестве ошибки только производную функции потерь и пренебречь умножением на значение функции потерь, так как это линейное масштабирование будет компенсировано коэффициентом обучения, то при использовании кросс-энтропии мы вынуждены осуществить умножение на значение функции потерь. Дело в том, что при равенстве целевого и расчетного значений функция потерь даст 0, а ее производная будет равна −1. И если мы производную не умножим на ошибку, то продолжим корректировать параметры модели при отсутствии ошибки.
При этом вовсе не обязательно полностью вычислять значение функции потерь. Кросс-энтропия в качестве функции потерь чаще всего используется при решении задач классификации. Следовательно, в качестве целевых значений мы ожидаем получить вектор, в котором только один элемент будет содержать единицу, а все остальные будут равны нулю. Для нулевых значений производная тоже будет равна нулю, а умножение на 1 не изменяет результат. Поэтому нам достаточно производную умножить логарифм расчетного значения. Именно логарифм от 1 даст 0, указывающий на отсутствие ошибки.
С учетом вышесказанного для вычисления соответствующего градиента ошибки на выходе модели воспользуемся переключателем switch, на котором создадим разветвление процесса в зависимости от используемой функции потерь. В случае отсутствия указанной функции потерь мы посчитаем простое отклонение расчетных результатов от целевых значений.
//--- разветвление алгоритма в зависимости от устройства выполнения операций |
case LOSS_BCE: |
После получения ошибки на выходе нейронной сети необходимо определить, какое влияние на нее оказывает каждый нейрон нашей сети. Для этого нужно слой за слоем распространить градиент ошибки до каждого нейрона. За организацию цикла перебора слоев нейронной сети отвечает менеджер сети — базовый класс нейронной сети CNet. Сейчас мы рассмотрим организацию процесса внутри одного нейронного слоя.
В параметрах метод CalcHiddenGradient получает указатель на предыдущий слой нейронной сети. Он нам потребуется для записи передаваемого градиента ошибки. В предыдущем методе мы определили ошибку на выходе нейрона, но выходное значение нейрона зависит от функции активации. Чтобы определить влияние каждого элемента исходных данных на конечный результат, нужно исключить влияние на ошибку функции активации. Для этого скорректируем градиент ошибки на производную функции активации. Эта операция, как и вычисление самой функции активации, вынесена в отдельный класс.
bool CNeuronBase::CalcHiddenGradient(CNeuronBase *prevLayer) |
Далее идет блок проверок указателей используемых объектов. Сначала проверим действительность полученного указателя на предыдущий слой. Затем извлечем и проверим указатели на буферы результатов и градиентов предыдущего слоя. Также проверим соответствие количества элементов в указанных буферах. Кроме того, проверим наличие достаточного количества элементов в матрице весов. Такое количество превентивных контролей необходимо для стабильной работы метода и исключения возможных ошибок при обращении к массивам данных.
//--- проверка буферов предыдущего слоя |
После успешного прохождения всех проверок переходим непосредственно к вычислительной части. Напомню, производная от произведения переменной на константу является константой. В данном случае производной по нейрону является соответствующий весовой коэффициент. Соответственно, влияние нейрона на результат является произведением градиента ошибки на выходе из функции на соответствующий весовой коэффициент. Сумму таких произведений мы и посчитаем для каждого нейрона предыдущего слоя. Полученные значения запишем в соответствующую ячейку буфера градиентов предыдущего слоя.
Как и в описанных выше методах мы осуществляем разделение алгоритма в зависимости от используемого вычислительного устройства. С алгоритмом реализации многопоточных вычислений мы познакомимся немного позже. Сейчас же рассмотрим реализацию алгоритма средствами MQL5. Как уже сказано выше, нам необходимо для каждого нейрона собрать сумму произведений градиентов ошибки зависящих от него нейронов на соответствующие весовые коэффициенты. Выполнение данной операции легко осуществляется с использованием операции матричного умножения. В данном случае достаточно умножить матрицу градиентов ошибки на матрицу весовых коэффициентов. Результат операции запишем в локальную матрицу.
Мы не можем сразу записать результат операции матрицу градиентов ошибки предыдущего слоя. Если вы посмотрите на метод прямого прохода, то увидите, как мы добавляли элемент bias-смещения. Соответственно, при умножении матриц мы получим результат с учетом ошибки на элементе смещения. Но предыдущий слой не ожидает это значение, и размер матрицы градиентов меньше. Поэтому мы сначала изменим размер матрицы, полученной в результате операции умножения, до требуемых размеров, а затем перенесем ее значения в матрицу градиентов предыдущего слоя.
Обратите внимание, что в данном методе мы не корректируем полученный на выходе предыдущего слоя градиент на производную функцию активации нейронов в предыдущем слое, ведь именно с аналогичной операции мы начинали данный метод. Поэтому если предыдущий слой является скрытым слоем нашей сети, то первое, что будет сделано при вызове рассмотренного метода на нижнем слое, — это корректировка градиента на производную функции активации. Задвоение операции приведет к ошибкам.
//--- разветвление алгоритма в зависимости от устройства выполнения операций |
Теперь у нас есть посчитанный градиент ошибки на каждом нейроне нашей сети. Данных достаточно для проведения обновления весовых коэффициентов. Но мы помним, что обновление весовых коэффициентов не всегда будет осуществляться после каждой итерации обратного прохода. Поэтому процесс обновления матрицы весовых коэффициентов мы разбили на два метода. В первом мы определим градиент ошибки для каждого весового коэффициента по аналогии с градиентом ошибке на нейроне предыдущего слоя, а во втором произведем корректировку матрицы весов.
Вычислять величину градиент ошибки для матрицы весовых коэффициентов будем в методе CalcDeltaWeights. В параметрах метода, как и предыдущего, будет указатель на предшествующей слой нейронной сети, но теперь из него мы будем использовать не буфер градиентов, а массив выходных значений.
По аналогии с ранее рассмотренными методами метод начинается с блока проверок. За ним идет блок вычислений.
bool CNeuronBase::CalcDeltaWeights(CNeuronBase *prevLayer, bool read);
|
В предыдущем методе мы уже скорректировали градиент на производную функции активации. Поэтому эту итерацию мы пропустим и перейдем сразу к непосредственному расчету градиента на весовых коэффициентах. Здесь, как в других методах, идет разветвление алгоритма по устройству вычислений. В блоке MQL5 аналогично предыдущему методу мы воспользуемся матричным умножением, ведь, по существу, оба метода выполняют аналогичную операцию только для разных матриц. Но здесь есть несколько отличий.
Во-первых, в предыдущем методе мы убирали элемент bias-смещения. Сейчас же нам наоборот надо добавить единичный элемент к вектору результатов предыдущего слоя для определения градиента ошибки на соответствующем весовом коэффициенте.
Во-вторых, ранее мы умножали матрицу градиентов на матрицу весов. Теперь мы умножаем транспонированную матрицу градиентов ошибки на вектор результатов предыдущего слоя с элементом смещения.
Кроме того, мы перезаписывали градиент ошибки предыдущего слоя, а градиент весовых коэффициентов мы будем суммировать и тем самым накапливать градиент ошибки за весь период между операциями обновления весовых коэффициентов.
//--- разветвление алгоритма в зависимости от устройства выполнения операций |
В заключение процесса обратного прохода нам остается скорректировать матрицу весовых коэффициентов. Для выполнения этого функционала в нашем классе предусмотрен метод UpdateWeights. Не будем забывать, что у нас возможны варианты с выбором метода оптимизации. Вопрос был решен простым и наглядным способом. В публичном методе обновления весовых коэффициентов реализована диспетчерская функция по выбору метода оптимизации в соответствии с выбором пользователя. Непосредственно процесс корректировки матрицы вынесен в отдельные методы, по одному методу на каждый вариант оптимизации.
bool CNeuronBase::UpdateWeights(int batch_size, TYPE learningRate, |
case SGD: |
Алгоритмы каждого из методов оптимизации весовых коэффициентов уже были представлены при изучении их особенностей. Мы не будем их здесь дублировать, но вынесем в блок protected нашего базового класса нейронного слоя.
Выше мы уже рассмотрели реализацию операций прямого и обратного прохода в полносвязном нейронном слое. Но мы же не будем заново обучать нейронную сеть при каждом запуске. Поэтому нам нужны методы работы с файлами: записи и чтения данных состояния нейронного. Мы должны экономно относиться к нашим ресурсам, поэтому давайте подумаем какую информацию мы будем сохранять. Общее привило: нужно сохранять минимум информации, но ее должно быть достаточно для быстрого запуска и функционирования класса как без прерывания процесса. Давайте посмотрим на внутренние переменные класса и критически оценим необходимость сохранения их содержимого в файл.
- m_cOpenCL — указатель на экземпляр класса работы с технологией OpenCL, который отвечает за отдельный функционал, но не содержит дополнительной информации. Не подлежит записи в файл.
- m_cActivation — указатель на объект функций активаций. Тип функции активации задается пользователем при конструировании нейронной сети. Использование другой функции активации может привести к искажению результатов работы всей сети. Сохраняем.
- m_eOptimization — тип метода оптимизации нейронов при обучении, который задается пользователем при конструировании нейронной сети. Влияет на процесс обучения. Сохраняем.
- m_cOutputs — массив значений на выходе нейронов. Количество элементов задается архитектором нейронной сети. Содержимое перезаписывается при каждом прямом проходе. Достаточно сохранить количество нейронов в слое и не сохранять весь массив.
- m_cWeights — матрица весовых коэффициентов. Значение элементов формируется в процессе обучения нейронной сети. Сохраняем.
- m_cDeltaWeights — матрица для накопления не выполненных обновлений весовых коэффициентов (суммарный градиент ошибки для каждого весового коэффициента после последнего обновления). Значения накапливаются между обновлениями матрицы весов и обнуляются после корректировки весов. Размер массива равен матрице весовых коэффициентов. Не подлежит записи в файл.
- m_cGradients — градиент ошибки на выходе нейронного слоя в результате последней итерации обратного прохода. Содержимое перезаписывается при каждом обратном проходе. Размер массива равен буферу выходного сигнала. Не подлежит записи в файл.
- m_cMomenum — в отличие от остальных переменных, это будет массив из двух элементов для записи указателей на массивы накопления моментов. Использование буферов зависит от метода оптимизации. Содержимое накапливается в процессе обучения нейронной сети. Сохраняем.
После определения объема данных для записи в файл приступим к созданию метода записи в файл Save. Этот виртуальный метод существует во всех классах-наследниках класса CObject. В параметрах метод получает хендл файла для записи.
В теле метода мы сначала проверим полученный хендл и действительность указателя на буфер результатов нейронного слоя. Как мы помним, нейронный слой может использоваться как с полным функционалом, так и нет. При использовании объекта в качестве слоя исходных данных мы удаляли все буферы, кроме буфера исходных данных. Поэтому наличие этого буфера является обязательным для нейронного слоя. Если какой-либо из контролей не пройден, выходим из метода с результатом false.
Далее записываем в файл тип нейронного слоя и размер буфера результатов. При этом не забываем проверить результат выполнения операций.
bool CNeuronBase::Save(const int file_handle) |
После успешной записи размера буфера результатов мы проверяем действительность указателей на объекты функции активации и матрицы весовых коэффициентов. При отсутствии хотя бы одного объекта мы считаем текущий нейронный слой слоем исходных данных. В подтверждение этого записываем в файл 1 в качестве флага сохранения слоя исходных данных. В противном случае сохраняем 0, что будет свидетельствовать о сохранении нейронного слоя с полной функциональностью.
//--- проверка и запись флага слоя исходных данных |
Далее по методу оптимизации определяем количество необходимых для записи буферов моментов.
int momentums = 0; |
Сразу же организовываем цикл проверки действительности указателей на буфера моментов.
for(int i = 0; i < momentums; i++) |
За блоком контролей идут операции непосредственной записи данных в файл. Сначала сохраним значение переменных, а следом вызовем методы записи в файл объектов, подлежащих сохранению.
//--- сохранение матрицы весовых коэффициентов, моментов и функции активации |
Как видно из представленного кода, объекты не подлежащие сохранению мы просто пропустили. Но при загрузке данных из файла такой подход неприменим, так как даже пропущенные объекты необходимы для нормального функционирования нейронного слоя. Поэтому метод загрузки данных Load должен быть дополнен блоком инициализации пропущенных объектов. Посмотрим, как это реализовано.
Так же, как при записи в файл, в параметрах метод получает хендл файла с данными. Соответственно, в начале метода проверяем действительность полученного хендла.
bool CNeuronBase::Load(const int file_handle) |
Считывание данных из файла должно осуществляться в точном соответствии с последовательностью записи данных. Первым мы сохранили тип нейронного слоя и количество элементов в буфере в буфере результатов. Тип нейронного слоя будет считывать метод объекта верхнего уровня (динамического массива нейронных слоев), чтобы создать требуемый нейронный слой. В теле данного метода мы прочитаем количество элементов в буфере результата и инициализируем буфер соответствующего размера.
//--- загрузка буфера результатов |
И сразу создадим буфер градиентов аналогичного размера.
//--- создание буфера градиентов ошибки |
Далее проверяем флаг загрузки нейронного слоя исходных данных. В случае загрузки такового удаляем неиспользуемые объекты и выходим из метода с положительным результатом.
//--- проверка флага слоя исходных данных |
Дальнейший код выполняется только при загрузке полностью функционального нейронного слоя. В начале этого блока считываем из файла метод оптимизации и количество используемых буферов моментов.
m_eOptimization = (ENUM_OPTIMIZATION)FileReadInteger(file_handle); |
После этого проверяем указатель на объект матрицы весов. В случае необходимости создаем новый экземпляр объекта и сразу вызываем метод загрузки буфера данных.
//--- создание объектов перед загрузкой данных |
Затем считаем из файла тип используемой функции активации и инициализируем экземпляр соответствующего класса с помощью метода SetActivation. Параметры функции активации будут загружены путем вызова одноименного метода загрузки данных объекта функции активации.
//--- функция активации |
Аналогичным образом загрузим данные буферов моментов.
//--- |
После загрузки данных проведем инициализацию буфера m_cDeltaWeights. Буфер будем инициализировать нулевыми значениями. При этом размер буфера равен числу элементов в матрице весов.
Сначала проверим указатель на объект и при необходимости создадим новый. Затем запишем во все элементы буфера 0.
//--- инициализация оставшихся буферов |
В заключение метода передадим текущий указатель m_cOpenCL во все внутренние объекты. Здесь мы не ставим проверку на действительность указателя. Напомню, все объекты нейронной сети работают в одном контексте OpenCL, поэтому даже недействительный указатель передаем в объекты.
//--- передача указателя на контекст OpenCL в объекты |
В результате реализации всех вышеописанных методов финальная структура нашего класса приняла нижеследующий вид.
class CNeuronBase : public CObject |
//--- |
public: |
virtual bool SetOutputs(CBufferType* buffer, bool delete_prevoius = true); |