Рубрики
Без рубрики

Создание нейронной сети с нуля в Python: Добавление скрытых слоев

Автор оригинала: Usman Malik.

Создание нейронной сети с нуля в Python: Добавление скрытых слоев

Это вторая статья из серии статей на тему “Создание нейронной сети с нуля в Python”.

  • Создание нейронной сети с нуля в Python
  • Создание нейронной сети с нуля в Python: Добавление скрытых слоев
  • Создание нейронной сети с нуля в Python: Многоклассовая классификация

Если вы абсолютно новичок в нейронных сетях, вам следует сначала прочитать Часть 1 этой серии (ссылка выше). Как только вы освоитесь с понятиями, описанными в этой статье, вы можете вернуться и продолжить эту статью.

Вступление

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

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

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

Набор данных

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

К счастью, библиотека Python Scikit Learn поставляется с различными инструментами, которые можно использовать для автоматического создания различных типов наборов данных.

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

from sklearn import datasets

np.random.seed(0)
feature_set, labels = datasets.make_moons(100, noise=0.10)
plt.figure(figsize=(10,7))
plt.scatter(feature_set[:,0], feature_set[:,1], c=labels, cmap=plt.cm.winter)

В приведенном выше скрипте мы импортируем класс datasets из библиотеки sklearn . Чтобы создать нелинейный набор данных из 100 точек данных, мы используем метод make_moons и передаем ему 100 в качестве первого параметра. Метод возвращает набор данных, который при построении графика содержит два чередующихся полукруга, как показано на рисунке ниже:

Набор данных лун

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

Давайте проверим эту концепцию. Для этого мы используем простой персептрон с одним входным и одним выходным слоями (тот, который мы создали в предыдущей статье) и попытаемся классифицировать наш набор данных “луны”. Выполните следующий сценарий:

from sklearn import datasets
import numpy as np
import matplotlib.pyplot as plt

np.random.seed(0)
feature_set, labels = datasets.make_moons(100, noise=0.10)
plt.figure(figsize=(10,7))
plt.scatter(feature_set[:,0], feature_set[:,1], c=labels, cmap=plt.cm.winter)

labels = labels.reshape(100, 1)

def sigmoid(x):
    return 1/(1+np.exp(-x))

def sigmoid_der(x):
    return sigmoid(x) *(1-sigmoid (x))

np.random.seed(42)
weights = np.random.rand(2, 1) 
lr = 0.5
bias = np.random.rand(1)

for epoch in range(200000):
    inputs = feature_set

    # feedforward step 1
    XW = np.dot(feature_set,weights) + bias

    # feedforward step 2
    z = sigmoid(XW)

    # backpropagation step 1
    error_out = ((1 / 2) * (np.power((z - labels), 2)))
    print(error_out.sum())

    error = z - labels

    # backpropagation step 2
    dcost_dpred = error
    dpred_dz = sigmoid_der(z) 

    z_delta = dcost_dpred * dpred_dz

    inputs = feature_set.T
    weights -= lr * np.dot(inputs, z_delta)

    for num in z_delta:
        bias -= lr * num

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

Нейронные сети с Одним Скрытым слоем

В этом разделе мы создадим нейронную сеть с одним входным слоем, одним скрытым слоем и одним выходным слоем. Архитектура нашей нейронной сети будет выглядеть так:

Нейронная сеть со скрытым слоем

На рисунке выше мы имеем нейронную сеть с 2 входами, одним скрытым слоем и одним выходным слоем. Скрытый слой имеет 4 узла. Выходной слой имеет 1 узел, так как мы решаем задачу двоичной классификации, где может быть только два возможных выхода. Эта архитектура нейронной сети способна находить нелинейные границы.

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

Это довольно просто, если нет никакого скрытого слоя, как мы видели в предыдущей статье.

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

В этой статье мы увидим, как выполнять шаги прямого и обратного распространения для нейронной сети, имеющей один или несколько скрытых слоев.

Подача Вперед

Для каждой записи у нас есть две функции “x1” и “x2”. Чтобы вычислить значения для каждого узла в скрытом слое, мы должны умножить входные данные на соответствующие веса узла, для которого мы вычисляем значение. Затем мы пропускаем точечный продукт через функцию активации, чтобы получить конечное значение.

Например, чтобы вычислить конечное значение для первого узла в скрытом слое, который обозначается “h1”, необходимо выполнить следующее вычисление:

$$ + $$ + x2 w2 $$

$$ $$ ah1 = \frac{\mathrm{1} }{\mathrm{1} + e^{-zh 1} } $$

Это результирующее значение для самого верхнего узла скрытого слоя. Таким же образом можно вычислить значения для 2-го, 3-го и 4-го узлов скрытого слоя.

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

Эта операция может быть математически выражена следующим уравнением:

$$ + ah2w10 + ah3w11 + ah4w12 $$

