«Это 19-й день моего участия в ноябрьском испытании обновлений. Подробную информацию об этом событии см.:Вызов последнего обновления 2021 г.".
Многие книги о нейронных сетях вводят понятие различных графов и знакомят с принципами нейронных сетей с точки зрения вычислительных графов, вводя немало ветвей и операций. Но для простой нейронной сети с макроэкономической точки зрения можно более кратко понять ее конкретный рабочий процесс.
Предположим, у нас есть такая нейронная сеть только с одним скрытым слоем и одним выходным слоем. Количество весов и размеры их ввода и вывода в первую очередь не учитываются. Мы рассматриваем конкретный процесс прямого распространения и обратного распространения от этой нейронной сети.
Здесь линейный блок и функция активации разделены. Для простоты предполагается, что все функции активации используют сигмовидную функцию
Тогда в нем всего два элемента, линейные функции и сигмовидные функции. Давайте пока проигнорируем конкретные входные данные для функции.
для линейных функций, учитывая его градиент, для слоя, в котором он находится, в нейронной сети его следует рассматривать примерно какТернарную функцию , для других слоев следует рассматривать какУнарная функция . Таким образом, его градиенты соответственно,,Спросите о предвзятости. детали следующим образом:
- для х есть
- Для w имеем
- Для b тогда имеем
Давайте посмотрим на так называемую сигмовидную функцию активации, которая является функцией с одной переменной, поэтому ее частная производная выглядит следующим образом.
Теперь рассмотрим процесс прямого распространения.
Скрытый слой:
Первая активация:, вдля вышеперечисленного
выходной слой:, вдля вышеперечисленного
Вторая активация:, из которыхдля вышеперечисленного
Это написано по причине, которая будет указана позже. Теперь, когда у нас есть выходные данные нейронной сети, рассмотрите возможность расчета ее точности с помощью так называемой функции потерь квадратичной разности с добавленным здесь для удобства коэффициентом.
где t — значение тега
Предполагая, что есть ошибка, нам нужно найти способ максимально уменьшить ее значение.Принципом является метод градиентного спуска, который заключается в настройке переменных функции с целью уменьшения функции потерь.
Очень классическое недоразумение состоит в том, что слишком много внимания уделяется x или входной части, но на самом деле параметры в нейронной сети можно рассматривать как переменные в функции, а вход фактически неизменяем. То есть промежуточный корректировочный веси предвзятостьДля того, чтобы сделать потери как можно меньше.
Сначала мы рассмотрим настройку выходного слоя и рассмотрим настройку его весов.
Градиентный спуск говорит нам, что данные должны двигаться в направлении отрицательного градиента, скорость обучения lr здесь на самом деле не при чем, ее значение здесь нас не волнует, хотя оно довольно важно.
В общем, регулировка делается следующим образом.
Для этого требуется частная производная, и мы пытаемся найти эту частную производную, что включает в себя применение цепного правила.
Ранее мы рассматривали градиенты двух функций, но не рассматривали только функцию потерь. Теперь рассмотрим три частные производные вместе
Функция потерь принимает производную от J, а специально установленные ранее коэффициенты нужны для того, чтобы частная производная была более лаконичной.
Эта функция относится к более магической категории.Вообще говоря, сама производная не имеет ничего общего с функцией, но некоторые функции являются специальными, и ее производную можно узнать через функцию, которая чем-то похожа, хотя математически они не должны быть связаны, но на самом деле они связаны, что избавляет нас от некоторых вычислительных накладных расходов.
Можно обнаружить, что корректировка весов выходного слоя зависит от вычисления первых двух частных производных. Давайте посмотрим на регулировку смещения b. Формула выглядит следующим образом:
На самом деле, это в основном то же самое, движение в направлении отрицательного градиента.
Раскройте частные производные следующим образом:
Видно, что для параметров этого слоя все они зависят от части значения, предоставляемого последующим слоем (произведение двух частных производных).
Для более точного понимания глубины рассмотрим обновление весов скрытого слоя.
Он также следует принципу градиентного спуска, то есть для обновления необходимо вычислить градиент.
Обратите внимание, что w здесь и w выше не совпадают. Далее следует расширить частную производную
Конкретнее здесь писаться не будет, это тоже будет в виде умножения частных производных некоторых верхних слоев на частные производные его слоев по w. Смещение b аналогично. Поэтому повторения не будет.
Частные производные становятся все длиннее и длиннее, но, как и прежде, их обновления зависят от частных производных, вычисляемых последующими слоями. И среди этих частных производных значительную часть составляет повторный расчет. Для этих повторяющихся вычислений повторных вычислений можно избежать путем резервирования.
В этом и заключается суть алгоритма bp, он находит общие части между этими операциями и ожидает повторного использования этих общих частей, что чем-то похоже на идею динамического программирования.
Также стоит отметить, что при использовании обновления весов мы фактически используем x , обратите внимание, что это относится к выходным данным его предыдущего слоя. это означаетВвод данных в этот слой должен быть сохранен, для чего предварительно написаны причины для каждого слоя отдельно.
После завершения математического формального понимания мы, наконец, рассмотрим реализацию из кода.Мы проигнорируем конкретное использование данных и сосредоточимся только на процессе прямого и обратного распространения.
import numpy as np
Tensor = np.ndarray
X: Tensor
t: Tensor
w1: Tensor
w2: Tensor
b1: Tensor
b2: Tensor
lr = 1e-6
def sigmoid(x: Tensor):
return 1 / (1 + np.exp(-x))
def diff_sigmoid(x: Tensor): # 这里为了简化计算考虑,实际上并非是在 x 处的导数,而是在 x = sig(...) 时对应的导数
return x * (1 - x)
def loss(x: Tensor):
return 1 / 2 * (t - x)
def diff_loss(x: Tensor): # 误差在x的导数
return t - x # t 是标签
for epoch in range(10):
f = np.dot(X, w1) + b1 # 隐藏层
g = sigmoid(f) # 隐藏层经过激活函数
h = np.dot(g, w2) + b2 # 输出层
J = sigmoid(h) # 输出
# 考虑误差对输出的偏导
diff_L_to_j = diff_loss(J)
# 激活函数部分的导数, 这里依赖了 sigmoid 激活运算后的结果 J ,正常应该是依赖 h
diff_J_to_h = diff_sigmoid(J) # (1-sigmoid(h)) * sigmoid(h) 但是这样开销大
# 合并一部分,相当于传播梯度
diff_L_to_h = diff_L_to_j * diff_J_to_h
# 输出层对w的偏导,依赖输入的 g
diff_h_to_w = g
# 输出层对 b的偏导,实际上可以不用算
diff_h_to_b = 1
# 计算 h 对前一层的导数
diff_h_to_g = w2
# 计算激活函数 g 对前一层的导数 依赖本层输出 g
diff_g_to_f = diff_sigmoid(g)
# 合并一部分,相当于传播梯度
diff_L_to_f = diff_J_to_h * diff_h_to_g * diff_g_to_f
# 计算 f 对 w 的偏导, 依赖输入 X
diff_f_to_w = X
# 计算 f 对 b 的偏导, 实际可以不用计算
diff_f_to_b = 1
# 调整权值
w2 = w2 - lr * diff_L_to_h * diff_h_to_w
b2 = b2 - lr * diff_L_to_h # * diff_h_to_b2
w1 = w1 - lr * diff_L_to_f * diff_f_to_w
b1 = b1 - lr * diff_L_to_f # * diff_f_to_b
Программа здесь на самом деле не обучает, но делает некоторые компромиссы, не конкретизирует данные и не использует больше промежуточных переменных, поэтому при реальном использовании может потребоваться некоторая тонкая настройка.
Также из кода видно, что как прямое, так и обратное распространение требуют значительного объема памяти для сохранения промежуточных результатов. Это также является причиной того, что обучение нейронных сетей предъявляет определенные требования к памяти.