Все курсы > Оптимизация > Занятие 6
В рамках вводного курса мы начали изучать нейросети. Кроме того, нам знаком важный элемент алгоритма нейронной сети — умножение матриц.
На сегодняшнем занятии мы более подробно поговорим про то, почему нейросеть может оказаться более эффективным алгоритмом, чем рассмотренные ранее линейная и логистическая регрессия.
Кроме того, обладая знаниями о производной и градиенте, мы разберем до сих пор неизученный компонент, а именно процесс оптимизации, который в терминологии нейросетей называется обратным распространением, и построим несколько алгоритмов с нуля.
- Зачем нужна нейронная сеть
- Подготовка данных
- Нейросеть без смещения
- Архитектура сети
- Размерность матриц
- Код прямого распространения
- Обратное распространение
- Код обратного распространения
- Обучение модели
- Прогноз и оценка качества
- Инициализация весов
- Масштабирование целевой переменной
- Модель в Tensorflow и Keras
- Нейросеть со смещением
- Два скрытых слоя
- Многоклассовая классификация
- Подведем итог
Зачем нужна нейронная сеть
Нелинейная гипотеза
Мы уже знаем, что некоторые гипотезы нелинейны. В случае задачи классификации и алгоритма логистической регрессии это означает, что, как видно на графике ниже, два класса нельзя разделить прямой линией (еще говорят, что они линейно неразделимы).

Можно обучить полиномиальную логистическую регрессию, однако с ростом количества признаков и степени полинома итоговое количество признаков, а значит и «затратность» алгоритма с точки зрения вычислительных ресурсов будет расти.
Для полинома n-ой степени (с одним признаком!) формула выглядит следующим образом.
$$ y = \sum{}^n_{j=0} \theta_j x^j $$
Например, полином второй степени будет иметь три коэффициента.
$$ y = \theta_0 + \theta_{1}x + \theta_{2}x^2 $$
Полином второй степени с двумя признаками уже будет иметь шесть коэффициентов.
$$ y = \theta_{0} + \theta_{1}x_1 + \theta_{2}x_2 + \theta_{3} x_1^2 + \theta_{4} x_1x_2 + \theta_{5} x_2^2 $$
В целом, количество полиномиальных коэффициентов (N) можно рассчитать по формуле.
$$ N(n, d) = C(n+d, d), \text{где} $$
- n — количество линейных признаков
- d — степень полинома
- C — количество возможных сочетаний
Используя пример выше, получим
$$ N(2, 2) = C(4, 2) = 6 $$
Полином третьей степени на основе десяти линейных признаков уже потребует создать 286 коэффициентов.
$$ N(10, 3) = C(13, 10) = 286 $$
Если речь идет о картинках 28 х 28 пикселей, то после «вытягивания» каждой картинки у нас появится датасет с 784 признаками. Значит, количество членов полинома второй степени составит
$$ N(784, 2) = C(786, 2) = 308 505 $$
Замечу, что примерное количество признаков полинома второй степени также можно посчитать по формуле $ \frac{(n)^2}{2} $, то есть $ \frac{(784)^2}{2} = 307 328 $
Такое количество признаков потребует очень больших вычислительных ресурсов. Посмотрим, как нейросеть может помочь справиться с этой сложностью.
Работа нейронной сети
Рассмотрим работу нейронных сетей с трех различных углов зрения.
Нейрон как дополнительный признак
Возьмем упрощенную модель нейронной сети с двумя скрытыми слоями.

Каждый нейрон каждого из скрытых слоев можно рассматривать как новый, дополнительный признак, зачастую нелинейный, способный уловить то, что не могут уловить линейные признаки. При этом умножение матриц, лежащее в основе работы нейронной сети, и векторизация операций позволяют сделать этот алгоритм достаточно быстрым.
Слой как модель логистической регрессии
Одновременно, если считать, что каждый скрытый слой проходит через функцию активации (activation function), зачастую сигмоиду, то каждый слой, кроме выходного, можно представить как, например, модель логистический регрессии.
Для модели представленной выше рассмотрим как из второго скрытого слоя получается значение выходного слоя.