$$ a0 = \frac{\mathrm{1} }{\mathrm{1} + e^{-z0} } $$

Здесь “а0” – это конечный результат работы нашей нейронной сети. Помните, что функция активации, которую мы используем, – это сигмоидная функция, как и в предыдущей статье.

Примечание: Для простоты мы не добавляли термин смещения к каждому весу. Вы увидите, что нейронная сеть со скрытым слоем будет работать лучше, чем персептрон, даже без смещения.

Обратное распространение

Шаг подачи вперед относительно прямолинейен. Однако обратное распространение не так прямолинейно, как это было в части 1 этой серии.

В фазе обратного распространения мы сначала определим нашу функцию потерь. Мы будем использовать функцию среднеквадратичная ошибка стоимость. Его можно представить математически как:

$$ MSE = \frac{\mathrm{1} }{\mathrm{n}}} ^{n} (прогнозируемый – наблюдаемый)^{2} $$

Здесь n – число наблюдений.

Фаза 1

На первой фазе обратного распространения нам нужно обновить веса выходного слоя, то есть w9, w10, w11 и w12. Поэтому на данный момент просто подумайте, что наша нейронная сеть имеет следующую часть:

Фаза обратного распространения 1

Это похоже на восприятие, которое мы развили в предыдущей статье. Целью первой фазы обратного распространения является обновление весов w9, w10, w11 и w12 таким образом, чтобы конечная ошибка была сведена к минимуму. Это задача оптимизации, в которой мы должны найти минимумы функций для нашей функции затрат.

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

Подробности о том, как градиентная функция минимизирует стоимость, уже обсуждались в предыдущей статье. Здесь мы просто увидим математические операции, которые нам нужно выполнить.

Наша функция затрат такова:

$$ MSE = \frac{\mathrm{1} }{\mathrm{n}}}^{n}(прогнозируемый – наблюдаемый)^{2} $$

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

$$ cost = \frac{\mathrm{1} }{\mathrm{n}}}^{n}(ao – observed)^{2} $$

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

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

$$ \$$ \frac {d cost}{down} = \frac {d cost}{dao} *, \frac {dao}{dz} * \frac {dz}{down} …… (1) $$

Здесь “wo” относится к весам в выходном слое. Буква “д” в начале каждого термина обозначает производное.

Давайте найдем значение для каждого выражения в Уравнение 1 .

Здесь,

$$ \$$ \frac {d cost}{dao} = \frac {2}{n} * (ao – метки) $$

Здесь 2 и n постоянны. Если мы их проигнорируем, то получим следующее уравнение.

$$ \frac {dcost}{dao} = (ao – метки) …….. (5) $$

Далее, мы можем найти “дао” по отношению к “делать” следующим образом:

$$ \frac(zo) * (1-сигмовидная(zo)) …….. (6) $$

Наконец, нам нужно найти “дзо” по отношению к “два”. Производная – это просто входные данные, поступающие из скрытого слоя, как показано ниже:

$$ \frac $$

Здесь “ах” относится к 4 входам из скрытых слоев. Уравнение 1 можно использовать для поиска обновленных значений веса для весов выходного слоя. Чтобы найти новые значения веса, значения, возвращаемые уравнением 1 , можно просто умножить на скорость обучения и вычесть из текущих значений веса. Это прямолинейно, и мы уже делали это раньше.

Фаза 2

В предыдущем разделе мы видели, как мы можем найти обновленные значения весов выходного слоя, то есть w9, w10, w11 и 12. В этом разделе мы снова распространим нашу ошибку на предыдущий слой и найдем новые значения веса для весов скрытого слоя, то есть веса от w1 до w8.

Давайте вместе обозначим скрытые веса слоев как “wh”. Мы в основном должны дифференцировать функцию затрат по отношению к “wh”. Математически мы можем использовать цепное правило дифференцирования, чтобы представить его как:

$$ \$$ \frac {d cost}{dw} = \frac {d cost}{dah} *, \frac {dh}{dz} * \frac {dz}{dw} …… (2) $$

Вот опять сломаемся Уравнение 2 в отдельные члены.

Первый термин “стоимость” может быть дифференцирован по отношению к “dah”, используя цепное правило дифференциации следующим образом:

$$ \$$ \frac {d cost}{dah} = \frac {d cost}{dzo} *, \frac {dot}{dah} …… (3) $$

Давайте снова разберем Уравнение 3 на отдельные члены. Снова используя цепное правило, мы можем дифференцировать “стоимость” по отношению к “дзо” следующим образом:

$$ \$$ \frac {d cost}{dzo} = \frac {d core}{dao} *, \frac {dao}{dzo} …… (4) $$

Мы уже рассчитали значение core/dao в Уравнение 5 и dao/dz in Уравнение 6 .

Теперь нам нужно найти ду/дах из Уравнение 3 . Если мы посмотрим на zo, то он имеет следующее значение:

