Перевод
Ссылка на автора
Алгоритм обратного распространения — это классическая искусственная нейронная сеть с прямой связью.
Эта техника до сих пор используется для тренировки большого глубокое обучение сетей.
В этом руководстве вы узнаете, как реализовать алгоритм обратного распространения с нуля с помощью Python.
После завершения этого урока вы узнаете:
- Как переадресовать входные данные для вычисления выходных данных.
- Как распространять ошибки и обучать сеть.
- Как применить алгоритм обратного распространения к реальной задаче прогнозного моделирования.
Давайте начнем.
- Обновление ноябрь 2016: Исправлена ошибка в функции activ (). Спасибо Алекс!
- Обновление январь / 2017: Изменено вычисление fold_size в cross_validation_split (), чтобы оно всегда было целым числом. Исправляет проблемы с Python 3.
- Обновление январь / 2017: Обновлена небольшая ошибка в update_weights (). Спасибо, Томаш!
- Обновление апрель / 2018: Добавлена прямая ссылка на набор данных CSV.
- Обновление Авг / 2018: Протестировано и обновлено для работы с Python 3.6.

Описание
В этом разделе дается краткое введение в алгоритм обратного распространения и набор данных семян пшеницы, которые мы будем использовать в этом руководстве.
Алгоритм обратного распространения
Алгоритм обратного распространения — это контролируемый метод обучения для многослойных сетей прямой связи из области искусственных нейронных сетей.
Прямые нейронные сети вдохновлены обработкой информации одной или нескольких нейронных клеток, называемых нейронами. Нейрон принимает входные сигналы через свои дендриты, которые передают электрический сигнал в тело клетки. Аксон передает сигнал в синапсы, которые являются связями аксона клетки с дендритами другой клетки.
Принцип обратного распространения заключается в моделировании заданной функции путем изменения внутренних весовых коэффициентов входных сигналов для получения ожидаемого выходного сигнала. Система обучается с использованием метода контролируемого обучения, где ошибка между выходными данными системы и известным ожидаемым выходным значением представляется системе и используется для изменения ее внутреннего состояния.
Технически алгоритм обратного распространения — это метод обучения весов в многослойной нейронной сети с прямой связью. Как таковой, он требует, чтобы сетевая структура была определена из одного или нескольких уровней, где один уровень полностью связан со следующим уровнем. Стандартная сетевая структура — это один входной слой, один скрытый слой и один выходной слой.
Обратное распространение можно использовать как для задач классификации, так и для задач регрессии, но в этом руководстве мы сосредоточимся на классификации.
В задачах классификации наилучшие результаты достигаются, когда сеть имеет один нейрон в выходном слое для каждого значения класса. Например, проблема 2-классовой или двоичной классификации со значениями классов A и B. Эти ожидаемые результаты должны быть преобразованы в двоичные векторы с одним столбцом для каждого значения класса. Например, [1, 0] и [0, 1] для A и B соответственно. Это называется горячим кодированием.
Набор данных семян пшеницы
Набор данных семян включает в себя прогнозирование видов с учетом измерений семян из разных сортов пшеницы.
Есть 201 записей и 7 числовых входных переменных. Это проблема классификации с 3 выходными классами. Шкала для каждого числового входного значения варьируется, поэтому может потребоваться некоторая нормализация данных для использования с алгоритмами, которые взвешивают входные данные, такие как алгоритм обратного распространения.
Ниже приведен образец первых 5 строк набора данных.
15.26,14.84,0.871,5.763,3.312,2.221,5.22,1
14.88,14.57,0.8811,5.554,3.333,1.018,4.956,1
14.29,14.09,0.905,5.291,3.337,2.699,4.825,1
13.84,13.94,0.8955,5.324,3.379,2.259,4.805,1
16.14,14.99,0.9034,5.658,3.562,1.355,5.175,1
При использовании алгоритма нулевого правила, который прогнозирует наиболее распространенное значение класса, базовая точность задачи составляет 28,095%.
Вы можете узнать больше и загрузить набор данных семян из UCI Хранилище Машинного Обучения,
Загрузите набор данных seed и поместите его в текущий рабочий каталог с именем файлаseeds_dataset.csv,
Набор данных представлен в формате табуляции, поэтому его необходимо преобразовать в CSV с помощью текстового редактора или программы для работы с электронными таблицами.
Обновите, загрузите набор данных в формате CSV напрямую:
- Скачать набор данных семян пшеницы
Руководство
Этот урок разбит на 6 частей:
- Инициализировать сеть.
- Вперед Распространять.
- Ошибка обратного распространения.
- Сеть поездов.
- Предсказать.
- Пример набора данных семян.
Эти шаги обеспечат основу, необходимую для реализации алгоритма обратного распространения с нуля и применения его к собственным задачам прогнозного моделирования.
1. Инициализировать сеть
Давайте начнем с чего-то простого, создания новой сети, готовой к обучению.
Каждый нейрон имеет набор весов, которые необходимо поддерживать. Один вес для каждого входного соединения и дополнительный вес для смещения. Нам нужно будет хранить дополнительные свойства для нейрона во время обучения, поэтому мы будем использовать словарь для представления каждого нейрона и сохранять свойства по именам, таким как ‘веса‘Для весов.
Сеть организована в слои. Входной слой на самом деле просто строка из нашего набора обучающих данных Первый настоящий слой — это скрытый слой. Затем следует выходной слой, который имеет один нейрон для каждого значения класса.
Мы организуем слои как массивы словарей и будем рассматривать всю сеть как массив слоев.
Хорошей практикой является инициализация весов сети небольшими случайными числами. В этом случае мы будем использовать случайные числа в диапазоне от 0 до 1.
Ниже приведена функция с именемinitialize_network ()это создает новую нейронную сеть, готовую к обучению. Он принимает три параметра: количество входов, количество нейронов в скрытом слое и количество выходов.
Вы можете видеть, что для скрытого слоя мы создаемn_hiddenнейроны и каждый нейрон в скрытом слое имеетn_inputs + 1веса, один для каждого входного столбца в наборе данных и дополнительный для смещения.
Вы также можете видеть, что выходной слой, который подключается к скрытому слою, имеетn_outputsнейроны, каждый сn_hidden + 1веса. Это означает, что каждый нейрон в выходном слое соединяется (имеет вес) с каждым нейроном в скрытом слое.
# Initialize a network
def initialize_network(n_inputs, n_hidden, n_outputs):
network = list()
hidden_layer = [{'weights':[random() for i in range(n_inputs + 1)]} for i in range(n_hidden)]
network.append(hidden_layer)
output_layer = [{'weights':[random() for i in range(n_hidden + 1)]} for i in range(n_outputs)]
network.append(output_layer)
return network
Давайте проверим эту функцию. Ниже приведен полный пример, который создает небольшую сеть.
from random import seed
from random import random
# Initialize a network
def initialize_network(n_inputs, n_hidden, n_outputs):
network = list()
hidden_layer = [{'weights':[random() for i in range(n_inputs + 1)]} for i in range(n_hidden)]
network.append(hidden_layer)
output_layer = [{'weights':[random() for i in range(n_hidden + 1)]} for i in range(n_outputs)]
network.append(output_layer)
return network
seed(1)
network = initialize_network(2, 1, 2)
for layer in network:
print(layer)
Запустив пример, вы можете увидеть, что код распечатывает каждый слой по одному. Вы можете видеть, что скрытый слой имеет один нейрон с 2 входными весами плюс смещение. Выходной слой имеет 2 нейрона, каждый с 1 весом плюс смещение.
[{'weights': [0.13436424411240122, 0.8474337369372327, 0.763774618976614]}]
[{'weights': [0.2550690257394217, 0.49543508709194095]}, {'weights': [0.4494910647887381, 0.651592972722763]}]
Теперь, когда мы знаем, как создать и инициализировать сеть, давайте посмотрим, как мы можем использовать ее для вычисления результата.
2. Вперед Распространение
Мы можем вычислить выход из нейронной сети, распространяя входной сигнал через каждый уровень, пока выходной уровень не выведет свои значения.
Мы называем это продвижением вперед.
Это метод, который нам понадобится для генерации прогнозов во время обучения, который необходимо будет исправить, и это метод, который нам понадобится после обучения сети для прогнозирования новых данных.
Мы можем разбить распространение вперед на три части:
- Активация нейронов.
- Передача нейронов.
- Вперед Распространение.
2.1. Активация нейронов
Первым шагом является вычисление активации одного нейрона с учетом входных данных.
Входными данными может быть строка из нашего обучающего набора данных, как в случае со скрытым слоем. Это также могут быть выходы от каждого нейрона в скрытом слое, в случае выходного слоя.
Активация нейрона рассчитывается как взвешенная сумма входов. Очень похоже на линейную регрессию.
activation = sum(weight_i * input_i) + bias
кудавесвес сети,входявляется входом,яэто индекс веса или ввода исмещениеэто специальный вес, который не имеет входных данных для умножения (или вы можете думать, что входные данные всегда равны 1,0).
Ниже приведена реализация этого в функции с именемактивировать (), Вы можете видеть, что функция предполагает, что смещение является последним весом в списке весов. Это помогает здесь и позже сделать код легче для чтения.
# Calculate neuron activation for an input
def activate(weights, inputs):
activation = weights[-1]
for i in range(len(weights)-1):
activation += weights[i] * inputs[i]
return activation
Теперь давайте посмотрим, как использовать активацию нейронов.
2.2. Нейрон Трансфер
Как только нейрон активирован, нам нужно перенести активацию, чтобы увидеть, что на самом деле представляет собой выход нейрона.
Различные передаточные функции могут быть использованы. Традиционно использовать функция активации сигмовидной кишки, но вы также можете использовать танх (тангенс гиперболический) функция для передачи выходов. Совсем недавно передаточная функция выпрямителя был популярен в крупных сетях глубокого обучения.
Функция активации сигмоида выглядит как S-образная форма, ее также называют логистической функцией. Он может принимать любое входное значение и производить число от 0 до 1 на S-кривой. Это также функция, из которой мы можем легко рассчитать производную (наклон), которая понадобится нам позже при ошибке обратного распространения.
Мы можем передать функцию активации с помощью функции сигмоида следующим образом:
output = 1 / (1 + e^(-activation))
кудаеявляется основанием натуральных логарифмов (Номер Эйлера).
Ниже приведена функция с именемперечислить()который реализует сигмовидное уравнение.
# Transfer neuron activation
def transfer(activation):
return 1.0 / (1.0 + exp(-activation))
Теперь, когда у нас есть кусочки, давайте посмотрим, как они используются.
2,3. Прямое распространение
Вперед, распространение входных данных просто.
Мы работаем через каждый слой нашей сети, вычисляя выходы для каждого нейрона. Все выходы из одного слоя становятся входами для нейронов на следующем слое.
Ниже приведена функция с именемforward_propagate ()это реализует прямое распространение для ряда данных из нашего набора данных с нашей нейронной сетью.
Вы можете видеть, что выходное значение нейрона хранится в нейроне с именем ‘выход«. Вы также можете увидеть, что мы собираем выходные данные для слоя в массиве с именемnew_inputsэто становится массивомвходныеи используется в качестве входных данных для следующего слоя.
Функция возвращает выходные данные из последнего слоя, также называемого выходным слоем.
# Forward propagate input to a network output
def forward_propagate(network, row):
inputs = row
for layer in network:
new_inputs = []
for neuron in layer:
activation = activate(neuron['weights'], inputs)
neuron['output'] = transfer(activation)
new_inputs.append(neuron['output'])
inputs = new_inputs
return inputs
Давайте соединим все эти части и протестируем прямое распространение нашей сети.
Мы определяем нашу сеть, встроенную одним скрытым нейроном, который ожидает 2 входных значения, и выходной слой с двумя нейронами.
from math import exp
# Calculate neuron activation for an input
def activate(weights, inputs):
activation = weights[-1]
for i in range(len(weights)-1):
activation += weights[i] * inputs[i]
return activation
# Transfer neuron activation
def transfer(activation):
return 1.0 / (1.0 + exp(-activation))
# Forward propagate input to a network output
def forward_propagate(network, row):
inputs = row
for layer in network:
new_inputs = []
for neuron in layer:
activation = activate(neuron['weights'], inputs)
neuron['output'] = transfer(activation)
new_inputs.append(neuron['output'])
inputs = new_inputs
return inputs
# test forward propagation
network = [[{'weights': [0.13436424411240122, 0.8474337369372327, 0.763774618976614]}],
[{'weights': [0.2550690257394217, 0.49543508709194095]}, {'weights': [0.4494910647887381, 0.651592972722763]}]]
row = [1, 0, None]
output = forward_propagate(network, row)
print(output)
Выполнение примера распространяет входной шаблон [1, 0] и выдает выходное значение, которое печатается. Поскольку выходной слой имеет два нейрона, мы получаем список из двух чисел в качестве вывода.
Фактические выходные значения на данный момент просто бессмыслица, но затем мы начнем изучать, как сделать веса в нейронах более полезными.
[0.6629970129852887, 0.7253160725279748]
3. Ошибка обратного распространения
Алгоритм обратного распространения назван по способу обучения весов.
Ошибка рассчитывается между ожидаемыми выходами и выходами, передаваемыми по сети. Эти ошибки затем распространяются в обратном направлении через сеть от выходного уровня к скрытому слою, назначая вину за ошибку и обновляя веса по мере их появления.
Математика для ошибки обратного распространения коренится в исчислении, но мы останемся на высоком уровне в этом разделе и сосредоточимся на том, что рассчитывается и как, а не почему расчеты принимают эту конкретную форму.
Эта часть разбита на две части.
- Передача Производная.
- Ошибка обратного распространения.
3.1. Производная передача
Учитывая выходное значение от нейрона, нам нужно вычислить его наклон.
Мы используем передаточную функцию сигмоида, производную которой можно рассчитать следующим образом:
derivative = output * (1.0 - output)
Ниже приведена функция с именемtransfer_derivative ()который реализует это уравнение.
# Calculate the derivative of an neuron output
def transfer_derivative(output):
return output * (1.0 - output)
Теперь посмотрим, как это можно использовать.
3.2. Ошибка обратного распространения
Первым шагом является вычисление ошибки для каждого выходного нейрона, это даст нам наш сигнал ошибки (вход) для распространения в обратном направлении по сети.
Ошибка для данного нейрона может быть рассчитана следующим образом:
error = (expected - output) * transfer_derivative(output)
кудаожидаемыйявляется ожидаемым выходным значением для нейрона,выходэто выходное значение для нейрона иtransfer_derivative ()вычисляет наклон выходного значения нейрона, как показано выше.
Этот расчет ошибки используется для нейронов в выходном слое. Ожидаемое значение — это само значение класса. В скрытом слое все немного сложнее.
Сигнал ошибки для нейрона в скрытом слое рассчитывается как взвешенная ошибка каждого нейрона в выходном слое. Подумайте об ошибке, связанной с перемещением весов выходного слоя к нейронам в скрытом слое.
Обратно распространяющийся сигнал ошибки накапливается и затем используется для определения ошибки для нейрона в скрытом слое следующим образом:
error = (weight_k * error_j) * transfer_derivative(output)
кудаerror_jэто сигнал ошибки отJй нейрон в выходном слое,weight_kэто вес, который соединяетКТретий нейрон к текущему нейрону и выход — это выход для текущего нейрона.
Ниже приведена функция с именемbackward_propagate_error ()который реализует эту процедуру.
Вы можете видеть, что сигнал ошибки, рассчитанный для каждого нейрона, хранится с именем «delta». Вы можете видеть, что слои сети перебираются в обратном порядке, начиная с выхода и работая в обратном направлении. Это гарантирует, что нейроны в выходном слое сначала рассчитывают значения «дельта», которые нейроны в скрытом слое могут использовать в последующей итерации. Я выбрал имя «дельта», чтобы отразить изменение, которое ошибка вносит в нейрон (например, дельта веса).
Вы можете видеть, что сигнал ошибки для нейронов в скрытом слое накапливается от нейронов в выходном слое, где скрыт номер нейронаJтакже индекс веса нейрона в выходном слоенейрон [грузики ‘] [J],
# Backpropagate error and store in neurons
def backward_propagate_error(network, expected):
for i in reversed(range(len(network))):
layer = network[i]
errors = list()
if i != len(network)-1:
for j in range(len(layer)):
error = 0.0
for neuron in network[i + 1]:
error += (neuron['weights'][j] * neuron['delta'])
errors.append(error)
else:
for j in range(len(layer)):
neuron = layer[j]
errors.append(expected[j] - neuron['output'])
for j in range(len(layer)):
neuron = layer[j]
neuron['delta'] = errors[j] * transfer_derivative(neuron['output'])
Давайте соберем все части вместе и посмотрим, как это работает.
Мы определяем фиксированную нейронную сеть с выходными значениями и распространяем ожидаемый выходной шаблон. Полный пример приведен ниже.
# Calculate the derivative of an neuron output
def transfer_derivative(output):
return output * (1.0 - output)
# Backpropagate error and store in neurons
def backward_propagate_error(network, expected):
for i in reversed(range(len(network))):
layer = network[i]
errors = list()
if i != len(network)-1:
for j in range(len(layer)):
error = 0.0
for neuron in network[i + 1]:
error += (neuron['weights'][j] * neuron['delta'])
errors.append(error)
else:
for j in range(len(layer)):
neuron = layer[j]
errors.append(expected[j] - neuron['output'])
for j in range(len(layer)):
neuron = layer[j]
neuron['delta'] = errors[j] * transfer_derivative(neuron['output'])
# test backpropagation of error
network = [[{'output': 0.7105668883115941, 'weights': [0.13436424411240122, 0.8474337369372327, 0.763774618976614]}],
[{'output': 0.6213859615555266, 'weights': [0.2550690257394217, 0.49543508709194095]}, {'output': 0.6573693455986976, 'weights': [0.4494910647887381, 0.651592972722763]}]]
expected = [0, 1]
backward_propagate_error(network, expected)
for layer in network:
print(layer)
При выполнении примера печатается сеть после обратного распространения ошибки. Вы можете видеть, что значения ошибок рассчитываются и сохраняются в нейронах для выходного слоя и скрытого слоя.
[{'output': 0.7105668883115941, 'weights': [0.13436424411240122, 0.8474337369372327, 0.763774618976614], 'delta': -0.0005348048046610517}]
[{'output': 0.6213859615555266, 'weights': [0.2550690257394217, 0.49543508709194095], 'delta': -0.14619064683582808}, {'output': 0.6573693455986976, 'weights': [0.4494910647887381, 0.651592972722763], 'delta': 0.0771723774346327}]
Теперь давайте использовать обратное распространение ошибки для обучения сети.
4. Сеть поездов
Сеть обучается с использованием стохастического градиентного спуска.
Это включает в себя несколько итераций представления обучающего набора данных в сеть и для каждой строки данных, передавая входные данные, распространяя ошибку в обратном направлении и обновляя веса сети.
Эта часть разбита на две части:
- Обновление весов.
- Сеть поездов.
4.1. Обновление весов
Как только ошибки рассчитаны для каждого нейрона в сети с помощью метода обратного распространения, описанного выше, их можно использовать для обновления весов.
Веса сети обновляются следующим образом:
weight = weight + learning_rate * error * input
кудавесзаданный вес,learning_rateэто параметр, который вы должны указать,ошибкаошибка, рассчитанная по процедуре обратного распространения для нейрона ивходэто входное значение, вызвавшее ошибку
Та же процедура может использоваться для обновления веса смещения, за исключением того, что нет входного термина или входное значение является фиксированным значением 1,0.
Скорость обучения определяет, насколько изменить вес, чтобы исправить ошибку. Например, значение 0,1 обновит вес на 10% от суммы, которую он мог бы обновить. Предпочтительными являются малые скорости обучения, которые вызывают более медленное обучение в течение большого количества итераций обучения. Это увеличивает вероятность того, что сеть найдет хороший набор весов на всех уровнях, а не самый быстрый набор весов, которые минимизируют ошибку (так называемая преждевременная сходимость).
Ниже приведена функция с именемupdate_weights ()который обновляет весовые коэффициенты для сети с учетом входной строки данных, скорости обучения и предполагает, что прямое и обратное распространение уже выполнено.
Помните, что вход для выходного слоя представляет собой набор выходов из скрытого слоя.
# Update network weights with error
def update_weights(network, row, l_rate):
for i in range(len(network)):
inputs = row[:-1]
if i != 0:
inputs = [neuron['output'] for neuron in network[i - 1]]
for neuron in network[i]:
for j in range(len(inputs)):
neuron['weights'][j] += l_rate * neuron['delta'] * inputs[j]
neuron['weights'][-1] += l_rate * neuron['delta']
Теперь мы знаем, как обновить вес сети, давайте посмотрим, как мы можем сделать это многократно.
4.2. Сеть поездов
Как уже упоминалось, сеть обновляется с использованием стохастического градиентного спуска.
Это включает в себя первый цикл для фиксированного числа эпох и в каждой эпохе обновление сети для каждой строки в наборе обучающих данных.
Поскольку обновления производятся для каждого шаблона обучения, этот тип обучения называется онлайн-обучением. Если ошибки были накоплены за период до обновления весов, это называется периодическим обучением или пакетным градиентным спуском.
Ниже приведена функция, которая реализует обучение уже инициализированной нейронной сети с заданным набором обучающих данных, скоростью обучения, фиксированным числом эпох и ожидаемым количеством выходных значений.
Ожидаемое количество выходных значений используется для преобразования значений класса в обучающих данных в одно горячее кодирование. Это двоичный вектор с одним столбцом для каждого значения класса, чтобы соответствовать выходу сети. Это необходимо для расчета ошибки для выходного слоя.
Вы также можете видеть, что ошибка квадрата суммы между ожидаемым выходом и выходом сети накапливается каждую эпоху и печатается. Это полезно для отслеживания того, насколько сеть изучает и совершенствует каждую эпоху.
# Train a network for a fixed number of epochs
def train_network(network, train, l_rate, n_epoch, n_outputs):
for epoch in range(n_epoch):
sum_error = 0
for row in train:
outputs = forward_propagate(network, row)
expected = [0 for i in range(n_outputs)]
expected[row[-1]] = 1
sum_error += sum([(expected[i]-outputs[i])**2 for i in range(len(expected))])
backward_propagate_error(network, expected)
update_weights(network, row, l_rate)
print('>epoch=%d, lrate=%.3f, error=%.3f' % (epoch, l_rate, sum_error))
Теперь у нас есть все для обучения сети. Мы можем собрать пример, который включает в себя все, что мы видели до сих пор, включая инициализацию сети и обучение сети на небольшом наборе данных.
Ниже приведен небольшой надуманный набор данных, который мы можем использовать для тестирования нашей нейронной сети.
X1 X2 Y
2.7810836 2.550537003 0
1.465489372 2.362125076 0
3.396561688 4.400293529 0
1.38807019 1.850220317 0
3.06407232 3.005305973 0
7.627531214 2.759262235 1
5.332441248 2.088626775 1
6.922596716 1.77106367 1
8.675418651 -0.242068655 1
7.673756466 3.508563011 1
Ниже приведен полный пример. Мы будем использовать 2 нейрона в скрытом слое. Это проблема двоичной классификации (2 класса), поэтому в выходном слое будет два нейрона. Сеть будет обучаться в течение 20 эпох со скоростью обучения 0,5, что является высоким показателем, потому что мы готовим так мало итераций.
from math import exp
from random import seed
from random import random
# Initialize a network
def initialize_network(n_inputs, n_hidden, n_outputs):
network = list()
hidden_layer = [{'weights':[random() for i in range(n_inputs + 1)]} for i in range(n_hidden)]
network.append(hidden_layer)
output_layer = [{'weights':[random() for i in range(n_hidden + 1)]} for i in range(n_outputs)]
network.append(output_layer)
return network
# Calculate neuron activation for an input
def activate(weights, inputs):
activation = weights[-1]
for i in range(len(weights)-1):
activation += weights[i] * inputs[i]
return activation
# Transfer neuron activation
def transfer(activation):
return 1.0 / (1.0 + exp(-activation))
# Forward propagate input to a network output
def forward_propagate(network, row):
inputs = row
for layer in network:
new_inputs = []
for neuron in layer:
activation = activate(neuron['weights'], inputs)
neuron['output'] = transfer(activation)
new_inputs.append(neuron['output'])
inputs = new_inputs
return inputs
# Calculate the derivative of an neuron output
def transfer_derivative(output):
return output * (1.0 - output)
# Backpropagate error and store in neurons
def backward_propagate_error(network, expected):
for i in reversed(range(len(network))):
layer = network[i]
errors = list()
if i != len(network)-1:
for j in range(len(layer)):
error = 0.0
for neuron in network[i + 1]:
error += (neuron['weights'][j] * neuron['delta'])
errors.append(error)
else:
for j in range(len(layer)):
neuron = layer[j]
errors.append(expected[j] - neuron['output'])
for j in range(len(layer)):
neuron = layer[j]
neuron['delta'] = errors[j] * transfer_derivative(neuron['output'])
# Update network weights with error
def update_weights(network, row, l_rate):
for i in range(len(network)):
inputs = row[:-1]
if i != 0:
inputs = [neuron['output'] for neuron in network[i - 1]]
for neuron in network[i]:
for j in range(len(inputs)):
neuron['weights'][j] += l_rate * neuron['delta'] * inputs[j]
neuron['weights'][-1] += l_rate * neuron['delta']
# Train a network for a fixed number of epochs
def train_network(network, train, l_rate, n_epoch, n_outputs):
for epoch in range(n_epoch):
sum_error = 0
for row in train:
outputs = forward_propagate(network, row)
expected = [0 for i in range(n_outputs)]
expected[row[-1]] = 1
sum_error += sum([(expected[i]-outputs[i])**2 for i in range(len(expected))])
backward_propagate_error(network, expected)
update_weights(network, row, l_rate)
print('>epoch=%d, lrate=%.3f, error=%.3f' % (epoch, l_rate, sum_error))
# Test training backprop algorithm
seed(1)
dataset = [[2.7810836,2.550537003,0],
[1.465489372,2.362125076,0],
[3.396561688,4.400293529,0],
[1.38807019,1.850220317,0],
[3.06407232,3.005305973,0],
[7.627531214,2.759262235,1],
[5.332441248,2.088626775,1],
[6.922596716,1.77106367,1],
[8.675418651,-0.242068655,1],
[7.673756466,3.508563011,1]]
n_inputs = len(dataset[0]) - 1
n_outputs = len(set([row[-1] for row in dataset]))
network = initialize_network(n_inputs, 2, n_outputs)
train_network(network, dataset, 0.5, 20, n_outputs)
for layer in network:
print(layer)
При запуске примера сначала выводится ошибка квадрата суммы в каждую эпоху обучения. Мы можем видеть тенденцию уменьшения этой ошибки с каждой эпохой.
После обучения распечатывается сеть с отображением изученных весов. Также все еще в сети находятся выходные и дельта-значения, которые можно игнорировать. Мы могли бы обновить нашу функцию обучения, чтобы удалить эти данные, если мы хотим.
>epoch=0, lrate=0.500, error=6.350
>epoch=1, lrate=0.500, error=5.531
>epoch=2, lrate=0.500, error=5.221
>epoch=3, lrate=0.500, error=4.951
>epoch=4, lrate=0.500, error=4.519
>epoch=5, lrate=0.500, error=4.173
>epoch=6, lrate=0.500, error=3.835
>epoch=7, lrate=0.500, error=3.506
>epoch=8, lrate=0.500, error=3.192
>epoch=9, lrate=0.500, error=2.898
>epoch=10, lrate=0.500, error=2.626
>epoch=11, lrate=0.500, error=2.377
>epoch=12, lrate=0.500, error=2.153
>epoch=13, lrate=0.500, error=1.953
>epoch=14, lrate=0.500, error=1.774
>epoch=15, lrate=0.500, error=1.614
>epoch=16, lrate=0.500, error=1.472
>epoch=17, lrate=0.500, error=1.346
>epoch=18, lrate=0.500, error=1.233
>epoch=19, lrate=0.500, error=1.132
[{'weights': [-1.4688375095432327, 1.850887325439514, 1.0858178629550297], 'output': 0.029980305604426185, 'delta': -0.0059546604162323625}, {'weights': [0.37711098142462157, -0.0625909894552989, 0.2765123702642716], 'output': 0.9456229000211323, 'delta': 0.0026279652850863837}]
[{'weights': [2.515394649397849, -0.3391927502445985, -0.9671565426390275], 'output': 0.23648794202357587, 'delta': -0.04270059278364587}, {'weights': [-2.5584149848484263, 1.0036422106209202, 0.42383086467582715], 'output': 0.7790535202438367, 'delta': 0.03803132596437354}]
Как только сеть обучена, мы должны использовать ее для прогнозирования.
5. Предсказать
Делать прогнозы с помощью обученной нейронной сети достаточно просто.
Мы уже видели, как распространять входной шаблон для получения выходного сигнала. Это все, что нам нужно сделать, чтобы сделать прогноз. Мы можем непосредственно использовать выходные значения как вероятность того, что шаблон принадлежит каждому выходному классу.
Возможно, было бы более полезно превратить этот вывод в четкое предсказание класса. Мы можем сделать это, выбрав значение класса с большей вероятностью. Это также называется функция arg max,
Ниже приведена функция с именемпредсказать, ()который реализует эту процедуру. Возвращает индекс в выходных данных сети, который имеет наибольшую вероятность. Предполагается, что значения класса были преобразованы в целые числа, начиная с 0.
# Make a prediction with a network
def predict(network, row):
outputs = forward_propagate(network, row)
return outputs.index(max(outputs))
Мы можем соединить это с нашим кодом выше для входного распространения и с нашим небольшим надуманным набором данных, чтобы проверить предсказания с уже обученной сетью. Пример жестко кодирует сеть, обученную на предыдущем шаге.
Полный пример приведен ниже.
from math import exp
# Calculate neuron activation for an input
def activate(weights, inputs):
activation = weights[-1]
for i in range(len(weights)-1):
activation += weights[i] * inputs[i]
return activation
# Transfer neuron activation
def transfer(activation):
return 1.0 / (1.0 + exp(-activation))
# Forward propagate input to a network output
def forward_propagate(network, row):
inputs = row
for layer in network:
new_inputs = []
for neuron in layer:
activation = activate(neuron['weights'], inputs)
neuron['output'] = transfer(activation)
new_inputs.append(neuron['output'])
inputs = new_inputs
return inputs
# Make a prediction with a network
def predict(network, row):
outputs = forward_propagate(network, row)
return outputs.index(max(outputs))
# Test making predictions with the network
dataset = [[2.7810836,2.550537003,0],
[1.465489372,2.362125076,0],
[3.396561688,4.400293529,0],
[1.38807019,1.850220317,0],
[3.06407232,3.005305973,0],
[7.627531214,2.759262235,1],
[5.332441248,2.088626775,1],
[6.922596716,1.77106367,1],
[8.675418651,-0.242068655,1],
[7.673756466,3.508563011,1]]
network = [[{'weights': [-1.482313569067226, 1.8308790073202204, 1.078381922048799]}, {'weights': [0.23244990332399884, 0.3621998343835864, 0.40289821191094327]}],
[{'weights': [2.5001872433501404, 0.7887233511355132, -1.1026649757805829]}, {'weights': [-2.429350576245497, 0.8357651039198697, 1.0699217181280656]}]]
for row in dataset:
prediction = predict(network, row)
print('Expected=%d, Got=%d' % (row[-1], prediction))
При выполнении примера выводится ожидаемый результат для каждой записи в наборе обучающих данных, за которым следует четкое предсказание, сделанное сетью.
Это показывает, что сеть достигает 100% точности в этом небольшом наборе данных.
Expected=0, Got=0
Expected=0, Got=0
Expected=0, Got=0
Expected=0, Got=0
Expected=0, Got=0
Expected=1, Got=1
Expected=1, Got=1
Expected=1, Got=1
Expected=1, Got=1
Expected=1, Got=1
Теперь мы готовы применить наш алгоритм обратного распространения к реальному набору данных.
6. Набор данных семян пшеницы
В этом разделе применяется алгоритм обратного распространения к набору данных семян пшеницы.
Первым шагом является загрузка набора данных и преобразование загруженных данных в числа, которые мы можем использовать в нашей нейронной сети. Для этого мы будем использовать вспомогательную функциюload_csv ()загрузить файл,str_column_to_float ()преобразовать строковые числа в числа с плавающей запятой иstr_column_to_int ()преобразовать столбец класса в целочисленные значения.
Входные значения различаются по масштабу и должны быть нормализованы в диапазоне от 0 до 1. Обычно рекомендуется нормализовать входные значения в диапазоне выбранной передаточной функции, в данном случае сигмоидальной функции, которая выводит значения в диапазоне от 0 до 1. .dataset_minmax ()а такжеnormalize_dataset ()вспомогательные функции были использованы для нормализации входных значений.
Мы оценим алгоритм с использованием k-кратной перекрестной проверки с 5-кратным увеличением. Это означает, что 201/5 = 40,2 или 40 записей будут в каждом сгибе. Мы будем использовать вспомогательные функцииevaluate_algorithm ()оценить алгоритм с перекрестной проверкой иaccuracy_metric ()рассчитать точность прогнозов.
Новая функция с именемback_propagation ()был разработан для управления приложением алгоритма Backpropagation, сначала инициализируя сеть, обучая ее на наборе обучающих данных, а затем используя обученную сеть, чтобы делать прогнозы на тестовом наборе данных.
Полный пример приведен ниже.
# Backprop on the Seeds Dataset
from random import seed
from random import randrange
from random import random
from csv import reader
from math import exp
# Load a CSV file
def load_csv(filename):
dataset = list()
with open(filename, 'r') as file:
csv_reader = reader(file)
for row in csv_reader:
if not row:
continue
dataset.append(row)
return dataset
# Convert string column to float
def str_column_to_float(dataset, column):
for row in dataset:
row[column] = float(row[column].strip())
# Convert string column to integer
def str_column_to_int(dataset, column):
class_values = [row[column] for row in dataset]
unique = set(class_values)
lookup = dict()
for i, value in enumerate(unique):
lookup[value] = i
for row in dataset:
row[column] = lookup[row[column]]
return lookup
# Find the min and max values for each column
def dataset_minmax(dataset):
minmax = list()
stats = [[min(column), max(column)] for column in zip(*dataset)]
return stats
# Rescale dataset columns to the range 0-1
def normalize_dataset(dataset, minmax):
for row in dataset:
for i in range(len(row)-1):
row[i] = (row[i] - minmax[i][0]) / (minmax[i][1] - minmax[i][0])
# Split a dataset into k folds
def cross_validation_split(dataset, n_folds):
dataset_split = list()
dataset_copy = list(dataset)
fold_size = int(len(dataset) / n_folds)
for i in range(n_folds):
fold = list()
while len(fold) < fold_size:
index = randrange(len(dataset_copy))
fold.append(dataset_copy.pop(index))
dataset_split.append(fold)
return dataset_split
# Calculate accuracy percentage
def accuracy_metric(actual, predicted):
correct = 0
for i in range(len(actual)):
if actual[i] == predicted[i]:
correct += 1
return correct / float(len(actual)) * 100.0
# Evaluate an algorithm using a cross validation split
def evaluate_algorithm(dataset, algorithm, n_folds, *args):
folds = cross_validation_split(dataset, n_folds)
scores = list()
for fold in folds:
train_set = list(folds)
train_set.remove(fold)
train_set = sum(train_set, [])
test_set = list()
for row in fold:
row_copy = list(row)
test_set.append(row_copy)
row_copy[-1] = None
predicted = algorithm(train_set, test_set, *args)
actual = [row[-1] for row in fold]
accuracy = accuracy_metric(actual, predicted)
scores.append(accuracy)
return scores
# Calculate neuron activation for an input
def activate(weights, inputs):
activation = weights[-1]
for i in range(len(weights)-1):
activation += weights[i] * inputs[i]
return activation
# Transfer neuron activation
def transfer(activation):
return 1.0 / (1.0 + exp(-activation))
# Forward propagate input to a network output
def forward_propagate(network, row):
inputs = row
for layer in network:
new_inputs = []
for neuron in layer:
activation = activate(neuron['weights'], inputs)
neuron['output'] = transfer(activation)
new_inputs.append(neuron['output'])
inputs = new_inputs
return inputs
# Calculate the derivative of an neuron output
def transfer_derivative(output):
return output * (1.0 - output)
# Backpropagate error and store in neurons
def backward_propagate_error(network, expected):
for i in reversed(range(len(network))):
layer = network[i]
errors = list()
if i != len(network)-1:
for j in range(len(layer)):
error = 0.0
for neuron in network[i + 1]:
error += (neuron['weights'][j] * neuron['delta'])
errors.append(error)
else:
for j in range(len(layer)):
neuron = layer[j]
errors.append(expected[j] - neuron['output'])
for j in range(len(layer)):
neuron = layer[j]
neuron['delta'] = errors[j] * transfer_derivative(neuron['output'])
# Update network weights with error
def update_weights(network, row, l_rate):
for i in range(len(network)):
inputs = row[:-1]
if i != 0:
inputs = [neuron['output'] for neuron in network[i - 1]]
for neuron in network[i]:
for j in range(len(inputs)):
neuron['weights'][j] += l_rate * neuron['delta'] * inputs[j]
neuron['weights'][-1] += l_rate * neuron['delta']
# Train a network for a fixed number of epochs
def train_network(network, train, l_rate, n_epoch, n_outputs):
for epoch in range(n_epoch):
for row in train:
outputs = forward_propagate(network, row)
expected = [0 for i in range(n_outputs)]
expected[row[-1]] = 1
backward_propagate_error(network, expected)
update_weights(network, row, l_rate)
# Initialize a network
def initialize_network(n_inputs, n_hidden, n_outputs):
network = list()
hidden_layer = [{'weights':[random() for i in range(n_inputs + 1)]} for i in range(n_hidden)]
network.append(hidden_layer)
output_layer = [{'weights':[random() for i in range(n_hidden + 1)]} for i in range(n_outputs)]
network.append(output_layer)
return network
# Make a prediction with a network
def predict(network, row):
outputs = forward_propagate(network, row)
return outputs.index(max(outputs))
# Backpropagation Algorithm With Stochastic Gradient Descent
def back_propagation(train, test, l_rate, n_epoch, n_hidden):
n_inputs = len(train[0]) - 1
n_outputs = len(set([row[-1] for row in train]))
network = initialize_network(n_inputs, n_hidden, n_outputs)
train_network(network, train, l_rate, n_epoch, n_outputs)
predictions = list()
for row in test:
prediction = predict(network, row)
predictions.append(prediction)
return(predictions)
# Test Backprop on Seeds dataset
seed(1)
# load and prepare data
filename = 'seeds_dataset.csv'
dataset = load_csv(filename)
for i in range(len(dataset[0])-1):
str_column_to_float(dataset, i)
# convert class column to integers
str_column_to_int(dataset, len(dataset[0])-1)
# normalize input variables
minmax = dataset_minmax(dataset)
normalize_dataset(dataset, minmax)
# evaluate algorithm
n_folds = 5
l_rate = 0.3
n_epoch = 500
n_hidden = 5
scores = evaluate_algorithm(dataset, back_propagation, n_folds, l_rate, n_epoch, n_hidden)
print('Scores: %s' % scores)
print('Mean Accuracy: %.3f%%' % (sum(scores)/float(len(scores))))
Была построена сеть с 5 нейронами в скрытом слое и 3 нейронами в выходном слое. Сеть была подготовлена для 500 эпох с темпом обучения 0,3. Эти параметры были найдены с небольшой пробой и ошибкой, но вы можете сделать это намного лучше.
При выполнении примера выводится средняя точность классификации для каждого сгиба, а также средняя производительность по всем сгибам.
Вы можете видеть, что обратное распространение и выбранная конфигурация достигли средней точности классификации около 93%, что значительно лучше, чем алгоритм нулевого правила, который немного лучше, чем точность 28%.
Scores: [92.85714285714286, 92.85714285714286, 97.61904761904762, 92.85714285714286, 90.47619047619048]
Mean Accuracy: 93.333%
расширения
В этом разделе перечислены расширения к учебнику, которые вы можете изучить.
- Параметры алгоритма настройки, Попробуйте большие или меньшие сети, обученные дольше или короче. Посмотрите, сможете ли вы улучшить производительность набора данных seed.
- Дополнительные методы, Поэкспериментируйте с различными методами инициализации веса (такими как небольшие случайные числа) и различными передаточными функциями (такими как tanh).
- Больше слоев, Добавьте поддержку для большего количества скрытых слоев, обученных так же, как один скрытый слой, используемый в этом руководстве.
- регрессия, Измените сеть так, чтобы в выходном слое был только один нейрон, и чтобы было предсказано реальное значение. Выберите регрессионный набор данных для практики. Линейная передаточная функция может использоваться для нейронов в выходном слое, или выходные значения выбранного набора данных могут быть масштабированы до значений между 0 и 1.
- Пакетный градиентный спуск, Измените процедуру обучения с онлайн на пакетный градиентный спуск и обновляйте веса только в конце каждой эпохи.
Вы пробовали какие-либо из этих расширений?
Поделитесь своим опытом в комментариях ниже.
Обзор
В этом руководстве вы узнали, как реализовать алгоритм обратного распространения с нуля.
В частности, вы узнали:
- Как переслать распространение входа для расчета выхода сети.
- Как обратно распространять ошибки и обновлять вес сети.
- Как применить алгоритм обратного распространения к реальному набору данных.
У вас есть вопросы?
Задайте свои вопросы в комментариях ниже, и я сделаю все возможное, чтобы ответить.
Практическая работа №1: Реализация метода обратного распространения ошибки для двухслойной полностью связанной нейронной сети
Задача
Требуется вывести расчетные формулы и спроектировать программную реализацию метода обратного распространения ошибки для двухслойной полносвязной нейронной сети. Обучение и тестирование сети происходит на наборе данных MNIST, функция активации скрытого слоя – relu, функция активации выходного слоя – softmax, функция ошибки – кросс-энтропия.
Математическая модель
Модель нейрона описывается следующими уравнениями:
где – входной сигнал,
– синаптический вес сигнала
,
– функция активации,
– смещение
Прямой ход
Для получения предсказания сети, производится прямой ход: для каждого нейрона последовательно, от начальных слоёв к конечным, вычисляется линейная активация входных сигналов, к ней применяется функция активации, после чего этот сигнал передаётся на следующий слой. В случае данной архитектуры:
где – выход скрытого слоя,
– функция активации скрытого слоя (relu),
– вход сети,
– выход сети,
– функция активации выходного слоя (softmax),
Метод обратного распространения ошибки
Метод обратного распространения ошибки определяет стратегию выбора весов сети 𝑤 с использованием градиентных методов оптимизации.
Схема обратного распространения ошибки состоит из следующих этапов:
-
Прямой проход по нейронной сети. На данном этапе вычисляются значения выходных сигналов каждого слоя, а так же производные их функций активации.
-
Вычисление значений целевой функции и её производной.
Целевая функция – кросс-энтропия, вычисляется как
где
– ожидаемый выход (метки)
Производную целевой функции по весам можно вывести следующим образом:
По весам второго слоя:
из условия
получаем
По весам первого слоя
-
Обратный проход нейронной сети и корректировка весов
-
Повторение этапов 1-3 до выполнения условия останова
Описание программной реализации
Network.py
Содержит реализацию нейронной сети
Класс NN содержит данные и методы для работы с сетью
Поля класса NN:
_input_size – размер входного слоя
_hidden_size – размер скрытого слоя
_output_size – размер выходного слоя
_w1, _b1 – массивы для хранения весов и смещений первого слоя
_w2, _b2 – массивы для хранения весов и смещений второго слоя
Методы класса NN:
_forward(input) – прямой проход сети. Возвращает выходной сигнал первого и второго слоя
_calculate_dE(input, label, output1, output2) – вычисление градиента функции ошибки. Возвращает градиент функции по весам и биасам первого и второго слоёв
_backprop(learning_rate, size, dEb1, dEb2) – корректировка весов сети при помощи посчитанных градиентов
init_weights() – инициализация весов нормальным распределением с дисперсией 1/10
fit(input, label, validate_data = None, batch_size = 100, learning_rate = 0.1, epochs = 100) – пакетное обучение сети на epochs эпохах, скоростью обучения learning_rate, размером пакета batch_size. Выводит точность и значение целевой функции на каждой эпохе
predict(input) – получение предсказания сети
utils.py:
Содержит вспомогательные функции.
relu(X) – функция relu
reluD(X) – производная функции relu
calcilate_E(predict, label) – подсчёт функции ошибки на основании предсказания сети и верной разметки
calculate_acc(prediction ,label) – посчёт точности на основании предсказания сети и верной разметки
main.py
Обучает сеть из класса NN на MNIST с параметрами из аргументов запуска. Измеряет время обучения.
Аргументы:
-
—hidden – количество нейронов в скрытом слое
-
—epochs – количество эпох обучения
-
—lr – скорость обучения
-
—batch – размер пакета
Как вызывать:
python main.py --hidden 30 --epochs 20 --lr 0,1 --batch 100
(в примере указаны параметры по умолчанию)
Эксперименты
Размер скрытого слоя: 30, эпох: 20, скорость обучения: 0,1, размер пакета: 100
train accuracy: 0.9729 train error: 0.0913
validate accuracy: 0.9613 validate error: 0.1238
Time: 28.309726 seconds
Размер скрытого слоя: 10, эпох: 20, скорость обучения: 0,1, размер пакета: 100
train accuracy: 0.9469 train error: 0.1805
validate accuracy: 0.9404 validate error: 0.2013
Time: 18.23942 seconds
Размер скрытого слоя: 10, эпох: 20, скорость обучения: 0,1, размер пакета: 100
train accuracy: 0.9874 train error: 0.0467
validate accuracy: 0.9721 validate error: 0.0878
Time: 58.233283 seconds
Размер скрытого слоя: 30, эпох: 50, скорость обучения: 0,1, размер пакета: 100
train accuracy: 0.9875 train error: 0.0418
validate accuracy: 0.9678 validate error: 0.1216
Time: 70.004101 seconds
Размер скрытого слоя: 30, эпох: 20, скорость обучения: 0,5, размер пакета: 100
train accuracy: 0.9766 train error: 0.0732
validate accuracy: 0.9622 validate error: 0.1614
Time: 26.213141 seconds
Размер скрытого слоя: 30, эпох: 30, скорость обучения: 0,05, размер пакета: 100
train accuracy: 0.971 train error: 0.0998
validate accuracy: 0.9625 validate error: 0.1225
Time: 40.577051 seconds
Размер скрытого слоя: 30, эпох: 20, скорость обучения: 0,1, размер пакета: 10
train accuracy: 0.9724 train error: 0.0928
validate accuracy: 0.9533 validate error: 0.2423
Time: 65.179444 seconds
Размер скрытого слоя: 30, эпох: 20, скорость обучения: 0,1, размер пакета: 1000
train accuracy: 0.9268 train error: 0.2588
validate accuracy: 0.9264 validate error: 0.2576
Time: 25.455479 seconds
Background
Backpropagation is a common method for training a neural network. There is no shortage of papers online that attempt to explain how backpropagation works, but few that include an example with actual numbers. This post is my attempt to explain how it works with a concrete example that folks can compare their own calculations to in order to ensure they understand backpropagation correctly.
Backpropagation in Python
You can play around with a Python script that I wrote that implements the backpropagation algorithm in this Github repo.
Backpropagation Visualization
For an interactive visualization showing a neural network as it learns, check out my Neural Network visualization.
Additional Resources
If you find this tutorial useful and want to continue learning about neural networks, machine learning, and deep learning, I highly recommend checking out Adrian Rosebrock’s new book, Deep Learning for Computer Vision with Python. I really enjoyed the book and will have a full review up soon.
Overview
For this tutorial, we’re going to use a neural network with two inputs, two hidden neurons, two output neurons. Additionally, the hidden and output neurons will include a bias.
Here’s the basic structure:

