DevGang
Авторизоваться

Построение глубокой нейронной сети с нуля с использованием Python

Эта статья посвящена созданию глубокой нейронной сети с нуля без использования таких библиотек, как Tensorflow, keras или Pytorch и т. д. Она состоит из двух разделов. В первой части мы увидим, что такое глубокая нейронная сеть, как она может учиться на данных, математику, стоящую за ней, а во второй части мы поговорим о ее создании с нуля с использованием Python.

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

Что такое глубокая нейронная сеть?

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

Биологический нейрон
Биологический нейрон

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

Искусственная нейронная сеть (ANN) смутно вдохновлена ​​биологической нейронной сетью. Это набор связанных искусственных нейронов. Как и биологический нейрон, искусственный нейрон также принимает входные данные от одного нейрона, выполняет некоторые вычисления и передает сигнал другому нейрону, который к нему подключен.

Глубокая нейронная сеть (DNN) - это искусственная нейронная сеть с несколькими уровнями между входным и выходным уровнями. Каждый нейрон в одном слое соединяется со всеми нейронами следующего слоя. Один или несколько слоев между входным и выходным слоями называются скрытыми слоями.

Каждое соединение, которое соединяет нейрон из одного слоя с нейроном в предыдущем слое, имеет так называемый вес w, который говорит о том, насколько чувствительна активация нашего текущего нейрона к активации нейрона в предыдущем слое. Каждый нейрон в данном слое имеет нечто, называемое смещением b. Если вы знакомы с линейной регрессией, член смещения действует как перехватчик «c» в y = mx + c. Если сумма (mx) не пересекает порог, но нейрон должен активироваться, смещение будет скорректировано, чтобы понизить порог этого нейрона, чтобы заставить его сработать.

Глубокая нейронная сеть
Глубокая нейронная сеть

Вся сеть выглядит очень сложной, верно! Но это не так. Думайте об этом как о гигантской функции y = f (x), где x - ваш вход, y - выход. Затем внутри функции f (x) она вызывает цепочку функций, в которой вывод одной функции передается другой. Эти внутренние функции - не что иное, как скрытые слои.

Один искусственный нейрон
Один искусственный нейрон

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

Линейное преобразование входов
Линейное преобразование входов

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

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

Сигмоидная функция принимает взвешенную сумму и преобразует значение от 0 до 1. Она преобразует -infinity в 0 и + infinity в 1. Значение между 0 и 1 представляет силу активации конкретного нейрона.

Сигмовидная функция
Сигмовидная функция

Активацию нейрона на данном слое можно записать следующим образом:

Функция активации одиночного нейрона на слое L
Функция активации одиночного нейрона на слое L

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

Матричная форма активации на уровне L
Матричная форма активации на уровне L

Обучение глубокой нейронной сети

Глубокая нейронная сеть сама будет учиться на предоставленных данных и будет использоваться для прогнозирования невидимых данных. Но что мы подразумеваем под обучением на основе данных?

Как мы уже обсуждали, DNN имеет набор весов и смещений на каждом уровне. Активация нейрона зависит от соответствующих весов и смещений. Таким образом, обучение на основе данных означает определение наилучших весов и смещений сети. Но как нам найти веса и смещения?

Чтобы найти веса и смещения, глубокая нейронная сеть делает следующее:

  1. Присваивает весам и смещениям некоторые случайные значения
  2. Запускает обучающие данные (которые имеют входные и фактические выходы) в сети, используя эти случайно назначенные веса и смещения. Во время этого выходные данные функции активации в одном слое будут передаваться в качестве входных данных на следующий уровень, пока мы не получим выходные данные из выходного слоя. Этот процесс называется прямым распространением.
  3. Первоначальный вывод из сети всегда будет ужасным, поскольку мы использовали случайные веса и смещения. Мы вычисляем ошибку (разницу между предсказанием сети и фактическим выходом), используя какую-то функцию стоимости или ошибки. В этом посте мы собираемся использовать сумму квадратов ошибок.
Сумма квадратов ошибок
Сумма квадратов ошибок

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

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

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

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

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

Правило цепи
Правило цепи

Частные производные каждого компонента в приведенном выше уравнении равны

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

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

Примечание. Хотя L и L-1 представляют выходной слой и скрытый слой соответственно, я использовал суб-нотацию «o» для вывода и «h» для скрытого слоя, чтобы быть более четким.

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

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

Градиентный спуск

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

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

Градиентный спуск
Градиентный спуск

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

  1. Назначьте случайные значения для весов w и смещений b и постоянное значение для скорости обучения
  2. Обновите веса и смещения, используя градиент (мы рассчитали с использованием частных производных) и скорость обучения.