$$ + a02w10 + a03w11 + a04w12 $$

Если мы дифференцируем его по отношению ко всем входам из скрытого слоя, обозначаемого “ao”, то у нас остаются все веса из выходного слоя, обозначаемого “wo”. Следовательно,

$$ \frac …… (7) $$

Теперь мы можем найти значение cost/dah, заменив значения из Уравнения 7 и 4 в Уравнение 3 .

Возвращаясь к Уравнение 2 , нам еще предстоит найти dh/dz и dz/dw.

Первый член dah/dzh может быть вычислен как:

$$ \frac(zh) * (1-сигмовидная(zh)) …….. (8) $$

И, наконец, dzh/dwh-это просто входные значения:

$$ \особенности frac …….. (9) $$

Если мы заменим значения из Уравнения 3 , 8 и 9 в Уравнение 3 , мы можем получить обновленную матрицу для весов скрытого слоя. Чтобы найти новые значения веса для весов скрытого слоя “wh”, значения, возвращаемые уравнением 2 , можно просто умножить на скорость обучения и вычесть из текущих значений веса. И это почти все.

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

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

Код для нейронных сетей с одним скрытым слоем

Теперь давайте реализуем нейронную сеть, которую мы только что обсуждали в Python, с нуля. Вы ясно увидите соответствие между фрагментами кода и теорией, которую мы обсуждали в предыдущем разделе. Мы снова попытаемся классифицировать нелинейные данные, которые мы создали в разделе Dataset этой статьи. Взгляните на следующий сценарий.

# -*- coding: utf-8 -*-
"""
Created on Tue Sep 25 13:46:08 2018

@author: usman
"""

from sklearn import datasets
import numpy as np
import matplotlib.pyplot as plt

np.random.seed(0)
feature_set, labels = datasets.make_moons(100, noise=0.10)
plt.figure(figsize=(10,7))
plt.scatter(feature_set[:,0], feature_set[:,1], c=labels, cmap=plt.cm.winter)

labels = labels.reshape(100, 1)

def sigmoid(x):
    return 1/(1+np.exp(-x))

def sigmoid_der(x):
    return sigmoid(x) *(1-sigmoid (x))

wh = np.random.rand(len(feature_set[0]),4) 
wo = np.random.rand(4, 1)
lr = 0.5

for epoch in range(200000):
    # feedforward
    zh = np.dot(feature_set, wh)
    ah = sigmoid(zh)

    zo = np.dot(ah, wo)
    ao = sigmoid(zo)

    # Phase1 =======================

    error_out = ((1 / 2) * (np.power((ao - labels), 2)))
    print(error_out.sum())

    dcost_dao = ao - labels
    dao_dzo = sigmoid_der(zo) 
    dzo_dwo = ah

    dcost_wo = np.dot(dzo_dwo.T, dcost_dao * dao_dzo)

    # Phase 2 =======================

    # dcost_w1 = dcost_dah * dah_dzh * dzh_dw1
    # dcost_dah = dcost_dzo * dzo_dah
    dcost_dzo = dcost_dao * dao_dzo
    dzo_dah = wo
    dcost_dah = np.dot(dcost_dzo , dzo_dah.T)
    dah_dzh = sigmoid_der(zh) 
    dzh_dwh = feature_set
    dcost_wh = np.dot(dzh_dwh.T, dah_dzh * dcost_dah)

    # Update Weights ================

    wh -= lr * dcost_wh
    wo -= lr * dcost_wo

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

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

zh = np.dot(feature_set, wh)
ah = sigmoid(zh)

zo = np.dot(ah, wo)
ao = sigmoid(zo)

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

error_out = ((1 / 2) * (np.power((ao - labels), 2)))
print(error_out.sum())

dcost_dao = ao - labels
dao_dzo = sigmoid_der(zo) 
dzo_dwo = ah

dcost_wo = np.dot(dzo_dwo.T, dcost_dao * dao_dzo)

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

dcost_dzo = dcost_dao * dao_dzo
dzo_dah = wo
dcost_dah = np.dot(dcost_dzo , dzo_dah.T)
dah_dzh = sigmoid_der(zh) 
dzh_dwh = feature_set
dcost_wh = np.dot( dzh_dwh.T, dah_dzh * dcost_dah)

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

wh -= lr * dcost_wh
wo -= lr * dcost_wo

При выполнении приведенного выше скрипта вы увидите минимальное значение среднеквадратичной ошибки 1,50, что меньше, чем наша предыдущая среднеквадратичная ошибка 4,17, полученная с помощью персептрона. Это показывает, что нейронная сеть со скрытыми слоями лучше работает в случае нелинейно разделимых данных.

Вывод

В этой статье мы увидели, как мы можем создать нейронную сеть с 1 скрытым слоем, с нуля в Python. Мы видели, как наша нейронная сеть превосходит нейронную сеть без скрытых слоев для бинарной классификации нелинейных данных.

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