In order to have some numbers to work with, here are the initial weights, the biases, and training inputs/outputs:

The goal of backpropagation is to optimize the weights so that the neural network can learn how to correctly map arbitrary inputs to outputs.
For the rest of this tutorial we’re going to work with a single training set: given inputs 0.05 and 0.10, we want the neural network to output 0.01 and 0.99.
The Forward Pass
To begin, lets see what the neural network currently predicts given the weights and biases above and inputs of 0.05 and 0.10. To do this we’ll feed those inputs forward though the network.
We figure out the total net input to each hidden layer neuron, squash the total net input using an activation function (here we use the logistic function), then repeat the process with the output layer neurons.
Total net input is also referred to as just net input by some sources.
Here’s how we calculate the total net input for :
We then squash it using the logistic function to get the output of :
Carrying out the same process for we get:
We repeat this process for the output layer neurons, using the output from the hidden layer neurons as inputs.
Here’s the output for :
And carrying out the same process for we get:
Calculating the Total Error
We can now calculate the error for each output neuron using the squared error function and sum them to get the total error:
Some sources refer to the target as the ideal and the output as the actual.
The is included so that exponent is cancelled when we differentiate later on. The result is eventually multiplied by a learning rate anyway so it doesn’t matter that we introduce a constant here [1].
For example, the target output for is 0.01 but the neural network output 0.75136507, therefore its error is:
Repeating this process for (remembering that the target is 0.99) we get:
The total error for the neural network is the sum of these errors:
The Backwards Pass
Our goal with backpropagation is to update each of the weights in the network so that they cause the actual output to be closer the target output, thereby minimizing the error for each output neuron and the network as a whole.
Output Layer
Consider . We want to know how much a change in
affects the total error, aka
.
is read as “the partial derivative of
with respect to
“. You can also say “the gradient with respect to
“.
By applying the chain rule we know that:
Visually, here’s what we’re doing:

We need to figure out each piece in this equation.
First, how much does the total error change with respect to the output?
is sometimes expressed as
When we take the partial derivative of the total error with respect to , the quantity
becomes zero because
does not affect it which means we’re taking the derivative of a constant which is zero.
Next, how much does the output of change with respect to its total net input?
The partial derivative of the logistic function is the output multiplied by 1 minus the output:
Finally, how much does the total net input of change with respect to
?
Putting it all together:
You’ll often see this calculation combined in the form of the delta rule:
Alternatively, we have and
which can be written as
, aka
(the Greek letter delta) aka the node delta. We can use this to rewrite the calculation above:
Therefore:
Some sources extract the negative sign from so it would be written as:
To decrease the error, we then subtract this value from the current weight (optionally multiplied by some learning rate, eta, which we’ll set to 0.5):
We can repeat this process to get the new weights ,
, and
:
We perform the actual updates in the neural network after we have the new weights leading into the hidden layer neurons (ie, we use the original weights, not the updated weights, when we continue the backpropagation algorithm below).
Hidden Layer
Next, we’ll continue the backwards pass by calculating new values for ,
,
, and
.
Big picture, here’s what we need to figure out:
Visually:

We’re going to use a similar process as we did for the output layer, but slightly different to account for the fact that the output of each hidden layer neuron contributes to the output (and therefore error) of multiple output neurons. We know that affects both
and
therefore the
needs to take into consideration its effect on the both output neurons:
Starting with :
We can calculate using values we calculated earlier:
And is equal to
:
Plugging them in:
Following the same process for , we get:
Therefore:
Now that we have , we need to figure out
and then
for each weight:
We calculate the partial derivative of the total net input to with respect to
the same as we did for the output neuron:
Putting it all together:
You might also see this written as:
We can now update :
Repeating this for ,
, and
Finally, we’ve updated all of our weights! When we fed forward the 0.05 and 0.1 inputs originally, the error on the network was 0.298371109. After this first round of backpropagation, the total error is now down to 0.291027924. It might not seem like much, but after repeating this process 10,000 times, for example, the error plummets to 0.0000351085. At this point, when we feed forward 0.05 and 0.1, the two outputs neurons generate 0.015912196 (vs 0.01 target) and 0.984065734 (vs 0.99 target).
If you’ve made it this far and found any errors in any of the above or can think of any ways to make it clearer for future readers, don’t hesitate to drop me a note. Thanks!
And while I have you…
In addition to dabbling in data science, I run Preceden timeline maker, the best timeline maker software on the web. If you ever need to create a high level timeline or roadmap to get organized or align your team, Preceden is a great option.
Введение
В предыдущей статье мы увидели, как создать простой нейрон (перцептрон), и изучили, как выполняются вычисления градиентного спуска, как строится сеть (MLP), состоящая из взаимосвязанных перцептронов, и как выполняется обучение этого типа сети.
В этой статье я хочу продемонстрировать, насколько просто реализовать этот тип алгоритма с помощью языка Python.
Python был внедрен в набор инструментов MQL5 и открывает двери для многих возможностей, таких как изучение данных, создание и использование моделей машинного обучения.
Эта встроенная интеграция MQL5 в Python открывает для нас много возможностей, которые позволяют построить от простой линейной регрессии до моделей глубокого обучения. Поскольку это язык для профессионального использования, существует множество библиотек, которые берут на себя всю тяжелую работу, связанную с вычислениями.
В нашем примере мы построим сеть вручную, но, как было сказано в предыдущей статье, это всего лишь шаг, чтобы понять, что на самом деле происходит в процессе обучения и прогнозирования, а затем я покажу более сложный пример с использованием TensorFlow и Keras.
Что такое TensorFlow?
TensorFlow — это библиотека с открытым кодом для быстрой числовой обработки.
Он был создан, поддержан и выпущен Google под лицензией Apache 2.0 с открытым кодом. API виртуально предназначен для языка программирования Python, хотя есть доступ к базовому C++ API.
В отличие от других числовых библиотек, предназначенных для использования в глубоком обучении, таких как Theano, TensorFlow предназначен для использования как в научно-исследовательских, так и в системах разработки и производства, особенно RankBrain в поисковой системе Google и в интересном проекте DeepDream.
Он может работать в системах на одном процессоре, на графическом процессоре, а также на мобильных устройствах и на крупномасштабных системах, распределенных на сотни машин.
Что такое Keras?
Keras — это мощная библиотека Python с открытым кодом, которую легко использовать при разработке и оценке моделей глубокого обучения.
Он охватывает эффективные библиотеки вычислений Theano и TensorFlow и позволяет определять и обучать модели нейронных сетей всего за несколько строк кода.
Руководство
Это руководство разделено на 4 части:
- Установка и подготовка среды Python в MetaEditor.
- Первые шаги и реконструкция модели (перцептрон и MLP).
- Создание простой модели с использованием Keras и TensorFlow.
- Как интегрировать MQL5 в Python.
1. Установка и подготовка среды Python.
Начнем с загрузки Python с официального сайта www.python.org/downloads/
Чтобы работать с TensorFlow необходимо установить версию выше 3.3 и ниже 3.8; я использую версию 3.7.
После загрузки и начала процесса установки мы активируем опцию «Добавить Python 3.7 в PATH», это гарантирует, что некоторые вещи будут работать без необходимости дополнительной настройки в будущем.
Чтобы запустить скрипт Python прямо из нашего терминала MetaTrader 5, нам нужно только выполнить предыдущую настройку.
- Установить путь исполняемого файла Python (среда)
- Установите необходимые зависимости для проекта
Нам нужно открыть MetaEditor и перейти в Инструменты>Параметры.
В данном этапе нам нужно определить путь, на котором находится наш исполняемый файл Python, и иметь в виду, что после установки он, вероятно, будет содержать путь Python по умолчанию, если по какой-то причине это не так, то введите полный путь к исполняемому файлу, чтобы вы могли запустить скрипты прямо из вашего терминала MetaTrader 5.
В моем случае я использую полностью отдельную библиотечную среду, называемую виртуальной средой, это способ получить «чистую» установку и собрать только библиотеки, необходимые для проекта.
Для получения дополнительной информации о пакете venv перейдите по ссылке.
Как только это будет готово, мы сможем запускать скрипты Python прямо из терминала. Для нашего эксперимента нам потребуется установить библиотеки:
Если у вас есть вопросы о том, как установить библиотеки, смотрите руководство по установке модуля.
- MetaTrader 5
- TensorFlow
- Matplotlib
- Pandas
- Sklearn
Теперь, когда мы установили и настроили среду, давайте проведем небольшой тест, чтобы понять, как создать и запустить скрипт в нашем терминале. Чтобы запустить новый скрипт прямо из MetaEditor, мы можем выполнить следующие шаги:
Новый > Python script

