Понимание математики!

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

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

Интуиция

Допустим, вы изучаете новый вид спорта. Во-первых, вы пройдете тренировку для новичков и ознакомитесь с основными правилами этого вида спорта. Точно так же нейронная сеть также обучается на основе предоставленных ей основных параметров и набора данных. Это называется «Пересылка вперед».

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

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

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

Сеть

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

Архитектура

Рассмотрим приведенную ниже нейронную сеть. Он содержит входной слой, 1 скрытый слой и выходной слой. Входной слой содержит 3 нейрона, скрытый слой содержит 4, а конечный выходной слой содержит один нейрон.

1. Нейроны

Каждый нейрон представляет собой математическую функцию, которая принимает входные данные и производит выходные данные, которые передаются другим нейронам следующего слоя. Результат создается на основе определенных параметров, называемых весами и смещениями. Входные данные для каждого нейрона представляют собой взвешенную сумму точек данных и смещения 𝑊𝑥+𝑏. Давайте определим класс с этими основными компонентами.

2. Веса и смещения

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

class NeuralNetwork:
    def __init__(self, x, y):
        self.input      = x
        self.weights1   = np.random.rand(self.input.shape[1],4) 
        self.weights2   = np.random.rand(4,1)                 
        self.y          = y
        self.output     = np.zeros(y.shape)

3. Функция активации

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

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

4. Упреждающая связь

Если на входе 𝑥, то на выходе ŷ простой двухслойной нейронной сети, подобной приведенной выше, для каждого из наших двух слоев:

Здесь 𝑦1 — это выходные данные первого уровня, которые передаются в качестве входных данных второму слою. W1 и W2 — веса, а b1 и b2 — байзы. Объединение двух приведенных выше уравнений:

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

  • Входной слой: (3,1) — XOR входы
  • Веса 1: (3,4) — лежат между входным слоем с 3 нейронами и скрытым слоем с 4 нейронами
  • Веса 2: (4,1) — соединяют из скрытого слоя с 4 нейронами в выходной слой с одним нейроном
  • Bais 1: (4,1) — это смещение применяется к скрытому слою с 4 нейронами.
  • Смещение 2: (1,1) — применяется к выходному слою с одним нейроном
def feedforward(self):
        self.layer1 = sigmoid(np.dot(self.input, self.weights1))
        self.output = sigmoid(np.dot(self.layer1, self.weights2))

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

5. Функция потерь

Функция потерь, также называемая функцией стоимости, представляет собой математическую функцию, которая измеряет разницу между прогнозируемым выходом нейронной сети и фактическим выходом, также известным как основная правда. Он оценивает производительность модели и указывает корректировки, которые необходимо внести в веса и смещения для повышения точности. Цель состоит в том, чтобы найти оптимальный набор гирь и байзов, чтобы минимизировать функцию потерь. Выбор функции потерь зависит от категории задачи. Здесь мы будем использовать простую ошибку суммы квадратов в качестве нашей функции потерь: ошибка суммы квадратов — это просто сумма разницы между каждым предсказанным наблюдением 𝑦̂ и фактическим наблюдением 𝑦 . Разница возводится в квадрат, поэтому мы измеряем абсолютное значение разницы:

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

Вот и наступил важный шаг! Теперь, когда у нас есть мера качества прогнозов, которая является функцией потерь, мы обновим веса и смещения с целью минимизировать ошибку. Для этого возьмем производную (градиентный спуск) функции потерь по весам и байзам. Градиентный спуск дает наклон функции и говорит нам, насколько мы далеки от минимумов. Поэтому мы будем обновлять наши веса (и смещения) следующим образом:

где ∂ — частная производная и L = Loss (𝑦,𝑦̂)

7. Математика!

Обновление весов.Поскольку у нас есть два набора весов, нам нужны два вывода Loss(𝑦,𝑦̂):

Функция потерь зависит от y и 𝑦̂, а не от весов. Но y и 𝑦̂ сами по себе являются функциями весов. Таким образом, мы можем переписать уравнения как:

Давайте сначала решим для общего термина,

Подставляя это и уравнения для 𝑧 и 𝑦̂, мы получаем:

Чтобы взять производные, мы применим цепное правило. Цепное правило формулируется следующим образом:

Итак, с заменой переменной 𝑊2→𝑞:

Таким образом, первое уравнение становится:

Аналогично, изменив переменную 𝑊1→𝑞:

И второе уравнение:

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

