Архитектуры нейронных сетей неуклонно развиваются по размеру и сложности, например. PALM от Google или известные языковые модели GPT-2 и GPT-3.
В этой статье я хочу сделать шаг назад, к основным понятиям нейронных сетей. Мы реализуем нейронную сеть в нескольких строках кода Python без каких-либо зависимостей, но Numpy для предсказания знаменитого набора данных Iris.
Вам не нужны какие-либо базовые знания, кроме некоторых основ исчисления и матричных вычислений. .
Выше вы можете увидеть визуализацию структуры сети. У нас есть входной слой i размера 4 для входных данных Iris, скрытый слой j с сигмовидной активацией с размером 10 и выходным слоем k с размером 3, чтобы предсказать 3 класса цветов. Всего получается 70 обучаемых параметров (4*10 + 3*10 = 70; без параметров смещения).
Итак, давайте начнем!
Сначала мы инициализируем веса сети.
Итак, подведем итог: нам нужны две матрицы, которые содержат обучаемые веса (параметры) сети. Wij с формой (4,10) и Wjk с формой (10,3). Мы инициализируем эти весовые матрицы случайными числами от -1 до 1:
self.w1 = np.random.uniform(-1, 1, size=(4, 10)) self.w2 = np.random.uniform(-1, 1, size=(10, 3))
Здесь у вас есть иллюстрация нашей функции активации, сигмовидной функции. Он отображает произвольный ввод x в значение между [0,1].
Поместите в функции python сигмовидную функцию и производную от сигмовидной функции, которые определяются как:
def sigm(x): return 1 / (1 + np.exp(-x)) def d_sigm(x): return sigm(x) *(1-sigm(x))
Переход вперед:
Прямой проход состоит из 3 шагов:
- Матричное умножение входных параметров Iris x и весовой матрицы Wij
- Расчет сигмовидной активации исхода 1.
- Умножение матриц с результатом 2. и Wjk
def forward(self, x): s_j = np.dot(x, self.w1) a_j = sigm(s_j) s_k = np.dot(a_j, self.w2) y_hat = s_k return y_hat, s_j, a_j
Обратите внимание, что мы также сохраняем предварительные активации s_j и активации a_j, потому что они понадобятся нам позже при обратном проходе.
Обратный проход (обратное распространение):
Здесь вы можете ознакомиться с обозначениями и важными формулами, которые мы используем для выполнения обратного прохода:
Важную роль в обратном пасе играют дельты. Вы можете думать о дельтах как об ошибке предсказания сети, которая передается обратно через сеть слой за слоем для расчета адаптации весов, градиентов.
Градиенты говорят нам, в каком направлении и насколько веса должны быть адаптированы, чтобы свести эмпирическую ошибку сети к минимуму.
Теперь давайте выведем конкретный обратный проход для нашей Iris Net.
Сначала мы вычисляем дельты для выходного слоя k и скрытого слоя j.
Затем мы вычисляем градиенты матриц W_kj и W_ij.
Обратите внимание, что у нас есть функции активации только в скрытом слое, у нас нет активаций в выходном слое, что означает, что термин f’(s_k) = 1, и его можно исключить из уравнения в строке 3.
Чтобы поместить это в код:
def backward(self,x): y_hat, s_j, a_j = self.forward(x) loss = y - y_hat d_k = loss d_j = self.w2.dot(d_k.T).T * d_sigm(s_j) grads_w2 = np.dot(a_j.T, d_k) grads_w1 = np.dot(x.T,d_j) self.w1 += grads_w1 * self.lr self.w2 += grads_w2 * self.lr return y_hat, np.mean(loss)
После вычисления градиентов мы обновляем веса нашей сети, умножая градиенты на нашу скорость обучения lr и добавляя результат к нашим матрицам:
self.w1 += grads_w1 * self.lr self.w2 += grads_w2 * self.lr
Для процесса обучения мы запускаем обратную функцию до тех пор, пока не достигнем сходимости, в нашем случае мы запускаем ее до тех пор, пока не достигнем точности более 97% в наборе данных Iris, правильно классифицировав более 146 из 150 образцов.
nn = NN() for i in range(10000): y_hat, loss = nn.backward(x) if np.count_nonzero((np.argmax(y_hat,axis=1)-iris.target)) < 4: print("Iteration", i) print("Accuracy > 97%") print("Label differences:") print(np.argmax(y_hat,axis=1) - iris.target) break
Чтобы собрать все это в 60 строк кода: