• 6.1.4.1 Подготовка скрипта для тестирования пакетной нормализации

6.Подготовка скрипта для тестирования пакетной нормализации

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

Для проведения такого эксперимента мы возьмем скрипт тестирования моделей перцептрона perceptron.py и создадим его копию с именем batch_norm.py. Внесем в него небольшие изменения.

В начале скрипта мы как обычно импортируем необходимые библиотеки.

# Импорт библиотек
import os
import pandas as pd
import numpy as np
import tensorflow as tf 
from tensorflow import keras 
import matplotlib.pyplot as plt
import MetaTrader5 as mt5

Перед обучением нам необходимо загрузить обучающие выборки, которые лежат в песочнице терминала MetaTrader 5. Чтобы определить путь в песочницу, мы подключаемся к терминалу и получаем путь к папке данных терминала. К полученному пути добавляем MQL5\Files. В результате мы получим путь в песочницу терминала. Если вы сохранили данные обучающей выборки в подкаталог, его также надо добавить к нашему пути в песочницу. Теперь можно отключиться от терминала. Создаем две локальные переменные с полным путем к фалам обучающих выборок, один с нормализованными данными, второй с ненормализованными.

# Загрузка обучающей выборки
if not mt5.initialize():
    print("initialize() failed, error code =",mt5.last_error())
    quit()
 
path=os.path.join(mt5.terminal_info().data_path,r'MQL5\Files')
mt5.shutdown()
filename = os.path.join(path,'study_data.csv')
filename_not_norm = os.path.join(path,'study_data_not_norm.csv')

Сначала мы загрузим данные нормированной выборки.

data = np.asarray( pd.read_table(filename,
                   sep=',',
                   header=None,
                   skipinitialspace=True,
                   encoding='utf-8',
                   float_precision='high',
                   dtype=np.float64,
                   low_memory=False))

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

# Разделение обучающей выборки на исходные данные и цели
targets=2
inputs=data.shape[1]-targets
train_data=data[:,0:inputs]
train_target=data[:,inputs:]

После этого мы аналогично загрузим и разделим данные не нормированной обучающей выборки.

#загрузка ненормированной обучающей выборки
data = np.asarray( pd.read_table(filename_not_norm,
                   sep=',',
                   header=None,
                   skipinitialspace=True,
                   encoding='utf-8',
                   float_precision='high',
                   dtype=np.float64,
                   low_memory=False))

# Разделение ненормированной обучающей выборки на исходные данные и цели
train_nn_data=data[:,0:inputs]
train_nn_target=data[:,inputs:]
 
del data

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

Следующим этапом после загрузки данных мы приступаем к созданию моделей нейронных сетей для проведения тестирования.

Первой мы создадим небольшой полносвязный перцептрон с одним скрытым слоем на 40 элементов и слоем результатов из 2 элементов.

# Создание первой модели с одним скрытым слоем
model1 = keras.Sequential([keras.layers.InputLayer(input_shape=inputs),
                           keras.layers.Dense(40, activation=tf.nn.swish), 
                           keras.layers.Dense(targets, activation=tf.nn.tanh) 
                         ])

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

callback = tf.keras.callbacks.EarlyStopping(monitor='loss', patience=5)
model1.compile(optimizer='Adam'
               loss='mean_squared_error'
               metrics=['accuracy'])
model1.summary()

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

# Добавление пакетной нормализации для исходных данных 
# в модель с одним скрытым слоем
model1bn = keras.Sequential([keras.layers.InputLayer(input_shape=inputs),
                             keras.layers.BatchNormalization(),
                             keras.layers.Dense(40, activation=tf.nn.swish), 
                             keras.layers.Dense(targets, activation=tf.nn.tanh) 
                            ])

И скомпилируем модель с теми же параметрами.

model1bn.compile(optimizer='Adam'
               loss='mean_squared_error'
               metrics=['accuracy'])
model1bn.summary()

Модели для нашего первого эксперимента готовы.

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

# Создание модели с тремя скрытыми слоями
model2 = keras.Sequential([keras.layers.InputLayer(input_shape=inputs),
                           keras.layers.Dense(40, activation=tf.nn.swish), 
                           keras.layers.Dense(40, activation=tf.nn.swish), 
                           keras.layers.Dense(40, activation=tf.nn.swish), 
                           keras.layers.Dense(targets, activation=tf.nn.tanh) 
                         ])

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

model2.compile(optimizer='Adam'
               loss='mean_squared_error'
               metrics=['accuracy'])
model2.summary()

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

# Добавление пакетной нормализации для исходных данных и скрытых слоев второй модели
model2bn = keras.Sequential([keras.layers.InputLayer(input_shape=inputs),
                             keras.layers.BatchNormalization(),
                             keras.layers.Dense(40, activation=tf.nn.swish), 
                             keras.layers.BatchNormalization(),
                             keras.layers.Dense(40, activation=tf.nn.swish), 
                             keras.layers.BatchNormalization(),
                             keras.layers.Dense(40, activation=tf.nn.swish), 
                             keras.layers.Dense(targets, activation=tf.nn.tanh) 
                            ])

Как и ранее, компиляцию модели осуществляем без изменения параметров.

model2bn.compile(optimizer='Adam'
               loss='mean_squared_error'
               metrics=['accuracy'])
model2bn.summary()

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

Сначала проведем обучение модели с одним скрытым слоем на нормализованных данных.

# Обучение первой модели на ненормализованных данных
history1 = model1.fit(train_data, train_target,
                      epochs=500, batch_size=1000,
                      callbacks=[callback],
                      verbose=2,
                      validation_split=0.1,
                      shuffle=True)
model1.save(os.path.join(path,'perceptron1.h5'))

Следом обучим ту же модель на ненормализованных данных.

# Обучение первой модели на ненормализованных данных
history1nn = model1.fit(train_nn_data, train_nn_target,
                      epochs=500, batch_size=1000,
                      callbacks=[callback],
                      verbose=2,
                      validation_split=0.1,
                      shuffle=True)

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

history1bn = model1bn.fit(train_nn_data, train_nn_target,
                      epochs=500, batch_size=1000,
                      callbacks=[callback],
                      verbose=2,
                      validation_split=0.1,
                      shuffle=True)
model1bn.save(os.path.join(path,'perceptron1bn.h5'))

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

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

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

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

history2 = model2.fit(train_data, train_target,
                      epochs=500, batch_size=1000,
                      callbacks=[callback],
                      verbose=2,
                      validation_split=0.1,
                      shuffle=True)
model2.save(os.path.join(path,'perceptron2.h5'))

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

history2bn = model2bn.fit(train_nn_data, train_nn_target,
                      epochs=500, batch_size=1000,
                      callbacks=[callback],
                      verbose=2,
                      validation_split=0.1,
                      shuffle=True)
model2bn.save(os.path.join(path,'perceptron2bn.h5'))

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

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

# Отрисовка результатов обучения моделей с одним скрытым слоем
plt.plot(history1.history['loss'], label='Normalized inputs train')
plt.plot(history1.history['val_loss'], label='Normalized inputs validation')
plt.plot(history1nn.history['loss'], label='Unnormalized inputs train')
plt.plot(history1nn.history['val_loss'], label='Unnormalized inputs vvalidation')
plt.plot(history1bn.history['loss'],
                        label='Unnormalized inputs\nvs BatchNormalization train')
plt.plot(history1bn.history['val_loss'],
                   label='Unnormalized inputs\nvs BatchNormalization validation')
plt.ylabel('$MSE$ $loss$')
plt.xlabel('$Epochs$')
plt.title('Model training dynamics\n1 hidden layer')
plt.legend(loc='upper right', ncol=2)

В дополнение к первому графику построим график динамики изменения метрики Accuracy.

plt.figure()
plt.plot(history1.history['accuracy'], label='Normalized inputs train')
plt.plot(history1.history['val_accuracy'], label='Normalized inputs validation')
plt.plot(history1nn.history['accuracy'], label='Unnormalized inputs train')
plt.plot(history1nn.history['val_accuracy'], label='Unnormalized inputs validation')
plt.plot(history1bn.history['accuracy'],
                           label='Unnormalized inputs\nvs BatchNormalization train')
plt.plot(history1bn.history['val_accuracy'],
                      label='Unnormalized inputs\nvs BatchNormalization validation')
plt.ylabel('$Accuracy$')
plt.xlabel('$Epochs$')
plt.title('Model training dynamics\n1 hidden layer')
plt.legend(loc='lower right', ncol=2)

Построим аналогичные графики для вывода результатов второго эксперимента.

# Отрисовка результатов обучения моделей с тремя скрытыми слоями
plt.figure()
plt.plot(history2.history['loss'], label='Normalized inputs train')
plt.plot(history2.history['val_loss'], label='Normalized inputs validation')
plt.plot(history2bn.history['loss'],
                   label='Unnormalized inputs\nvs BatchNormalization train')
