Искусственные нейронные сети похожи на волшебство для тех, кто не понимает, как они работают. зачем быть зрителем, если можно быть волшебником !. Я хотел бы обсудить, насколько легко начать работу со стандартной библиотекой машинного обучения (keras) и насколько интересным это может быть, если мы попытаемся реализовать нейронную сеть с нуля (numpy) вместе с «небольшой» математикой. .

Какие новости!

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

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

Подготовка данных

Давайте использовать набор данных цифр из sklearn. Это мультиклассовая классификация.

import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
dig = load_digits()
plt.gray()
plt.matshow(dig.images[25])

Похоже на мини-версию mnist. Все должно быть хорошо. Этот набор данных содержит входы в data и выходы в target переменной. target значения варьируются от 0 до 9. Его необходимо преобразовать в одноразовое представление, где массив содержит все нули, кроме индекса значения как 1. т.е. значение 4 будет представлено как [0,0,0,0,1,0,0, 0,0]. pandas делает это за одну функцию. Такие проблемы многоклассовой классификации должны быть переведены в одноразовые представления для обучения модели NN из-за природы функции ошибок.

onehot_target = pd.get_dummies(dig.target)

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

x_train, x_val, y_train, y_val = train_test_split(dig.data, onehot_target, test_size=0.1, random_state=20)
# shape of x_train :(1617, 64)
# shape of y_train :(1617, 10)

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

Архитектура искусственной нейронной сети:

Построим четырехслойную сеть (вход, 2-скрытые слои, выход).

Входной слой - 64 нейрона (входной массив изображений)
Скрытый слой 1 - 128 нейронов (произвольно)
Скрытый слой 2 - 128 нейронов (произвольно)
Выходной слой - 10 нейронов (выход горячий массив)

Керас - легкий путь:

Keras - фантастическая библиотека для начала. Быстро и просто.

from keras.layers import Dense
from keras.models import Sequential
from keras.optimizers import RMSprop, Adadelta, Adam
model = Sequential()
model.add(Dense(128, input_dim=x_train.shape[1], activation='sigmoid'))
model.add(Dense(128, activation='sigmoid'))
model.add(Dense(10, activation='softmax'))

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

Модель добавляет первый скрытый слой со 128 нейронами, где также input_dim указывает размер входного слоя. А потом второй скрытый слой с теми же 128 нейронами. Наконец, выходной слой с 10 нейронами.

model.summary()

Модель не является полной без указания функции стоимости и оптимизации градиентного спуска.

model.compile(optimizer=Adadelta(), loss='categorical_crossentropy', metrics=['categorical_accuracy'])
model.fit(x_train, y_train, epochs=50, batch_size=64)

Хорошо. Это самый простой способ сделать это. Собирая все вместе,

ANN с нуля - трудный путь

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

import numpy as np
class MyNN:
    def __init__(self, x, y):
        self.input = x
        self.output = y
        neurons = 128       # neurons for hidden layers
        self.lr = 0.5       # user defined learning rate
        ip_dim = x.shape[1] # input layer size 64
        op_dim = y.shape[1] # output layer size 10
        self.w1 = np.random.randn(ip_dim, neurons) # weights
        self.b1 = np.zeros((1, neurons))           # biases
        self.w2 = np.random.randn(neurons, neurons)
        self.b2 = np.zeros((1, neurons))
        self.w3 = np.random.randn(neurons, op_dim)
        self.b3 = np.zeros((1, op_dim))

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

Прямая связь

def sigmoid(s):
    return 1/(1 + np.exp(-s))
# for numerical stability, values are normalised
def softmax(s):
    exps = np.exp(s - np.max(s, axis=1, keepdims=True))
    return exps/np.sum(exps, axis=1, keepdims=True)
def feedforward(self):
    z1 = np.dot(self.x, self.w1) + self.b1
    self.a1 = sigmoid(z1)
    z2 = np.dot(self.a1, self.w2) + self.b2
    self.a2 = sigmoid(z2)
    z3 = np.dot(self.a2, self.w3) + self.b3
    self.a3 = softmax(z3)

Процесс прямой связи довольно прост. (input x weights) + bias вычисляет z и передается в слои, которые содержат определенные функции активации. Эти функции активации производят выход a. Выход текущего слоя будет входом для следующего слоя и так далее. Как вы могли видеть, первый и второй скрытые уровни содержат функцию сигмоида в качестве функции активации, а выходной слой имеет softmax в качестве функции активации. Конечный результат a3, созданный softmax, - это вывод нейронной сети.

Тип функции, применяемой к слою, имеет большое значение. Сигмоидальная функция сжимает вход до (0, 1), и Softmax делает то же самое. Но Softmax гарантирует, что сумма выходов равна 1. В случае нашего выхода мы хотели бы измерить, какова вероятность входа для каждого класса. например, если изображение имеет вероятность 0,9 быть цифрой 5, разумно иметь 0,1 вероятность, распределенную между другими классами, что выполняется с помощью softmax.

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

Хорошо. Задняя опора может показаться сложной, поскольку вы имеете дело с несколькими слоями, несколькими весами, функцией потерь, градиентами. Не волнуйтесь, мы попробуем это математически, интуитивно и с реализацией кода. По сути, back-prop вычисляет ошибку из вывода прямой связи и истинного значения. Эта ошибка распространяется обратно на все матрицы весов посредством вычисления градиентов в каждом слое, и эти веса обновляются. Звучит просто, правда. Эммм. Давайте посмотрим.

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

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

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

Цепочка - правило:

Давайте рассмотрим функцию затрат как c. Из предварительных данных мы знаем, что

z = xw + b                 -> z = function(w)
a = sig(z) or softmax(z)   -> a = function(z)
c = -(y*log(a3))           -> c = function(a)

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

Внешний слой:

dc     dc    da3   dz3
---  = --- . --- . ---
dw3    da3   dz3   dw3
z3 = a2w3 + b3
a3 = softmax(z3)
dz3/dw3 = a2
da3/dz3 = softmax derivative(z3)
dc/da3  = cost function derivative(a3) = -y/a3

Удивительно, но поскольку кросс-энтропия часто используется с функцией активации softmax, нам на самом деле не нужно вычислять обе эти производные. Потому что некоторые части этих производных взаимно компенсируют друг друга, как ясно объяснено здесь. Таким образом, predicted value — real value является результатом их продукта.

Let, a3_delta be the product of these terms as it will be needed in the upcoming chain rules.
           dc    da3
a3_delta = --- . --- 
           da3   dz3
Thus, a3_delta = a3-y (the error to be propagated)
dc     
---  = (a3 - y) . a2
dw3    
w3 = w3 - dc/dw3
For changes in biases,
dc     dc    da3   dz3
---  = --- . --- . ---
db3    da3   dz3   db3
dz3/db3 = 1. Rest is already calculated
b3 = b3 - dc/db3 => b3 = b3 - a3_delta

Скрытые слои:

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

z2 = a1w2 + b2
a2 = sigmoid(z2)
dc     dc    da2   dz2
---  = --- . --- . ---
dw2    da2   dz2   dw2
dz2/dw2 = a1
da2/dz2 = sigmoid_derv(z2)
dc     dc    da3   dz3
---  = --- . --- . --- => dc/da2 = a3_delta.w3
da2    da3   dz3   da2
w2 = w2 - dc/dw2
and set a2_delta = dc/da2 . da2/dz2
dc     dc    da2   dz2
---  = --- . --- . ---
db2    da2   dz2   db2
dz2/db2 = 1
b2 = b2 - dc/db2 => b2 = b2 - a2_delta

Аналогично для производной функции стоимости w.r.t w1

z1 = x.w1 + b1
a1 = sigmoid(z1)
c  = a1.w2 + b2
dc     dc    da1   dz1
---  = --- . --- . ---
dw1    da1   dz1   dw1
dz1/dw1 = x
da1/dz1 = sigmoid_derv(z1)
dc     dc    da2   dz2
---  = --- . --- . --- => dc/da1 = a2_delta.w2
da1    da2   dz2   da1
w1 = w1 - dc/dw1
and set a1_delta = dc/da1 . da1/dz1
dc     dc    da1   dz1
---  = --- . --- . ---
db1    da1   dz1   db1
dz1/db1 = 1
b1 = b1 - dc/db1 => b1 = b1 - a1_delta

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

Feed forward equations:
z1 = x.w1+b1
a1 = sigmoid(z1)
z2 = a1.w2+b2
a2 = sigmoid(z2)
z3 = a2.w3+b3
a3 = softmax(z3)
Back propagation equations:
There is no z3_delta and softmax_derv(a3), as explained before.
a3_delta = a3-y    
z2_delta = a3_delta.w3.T
a2_delta = z2_delta.sigmoid_derv(a2)
z1_delta = a2_delta.w2.T
a1_delta = z1_delta.sigmoid_derv(a1)

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

def sigmoid_derv(x):
    return x * (1 - x)

Если вход z и не прошел активацию softmax, тогда

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

При вычислении цепного правила используется сигмовидная производная z. Но в реализации используется сигмовидная производная от a. Итак, используется первое уравнение. Надеюсь, это достаточно ясно. Теперь с реализацией numpy.

def sigmoid_derv(s):
    return s * (1 - s)
def cross_entropy(pred, real):
    n_samples = real.shape[0]
    res = pred - real
    return res/n_samples
def error(pred, real):
    n_samples = real.shape[0]
    logp = - np.log(pred[np.arange(n_samples), real.argmax(axis=1)])
    loss = np.sum(logp)/n_samples
    return loss
def backprop(self):
    loss = error(self.a3, self.y)
    print('Error :', loss)
    a3_delta = cross_entropy(self.a3, self.y) # w3
    z2_delta = np.dot(a3_delta, self.w3.T)
    a2_delta = z2_delta * sigmoid_derv(self.a2) # w2
    z1_delta = np.dot(a2_delta, self.w2.T)
    a1_delta = z1_delta * sigmoid_derv(self.a1) # w1
    self.w3 -= self.lr * np.dot(self.a2.T, a3_delta)
    self.b3 -= self.lr * np.sum(a3_delta, axis=0, keepdims=True)
    self.w2 -= self.lr * np.dot(self.a1.T, a2_delta)
    self.b2 -= self.lr * np.sum(a2_delta, axis=0)
    self.w1 -= self.lr * np.dot(self.x.T, a1_delta)
    self.b1 -= self.lr * np.sum(a1_delta, axis=0)

Этап прогноза:

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

def predict(self, data):
    self.x = data
    self.feedforward()
    return self.a3.argmax()

Собирая все вместе,

Последние мысли

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

Спасибо, что прочитали пост. Хлопайте в ладоши, если вам это понравилось. Если вы заметили ошибку или сомневаетесь, дайте мне знать в комментариях.

Не стесняйтесь обращаться ко мне через Github, Twitter и Linkedin. Ваше здоровье!.