Определите имя для вашего скрипта, мастер создания MetaEditor подсказывает нам автоматически импортировать некоторые библиотеки, это очень интересно и для нашего эксперимента мы выберем вариант numpy.

Теперь давайте создадим простой скрипт, который генерирует синусоидальный график.
# Copyright 2021, Lethan Corp. # https: import numpy as np import matplotlib.pyplot as plt data = np.linspace(-np.pi, np.pi, 201) plt.plot(data, np.sin(data)) plt.xlabel('Angle [rad]') plt.ylabel('sin(data)') plt.axis('tight') plt.show()
Чтобы запустить скрипт, просто нажмите на компилировать (F7) или откройте терминал MetaTrader 5 и прикрепите скрипт к графику. После этого результаты будут отображаться во вкладке экспертов в случае, если есть распечатки, как при разработке алгоритма MQL5, в нашем случае откроется окно с графиком функции, которую мы создали выше.
2 — Первые шаги и реконструкция модели (перцептрон и MLP).
Мы будем использовать тот же набор данных, что и пример в MQL5, чтобы было проще.
Затем мы увидим функцию под названием predict(), которая прогнозирует выходное значение для строки с заданным набором весов, в этом случае первым весом всегда будет смещение. А также функция активации.
# Transfer neuron activation def activation(activation): return 1.0 if activation >= 0.0 else 0.0 # Make a prediction with weights def predict(row, weights): z = weights[0] for i in range(len(row) - 1): z += weights[i + 1] * row[i] return activation(z)
Как мы уже знаем, для обучения сети нам необходимо выполнить процесс градиентного спуска, который подробно обсуждался в предыдущей статье, а в продолжение я покажу обучающую функцию «train_weights()».
# Estimate Perceptron weights using stochastic gradient descent def train_weights(train, l_rate, n_epoch): weights = [0.0 for i in range(len(train[0]))] #random.random() for epoch in range(n_epoch): sum_error = 0.0 for row in train: y = predict(row, weights) error = row[-1] - y sum_error += error**2 weights[0] = weights[0] + l_rate * error for i in range(len(row) - 1): weights[i + 1] = weights[i + 1] + l_rate * error * row[i] print('>epoch=%d, lrate=%.3f, error=%.3f' % (epoch, l_rate, sum_error)) return weights
Применение модели MLP:
Это руководство разделено на 5 частей:
- Запуск сети
- Распространение (FeedForward)
- Обратное распространение (BackPropagation)
- Обучение
- Прогноз
Запуск сети:
Начнем с чего-нибудь простого, для этого создадим новую сеть, готовую к обучению.
У каждого нейрона есть набор весов, которые необходимо поддерживать. Один вес для каждого входного соединения и один дополнительный вес для смещения. Нам нужно будет сохранить дополнительные свойства нейрона во время обучения, поэтому мы будем использовать словарь для отображения каждого нейрона и чтобы хранить свойства по именам, например, как «веса» для весов.
from random import seed from random import random # Initialize a network def initialize_network(n_inputs, n_hidden, n_outputs): network = list() hidden_layer = [{'weights':[random() for i in range(n_inputs + 1)]} for i in range(n_hidden)] network.append(hidden_layer) output_layer = [{'weights':[random() for i in range(n_hidden + 1)]} for i in range(n_outputs)] network.append(output_layer) return network seed(1) network = initialize_network(2, 1, 2) for layer in network: print(layer)
Теперь, когда мы знаем, как создать и запустить сеть, давайте посмотрим, как мы можем использовать ее для расчета выходных данных.
Распространение (FeedForward)
from math import exp # Calculate neuron activation for an input def activate(weights, inputs): activation = weights[-1] for i in range(len(weights)-1): activation += weights[i] * inputs[i] return activation # Transfer neuron activation def transfer(activation): return 1.0 / (1.0 + exp(-activation)) # Forward propagate input to a network output def forward_propagate(network, row): inputs = row for layer in network: new_inputs = [] for neuron in layer: activation = activate(neuron['weights'], inputs) neuron['output'] = transfer(activation) new_inputs.append(neuron['output']) inputs = new_inputs return inputs # test forward propagation network = [[{'weights': [0.13436424411240122, 0.8474337369372327, 0.763774618976614]}], [{'weights': [0.2550690257394217, 0.49543508709194095]}, {'weights': [0.4494910647887381, 0.651592972722763]}]] row = [1, 0, None] output = forward_propagate(network, row) print(output)
При выполнении приведенного выше скрипта мы получим результат:
[0.6629970129852887, 0.7253160725279748]
Фактические выходные значения пока просто абсурдны, но скоро мы узнаем, как сделать так, чтобы веса в нейронах были более полезными.
Обратное распространение
# Calculate the derivative of an neuron output def transfer_derivative(output): return output * (1.0 - output) # Backpropagate error and store in neurons def backward_propagate_error(network, expected): for i in reversed(range(len(network))): layer = network[i] errors = list() if i != len(network)-1: for j in range(len(layer)): error = 0.0 for neuron in network[i + 1]: error += (neuron['weights'][j] * neuron['delta']) errors.append(error) else: for j in range(len(layer)): neuron = layer[j] errors.append(expected[j] - neuron['output']) for j in range(len(layer)): neuron = layer[j] neuron['delta'] = errors[j] * transfer_derivative(neuron['output']) # test backpropagation of error network = [[{'output': 0.7105668883115941, 'weights': [0.13436424411240122, 0.8474337369372327, 0.763774618976614]}], [{'output': 0.6213859615555266, 'weights': [0.2550690257394217, 0.49543508709194095]}, {'output': 0.6573693455986976, 'weights': [0.4494910647887381, 0.651592972722763]}]] expected = [0, 1] backward_propagate_error(network, expected) for layer in network: print(layer)
Образец выполнения распечатывает сеть после завершения проверки ошибок. Как мы видим, значения ошибок вычисляются и сохраняются в нейронах для выходного слоя и скрытого слоя.
[{‘output’: 0.7105668883115941, ‘weights’: [0.13436424411240122, 0.8474337369372327, 0.763774618976614], ‘delta’: -0.0005348048046610517}]
[{‘output’: 0.6213859615555266, ‘weights’: [0.2550690257394217, 0.49543508709194095], ‘delta’: -0.14619064683582808}, {‘output’: 0.6573693455986976, ‘weights’: [0.4494910647887381, 0.651592972722763], ‘delta’: 0.0771723774346327}]
Обучение сети
from math import exp from random import seed from random import random # Initialize a network def initialize_network(n_inputs, n_hidden, n_outputs): network = list() hidden_layer = [{'weights':[random() for i in range(n_inputs + 1)]} for i in range(n_hidden)] network.append(hidden_layer) output_layer = [{'weights':[random() for i in range(n_hidden + 1)]} for i in range(n_outputs)] network.append(output_layer) return network # Calculate neuron activation for an input def activate(weights, inputs): activation = weights[-1] for i in range(len(weights)-1): activation += weights[i] * inputs[i] return activation # Transfer neuron activation def transfer(activation): return 1.0 / (1.0 + exp(-activation)) # Forward propagate input to a network output def forward_propagate(network, row): inputs = row for layer in network: new_inputs = [] for neuron in layer: activation = activate(neuron['weights'], inputs) neuron['output'] = transfer(activation) new_inputs.append(neuron['output']) inputs = new_inputs return inputs # Calculate the derivative of an neuron output def transfer_derivative(output): return output * (1.0 - output) # Backpropagate error and store in neurons def backward_propagate_error(network, expected): for i in reversed(range(len(network))): layer = network[i] errors = list() if i != len(network)-1: for j in range(len(layer)): error = 0.0 for neuron in network[i + 1]: error += (neuron['weights'][j] * neuron['delta']) errors.append(error) else: for j in range(len(layer)): neuron = layer[j] errors.append(expected[j] - neuron['output']) for j in range(len(layer)): neuron = layer[j] neuron['delta'] = errors[j] * transfer_derivative(neuron['output']) # Update network weights with error def update_weights(network, row, l_rate): for i in range(len(network)): inputs = row[:-1] if i != 0: inputs = [neuron['output'] for neuron in network[i - 1]] for neuron in network[i]: for j in range(len(inputs)): neuron['weights'][j] += l_rate * neuron['delta'] * inputs[j] neuron['weights'][-1] += l_rate * neuron['delta'] # Train a network for a fixed number of epochs def train_network(network, train, l_rate, n_epoch, n_outputs): for epoch in range(n_epoch): sum_error = 0 for row in train: outputs = forward_propagate(network, row) expected = [0 for i in range(n_outputs)] expected[row[-1]] = 1 sum_error += sum([(expected[i]-outputs[i])**2 for i in range(len(expected))]) backward_propagate_error(network, expected) update_weights(network, row, l_rate) print('>epoch=%d, lrate=%.3f, error=%.3f' % (epoch, l_rate, sum_error)) # Test training backprop algorithm seed(1) dataset = [[2.7810836,2.550537003,0], [1.465489372,2.362125076,0], [3.396561688,4.400293529,0], [1.38807019,1.850220317,0], [3.06407232,3.005305973,0], [7.627531214,2.759262235,1], [5.332441248,2.088626775,1], [6.922596716,1.77106367,1], [8.675418651,-0.242068655,1], [7.673756466,3.508563011,1]] n_inputs = len(dataset[0]) - 1 n_outputs = len(set([row[-1] for row in dataset])) network = initialize_network(n_inputs, 2, n_outputs) train_network(network, dataset, 0.5, 20, n_outputs) for layer in network: print(layer)
После обучения сеть распечатывается и отображаются выученные веса. Также в сети еще остаются выходные значения и значения дельты, которые можно игнорировать. Мы могли бы обновить нашу функцию обучения, чтобы удалить эти данные, если бы захотели.
>epoch=13, lrate=0.500, error=1.953
>epoch=14, lrate=0.500, error=1.774
>epoch=15, lrate=0.500, error=1.614
>epoch=16, lrate=0.500, error=1.472
>epoch=17, lrate=0.500, error=1.346
>epoch=18, lrate=0.500, error=1.233
>epoch=19, lrate=0.500, error=1.132
[{‘weights’: [-1.4688375095432327, 1.850887325439514, 1.0858178629550297], ‘output’: 0.029980305604426185, ‘delta’: -0.0059546604162323625}, {‘weights’: [0.37711098142462157, -0.0625909894552989, 0.2765123702642716], ‘output’: 0.9456229000211323, ‘delta’: 0.0026279652850863837}]
[{‘weights’: [2.515394649397849, -0.3391927502445985, -0.9671565426390275], ‘output’: 0.23648794202357587, ‘delta’: -0.04270059278364587}, {‘weights’: [-2.5584149848484263, 1.0036422106209202, 0.42383086467582715], ‘output’: 0.7790535202438367, ‘delta’: 0.03803132596437354}]
Чтобы сделать прогноз, мы можем использовать уже настроенный в предыдущем примере набор весов.
Прогнозирование
from math import exp # Calculate neuron activation for an input def activate(weights, inputs): activation = weights[-1] for i in range(len(weights)-1): activation += weights[i] * inputs[i] return activation # Transfer neuron activation def transfer(activation): return 1.0 / (1.0 + exp(-activation)) # Forward propagate input to a network output def forward_propagate(network, row): inputs = row for layer in network: new_inputs = [] for neuron in layer: activation = activate(neuron['weights'], inputs) neuron['output'] = transfer(activation) new_inputs.append(neuron['output']) inputs = new_inputs return inputs # Make a prediction with a network def predict(network, row): outputs = forward_propagate(network, row) return outputs.index(max(outputs)) # Test making predictions with the network dataset = [[2.7810836,2.550537003,0], [1.465489372,2.362125076,0], [3.396561688,4.400293529,0], [1.38807019,1.850220317,0], [3.06407232,3.005305973,0], [7.627531214,2.759262235,1], [5.332441248,2.088626775,1], [6.922596716,1.77106367,1], [8.675418651,-0.242068655,1], [7.673756466,3.508563011,1]] network = [[{'weights': [-1.482313569067226, 1.8308790073202204, 1.078381922048799]}, {'weights': [0.23244990332399884, 0.3621998343835864, 0.40289821191094327]}], [{'weights': [2.5001872433501404, 0.7887233511355132, -1.1026649757805829]}, {'weights': [-2.429350576245497, 0.8357651039198697, 1.0699217181280656]}]] for row in dataset: prediction = predict(network, row) print('Expected=%d, Got=%d' % (row[-1], prediction))
При выполнении примера распечатывается ожидаемый результат для каждой записи в наборе обучающих данных, после чего следует четкий прогноз, сделанный сетью.
Это показывает, что сеть достигает 100% точности на этом небольшом наборе данных.
Expected=0, Got=0
Expected=0, Got=0
Expected=0, Got=0
Expected=0, Got=0
Expected=0, Got=0
Expected=1, Got=1
Expected=1, Got=1
Expected=1, Got=1
Expected=1, Got=1
Expected=1, Got=1
3. Создание простой модели с использованием Keras и TensorFlow.
Для сбора данных мы будем использовать пакет MetaTrader 5. Запускаем наш скрипт путем импорта библиотек, необходимых для извлечения, преобразования и прогнозирования цен. Обратите внимание, что мы не будем детально рассматривать часть подготовки данных, но не забывайте, что это очень важный шаг для модели.
Начнем с краткого предварительного просмотра данных. Набор данных (dataset) состоит из последних 1000 баров актива EURUSD. Для этого нам понадобятся несколько шагов, например:
- Импорт библиотек
- Подключение к MetaTrader
- Сбор данных
- Преобразование данных, установка даты
- Графические данные
import MetaTrader5 as mt5 from pandas import to_datetime, DataFrame import matplotlib.pyplot as plt symbol = "EURUSD" if not mt5.initialize(): print("initialize() failed") mt5.shutdown() rates = mt5.copy_rates_from_pos(symbol, mt5.TIMEFRAME_D1, 0, 1000) mt5.shutdown() rates = DataFrame(rates) rates['time'] = to_datetime(rates['time'], unit='s') rates = rates.set_index(['time']) plt.figure(figsize = (15,10)) plt.plot(rates.close) plt.show()
После запуска кода мы визуализируем данные закрытия на графике с линиями, как показано ниже.