3. Повторяйте шаг 2, пока не найдем минимальное значение или не достигнем максимального количества итераций.

Резюме обучения

Подведем итог всему процессу обучения, написав псевдокод для сети, которая имеет 1-входной, 1-скрытый и 1-выходной уровни.

initialize_weights_and_biases():
  output_w = initialize_random_w
  output_b = initialize_random_b
  hidden_w = initialize_random_w
  hidden_b = initialize_random_b
train(x_train, y_train, no_of_iterations, learning_rate):
   # 1. initialize network weights and biases
   initialize_weights_and_biases()
   for iteration in range(no_of_iterations): #Run gradient descent algorithm no_of_iterations times
       #initialize delta of weights and biases
       wo_delta = initialize_random_w_delta
       bo_delta = initialize_random_b_delta
       wh_delta = initialize_random_w_delta
       wh_delta = initialize_random_b_delta
       for x, y in zip (x_train, x_train): #Iterate through each sample in the training data
           # 2.forward propagation
           z_h = hidden_w * x + hidden_b
           a_h = sigmoid(z_h )
           z_o = output_w * a+ output_b
           predicted = sigmoid(z_o)
           # 3.find the error
           error = (predicted - y)
           # 4.Back propagate the error
           delta = 2 error * sigmoid_prime(z_o)
           wo_delta+= delta * a_h
           bo_delta+= delta 
           wh_delta+= delta * output_w * sigmoid_prime(z_h) * x
           bh_delta+= delta * output_w * sigmoid_prime(z_h)
      
       # 5. after 1 pass of all the inputs, update the network weights
       output_w = output_w - learning_rate * wo_delta
       output_b = output_b - learning_rate * bo_delta
       hidden_w = hidden_w - learning_rate * wh_delta
       hidden_b = hidden_b - learning_rate * bh_delta

Прогноз

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

Создание сети для распознавания рукописных цифр

Хватит теории, давайте запачкаем руки, написав программу на Python для построения глубокой нейронной сети. Мы собираемся использовать набор данных mnist и построить сеть, распознающую рукописные цифры, программу hello world Deep Neural Network.

Данные mnist состоят из сканированных рукописных изображений размером 28 x 28 пикселей.

mnist данные
mnist данные

Мы снова рассмотрим создание сети с 1 входным слоем, 1 скрытым слоем и 1 выходным слоем.

Следующая программа представляет собой версию псевдокода для Python, о которой мы говорили выше. Единственная разница в том, что мы ввели пакетную обработку, потому что mnist data содержит 60000 строк данных. Загрузка всех 60000 строк в память для каждой итерации уничтожит память.

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

def sigmoid_prime(z):
    return sigmoid(z)*(1-sigmoid(z))

def vectorized_result(j):
    e = np.zeros((10, 1))
    e[j] = 1.0
    return e

class NeuralNetwork:
    
    def __init__(self, layers):
        self.h_biases = np.random.randn(layers[1],1)
        self.o_biases = np.random.randn(layers[2],1)
        
        self.h_weights = np.random.randn(layers[1],layers[0])
        self.o_weights = np.random.randn(layers[2],layers[1])
    
    def forward_propagation(self, x):
        a = sigmoid(np.dot(self.h_weights, x) + self.h_biases)
        
        output = sigmoid(np.dot(self.o_weights, a) + self.o_biases)
        
        return output
    
    def update_mini_batch(self, batch, l_rate):
        o_b = np.zeros(self.o_biases.shape)
        h_b = np.zeros(self.h_biases.shape)
        
        o_w = np.zeros(self.o_weights.shape)
        h_w = np.zeros(self.h_weights.shape)
        
        for x, y in batch:
            o_del_b, h_del_b, o_del_w, h_del_w = self.backprop(x,y)
            
            o_b = o_b + o_del_b
            h_b = h_b + h_del_b
            o_w = o_w + o_del_w
            h_w = h_w + h_del_w
            
        self.o_weights = self.o_weights - (l_rate/len(batch))*o_w
        self.h_weights = self.h_weights - (l_rate/len(batch))*h_w
        self.o_biases = self.o_biases - (l_rate/len(batch))*o_b
        self.h_biases = self.h_biases - (l_rate/len(batch))*h_b
    
    def backprop(self, x, y):
        z_h = np.dot(self.h_weights, x) + self.h_biases
        a_h = sigmoid(z_h)
        
        z_o = np.dot(self.o_weights, a_h) + self.o_biases
        predicted = sigmoid(z_o)
        
        delta = (predicted - y) * sigmoid_prime(z_o)
        
        o_del_b = delta
        o_del_w = np.dot(delta, a_h.transpose())
        
        delta = np.dot(self.o_weights.transpose(), delta) * sigmoid_prime(z_h)
        
        h_del_b = delta
        h_del_w = np.dot(delta, x.transpose())
        
        return (o_del_b, h_del_b, o_del_w, h_del_w)
        
    def fit(self, train_data, epochs, mini_batch_size, learning_rate):
        n = len(train_data)
        for i in range(epochs):
            random.shuffle(train_data)
            batches = [train_data[j:j+mini_batch_size] for j in range(0,n, mini_batch_size)]
            for batch in batches:
                self.update_mini_batch(batch, learning_rate)
            print("epoch {} completed".format(i))
    
    def accuracy(self, test_data):
        test_results = [(np.argmax(self.forward_propagation(x)), y)
                        for (x, y) in test_data]
        return sum(int(x == y) for (x, y) in test_results)