Мы можем переписать уравнения как:

Решение для отдельных условий:

Как рассчитали ранее, имеем:

Таким образом, первое уравнение становится:

И второе уравнение становится:

Ух! Это была сложная математика! Поздравляю! Мы закончили с трудной частью. Теперь давайте добавим функцию обратного распространения в наш код Python.

#function to calculate the deivative of sigmoid activation function
def sigmoid_derivative(x):
    return sigmoid(x) * (1.0 - sigmoid(x))

def backpropagation(self):
        # application of the chain rule to find derivative of the loss function with respect to weights2 and weights1
        sigmoid_derivative_1 = self.sigmoid_derivative(self.z1) #sigma'(W1 x + b1)
        sigmoid_derivative_2 = self.sigmoid_derivative(self.z2) #sigma'(W2 z + b2)
        d_weights2 = np.dot(self.layer1.T, 
                            (2*(self.y - self.output) * sigmoid_derivative_2))
        d_weights1 = np.dot(self.input.T,  
                            np.dot(2*(self.y - self.output) * sigmoid_derivative_2, self.weights2.T) * 
                            sigmoid_derivative_1)

        # update the weights
        self.weights1 += d_weights1
        self.weights2 += d_weights2
        
        d_bias2 = np.sum((2*(self.y - self.output) * sigmoid_derivative_2), axis=0)
        d_bias1 = np.sum((np.dot(2*(self.y - self.output) * sigmoid_derivative_2, self.weights2.T)), axis=0)

        # update the biases 
        self.bias1 += d_bias1
        self.bias2 += d_bias2

8. Обучение

Соберем все вместе в класс и обучим нашу нейросеть

import numpy as np
import matplotlib.pyplot as plt
class NeuralNetwork:
    def __init__(self, x, y):
        self.input      = x
        self.weights1   = np.random.rand(self.input.shape[1],4) 
        self.bias1      = np.random.rand(4) 
        self.weights2   = np.random.rand(4,1)
        self.bias2      = np.random.rand(1)                            
        self.y          = y
        self.output     = np.zeros(self.y.shape)

    def feedforward(self):
        self.z1     = np.dot(self.input, self.weights1) + self.bias1
        self.layer1 = self.sigmoid(self.z1)
        self.z2     = np.dot(self.layer1, self.weights2) + self.bias2
        self.output = self.sigmoid(self.z2)
        return self.calculate_loss()
        
    def reload(self, x):
        self.input = x
        
    def predict(self):
        return self.output
    
    def calculate_loss(self):
        return (self.y - self.output) ** 2
    
    def sigmoid(self,x):
        return 1.0/(1+ np.exp(-x))
    
    #function to calculate the deivative of sigmoid activation function
    def sigmoid_derivative(self,x):
        return self.sigmoid(x) * (1.0 - self.sigmoid(x))

    def backprop(self):
        # application of the chain rule to find derivative of the loss function with respect to weights2 and weights1
        sigmoid_derivative_1 = self.sigmoid_derivative(self.z1) #sigma'(W1 x + b1)
        sigmoid_derivative_2 = self.sigmoid_derivative(self.z2) #sigma'(W2 z + b2)
        d_weights2 = np.dot(self.layer1.T, 
                            (2*(self.y - self.output) * sigmoid_derivative_2))
        d_weights1 = np.dot(self.input.T,  
                            np.dot(2*(self.y - self.output) * sigmoid_derivative_2, self.weights2.T) * 
                            sigmoid_derivative_1)

        # update the weights with the derivative (slope) of the loss function
        self.weights1 += d_weights1
        self.weights2 += d_weights2
        
        d_bias2 = np.sum((2*(self.y - self.output) * sigmoid_derivative_2), axis=0)
        d_bias1 = np.sum((np.dot(2*(self.y - self.output) * sigmoid_derivative_2, self.weights2.T) * 
                            sigmoid_derivative_1), axis=0)

        # update the weights with the derivative (slope) of the loss function
        self.bias1 += d_bias1
        self.bias2 += d_bias2

Мы тренируемся на наборе данных XOR. Таким образом, наши X и y будут такими, как показано ниже:

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

Давайте тренироваться !!!

nn = NeuralNetwork(X,y)
for i in range(1500):
    nn.feedforward()
    nn.backprop()
print(nn.output)

Полученные результаты

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

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

Полный рабочий код вы можете найти здесь.

Спасибо профессору Дино за понимание и поддержку!