- На основе первого скрытого слоя мы получили некоторые значения нейронов второго скрытого слоя ($a_1^{(2)}$ и $a_2^{(2)}$)
- У нас есть вектор весов ($w_1^{(2)}$ и $w_2^{(2)}$)
- Кроме того, мы добавим смещение (b^{(2))
Замечу, что для удобства матричных операций мы можем добавить еще один нейрон скрытого слоя ($w_0^{(2)}$) со значением 1 так, как мы это делали, например, в модели линейной регрессии.
Результат умножения двух векторов мы пропустим через сигмодиду или функцию активации (отсюда выбор буквы a для обозначения этих нейронов) и таким образом получим значение выходного слоя ($a^{(3)}$). Уверен, вы распознали уравнение логистической регрессии.
$$ a^{(3)} = g(w_0^{(2)} \cdot b^{(2)} + w_1^{(2)} \cdot a_1^{(2)} + w_2^{(2)} \cdot a_2^{(2)}) $$
Соответственно, имея два скрытых слоя, мы строим две связанные между собой логистические регрессии. И вдвоем они могут запомнить более сложные зависимости, чем смогла бы одна такая модель, одновременно преодолевая проблему роста количества признаков полиномиальной модели.
Нейросеть и таблица истинности
Продемонстрируем, как нейросеть обучается на нелинейной гипотезе с помощью таблиц истинности.
Этот пример взят из курса по машинному обучению Эндрю Ына⧉ (Andrew Ng).
Рассмотрим линейно неразделимые данные двух классов (на рисунке слева) и упростим их до четырех наблюдений, которые могут принимать только значения 0 и 1 (на рисунке справа).

Логически такая схема соответствует условию $x_1 XNOR x_2$ или $ NOT x_1 XNOR x_2 $. В таблице истинности это условие выглядит так.

Другими словами, когда наблюдение по обоим признакам $x_1$ и $x_2$ имеет значение 0, то результатом будет класс 1, когда хотя бы один из признаков равен единице, то класс 0.
Построим нейросеть, которая будет предсказывать именно такую зависимость. Начнем с более простого компонента, а именно нейросети, которая делает прогноз в соответствии с логическим И (AND).

Итак, $x_1. x_2 \in \{0, 1\} $ и $y = x_1 AND x_2 $. В нейросети будет два нейрона для признаков + смещение. Одновременно сразу пропишем веса модели.

Тогда выражение будет иметь вид, $y_{AND} = sigmoid(-30 + 20x_1 + 20x_2)$. Вспомним, как выглядит график сигмоиды.

Рассмотрим четыре варианта значений $x_1, x_2$ применительно к такой гипотезе.
- Если оба признака будут равны нулю, то результат линейного выражения будет равен $-30$. Если «пропустить» это значение через сигмоиду, то результат будет близок к нулю.
- Если один из них будет равен нулю, а второй единице, то результат будет равен $-10$. Сигмоида опять выдаст близкое к нулю значение.
- И только если оба признака равны единице, то результат будет равен 10 и сигмоида выдаст значение близкое к единице.
Это и есть условие логического И. Аналогичным образом можно подобрать веса для логического ИЛИ (OR).


Соответственно $y_{OR} = sigmoid(-10 + 20x_1 + 20x_2)$. Создадим еще более простую сеть для логического НЕ (NOT).


Как следствие, $y_{NOT} = sigmoid(10-20x_1)$. Создадим нейросеть, которая будет предсказывать NOT($x1$) AND NOT($x2$).


Объединим эти сети в одну.

Рассчитаем таблицу истинности.

Таким образом, мы видим, что на каждом последующем слое нейросеть строит все более сложную зависимость. Первый скрытый слой обучился достаточно простым зависимостям ($x_1$ AND $x_2$ и NOT($x_1$) AND NOT($x_2$)), второй слой дополнил это знание новым ($x1$ OR $x2$), и вместе они обучились выдавать достаточно сложный результат ($x_1$ XNOR $x_2$).
Подготовка данных
Давайте вновь возьмем данные о вине, построим вначале бинарный, а затем мультиклассовый классификатор и посмотрим, сможет ли нейросеть улучшить показатели логистической регрессии.
Откроем ноутбук к этому занятию⧉
Импортируем датасет о вине, удалим класс 2, из признаков оставим спирт и пролин, масштабируем данные.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# импортируем датасет from sklearn import datasets data = datasets.load_wine() # сформируем датафрейм и добавим целевую переменную df = pd.DataFrame(data.data, columns = data.feature_names) df['target'] = data.target # удалим класс 2 df = df[df.target != 2] # оставим только спирт и пролин X = df[['alcohol', 'proline']] y = df['target'] # масштабируем признаки X = (X - X.mean()) / X.std() # посмотрим на размерность и оставшиеся классы df.shape, df.target.unique() |
1 |
((130, 14), array([0, 1])) |
Дополнительно преобразуем датафрейм признаков X в массив Numpy с размерность 2 x 130 и сделаем целевую переменную y двумерным массивом.
1 2 3 |
# каждый столбец - это одно наблюдение X = X.to_numpy().T X.shape |
1 |
(2, 130) |
1 2 |
y = y.to_numpy().reshape(1, -1) y.shape |
1 |
(1, 130) |
Нейросеть без смещения
Архитектура сети
Предлагаю идти от простого к сложному и на первом этапе создать алгоритм для бинарной классификации без смещения (bias) с приведенной ниже архитектурой.

Итак, сеть будет состоять из следующих слоев:
- входной слой $A^{(1)}$ из двух нейронов $a^{(1)}_1$ и $a^{(1)}_2$ т.е. двух признаков, на основе которых мы будем предсказывать класс вина;
- один скрытый слой $A^{(2)}$, состоящий из трех нейронов: $a^{(2)}_1$, $a^{(2)}_2$ и $a^{(2)}_3$; и
- выходной слой из одного нейрона $a^{(3)}$, т.е. вероятности принадлежности к одному из двух классов
Заглавной буквой, например, $A^{(1)}$ обозначаются сразу все нейроны, в данном случае, входного слоя, строчной с соответствющим индексом $a^{(1)}_1$ — отдельный нейрон этого слоя.
Также напомню, что у нас последовательная (sequential) архитектура сети, при которой каждый слой получает один тензор на вход и выдает также только один тензор. Кроме этого, мы используем полносвязные (дословно, «плотно связанные», dense, densely connected) слои, где каждый нейрон одного слоя связан с каждым нейроном последующего (обратите внимание на красные и зеленые стрелки между входным и скрытым слоями).
Размерность матриц
Алгоритм нейронной сети, в конечном счете, не более чем умножение матриц или, в общем случае, тензоров (отсюда, в частности, название библиотеки Tensorflow⧉, которой мы пользовались, изучая основы нейронных сетей) и после выбора архитектуры модели нам важно определиться с размерностью используемых нами матриц.
Первая матрица весов
Начнем со входного слоя, в который мы одновременно (за счет умножения матриц и векторизации кода) подадим матрицу из 130 наблюдений и двух признаков (после транспонирования размерность напомню была 2 x 130).
На нее мы будем умножать матрицу весов ($W^{(1)} \cdot X$ или в терминологии слоев $W^{(1)} \cdot A^{(1)}$). Как определить размерность матрицы весов? Очень просто, нужно взглянуть на количество нейронов скрытого слоя. Их три. Значит размерность первой матрицы весов составит 3 х 2.

Практический совет. Для того чтобы быстро найти размерность матрицы весов вспомним про две особенности умножения матриц:
- внутренние размеры, т.е. количество столбцов первой и строк второй, должны совпадать, в нашем случае 2 = 2.
- размерность результирующей матрицы будет равна внешним размерам умножаемых матриц, 3 и 130.
Итак, в скрытом слое у нас будет матрица 3 х 130, где каждый столбец — это три (активационных) нейрона для каждого из наблюдений.
Полносвязный слой
Убедимся, что такое умножение матриц обеспечивает умножение каждого нейрона входного слоя на каждый нейрон скрытого слоя (т.е. полносвязность, density, слоев). Для простоты, предположим, что у нас только четыре наблюдения, я не 130.

Рассмотрим первую операцию. Здесь веса $w_1$ и $w_4$ умножаются на нейроны входного слоя $a_1$ и $a_2$ и, таким образом, обеспечивают их «участие» в значении нейрона $a_1$ скрытого слоя. Аналогично, при второй операции за это отвечают $w_2$ и $w_5$. Наконец третий нейрон скрытого слоя рассчитывается благодаря весам $w_3$ и $w_6$ и опять же обоим нейронам входного слоя.
Эти же операции можно посмотреть на стрелках на схеме архитектуры сети.
Вторая матрица весов
Теперь, чтобы получить один единственный нейрон выходного слоя (вернее вектор-строку из 130 таких нейронов, 1 х 130), нам нужно новую матрицу весов умножить на результат скрытого слоя на $W^{(2)} \cdot A^{(2)}$.

Очевидно, это должна быть матрица 1 х 3, потому что только она даст нам нужную итоговую размерность 1 x 130.
Важная деталь. Как нейроны скрытого слоя, так и нейрон выходного слоя проходят через функцию активации (activation function), в нашем случае сигмоиду (на схемах выше не показана).
В целом мы только что рассмотрели прямое распространение. Давайте напишем соответствующий код.
Код прямого распространения
Вначале объявим знакомые нам функции сигмоиды (функция активации) и логистической ошибки (функция потерь). Их место в архитектуре сети можно увидеть на схеме ниже.

Дополнительно замечу, что в нашей терминологии $z^(1)$ и $z^(2)$ — это результат умножения матрицы весов на матрицу нейронов соответствующего слоя, который мы «пропускаем» через сигмоиду (g). Т.е., например, для скрытого слоя
$$ Z^{(1)} = W^{(1)} \cdot A^{(1)} $$
$$ A^{(2)} = g(Z^{(1)}) $$
1 2 3 4 5 6 7 8 9 10 11 |
# функция активации def sigmoid(z): s = 1 / (1 + np.exp(-z)) return s # функция потерь def objective(y, y_pred): y_one_loss = y * np.log(y_pred + 1e-9) y_zero_loss = (1 - y) * np.log(1 - y_pred + 1e-9) return -np.mean(y_zero_loss + y_one_loss) |
Теперь объявим веса и поместим признаки в нейроны скрытого слоя (исключительно ради единнобразия терминологии).
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# зададим точку отсчета np.random.seed(33) # инициализируем случайные веса, # взятые из стандартного нормального распределения W1 = np.random.randn(3, 2) W2 = np.random.randn(1, 3) # найдем количество наблюдений n = X.shape[1] # поместим признаки в нейроны входного слоя A1 = X |
Последовательно выполним операции умножения весов на нейроны и пропустим результаты через две сигмоиды.
1 2 3 4 5 6 7 8 9 10 11 |
# выполним умножение матриц W1 и A1 и "пропустим" результат через сигмоиду # в скобках указана итоговая размерность операции Z1 = np.dot(W1, A1) # (3 x 130) A2 = sigmoid(Z1) # (3 x 130) # поступим аналогично с матрицами W2 и A2 Z2 = np.dot(W2, A2) # (1 x 130) A3 = sigmoid(Z2) # (1 x 130) # посмотрим, какую вероятность модель выдала для первого наблюдения A3[:,0] |
1 |
array([0.51555295]) |
Найдем ошибку при текущих весах.
1 2 |
loss = objective(A3, y) loss |
1 |
7.522291935047792 |
Обратное распространение
Теперь главный вопрос. Как обновить веса так, чтобы уменьшить ошибку?
По большому счету нам нужно рассчитать частную производную функции логистической ошибки ($L$) относительно каждого веса ($w_1, w_2, w_3, …, w_9$), ведь именно их мы и будем обновлять. Начнем с весов второго слоя, а именно, $w_7, w_8, w_9$ (все вместе мы будем обозначать их как $W^{(2)}$).
Частные производные весов $W^{(2)}$
Согласно chain rule градиент (т.е. совокупность частных производных) весов второго слоя будет иметь вид
$$ \frac{\partial L}{\partial w^{(2)}} = \frac{\partial L}{\partial a^{(3)}} \circ \frac{\partial a^{(3)} }{\partial z^{(2)}} \circ \frac{ \partial z^{(2)} }{\partial w^{(2)} } $$
Что нам нужно сделать?
- Вначале найти производную функции логистической ошибки $ \frac{\partial L}{\partial a^{(3)}} $
- После этого производную сигмоиды $\frac{\partial a^{(3)} }{\partial z^{(2)}}$
- И наконец линейной функции $\frac{ \partial z^{(2)} }{\partial w^{(2)} }$
- Перемножить эти производные
Возможно вы заметили, что выше использовались индексы (3) и (2), индекс (3) относит активационную функцию $a^{(3)}$ к третьему выходному слою, а линейную функцию $z^{(2)}$ и веса линейной функции $ w^{(2)} $ ко второму. В такой нотации нам будет удобнее в дальнейшем рассчитывать градиенты и писать код.
На всякий случай также уточню, что это будет поэлементное умножение или произведение Адамара (Hadamard product), которое мы будем обозначать через оператор $\circ$.
Далее, уверен, вы обратили внимание на то, что мы выполняем операции в обратном от прямого распространения порядке: сначала производная ошибки, потом сигмоиды третьего слоя, затем линейной функции второго. Именно поэтому процесс называется обратным распространением ошибки (error back propagation).
Производная функции логистической ошибки
$$ \frac{\partial L}{\partial a^{(3)}} = \frac{\partial}{\partial a^{(3)}} \left( -y \log(a^{(3)})-(1-y) \log(1-a^{(3)}) \right) $$
Применим правило производной разности и вынесем константы.
$$ -y \frac{\partial}{\partial a^{(3)}} \log(a^{(3)})-(1-y) \frac{\partial}{\partial a^{(3)}} \log(1-a^{(3)}) $$
Найдем производную натурального логарифма, вынесем минус за скобку и вычтем одну дробь из другой.
$$ -\left( \frac{y}{a^{(3)}}-\frac{(1-y) }{1-a^{(3)}} \right) = \frac{a^{(3)}-y}{a^{(3)}(1-a^{(3)})} $$
Производная сигмоиды
Производную сигмоиды мы уже находили.
$$ \frac{\partial a^{(3)} }{\partial z^{(2)}} = g(z^{(2)}) (1-g(z^{(2)})) $$
При этом так как результат сигмоиды $ g(z^{(2)}) $ — это нейрон выходного слоя $ a^{(3)} $, то
$$ \frac{\partial a^{(3)} }{\partial z^{(2)}} = a^{(3)} (1-a^{(3)}) $$
Производная линейной функции
Найдем производную линейной функции, расписав умножение для каждого веса и для каждого нейрона.
$$ \frac{ \partial }{\partial w^{(2)} } \left( w_7 \times a^{(2)}_1 + w_8 \times a^{(2)}_2 + w_9 \times a^{(2)}_3 \right) $$
Для того чтобы найти производную относительно, например, веса $w_7$, мы «замораживаем» (считаем константами, производная которых равна нулю) все веса кроме первого и тогда
$$ w_7^{1-1} \times a^{(2)}_1 + 0 \times a^{(2)}_2 + 0 \times a^{(2)}_3 = $$
$$ 1 \times a^{(2)}_1 + 0 \times a^{(2)}_2 + 0 \times a^{(2)}_3 = a^{(2)}_1 $$
Аналогичный результат мы получим, продифференцировав относительно других весов. Тогда,
$$ \frac{ \partial z^{(2)} }{\partial w^{(2)}} = a^{(2)} $$
Наконец перемножим найденные производные и упростим выражение.
$$ \frac{\partial L}{\partial w^{(2)}} = \frac{a^{(3)}-y}{a^{(3)}(1-a^{(3)})} \circ a^{(3)} (1-a^{(3)}) \circ a^{(2)} = $$
$$ (a^{(3)}-y) \circ a^{(2)} $$
В векторной нотации (и матричном умножении) получим
$$ \frac{\partial L}{\partial W^{(2)}} = (A^{(3)}-y) \cdot A^{(2)}.T \times \frac{1}{n} $$
Множитель $ \frac{1}{n} $ усредняет градиент на количество наблюдений.
Дельта-правило ($ \delta_2 $)
Замечу, что $\frac{\partial L}{\partial a^{(3)}} \circ \frac{\partial a^{(3)} }{\partial z^{(2)}}$ также обозначают через греческую букву «дельта» (в нашем случае $\delta_2$), и тогда градиент для обновления весов $W^{(2)}$, с учетом векторизованного кода, приобретет вид (опять же в векторной нотации)
$$ \frac{\partial L}{\partial W^{(2)}} = \delta_2 \cdot A^{(2)}.T \times \frac{1}{n} $$
В дальнейшем использование так называемого «дельта-правила» (delta rule) упростит наш код.
Обновление весов $W^{(2)}$
Остается только обновить веса $W^{(2)}$ в направлении антиградиента, умноженного на коэффициент скорости обучения.
$$ W^{(2)} := W^{(2)}-\alpha \times \frac{\partial L}{\partial W^{(2)}}$$
Частные производные весов $W^{(1)}$
Теперь нужно найти производные относительно весов ($w_1, …, w_6$) или $W^{(1)}$. И мы снова должны «раскручивать» chain rule от функции логистической ошибки. На этот раз цепь будет более длинной.
$$ \frac{\partial L}{\partial w^{(1)}} = \frac{\partial L}{\partial a^{(3)}} \circ \frac{\partial a^{(3)} }{\partial z^{(2)}} \circ \frac{ \partial z^{(2)} }{\partial a^{(2)} } \circ \frac{ \partial a^{(2)} }{\partial z^{(1)} } \circ \frac{ \partial z^{(1)} }{\partial w^{(1)} } $$
Нахождение производных
Вспомним, что первые два множителя $\frac{\partial L}{\partial a^{(3)}} \circ \frac{\partial a^{(3)} }{\partial z^{(2)}}$ мы обозначили через $\delta_2$.
Обратим внимание на третий множитель $ \frac{ \partial z^{(2)} }{\partial a^{(2)} } $. В отличие от градиента весов $W^{(2)}$, где мы, напомню, искали производную линейной функции относительно весов $\frac{ \partial z^{(2)} }{\partial w^{(2)} }$, здесь нас интересует частная производная относительно нейронов активационного слоя $a^{(2)}$.
Тогда в данном случае мы «замораживаем» (считаем константами) не веса, а нейроны $ a^{(2)} $ (считая веса просто числами) и, например, частная производная относительно $a^{(2)}_1$ будет равна
$$ \frac{ \partial }{\partial a^{(2)}_1 } \left( w_7 \times a^{(2)}_1 + w_8 \times a^{(2)}_2 + w_9 \times a^{(2)}_3 \right) $$
$$ w_7 \times 1 + w_8 \times 0 + w_9 \times 0 = w_7 $$
Аналогично находим производные относительно других нейронов. В векторной нотации,
$$ \frac{ \partial z^{(2)} }{\partial a^{(2)} } = W^{(2)}$$
Интересно, что ошибкой скрытого слоя $E_2$ (ошибкой $E_1$ была бы общая ошибка, которую мы рассчитали с помощью функции логистической ошибки) называют произведение
$$ E_2 = \frac{\partial L}{\partial a^{(3)}} \circ \frac{\partial a^{(3)} }{\partial z^{(2)}} \circ \frac{ \partial z^{(2)} }{\partial a^{(2)} } $$
Это утверждение более понятно, если переписать (в векторной нотации) выражение выше как,
$$ E_2 = W^{(2)}.T \cdot \delta_2 $$
То есть, мы по сути распространяем «ошибку» $ \delta_2 $ (число, скаляр) на каждый из трех весов $W^{(2)}$.
Перейдем к четвертому множителю $ \frac{ \partial a^{(2)} }{\partial z^{(1)} }$. Это снова производная сигмоиды, только уже «на слой раньше»,
$$ \frac{ \partial a^{(2)} }{\partial z^{(1)} } = g(z^{(1)}) (1-g(z^{(1)})) = a^{(2)} (1-a^{(2)}) $$
И наконец пятый компонент,
$$ \frac{ \partial z^{(1)} }{\partial w^{(1)} } = a^{(1)} $$
Дельта-правило ($ \delta_1 $)
Аналогично предыдущему слою мы можем обозначить $ \frac{\partial L}{\partial a^{(3)}} \circ \frac{\partial a^{(3)} }{\partial z^{(2)}} \circ \frac{ \partial z^{(2)} }{\partial a^{(2)} } \circ \frac{ \partial a^{(2)} }{\partial z^{(1)} } $ как $ \delta_1 $ (то есть мы опять взяли все множители, кроме последнего).
Градиент относительно $W^{(1)}$
В итоге градиент относительно весов $W^{(1)}$ имел бы вид,
$$ \frac{\partial L}{\partial W^{(1)}} = \left( E_2 \circ A^{(2)} \circ (1-A^{(2)}) \right) \cdot A^{(1)}.T \times \frac{1}{n} $$
Или, раскрыв $E_2$,
$$ \frac{\partial L}{\partial W^{(1)}} = \left( ( W^{(2)}.T \cdot \delta_2) \circ A^{(2)} \circ (1-A^{(2)}) \right) \cdot A^{(1)}.T \times \frac{1}{n} $$
Или через $ \delta_1 $
$$ \frac{\partial L}{\partial W^{(1)}} = \delta_1 \cdot A^{(1)}.T \times \frac{1}{n} $$
Обратите внимание на паттерн, градиент каждого слоя представляет собой произведение дельты на соответствующие нейроны активационного слоя, усредненное на количество наблюдений.
$$ \frac{\partial L}{\partial W^{(2)}} = \delta_2 \cdot A^{(2)}.T \times \frac{1}{n} $$
$$ \frac{\partial L}{\partial W^{(1)}} = \delta_1 \cdot A^{(1)}.T \times \frac{1}{n} $$
Разумеется, это правило справедливо и для большего количества скрытых слоев.
Обновление весов $W^{(1)}$
Обновление весов $W^{(1)}$ аналогично предыдущему слою.
$$ W^{(1)} := W^{(1)}-\alpha \times \frac{\partial L}{\partial W^{(1)}}$$
Перейдем к коду.
Код обратного распространения
Продолжим писать код, которые мы начали, изучая прямое распространение.
1 2 |
# найдем дельту весов между слоями 3 и 2 W2_delta = A3 - y # (1 x 130) |
1 2 3 |
# обратите внимание, это одно число, как и результат # третьего слоя A3 (мы выводим первый столбец из 130) W2_delta[:, 0] |
1 |
array([0.51555295]) |
1 2 |
# найдем дельту весов между слоями 1 и 2 W1_delta = np.dot(W2.T, W2_delta) * A2 * (1 - A2) # (3 x 130) |
1 2 |
# дельта 1 состоит уже из трех чисел, как и скрытый слой нейросети W1_delta[:, 0] |
1 |
array([-0.0099838 , 0.06300821, -0.01243332]) |
1 2 3 4 5 6 7 |
# напомню, что умножение дельты 2 на веса скрытого слоя W2 можно # считать "промежуточной ошибкой" сети или ошибкой скрытого слоя E2 = np.dot(W2.T, W2_delta) # то есть одно число W2_delta мы "распространили" на весь скрытый слой, # поэтому ошибка состоит из трех чисел E2[:, 0] |
1 |
array([-0.07704936, 1.03666489, -0.04989736]) |
1 2 3 |
# наконец найдем частную производную относительно весов W2 W2_derivative = np.dot(W2_delta, A2.T) / n # (1 x 3) W2_derivative |
1 |
array([[-0.16738339, -0.23720379, 2.99973404]]) |
1 2 3 4 |
# и весов W1 # (размерность опять же должна совпадать с размерностью матриц весов) W1_derivative = np.dot(W1_delta, A1.T) / n # (3 x 3) W1_derivative |
1 2 3 |
array([[-0.14145948, -0.12624909], [ 1.41742921, 1.87529043], [-0.19429558, -0.21266884]]) |
1 2 3 |
# обновим веса (скорость обучения возьмем равной единице) W2 = W2 - 1 * W2_derivative W1 = W1 - 1 * W1_derivative |
Обучение модели
Теперь соединим прямое и обратное распространение и с помощью цикла произведем обучение нейронной сети.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
np.random.seed(33) W1 = np.random.randn(3, 2) W2 = np.random.randn(1, 3) epochs = 100000 learning_rate = 1 n = X.shape[1] A1 = X for i in range(epochs): # рассчитываем прямое распространение Z1 = np.dot(W1, A1) # (3 x 130) A2 = sigmoid(Z1) # (3 x 130) Z2 = np.dot(W2, A2) # (1 x 130) A3 = sigmoid(Z2) # (1 x 130) # рассчитываем ошибку loss = objective(A3, y) # находим дельту весов между слоями 3 и 2 W2_delta = A3 - y # (1 x 130) # находим дельту весов между слоями 2 и 1 W1_delta = np.dot(W2.T, W2_delta) * A2 * (1 - A2) # (3 x 130) # находим частные производные W2_derivative = np.dot(W2_delta, A2.T)/n # (1 x 3) W1_derivative = np.dot(W1_delta, A1.T)/n # (3 x 3) # обновляем веса W2 = W2 - learning_rate * W2_derivative W1 = W1 - learning_rate * W1_derivative # периодически выводим количество итераций и текущую ошибку if i % (epochs / 5) == 0: print('Эпоха:', i) print('Ошибка:', loss) print('-----------------------') # можем добавить паузу для более аккуратного вывода time.sleep(0.5) print('Итоговая ошибка', loss) print('Нейросеть успешно обучена') |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Эпоха: 0 Ошибка: 7.522291935047792 ----------------------- Эпоха: 20000 Ошибка: 0.7636370744482613 ----------------------- Эпоха: 40000 Ошибка: 0.7137144818217118 ----------------------- Эпоха: 60000 Ошибка: 0.6981278394758277 ----------------------- Эпоха: 80000 Ошибка: 0.6901729606032463 ----------------------- Итоговая ошибка 0.6847941294287265 Нейросеть успешно обучена |
Прогноз и оценка качества
Сделаем прогноз и оценим качество.
1 2 3 4 5 6 7 8 9 10 |
A1 = X Z1 = np.matmul(W1, A1) A2 = sigmoid(Z1) Z2 = np.matmul(W2, A2) A3 = sigmoid(Z2) # A3.flatten() делает массив одномерным, # условие выводит True или False (1 или 0) y_pred, y_true = A3.flatten() >= 0.5, y.flatten() |
1 |
W1, W2 |
1 2 3 4 |
(array([[ 1.59357193e-03, -1.11510787e+00], [-2.70942468e+01, 1.10767378e+01], [-1.78982169e-03, 1.11459111e+00]]), array([[ 14.76831281, 8.70860599, -21.63309402]])) |
1 2 |
from sklearn.metrics import accuracy_score accuracy_score(y_true, y_pred) |
1 |
0.9769230769230769 |
Инициализация весов
В моделях линейной и логистической регрессии в качестве начальных значений коэффициентов мы использовали нули, в алгоритме нейронной сети — случайные значения, почему так?

Если веса изначально равны нулю, то произойдет несколько нежелательных событий:
- значения активационных слоев $a_1^(2) = a_2^(2) = a_3^(2) $ будут одинаковыми, то есть запоминать одну и ту же зависимость
- более того, так как веса между вторым и третьим (выходным) слоем будут одинаковыми, то и значения матрицы $ \delta_2 $ будут одинаковыми,
- а значит и частные производные, относящиеся к весам одного входного нейрона (например, $w_1, w_2, w_3$) будут одинаковыми
Таким образом, после, например, одного обновления весов, хотя значения весов $w_1, w_2, w_3$ не будут нулевыми, они будут одинаковыми. То же можно сказать про веса $w_4, w_5, w_6$. И снова $a_1^(2) = a_2^(2) = a_3^(2) $.
Как следствие, мы существенно ограничиваем возможности (гибкость) нейронной сети запоминать сложные зависимости.
Масштабирование целевой переменной
Как мы только что убедились, градиент нейронной сети зависит от целевой переменной. Если эта переменная имеет большой диапазон, то это может создать большую ошибку при вычислении градиента, что, в свою очередь, вызовет существенное изменение весов и дестабилизирует процесс обучения.
Очевидно после обучения и прогноза целевую переменную нужно вернуть к прежнему масштабу.
Модель в Tensorflow и Keras
Библиотека Keras представляет собой «надстройку«⧉ (интерфейс, API), через которую удобно создавать нейросети в библиотеке Tensorflow. Реализуем созданную выше несложную нейросеть в библиотеке Keras.
1 2 3 4 |
# в Google Colab уже установлена вторая версия библиотеки Tensorflow, # которая существенно отличается от первой версии import tensorflow as tf tf.__version__ |
1 |
2.9.2 |
В нейросеть мы будем подавать признаки и целевую переменную таким образом, чтобы объекты были строками.
1 |
X.T.shape, y.T.shape |
1 |
((130, 2), (130, 1)) |
Перейдем к созданию и обучению нейросети.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# зададим две точки отсчета, одну в библиотеке Numpy np.random.seed(42) # вторую непосредственно в Tensorflow tf.random.set_seed(42) # зададим архитектуру: последовательная модель model = tf.keras.models.Sequential([ # с полносвязными слоями # также укажем нейроны скрытого и выходного слоев # откажемся от смещения tf.keras.layers.Dense(3, activation = 'sigmoid', use_bias = False), tf.keras.layers.Dense(1, activation = 'sigmoid', use_bias = False) ]) # зададим особенности стохастического градиентного спуска (SGD) # в частности, откажемся от импульса # (подробнее об этом на последующих занятиях) sgd = tf.keras.optimizers.SGD(learning_rate = 1, momentum = 0, nesterov = False) # соберем все вместе, дополнительно укажем тип функции потерь и метрику качества model.compile(optimizer = sgd, loss = 'binary_crossentropy', metrics = ['accuracy']) # зададим количество эпох, # размер batch, после которой мы обновляем веса, равен объему данных (fullbatch) model.fit(X.T, y.T, epochs = 10000, batch_size = 130, verbose = 0) |
1 |
<keras.callbacks.History at 0x7f5f49c0aa90> |
1 |
model.summary() |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Model: "sequential_3" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= dense_6 (Dense) (130, 3) 6 dense_7 (Dense) (130, 1) 3 ================================================================= Total params: 9 Trainable params: 9 Non-trainable params: 0 _________________________________________________________________ |
Оценим качество.
1 |
model.history.history['accuracy'][-1] |
1 |
0.9615384340286255 |
Нейросеть со смещением
Добавим смещение, это должно сделать нашу модель более гибкой.

В отличие от линейных моделей, мы не будем использовать одну и ту же производную и для $b$, и для $W$ (с добавлением столбца из единиц в X). Это связано с тем, что в нейросетях при обратном распространении ошибку на смещение мы не распространяем.
Найдем производные смещения относительно сигмоиды (последний компонент в цепи производных).
$$ \frac{ \partial z^{(2)} }{\partial b^{(2)}} = 1 $$
$$ \frac{ \partial z^{(2)} }{\partial b^{(1)}} = 1 $$
Тогда в целом, используя дельта-правило, частные производные относительно $b^{(2)}$ и $b^{(1)}$ будут равны
$$ \frac{\partial L}{\partial b^{(2)}} = \sum \delta_2 \times \frac{1}{n} $$
$$ \frac{\partial L}{\partial b^{(1)}} = \sum \delta_1 \times \frac{1}{n} $$
Перейдем к коду.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
np.random.seed(33) # инициализируем веса W1 = np.random.randn(3, 2) # b1 будет иметь размерность 3 х 1, # потому что распространяется на три нейрона скрытого слоя b1 = np.random.randn(3, 1) W2 = np.random.randn(1, 3) b2 = np.random.randn(1, 1) n = X.shape[1] epochs = 100000 learning_rate = 1 A1 = X for i in range(epochs): # 3 х 2 на 2 х 130 Z1 = np.dot(W1, A1) + b1 A2 = sigmoid(Z1) # (3 x 130) # 1 х 3 на 3 х 130 Z2 = np.dot(W2, A2) + b2 A3 = sigmoid(Z2) # (1 x 130) loss = objective(A3, y) W2_delta = A3 - y # (1 x 130) W1_delta = np.dot(W2.T, W2_delta) * A2 * (1 - A2) # (3 x 130) # keepdims сохраняет исходную размерность # 1 х 130 на 130 х 3 W2_derivative = np.dot(W2_delta, A2.T) / n # (1 x 3) b2_derivative = np.sum(W2_delta, keepdims = True) / n # (1 x 1) # 3 х 130 на 130 х 2 W1_derivative = np.dot(W1_delta, A1.T) / n # (3 x 2) b1_derivative = np.sum(W1_delta, keepdims = True) / n # (1 x 1) W2 = W2 - learning_rate * W2_derivative b2 = b2 - learning_rate * b2_derivative W1 = W1 - learning_rate * W1_derivative b1 = b1 - learning_rate * b1_derivative if i % (epochs / 5) == 0: print('Эпоха:', i) print('Ошибка:', loss) print('-----------------------') time.sleep(0.5) print('Итоговая ошибка', loss) print('Нейросеть успешно обучена') |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Эпоха: 0 Ошибка: 10.080172158274355 ----------------------- Эпоха: 20000 Ошибка: 0.5460931772435075 ----------------------- Эпоха: 40000 Ошибка: 0.5235933834724054 ----------------------- Эпоха: 60000 Ошибка: 0.5153108060799304 ----------------------- Эпоха: 80000 Ошибка: 0.5104559107317911 ----------------------- Итоговая ошибка 0.5070018571750416 Нейросеть успешно обучена |
1 2 3 4 5 6 7 8 |
Z1 = np.matmul(W1, A1) + b1 A2 = sigmoid(Z1) Z2 = np.matmul(W2, A2) + b2 A3 = sigmoid(Z2) y_pred, y_true = A3.flatten() > 0.5, y.flatten() accuracy_score(y_true, y_pred) |
1 |
0.9846153846153847 |
TF / Keras. Добавим смещение в нашу модель в библиотеке Keras.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
np.random.seed(42) tf.random.set_seed(42) model = tf.keras.models.Sequential([ tf.keras.layers.Dense(3, activation = 'sigmoid', use_bias = True), tf.keras.layers.Dense(1, activation = 'sigmoid', use_bias = True) ]) sgd = tf.keras.optimizers.SGD(learning_rate = 1, momentum = 0, nesterov = False) model.compile(optimizer = sgd, loss = 'binary_crossentropy', metrics = ['accuracy']) model.fit(X.T, y.T, epochs = 10000, batch_size = 130, verbose = 0) model.summary() |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Model: "sequential_6" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= dense_12 (Dense) (130, 3) 9 dense_13 (Dense) (130, 1) 4 ================================================================= Total params: 13 Trainable params: 13 Non-trainable params: 0 _________________________________________________________________ |
1 |
model.history.history['accuracy'][-1] |
1 |
0.9846153855323792 |
1 |
W1, W2 |
1 2 3 4 |
(array([[ 6.12093182, -12.02342998], [-22.00510547, -9.19535995], [ 14.35718419, 9.69417827]]), array([[ 12.32118926, 15.26008905, -10.91093576]])) |
Два скрытых слоя
Добавим второй скрытый слой.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
np.random.seed(33) W1 = np.random.randn(3, 2) b1 = np.random.randn(3, 1) W2 = np.random.randn(3, 3) b2 = np.random.randn(3, 1) W3 = np.random.randn(1, 3) b3 = np.random.randn(1, 1) n = X.shape[1] epochs = 10000 learning_rate = 1 A1 = X for i in range(epochs): Z1 = np.dot(W1, A1) + b1 # (3 x 130) A2 = sigmoid(Z1) Z2 = np.dot(W2, A2) + b2 # (3 x 130) A3 = sigmoid(Z2) Z3 = np.dot(W3, A3) + b3 # (1 x 130) A4 = sigmoid(Z3) loss = objective(A4, y) W3_delta = A4 - y # (1 x 130) W2_delta = np.dot(W3.T, W3_delta) * A3 * (1 - A3) # (3 x 130) W1_delta = np.dot(W2.T, W2_delta) * A2 * (1 - A2) # (3 x 130) # 3 х 130 на 130 х 3 W3_derivative = np.dot(W3_delta, A3.T) / n # (3 x 3) b3_derivative = np.sum(W3_delta, keepdims = True) / n # (1 x 1) # 3 х 130 на 130 х 3 W2_derivative = np.dot(W2_delta, A2.T) / n # (3 x 3) b2_derivative = np.sum(W2_delta, keepdims = True) / n # (1 x 1) W1_derivative = np.dot(W1_delta, A1.T) / n # (3 x 2) b1_derivative = np.sum(W1_delta, keepdims = True) / n # (1 x 1) W3 = W3 - learning_rate * W3_derivative b3 = b3 - learning_rate * b3_derivative W2 = W2 - learning_rate * W2_derivative b2 = b2 - learning_rate * b2_derivative W1 = W1 - learning_rate * W1_derivative b1 = b1 - learning_rate * b1_derivative if i % (epochs / 5) == 0: print('Эпоха:', i) print('Ошибка:', loss) print('-----------------------') time.sleep(1) print('Итоговая ошибка', loss) print('Нейросеть успешно обучена') |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Эпоха: 0 Ошибка: 11.483275165239569 ----------------------- Эпоха: 2000 Ошибка: 0.7137240298878413 ----------------------- Эпоха: 4000 Ошибка: 0.683999037922229 ----------------------- Эпоха: 6000 Ошибка: 0.6718618481850791 ----------------------- Эпоха: 8000 Ошибка: 0.6640320570961954 ----------------------- Итоговая ошибка 0.6583025695728971 Нейросеть успешно обучена |
1 2 3 4 5 6 7 8 9 10 |
Z1 = np.matmul(W1, A1) + b1 A2 = sigmoid(Z1) Z2 = np.matmul(W2, A2) + b2 A3 = sigmoid(Z2) Z3 = np.matmul(W3, A3) + b3 A4 = sigmoid(Z3) y_pred, y_true = A4.flatten() > 0.5, y.flatten() accuracy_score(y_true, y_pred) |
1 |
0.9846153846153847 |
Сравним с моделью в Keras.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
np.random.seed(42) tf.random.set_seed(42) model = tf.keras.models.Sequential([ tf.keras.layers.Dense(3, activation = 'sigmoid', use_bias = True), tf.keras.layers.Dense(3, activation = 'sigmoid', use_bias = True), tf.keras.layers.Dense(1, activation = 'sigmoid', use_bias = True) ]) sgd = tf.keras.optimizers.SGD(learning_rate = 1, momentum = 0, nesterov = False) model.compile(optimizer = sgd, loss = 'binary_crossentropy', metrics = ['accuracy']) model.fit(X.T, y.T, epochs = 10000, batch_size = 130, verbose = 0) model.summary() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Model: "sequential_2" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= dense_4 (Dense) (130, 3) 9 dense_5 (Dense) (130, 3) 12 dense_6 (Dense) (130, 1) 4 ================================================================= Total params: 25 Trainable params: 25 Non-trainable params: 0 |
1 |
model.history.history['accuracy'][-1] |
1 |
0.9846153855323792 |
Многоклассовая классификация
Создадим нейросеть, которая будет предсказывать вероятности нескольких классов. Во многом, идея алгоритма совпадает с softmax логистической регрессией.
Постановка задачи и архитектура
Возьмемся за ту же задачу, которую мы решили в рамках вводного курса с помощью библиотеки Keras, а именно создадим нейросеть, которая будет распознавать рукописные цифры из датасета MNIST.
Архитектуру модели сохраним прежней.

Функции активации
Как видно на графике, мы будем использовать сигмоиду для промежуточных слоев и softmax для выходного слоя.
1 2 3 |
def sigmoid(z): s = 1 / (1 + np.exp(-z)) return s |
1 2 3 4 5 6 7 8 |
def softmax(z): # на выходном слое тензор будет иметь размерность 10 х 60000, # поэтому складывать мы будем по столбцам z = z - np.max(z, axis = 0, keepdims = True) numerator = np.exp(z) denominator = np.sum(numerator, axis = 0, keepdims = True) softmax = numerator / denominator return softmax |
Функция потерь
Функцией потерь будет категориальная кросс-энтропия.
1 2 3 4 5 6 7 |
def cross_entropy(probs, y_enc, epsilon = 1e-9): # опять же, так как softmax выдаст тензор 10 х 60000 # количество наблюдений содержится в атрибуте shape[1] n = probs.shape[1] ce = -np.sum(y_enc * np.log(probs + epsilon)) / n return ce |
Обратное распространение
Очевидно, так как изменилась функция потерь и функция активации выходного слоя (softmax) необходимо заново рассчитать производные. Напомню, для весов $W^{(3)}$ цепное правило будет работать следующим образом.
$$ \frac{\partial L}{\partial w^{(3)}} = \frac{\partial L}{\partial a^{(4)}} \circ \frac{\partial a^{(4)} }{\partial z^{(3)}} \circ \frac{ \partial z^{(3)} }{\partial w^{(3)} } $$
При этом, оказывается, что производная первых двух компонентов $\frac{\partial L}{\partial a^{(4)}} \circ \frac{\partial a^{(4)} }{\partial z^{(3)}}$ (т.е. кросс-энтропии и softmax) сводится к $a^{(4)}-y$ (она аналогична бинарной кросс-энтропии и сигмоиде, но находится⧉, разумеется, иначе).
Одновременно этот компонент производной представляет собой $\delta_3$, которую для нахождения градиента необходимо умножить на $A^{(3)}.T$.
$$ \frac{\partial L}{\partial W^{(3)}} = \delta_3 \cdot A^{(3)}.T \times \frac{1}{n} $$
Остальные производные находятся аналогично предыдущим моделям.
$$ \frac{\partial L}{\partial W^{(2)}} = \delta_2 \cdot A^{(2)}.T \times \frac{1}{n} $$
$$ \frac{\partial L}{\partial W^{(1)}} = \delta_1 \cdot A^{(1)}.T \times \frac{1}{n} $$
Подготовка данных
1 |
!pip install mnist |
1 2 |
import mnist from tensorflow import keras |
1 2 3 4 5 6 7 |
def ohe(y): examples, features = y.shape[0], len(np.unique(y)) zeros_matrix = np.zeros((examples, features)) for i, (row, digit) in enumerate(zip(zeros_matrix, y)): zeros_matrix[i][digit] = 1 return zeros_matrix |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
X_train = mnist.train_images() y_train = mnist.train_labels() X_test = mnist.test_images() y_test = mnist.test_labels() X_train = 2. * (X_train - np.min(X_train)) / np.ptp(X_train) - 1 X_test = 2. * (X_test - np.min(X_test)) / np.ptp(X_test) - 1 X_train = X_train.reshape((-1, 784)).T X_test = X_test.reshape((-1, 784)).T y_train_enc, y_test_enc = ohe(y_train).T, ohe(y_test).T X_train.shape, y_train_enc.shape |
1 |
((784, 60000), (10, 60000)) |
Обучение модели
Код ниже исполняется в Google Colab около 10 минут.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
np.random.seed(33) W1 = np.random.randn(64, 784) b1 = np.random.randn(64, 1) W2 = np.random.randn(64, 64) b2 = np.random.randn(64, 1) W3 = np.random.randn(10, 64) b3 = np.random.randn(10, 1) n = X_train.shape[1] epochs = 500 learning_rate = 1 A1 = X_train for i in range(epochs): # 64 x 784 на 784 x 60000 --> 64 x 60000 Z1 = np.dot(W1, A1) + b1 A2 = sigmoid(Z1) # 64 x 64 на 64 x 60000 --> 64 x 60000 Z2 = np.dot(W2, A2) + b2 A3 = sigmoid(Z2) # 10 x 64 на 64 x 60000 --> 10 x 60000 Z3 = np.dot(W3, A3) + b3 A4 = softmax(Z3) loss = cross_entropy(A4, y_train_enc) W3_delta = A4 - y_train_enc # (10 x 60000) W2_delta = np.dot(W3.T, W3_delta) * A3 * (1 - A3) # (64 x 60000) W1_delta = np.dot(W2.T, W2_delta) * A2 * (1 - A2) # (64 x 60000) # 10 x 60000 на 60000 x 64 --> 10 x 64 W3_derivative = np.dot(W3_delta, A3.T) / n b3_derivative = np.sum(W3_delta, keepdims = True) / n # (1 x 1) # 64 x 60000 на 60000 x 64 --> 64 x 64 W2_derivative = np.dot(W2_delta, A2.T) / n b2_derivative = np.sum(W2_delta, keepdims = True) / n # (1 x 1) # 64 x 60000 на 60000 x 784 --> 64 x 784 W1_derivative = np.dot(W1_delta, A1.T) / n b1_derivative = np.sum(W1_delta, keepdims = True) / n # (1 x 1) W3 = W3 - learning_rate * W3_derivative b3 = b3 - learning_rate * b3_derivative W2 = W2 - learning_rate * W2_derivative b2 = b2 - learning_rate * b2_derivative W1 = W1 - learning_rate * W1_derivative b1 = b1 - learning_rate * b1_derivative if i % (epochs / 5) == 0: print('Эпоха:', i) print('Ошибка:', loss) print('-----------------------') print('Итоговая ошибка', loss) print('Нейросеть успешно обучена') |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Эпоха: 0 Ошибка: 6.215969648082527 ----------------------- Эпоха: 100 Ошибка: 0.9316990279468341 ----------------------- Эпоха: 200 Ошибка: 0.7055264357349225 ----------------------- Эпоха: 300 Ошибка: 0.5950572560699322 ----------------------- Эпоха: 400 Ошибка: 0.5276765625306427 ----------------------- Итоговая ошибка 0.4813698685793663 Нейросеть успешно обучена |
Прогноз и оценка качества
Сделаем прогноз и оценим качество на обучающей выборке.
1 2 3 4 5 6 |
Z1 = np.matmul(W1, A1) + b1 A2 = sigmoid(Z1) Z2 = np.matmul(W2, A2) + b2 A3 = sigmoid(Z2) Z3 = np.matmul(W3, A3) + b3 A4 = softmax(Z3) |
1 2 |
y_pred = np.argmax(A4, axis = 0) y_pred[:4] |
1 |
array([5, 0, 4, 1]) |
1 2 |
from sklearn.metrics import accuracy_score accuracy_score(y_train, y_pred) |
1 |
0.8484333333333334 |
Теперь на тестовых данных.
1 2 3 4 5 6 7 8 9 10 11 12 |
A1 = X_test Z1 = np.matmul(W1, A1) + b1 A2 = sigmoid(Z1) Z2 = np.matmul(W2, A2) + b2 A3 = sigmoid(Z2) Z3 = np.matmul(W3, A3) + b3 A4 = softmax(Z3) y_pred = np.argmax(A4, axis = 0) accuracy_score(y_test, y_pred) |
1 |
0.8443 |
Алгоритм показал достаточно высокую точность и при увеличении количества эпох и настройке скорости обучения мог бы показать более высокий результат.
При этом очевидно, что модель, которую мы создали в рамках вводного курса в библиотеке Keras обучилась гораздо быстрее. На занятии по градиентному спуску мы посмотрим, как можно ускорить работу нашего алгоритма.
Подведем итог
На сегодняшнем занятии мы в деталях посмотрели на математику обратного распространения, а также с нуля построили несколько алгоритмов нейронных сетей.
На следующем занятии мы вернемся к теме качества алгоритмов, а также рассмотрим один из инструментов повышения этого качества, который называется регуляризацией.
Ответы на вопросы
Вопрос. Чем отличается умножение матриц от векторизации?
Ответ. Принцип умножения матриц относится к математике и описывает правила, по которым мы умножаем два двумерных тензора. Векторизация кода — термин из программирования, который позволяет избежать использования циклов в процессе выполнения кода.