__init__ инициализирует веса и смещения случайным образом для выходных и скрытых слоев.

forward_propagation выполняет прямое распространение для данного ввода

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

   o_del_b, h_del_b, o_del_w, h_del_w = self.backprop(x,y)
            
            o_b = o_b + o_del_b
            h_b = h_b + h_del_b
            o_w = o_w + o_del_w
            h_w = h_w + h_del_w

После каждого пакетного запуска он будет обновлять веса и смещения сети.

 self.o_weights = self.o_weights — (l_rate/len(batch))*o_w
 self.h_weights = self.h_weights — (l_rate/len(batch))*h_w
 self.o_biases = self.o_biases — (l_rate/len(batch))*o_b
 self.h_biases = self.h_biases — (l_rate/len(batch))*h_b

backprop распространяет градиент ошибки обратно на все слои, кроме входного. Это сердце нейронной сети. Как мы обсуждали ранее, мы будем вычислять частные производные функции ошибок по весам и смещениям на каждом уровне. В коде мы использовали метод .transpose (), чтобы он соответствовал правилу умножения матриц ( AXB возможен, только если A является матрицей mXn, а B - матрицей nXp. Матрица результата будет mXp).

delta = (predicted - y) * sigmoid_prime(z_o)
        
        o_del_b = delta
        o_del_w = np.dot(delta, a_h.transpose())
        
        delta = np.dot(self.o_weights.transpose(), delta) * sigmoid_prime(z_h)
        
        h_del_b = delta
        h_del_w = np.dot(delta, x.transpose())

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

Чтобы прочитать данные mnist, мы собираемся использовать fetch_openml из пакета sklearn.datasets. Мы будем использовать sklearn, чтобы разделить данные для обучения и тестирования.

X, y = fetch_openml('mnist_784', return_X_y=True)
y = y.astype(int)
X = (X/255).astype('float32')
X = [np.reshape(x, (784, 1)) for x in X]
X_train, X_test, y_train, y_test = train_test_split(X,y, test_size=0.2, random_state=7)
y_train = [vectorized_result(i) for i in y_train]

Данные mnist содержат оцифрованные изображения рукописных цифр, поэтому они будут иметь значения от 0 до 255. Чтобы нормализовать данные, разделите входные данные на 255, чтобы распределение изображений было между 0 и 1.

X = (X/255).astype('float32')

Поскольку каждое изображение имеет размер 28 x 28 пикселей и глубокая нейронная сеть ожидает ввода в векторном формате, ввод преобразуется в форму (784,1), потому что 28 * 28 = 784.

X = [np.reshape(x, (784, 1)) for x in X]

Сеть, которую мы собираемся построить, имеет 10 нейронов в выходном слое, поскольку нам нужно идентифицировать цифры от 0 до 9. Если сеть идентифицирует данную цифру как 3, то выходной нейрон, предназначенный для 3, будет иметь значение 1 а все остальные нейроны будут иметь значение 0.

[0, 0, 0, 1, 0, 0, 0, 0, 0, 0]

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

def vectorized_result(j):
    e = np.zeros((10, 1))
    e[j] = 1.0
    return e

y_train = [vectorized_result(i) for i in y_train]

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

network = NeuralNetwork([784, 100, 10])
train_data = list(zip(X_train, y_train))
network.fit(train_data, 30, 10, 3.0)

Здесь у нас есть сеть с входным слоем из 784 нейронов, скрытым слоем из 100 нейронов (почему 100 нейронов? Это выбор, мы можем использовать любое количество нейронов и посмотреть, как ведет себя сеть) и выходной слой из 10 нейронов.

network.fit(train_data, 30, 10, 3.0)

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

Точность

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

Для построенной нами сети точность тестирования составляет 96,59%, что очень хорошо.

Полная программа доступна в моем репозитории git

Источник:

#Python
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

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

Попробовать

В подарок 100$ на счет при регистрации

Получить