Искусственные нейронные сети похожи на волшебство для тех, кто не понимает, как они работают. зачем быть зрителем, если можно быть волшебником !. Я хотел бы обсудить, насколько легко начать работу со стандартной библиотекой машинного обучения (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. Ваше здоровье!.