рекомендации

пятница, 7 августа 2020 г.

Создание нейронных сетей с нуля в Python и R


Введение

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

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

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

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

Простое объяснение работы нейронных сетей

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

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

Далее мы сравниваем результат с фактическим результатом. Задача состоит в том, чтобы сделать вывод нейронной сети максимально приближенным к фактическому (желаемому) выводу. Каждый из этих нейронов вносит некоторую ошибку в окончательный вывод. Как вы уменьшаете ошибку?

Мы стараемся свести к минимуму значение/вес нейронов, которые вносят больший вклад в ошибку, и это происходит во время путешествия обратно к нейронам нейронной сети и определения места ошибки. Этот процесс известен как «обратное распространение».

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

Вот и все - так работает нейронная сеть! Я знаю, что это очень простое представление, но оно поможет вам понять вещи простым способом.

Многослойный перцептрон и его основы

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

Перцептрон можно понимать как все, что принимает несколько входов и производит один вывод. Например, посмотрите на изображение ниже.


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

Ниже я рассмотрел три способа создания отношений ввода-вывода:

1. Путем непосредственного сложения входных данных и вычисления выходных данных на основе порогового значения. Например: возьмите x1 = 0, x2 = 1, x3 = 1 и установите порог = 0. Таким образом, если x1 + x2 + x3> 0, выход равен 1, иначе 0. Вы можете видеть, что в этом случае персептрон вычисляет выход как 1.
2. Далее, давайте добавим к входам веса. Веса важны для ввода. Например, вы назначаете w1 = 2, w2 = 3 и w3 = 4 для x1, x2 и x3 соответственно. Для вычисления выходных данных мы умножим входные данные на соответствующие веса и сравним с пороговым значением как w1*x1 + w2*x2 + w3*x3 > порог. Эти веса придают большее значение x3 по сравнению с x1 и x2.
3. Далее, давайте добавим смещение: у каждого перцептрона также есть смещение, которое можно представить как гибкость перцептрона. Это похоже на константу b линейной функции y = ax + b. Она позволяет нам перемещать линию вверх и вниз, чтобы прогноз лучше соответствовал  данным. Без b линия всегда проходит через начало координат (0, 0), и вы можете получить худшее соответствие. Например, перцептрон может иметь два входа, в этом случае требуется три веса. Один для каждого входа и один для смещения. Теперь линейное представление ввода будет выглядеть так: w1*x1 + w2*x2 + w3*x3 + 1*b.

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

Что такое функция активации?

Функция активации принимает в качестве аргумента сумму взвешенного ввода (w1*x1 + w2*x2 + w3*x3 + 1*b) и возвращает выход нейрона. 


В вышеприведенном уравнении мы представили 1 как x0 и b как w0.

Функция активации в основном используется для нелинейного преобразования, которое позволяет нам подбирать нелинейные гипотезы или оценивать сложные функции. Есть несколько функций активации, таких как: «Sigmoid», «Tanh», ReLu и многие другие.

Прямое распространение, обратное распространение и эпохи

До сих пор мы вычисляли выходные данные, и этот процесс известен как «прямое распространение». Но что, если предполагаемый выходной сигнал далек от фактического выходного значения (большая ошибка). В нейронной сети, при этом, мы обновляем смещения и веса на основе ошибки. Этот процесс обновления веса и смещения известен как «обратное распространение».

Алгоритмы обратного распространения (BP) работают, определяя потери (или ошибки) на выходе и затем распространяя их обратно в сеть. Веса обновляются, чтобы минимизировать ошибку, возникающую из-за каждого нейрона. Первым шагом в минимизации ошибки является определение градиента (производных) каждого узла в отношении окончательного вывод. Чтобы увидеть математический обзор обратного распространения, обратитесь к разделу ниже.

Этот раунд итерации прямого и обратного распространения известен как одна обучающая итерация, известная как «Эпоха».

Многослойный перцептрон (MLP)

Теперь давайте перейдем к следующей части. До сих пор мы видели только один слой, состоящий из 3 входных узлов, то есть x1, x2 и x3, и выходной слой, состоящий из одного нейрона. Но для практических целей однослойная сеть может сделать не очень много. MLP состоит из нескольких слоев, называемых скрытыми слоями, расположенных между входным слоем и выходным слоем, как показано ниже.



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

Давайте перейдем к следующей теме - алгоритму обучения нейронной сети (чтобы минимизировать ошибку). Здесь мы рассмотрим наиболее распространенный алгоритм обучения, известный как градиентный спуск.

Полный пакетный градиентный спуск и стохастический градиентный спуск (SGD)

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

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

Давайте разберемся с этим на простом примере набора данных из 10 точек данных с двумя весами w1 и w2.

Full Batch: Вы используете 10 точек данных (полные обучающие данные) и рассчитываете изменение w1 (Δw1) и изменение w2 (Δw2), и обновляете w1 и w2.

SGD: Вы используете 1-ю точку данных и рассчитываете изменение w1 (Δw1) и изменение w2 (Δw2) и обновляете w1 и w2. Далее, когда вы используете 2-ю точку данных, вы будете работать с обновленными весами

Для более подробного объяснения обоих методов вы можете взглянуть на эту статью.

Пошаговая методология построения нейронной сети



Давайте рассмотрим пошаговую методологию построения нейронной сети (MLP с одним скрытым уровнем, похожим на показанную выше архитектуру). На выходном уровне у нас есть только один нейрон, так как мы решаем задачу двоичной классификации (прогноз 0 или 1). Мы могли бы также иметь два нейрона для предсказания каждого из обоих классов.

Сначала посмотрите на основные шаги:

0.) Задаем ввод и вывод:
X в качестве входной матрицы
у в качестве выходной матрицы

1.) Мы инициализируем веса и смещения случайными значениями (это однократное инициирование. На следующей итерации мы будем использовать обновленные веса и смещения). Давайте зададим:
wh как матрица веса для скрытого слоя;
bh как матрица смещения для скрытого слоя;
wout как матрица весов для выходного слоя;
bout как матрица смещения для выходного слоя.

2.) Мы берем произведение матриц ввода и весов, назначенных ребрам между входным и скрытым слоями, затем прибавляем смещения нейронов скрытого слоя к соответствующим входам, это называется линейным преобразованием:

hidden_layer_input= matrix_dot_product(X,wh) + bh

3) Выполняем нелинейное преобразование, используя функцию активации (Sigmoid). Sigmoid вернет вывод как 1/(1 + exp (-x)).

hiddenlayer_activations = sigmoid(hidden_layer_input)

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

output_layer_input = matrix_dot_product (hiddenlayer_activations * wout ) + bout
output = sigmoid(output_layer_input)

Все вышеперечисленные шаги известны как «прямое распространение»

5.) Сравниваем прогноз с фактическим выходом и рассчитываем градиент ошибки (Actual - Predicted). Ошибка - средняя  среднеквадратичная = ((Y-t) ^ 2)/2

E = y – output 

6.) Вычисляем наклон/градиент скрытых и выходных нейронов слоя (чтобы вычислить наклон, мы вычисляем производные нелинейных активаций x на каждом слое для каждого нейрона). Градиент сигмоиды может быть представлен как x * (1 - x).

slope_output_layer = derivatives_sigmoid(output)
slope_hidden_layer = derivatives_sigmoid(hiddenlayer_activations)

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

d_output = E * slope_output_layer

8.) На этом этапе ошибка будет распространяться обратно в сеть, что означает ошибку на скрытом уровне. Для этого возьмем скалярное произведение delta выходного слоя с весовыми параметрами ребер между скрытым и выходным слоями (wout.T).

Error_at_hidden_layer = matrix_dot_product(d_output, wout.Transpose)

9.) Вычисляем коэффициент delta на скрытом слое, умножаем ошибку на скрытом слое на наклон активации скрытого слоя

d_hiddenlayer = Error_at_hidden_layer * slope_hidden_layer

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

wout = wout + matrix_dot_product(hiddenlayer_activations.Transpose, d_output)*learning_rate

