Программируем свою нейронную сеть
Нейронные сети: теория и реализация на NumPy
Нейронные сети являются своего рода трендом в области развития computer science. Появляется всё больше софта, которое использует machine learning алгоритмы внутри. На мой взгляд, бизнес в большей степени сейчас решает задачи на основе классических алгоритмов машинного обучения (т.е. без использования нейронных сетей). Однако есть ряд задач, где нейронные сети хорошо себя зарекомендовали:
- обработка изображений
- обработка видео
- голосовые ассистенты и помощники
- обработка и распознавание текста
- self-driving машины
- разведка ландшафта дронами
Эта статья преследует цель глубже понять строение нейронной сети и даёт возможность написать свою нейронную сеть без использования deep learning библиотек.
Проблема
Нейросети, как правило, решают какую-то проблему. Поэтому придумаем для себя следующую задачу.
Представим, что есть некоторые люди, которые страдают проблемой ожирения и диабетом.
| Person | Smoking | Obesity | Exercise | Diabetic |
|---|---|---|---|---|
| Person 1 | 0 | 1 | 0 | 1 |
| Person 2 | 0 | 0 | 1 | 0 |
| Person 3 | 1 | 0 | 0 | 0 |
| Person 4 | 1 | 1 | 0 | 1 |
| Person 5 | 1 | 1 | 1 | 1 |
Человек может курить (smoking), страдать ожирением (obesity), заниматься спортом (exercise), быть диабетиком (diabetic). Если в ячейке стоит 1 — это истина, 0 — ложь. Например, Person 1 страдает ожирением и является диабетиком.
Мы хотим на новых данных научиться предсказывать эти характеристики. Создадим простую нейронную сеть с одним слоем на вход и одним слоем на выход.
Краткое введение в нейронные сети
Нейронные сети относятся к классу задач обучения с учителем. Это значит, что мы вначале подготавливаем некие данные, на которых нейронная сеть должна обучаться. Затем на новых данных мы проверяем, насколько хорошо нейронная сеть обучилась.
Сначала нейронная сеть действует практически наугад. Затем получившиеся данные она сравнивает с ответом (который у нас есть на обучающей выборке) и вычисляет разницу между получившимся результатом и правильными данными. Эта функция называется функцией стоимости (cost function), или по-другому функцией потерь (loss function). Под стоимостью/потерей подразумевается ошибка. Наша цель заключается в минимизации этой ошибки.
xi представляют собой независимые переменные (или характеристики), wi представляют собой веса или коэффициенты каждой характеристики. Выходной слой является взвешенной суммой.
Основные составляющие нейронной сети
Слои
Каждая нейронная сеть состоит из нескольких слоёв. На каждом слое происходит трансформация данных. Входные данные проходят через каждый слой и в конце достигают выходного слоя, где мы получаем результат.
- Входной слой — слой, куда приходят наши данные на начальном этапе. Это первый слой нейронной сети.
- Выходной слой — слой, где мы получаем результат работы нейронной сети.
- Скрытый слой — все остальные слои в сети. Мы не знаем, что происходит в этих слоях, поэтому они и называются скрытыми.
Нейроны
Каждый слой состоит из нейронов. Нейроны представляют наши данные, каждый нейрон содержит одно числовое значение. Например, если вы передаёте картинку для анализа размерами 640×480 пикселей, вам потребуется 640×480 = 307 200 нейронов на входном слое.
Связанные слои
Нейроны могут быть связаны со следующим слоем различными способами. Например, если каждый нейрон связан с каждым нейроном следующего слоя, такая связь называется плотной.
Веса
Веса представляют собой связи между нейронами различных слоёв. По сути, они обозначают, насколько сильна связь между нейронами. Эти веса обновляются во время тренировки нейронной сети.
Bias (отклонение)
Bias является некой константой. Служит для более точной настройки нейронной сети.
Функция активации
Функция, которая применяется к взвешенной сумме нейрона. Наиболее распространённые функции:
- RELU
- Гиперболический тангенс
- Сигмоида
Обратное распространение
Является ключевым алгоритмом в тренировке нейронной сети. Именно он отвечает за подбор весов нейронов и bias. Работа нейронной сети происходит в два этапа.
Прямая связь (Feedforward)
На этом этапе предсказания осуществляются на основе входных данных и весов нейронов. В нашем примере 3 нейрона, которые обозначают: курение, ожирение, занятия спортом.
Также у нас есть bias (отклонение) — очень важный параметр в тренировке сети. Представьте ситуацию, где человек не курит, не страдает ожирением и не занимается спортом. Тогда итоговое значение будет всегда нулём, независимо от подобранных весов. Поэтому мы добавляем отклонение, чтобы получать более надёжный результат:
X·W = x1·w1 + x2·w2 + x3·w3 + b
Веса могут быть любыми значениями, однако на выходе мы хотим получить значение 0 или 1. Поэтому в конце применяется функция активации, которая отображает результат в отрезок [0, 1]. В качестве такой функции возьмём сигмоиду.
Если у вас установлены numpy и matplotlib, можете нарисовать эту функцию:
input = np.linspace(-10, 10, 100)
def sigmoid(x):
return 1/(1+np.exp(-x))
from matplotlib import pyplot as plt
plt.plot(input, sigmoid(input), c="r")
Обратное распространение (Backpropagation)
Сначала мы совершенно случайно берём параметры, смотрим на итоговый результат. Затем сравниваем с правильным ответом, считаем ошибку и подбираем веса и отклонение так, чтобы ошибка стремилась к нулю.
В качестве функции потерь возьмём среднеквадратичную ошибку (MSE):
MSE = (1/n) · Σ (predicted − observed)²
где n — количество наблюдений.
Наша главная цель — минимизировать ошибку. Решение этой задачи относится к поиску минимума в классе оптимизационных задач. Решение может быть получено при помощи алгоритма градиентного спуска.
Реализация при помощи NumPy
Запишем наши характеристики и метки:
import numpy as np
features = np.array([[0, 1, 0], [0, 0, 1], [1, 0, 0], [1, 1, 0], [1, 1, 1]])
labels = np.array([[1, 0, 0, 1, 1]])
labels = labels.reshape(5, 1)
Определим веса и bias:
np.random.seed(123)
weights = np.random.rand(3, 1)
bias = np.random.rand(1)
lr = 0.05
Тренируем нашу сеть:
for epoch in range(20000):
# feedforward
XW = np.dot(features, weights) + bias
output = sigmoid(XW)
# backpropagation step 1
error = output - labels
print(error.sum())
# backpropagation step 2
dcost_dpred = error
dpred_dz = sigmoid_der(output)
output_delta = dcost_dpred * dpred_dz
inputs = features.T
weights -= lr * np.dot(inputs, output_delta)
for num in output_delta:
bias -= lr * num
Нам нужно посчитать производную функции потерь:
d_cost/dw = d_cost/d_pred · d_pred/dz · dz/dw
dcost/dpredвычисляется как2(predicted − observed). Константу 2 можно убрать — получаем простоerror.dpredявляется нашей сигмоидной функцией, производную которой мы посчитали выше.dz/dwявляется по сути входными характеристиками.
Вместо того чтобы проходиться по каждой записи и умножать на output_delta, можно транспонировать матрицу характеристик и матрично перемножить с output_delta. В конце умножаем на lr (learning rate) — скорость обучения нейронной сети.
В начале ошибка довольно большая — 0.83. Однако в конце она уменьшается:
0.001682051587058541
0.0016819688044010135
0.0016818860299710938
0.001681803263767337
0.0016817205057886135
0.0016816377560336122
0.0016815550145013198
0.001681472281190189
0.0016813895560991061
0.0016813068392270755
0.0016812241305727664
0.001681141430134642
0.0016810587379115956
0.0016809760539027597
0.001680893378106809
Теперь предскажем новые данные:
new_value = np.array([0, 1, 0])
result = sigmoid(np.dot(new_value, weights) + bias)
print("result is {:.10f}".format(result[0]))
Результат будет 0.9983763058 — т.е. почти 100% вероятность того, что человек будет диабетиком, если он не курит, страдает ожирением и не занимается спортом.
Полный код доступен на GitHub. Там же можно посмотреть пример реализации на TensorFlow.