- 5.1.2.1 Метод прямого прохода Self-Attention
- 5.1.2.2 Методы обратного прохода Self-Attention
- 5.1.2.3 Методы работы с файлами
5.Методы работы с файлами
Мы уже построили методы прямого и обратного проходов нашего слоя внимания. Можно добавить слой в свою модель и обучить ее, но мы же не будем каждый раз заново обучать модель перед ее использованием. Нам нужна возможность сохранить однажды обученную модель в файл и при необходимости загрузить из файла готовую к использованию нейронную сеть. За работу с файлами в нашем базовом нейронном слое отвечает два метода: Save и Load. Для корректного выполнения функции в нашем новом слое необходимо переопределить указанные методы.
Подобную итерацию мы осуществляем при создании каждого нового типа нейронного слоя. Сейчас мы пойдем уже проторенной дорогой: обратим свой взор на структуру нашего класса и определим, что нужно сохранить в файл, а какие переменные и объекты мы просто создадим и инициализируем начальными значениями.
В первую очередь, нужно сохранить внутренние нейронные слои, содержащие матрицы весовых коэффициентов m_cQuerys, m_cKeys, m_cValues, m_cFF1 и m_cFF2. Кроме того, нам нужно сохранить значения переменных, определяющих архитектуру нейронного слоя: m_iWindow, m_iUnits и m_iKeysSize.
Нам нет необходимости сохранять в файл никакой информации из буфера m_cScores, так как он содержит только промежуточные данные, перезаписываемые при каждом прямом проходе. Его размер легко определить на основе количества элементов в последовательности, записанного в переменной m_iUnits.
Внутренний слой m_cAttentionOut не содержит матрицы весовых коэффициентов, и его данные, так же как и данные буфера m_cScores, перезаписываются на каждой итерации прямого и обратного прохода. Но давайте посмотрим на ситуацию с другой стороны. Вспомним процедуру инициализации нейронного слоя:
- создать объект описания нейронного слоя,
- заполнить объект описания нейронного слоя необходимой информацией,
- вызвать метод инициализации нейронного слоя с передачей описания,
- удалить объект описания нейронного слоя.
В то же время вызов метода сохранения для базового нейронного слоя без матрицы весов запишет в файл всего 3 целочисленных числа с общим размером в 12 байт. То есть пожертвовав 12 байт пространства на диске мы сокращаем свои трудозатраты на написание кода инициализации нейронного слоя в методе загрузки данных.
class CNeuronAttention : public CNeuronBase
|
После того, как мы определились с объектами для записи данных в файл, можно приступать к работе над нашими методами. Начнем работу с метода записи данных в файл Save. В параметрах метод получает хендл файла для записи данных. Но мы не будем сразу проверять полученный хендл. Вместо этого мы вызовем аналогичный метод родительского класса, в котором уже реализованы все контрольные точки и сохранение унаследованных объектов. Результат выполнения метода родительского класса укажет на результат отработки блока контролей.
bool CNeuronAttention::Save(const int file_handle)
|
После выполнения метода родительского класса мы поочередно вызываем метод сохранения внутренних объектов. При этом не забываем проверить результаты выполнения операций.
if(!m_cQuerys.Save(file_handle))
|
После сохранения данных внутренних объектов сохраним значения переменных, определяющих архитектуру нейронного слоя. Конечно, мы проверяем результат выполнения операций.
if(FileWriteInteger(file_handle, m_iUnits) <= 0)
|
После успешного сохранения всех необходимых данных завершаем метод с положительным результатом.
После создания метода записи данных переходим к работе над методом чтения данных Load. В параметрах метод получает хендл файла для чтения данных. Как и в случае с записью данных, мы не создаем новый блок контролей в своем методе, а вместо этого вызовем метод родительского класса, в котором уже реализованы все контроли и считывание унаследованных объектов и переменных. Проверка результата выполнения метода родительского класса сразу нам расскажет и о прохождении блока контролей, и о загрузке данных унаследованных объектов и переменных.
bool CNeuronAttention::Load(const int file_handle)
|
После успешного выполнения метода загрузки данных родительского класса мы поочередно будем считывать данные внутренних объектов. Напомню, что считывание данных из файла осуществляется в строгом соответствии последовательности записи данных. При записи данных в файл первым мы сохранили информацию из внутреннего нейронного слоя m_cQuerys. Следовательно, первым мы будем загружать данные именно в этот объект. Но не забываем про нюанс загрузки внутренних нейронных слоев: мы сначала проверяем тип загружаемого объекта и только потом вызываем метод загрузки соответствующего объекта.
if(FileReadInteger(file_handle) != defNeuronConv || !m_cQuerys.Load(file_handle))
|
Аналогичный алгоритм повторяем для всех ранее сохраненных объектов.
if(FileReadInteger(file_handle) != defNeuronConv || !m_cKeys.Load(file_handle))
|
После загрузки данных объектов внутренних нейронных слоев мы считываем из файла значение переменных, определяющих архитектуру нашего нейронного слоя внимания.
m_iUnits = FileReadInteger(file_handle);
|
Дальше нам остается лишь инициализировать буфер коэффициентов зависимостей m_cScores нулевыми значениями. Предварительно мы не изменяем размер буфера, так как методом инициализации буфера предусмотрено изменение его размера до необходимого уровня.
if(!m_cScores.BufferInit(m_iUnits, m_iUnits, 0))
|
Теперь мы загрузили все данные и инициализировали объекты. Остается вспомнить, что для исключения излишнего копирования данных мы сделали подмену указателей буферов результатов и градиентов внутреннего слоя m_cFF2 и самого слоя внимания. И без этой подмены указателей вся работа нашего нейронного слоя будет некорректной. Но если по какой-либо причине мы заново создадим объект внутреннего слоя m_cFF2, то будут созданы и новые объекты буферов указанного внутреннего нейронного слоя. В таком случае нам нужно осуществить такую подмену указателей повторно. В то же время, если обе переменных содержат указатель на один объект, то, удалив объект по одному указателю, мы получим не действительный указатель и во второй переменной. Это узкий момент, с которым нужно быть аккуратней.
Мы, конечно, добавим подмену буферов, но предварительно проверим соответствие указателей.
if(m_cFF2.GetOutputs() != m_cOutputs)
|
if(m_cFF2.GetGradients() != m_cGradients)
|
После успешного выполнения всех операций выходим из метода с положительным результатом.
На этом можно считать завершенной работу по созданию нейронного слоя внимания стандартными средствами языка MQL5. В таком варианте мы уже можем вставить нейронный слой внимания в свою модель и проверить его работоспособность. Но для наиболее эффективного использования созданного класса нам необходимо дополнить его методы средствами многопоточных вычислений.