wh =  wh + matrix_dot_product(X.Transpose,d_hiddenlayer)*learning_rate

learning_rate: скорость обновления весов контролируется параметром конфигурации, называемым скоростью обучения.

11.) Обновляем смещения на выходном и скрытом уровнях: смещения в сети могут обновляться на основе агрегированных ошибок в этом нейроне.

bias at output_layer =bias at output_layer + sum of delta of output_layer at row-wise * learning_rate

bias at hidden_layer =bias at hidden_layer + sum of delta of output_layer at row-wise * learning_rate

bh = bh + sum(d_hiddenlayer, axis=0) * learning_rate
bout = bout + sum(d_output, axis=0)*learning_rate

Шаги с 5 по 11 известны как «обратное распространение»

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

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

Визуализация пошаговой методологии построения нейронной сети

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

Замечания:

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

Шаг 0: чтение ввода и вывода


Шаг 1: Инициализация весов и смещений случайными значениями (есть специальные методы для инициализации весов и смещений, но на данный момент все инициализировать случайными значениями).


Шаг 2: Расчет ввода скрытого слоя:

hidden_layer_input= matrix_dot_product(X,wh) + bh


Шаг 3: Нелинейное преобразование на скрытом линейном входе

hiddenlayer_activations = sigmoid(hidden_layer_input)


Шаг 4. Линейное и нелинейное преобразование активации скрытого слоя на выходном слое.

output_layer_input = matrix_dot_product (hiddenlayer_activations * wout ) + bout
output = sigmoid(output_layer_input)


Шаг 5: Расчет градиента ошибки (E) на выходном слое.

E = y-output


Шаг 6: Вычисление наклона на выходном и скрытом слое.

Slope_output_layer= derivatives_sigmoid(output)
Slope_hidden_layer = derivatives_sigmoid(hiddenlayer_activations)


Шаг 7: Вычисление delta на выходном слое

d_output = E * slope_output_layer*lr


Шаг 8: Расчет ошибки на скрытом слое

Error_at_hidden_layer = matrix_dot_product(d_output, wout.Transpose)


Шаг 9: Расчет delta на скрытом слое

d_hiddenlayer = Error_at_hidden_layer * slope_hidden_layer


Шаг 10: Обновляем вес как на выходе, так и в скрытом слое

wout = wout + matrix_dot_product(hiddenlayer_activations.Transpose, d_output)*learning_rate
wh =  wh+ matrix_dot_product(X.Transpose,d_hiddenlayer)*learning_rate


Шаг 11: Обновляем смещения как в выходном, так и в скрытом слоях

bh = bh + sum(d_hiddenlayer, axis=0) * learning_rate
bout = bout + sum(d_output, axis=0)*learning_rate


Выше вы можете видеть, что все еще имеется большая ошибка, значение не близко к фактическому целевому значению, потому что мы завершили только одну обучающую итерацию. Если мы будем обучать модель несколько раз, то это будет очень близкий фактический результат. Я выполнил тысячи итераций, и мой результат близок к фактическим целевым значениям ([[0.98032096] [0.96845624] [0.04532167]]).

Реализация нейронной сети с помощью Numpy (Python)

import numpy as np

#Input array
X=np.array([[1,0,1,0],[1,0,1,1],[0,1,0,1]])

#Output
y=np.array([[1],[1],[0]])

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

#Derivative of Sigmoid Function
def derivatives_sigmoid(x):
return x * (1 - x)

#Variable initialization
epoch=5000 #Setting training iterations
lr=0.1 #Setting learning rate
inputlayer_neurons = X.shape[1] #number of features in data set
hiddenlayer_neurons = 3 #number of hidden layers neurons
output_neurons = 1 #number of neurons at output layer

#weight and bias initialization
wh=np.random.uniform(size=(inputlayer_neurons,hiddenlayer_neurons))
bh=np.random.uniform(size=(1,hiddenlayer_neurons))
wout=np.random.uniform(size=(hiddenlayer_neurons,output_neurons))
bout=np.random.uniform(size=(1,output_neurons))

for i in range(epoch):

#Forward Propogation
hidden_layer_input1=np.dot(X,wh)
hidden_layer_input=hidden_layer_input1 + bh
hiddenlayer_activations = sigmoid(hidden_layer_input)
output_layer_input1=np.dot(hiddenlayer_activations,wout)
output_layer_input= output_layer_input1+ bout
output = sigmoid(output_layer_input)

#Backpropagation
E = y-output
slope_output_layer = derivatives_sigmoid(output)
slope_hidden_layer = derivatives_sigmoid(hiddenlayer_activations)
d_output = E * slope_output_layer
Error_at_hidden_layer = d_output.dot(wout.T)
d_hiddenlayer = Error_at_hidden_layer * slope_hidden_layer
wout += hiddenlayer_activations.T.dot(d_output) *lr
bout += np.sum(d_output, axis=0,keepdims=True) *lr
wh += X.T.dot(d_hiddenlayer) *lr
bh += np.sum(d_hiddenlayer, axis=0,keepdims=True) *lr

print output

Реализация нейронной сети в R

# input matrix
X=matrix(c(1,0,1,0,1,0,1,1,0,1,0,1),nrow = 3, ncol=4,byrow = TRUE)

# output matrix
Y=matrix(c(1,1,0),byrow=FALSE)

#sigmoid function
sigmoid=function(x){
1/(1+exp(-x))
}

# derivative of sigmoid function
derivatives_sigmoid=function(x){
x*(1-x)
}

# variable initialization
epoch=5000
lr=0.1
inputlayer_neurons=ncol(X)
hiddenlayer_neurons=3
output_neurons=1

#weight and bias initialization
wh=matrix( rnorm(inputlayer_neurons*hiddenlayer_neurons,mean=0,sd=1), inputlayer_neurons, hiddenlayer_neurons)
bias_in=runif(hiddenlayer_neurons)
bias_in_temp=rep(bias_in, nrow(X))
bh=matrix(bias_in_temp, nrow = nrow(X), byrow = FALSE)
wout=matrix( rnorm(hiddenlayer_neurons*output_neurons,mean=0,sd=1), hiddenlayer_neurons, output_neurons)

bias_out=runif(output_neurons)
bias_out_temp=rep(bias_out,nrow(X))
bout=matrix(bias_out_temp,nrow = nrow(X),byrow = FALSE)
# forward propagation
for(i in 1:epoch){

hidden_layer_input1= X%*%wh
hidden_layer_input=hidden_layer_input1+bh
hidden_layer_activations=sigmoid(hidden_layer_input)
output_layer_input1=hidden_layer_activations%*%wout
output_layer_input=output_layer_input1+bout
output= sigmoid(output_layer_input)

# Back Propagation

E=Y-output
slope_output_layer=derivatives_sigmoid(output)
slope_hidden_layer=derivatives_sigmoid(hidden_layer_activations)
d_output=E*slope_output_layer
Error_at_hidden_layer=d_output%*%t(wout)
d_hiddenlayer=Error_at_hidden_layer*slope_hidden_layer
wout= wout + (t(hidden_layer_activations)%*%d_output)*lr
bout= bout+rowSums(d_output)*lr
wh = wh +(t(X)%*%d_hiddenlayer)*lr
bh = bh + rowSums(d_hiddenlayer)*lr

}
output

[Необязательно] Математическая основа алгоритма обратного распространения

Пусть Wi будет весами между входным слоем и скрытым слоем. Wh будет весами между скрытым слоем и выходным слоем.

Теперь h = σ(u) = σ(WiX), т.е. h является функцией от u, а u является функцией от Wi и X. здесь мы представляем нашу функцию как σ:

Y = σ(u ’) = σ (Whh), т. е. Y является функцией u’, а u ’является функцией Wh и h.

Мы будем постоянно ссылаться на приведенные выше уравнения для вычисления частных производных.