Давайте использовать регрессионный подход, в котором будет предсказано значение закрытия следующего периода.
В этом примере мы будем использовать одномерный подход и, как указано выше, не обращая внимания на передовые методы подготовки данных.
Одномерный временной ряд — это набор данных, состоящий из одной серии наблюдений с временным порядком, и ему необходима модель, чтобы учиться на серии прошлых наблюдений, чтобы предсказать следующее значение в последовательности.
Первый шаг — разделить загруженную серию на набор для обучения и тестирования. Для этого создадим функцию, которая разделит такую серию на две части; мы будем использовать процентное значение от общего размера серии для отсечения, например, 70% для обучения и 30% для тестов. Для валидации (backtest) у нас есть другие подходы, такие как разделение серии на обучение, тестирование и валидацию. Поскольку мы говорим о финансовых сериях, нам надо будет особо стараться избегать переобучения.
В нашей функции мы ожидаем получить массив numpy и значение отсечения, и мы вернем две подобранные серии.
Первый возврат — это весь набор от первой позиции 0 до размера, который представляет размер фактора, а второй ряд — все остальное.
def train_test_split(values, fator): train_size = int(len(values) * fator) return values[0:train_size], values[train_size:len(values)]
Модели на Keras можно определить как последовательность слоев.
Мы создаем последовательную модель и добавляем слои — по одному за раз — до тех пор, пока мы не будем довольны нашей сетевой архитектурой.
Первое, что нужно сделать правильно, это убедиться, что входной слой имеет правильное количество входных ресурсов. Это можно указать при создании первого слоя с аргументом input_dim.
Как узнать количество слоев и их типы?
Это очень сложный вопрос. Есть эвристики, которые мы можем использовать, и зачастую лучшая сетевая структура находится в процессе экспериментов методом проб и ошибок. Как правило, требуется достаточно большая сеть, чтобы охватить структуру проблемы.
В этом примере мы будем использовать полносвязную однослойную сетевую структуру.
Полносвязные слои определяются с помощью класса Dense. Мы можем указать количество нейронов или узлов в слое в качестве первого аргумента и указать функцию активации с помощью аргумента активации.
В первом слое, мы будем использовать функцию активации выпрямленного линейного блока, называемого ReLU.
Прежде чем можно будет предсказать одномерный ряд, его необходимо подготовить.
Модель MLP будет обучаться с помощью функции, которая назначает последовательность прошлых наблюдений в качестве входных данных для выходных наблюдений. Таким образом, последовательность наблюдений должна быть преобразована в множество примеров, на которых модель смогла бы учиться.
Рассмотрим одномерную последовательность:
[10, 20, 30, 40, 50, 60, 70, 80, 90]
Мы можем разделить последовательность на несколько шаблонов входа/выхода, называемых выборкой.
В нашем примере мы будем использовать три временных этапа, которые используются в качестве входных данных, и один временной шаг, который используется для изучаемого прогноза.
X, y
10, 20, 30 40
20, 30, 40 50
30, 40, 50 60
…
Далее, мы создаем функцию split_sequence(), которая реализует это поведение и разделит одномерный набор на несколько выборок, каждая из которых имеет определенное количество временных шагов, и выход — это единичный временной шаг.
Мы можем протестировать нашу функцию на небольшом наборе данных, таких как уже придуманные в вышеуказанном примере.
# univariate data preparation from numpy import array # split a univariate sequence into samples def split_sequence(sequence, n_steps): X, y = list(), list() for i in range(len(sequence)): # find the end of this pattern end_ix = i + n_steps # check if we are beyond the sequence if end_ix > len(sequence)-1: break # gather input and output parts of the pattern seq_x, seq_y = sequence[i:end_ix], sequence[end_ix] X.append(seq_x) y.append(seq_y) return array(X), array(y) # define input sequence raw_seq = [10, 20, 30, 40, 50, 60, 70, 80, 90] # choose a number of time steps n_steps = 3 # split into samples X, y = split_sequence(raw_seq, n_steps) # summarize the data for i in range(len(X)): print(X[i], y[i])
При выполнении примера одномерная серия делится на шесть выборок, каждая из которых имеет три временных шага входа и один временной шаг выхода.
[10 20 30] 40
[20 30 40] 50
[30 40 50] 60
[40 50 60] 70
[50 60 70] 80
[60 70 80] 90
Чтобы продолжить, нам нужно разделить выборку на X (feature) и y (target), чтобы мы могли обучить сеть. Для этого мы будем использовать функцию split_sequence(), которую мы создали ранее.
X_train, y_train = split_sequence(train, 3) X_test, y_test = split_sequence(test, 3)
Теперь, когда мы подготовили наши образцы данных, мы можем создать сеть MLP.
Простая модель MLP имеет единственный скрытый слой узлов (нейронов) и выходной слой, который используется для прогнозирования.
Мы можем определить MLP для прогнозирования одномерных временных рядов следующим образом.
# define model model = Sequential() model.add(Dense(100, activation='relu', input_dim=n_steps)) model.add(Dense(1)) model.compile(optimizer='adam', loss='mse')
Важным моментом в определении является форма входных данных, это то, что модель ожидает в качестве входных данных для каждой выборки с точки зрения количества временных шагов.
Количество временных шагов в качестве входных данных — это число, которое мы выбираем при подготовке нашего набора данных как аргумент функции split_sequence().
Размер входных данных для каждой выборки указывается в аргументе input_dim в определении первого скрытого слоя. Технически модель будет показывать каждый временной шаг как отдельный ресурс, а не как отдельные временные шаги.
У нас почти всегда есть несколько образцов, поэтому модель ожидает, что входной обучающий компонент данных будет иметь размеры или форму:
[samples, features]
Функция split_sequence() генерирует X в форме [образцы, характеристики], готовой к использованию.
Модель обучается с использованием эффективного алгоритма под названием Adam для стохастического градиентного спуска и он оптимизируется с использованием функции потерь среднеквадратичной ошибки, или ‘mse’.
Как только модель определена, мы можем обучить ее с помощью набора данных.
model.fit(X_train, y_train, epochs=100, verbose=2)
После обучения модели мы можем предсказать будущее значение; модель ожидает, что входная форма будет двумерной с [образцы, характеристики], поэтому нам нужно изменить форму единственной входной выборки до того, как сделать прогноз, например, с помощью формы [1, 3] для 1 выборки и 3 временных шагов, используемых в качестве входных характеристик.
Например, мы выберем последнюю запись тестовой выборки «X_test» и после предсказания сравним ее с реальным значением, содержащимся в последней выборке «y_test».
# demonstrate prediction x_input = X_test[-1] x_input = x_input.reshape((1, n_steps)) yhat = model.predict(x_input, verbose=0) print("Valor previsto: ", yhat) print("Valor real: ", y_test[-1])
4. Как интегрировать MQL5 в Python.
Чтобы использовать модель в торговом счете, у нас есть несколько вариантов, один из них — использовать встроенные функции в Python, которые открывают и закрывают позиции, но для этого сценария у нас не будет многих возможностей, которые предлагает нам MQL; по этой причине я выбрал одну интеграцию между Python и средой MQL, что даст нам больше автономии в управлении нашими позициями/ордерами.
Основываясь на статье MQL5 Соединение MetaTrader 5 и Python: получение и отправка данных, написанной Максимом Дмитриевским, я реализовал этот класс, используя шаблон разработки под названием Singleton, который будет отвечать за создание Socket-клиента для соединения. Этот шаблон гарантирует, что существует только одна копия объекта определенного типа, потому что если программа использует два указателя, относящиеся к одному и тому же объекту, они будут указывать на один и тот же объект.
class CClientSocket { private: static CClientSocket* m_socket; int m_handler_socket; int m_port; string m_host; int m_time_out; CClientSocket(void); ~CClientSocket(void); public: static bool DeleteSocket(void); bool SocketSend(string payload); string SocketReceive(void); bool IsConnected(void); static CClientSocket *Socket(void); bool Config(string host, int port); bool Close(void); };
Класс CClienteSocke хранит статический указатель как частный член; у класса есть только конструктор, который является частным и не может быть вызван, вместо вызова конструктора можно использовать метод Socket, этим мы гарантируем, что используется только один объект.
static CClientSocket *CClientSocket::Socket(void) { if(CheckPointer(m_socket)==POINTER_INVALID) m_socket=new CClientSocket(); return m_socket; }
Этот метод проверяет, указывает ли статический указатель на объект CClienteSocket, и если это правда, то он возвращает ссылку; в противном случае создается новый объект и связывается с указателем, тем самым гарантируя исключительность этого объекта в нашей системе.
Чтобы установить соединение с сервером, необходимо запустить соединение, поэтому мы создали метод IsConnected, чтобы установить соединение и мы можем начать передачу/получение данных.
bool CClientSocket::IsConnected(void) { ResetLastError(); bool res=true; m_handler_socket=SocketCreate(); if(m_handler_socket==INVALID_HANDLE) res=false; if(!::SocketConnect(m_handler_socket,m_host,m_port,m_time_out)) res=false; return res; }
После успешного подключения и успешной передачи сообщений, необходимо закрыть это соединение, для этого мы воспользуемся методом Close, чтобы закрыть ранее открытое соединение.
bool CClientSocket::Close(void) { bool res=false; if(SocketClose(m_handler_socket)) { res=true; m_handler_socket=INVALID_HANDLE; } return res; }
Теперь нам нужно прописать наш сервер, который будет получать новые MQL-соединения и отправлять прогнозы нашей модели.
import socket class socketserver(object): def __init__(self, address, port): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.address = address self.port = port self.sock.bind((self.address, self.port)) def socket_receive(self): self.sock.listen(1) self.conn, self.addr = self.sock.accept() self.cummdata = '' while True: data = self.conn.recv(10000) self.cummdata+=data.decode("utf-8") if not data: self.conn.close() break return self.cummdata def socket_send(self, message): self.sock.listen(1) self.conn, self.addr = self.sock.accept() self.conn.send(bytes(message, "utf-8")) def __del__(self): self.conn.close()
Наш объект простой, в его конструкторе мы получаем адрес и порт, с которым будем создавать свой сервер. Метод socket_received отвечает за принятие нового соединения и проверку наличия отправленных сообщений; если есть сообщения, которые нужно получить, мы запускаем цикл, пока не получим все части сообщения, затем мы закрываем соединение с клиентом и выходим из цикла. С другой стороны, метод socket_send отвечает за отправку сообщений нашему клиенту, но имейте в виду, что эта предлагаемая модель не только позволяет нам отправлять прогнозы нашей модели, а также открывает возможности для нескольких других вещей — всё будет зависеть от вашей креативности и потребности.
Теперь, когда у нас есть готовая коммуникация, мы должны подумать о двух вещах:
- Обучить модель и сохранить ее.
- Использовать обученную модель, чтобы делать прогнозы.
Я говорю так, потому что было бы непрактично и неправильно получать данные и проводить обучение каждый раз, когда мы хотим сделать прогноз. По этой причине я всегда провожу обучение, нахожу лучшие гиперпараметры и сохраняю свою модель для её дальнейшего использования.
Я создам файл с именем model_train, который будет содержать обучающий код сети, в нем мы будем использовать процентную разницу между ценами закрытия и попытаемся предсказать эту разницу. Обратите внимание, что меня не интересует модель, я хочу продемонстрировать, как использовать модель, интегрируя ее со средой MQL.
import MetaTrader5 as mt5 from numpy.lib.financial import rate from pandas import to_datetime, DataFrame from datetime import datetime, timezone from matplotlib import pyplot from sklearn.metrics import mean_squared_error from math import sqrt import numpy as np from tensorflow.keras import Sequential from tensorflow.keras.layers import Dense from tensorflow.keras.callbacks import * symbol = "EURUSD" date_ini = datetime(2020, 1, 1, tzinfo=timezone.utc) date_end = datetime(2021, 7, 1, tzinfo=timezone.utc) period = mt5.TIMEFRAME_D1 def train_test_split(values, fator): train_size = int(len(values) * fator) return np.array(values[0:train_size]), np.array(values[train_size:len(values)]) # split a univariate sequence into samples def split_sequence(sequence, n_steps): X, y = list(), list() for i in range(len(sequence)): end_ix = i + n_steps if end_ix > len(sequence)-1: break seq_x, seq_y = sequence[i:end_ix], sequence[end_ix] X.append(seq_x) y.append(seq_y) return np.array(X), np.array(y) if not mt5.initialize(): print("initialize() failed") mt5.shutdown() raise Exception("Error Getting Data") rates = mt5.copy_rates_range(symbol, period, date_ini, date_end) mt5.shutdown() rates = DataFrame(rates) if rates.empty: raise Exception("Error Getting Data") rates['time'] = to_datetime(rates['time'], unit='s') rates.set_index(['time'], inplace=True) rates = rates.close.pct_change(1) rates = rates.dropna() X, y = train_test_split(rates, 0.70) X = X.reshape(X.shape[0]) y = y.reshape(y.shape[0]) train, test = train_test_split(X, 0.7) n_steps = 60 verbose = 1 epochs = 50 X_train, y_train = split_sequence(train, n_steps) X_test, y_test = split_sequence(test, n_steps) X_val, y_val = split_sequence(y, n_steps) # define model model = Sequential() model.add(Dense(200, activation='relu', input_dim=n_steps)) model.add(Dense(1)) model.compile(optimizer='adam', loss='mse') history = model.fit(X_train ,y_train ,epochs=epochs ,verbose=verbose ,validation_data=(X_test, y_test)) model.save(r'C:YOUR_PATHMQL5ExpertsYOUR_PATHmodel_train_'+symbol+'.h5') pyplot.title('Loss') pyplot.plot(history.history['loss'], label='train') pyplot.plot(history.history['val_loss'], label='test') pyplot.legend() pyplot.show() history = list() yhat = list() for i in range(0, len(X_val)): pred = X_val[i] pred = pred.reshape((1, n_steps)) history.append(y_val[i]) yhat.append(model.predict(pred).flatten()[0]) pyplot.figure(figsize=(10, 5)) pyplot.plot(history,"*") pyplot.plot(yhat,"+") pyplot.plot(history, label='real') pyplot.plot(yhat, label='prediction') pyplot.ylabel('Price Close', size=10) pyplot.xlabel('time', size=10) pyplot.legend(fontsize=10) pyplot.show() rmse = sqrt(mean_squared_error(history, yhat)) mse = mean_squared_error(history, yhat) print('Test RMSE: %.3f' % rmse) print('Test MSE: %.3f' % mse)
Теперь, когда у нас есть обученная и сохраненная в нашей папке модель с расширением .h5, мы можем использовать ее для прогнозов, поэтому я создам объект, который создаст экземпляр этой модели для использования в соединении.
from tensorflow.keras.models import * class Model(object): def __init__(self, n_steps:int, symbol:str, period:int) -> None: super().__init__() self.n_steps = n_steps self.model = load_model(r'C:YOUR_PATHMQL5ExpertsYOUR_PATHmodel_train_'+symbol+'.h5') def predict(self, data): return(self.model.predict(data.reshape((1, self.n_steps))).flatten()[0])
Наш объект прост, его конструктор создает экземпляр атрибута, называемого моделью, который будет содержать сохраненную модель, а метод прогнозирования отвечает за осуществление прогнозирования.
Теперь нам нужен основной метод, который будет работать и взаимодействовать с клиентами, получая функции и отправляя прогнозы.
import ast import pandas as pd from model import Model from server_socket import socketserver host = 'localhost' port = 9091 n_steps = 60 TIMEFRAME = 24 | 0x4000 model = Model(n_steps, "EURUSD", TIMEFRAME) if __name__ == "__main__": serv = socketserver(host, port) while True: print("<<--Waiting Prices to Predict-->>") rates = pd.DataFrame(ast.literal_eval(serv.socket_receive())) rates = rates.rates.pct_change(1) rates.dropna(inplace=True) rates = rates.values.reshape((1, n_steps)) serv.socket_send(str(model.predict(rates).flatten()[0]))
На стороне нашего MQL-клиента нам нужно создать робота, который будет собирать данные и отправлять их на наш сервер, а затем получать прогнозы. Поскольку наша модель была обучена с данными от закрытых свечей, мы создадим проверку для сбора и отправки этих данных только после закрытия свечи, поэтому у нас будут полные данные для прогнозирования процентной разницы от закрытия только что начавшегося бара, для этого воспользуемся функцией, которая проверяет появление нового бара.
bool NewBar(void) { datetime time[]; if(CopyTime(Symbol(), Period(), 0, 1, time) < 1) return false; if(time[0] == m_last_time) return false; return bool(m_last_time = time[0]); }
Переменная m_last_time объявлена в глобальной области видимости и будет хранить дату и время открытия бара, поэтому мы проводим тест, проверяя, отличается ли переменная time от m_last_time, потому что если это true, это означает, что начал формироваться новый бар. Тут надо заменить значение m_last_time значением time.
Советник не должен открывать позиций, не закрыв предыдущую, поэтому проверяем наличие открытых позиций — для этого используем метод CheckPosition, который задаст значения true или false переменным покупки и продажи, объявленные в глобальной области видимости.
void CheckPosition(void) { buy = false; sell = false; if(PositionSelect(Symbol())) { if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY&&PositionGetInteger(POSITION_MAGIC) == InpMagicEA) { buy = true; sell = false; } if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL&&PositionGetInteger(POSITION_MAGIC) == InpMagicEA) { sell = true; buy = false; } } }
После появления нового бара мы проверяем наличие открытых позиций, и если есть открытые, то будем ждать закрытия. Если открытой позиции нет, мы запустим процесс соединения, вызвав метод IsConnected класса CClienteSocket.
if(NewBar()) { if(!Socket.IsConnected()) Print("Error : ", GetLastError(), " Line: ", __LINE__); ... }
если возвращается true, это потому, что мы можем установить соединение с нашим сервером, мы соберем данные и отправим их.
string payload = "{'rates':["; for(int i=InpSteps; i>=0; i--) { if(i>=1) payload += string(iClose(Symbol(), Period(), i))+","; else payload += string(iClose(Symbol(), Period(), i))+"]}"; }
Я решил отправить данные в формате {‘rates’:[1,2,3,4]}, потому что таким образом мы просто преобразуем их в pandas-датафрейм, и мы не будем тратить время на конверсии.
Как только данные собраны, мы отправляем их и ждем получения прогноза, чтобы мы могли потом принять решение. В этом случае, я буду использовать скользящую среднюю, чтобы проверить направление цены; и в зависимости от стороны и ценности прогноза мы будем покупать или продавать.
void OnTick(void) { .... bool send = Socket.SocketSend(payload); if(send) { if(!Socket.IsConnected()) Print("Error : ", GetLastError(), " Line: ", __LINE__); double yhat = StringToDouble(Socket.SocketReceive()); Print("Value of Prediction: ", yhat); if(CopyBuffer(handle, 0, 0, 4, m_fast_ma)==-1) Print("Error in CopyBuffer"); if(m_fast_ma[1]>m_fast_ma[2]&&m_fast_ma[2]>m_fast_ma[3]) { if((iClose(Symbol(), Period(), 2)>iOpen(Symbol(), Period(), 2)&&iClose(Symbol(), Period(), 1)>iOpen(Symbol(), Period(), 1))&&yhat<0) { m_trade.Sell(mim_vol); } } if(m_fast_ma[1]<m_fast_ma[2]&&m_fast_ma[2]<m_fast_ma[3]) { if((iClose(Symbol(), Period(), 2)<iOpen(Symbol(), Period(), 2)&&iClose(Symbol(), Period(), 1)<iOpen(Symbol(), Period(), 1))&&yhat>0) { m_trade.Buy(mim_vol); } } } Socket.Close(); } }
Последний шаг — закрыть ранее установленное соединение и дождаться появления нового бара, и таким образом запустить процесс отправки и получения прогноза.
Эта архитектура оказалась очень практичная и с низкой задержкой, поскольку ее можно протестировать. В некоторых личных проектах я решил использовать эту архитектуру в коммерческих учетных записях, потому что она позволяет мне пользоваться всей мощью MQL и всеми ресурсами, доступными для машинного обучения, имеющимся в Python.
Что будет дальше?
В следующей статье я хочу разработать немного более гибкую архитектуру, которая позволяет использовать модели в тестере стратегий, поэтому мы преодолеем единственный барьер, который может помешать нам использовать предложенную архитектуру.
Заключение:
Надеюсь, я дал небольшое руководство о том, как мы можем использовать и разрабатывать различные модели Python и передавать их в среду MQL.
Вы научились:
- Настраивать среду разработки Python.
- Мы вспоминали и реализовали нейрон перцептрона и сеть MLP в Python.
- Мы приготовили одномерные данные для изучения простой сети.
- Мы настраивали архитектуру связи между Python и MQL.
Расширения:
В этом разделе перечислены некоторые идеи, которые, возможно, помогут вам расширить руководство, если у вас есть желание их изучать.
- Размер входных данных. Примерно изучите количество дней, используемых в качестве входных данных для модели, например три дня, 21 день, 30 дней и т.д.
- Настройка модели. Изучите различные структуры и гиперпараметры модели и получите среднюю производительность модели.
- Масштабирование данных. Узнайте, можно ли использовать размер данных, например стандартизацию и нормализацию, для повышения производительности модели.
- Диагностика обучения. Используйте такую диагностику, как кривые обучения для потери обучения и валидации, и среднеквадратичную ошибку, чтобы помочь настроить структурy и гиперпараметры модели.
Если вы изучите какое-либо из этих расширений, я был бы рад узнать об этом.
by Samay Shamdasani
Neural networks can be intimidating, especially for people new to machine learning. However, this tutorial will break down how exactly a neural network works and you will have a working flexible neural network by the end. Let’s get started!
Understanding the process
With approximately 100 billion neurons, the human brain processes data at speeds as fast as 268 mph! In essence, a neural network is a collection of neurons connected by synapses.
This collection is organized into three main layers: the input later, the hidden layer, and the output layer.
You can have many hidden layers, which is where the term deep learning comes into play. In an artificial neural network, there are several inputs, which are called features, which produce at least one output — which is called a label.
In the drawing above, the circles represent neurons while the lines represent synapses.
The role of a synapse is to take and multiply the inputs and weights.
You can think of weights as the “strength” of the connection between neurons. Weights primarily define the output of a neural network. However, they are highly flexible. After, an activation function is applied to return an output.
Here’s a brief overview of how a simple feedforward neural network works:
- Take inputs as a matrix (2D array of numbers)
- Multiply the inputs by a set of weights (this is done by matrix multiplication, aka taking the ‘dot product’)
- Apply an activation function
- Return an output
- Error is calculated by taking the difference between the desired output from the model and the predicted output. This is a process called gradient descent, which we can use to alter the weights.
- The weights are then adjusted, according to the error found in step 5.
- To train, this process is repeated 1,000+ times. The more the data is trained upon, the more accurate our outputs will be.
At their core, neural networks are simple.
They just perform matrix multiplication with the input and weights, and apply an activation function.
When weights are adjusted via the gradient of loss function, the network adapts to the changes to produce more accurate outputs.
Our neural network will model a single hidden layer with three inputs and one output. In the network, we will be predicting the score of our exam based on the inputs of how many hours we studied and how many hours we slept the day before. The output is the ‘test score’.
Here’s our sample data of what we’ll be training our Neural Network on:
As you may have noticed, the ? in this case represents what we want our neural network to predict. In this case, we are predicting the test score of someone who studied for four hours and slept for eight hours based on their prior performance.
Forward Propagation
Let’s start coding this bad boy! Open up a new python file. You’ll want to import numpy as it will help us with certain calculations.
First, let’s import our data as numpy arrays using np.array. We’ll also want to normalize our units as our inputs are in hours, but our output is a test score from 0-100. Therefore, we need to scale our data by dividing by the maximum value for each variable.
Next, let’s define a python class and write an init function where we’ll specify our parameters such as the input, hidden, and output layers.
It is time for our first calculation. Remember that our synapses perform a dot product, or matrix multiplication of the input and weight. Note that weights are generated randomly and between 0 and 1.
The calculations behind our network
In the data set, our input data, X, is a 3×2 matrix. Our output data, y, is a 3×1 matrix. Each element in matrix X needs to be multiplied by a corresponding weight and then added together with all the other results for each neuron in the hidden layer. Here’s how the first input data element (2 hours studying and 9 hours sleeping) would calculate an output in the network:
This image breaks down what our neural network actually does to produce an output. First, the products of the random generated weights (.2, .6, .1, .8, .3, .7) on each synapse and the corresponding inputs are summed to arrive as the first values of the hidden layer. These sums are in a smaller font as they are not the final values for the hidden layer.
To get the final value for the hidden layer, we need to apply the activation function.
The role of an activation function is to introduce nonlinearity. An advantage of this is that the output is mapped from a range of 0 and 1, making it easier to alter weights in the future.
There are many activation functions out there, for many different use cases. In this example, we’ll stick to one of the more popular ones — the sigmoid function.
Now, we need to use matrix multiplication again, with another set of random weights, to calculate our output layer value.
Lastly, to normalize the output, we just apply the activation function again.
And, there you go! Theoretically, with those weights, out neural network will calculate .85 as our test score! However, our target was .92. Our result wasn’t poor, it just isn’t the best it can be. We just got a little lucky when I chose the random weights for this example.
How do we train our model to learn? Well, we’ll find out very soon. For now, let’s countinue coding our network.
If you are still confused, I highly reccomend you check out this informative video which explains the structure of a neural network with the same example.
Implementing the calculations
Now, let’s generate our weights randomly using np.random.randn(). Remember, we’ll need two sets of weights. One to go from the input to the hidden layer, and the other to go from the hidden to output layer.
Once we have all the variables set up, we are ready to write our forward propagation function. Let’s pass in our input, X, and in this example, we can use the variable z to simulate the activity between the input and output layers.
As explained, we need to take a dot product of the inputs and weights, apply an activation function, take another dot product of the hidden layer and second set of weights, and lastly apply a final activation function to receive our output:
Lastly, we need to define our sigmoid function:
And, there we have it! A (untrained) neural network capable of producing an output.
As you may have noticed, we need to train our network to calculate more accurate results.
Backpropagation — the “learning” of our network
Since we have a random set of weights, we need to alter them to make our inputs equal to the corresponding outputs from our data set. This is done through a method called backpropagation.
Backpropagation works by using a loss function to calculate how far the network was from the target output.
Calculating error
One way of representing the loss function is by using the mean sum squared loss function:
In this function, o is our predicted output, and y is our actual output. Now that we have the loss function, our goal is to get it as close as we can to 0. That means we will need to have close to no loss at all. As we are training our network, all we are doing is minimizing the loss.
To figure out which direction to alter the weights, we need to find the rate of change of our loss with respect to our weights. In other words, we need to use the derivative of the loss function to understand how the weights affect the input.
In this case, we will be using a partial derivative to allow us to take into account another variable.
This method is known as gradient descent. By knowing which way to alter our weights, our outputs can only get more accurate.
Here’s how we will calculate the incremental change to our weights:
- Find the margin of error of the output layer (o) by taking the difference of the predicted output and the actual output (y)
- Apply the derivative of our sigmoid activation function to the output layer error. We call this result the delta output sum.
- Use the delta output sum of the output layer error to figure out how much our z² (hidden) layer contributed to the output error by performing a dot product with our second weight matrix. We can call this the z² error.
- Calculate the delta output sum for the z² layer by applying the derivative of our sigmoid activation function (just like step 2).
- Adjust the weights for the first layer by performing a dot product of the input layer with the hidden (z²) delta output sum. For the second weight, perform a dot product of the hidden(z²) layer and the output (o) delta output sum.
Calculating the delta output sum and then applying the derivative of the sigmoid function are very important to backpropagation. The derivative of the sigmoid, also known as sigmoid prime, will give us the rate of change, or slope, of the activation function at output sum.
Let’s continue to code our Neural_Network class by adding a sigmoidPrime (derivative of sigmoid) function:
Then, we’ll want to create our backward propagation function that does everything specified in the four steps above:
We can now define our output through initiating foward propagation and intiate the backward function by calling it in the train function:
To run the network, all we have to do is to run the train function. Of course, we’ll want to do this multiple, or maybe thousands, of times. So, we’ll use a for loop.
Here’s the full 60 lines of awesomeness:
There you have it! A full-fledged neural network that can learn from inputs and outputs.
While we thought of our inputs as hours studying and sleeping, and our outputs as test scores, feel free to change these to whatever you like and observe how the network adapts!
After all, all the network sees are the numbers. The calculations we made, as complex as they seemed to be, all played a big role in our learning model.
If you’d like to predict an output based on our trained data, such as predicting the test score if you studied for four hours and slept for eight, check out the full tutorial here.
Demo & Source
References
Steven Miller
Welch Labs
Kabir Shah
This tutorial was originally posted on Enlight, a website that hosts a variety of tutorials and projects to learn by building! Check it out for more projects like these 🙂
by Samay Shamdasani
Neural networks can be intimidating, especially for people new to machine learning. However, this tutorial will break down how exactly a neural network works and you will have a working flexible neural network by the end. Let’s get started!
Understanding the process
With approximately 100 billion neurons, the human brain processes data at speeds as fast as 268 mph! In essence, a neural network is a collection of neurons connected by synapses.
This collection is organized into three main layers: the input later, the hidden layer, and the output layer.
You can have many hidden layers, which is where the term deep learning comes into play. In an artificial neural network, there are several inputs, which are called features, which produce at least one output — which is called a label.
In the drawing above, the circles represent neurons while the lines represent synapses.
The role of a synapse is to take and multiply the inputs and weights.
You can think of weights as the “strength” of the connection between neurons. Weights primarily define the output of a neural network. However, they are highly flexible. After, an activation function is applied to return an output.
Here’s a brief overview of how a simple feedforward neural network works:
- Take inputs as a matrix (2D array of numbers)
- Multiply the inputs by a set of weights (this is done by matrix multiplication, aka taking the ‘dot product’)
- Apply an activation function
- Return an output
- Error is calculated by taking the difference between the desired output from the model and the predicted output. This is a process called gradient descent, which we can use to alter the weights.
- The weights are then adjusted, according to the error found in step 5.
- To train, this process is repeated 1,000+ times. The more the data is trained upon, the more accurate our outputs will be.
At their core, neural networks are simple.
They just perform matrix multiplication with the input and weights, and apply an activation function.
When weights are adjusted via the gradient of loss function, the network adapts to the changes to produce more accurate outputs.
Our neural network will model a single hidden layer with three inputs and one output. In the network, we will be predicting the score of our exam based on the inputs of how many hours we studied and how many hours we slept the day before. The output is the ‘test score’.
Here’s our sample data of what we’ll be training our Neural Network on:
As you may have noticed, the ? in this case represents what we want our neural network to predict. In this case, we are predicting the test score of someone who studied for four hours and slept for eight hours based on their prior performance.
Forward Propagation
Let’s start coding this bad boy! Open up a new python file. You’ll want to import numpy as it will help us with certain calculations.
First, let’s import our data as numpy arrays using np.array. We’ll also want to normalize our units as our inputs are in hours, but our output is a test score from 0-100. Therefore, we need to scale our data by dividing by the maximum value for each variable.
Next, let’s define a python class and write an init function where we’ll specify our parameters such as the input, hidden, and output layers.
It is time for our first calculation. Remember that our synapses perform a dot product, or matrix multiplication of the input and weight. Note that weights are generated randomly and between 0 and 1.
The calculations behind our network
In the data set, our input data, X, is a 3×2 matrix. Our output data, y, is a 3×1 matrix. Each element in matrix X needs to be multiplied by a corresponding weight and then added together with all the other results for each neuron in the hidden layer. Here’s how the first input data element (2 hours studying and 9 hours sleeping) would calculate an output in the network:
This image breaks down what our neural network actually does to produce an output. First, the products of the random generated weights (.2, .6, .1, .8, .3, .7) on each synapse and the corresponding inputs are summed to arrive as the first values of the hidden layer. These sums are in a smaller font as they are not the final values for the hidden layer.
To get the final value for the hidden layer, we need to apply the activation function.
The role of an activation function is to introduce nonlinearity. An advantage of this is that the output is mapped from a range of 0 and 1, making it easier to alter weights in the future.
There are many activation functions out there, for many different use cases. In this example, we’ll stick to one of the more popular ones — the sigmoid function.
Now, we need to use matrix multiplication again, with another set of random weights, to calculate our output layer value.
Lastly, to normalize the output, we just apply the activation function again.
And, there you go! Theoretically, with those weights, out neural network will calculate .85 as our test score! However, our target was .92. Our result wasn’t poor, it just isn’t the best it can be. We just got a little lucky when I chose the random weights for this example.
How do we train our model to learn? Well, we’ll find out very soon. For now, let’s countinue coding our network.
If you are still confused, I highly reccomend you check out this informative video which explains the structure of a neural network with the same example.
Implementing the calculations
Now, let’s generate our weights randomly using np.random.randn(). Remember, we’ll need two sets of weights. One to go from the input to the hidden layer, and the other to go from the hidden to output layer.
Once we have all the variables set up, we are ready to write our forward propagation function. Let’s pass in our input, X, and in this example, we can use the variable z to simulate the activity between the input and output layers.
As explained, we need to take a dot product of the inputs and weights, apply an activation function, take another dot product of the hidden layer and second set of weights, and lastly apply a final activation function to receive our output:
Lastly, we need to define our sigmoid function:
And, there we have it! A (untrained) neural network capable of producing an output.
As you may have noticed, we need to train our network to calculate more accurate results.
Backpropagation — the “learning” of our network
Since we have a random set of weights, we need to alter them to make our inputs equal to the corresponding outputs from our data set. This is done through a method called backpropagation.
Backpropagation works by using a loss function to calculate how far the network was from the target output.
Calculating error
One way of representing the loss function is by using the mean sum squared loss function:
In this function, o is our predicted output, and y is our actual output. Now that we have the loss function, our goal is to get it as close as we can to 0. That means we will need to have close to no loss at all. As we are training our network, all we are doing is minimizing the loss.
To figure out which direction to alter the weights, we need to find the rate of change of our loss with respect to our weights. In other words, we need to use the derivative of the loss function to understand how the weights affect the input.
In this case, we will be using a partial derivative to allow us to take into account another variable.
This method is known as gradient descent. By knowing which way to alter our weights, our outputs can only get more accurate.
Here’s how we will calculate the incremental change to our weights:
- Find the margin of error of the output layer (o) by taking the difference of the predicted output and the actual output (y)
- Apply the derivative of our sigmoid activation function to the output layer error. We call this result the delta output sum.
- Use the delta output sum of the output layer error to figure out how much our z² (hidden) layer contributed to the output error by performing a dot product with our second weight matrix. We can call this the z² error.
- Calculate the delta output sum for the z² layer by applying the derivative of our sigmoid activation function (just like step 2).
- Adjust the weights for the first layer by performing a dot product of the input layer with the hidden (z²) delta output sum. For the second weight, perform a dot product of the hidden(z²) layer and the output (o) delta output sum.
Calculating the delta output sum and then applying the derivative of the sigmoid function are very important to backpropagation. The derivative of the sigmoid, also known as sigmoid prime, will give us the rate of change, or slope, of the activation function at output sum.
Let’s continue to code our Neural_Network class by adding a sigmoidPrime (derivative of sigmoid) function:
Then, we’ll want to create our backward propagation function that does everything specified in the four steps above:
We can now define our output through initiating foward propagation and intiate the backward function by calling it in the train function:
To run the network, all we have to do is to run the train function. Of course, we’ll want to do this multiple, or maybe thousands, of times. So, we’ll use a for loop.
Here’s the full 60 lines of awesomeness:
There you have it! A full-fledged neural network that can learn from inputs and outputs.
While we thought of our inputs as hours studying and sleeping, and our outputs as test scores, feel free to change these to whatever you like and observe how the network adapts!
After all, all the network sees are the numbers. The calculations we made, as complex as they seemed to be, all played a big role in our learning model.
If you’d like to predict an output based on our trained data, such as predicting the test score if you studied for four hours and slept for eight, check out the full tutorial here.
Demo & Source
References
Steven Miller
Welch Labs
Kabir Shah
This tutorial was originally posted on Enlight, a website that hosts a variety of tutorials and projects to learn by building! Check it out for more projects like these 🙂