plt.plot(history2bn.history['val_loss'],
              label='Unnormalized inputs\nvs BatchNormalization validation')
plt.ylabel('$MSE$ $loss$')
plt.xlabel('$Epochs$')
plt.title('Model training dynamics\n3 hidden layers')
plt.legend(loc='upper right', ncol=2)

plt.figure()
plt.plot(history2.history['accuracy'], label='Normalized inputs train')
plt.plot(history2.history['val_accuracy'], label='Normalized inputs validation')
plt.plot(history2bn.history['accuracy'],
                       label='Unnormalized inputs\nvs BatchNormalization train')
plt.plot(history2bn.history['val_accuracy'],
                  label='Unnormalized inputs\nvs BatchNormalization validation')
plt.ylabel('$Accuracy$')
plt.xlabel('$Epochs$')
plt.title('Model training dynamics\n3 hidden layers')
plt.legend(loc='lower right', ncol=2)

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

Загрузим тестовые выборки точно так же, как мы загружали обучающие выборки. Сначала загрузим данные нормализованной тестовой выборки.

# Загрузка тестовой выборки
test_filename = os.path.join(path,'test_data.csv')
test = np.asarray( pd.read_table(test_filename,
                   sep=',',
                   header=None,
                   skipinitialspace=True,
                   encoding='utf-8',
                   float_precision='high',
                   dtype=np.float64,
                   low_memory=False))

Разделим загруженные данные на паттерны и целевые значения.

# Разделение тестовой выборки на исходные данные и цели
test_data=test[:,0:inputs]
test_target=test[:,inputs:]

Потом повторим алгоритм для загрузки ненормализованной тестовой выборки.

test_filename = os.path.join(path,'test_data_not_norm.csv')
test = np.asarray( pd.read_table(test_filename,
                   sep=',',
                   header=None,
                   skipinitialspace=True,
                   encoding='utf-8',
                   float_precision='high',
                   dtype=np.float64,
                   low_memory=False))

# Разделение тестовой выборки на исходные данные и цели
test_nn_data=test[:,0:inputs]
test_nn_target=test[:,inputs:]
 
del test

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

Далее мы проверим работу всех моделей на тестовых выборках. Работу моделей без слоев пакетной нормализации мы проверяем на нормализованных данных. Модели с использованием слоев пакетной нормализации мы протестируем на ненормализованных данных тестовой выборки.

# Проверка результатов моделей на тестовой выборке
test_loss1, test_acc1 = model1.evaluate(test_data, test_target, verbose=2
test_loss1bn, test_acc1bn = model1bn.evaluate(test_nn_data, test_nn_target,
                                                                verbose=2
test_loss2, test_acc2 = model2.evaluate(test_data, test_target, verbose=2
test_loss2bn, test_acc2bn = model2bn.evaluate(test_nn_data, test_nn_target,
                                                                verbose=2

Результаты тестирования мы выведем в журнал.

# Вывод результатов тестирования в журнал
print('Model 1 hidden layer')
print('Test accuracy:', test_acc1)
print('Test loss:', test_loss1)

print('Model 1 hidden layer with BatchNormalization')
print('Test accuracy:', test_acc1bn)
print('Test loss:', test_loss1bn)

print('Model 3 hidden layers')
print('Test accuracy:', test_acc2)
print('Test loss:', test_loss2)

print('Model 3 hidden layer with BatchNormalization')
print('Test accuracy:', test_acc2bn)
print('Test loss:', test_loss2bn)

Для наглядности сделаем графическое представление результатов отдельно по среднеквадратичному отклонению и по метрике Accuracy.

plt.figure()
plt.bar(['1 hidden layer','1 hidden layer\nvs BatchNormalization',
         '3 hidden layers','3 hidden layers\nvs BatchNormalization'],
        [test_loss1,test_loss1bn,test_loss2,test_loss2bn])
plt.ylabel('$MSE$ $Loss$')
plt.title('Test results')

plt.figure()
plt.bar(['1 hidden layer','1 hidden layer\nvs BatchNormalization',
         '3 hidden layers','3 hidden layers\nvs BatchNormalization'],
        [test_acc1,test_acc1bn,test_acc2,test_acc2bn])
plt.ylabel('$Accuracy$')
plt.title('Test results')
 
plt.show()

После построения графиков вызовем команду их отрисовки на экране пользователя.

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