Мы в первую очередь заинтересованы в нахождении двух значений, ∂E/∂Wi и ∂E/∂Wh, т. е. изменение ошибки при изменении весов между входным и скрытым слоями и изменение ошибки при изменении весов между скрытым слоем и выходным слоем.

Но чтобы вычислить обе эти частные производные, нам нужно будет использовать цепное правило частичного дифференцирования, так как E является функцией Y, а Y является функцией u ', а u' является функцией Wi.

Давайте используем это свойство и вычислим градиенты.

∂E/∂Wh = (∂E/∂Y).( ∂Y/∂u’).( ∂u’/∂Wh), ……..(1)

Мы знаем, что E имеет вид E = (Y-t) 2/2.

поэтому, (∂E/∂Y)= (Y-t)

Теперь σ является сигмоидальной функцией и имеет интересную производную вида σ (1- σ). Я призываю читателей решить эту проблему самостоятельно для проверки.

So, (∂Y/∂u’)= ∂( σ(u’)/ ∂u’= σ(u’)(1- σ(u’)).

но, σ(u’)=Y, So,

(∂Y/∂u’)=Y(1-Y)

теперь, ( ∂u’/∂Wh)= ∂( Whh)/ ∂Wh = h

Заменив значения в уравнении (1) получим,

∂E/∂Wh = (Y-t). Y(1-Y).h

Итак, теперь мы вычислили градиент между скрытым слоем и выходным слоем. Настало время вычислить градиент между входным слоем и скрытым слоем.

∂E/∂Wi =(∂ E/∂ h). (∂h/∂u).( ∂u/∂Wi)

но, (∂ E/∂ h) = (∂E/∂Y).( ∂Y/∂u’).( ∂u’/∂h). Заменив это значение в приведенном выше уравнении, мы получим,

∂E/∂Wi =[(∂E/∂Y).( ∂Y/∂u’).( ∂u’/∂h)]. (∂h/∂u).( ∂u/∂Wi)……………(2)

Итак, каково было преимущество первого вычисления градиента между скрытым слоем и выходным слоем?

Как вы можете видеть в уравнении (2), мы уже вычислили ∂E/∂Y и ∂Y/∂u’, сэкономив пространство и время вычислений. Через некоторое время мы узнаем, почему этот алгоритм называется алгоритмом обратного распространения.

Вычислим неизвестные производные в уравнении (2).

∂u’/∂h = ∂(Whh)/ ∂h = Wh

∂h/∂u = ∂( σ(u)/ ∂u= σ(u)(1- σ(u))

но, σ(u)=h, So,

(∂Y/∂u)=h(1-h)

теперь, ∂u/∂Wi = ∂(WiX)/ ∂Wi = X

Заменив все эти значения в уравнении (2), получим,

∂E/∂Wi = [(Y-t). Y(1-Y).Wh].h(1-h).X

Итак, теперь, так как мы рассчитали оба градиента, веса могут быть обновлены как:

Wh = Wh + η . ∂E/∂Wh

Wi = Wi + η . ∂E/∂Wi

Где η - скорость обучения.

Итак, возвращаясь к вопросу: почему этот алгоритм называется алгоритмом обратного распространения?

Причина в том, что: если вы обратите внимание на окончательную форму ∂E/∂Wh и ∂E/∂Wi, вы увидите (Yt), т. е. выходную ошибку, с которой мы начали, а затем распространили ее обратно на входной слой для обновления веса.

Итак, где эта математика описывается в коде?

hiddenlayer_activations=h

E= Y-t

Slope_output_layer = Y(1-Y)

lr = η

slope_hidden_layer = h(1-h)

wout = Wh

Теперь вы можете легко связать код с математикой.

Заключение

Эта статья посвящена построению нейронной сети с нуля и пониманию ее основных концепций. Я надеюсь, что теперь вы понимаете, как работает нейронная сеть, например, как работает прямое и обратное распространение, алгоритмы оптимизации (полный пакетный и стохастический градиентный спуск), как обновлять веса и смещения, визуализация каждого шага в Excel и поверх этого код в python и R.

Комментариев нет:

Отправить комментарий